Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dcca8bd83d | |||
|
|
fd0e06b71e | ||
| 3ce708b86a | |||
| 790a1db788 | |||
| fb095db7a6 | |||
| f898da653b | |||
| 19ef13f068 | |||
| 7ba08090fe | |||
| d57940d1b3 | |||
| c11589079b | |||
| 943203eeaf | |||
| 92e5003989 | |||
| a08c41067e | |||
| 8dd73568ce | |||
| f61a77af31 | |||
| 0be502cc2e | |||
| d9bfdb655d | |||
| e0843a11a6 | |||
| 95a79892c6 | |||
| 76b64886da | |||
| 92dfea7329 | |||
| 27f08e92e5 | |||
| b4fe5edfbb | |||
| 2cd326d0b1 | |||
| ecca5241bc | |||
| 8ddc8db536 | |||
| 35414cdfcb | |||
| cabe83aa98 | |||
| de0a153bce | |||
| 2777ba34ca | |||
| f3ecb8f3dd | |||
| 7d6080edbf | |||
| cd2c2f8167 | |||
| 930663d495 | |||
| 4e69b8ef77 | |||
| fc6d8950d4 | |||
| 1b24e7cda1 | |||
| 1621defe34 | |||
| 82a483d7ab | |||
| 69149b73bd | |||
| 8bff0f503f | |||
| 25f39ac41d | |||
| 4a3be94fef | |||
| 7f75a36d73 | |||
| ee5955fc75 | |||
| 8e831fd993 | |||
| 20018f09a9 | |||
| b1e8af7ec3 | |||
| 05a94c58f9 | |||
| eef6c68426 | |||
| b704e266d6 | |||
| 018571c950 | |||
| 1dd8a979ee | |||
| 70740e31e6 | |||
| 4980ca8f9f | |||
| 692e1da4b0 | |||
| 3c7a28ceb2 | |||
| 10da530e9e | |||
| f81235c68d | |||
| d7aedad9b4 | |||
| d176de6cb1 | |||
| 63abd5697f | |||
| 3f2275179b | |||
| de3150b74b | |||
| 6df912f428 | |||
| 74b152969b | |||
| e3e90d4779 | |||
| 1828191317 | |||
| de09cf4ebd | |||
| 14518978d2 | |||
| 29f1ff2d54 | |||
| cc2f790c62 | |||
| 0e6b235c1b | |||
| b09ef8a997 | |||
| c484a3c303 | |||
| b100967fda | |||
| 6ed9ee1109 | |||
| 31e36b2abc | |||
| 94e9a67f6e | |||
| d3ab472531 | |||
|
|
f02536ca49 |
1
.envrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
||||||
|
|
@ -5,74 +5,110 @@ on:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- flake.nix
|
- flake.nix
|
||||||
|
|
||||||
|
env:
|
||||||
|
NIX_CONFIG: experimental-features = nix-command flakes
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-web:
|
build-web:
|
||||||
runs-on: docker
|
runs-on: nixos
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install node
|
||||||
|
run: nix profile add nixpkgs#nodejs_24
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: https://data.forgejo.org/actions/checkout@v4
|
uses: https://data.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Packages
|
|
||||||
run: apt-get update && apt-get install -y sudo curl jq fakeroot dpkg rpm
|
|
||||||
|
|
||||||
- name: Install Nix
|
|
||||||
uses: https://github.com/cachix/install-nix-action@v30
|
|
||||||
|
|
||||||
- name: Install Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: nix develop .#electron --command pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Copy licenses
|
- name: Copy licenses
|
||||||
run: bun run copy-licenses
|
run: nix develop .#electron --command pnpm run copy-licenses
|
||||||
|
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: bun run build:packages
|
run: nix develop .#electron --command pnpm run build:packages
|
||||||
|
|
||||||
- name: Build web
|
- name: Build web
|
||||||
run: bun run build:web
|
run: nix develop .#electron --command pnpm run build:web
|
||||||
|
|
||||||
- name: Install rsync
|
|
||||||
run: apt-get update && apt-get install -y rsync
|
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
run: rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-dev/
|
run: nix develop .#electron --command rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-dev/
|
||||||
|
|
||||||
build-mobile:
|
build-mobile:
|
||||||
runs-on: docker
|
runs-on: nixos
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install node
|
||||||
|
run: nix profile add nixpkgs#nodejs_24
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: https://data.forgejo.org/actions/checkout@v4
|
uses: https://data.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Packages
|
|
||||||
run: apt-get update && apt-get install -y sudo curl jq fakeroot dpkg rpm
|
|
||||||
|
|
||||||
- name: Install Nix
|
|
||||||
uses: https://github.com/cachix/install-nix-action@v30
|
|
||||||
|
|
||||||
- name: Install Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: nix develop .#tauri --command pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Copy licenses
|
- name: Copy licenses
|
||||||
run: bun run copy-licenses
|
run: nix develop .#tauri --command pnpm run copy-licenses
|
||||||
|
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: bun run build:packages
|
run: nix develop .#tauri --command pnpm run build:packages
|
||||||
|
|
||||||
- name: Setup Android Keystore
|
- name: Setup Android Keystore
|
||||||
env:
|
env:
|
||||||
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
|
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
|
||||||
run: |
|
run: |
|
||||||
bun -e "require('fs').writeFileSync('keystore.jks', Buffer.from(process.env.KEYSTORE_BASE64.replace(/\s+/g, ''), 'base64'))"
|
nix profile add nixpkgs#gnused
|
||||||
bun -e "const content = process.env.KEYSTORE_PROPERTIES.replace(/\\n/g, '\n').replace(/\r/g, '').split('\n').map(l => l.trim()).filter(l => l).join('\n'); require('fs').writeFileSync('keystore.properties', content)"
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ -z "$KEYSTORE_BASE64" ]; then
|
||||||
|
echo "ANDROID_KEYSTORE_BASE64 secret is missing or empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$KEYSTORE_PROPERTIES" ]; then
|
||||||
|
echo "ANDROID_KEYSTORE_PROPERTIES secret is missing or empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$KEYSTORE_BASE64" \
|
||||||
|
| tr -d '[:space:]' \
|
||||||
|
| base64 -d > keystore.jks
|
||||||
|
|
||||||
|
printf '%s' "$KEYSTORE_PROPERTIES" \
|
||||||
|
| sed 's/\\n/\n/g' \
|
||||||
|
| tr -d '\r' \
|
||||||
|
| sed 's|^[[:space:]]*storeFile[[:space:]]*=.*|storeFile=keystore.jks|' \
|
||||||
|
> keystore.properties
|
||||||
|
|
||||||
|
grep -q '^[[:space:]]*storeFile[[:space:]]*=' keystore.properties || printf '\nstoreFile=keystore.jks\n' >> keystore.properties
|
||||||
|
|
||||||
|
if [ ! -s keystore.jks ]; then
|
||||||
|
echo "Decoded keystore.jks is missing or empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -s keystore.properties ]; then
|
||||||
|
echo "Generated keystore.properties is missing or empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q '^[[:space:]]*keyAlias[[:space:]]*=' keystore.properties; then
|
||||||
|
echo "keystore.properties is missing keyAlias"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -Eq '^[[:space:]]*(keyPassword|password)[[:space:]]*=' keystore.properties; then
|
||||||
|
echo "keystore.properties is missing keyPassword or password"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -Eq '^[[:space:]]*(storePassword|password)[[:space:]]*=' keystore.properties; then
|
||||||
|
echo "keystore.properties is missing storePassword or password"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build mobile
|
- name: Build mobile
|
||||||
run: bun run build:mobile
|
run: nix develop .#tauri --command pnpm run build:mobile
|
||||||
|
|
||||||
- name: Upload mobile artifact
|
- name: Upload mobile artifact
|
||||||
uses: https://data.forgejo.org/actions/upload-artifact@v3
|
uses: https://data.forgejo.org/actions/upload-artifact@v3
|
||||||
|
|
@ -81,31 +117,30 @@ jobs:
|
||||||
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
|
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
|
||||||
|
|
||||||
build-desktop:
|
build-desktop:
|
||||||
runs-on: docker
|
runs-on: nixos
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
target: [linux]
|
target: [linux]
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install node
|
||||||
|
run: nix profile add nixpkgs#nodejs_24
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: https://data.forgejo.org/actions/checkout@v4
|
uses: https://data.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Packages
|
|
||||||
run: apt-get update && apt-get install -y sudo curl jq fakeroot dpkg rpm xz-utils
|
|
||||||
|
|
||||||
- name: Install Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: nix develop .#electron --command pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Copy licenses
|
- name: Copy licenses
|
||||||
run: bun run copy-licenses
|
run: nix develop .#electron --command pnpm run copy-licenses
|
||||||
|
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: bun run build:packages
|
run: nix develop .#electron --command pnpm run build:packages
|
||||||
|
|
||||||
- name: Set Electron dev version
|
- name: Set Electron dev version
|
||||||
run: |
|
run: |
|
||||||
|
nix develop .#electron --command bash <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
VERSION="$(node -p "require('./package.json').version")"
|
VERSION="$(node -p "require('./package.json').version")"
|
||||||
SHORT_SHA="$(git rev-parse --short HEAD)"
|
SHORT_SHA="$(git rev-parse --short HEAD)"
|
||||||
DEV_VERSION="$VERSION-dev-$SHORT_SHA"
|
DEV_VERSION="$VERSION-dev-$SHORT_SHA"
|
||||||
|
|
@ -117,9 +152,15 @@ jobs:
|
||||||
pkg.version = process.env.DEV_VERSION;
|
pkg.version = process.env.DEV_VERSION;
|
||||||
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
|
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
|
||||||
'
|
'
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Build Electron desktop
|
- name: Build Electron desktop
|
||||||
run: bun run build:desktop
|
run: |
|
||||||
|
nix develop .#electron --command bash <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
cd apps/electron
|
||||||
|
pnpm run package:raw
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Upload desktop artifacts
|
- name: Upload desktop artifacts
|
||||||
uses: https://data.forgejo.org/actions/upload-artifact@v3
|
uses: https://data.forgejo.org/actions/upload-artifact@v3
|
||||||
|
|
@ -128,22 +169,19 @@ jobs:
|
||||||
path: apps/electron/release/
|
path: apps/electron/release/
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: docker
|
runs-on: nixos
|
||||||
needs: [build-web, build-mobile, build-desktop]
|
needs: [build-web, build-mobile, build-desktop]
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install node
|
||||||
|
run: nix profile add nixpkgs#nodejs_24
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: https://data.forgejo.org/actions/checkout@v4
|
uses: https://data.forgejo.org/actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install Packages
|
|
||||||
run: apt-get update && apt-get install -y sudo curl jq
|
|
||||||
|
|
||||||
- name: Install Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: nix develop .#electron --command pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Download mobile artifact
|
- name: Download mobile artifact
|
||||||
uses: https://data.forgejo.org/actions/download-artifact@v3
|
uses: https://data.forgejo.org/actions/download-artifact@v3
|
||||||
|
|
@ -160,21 +198,27 @@ jobs:
|
||||||
- name: Read version and hash
|
- name: Read version and hash
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
|
nix develop .#electron --command bash <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
VERSION="$(node -p "require('./package.json').version")"
|
VERSION="$(node -p "require('./package.json').version")"
|
||||||
SHORT_SHA="$(git rev-parse --short HEAD)"
|
SHORT_SHA="$(git rev-parse --short HEAD)"
|
||||||
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
|
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
|
||||||
echo "short_sha=$SHORT_SHA" >> "$FORGEJO_OUTPUT"
|
echo "short_sha=$SHORT_SHA" >> "$FORGEJO_OUTPUT"
|
||||||
echo "tag=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
|
echo "tag=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
|
||||||
echo "title=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
|
echo "title=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Copy releases
|
- name: Copy releases
|
||||||
env:
|
env:
|
||||||
TENSAMIN_RELEASE_VERSION: ${{ steps.version.outputs.tag }}
|
TENSAMIN_RELEASE_VERSION: ${{ steps.version.outputs.tag }}
|
||||||
TENSAMIN_RELEASE_TAG: ${{ steps.version.outputs.tag }}
|
TENSAMIN_RELEASE_TAG: ${{ steps.version.outputs.tag }}
|
||||||
run: |
|
run: |
|
||||||
|
nix develop .#electron --command bash <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
ASSET_BASE_URL="${{ forgejo.api_url }}"
|
ASSET_BASE_URL="${{ forgejo.api_url }}"
|
||||||
ASSET_BASE_URL="${ASSET_BASE_URL%/api/v1}/${{ forgejo.repository }}/releases/download/${{ steps.version.outputs.tag }}"
|
ASSET_BASE_URL="${ASSET_BASE_URL%/api/v1}/${{ forgejo.repository }}/releases/download/${{ steps.version.outputs.tag }}"
|
||||||
FORGEJO_RELEASE_ASSET_BASE_URL="$ASSET_BASE_URL" bun --bun run copy-releases
|
FORGEJO_RELEASE_ASSET_BASE_URL="$ASSET_BASE_URL" pnpm run copy-releases
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Create pre-release and upload files
|
- name: Create pre-release and upload files
|
||||||
env:
|
env:
|
||||||
|
|
@ -185,6 +229,7 @@ jobs:
|
||||||
TAG: ${{ steps.version.outputs.tag }}
|
TAG: ${{ steps.version.outputs.tag }}
|
||||||
TITLE: ${{ steps.version.outputs.title }}
|
TITLE: ${{ steps.version.outputs.title }}
|
||||||
run: |
|
run: |
|
||||||
|
nix develop .#electron --command bash <<'EOF'
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
test -d releases
|
test -d releases
|
||||||
|
|
@ -273,3 +318,4 @@ jobs:
|
||||||
-H "Authorization: token $TOKEN" \
|
-H "Authorization: token $TOKEN" \
|
||||||
-F "attachment=@$file"
|
-F "attachment=@$file"
|
||||||
done
|
done
|
||||||
|
EOF
|
||||||
|
|
|
||||||
|
|
@ -6,74 +6,110 @@ on:
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- flake.nix
|
- flake.nix
|
||||||
|
|
||||||
|
env:
|
||||||
|
NIX_CONFIG: experimental-features = nix-command flakes
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-web:
|
build-web:
|
||||||
runs-on: docker
|
runs-on: nixos
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install node
|
||||||
|
run: nix profile add nixpkgs#nodejs_24
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: https://data.forgejo.org/actions/checkout@v4
|
uses: https://data.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Packages
|
|
||||||
run: apt-get update && apt-get install -y sudo curl jq fakeroot dpkg rpm
|
|
||||||
|
|
||||||
- name: Install Nix
|
|
||||||
uses: https://github.com/cachix/install-nix-action@v30
|
|
||||||
|
|
||||||
- name: Install Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: nix develop .#electron --command pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Copy licenses
|
- name: Copy licenses
|
||||||
run: bun run copy-licenses
|
run: nix develop .#electron --command pnpm run copy-licenses
|
||||||
|
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: bun run build:packages
|
run: nix develop .#electron --command pnpm run build:packages
|
||||||
|
|
||||||
- name: Build web
|
- name: Build web
|
||||||
run: bun run build:web
|
run: nix develop .#electron --command pnpm run build:web
|
||||||
|
|
||||||
- name: Install rsync
|
|
||||||
run: apt-get update && apt-get install -y rsync
|
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
run: rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-prod/
|
run: nix develop .#electron --command rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-prod/
|
||||||
|
|
||||||
build-mobile:
|
build-mobile:
|
||||||
runs-on: docker
|
runs-on: nixos
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install node
|
||||||
|
run: nix profile add nixpkgs#nodejs_24
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: https://data.forgejo.org/actions/checkout@v4
|
uses: https://data.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Packages
|
|
||||||
run: apt-get update && apt-get install -y sudo curl jq fakeroot dpkg rpm
|
|
||||||
|
|
||||||
- name: Install Nix
|
|
||||||
uses: https://github.com/cachix/install-nix-action@v30
|
|
||||||
|
|
||||||
- name: Install Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: nix develop .#tauri --command pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Copy licenses
|
- name: Copy licenses
|
||||||
run: bun run copy-licenses
|
run: nix develop .#tauri --command pnpm run copy-licenses
|
||||||
|
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: bun run build:packages
|
run: nix develop .#tauri --command pnpm run build:packages
|
||||||
|
|
||||||
- name: Setup Android Keystore
|
- name: Setup Android Keystore
|
||||||
env:
|
env:
|
||||||
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
|
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
|
||||||
run: |
|
run: |
|
||||||
bun -e "require('fs').writeFileSync('keystore.jks', Buffer.from(process.env.KEYSTORE_BASE64.replace(/\s+/g, ''), 'base64'))"
|
nix profile add nixpkgs#gnused
|
||||||
bun -e "const content = process.env.KEYSTORE_PROPERTIES.replace(/\\n/g, '\n').replace(/\r/g, '').split('\n').map(l => l.trim()).filter(l => l).join('\n'); require('fs').writeFileSync('keystore.properties', content)"
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [ -z "$KEYSTORE_BASE64" ]; then
|
||||||
|
echo "ANDROID_KEYSTORE_BASE64 secret is missing or empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$KEYSTORE_PROPERTIES" ]; then
|
||||||
|
echo "ANDROID_KEYSTORE_PROPERTIES secret is missing or empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$KEYSTORE_BASE64" \
|
||||||
|
| tr -d '[:space:]' \
|
||||||
|
| base64 -d > keystore.jks
|
||||||
|
|
||||||
|
printf '%s' "$KEYSTORE_PROPERTIES" \
|
||||||
|
| sed 's/\\n/\n/g' \
|
||||||
|
| tr -d '\r' \
|
||||||
|
| sed 's|^[[:space:]]*storeFile[[:space:]]*=.*|storeFile=keystore.jks|' \
|
||||||
|
> keystore.properties
|
||||||
|
|
||||||
|
grep -q '^[[:space:]]*storeFile[[:space:]]*=' keystore.properties || printf '\nstoreFile=keystore.jks\n' >> keystore.properties
|
||||||
|
|
||||||
|
if [ ! -s keystore.jks ]; then
|
||||||
|
echo "Decoded keystore.jks is missing or empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -s keystore.properties ]; then
|
||||||
|
echo "Generated keystore.properties is missing or empty"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q '^[[:space:]]*keyAlias[[:space:]]*=' keystore.properties; then
|
||||||
|
echo "keystore.properties is missing keyAlias"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -Eq '^[[:space:]]*(keyPassword|password)[[:space:]]*=' keystore.properties; then
|
||||||
|
echo "keystore.properties is missing keyPassword or password"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -Eq '^[[:space:]]*(storePassword|password)[[:space:]]*=' keystore.properties; then
|
||||||
|
echo "keystore.properties is missing storePassword or password"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build mobile
|
- name: Build mobile
|
||||||
run: bun run build:mobile
|
run: nix develop .#tauri --command pnpm run build:mobile
|
||||||
|
|
||||||
- name: Upload mobile artifact
|
- name: Upload mobile artifact
|
||||||
uses: https://data.forgejo.org/actions/upload-artifact@v3
|
uses: https://data.forgejo.org/actions/upload-artifact@v3
|
||||||
|
|
@ -82,31 +118,30 @@ jobs:
|
||||||
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
|
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
|
||||||
|
|
||||||
build-desktop:
|
build-desktop:
|
||||||
runs-on: docker
|
runs-on: nixos
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
target: [linux]
|
target: [linux]
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install node
|
||||||
|
run: nix profile add nixpkgs#nodejs_24
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: https://data.forgejo.org/actions/checkout@v4
|
uses: https://data.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Packages
|
|
||||||
run: apt-get update && apt-get install -y sudo curl jq fakeroot dpkg rpm xz-utils
|
|
||||||
|
|
||||||
- name: Install Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: nix develop .#electron --command pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Copy licenses
|
- name: Copy licenses
|
||||||
run: bun run copy-licenses
|
run: nix develop .#electron --command pnpm run copy-licenses
|
||||||
|
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: bun run build:packages
|
run: nix develop .#electron --command pnpm run build:packages
|
||||||
|
|
||||||
- name: Set Electron prod version
|
- name: Set Electron prod version
|
||||||
run: |
|
run: |
|
||||||
|
nix develop .#electron --command bash <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
VERSION="$(node -p "require('./package.json').version")"
|
VERSION="$(node -p "require('./package.json').version")"
|
||||||
export VERSION
|
export VERSION
|
||||||
node -e '
|
node -e '
|
||||||
|
|
@ -116,9 +151,15 @@ jobs:
|
||||||
pkg.version = process.env.VERSION;
|
pkg.version = process.env.VERSION;
|
||||||
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
|
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
|
||||||
'
|
'
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Build Electron desktop
|
- name: Build Electron desktop
|
||||||
run: bun run build:desktop
|
run: |
|
||||||
|
nix develop .#electron --command bash <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
cd apps/electron
|
||||||
|
pnpm run package:raw
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Upload desktop artifacts
|
- name: Upload desktop artifacts
|
||||||
uses: https://data.forgejo.org/actions/upload-artifact@v3
|
uses: https://data.forgejo.org/actions/upload-artifact@v3
|
||||||
|
|
@ -127,20 +168,17 @@ jobs:
|
||||||
path: apps/electron/release/
|
path: apps/electron/release/
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: docker
|
runs-on: nixos
|
||||||
needs: [build-web, build-mobile, build-desktop]
|
needs: [build-web, build-mobile, build-desktop]
|
||||||
steps:
|
steps:
|
||||||
|
- name: Install node
|
||||||
|
run: nix profile add nixpkgs#nodejs_24
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: https://data.forgejo.org/actions/checkout@v4
|
uses: https://data.forgejo.org/actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Packages
|
|
||||||
run: apt-get update && apt-get install -y sudo curl jq
|
|
||||||
|
|
||||||
- name: Install Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: nix develop .#electron --command pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Download mobile artifact
|
- name: Download mobile artifact
|
||||||
uses: https://data.forgejo.org/actions/download-artifact@v3
|
uses: https://data.forgejo.org/actions/download-artifact@v3
|
||||||
|
|
@ -157,18 +195,24 @@ jobs:
|
||||||
- name: Read version
|
- name: Read version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
|
nix develop .#electron --command bash <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
VERSION="$(node -p "require('./package.json').version")"
|
VERSION="$(node -p "require('./package.json').version")"
|
||||||
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
|
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
|
||||||
echo "tag=$VERSION" >> "$FORGEJO_OUTPUT"
|
echo "tag=$VERSION" >> "$FORGEJO_OUTPUT"
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Copy releases
|
- name: Copy releases
|
||||||
env:
|
env:
|
||||||
TENSAMIN_RELEASE_VERSION: ${{ steps.version.outputs.tag }}
|
TENSAMIN_RELEASE_VERSION: ${{ steps.version.outputs.tag }}
|
||||||
TENSAMIN_RELEASE_TAG: ${{ steps.version.outputs.tag }}
|
TENSAMIN_RELEASE_TAG: ${{ steps.version.outputs.tag }}
|
||||||
run: |
|
run: |
|
||||||
|
nix develop .#electron --command bash <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
ASSET_BASE_URL="${{ forgejo.api_url }}"
|
ASSET_BASE_URL="${{ forgejo.api_url }}"
|
||||||
ASSET_BASE_URL="${ASSET_BASE_URL%/api/v1}/${{ forgejo.repository }}/releases/download/${{ steps.version.outputs.tag }}"
|
ASSET_BASE_URL="${ASSET_BASE_URL%/api/v1}/${{ forgejo.repository }}/releases/download/${{ steps.version.outputs.tag }}"
|
||||||
FORGEJO_RELEASE_ASSET_BASE_URL="$ASSET_BASE_URL" bun --bun run copy-releases
|
FORGEJO_RELEASE_ASSET_BASE_URL="$ASSET_BASE_URL" pnpm run copy-releases
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Create release and upload files
|
- name: Create release and upload files
|
||||||
env:
|
env:
|
||||||
|
|
@ -178,6 +222,7 @@ jobs:
|
||||||
SHA: ${{ forgejo.sha }}
|
SHA: ${{ forgejo.sha }}
|
||||||
TAG: ${{ steps.version.outputs.tag }}
|
TAG: ${{ steps.version.outputs.tag }}
|
||||||
run: |
|
run: |
|
||||||
|
nix develop .#electron --command bash <<'EOF'
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
test -d releases
|
test -d releases
|
||||||
|
|
@ -231,18 +276,18 @@ jobs:
|
||||||
-H "Authorization: token $TOKEN" \
|
-H "Authorization: token $TOKEN" \
|
||||||
-F "attachment=@$file"
|
-F "attachment=@$file"
|
||||||
done
|
done
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Delete dev releases for prod version
|
- name: Delete dev releases
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ forgejo.token }}
|
TOKEN: ${{ forgejo.token }}
|
||||||
API: ${{ forgejo.api_url }}
|
API: ${{ forgejo.api_url }}
|
||||||
REPO: ${{ forgejo.repository }}
|
REPO: ${{ forgejo.repository }}
|
||||||
TAG: ${{ steps.version.outputs.tag }}
|
|
||||||
run: |
|
run: |
|
||||||
|
nix develop .#electron --command bash <<'EOF'
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
PAGE=1
|
PAGE=1
|
||||||
PREFIX="$TAG-dev-"
|
|
||||||
DELETE_RELEASES=delete-dev-releases.tsv
|
DELETE_RELEASES=delete-dev-releases.tsv
|
||||||
|
|
||||||
: > "$DELETE_RELEASES"
|
: > "$DELETE_RELEASES"
|
||||||
|
|
@ -257,8 +302,7 @@ jobs:
|
||||||
test "$COUNT" -gt 0 || break
|
test "$COUNT" -gt 0 || break
|
||||||
|
|
||||||
jq -r \
|
jq -r \
|
||||||
--arg prefix "$PREFIX" \
|
'.[] | select(.prerelease == true) | select(.tag_name | contains("-dev-")) | [.id, .tag_name] | @tsv' releases.json \
|
||||||
'.[] | select(.prerelease == true) | select(.tag_name | startswith($prefix)) | [.id, .tag_name] | @tsv' releases.json \
|
|
||||||
>> "$DELETE_RELEASES"
|
>> "$DELETE_RELEASES"
|
||||||
|
|
||||||
PAGE="$((PAGE + 1))"
|
PAGE="$((PAGE + 1))"
|
||||||
|
|
@ -271,11 +315,13 @@ jobs:
|
||||||
-H "Authorization: token $TOKEN" \
|
-H "Authorization: token $TOKEN" \
|
||||||
"$API/repos/$REPO/releases/$release_id"
|
"$API/repos/$REPO/releases/$release_id"
|
||||||
done < "$DELETE_RELEASES"
|
done < "$DELETE_RELEASES"
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Update root flake release hash
|
- name: Update root flake release hash
|
||||||
env:
|
env:
|
||||||
TAG: ${{ steps.version.outputs.tag }}
|
TAG: ${{ steps.version.outputs.tag }}
|
||||||
run: |
|
run: |
|
||||||
|
nix develop .#electron --command bash <<'EOF'
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
DEB="$(find releases -maxdepth 1 -type f -name 'Tensamin-*-linux-amd64.deb' -print -quit)"
|
DEB="$(find releases -maxdepth 1 -type f -name 'Tensamin-*-linux-amd64.deb' -print -quit)"
|
||||||
|
|
@ -324,3 +370,4 @@ jobs:
|
||||||
git add flake.nix
|
git add flake.nix
|
||||||
git -c user.name="forgejo-actions" -c user.email="forgejo-actions@localhost" commit -m "(qol): update release flake hash"
|
git -c user.name="forgejo-actions" -c user.email="forgejo-actions@localhost" commit -m "(qol): update release flake hash"
|
||||||
git push origin HEAD:dev
|
git push origin HEAD:dev
|
||||||
|
EOF
|
||||||
|
|
|
||||||
3
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
node_modules
|
node_modules
|
||||||
releases
|
releases
|
||||||
.fallow/
|
.fallow
|
||||||
|
.direnv
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 41 KiB |
61
apps/electron/flake.lock
generated
|
|
@ -1,61 +0,0 @@
|
||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731533236,
|
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1779508470,
|
|
||||||
"narHash": "sha256-Ap9KJX+5xHIn3bPIpfNgT6MEXdAECECwo4/rmlQD74M=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "29916453413845e54a65b8a1cf996842300cd299",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
{
|
|
||||||
description = "Electron Development Environment";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = {nixpkgs, flake-utils, ...}:
|
|
||||||
flake-utils.lib.eachDefaultSystem (system: let
|
|
||||||
pkgs = import nixpkgs {inherit system; config.allowUnfree = true;};
|
|
||||||
electronRuntimeLibs = with pkgs; [
|
|
||||||
alsa-lib
|
|
||||||
at-spi2-atk
|
|
||||||
at-spi2-core
|
|
||||||
atk
|
|
||||||
cairo
|
|
||||||
cups
|
|
||||||
dbus
|
|
||||||
expat
|
|
||||||
fontconfig
|
|
||||||
freetype
|
|
||||||
gdk-pixbuf
|
|
||||||
glib
|
|
||||||
gtk3
|
|
||||||
libdrm
|
|
||||||
libgbm
|
|
||||||
libglvnd
|
|
||||||
libnotify
|
|
||||||
libpulseaudio
|
|
||||||
libuuid
|
|
||||||
libxkbcommon
|
|
||||||
mesa
|
|
||||||
nspr
|
|
||||||
nss
|
|
||||||
pango
|
|
||||||
pipewire
|
|
||||||
systemd
|
|
||||||
wayland
|
|
||||||
# xorg
|
|
||||||
libX11
|
|
||||||
libXScrnSaver
|
|
||||||
libXcomposite
|
|
||||||
libXcursor
|
|
||||||
libXdamage
|
|
||||||
libXext
|
|
||||||
libXfixes
|
|
||||||
libXi
|
|
||||||
libXrandr
|
|
||||||
libXtst
|
|
||||||
libxcb
|
|
||||||
];
|
|
||||||
in {
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
packages = with pkgs; [
|
|
||||||
nodejs_22
|
|
||||||
corepack_22
|
|
||||||
bun
|
|
||||||
electron
|
|
||||||
pkg-config
|
|
||||||
python3
|
|
||||||
gcc
|
|
||||||
gnumake
|
|
||||||
git
|
|
||||||
jq
|
|
||||||
patchelf
|
|
||||||
dpkg
|
|
||||||
rpm
|
|
||||||
fpm
|
|
||||||
] ++ electronRuntimeLibs;
|
|
||||||
|
|
||||||
shellHook = ''
|
|
||||||
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath electronRuntimeLibs}:$LD_LIBRARY_PATH"
|
|
||||||
export ELECTRON_ENABLE_LOGGING=1
|
|
||||||
export ELECTRON_OZONE_PLATFORM_HINT="''${ELECTRON_OZONE_PLATFORM_HINT:-auto}"
|
|
||||||
export NPM_CONFIG_TARGET_ARCH="''${NPM_CONFIG_TARGET_ARCH:-x64}"
|
|
||||||
export npm_config_build_from_source=true
|
|
||||||
export USE_SYSTEM_FPM=true
|
|
||||||
|
|
||||||
alias electron-install='cd ../.. && bun install'
|
|
||||||
alias electron-build-web='cd ../.. && bun run build:web'
|
|
||||||
alias electron-dev='bun run dev'
|
|
||||||
alias electron-package='bun run package:linux'
|
|
||||||
alias electron-validate='bun run validate'
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -5,27 +5,28 @@
|
||||||
"description": "Tensamin desktop client",
|
"description": "Tensamin desktop client",
|
||||||
"author": "methanium",
|
"author": "methanium",
|
||||||
"homepage": "https://git.methanium.net/tensamin/client",
|
"homepage": "https://git.methanium.net/tensamin/client",
|
||||||
|
"desktopName": "Tensamin",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/main/main.js",
|
"main": "dist/main/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf dist release",
|
"clean": "rm -rf dist release",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"build:web": "cd ../.. && bun run build:web",
|
"build:web": "cd ../.. && pnpm run build:web",
|
||||||
"build": "tsc -p tsconfig.json && esbuild src/preload/preload.ts --bundle --platform=node --format=cjs --external:electron --outfile=dist/preload/preload.cjs",
|
"build": "tsc -p tsconfig.json && esbuild src/preload/preload.ts --bundle --platform=node --format=cjs --external:electron --outfile=dist/preload/preload.cjs",
|
||||||
"dev:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron . --verbose",
|
"dev:raw": "cd ../.. && pnpm run build:web && cd apps/electron && pnpm run build && electron . --verbose",
|
||||||
"dev": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run dev:raw; else bun run dev:raw; fi",
|
"dev": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run dev:raw; else pnpm run dev:raw; fi",
|
||||||
"start:raw": "bun run build && electron .",
|
"start:raw": "pnpm run build && electron .",
|
||||||
"start": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run start:raw; else bun run start:raw; fi",
|
"start": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run start:raw; else pnpm run start:raw; fi",
|
||||||
"package:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron-builder --publish never",
|
"package:raw": "cd ../.. && pnpm run build:web && cd apps/electron && pnpm run build && electron-builder --publish never",
|
||||||
"package": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run package:raw; else bun run package:raw; fi",
|
"package": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run package:raw; else pnpm run package:raw; fi",
|
||||||
"package:linux:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron-builder --linux --publish never",
|
"package:linux:raw": "cd ../.. && pnpm run build:web && cd apps/electron && pnpm run build && electron-builder --linux --publish never",
|
||||||
"package:linux": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run package:linux:raw; else bun run package:linux:raw; fi",
|
"package:linux": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run package:linux:raw; else pnpm run package:linux:raw; fi",
|
||||||
"package:windows:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron-builder --win --publish never",
|
"package:windows:raw": "cd ../.. && pnpm run build:web && cd apps/electron && pnpm run build && electron-builder --win --publish never",
|
||||||
"package:windows": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run package:windows:raw; else bun run package:windows:raw; fi",
|
"package:windows": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run package:windows:raw; else pnpm run package:windows:raw; fi",
|
||||||
"checksum": "bun scripts/generate-release-metadata.ts",
|
"checksum": "node scripts/generate-release-metadata.ts",
|
||||||
"generate-signing-key": "bun scripts/generate-signing-key.ts",
|
"generate-signing-key": "node scripts/generate-signing-key.ts",
|
||||||
"validate:raw": "bun run build && bun run package:linux:raw",
|
"validate:raw": "pnpm run build && pnpm run package:linux:raw",
|
||||||
"validate": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run validate:raw; else bun run validate:raw; fi"
|
"validate": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run validate:raw; else pnpm run validate:raw; fi"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -44,6 +45,9 @@
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "release"
|
"output": "release"
|
||||||
},
|
},
|
||||||
|
"toolsets": {
|
||||||
|
"appimage": "1.0.3"
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
"package.json"
|
"package.json"
|
||||||
|
|
@ -54,8 +58,12 @@
|
||||||
"to": "web"
|
"to": "web"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"from": "build/icons/icon.png",
|
"from": "build/icons",
|
||||||
"to": "icons/icon.png"
|
"to": "icons",
|
||||||
|
"filter": [
|
||||||
|
"32x32.png",
|
||||||
|
"icon.png"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
|
|
@ -68,6 +76,7 @@
|
||||||
"executableName": "tensamin",
|
"executableName": "tensamin",
|
||||||
"category": "Network",
|
"category": "Network",
|
||||||
"maintainer": "Methanium",
|
"maintainer": "Methanium",
|
||||||
|
"syncDesktopName": true,
|
||||||
"desktop": {
|
"desktop": {
|
||||||
"entry": {
|
"entry": {
|
||||||
"Name": "Tensamin",
|
"Name": "Tensamin",
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,18 @@ import {
|
||||||
import { checkForUpdates } from "./updates.js";
|
import { checkForUpdates } from "./updates.js";
|
||||||
import {
|
import {
|
||||||
ipcChannels,
|
ipcChannels,
|
||||||
|
type DesktopCallStatus,
|
||||||
type DesktopScreenShareAudioOutput,
|
type DesktopScreenShareAudioOutput,
|
||||||
type DesktopScreenShareCapabilities,
|
type DesktopScreenShareCapabilities,
|
||||||
} from "../shared/ipc.js";
|
} from "../shared/ipc.js";
|
||||||
|
import { initTray, setTrayCallStatus } from "./tray.js";
|
||||||
|
import {
|
||||||
|
clearSecureStorage,
|
||||||
|
deleteSecureStorage,
|
||||||
|
getSecureStorageStatus,
|
||||||
|
loadSecureStorage,
|
||||||
|
saveSecureStorage,
|
||||||
|
} from "./secureStorage.js";
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const verbose = process.argv.includes("--verbose");
|
const verbose = process.argv.includes("--verbose");
|
||||||
|
|
@ -188,6 +197,37 @@ function registerIpc() {
|
||||||
);
|
);
|
||||||
ipcMain.handle(ipcChannels.getVersion, () => app.getVersion());
|
ipcMain.handle(ipcChannels.getVersion, () => app.getVersion());
|
||||||
ipcMain.handle(ipcChannels.checkForUpdates, checkForUpdates);
|
ipcMain.handle(ipcChannels.checkForUpdates, checkForUpdates);
|
||||||
|
ipcMain.handle(ipcChannels.getSecureStorageStatus, getSecureStorageStatus);
|
||||||
|
ipcMain.handle(ipcChannels.loadSecureStorage, (_event, key: unknown) =>
|
||||||
|
loadSecureStorage(key),
|
||||||
|
);
|
||||||
|
ipcMain.handle(
|
||||||
|
ipcChannels.saveSecureStorage,
|
||||||
|
(_event, key: unknown, value: unknown) => saveSecureStorage(key, value),
|
||||||
|
);
|
||||||
|
ipcMain.handle(ipcChannels.deleteSecureStorage, (_event, key: unknown) =>
|
||||||
|
deleteSecureStorage(key),
|
||||||
|
);
|
||||||
|
ipcMain.handle(ipcChannels.clearSecureStorage, clearSecureStorage);
|
||||||
|
ipcMain.handle(ipcChannels.setCallStatus, (_event, status: unknown) => {
|
||||||
|
if (
|
||||||
|
typeof status !== "object" ||
|
||||||
|
status === null ||
|
||||||
|
typeof (status as DesktopCallStatus).inCall !== "boolean" ||
|
||||||
|
typeof (status as DesktopCallStatus).speaking !== "boolean" ||
|
||||||
|
((status as DesktopCallStatus).iconDataUrl !== undefined &&
|
||||||
|
(typeof (status as DesktopCallStatus).iconDataUrl !== "string" ||
|
||||||
|
!(status as DesktopCallStatus).iconDataUrl?.startsWith(
|
||||||
|
"data:image/png;base64,",
|
||||||
|
) ||
|
||||||
|
(status as DesktopCallStatus).iconDataUrl!.length > 16_384))
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid call status.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { inCall, iconDataUrl } = status as DesktopCallStatus;
|
||||||
|
setTrayCallStatus(inCall, iconDataUrl);
|
||||||
|
});
|
||||||
ipcMain.handle(ipcChannels.minimizeWindow, () => {
|
ipcMain.handle(ipcChannels.minimizeWindow, () => {
|
||||||
verboseLog("window:minimize");
|
verboseLog("window:minimize");
|
||||||
mainWindow?.minimize();
|
mainWindow?.minimize();
|
||||||
|
|
@ -310,6 +350,7 @@ async function start() {
|
||||||
verboseLog("app ready");
|
verboseLog("app ready");
|
||||||
registerIpc();
|
registerIpc();
|
||||||
registerDisplayMediaHandler();
|
registerDisplayMediaHandler();
|
||||||
|
initTray(() => mainWindow);
|
||||||
await createWindow();
|
await createWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
130
apps/electron/src/main/secureStorage.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { app, safeStorage } from "electron";
|
||||||
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import {
|
||||||
|
secureStorageLimits,
|
||||||
|
type DesktopSecureStorageStatus,
|
||||||
|
} from "../shared/ipc.js";
|
||||||
|
|
||||||
|
type StoredValues = Record<string, string>;
|
||||||
|
|
||||||
|
let pendingWrite = Promise.resolve();
|
||||||
|
|
||||||
|
function storagePath() {
|
||||||
|
return join(app.getPath("userData"), "secure-storage.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateKey(key: unknown): asserts key is string {
|
||||||
|
if (
|
||||||
|
typeof key !== "string" ||
|
||||||
|
key.length === 0 ||
|
||||||
|
Buffer.byteLength(key, "utf8") > secureStorageLimits.maxKeyBytes
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid secure storage key.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateValue(value: unknown): asserts value is string {
|
||||||
|
if (
|
||||||
|
typeof value !== "string" ||
|
||||||
|
Buffer.byteLength(value, "utf8") > secureStorageLimits.maxValueBytes
|
||||||
|
) {
|
||||||
|
throw new Error("Invalid secure storage value.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSecureStorageStatus(): DesktopSecureStorageStatus {
|
||||||
|
if (!safeStorage.isEncryptionAvailable()) {
|
||||||
|
return { available: false, backend: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const backend =
|
||||||
|
process.platform === "linux"
|
||||||
|
? safeStorage.getSelectedStorageBackend()
|
||||||
|
: process.platform === "darwin"
|
||||||
|
? "keychain"
|
||||||
|
: process.platform === "win32"
|
||||||
|
? "dpapi"
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
available: process.platform !== "linux" || backend !== "basic_text",
|
||||||
|
backend,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireAvailable() {
|
||||||
|
if (!getSecureStorageStatus().available) {
|
||||||
|
throw new Error("Secure storage is unavailable.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readValues(): Promise<StoredValues> {
|
||||||
|
try {
|
||||||
|
const parsed: unknown = JSON.parse(await readFile(storagePath(), "utf8"));
|
||||||
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
|
throw new Error("Invalid secure storage data.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = parsed as Record<string, unknown>;
|
||||||
|
if (Object.values(values).some((value) => typeof value !== "string")) {
|
||||||
|
throw new Error("Invalid secure storage data.");
|
||||||
|
}
|
||||||
|
return values as StoredValues;
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeValues(values: StoredValues) {
|
||||||
|
const path = storagePath();
|
||||||
|
const temporaryPath = `${path}.tmp`;
|
||||||
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
||||||
|
await writeFile(temporaryPath, JSON.stringify(values), { mode: 0o600 });
|
||||||
|
await rename(temporaryPath, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mutateValues(mutation: (values: StoredValues) => void) {
|
||||||
|
const operation = pendingWrite.then(async () => {
|
||||||
|
const values = await readValues();
|
||||||
|
mutation(values);
|
||||||
|
await writeValues(values);
|
||||||
|
});
|
||||||
|
pendingWrite = operation.catch(() => undefined);
|
||||||
|
return operation;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadSecureStorage(key: unknown): Promise<string | null> {
|
||||||
|
requireAvailable();
|
||||||
|
validateKey(key);
|
||||||
|
await pendingWrite;
|
||||||
|
const encrypted = (await readValues())[key];
|
||||||
|
if (encrypted === undefined) return null;
|
||||||
|
return safeStorage.decryptString(Buffer.from(encrypted, "base64"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveSecureStorage(key: unknown, value: unknown) {
|
||||||
|
requireAvailable();
|
||||||
|
validateKey(key);
|
||||||
|
validateValue(value);
|
||||||
|
const encrypted = safeStorage.encryptString(value).toString("base64");
|
||||||
|
return mutateValues((values) => {
|
||||||
|
values[key] = encrypted;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteSecureStorage(key: unknown) {
|
||||||
|
requireAvailable();
|
||||||
|
validateKey(key);
|
||||||
|
return mutateValues((values) => {
|
||||||
|
delete values[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSecureStorage() {
|
||||||
|
requireAvailable();
|
||||||
|
return mutateValues((values) => {
|
||||||
|
for (const key of Object.keys(values)) delete values[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
62
apps/electron/src/main/tray.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { app, BrowserWindow, Menu, nativeImage, Tray } from "electron";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
let tray: Tray | null = null;
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
function getTrayIconPath(filename: string) {
|
||||||
|
if (app.isPackaged) return path.join(process.resourcesPath, "icons", filename);
|
||||||
|
return path.resolve(__dirname, "../../build/icons", filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTrayCallStatus(
|
||||||
|
inCall: boolean,
|
||||||
|
iconDataUrl?: string,
|
||||||
|
) {
|
||||||
|
if (!tray) return;
|
||||||
|
|
||||||
|
if (inCall && iconDataUrl) {
|
||||||
|
const image = nativeImage.createFromDataURL(iconDataUrl);
|
||||||
|
if (!image.isEmpty()) {
|
||||||
|
tray.setImage(image);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tray.setImage(getTrayIconPath("32x32.png"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initTray(getMainWindow: () => BrowserWindow | null) {
|
||||||
|
tray = new Tray(getTrayIconPath("32x32.png")); // keep reference alive
|
||||||
|
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: "Restart",
|
||||||
|
type: "normal",
|
||||||
|
click: () => {
|
||||||
|
app.relaunch();
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ label: "Quit", type: "normal", click: () => app.quit() },
|
||||||
|
]);
|
||||||
|
|
||||||
|
tray.setContextMenu(contextMenu);
|
||||||
|
|
||||||
|
tray.on("click", () => {
|
||||||
|
const mainWindow = getMainWindow();
|
||||||
|
if (!mainWindow) return;
|
||||||
|
|
||||||
|
if (mainWindow.isVisible()) {
|
||||||
|
mainWindow.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||||
|
mainWindow.show();
|
||||||
|
mainWindow.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,23 @@
|
||||||
import { contextBridge, ipcRenderer } from "electron";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
import { ipcChannels, type DesktopScreenShareSource } from "../shared/ipc.js";
|
import {
|
||||||
|
ipcChannels,
|
||||||
|
type DesktopCallStatus,
|
||||||
|
type DesktopScreenShareSource,
|
||||||
|
secureStorageLimits,
|
||||||
|
} from "../shared/ipc.js";
|
||||||
|
|
||||||
function windowAction(channel: string) {
|
function windowAction(channel: string) {
|
||||||
return () => ipcRenderer.invoke(channel);
|
return () => ipcRenderer.invoke(channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validKey(key: string) {
|
||||||
|
return (
|
||||||
|
typeof key === "string" &&
|
||||||
|
key.length > 0 &&
|
||||||
|
Buffer.byteLength(key, "utf8") <= secureStorageLimits.maxKeyBytes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const desktopApi = {
|
const desktopApi = {
|
||||||
media: {
|
media: {
|
||||||
listScreenShareSources: () =>
|
listScreenShareSources: () =>
|
||||||
|
|
@ -27,6 +40,39 @@ const desktopApi = {
|
||||||
updates: {
|
updates: {
|
||||||
checkForUpdates: () => ipcRenderer.invoke(ipcChannels.checkForUpdates),
|
checkForUpdates: () => ipcRenderer.invoke(ipcChannels.checkForUpdates),
|
||||||
},
|
},
|
||||||
|
call: {
|
||||||
|
setStatus: (status: DesktopCallStatus) => {
|
||||||
|
if (
|
||||||
|
typeof status?.inCall !== "boolean" ||
|
||||||
|
typeof status?.speaking !== "boolean" ||
|
||||||
|
(status.iconDataUrl !== undefined &&
|
||||||
|
(typeof status.iconDataUrl !== "string" ||
|
||||||
|
!status.iconDataUrl.startsWith("data:image/png;base64,")))
|
||||||
|
) {
|
||||||
|
return Promise.reject(new Error("Invalid call status."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipcRenderer.invoke(ipcChannels.setCallStatus, status);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secureStorage: {
|
||||||
|
getStatus: () => ipcRenderer.invoke(ipcChannels.getSecureStorageStatus),
|
||||||
|
load: (key: string) =>
|
||||||
|
validKey(key)
|
||||||
|
? ipcRenderer.invoke(ipcChannels.loadSecureStorage, key)
|
||||||
|
: Promise.reject(new Error("Invalid secure storage key.")),
|
||||||
|
save: (key: string, value: string) =>
|
||||||
|
validKey(key) &&
|
||||||
|
typeof value === "string" &&
|
||||||
|
Buffer.byteLength(value, "utf8") <= secureStorageLimits.maxValueBytes
|
||||||
|
? ipcRenderer.invoke(ipcChannels.saveSecureStorage, key, value)
|
||||||
|
: Promise.reject(new Error("Invalid secure storage key or value.")),
|
||||||
|
delete: (key: string) =>
|
||||||
|
validKey(key)
|
||||||
|
? ipcRenderer.invoke(ipcChannels.deleteSecureStorage, key)
|
||||||
|
: Promise.reject(new Error("Invalid secure storage key.")),
|
||||||
|
clear: () => ipcRenderer.invoke(ipcChannels.clearSecureStorage),
|
||||||
|
},
|
||||||
window: {
|
window: {
|
||||||
minimize: () => windowAction(ipcChannels.minimizeWindow),
|
minimize: () => windowAction(ipcChannels.minimizeWindow),
|
||||||
maximize: () => windowAction(ipcChannels.maximizeWindow),
|
maximize: () => windowAction(ipcChannels.maximizeWindow),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,22 @@ export type DesktopScreenShareCapabilities = {
|
||||||
hasReliableSystemAudio: boolean;
|
hasReliableSystemAudio: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DesktopCallStatus = {
|
||||||
|
inCall: boolean;
|
||||||
|
speaking: boolean;
|
||||||
|
iconDataUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DesktopSecureStorageStatus = {
|
||||||
|
available: boolean;
|
||||||
|
backend: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const secureStorageLimits = {
|
||||||
|
maxKeyBytes: 256,
|
||||||
|
maxValueBytes: 1024 * 1024,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export type ReleaseArtifact = {
|
export type ReleaseArtifact = {
|
||||||
name: string;
|
name: string;
|
||||||
platform: string;
|
platform: string;
|
||||||
|
|
@ -55,4 +71,10 @@ export const ipcChannels = {
|
||||||
closeWindow: "window:close",
|
closeWindow: "window:close",
|
||||||
getVersion: "app:getVersion",
|
getVersion: "app:getVersion",
|
||||||
checkForUpdates: "updates:checkForUpdates",
|
checkForUpdates: "updates:checkForUpdates",
|
||||||
|
setCallStatus: "call:setStatus",
|
||||||
|
getSecureStorageStatus: "secureStorage:getStatus",
|
||||||
|
loadSecureStorage: "secureStorage:load",
|
||||||
|
saveSecureStorage: "secureStorage:save",
|
||||||
|
deleteSecureStorage: "secureStorage:delete",
|
||||||
|
clearSecureStorage: "secureStorage:clear",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 52 KiB |
96
apps/tauri/flake.lock
generated
|
|
@ -1,96 +0,0 @@
|
||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"flake-utils": {
|
|
||||||
"inputs": {
|
|
||||||
"systems": "systems"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731533236,
|
|
||||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1775036866,
|
|
||||||
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_2": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1744536153,
|
|
||||||
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-utils": "flake-utils",
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"rust-overlay": "rust-overlay"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rust-overlay": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs_2"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1775272153,
|
|
||||||
"narHash": "sha256-FwYb64ysv8J2TxaqsYYcDyHAHBUEaQlriPMWPMi1K7M=",
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"rev": "740fb0203b2852917b909a72b948d34d0b171ec0",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "oxalica",
|
|
||||||
"repo": "rust-overlay",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
{
|
|
||||||
description = "Tauri mobile development environment";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = {
|
|
||||||
self,
|
|
||||||
nixpkgs,
|
|
||||||
rust-overlay,
|
|
||||||
flake-utils,
|
|
||||||
}:
|
|
||||||
flake-utils.lib.eachDefaultSystem (
|
|
||||||
system: let
|
|
||||||
overlays = [(import rust-overlay)];
|
|
||||||
pkgs = import nixpkgs {
|
|
||||||
inherit system overlays;
|
|
||||||
config = {
|
|
||||||
allowUnfree = true;
|
|
||||||
android_sdk.accept_license = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
projectRoot = ".";
|
|
||||||
androidHome = "${projectRoot}/.android";
|
|
||||||
sdkRoot = "${androidHome}/sdk";
|
|
||||||
ndkVersion = "29.0.14206865";
|
|
||||||
|
|
||||||
android = pkgs.androidenv.composeAndroidPackages {
|
|
||||||
cmdLineToolsVersion = "8.0";
|
|
||||||
toolsVersion = "26.1.1";
|
|
||||||
platformToolsVersion = "35.0.2";
|
|
||||||
buildToolsVersions = ["35.0.0"];
|
|
||||||
platformVersions = ["35" "36"];
|
|
||||||
includeSources = false;
|
|
||||||
includeSystemImages = false;
|
|
||||||
includeNDK = true;
|
|
||||||
ndkVersions = [ndkVersion];
|
|
||||||
useGoogleAPIs = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
|
|
||||||
extensions = ["rust-src" "rust-analyzer"];
|
|
||||||
targets = [
|
|
||||||
"aarch64-linux-android"
|
|
||||||
"armv7-linux-androideabi"
|
|
||||||
"i686-linux-android"
|
|
||||||
"x86_64-linux-android"
|
|
||||||
"wasm32-unknown-unknown"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
in {
|
|
||||||
devShells.default = pkgs.mkShell {
|
|
||||||
buildInputs = with pkgs;
|
|
||||||
[
|
|
||||||
jdk17
|
|
||||||
rustToolchain
|
|
||||||
gradle
|
|
||||||
nodejs
|
|
||||||
pkg-config
|
|
||||||
]
|
|
||||||
++ [
|
|
||||||
android.androidsdk
|
|
||||||
pkgs.android-studio-tools
|
|
||||||
];
|
|
||||||
|
|
||||||
shellHook = ''
|
|
||||||
sdkSource="${android.androidsdk}/libexec/android-sdk"
|
|
||||||
|
|
||||||
mkdir -p "${androidHome}"
|
|
||||||
if [ -L "${sdkRoot}" ]; then
|
|
||||||
rm -f "${sdkRoot}"
|
|
||||||
fi
|
|
||||||
mkdir -p "${sdkRoot}"
|
|
||||||
|
|
||||||
ln -sfn "$sdkSource/build-tools" "${sdkRoot}/build-tools"
|
|
||||||
ln -sfn "$sdkSource/cmake" "${sdkRoot}/cmake"
|
|
||||||
ln -sfn "$sdkSource/licenses" "${sdkRoot}/licenses"
|
|
||||||
ln -sfn "$sdkSource/ndk" "${sdkRoot}/ndk"
|
|
||||||
ln -sfn "$sdkSource/ndk-bundle" "${sdkRoot}/ndk-bundle"
|
|
||||||
ln -sfn "$sdkSource/platforms" "${sdkRoot}/platforms"
|
|
||||||
ln -sfn "$sdkSource/platform-tools" "${sdkRoot}/platform-tools"
|
|
||||||
ln -sfn "$sdkSource/tools" "${sdkRoot}/tools"
|
|
||||||
|
|
||||||
mkdir -p "${sdkRoot}/cmdline-tools"
|
|
||||||
ln -sfn "$sdkSource/cmdline-tools/8.0" "${sdkRoot}/cmdline-tools/8.0"
|
|
||||||
ln -sfn "8.0" "${sdkRoot}/cmdline-tools/latest"
|
|
||||||
|
|
||||||
sdkRootAbs="$(realpath "${sdkRoot}")"
|
|
||||||
ndkRootAbs="''${sdkRootAbs}/ndk/${ndkVersion}"
|
|
||||||
|
|
||||||
export PATH="''${sdkRootAbs}/cmdline-tools/latest/bin:''${sdkRootAbs}/platform-tools:''${ndkRootAbs}:${pkgs.android-studio-tools}/bin:$PATH"
|
|
||||||
export ANDROID_HOME="''${sdkRootAbs}"
|
|
||||||
export ANDROID_SDK_ROOT="''${sdkRootAbs}"
|
|
||||||
export ANDROID_NDK_ROOT="''${ndkRootAbs}"
|
|
||||||
export ANDROID_NDK_HOME="$ANDROID_NDK_ROOT"
|
|
||||||
export NDK_HOME="$ANDROID_NDK_ROOT"
|
|
||||||
export NDK_PATH="$ANDROID_NDK_ROOT"
|
|
||||||
export JAVA_HOME="${pkgs.jdk17}"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="90mm"
|
width="90.476906mm"
|
||||||
height="90mm"
|
height="90.476906mm"
|
||||||
viewBox="0 0 90 90"
|
viewBox="0 0 90.476906 90.476906"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
sodipodi:docname="logo_raw.svg"
|
sodipodi:docname="logo_square_outline_gen.svg"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
|
@ -24,16 +24,32 @@
|
||||||
inkscape:pagecheckerboard="1"
|
inkscape:pagecheckerboard="1"
|
||||||
inkscape:deskcolor="#505050"
|
inkscape:deskcolor="#505050"
|
||||||
inkscape:document-units="mm"
|
inkscape:document-units="mm"
|
||||||
inkscape:zoom="0.99999999"
|
inkscape:zoom="2"
|
||||||
inkscape:cx="450"
|
inkscape:cx="191.5"
|
||||||
inkscape:cy="291.5"
|
inkscape:cy="117.25"
|
||||||
inkscape:window-width="1223"
|
inkscape:window-width="2500"
|
||||||
inkscape:window-height="1369"
|
inkscape:window-height="1403"
|
||||||
inkscape:window-x="26"
|
inkscape:window-x="0"
|
||||||
inkscape:window-y="23"
|
inkscape:window-y="0"
|
||||||
inkscape:window-maximized="0"
|
inkscape:window-maximized="1"
|
||||||
inkscape:current-layer="layer1" /><defs
|
inkscape:current-layer="layer1" /><defs
|
||||||
id="defs1"><linearGradient
|
id="defs1"><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect1"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1"
|
||||||
|
radius="1"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /><linearGradient
|
||||||
id="swatch15"
|
id="swatch15"
|
||||||
inkscape:swatch="solid"><stop
|
inkscape:swatch="solid"><stop
|
||||||
style="stop-color:#000000;stop-opacity:1;"
|
style="stop-color:#000000;stop-opacity:1;"
|
||||||
|
|
@ -51,7 +67,7 @@
|
||||||
id="stop3" /></linearGradient><linearGradient
|
id="stop3" /></linearGradient><linearGradient
|
||||||
id="swatch2"
|
id="swatch2"
|
||||||
inkscape:swatch="solid"><stop
|
inkscape:swatch="solid"><stop
|
||||||
style="stop-color:#000000;stop-opacity:1;"
|
style="stop-color:#b8f8ff;stop-opacity:1;"
|
||||||
offset="0"
|
offset="0"
|
||||||
id="stop2" /></linearGradient><linearGradient
|
id="stop2" /></linearGradient><linearGradient
|
||||||
id="swatch1"
|
id="swatch1"
|
||||||
|
|
@ -184,13 +200,23 @@
|
||||||
x1="63.191292"
|
x1="63.191292"
|
||||||
y1="148.5"
|
y1="148.5"
|
||||||
x2="146.82355"
|
x2="146.82355"
|
||||||
y2="148.5" /></defs><g
|
y2="148.5" /><linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#swatch2"
|
||||||
|
id="linearGradient1"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="63.191292"
|
||||||
|
y1="148.5"
|
||||||
|
x2="146.82355"
|
||||||
|
y2="148.5"
|
||||||
|
gradientTransform="matrix(1.0144629,0,0,1.0151924,217.46226,13.830072)" /></defs><g
|
||||||
inkscape:label="Layer 1"
|
inkscape:label="Layer 1"
|
||||||
inkscape:groupmode="layer"
|
inkscape:groupmode="layer"
|
||||||
id="layer1"
|
id="layer1"
|
||||||
transform="translate(-62.941767,-104.13688)"><g
|
transform="translate(-281.06837,-119.33314)"
|
||||||
|
><g
|
||||||
id="g2"
|
id="g2"
|
||||||
transform="matrix(0.78671048,0,0,0.83608014,-116.36038,-50.640745)"
|
transform="matrix(0.77770172,0,0,0.82714179,104.17421,-33.291714)"
|
||||||
inkscape:label="background"
|
inkscape:label="background"
|
||||||
style="display:inline;stroke:url(#linearGradient15);stroke-width:0.616508;stroke-dasharray:none"
|
style="display:inline;stroke:url(#linearGradient15);stroke-width:0.616508;stroke-dasharray:none"
|
||||||
clip-path="url(#clipPath1)"><path
|
clip-path="url(#clipPath1)"><path
|
||||||
|
|
@ -213,11 +239,19 @@
|
||||||
id="rect1-1-3-6-2"
|
id="rect1-1-3-6-2"
|
||||||
style="fill:#12a89e;stroke:url(#linearGradient21);stroke-width:0.616508;stroke-dasharray:none"
|
style="fill:#12a89e;stroke:url(#linearGradient21);stroke-width:0.616508;stroke-dasharray:none"
|
||||||
transform="rotate(-75)"
|
transform="rotate(-75)"
|
||||||
d="m -227.83395,371.18179 h 146.303341 v 24.39636 H -227.83395 Z" /></g><g
|
d="m -227.83395,371.18179 h 146.303341 v 24.39636 H -227.83395 Z" /></g><path
|
||||||
|
id="path1-5"
|
||||||
|
style="fill:none;stroke:url(#linearGradient1);stroke-width:2;stroke-dasharray:none"
|
||||||
|
inkscape:label="glow_outline"
|
||||||
|
d="m 323.73413,119.91784 -40.80246,15.9513 c -0.13609,0.0532 -0.26542,0.21326 -0.28843,0.35757 -2.90631,18.22859 -0.19736,34.27444 8.28042,47.36302 0.0794,0.12264 0.10916,0.33484 0.0657,0.47436 l -7.08112,22.75509 a 0.11130973,0.11130973 40.101955 0 0 0.15695,0.13218 l 18.7652,-9.59506 c 0.13011,-0.0665 0.32608,-0.044 0.43816,0.0497 5.75202,4.81168 12.45464,8.67546 20.46583,11.84762 0.13586,0.0538 0.3564,0.0538 0.49225,-3e-5 32.16862,-12.74646 46.41741,-39.59704 41.09168,-73.02692 -0.023,-0.14431 -0.15232,-0.30436 -0.28841,-0.35757 l -40.80297,-15.9513 a 0.67679193,0.67679193 179.99988 0 0 -0.49284,0 z"
|
||||||
|
sodipodi:nodetypes="cccccccc"
|
||||||
|
inkscape:original-d="m 323.98055,119.8215 -41.2953,16.14398 c -2.99079,18.43973 -0.24934,34.65473 8.38277,47.84598 l -7.23836,23.26035 19.23635,-9.83596 c 5.85568,4.9403 12.69983,8.88741 20.91454,12.11489 32.42602,-12.73985 46.75142,-39.74883 41.29581,-73.38526 z"
|
||||||
|
inkscape:path-effect="#path-effect1"
|
||||||
|
transform="matrix(0.98854883,0,0,0.98930922,6.03231,1.7325084)" /><g
|
||||||
id="g3"
|
id="g3"
|
||||||
inkscape:label="foreground"
|
inkscape:label="foreground"
|
||||||
style="stroke:url(#linearGradient16);stroke-width:0.5;stroke-dasharray:none"
|
style="stroke:url(#linearGradient16);stroke-width:0.5;stroke-dasharray:none"
|
||||||
transform="translate(3.3653788,0.84760028)"><g
|
transform="matrix(0.98854883,0,0,0.98930922,222.52896,17.64618)"><g
|
||||||
id="g1"
|
id="g1"
|
||||||
inkscape:label="outline"
|
inkscape:label="outline"
|
||||||
style="stroke:url(#linearGradient23);stroke-width:0.5;stroke-dasharray:none"><path
|
style="stroke:url(#linearGradient23);stroke-width:0.5;stroke-dasharray:none"><path
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -21,11 +21,11 @@
|
||||||
"dev:mobile:raw": "tauri android dev --host ${TAURI_DEV_HOST:-127.0.0.1}",
|
"dev:mobile:raw": "tauri android dev --host ${TAURI_DEV_HOST:-127.0.0.1}",
|
||||||
"start-adb:mobile:raw": "adb devices",
|
"start-adb:mobile:raw": "adb devices",
|
||||||
"build:mobile:raw": "tauri android build",
|
"build:mobile:raw": "tauri android build",
|
||||||
"dev:mobile": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:mobile:raw; else bun dev:mobile:raw; fi",
|
"dev:mobile": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#tauri --command pnpm run dev:mobile:raw; else pnpm run dev:mobile:raw; fi",
|
||||||
"start-adb:mobile": "if command -v nix >/dev/null 2>&1; then nix develop --command bun start-adb:mobile:raw; else bun start-adb:mobile:raw; fi",
|
"start-adb:mobile": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#tauri --command pnpm run start-adb:mobile:raw; else pnpm run start-adb:mobile:raw; fi",
|
||||||
"build:mobile": "bun run render-version.ts && if command -v nix >/dev/null 2>&1; then nix develop --command bun build:mobile:raw; else bun build:mobile:raw; fi && bun run render-version.ts --unrender",
|
"build:mobile": "node render-version.ts && trap 'node render-version.ts --unrender' EXIT && if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#tauri --command pnpm run build:mobile:raw; else pnpm run build:mobile:raw; fi",
|
||||||
"gen-icons": "tauri icon ./logo.json && bun scripts/sync-electron-icons.ts",
|
"gen-icons": "tauri icon ./logo.json && node scripts/sync-electron-icons.ts",
|
||||||
"format": "bunx prettier --write .",
|
"format": "pnpm exec prettier --write .",
|
||||||
"lint": "eslint src"
|
"lint": "eslint src"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
const PLACEHOLDER_VERSION = "0.0.0";
|
const PLACEHOLDER_VERSION = "0.0.0";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const rootPackageJsonPath = path.resolve(__dirname, "../../package.json");
|
const rootPackageJsonPath = path.resolve(__dirname, "../../package.json");
|
||||||
const cargoTomlPath = path.resolve(__dirname, "./src-tauri/Cargo.toml");
|
const cargoTomlPath = path.resolve(__dirname, "./src-tauri/Cargo.toml");
|
||||||
const tauriConfigPath = path.resolve(__dirname, "./src-tauri/tauri.conf.json");
|
const tauriConfigPath = path.resolve(__dirname, "./src-tauri/tauri.conf.json");
|
||||||
|
|
|
||||||
1266
apps/tauri/src-tauri/Cargo.lock
generated
|
|
@ -15,7 +15,7 @@ name = "mobile_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2", features = [] }
|
tauri-build = { git = "https://github.com/tauri-apps/tauri", branch = "feat/cef", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
|
|
@ -23,8 +23,6 @@ serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tauri-plugin-deep-link = "2"
|
tauri-plugin-deep-link = "2"
|
||||||
tauri-plugin-notification = "2"
|
tauri-plugin-notification = "2"
|
||||||
ttp-core = { git = "https://git.methanium.net/tensamin/ttp.git", package = "ttp-core" }
|
|
||||||
ttp-tauri = { git = "https://git.methanium.net/tensamin/ttp.git", package = "ttp-tauri" }
|
|
||||||
tauri-plugin-log = "2"
|
tauri-plugin-log = "2"
|
||||||
|
|
||||||
[target.'cfg(target_os = "android")'.dependencies.tauri]
|
[target.'cfg(target_os = "android")'.dependencies.tauri]
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,9 @@
|
||||||
"core:default",
|
"core:default",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"core:window:default",
|
"core:window:default",
|
||||||
"core:window:allow-start-dragging",
|
|
||||||
"core:window:allow-close",
|
|
||||||
"core:window:allow-toggle-maximize",
|
|
||||||
"core:window:allow-minimize",
|
|
||||||
"core:event:default",
|
"core:event:default",
|
||||||
"deep-link:default",
|
"deep-link:default",
|
||||||
"notification:default",
|
"notification:default",
|
||||||
"log:default",
|
"log:default"
|
||||||
"ttp-tauri:allow-connect",
|
|
||||||
"ttp-tauri:allow-send",
|
|
||||||
"ttp-tauri:allow-close",
|
|
||||||
"ttp-tauri:allow-ready-state"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,6 @@
|
||||||
"barcode-scanner:allow-scan",
|
"barcode-scanner:allow-scan",
|
||||||
"barcode-scanner:allow-cancel",
|
"barcode-scanner:allow-cancel",
|
||||||
"notification:default",
|
"notification:default",
|
||||||
"log:default",
|
"log:default"
|
||||||
"ttp-tauri:allow-connect",
|
|
||||||
"ttp-tauri:allow-send",
|
|
||||||
"ttp-tauri:allow-close",
|
|
||||||
"ttp-tauri:allow-ready-state"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 6 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 18 KiB |
|
|
@ -2,8 +2,7 @@
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let builder = tauri::Builder::default()
|
let builder = tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_log::Builder::new().level(tauri_plugin_log::log::LevelFilter::Info).build())
|
.plugin(tauri_plugin_log::Builder::new().level(tauri_plugin_log::log::LevelFilter::Info).build())
|
||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init());
|
||||||
.plugin(ttp_tauri::init());
|
|
||||||
|
|
||||||
let builder = builder
|
let builder = builder
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
"mainBinaryName": "tensamin",
|
"mainBinaryName": "tensamin",
|
||||||
"identifier": "net.tensamin.client",
|
"identifier": "net.tensamin.client",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "cd ../web && bun run dev && cd ../tauri",
|
"beforeDevCommand": "cd ../web && pnpm run dev && cd ../tauri",
|
||||||
"devUrl": "http://localhost:3000",
|
"devUrl": "http://localhost:3000",
|
||||||
"beforeBuildCommand": "cd ../.. && bun run build:web && cd apps/tauri",
|
"beforeBuildCommand": "cd ../.. && pnpm run build:web && cd apps/tauri",
|
||||||
"frontendDist": "../../web/dist"
|
"frontendDist": "../../web/dist"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
|
|
|
||||||
2
apps/web/.gitignore
vendored
|
|
@ -22,3 +22,5 @@ dist-ssr
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
.android
|
||||||
|
|
|
||||||
|
|
@ -4,38 +4,128 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "bunx prettier --write .",
|
"format": "pnpm exec prettier --write .",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
"dev": "vite --port 3000 --host 0.0.0.0",
|
"dev": "vite --port 3000 --host 0.0.0.0",
|
||||||
"test": "bun test --pass-with-no-tests",
|
"test": "vitest run --passWithNoTests",
|
||||||
"build": "bun run test && tsc -b && vite build",
|
"build": "pnpm run test && tsc -b && vite build",
|
||||||
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
|
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.0.0",
|
||||||
|
"@base-ui/utils": "0.3.1",
|
||||||
|
"@babel/runtime": "^7.29.2",
|
||||||
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"@fontsource-variable/public-sans": "^5.2.7",
|
"@fontsource-variable/public-sans": "^5.2.7",
|
||||||
|
"@floating-ui/core": "^1.7.0",
|
||||||
|
"@floating-ui/dom": "^1.7.0",
|
||||||
|
"@floating-ui/react-dom": "^2.1.8",
|
||||||
|
"@floating-ui/utils": "^0.2.11",
|
||||||
|
"@radix-ui/primitive": "^1.1.0",
|
||||||
|
"@radix-ui/react-compose-refs": "^1.1.1",
|
||||||
|
"@radix-ui/react-context": "^1.1.4",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
|
"@radix-ui/react-dismissable-layer": "^1.1.11",
|
||||||
|
"@radix-ui/react-focus-guards": "^1.1.4",
|
||||||
|
"@radix-ui/react-focus-scope": "^1.1.11",
|
||||||
|
"@radix-ui/react-id": "^1.1.0",
|
||||||
|
"@radix-ui/react-portal": "^1.1.13",
|
||||||
|
"@radix-ui/react-presence": "^1.1.6",
|
||||||
|
"@radix-ui/react-primitive": "^2.0.2",
|
||||||
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
|
"@radix-ui/react-use-callback-ref": "^1.1.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "^1.2.3",
|
||||||
|
"@radix-ui/react-use-effect-event": "^0.0.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "^1.1.0",
|
||||||
|
"@reduxjs/toolkit": "^2.0.0",
|
||||||
"@tailwindcss/vite": "^4.2.4",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
|
"@tanstack/devtools-event-client": "^0.3.0",
|
||||||
|
"@tanstack/query-core": "^5.0.0",
|
||||||
"@tanstack/react-router": "^1.169.1",
|
"@tanstack/react-router": "^1.169.1",
|
||||||
|
"@tanstack/history": "1.162.0",
|
||||||
|
"@tanstack/react-store": "^0.9.3",
|
||||||
|
"@tanstack/router-core": "^1.169.1",
|
||||||
|
"@tanstack/store": "^0.9.3",
|
||||||
|
"@tanstack/virtual-core": "^3.13.24",
|
||||||
"@tanstack/react-virtual": "^3.13.24",
|
"@tanstack/react-virtual": "^3.13.24",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tensamin/call": "workspace:*",
|
"@tensamin/call": "workspace:*",
|
||||||
|
"@tensamin/cache": "workspace:*",
|
||||||
"@tensamin/chat": "workspace:*",
|
"@tensamin/chat": "workspace:*",
|
||||||
"@tensamin/crypto": "workspace:*",
|
"@tensamin/crypto": "workspace:*",
|
||||||
"@tensamin/shared": "workspace:*",
|
"@tensamin/shared": "workspace:*",
|
||||||
|
"@tensamin/settings": "workspace:*",
|
||||||
"@tensamin/storage": "workspace:*",
|
"@tensamin/storage": "workspace:*",
|
||||||
"@tensamin/tauri": "workspace:*",
|
"@tensamin/tauri": "workspace:*",
|
||||||
"@tensamin/ttp": "workspace:*",
|
"@tensamin/mtp": "workspace:*",
|
||||||
"@tensamin/tauth": "workspace:*",
|
"@tensamin/tauth": "workspace:*",
|
||||||
"@tensamin/markdown": "workspace:*",
|
"@tensamin/markdown": "workspace:*",
|
||||||
"@tensamin/ui": "*",
|
"@tensamin/ui": "*",
|
||||||
"@tensamin/user": "workspace:*",
|
"@tensamin/user": "workspace:*",
|
||||||
"@tensamin/notifications": "workspace:*",
|
"@tensamin/notifications": "workspace:*",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
|
"cookie-es": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-color": "^3.1.0",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-format": "^3.1.0",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-path": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-time-format": "^4.1.0",
|
||||||
|
"d3-timer": "^3.0.1",
|
||||||
|
"date-fns": "^4.4.0",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
|
"detect-node-es": "^1.1.0",
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"embla-carousel": "8.6.0",
|
||||||
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"embla-carousel-reactive-utils": "8.6.0",
|
||||||
|
"es-toolkit": "^1.39.3",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"get-nonce": "^1.0.1",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
|
"internmap": "^2.0.3",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"lucide-react": "^1.14.0",
|
"lucide-react": "^1.14.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
|
"react-day-picker": "^10.0.1",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
"react-is": "^19.0.0",
|
||||||
|
"react-redux": "^9.0.0",
|
||||||
|
"react-remove-scroll": "^2.7.2",
|
||||||
|
"react-remove-scroll-bar": "^2.3.7",
|
||||||
|
"react-resizable-panels": "^4.11.2",
|
||||||
|
"react-style-singleton": "^2.2.3",
|
||||||
|
"recharts": "3.8.1",
|
||||||
|
"redux": "^5.0.0",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "5.1.1",
|
||||||
|
"scheduler": "^0.27.0",
|
||||||
|
"seroval": "^1.5.4",
|
||||||
|
"seroval-plugins": "^1.5.4",
|
||||||
|
"shadcn": "^4.11.0",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"tailwindcss": "^4.2.4",
|
"tailwindcss": "^4.2.4",
|
||||||
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwind-scrollbar-hide": "^4.0.0",
|
"tailwind-scrollbar-hide": "^4.0.0",
|
||||||
|
"tiny-invariant": "^1.3.3",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"use-callback-ref": "^1.3.3",
|
||||||
|
"use-sidecar": "^1.1.3",
|
||||||
|
"use-sync-external-store": "^1.2.2",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
|
"victory-vendor": "^37.0.2",
|
||||||
|
"yargs": "^15.3.1",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,13 @@ export function Basic({
|
||||||
extra?: React.ReactNode;
|
extra?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card className="animate-in fade-in duration-300 rounded-2xl py-0 m-px">
|
<Card className="animate-in fade-in duration-300 rounded-xl py-0 h-12.5!">
|
||||||
<CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2">
|
<CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2">
|
||||||
<div className="relative shrink-0 overflow-visible">
|
<div className="relative shrink-0 overflow-visible">
|
||||||
<Avatar>
|
<Avatar>
|
||||||
<AvatarImage src={user.avatar} />
|
<AvatarImage src={user.Avatar} />
|
||||||
<AvatarFallback>
|
<AvatarFallback>
|
||||||
{user.display.slice(0, 2).toUpperCase()}
|
{user.Display.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|
@ -34,7 +34,7 @@ export function Basic({
|
||||||
<div className="absolute -bottom-0.5 -right-0.5 z-10 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-card">
|
<div className="absolute -bottom-0.5 -right-0.5 z-10 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-card">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: getStatusColor(user.online_status),
|
backgroundColor: getStatusColor(user.OnlineStatus),
|
||||||
}}
|
}}
|
||||||
className="h-2.25 w-2.25 rounded-full"
|
className="h-2.25 w-2.25 rounded-full"
|
||||||
/>
|
/>
|
||||||
|
|
@ -42,15 +42,14 @@ export function Basic({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{user.online_status
|
{user.OnlineStatus.split("_")
|
||||||
.split("_")
|
|
||||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
.join(" ")}
|
.join(" ")}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 w-full items-start justify-center text-[15px]">
|
<div className="flex flex-col gap-1 w-full items-start justify-center text-[15px]">
|
||||||
<p>{user.display}</p>
|
<p>{user.Display}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="pr-1">{extra}</div>
|
<div className="pr-1">{extra}</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
@ -59,5 +58,5 @@ export function Basic({
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Loading() {
|
export function Loading() {
|
||||||
return <Skeleton className="h-12.5 rounded-2xl" />;
|
return <Skeleton className="h-12.5! rounded-2xl" />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,27 @@
|
||||||
import type { User } from "@tensamin/user/context";
|
import type { User } from "@tensamin/user/context";
|
||||||
import {
|
import { Avatar, AvatarFallback, AvatarImage, Button } from "@tensamin/ui";
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
Button,
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@tensamin/ui";
|
|
||||||
import { toLossySixDigitCode } from "@tensamin/shared/code";
|
|
||||||
import Text from "@tensamin/markdown/text";
|
import Text from "@tensamin/markdown/text";
|
||||||
import { ChevronDown, ChevronUp, Info } from "lucide-react";
|
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useCrypto } from "@tensamin/crypto/context";
|
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
|
||||||
import { useUser } from "@tensamin/user/context";
|
|
||||||
|
|
||||||
export default function Profile({ user }: { user: User }) {
|
export default function Profile({ user }: { user: User }) {
|
||||||
const [showAdvancedInformation, setShowAdvancedInformation] = useState(false);
|
const [showAdvancedInformation, setShowAdvancedInformation] = useState(false);
|
||||||
const [sharedSecret, setSharedSecret] = useState("");
|
|
||||||
|
|
||||||
const { getSharedSecret } = useCrypto();
|
|
||||||
const { load } = useStorage();
|
|
||||||
const { get } = useUser();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let active = true;
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
const ownId = await load("user_id");
|
|
||||||
const privateKey = await load("private_key");
|
|
||||||
const ownData = await get(ownId);
|
|
||||||
const secret = await getSharedSecret(
|
|
||||||
privateKey,
|
|
||||||
ownData.public_key,
|
|
||||||
user.public_key,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (active) {
|
|
||||||
setSharedSecret(secret);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (active) {
|
|
||||||
setSharedSecret("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
active = false;
|
|
||||||
};
|
|
||||||
}, [get, getSharedSecret, load, user.public_key]);
|
|
||||||
|
|
||||||
const sharedSecretCode = sharedSecret
|
|
||||||
? toLossySixDigitCode(sharedSecret)
|
|
||||||
: "------";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Avatar className="size-10">
|
<Avatar className="size-10">
|
||||||
<AvatarImage src={user.avatar} />
|
<AvatarImage src={user.Avatar} />
|
||||||
<AvatarFallback className="text-lg">
|
<AvatarFallback className="text-lg">
|
||||||
{user.display.slice(0, 2).toUpperCase()}
|
{user.Display.slice(0, 2).toUpperCase()}
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-lg font-semibold">{user.display}</p>
|
<p className="text-lg font-semibold">{user.Display}</p>
|
||||||
<p className="text-muted-foreground">{user.username}</p>
|
<p className="text-muted-foreground">{user.Username}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Text value={user.about || ""} />
|
<Text value={user.About || ""} />
|
||||||
<Button
|
<Button
|
||||||
className="h-auto justify-start gap-1.5 px-0 py-1 text-base font-medium text-white no-underline hover:no-underline"
|
className="h-auto justify-start gap-1.5 px-0 py-1 text-base font-medium text-white no-underline hover:no-underline"
|
||||||
variant="link"
|
variant="link"
|
||||||
|
|
@ -87,24 +37,14 @@ export default function Profile({ user }: { user: User }) {
|
||||||
</Button>
|
</Button>
|
||||||
{showAdvancedInformation && (
|
{showAdvancedInformation && (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-white flex gap-1 overflow-hidden text-ellipsis whitespace-nowrap">
|
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
Shared Code: <code>{sharedSecretCode}</code>{" "}
|
Iota ID: <code>{user.IotaId}</code>
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger render={<Info className="size-3.5" />} />
|
|
||||||
<TooltipContent>
|
|
||||||
You can compare this code with the conversation partner to
|
|
||||||
validate that this chat is E2EE
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
Iota ID: <code>{user.iota_id}</code>
|
User ID: <code>{user.UserId}</code>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
User ID: <code>{user.user_id}</code>
|
Public Key: <code>{user.PublicKey}</code>
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
Public Key: <code>{user.public_key}</code>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
const { id } = useSearch({ strict: false });
|
const { id } = useSearch({ strict: false });
|
||||||
|
|
||||||
const currentCalls = calls.filter((call) =>
|
const currentCalls = calls.filter((call) =>
|
||||||
call.call_members.some((member) => member === id),
|
call.CallMembers.some((member) => member === id),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
@ -58,7 +58,6 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
<House className="size-4.5" />
|
<House className="size-4.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
// @ts-expect-error TanStack router doesn't properly detect the settings route
|
|
||||||
onClick={() => navigate({ to: "/settings" })}
|
onClick={() => navigate({ to: "/settings" })}
|
||||||
className="w-9 h-9 aspect-square rounded-lg"
|
className="w-9 h-9 aspect-square rounded-lg"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -72,7 +71,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
userId={id}
|
userId={id}
|
||||||
component={(user) =>
|
component={(user) =>
|
||||||
isMobile ? (
|
isMobile ? (
|
||||||
<p className="font-medium text-[1.07rem]">{user?.display}</p>
|
<p className="font-medium text-[1.07rem]">{user?.Display}</p>
|
||||||
) : (
|
) : (
|
||||||
<Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}>
|
<Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
|
|
@ -85,7 +84,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="font-medium text-[1.07rem]">
|
<p className="font-medium text-[1.07rem]">
|
||||||
{user?.display}
|
{user?.Display}
|
||||||
</p>
|
</p>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +95,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
</Popover>
|
</Popover>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
loading={<Skeleton className="ml-3 w-40 h-5" />}
|
loading={<Skeleton className="w-30 h-5" />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -121,8 +120,8 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void joinCall(
|
void joinCall(
|
||||||
id,
|
id,
|
||||||
currentCalls[0].call_secret,
|
currentCalls[0].CallSecret,
|
||||||
currentCalls[0].call_id,
|
currentCalls[0].CallId,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
className="w-9 h-9 aspect-square rounded-lg"
|
className="w-9 h-9 aspect-square rounded-lg"
|
||||||
|
|
@ -147,13 +146,13 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||||
<SelectContent className="p-1">
|
<SelectContent className="p-1">
|
||||||
{currentCalls.map((call) => (
|
{currentCalls.map((call) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
value={call.call_id}
|
value={call.CallId}
|
||||||
key={call.call_id}
|
key={call.CallId}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
void joinCall(id, call.call_secret, call.call_id);
|
void joinCall(id, call.CallSecret, call.CallId);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{displayCallId(call.call_id)}
|
{displayCallId(call.CallId)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -195,7 +194,6 @@ export function MobileNavbar() {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// @ts-expect-error TanStack router doesn't properly detect the settings route
|
|
||||||
navigate({ to: "/settings" });
|
navigate({ to: "/settings" });
|
||||||
setOpenMobile(false);
|
setOpenMobile(false);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const fetchedUser = z.object({
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
username: z.string().min(1).max(255),
|
username: z.string().min(1).max(255),
|
||||||
private_key: z.string().min(1).max(92),
|
mtp_keyring: z.string().min(1).max(92),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,14 +61,7 @@ function parseTuFileContent(rawFileContent: string): {
|
||||||
? Number(userIdString.split("@")[0])
|
? Number(userIdString.split("@")[0])
|
||||||
: Number(userIdString);
|
: Number(userIdString);
|
||||||
|
|
||||||
const rawDomain = userIdString.includes("@")
|
const domain = userIdString.includes("@") ? userIdString.split("@")[1] : null;
|
||||||
? userIdString.split("@")[1]
|
|
||||||
: null;
|
|
||||||
const domain = rawDomain
|
|
||||||
? rawDomain.includes(":")
|
|
||||||
? rawDomain
|
|
||||||
: rawDomain + ":1984"
|
|
||||||
: null;
|
|
||||||
|
|
||||||
if (!userId || !privateKey) {
|
if (!userId || !privateKey) {
|
||||||
throw new Error("Invalid file");
|
throw new Error("Invalid file");
|
||||||
|
|
@ -76,7 +69,6 @@ function parseTuFileContent(rawFileContent: string): {
|
||||||
|
|
||||||
console.log({
|
console.log({
|
||||||
domain,
|
domain,
|
||||||
rawDomain,
|
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -88,6 +80,30 @@ export default function Form() {
|
||||||
const uploadRef = React.useRef<HTMLInputElement | null>(null);
|
const uploadRef = React.useRef<HTMLInputElement | null>(null);
|
||||||
const [isDragging, setIsDragging] = React.useState(false);
|
const [isDragging, setIsDragging] = React.useState(false);
|
||||||
const { save } = useStorage();
|
const { save } = useStorage();
|
||||||
|
const loginPendingRef = React.useRef(false);
|
||||||
|
|
||||||
|
const persistLogin = React.useCallback(
|
||||||
|
async (userId: number, privateKey: string, domain?: string | null) => {
|
||||||
|
if (loginPendingRef.current) return false;
|
||||||
|
loginPendingRef.current = true;
|
||||||
|
try {
|
||||||
|
if (domain) await save("omega_url", `https://${domain}/`);
|
||||||
|
await save("mtp_keyring", privateKey, { secure: true });
|
||||||
|
await save("session_id", Date.now());
|
||||||
|
await save("user_id", userId);
|
||||||
|
window.history.replaceState(
|
||||||
|
null,
|
||||||
|
"",
|
||||||
|
window.location.protocol === "file:" ? "#/" : "/",
|
||||||
|
);
|
||||||
|
window.location.reload();
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
loginPendingRef.current = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[save],
|
||||||
|
);
|
||||||
|
|
||||||
// Process dropped files
|
// Process dropped files
|
||||||
const processDroppedFile = React.useCallback(
|
const processDroppedFile = React.useCallback(
|
||||||
|
|
@ -101,20 +117,13 @@ export default function Form() {
|
||||||
const raw = await file.text();
|
const raw = await file.text();
|
||||||
const parsed = parseTuFileContent(raw);
|
const parsed = parseTuFileContent(raw);
|
||||||
|
|
||||||
await save("session_id", Date.now());
|
await persistLogin(parsed.userId, parsed.privateKey, parsed.domain);
|
||||||
await save("user_id", parsed.userId);
|
|
||||||
await save("private_key", parsed.privateKey);
|
|
||||||
if (parsed.domain) {
|
|
||||||
await save("ttp_url", `https://${parsed.domain}/`);
|
|
||||||
}
|
|
||||||
|
|
||||||
location.href = "/";
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(0, "login", "red", error);
|
log(0, "login", "red", error);
|
||||||
toast("error", "Failed to load file");
|
toast("error", "Failed to load file");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[save],
|
[persistLogin],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle .tu files
|
// Handle .tu files
|
||||||
|
|
@ -220,14 +229,9 @@ export default function Form() {
|
||||||
const actualUsername = inputUsername.includes("@")
|
const actualUsername = inputUsername.includes("@")
|
||||||
? inputUsername.split("@")[0]
|
? inputUsername.split("@")[0]
|
||||||
: inputUsername;
|
: inputUsername;
|
||||||
const rawDomain = inputUsername.includes("@")
|
const domain = inputUsername.includes("@")
|
||||||
? inputUsername.split("@")[1]
|
? inputUsername.split("@")[1]
|
||||||
: null;
|
: null;
|
||||||
const domain = rawDomain
|
|
||||||
? rawDomain.includes(":")
|
|
||||||
? rawDomain
|
|
||||||
: rawDomain + ":1984"
|
|
||||||
: null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
|
|
@ -249,20 +253,13 @@ export default function Form() {
|
||||||
|
|
||||||
const user = parse.data;
|
const user = parse.data;
|
||||||
|
|
||||||
await save("session_id", Date.now());
|
await persistLogin(user.user_id, inputParse.data.mtp_keyring, domain);
|
||||||
await save("user_id", user.user_id);
|
|
||||||
await save("private_key", inputParse.data.private_key);
|
|
||||||
if (domain) {
|
|
||||||
await save("ttp_url", `https://${domain}/`);
|
|
||||||
}
|
|
||||||
|
|
||||||
location.href = "/";
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(0, "login", "red", error);
|
log(0, "login", "red", error);
|
||||||
toast("error", "Failed to fetch user data");
|
toast("error", "Failed to fetch user data");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[save],
|
[persistLogin],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -282,15 +279,7 @@ export default function Form() {
|
||||||
const { userId, privateKey, domain } =
|
const { userId, privateKey, domain } =
|
||||||
parseTuFileContent(decoded);
|
parseTuFileContent(decoded);
|
||||||
|
|
||||||
await save("session_id", Date.now());
|
await persistLogin(userId, privateKey, domain);
|
||||||
await save("user_id", userId);
|
|
||||||
await save("private_key", privateKey);
|
|
||||||
|
|
||||||
if (domain) {
|
|
||||||
await save("ttp_url", `https://${domain}/`);
|
|
||||||
}
|
|
||||||
|
|
||||||
location.href = "/";
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(0, "login", "red", error);
|
log(0, "login", "red", error);
|
||||||
toast("error", "Failed to parse QR code data");
|
toast("error", "Failed to parse QR code data");
|
||||||
|
|
@ -346,8 +335,8 @@ export default function Form() {
|
||||||
<Input required type="text" id="username" name="username" />
|
<Input required type="text" id="username" name="username" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<Label htmlFor="private_key">Private Key</Label>
|
<Label htmlFor="mtp_keyring">MTP Keyring</Label>
|
||||||
<Input required type="password" id="private_key" name="private_key" />
|
<Input required type="password" id="mtp_keyring" name="mtp_keyring" />
|
||||||
</div>
|
</div>
|
||||||
<Button className="mt-auto" type="submit">
|
<Button className="mt-auto" type="submit">
|
||||||
Login
|
Login
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ import {
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuLabel,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
|
|
@ -38,12 +37,10 @@ import { Ellipsis, Check } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { User } from "@tensamin/user/context";
|
import type { User } from "@tensamin/user/context";
|
||||||
import type z from "zod";
|
import type z from "zod";
|
||||||
import { ttp } from "@tensamin/shared/data";
|
import { mtp } from "@tensamin/shared/data";
|
||||||
import { useTTP } from "@tensamin/ttp";
|
import { useMTP } from "@tensamin/mtp";
|
||||||
|
|
||||||
type OnlineStatus = z.infer<
|
type OnlineStatus = z.infer<typeof mtp.GetUserData.response.shape.OnlineStatus>;
|
||||||
typeof ttp.get_user_data.response.shape.online_status
|
|
||||||
>;
|
|
||||||
|
|
||||||
const onlineStatusLabels: Record<OnlineStatus, string> = {
|
const onlineStatusLabels: Record<OnlineStatus, string> = {
|
||||||
user_online: "Online",
|
user_online: "Online",
|
||||||
|
|
@ -74,7 +71,7 @@ function StatusDialog({
|
||||||
user: User;
|
user: User;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
send: ReturnType<typeof useTTP>["send"];
|
send: ReturnType<typeof useMTP>["send"];
|
||||||
draftStatus: string;
|
draftStatus: string;
|
||||||
setDraftStatus: (value: string) => void;
|
setDraftStatus: (value: string) => void;
|
||||||
draftOnlineStatus: OnlineStatus;
|
draftOnlineStatus: OnlineStatus;
|
||||||
|
|
@ -89,8 +86,8 @@ function StatusDialog({
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={(nextOpen) => {
|
onOpenChange={(nextOpen) => {
|
||||||
if (!nextOpen) {
|
if (!nextOpen) {
|
||||||
setDraftStatus(user.status ?? "");
|
setDraftStatus(user.Status ?? "");
|
||||||
setDraftOnlineStatus(user.online_status);
|
setDraftOnlineStatus(user.OnlineStatus);
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
setSaveSucceeded(false);
|
setSaveSucceeded(false);
|
||||||
}
|
}
|
||||||
|
|
@ -145,11 +142,10 @@ function StatusDialog({
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const payload = {
|
const payload = {
|
||||||
...(draftStatus && { status: draftStatus }),
|
...(draftStatus && { status: draftStatus }),
|
||||||
online_status: draftOnlineStatus,
|
OnlineStatus: draftOnlineStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
const validation =
|
const validation = mtp.ChangeUserData.request.safeParse(payload);
|
||||||
ttp.change_user_data.request.safeParse(payload);
|
|
||||||
|
|
||||||
if (!validation.success) {
|
if (!validation.success) {
|
||||||
setSaveSucceeded(false);
|
setSaveSucceeded(false);
|
||||||
|
|
@ -160,7 +156,7 @@ function StatusDialog({
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await send("change_user_data", validation.data);
|
await send("ChangeUserData", validation.data);
|
||||||
setSaveSucceeded(true);
|
setSaveSucceeded(true);
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -195,7 +191,7 @@ export default function Sidebar() {
|
||||||
const [statusErrorMessage, setStatusErrorMessage] = useState("");
|
const [statusErrorMessage, setStatusErrorMessage] = useState("");
|
||||||
const [statusSaveSucceeded, setStatusSaveSucceeded] = useState(false);
|
const [statusSaveSucceeded, setStatusSaveSucceeded] = useState(false);
|
||||||
|
|
||||||
const { send } = useTTP();
|
const { send } = useMTP();
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
|
|
@ -228,11 +224,10 @@ export default function Sidebar() {
|
||||||
/>
|
/>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel>Profile</DropdownMenuLabel>
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDraftStatus(user.status ?? "");
|
setDraftStatus(user.Status ?? "");
|
||||||
setDraftOnlineStatus(user.online_status);
|
setDraftOnlineStatus(user.OnlineStatus);
|
||||||
setStatusErrorMessage("");
|
setStatusErrorMessage("");
|
||||||
setStatusSaveSucceeded(false);
|
setStatusSaveSucceeded(false);
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
|
|
@ -250,8 +245,8 @@ export default function Sidebar() {
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
onOpenChange={(nextOpen) => {
|
onOpenChange={(nextOpen) => {
|
||||||
if (!nextOpen) {
|
if (!nextOpen) {
|
||||||
setDraftStatus(user.status ?? "");
|
setDraftStatus(user.Status ?? "");
|
||||||
setDraftOnlineStatus(user.online_status);
|
setDraftOnlineStatus(user.OnlineStatus);
|
||||||
setStatusErrorMessage("");
|
setStatusErrorMessage("");
|
||||||
setStatusSaveSucceeded(false);
|
setStatusSaveSucceeded(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ export default function List() {
|
||||||
{category === "conversations" ? (
|
{category === "conversations" ? (
|
||||||
contacts?.[virtualItem.index] ? (
|
contacts?.[virtualItem.index] ? (
|
||||||
<ConversationModal
|
<ConversationModal
|
||||||
userId={contacts[virtualItem.index].user_id}
|
userId={contacts[virtualItem.index].UserId}
|
||||||
/>
|
/>
|
||||||
) : null
|
) : null
|
||||||
) : communities?.[virtualItem.index] ? (
|
) : communities?.[virtualItem.index] ? (
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,10 @@ export default function Switch(props: {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="tablist"
|
role="tablist"
|
||||||
className="border relative grid grid-cols-2 rounded-full bg-card p-1 select-none"
|
className="border relative grid grid-cols-2 rounded-4xl bg-card p-1 select-none"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute top-1 bottom-1 rounded-full bg-input shadow-sm transition-all duration-300 ease-in-out"
|
className="absolute top-1 bottom-1 rounded-4xl bg-input shadow-sm transition-all duration-300 ease-in-out"
|
||||||
style={{
|
style={{
|
||||||
left: props.category === "conversations" ? "0.25rem" : "50%",
|
left: props.category === "conversations" ? "0.25rem" : "50%",
|
||||||
width: "calc(50% - 0.25rem)",
|
width: "calc(50% - 0.25rem)",
|
||||||
|
|
@ -35,7 +35,7 @@ export default function Switch(props: {
|
||||||
<button
|
<button
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={props.category === "conversations"}
|
aria-selected={props.category === "conversations"}
|
||||||
className={`relative z-10 w-full cursor-pointer rounded-full px-2.5 py-1.5 text-center text-sm font-medium transition-colors duration-300 ${
|
className={`relative z-10 w-full cursor-pointer rounded-4xl px-2.5 py-1.5 text-center text-sm font-medium transition-colors duration-300 ${
|
||||||
props.category === "conversations"
|
props.category === "conversations"
|
||||||
? "text-foreground"
|
? "text-foreground"
|
||||||
: "text-ring/50 hover:text-ring"
|
: "text-ring/50 hover:text-ring"
|
||||||
|
|
@ -47,7 +47,7 @@ export default function Switch(props: {
|
||||||
<button
|
<button
|
||||||
role="tab"
|
role="tab"
|
||||||
aria-selected={props.category === "communities"}
|
aria-selected={props.category === "communities"}
|
||||||
className={`relative z-10 w-full cursor-pointer rounded-full px-2.5 py-1.5 text-center text-sm font-medium transition-colors duration-300 ${
|
className={`relative z-10 w-full cursor-pointer rounded-4xl px-2.5 py-1.5 text-center text-sm font-medium transition-colors duration-300 ${
|
||||||
props.category === "communities"
|
props.category === "communities"
|
||||||
? "text-foreground"
|
? "text-foreground"
|
||||||
: "text-ring/50 hover:text-ring"
|
: "text-ring/50 hover:text-ring"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--radius-sm: calc(var(--radius) * 0.6);
|
||||||
|
--radius-md: calc(var(--radius) * 0.8);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) * 1.4);
|
||||||
|
--radius-2xl: calc(var(--radius) * 1.8);
|
||||||
|
--radius-3xl: calc(var(--radius) * 2.2);
|
||||||
|
--radius-4xl: calc(var(--radius) * 2.6);
|
||||||
|
--radius-full: calc(var(--radius) * 9999);
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#root {
|
#root {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import "@tensamin/ui/index.css";
|
||||||
import NotFound from "@/routes/404";
|
import NotFound from "@/routes/404";
|
||||||
|
|
||||||
import AppLayout from "@/routes/app/layout";
|
import AppLayout from "@/routes/app/layout";
|
||||||
import SettingsLayout from "@/features/settings/layout";
|
import { createSettingsRoute } from "@tensamin/settings";
|
||||||
|
|
||||||
import Home from "@/routes/app/home";
|
import Home from "@/routes/app/home";
|
||||||
import ChatScreen from "@tensamin/chat/screen";
|
import ChatScreen from "@tensamin/chat/screen";
|
||||||
|
|
@ -23,15 +23,16 @@ import Login from "@/routes/screens/login";
|
||||||
|
|
||||||
import CallPopout from "@tensamin/call/popout";
|
import CallPopout from "@tensamin/call/popout";
|
||||||
import ChatContext from "@tensamin/chat/context";
|
import ChatContext from "@tensamin/chat/context";
|
||||||
import { useInitializeCall } from "@tensamin/call/store";
|
import { useCall, useInitializeCall } from "@tensamin/call/store";
|
||||||
import { Provider as TTPProvider } from "@tensamin/ttp";
|
import { useIsSpeaking } from "@tensamin/call/speakingState";
|
||||||
import UserContext from "@tensamin/user/context";
|
import { Provider as MTPProvider } from "@tensamin/mtp";
|
||||||
|
import UserProvider from "@tensamin/user/context";
|
||||||
import DeeplinkContext from "@tensamin/tauri/deeplinkHandler";
|
import DeeplinkContext from "@tensamin/tauri/deeplinkHandler";
|
||||||
import NotificationsProvider from "@tensamin/notifications/context";
|
import NotificationsProvider from "@tensamin/notifications/context";
|
||||||
|
|
||||||
import TAuthWrapper from "@tensamin/tauth/context";
|
import TAuthWrapper from "@tensamin/tauth/context";
|
||||||
|
|
||||||
import { ThemeProvider, useTheme } from "@tensamin/ui";
|
import { ErrorScreen, ThemeProvider, useTheme } from "@tensamin/ui";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import { useEffect, useRef, useState, type ReactNode } from "react";
|
import { useEffect, useRef, useState, type ReactNode } from "react";
|
||||||
|
|
@ -40,8 +41,10 @@ import Storage from "@tensamin/storage/context";
|
||||||
import Session from "@tensamin/storage/session";
|
import Session from "@tensamin/storage/session";
|
||||||
import Crypto from "@tensamin/crypto/context";
|
import Crypto from "@tensamin/crypto/context";
|
||||||
import DesktopMediaProvider from "@tensamin/shared/desktopMedia";
|
import DesktopMediaProvider from "@tensamin/shared/desktopMedia";
|
||||||
|
import { log } from "@tensamin/shared/log";
|
||||||
|
|
||||||
import LegalWrapper from "@/features/legal/screen";
|
import LegalWrapper from "@/features/legal/screen";
|
||||||
|
import CacheSync from "@tensamin/cache/sync";
|
||||||
|
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import { useLocation, useNavigate } from "@tanstack/react-router";
|
import { useLocation, useNavigate } from "@tanstack/react-router";
|
||||||
|
|
@ -70,34 +73,39 @@ window.setLogLevelToMax = () => {
|
||||||
function LoginWrapper({ children }: { children: ReactNode }) {
|
function LoginWrapper({ children }: { children: ReactNode }) {
|
||||||
const [loggedIn, setLoggedIn] = useState<boolean | null>(null);
|
const [loggedIn, setLoggedIn] = useState<boolean | null>(null);
|
||||||
|
|
||||||
const { load } = useStorage();
|
const { load, secureStorage } = useStorage();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (secureStorage === null) return;
|
||||||
let active = true;
|
let active = true;
|
||||||
|
|
||||||
load("user_id").then((userId) => {
|
Promise.all([load("user_id"), load("mtp_keyring")])
|
||||||
if (!active) {
|
.then(([userId, keyring]) => {
|
||||||
return;
|
if (!active) return;
|
||||||
}
|
if (userId !== 0 && keyring !== "") {
|
||||||
|
setLoggedIn(true);
|
||||||
if (userId !== 0) {
|
if (location.pathname === "/login") {
|
||||||
setLoggedIn(true);
|
void navigate({ to: "/", replace: true });
|
||||||
return;
|
}
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
setLoggedIn(false);
|
setLoggedIn(false);
|
||||||
navigate({
|
void navigate({
|
||||||
to: "/login",
|
to: "/login",
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
log(0, "login", "red", "Failed to load login state", error);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
}, [load, navigate]);
|
}, [load, location.pathname, navigate, secureStorage]);
|
||||||
|
|
||||||
if (loggedIn !== true && location.pathname !== "/login") {
|
if (loggedIn !== true && location.pathname !== "/login") {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -119,6 +127,10 @@ function ThemeStorageBridge() {
|
||||||
setThemePolarity,
|
setThemePolarity,
|
||||||
themeTint,
|
themeTint,
|
||||||
setThemeTint,
|
setThemeTint,
|
||||||
|
themeBorderRadius,
|
||||||
|
setThemeBorderRadius,
|
||||||
|
themeCustomCss,
|
||||||
|
setThemeCustomCss,
|
||||||
} = useTheme();
|
} = useTheme();
|
||||||
const loadedRef = useRef(false);
|
const loadedRef = useRef(false);
|
||||||
|
|
||||||
|
|
@ -131,25 +143,41 @@ function ThemeStorageBridge() {
|
||||||
load("theme_primary_color"),
|
load("theme_primary_color"),
|
||||||
load("theme_polarity"),
|
load("theme_polarity"),
|
||||||
load("theme_tint"),
|
load("theme_tint"),
|
||||||
]).then(([color, palette, primaryColor, polarity, tint]) => {
|
load("theme_border_radius"),
|
||||||
if (!active) {
|
load("theme_custom_css"),
|
||||||
return;
|
]).then(
|
||||||
}
|
([
|
||||||
|
color,
|
||||||
|
palette,
|
||||||
|
primaryColor,
|
||||||
|
polarity,
|
||||||
|
tint,
|
||||||
|
borderRadius,
|
||||||
|
customCss,
|
||||||
|
]) => {
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setThemeColor(color);
|
setThemeColor(color);
|
||||||
setThemePalette(palette);
|
setThemePalette(palette);
|
||||||
setThemePrimaryColor(primaryColor);
|
setThemePrimaryColor(primaryColor);
|
||||||
setThemePolarity(polarity);
|
setThemePolarity(polarity);
|
||||||
setThemeTint(tint);
|
setThemeTint(tint);
|
||||||
loadedRef.current = true;
|
setThemeBorderRadius(borderRadius);
|
||||||
});
|
setThemeCustomCss(customCss);
|
||||||
|
loadedRef.current = true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
active = false;
|
active = false;
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
load,
|
load,
|
||||||
|
setThemeBorderRadius,
|
||||||
setThemeColor,
|
setThemeColor,
|
||||||
|
setThemeCustomCss,
|
||||||
setThemePalette,
|
setThemePalette,
|
||||||
setThemePolarity,
|
setThemePolarity,
|
||||||
setThemePrimaryColor,
|
setThemePrimaryColor,
|
||||||
|
|
@ -157,45 +185,33 @@ function ThemeStorageBridge() {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadedRef.current) {
|
if (loadedRef.current) save("theme_color", themeColor);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
save("theme_color", themeColor);
|
|
||||||
}, [save, themeColor]);
|
}, [save, themeColor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadedRef.current) {
|
if (loadedRef.current) save("theme_palette", themePalette);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
save("theme_palette", themePalette);
|
|
||||||
}, [save, themePalette]);
|
}, [save, themePalette]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadedRef.current) {
|
if (loadedRef.current) save("theme_primary_color", themePrimaryColor);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
save("theme_primary_color", themePrimaryColor);
|
|
||||||
}, [save, themePrimaryColor]);
|
}, [save, themePrimaryColor]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadedRef.current) {
|
if (loadedRef.current) save("theme_polarity", themePolarity);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
save("theme_polarity", themePolarity);
|
|
||||||
}, [save, themePolarity]);
|
}, [save, themePolarity]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadedRef.current) {
|
if (loadedRef.current) save("theme_tint", themeTint);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
save("theme_tint", themeTint);
|
|
||||||
}, [save, themeTint]);
|
}, [save, themeTint]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loadedRef.current) save("theme_border_radius", themeBorderRadius);
|
||||||
|
}, [save, themeBorderRadius]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loadedRef.current) save("theme_custom_css", themeCustomCss);
|
||||||
|
}, [save, themeCustomCss]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,6 +226,8 @@ function RootShell() {
|
||||||
paletteStorageKey={null}
|
paletteStorageKey={null}
|
||||||
primaryColorStorageKey={null}
|
primaryColorStorageKey={null}
|
||||||
tintStorageKey={null}
|
tintStorageKey={null}
|
||||||
|
borderRadiusStorageKey={null}
|
||||||
|
customCssStorageKey={null}
|
||||||
>
|
>
|
||||||
<div className="w-screen h-dvh overflow-hidden">
|
<div className="w-screen h-dvh overflow-hidden">
|
||||||
<Toaster
|
<Toaster
|
||||||
|
|
@ -244,9 +262,10 @@ function RootShell() {
|
||||||
|
|
||||||
function AppShell() {
|
function AppShell() {
|
||||||
return (
|
return (
|
||||||
<TTPProvider>
|
<MTPProvider>
|
||||||
|
<CacheSync />
|
||||||
<Session>
|
<Session>
|
||||||
<UserContext>
|
<UserProvider>
|
||||||
<CallInit />
|
<CallInit />
|
||||||
<CallPopout />
|
<CallPopout />
|
||||||
<TAuthWrapper>
|
<TAuthWrapper>
|
||||||
|
|
@ -258,18 +277,110 @@ function AppShell() {
|
||||||
</ChatContext>
|
</ChatContext>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</TAuthWrapper>
|
</TAuthWrapper>
|
||||||
</UserContext>
|
</UserProvider>
|
||||||
</Session>
|
</Session>
|
||||||
</TTPProvider>
|
</MTPProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createCallTrayIcon(color: string, speaking: boolean) {
|
||||||
|
const canvas = document.createElement("canvas");
|
||||||
|
canvas.width = 32;
|
||||||
|
canvas.height = 32;
|
||||||
|
|
||||||
|
const context = canvas.getContext("2d");
|
||||||
|
if (!context) return undefined;
|
||||||
|
|
||||||
|
context.globalAlpha = speaking ? 1 : 0.55;
|
||||||
|
context.fillStyle = color;
|
||||||
|
context.beginPath();
|
||||||
|
context.arc(16, 16, 13, 0, Math.PI * 2);
|
||||||
|
context.fill();
|
||||||
|
|
||||||
|
if (speaking) {
|
||||||
|
context.globalAlpha = 0.3;
|
||||||
|
context.fillStyle = "#ffffff";
|
||||||
|
context.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvas.toDataURL("image/png");
|
||||||
|
}
|
||||||
|
|
||||||
function CallInit() {
|
function CallInit() {
|
||||||
return useInitializeCall();
|
useInitializeCall();
|
||||||
|
|
||||||
|
const { load } = useStorage();
|
||||||
|
const {
|
||||||
|
themeColor,
|
||||||
|
themePalette,
|
||||||
|
themePrimaryColor,
|
||||||
|
themePolarity,
|
||||||
|
themeTint,
|
||||||
|
themeCustomCss,
|
||||||
|
} = useTheme();
|
||||||
|
const [localUserId, setLocalUserId] = useState(-1);
|
||||||
|
const [primaryColor, setPrimaryColor] = useState("");
|
||||||
|
const inCall = useCall((state) => state.state === "open");
|
||||||
|
const speaking = useIsSpeaking(localUserId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
|
||||||
|
load("user_id").then((userId) => {
|
||||||
|
if (active) setLocalUserId(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const frame = requestAnimationFrame(() => {
|
||||||
|
setPrimaryColor(
|
||||||
|
getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue("--primary")
|
||||||
|
.trim(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => cancelAnimationFrame(frame);
|
||||||
|
}, [
|
||||||
|
themeColor,
|
||||||
|
themeCustomCss,
|
||||||
|
themePalette,
|
||||||
|
themePolarity,
|
||||||
|
themePrimaryColor,
|
||||||
|
themeTint,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const iconDataUrl = primaryColor
|
||||||
|
? createCallTrayIcon(primaryColor, speaking)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
void window.tensaminDesktop?.call
|
||||||
|
?.setStatus?.({
|
||||||
|
inCall,
|
||||||
|
speaking: inCall && speaking,
|
||||||
|
iconDataUrl: inCall ? iconDataUrl : undefined,
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.error("Failed to update desktop call status", error);
|
||||||
|
});
|
||||||
|
}, [inCall, primaryColor, speaking]);
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootRoute = createRootRoute({
|
const rootRoute = createRootRoute({
|
||||||
component: RootShell,
|
component: RootShell,
|
||||||
|
errorComponent: ({ error }: { error: Error }) => (
|
||||||
|
<ErrorScreen
|
||||||
|
description={error.message}
|
||||||
|
error={"Unknown Error: " + error.name}
|
||||||
|
/>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const appRoute = createRoute({
|
const appRoute = createRoute({
|
||||||
|
|
@ -279,47 +390,7 @@ const appRoute = createRoute({
|
||||||
notFoundComponent: NotFound,
|
notFoundComponent: NotFound,
|
||||||
});
|
});
|
||||||
|
|
||||||
const settingsRoute = createRoute({
|
const settingsRoute = createSettingsRoute(appRoute);
|
||||||
getParentRoute: () => appRoute,
|
|
||||||
path: "settings",
|
|
||||||
component: SettingsLayout,
|
|
||||||
staticData: {
|
|
||||||
showMobileNavbar: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
type SettingsRouteModule = {
|
|
||||||
default?: () => React.JSX.Element;
|
|
||||||
component?: () => React.JSX.Element;
|
|
||||||
};
|
|
||||||
|
|
||||||
const settingsRouteModules = import.meta.glob<SettingsRouteModule>(
|
|
||||||
"./routes/settings/*.tsx",
|
|
||||||
{ eager: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
const settingsChildren = Object.entries(settingsRouteModules).map(
|
|
||||||
([filePath, module]) => {
|
|
||||||
const fileName = filePath.split("/").pop()?.replace(".tsx", "") ?? "";
|
|
||||||
const path = fileName === "index" ? "/" : fileName;
|
|
||||||
const component = module.component ?? module.default;
|
|
||||||
|
|
||||||
if (!component) {
|
|
||||||
throw new Error(
|
|
||||||
`Settings route module "${filePath}" must export a default component`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return createRoute({
|
|
||||||
getParentRoute: () => settingsRoute,
|
|
||||||
path,
|
|
||||||
component,
|
|
||||||
staticData: {
|
|
||||||
showMobileNavbar: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const homeRoute = createRoute({
|
const homeRoute = createRoute({
|
||||||
getParentRoute: () => appRoute,
|
getParentRoute: () => appRoute,
|
||||||
|
|
@ -361,12 +432,7 @@ const loginRoute = createRoute({
|
||||||
});
|
});
|
||||||
|
|
||||||
const routeTree = rootRoute.addChildren([
|
const routeTree = rootRoute.addChildren([
|
||||||
appRoute.addChildren([
|
appRoute.addChildren([homeRoute, chatRoute, callRoute, settingsRoute]),
|
||||||
homeRoute,
|
|
||||||
chatRoute,
|
|
||||||
callRoute,
|
|
||||||
settingsRoute.addChildren(settingsChildren),
|
|
||||||
]),
|
|
||||||
loginRoute,
|
loginRoute,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,27 +11,47 @@ import {
|
||||||
useIsMobile,
|
useIsMobile,
|
||||||
} from "@tensamin/ui";
|
} from "@tensamin/ui";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { useTTP } from "@tensamin/ttp";
|
import { useMTP } from "@tensamin/mtp";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { isTauri } from "@tauri-apps/api/core";
|
import { isTauri } from "@tauri-apps/api/core";
|
||||||
import { useSession } from "@tensamin/storage/session";
|
import { useSession } from "@tensamin/storage/session";
|
||||||
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
|
import { ShieldAlert } from "lucide-react";
|
||||||
|
|
||||||
// The page
|
// The page
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const { secureStorage } = useStorage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`px-3 flex gap-2 ${!(isTauri() && isMobile) && "pt-3"}`}>
|
<div
|
||||||
<AddConversationButton />
|
className={`px-3 flex flex-col gap-3 ${!(isTauri() && isMobile) && "pt-3"}`}
|
||||||
<Button disabled>Add Community</Button>
|
>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<AddConversationButton />
|
||||||
|
<Button disabled>Add Community</Button>
|
||||||
|
</div>
|
||||||
|
{secureStorage && !secureStorage.secure && (
|
||||||
|
<div className="flex max-w-2xl gap-3 rounded-lg border border-(--destructive)/60 bg-(--destructive)/10 p-3 text-sm">
|
||||||
|
<ShieldAlert className="mt-0.5 size-5 shrink-0 text-destructive" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">Secure storage is unavailable</p>
|
||||||
|
<p>{secureStorage.reason}</p>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Your keyring and cached messages will get saved in regular
|
||||||
|
storage.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Conversation Button Component
|
// Add Conversation Button Component
|
||||||
function AddConversationButton() {
|
function AddConversationButton() {
|
||||||
const { send } = useTTP();
|
const { send } = useMTP();
|
||||||
const { contacts, insertContact } = useSession();
|
const { contacts, insertContact } = useSession();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
@ -55,11 +75,11 @@ function AddConversationButton() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// user existence check
|
// user existence check
|
||||||
const user = await send("get_user_data", {
|
const user = await send("GetUserData", {
|
||||||
username: result.data,
|
Username: result.data,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.data.user_id === 0) {
|
if (data.data.UserId === 0) {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,7 +92,7 @@ function AddConversationButton() {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
// alrady added check
|
// alrady added check
|
||||||
if (contacts.some((contact) => contact.user_id === user.data.user_id)) {
|
if (contacts.some((contact) => contact.UserId === user.data.UserId)) {
|
||||||
setError("Conversation already exists");
|
setError("Conversation already exists");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -80,11 +100,11 @@ function AddConversationButton() {
|
||||||
// add the conv
|
// add the conv
|
||||||
const timeout = setTimeout(() => setLoading(true), 500);
|
const timeout = setTimeout(() => setLoading(true), 500);
|
||||||
|
|
||||||
send("add_conversation", {
|
send("AddConversation", {
|
||||||
chat_partner_name: result.data,
|
ChatPartnerName: result.data,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
insertContact(user.data.user_id);
|
insertContact(user.data.UserId);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardTitle,
|
|
||||||
CardHeader,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
Button,
|
|
||||||
Badge,
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@tensamin/ui";
|
|
||||||
import {
|
|
||||||
packages,
|
|
||||||
packageCount,
|
|
||||||
generatedAt,
|
|
||||||
} from "../../../../../licenses/third-party-credits.json";
|
|
||||||
|
|
||||||
const licenseTexts = import.meta.glob("../../../../../licenses/**/*", {
|
|
||||||
eager: true,
|
|
||||||
import: "default",
|
|
||||||
query: "?raw",
|
|
||||||
}) as Record<string, string>;
|
|
||||||
|
|
||||||
function getLicenseFiles(licensePackage: (typeof packages)[number]) {
|
|
||||||
return licensePackage.files.map((fileName) => {
|
|
||||||
const path =
|
|
||||||
"../../../../../" + licensePackage.licenseFolder + "/" + fileName;
|
|
||||||
|
|
||||||
return {
|
|
||||||
fileName,
|
|
||||||
text: licenseTexts[path],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<div className="flex h-full min-h-0 flex-col gap-7">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<p>Last generated: {generatedAt}</p>
|
|
||||||
<p>Package Count: {packageCount}</p>
|
|
||||||
</div>
|
|
||||||
<div className="min-h-0 flex-1 max-h-[calc(100vh-180px)] overflow-auto pr-2">
|
|
||||||
<div className="flex flex-col gap-5">
|
|
||||||
{packages.map((licensePackage) => (
|
|
||||||
<Card
|
|
||||||
key={licensePackage.name + licensePackage.version}
|
|
||||||
id={licensePackage.name + licensePackage.version}
|
|
||||||
>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex gap-2 items-center">
|
|
||||||
<Badge>{licensePackage.license}</Badge> {licensePackage.name}{" "}
|
|
||||||
{licensePackage.version}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
{licensePackage.description && (
|
|
||||||
<CardContent>
|
|
||||||
<CardDescription>
|
|
||||||
{licensePackage.description}
|
|
||||||
</CardDescription>
|
|
||||||
</CardContent>
|
|
||||||
)}
|
|
||||||
<CardFooter className="gap-2">
|
|
||||||
<LicenseDialog licensePackage={licensePackage} />
|
|
||||||
{licensePackage.repository ? (
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
href={licensePackage.repository
|
|
||||||
?.replace("git+", "")
|
|
||||||
.replace(".git", "")}
|
|
||||||
>
|
|
||||||
<Button variant="outline" className="cursor-pointer">
|
|
||||||
Open Repository
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<Button disabled variant="outline" className="cursor-pointer">
|
|
||||||
Open Repository
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{licensePackage.homepage ? (
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
href={licensePackage.homepage}
|
|
||||||
>
|
|
||||||
<Button variant="outline" className="cursor-pointer">
|
|
||||||
Open Homepage
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<Button disabled variant="outline" className="cursor-pointer">
|
|
||||||
Open Homepage
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LicenseDialog({
|
|
||||||
licensePackage,
|
|
||||||
}: {
|
|
||||||
licensePackage: (typeof packages)[number];
|
|
||||||
}) {
|
|
||||||
const licenseFiles = getLicenseFiles(licensePackage);
|
|
||||||
const hasLicenseText = licenseFiles.some(({ text }) => text);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger
|
|
||||||
render={
|
|
||||||
<Button disabled={!hasLicenseText} className="cursor-pointer">
|
|
||||||
Open License
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<DialogContent className="flex max-h-[85vh] min-h-0 flex-col overflow-hidden sm:max-w-3xl">
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain pr-2">
|
|
||||||
{licenseFiles.map(({ fileName, text }, index) => (
|
|
||||||
<section key={fileName} className="border-b last:border-b-0">
|
|
||||||
<h3
|
|
||||||
className={`border-b pb-2 text-sm font-medium ${index >= 1 && "pt-2"}`}
|
|
||||||
>
|
|
||||||
{fileName}
|
|
||||||
</h3>
|
|
||||||
<pre className="pt-2 whitespace-pre-wrap wrap-break-word text-xs leading-relaxed">
|
|
||||||
{text ||
|
|
||||||
"License text could not be loaded. Please contact support@tensamin.net"}
|
|
||||||
</pre>
|
|
||||||
</section>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
Button,
|
|
||||||
cn,
|
|
||||||
Input,
|
|
||||||
useIsMobile,
|
|
||||||
} from "@tensamin/ui";
|
|
||||||
import { useUser, type User } from "@tensamin/user/context";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import MDInput from "@tensamin/markdown/input";
|
|
||||||
import { ttp } from "@tensamin/shared/data";
|
|
||||||
import { useTTP } from "@tensamin/ttp";
|
|
||||||
import { Check } from "lucide-react";
|
|
||||||
|
|
||||||
async function prepImage(
|
|
||||||
file: File,
|
|
||||||
size = 300,
|
|
||||||
quality = 0.8,
|
|
||||||
): Promise<string> {
|
|
||||||
const bitmap = await createImageBitmap(file);
|
|
||||||
|
|
||||||
const canvas = document.createElement("canvas");
|
|
||||||
canvas.width = size;
|
|
||||||
canvas.height = size;
|
|
||||||
|
|
||||||
const ctx = canvas.getContext("2d");
|
|
||||||
if (!ctx) throw new Error("Could not get canvas context");
|
|
||||||
|
|
||||||
const scale = Math.max(size / bitmap.width, size / bitmap.height);
|
|
||||||
const width = bitmap.width * scale;
|
|
||||||
const height = bitmap.height * scale;
|
|
||||||
const x = (size - width) / 2;
|
|
||||||
const y = (size - height) / 2;
|
|
||||||
|
|
||||||
ctx.drawImage(bitmap, x, y, width, height);
|
|
||||||
|
|
||||||
return canvas.toDataURL("image/webp", quality);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
const { get } = useUser();
|
|
||||||
const { load } = useStorage();
|
|
||||||
const { send } = useTTP();
|
|
||||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
|
||||||
const [draftUser, setDraftUser] = useState<Partial<User>>({});
|
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
|
||||||
const [saveSucceeded, setSaveSucceeded] = useState(false);
|
|
||||||
const avatarUploadRef = useRef<HTMLInputElement>(null);
|
|
||||||
const draftInitializedRef = useRef(false);
|
|
||||||
const effectiveAvatar =
|
|
||||||
draftUser.avatar === "none" ? undefined : draftUser.avatar;
|
|
||||||
|
|
||||||
const updateDraftUser = (
|
|
||||||
updater: (previous: Partial<User>) => Partial<User>,
|
|
||||||
) => {
|
|
||||||
setSaveSucceeded(false);
|
|
||||||
setErrorMessage("");
|
|
||||||
setDraftUser(updater);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchUser = async () => {
|
|
||||||
const user = await get(await load("user_id"));
|
|
||||||
setCurrentUser(user);
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchUser();
|
|
||||||
}, [load, get]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!currentUser || draftInitializedRef.current) return;
|
|
||||||
|
|
||||||
setDraftUser(currentUser);
|
|
||||||
draftInitializedRef.current = true;
|
|
||||||
}, [currentUser]);
|
|
||||||
|
|
||||||
const handleAvatarUpload = async (file: File) => {
|
|
||||||
const final = await prepImage(file);
|
|
||||||
updateDraftUser((prev) => ({ ...prev, avatar: final }));
|
|
||||||
if (avatarUploadRef.current) {
|
|
||||||
avatarUploadRef.current.value = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
|
||||||
|
|
||||||
return currentUser ? (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
ref={avatarUploadRef}
|
|
||||||
hidden
|
|
||||||
onChange={(e) =>
|
|
||||||
e.target.files?.[0] && handleAvatarUpload(e.target.files[0])
|
|
||||||
}
|
|
||||||
type="file"
|
|
||||||
/>
|
|
||||||
<div className={cn("flex flex-col gap-5", isMobile ? "w-full" : "w-80")}>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Avatar className="size-14">
|
|
||||||
<AvatarImage src={effectiveAvatar} />
|
|
||||||
<AvatarFallback className="text-2xl">
|
|
||||||
{draftUser.display?.slice(0, 2).toUpperCase() ||
|
|
||||||
currentUser.display.slice(0, 2).toUpperCase()}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<p>Avatar</p>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Button onClick={() => avatarUploadRef.current?.click()}>
|
|
||||||
Upload avatar
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
updateDraftUser((prev) => {
|
|
||||||
return { ...prev, avatar: "none" };
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
variant="destructive"
|
|
||||||
disabled={effectiveAvatar === undefined}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
GIFs are supported in decentralised mode or with Tensamin Premium.
|
|
||||||
<br />
|
|
||||||
Maximum file size is 16mb.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Input
|
|
||||||
className="w-full"
|
|
||||||
onChange={(event) =>
|
|
||||||
updateDraftUser((prev) => ({
|
|
||||||
...prev,
|
|
||||||
display: event.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
placeholder="Display Name"
|
|
||||||
value={draftUser.display || ""}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
className="w-full"
|
|
||||||
onChange={(event) =>
|
|
||||||
updateDraftUser((prev) => ({
|
|
||||||
...prev,
|
|
||||||
username: event.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
placeholder="Username"
|
|
||||||
value={draftUser.username || ""}
|
|
||||||
/>
|
|
||||||
<MDInput
|
|
||||||
styled
|
|
||||||
paddingY="4px"
|
|
||||||
paddingX="10px"
|
|
||||||
fontSize=".875rem"
|
|
||||||
placeholder="About Me"
|
|
||||||
setValue={(value) =>
|
|
||||||
updateDraftUser((prev) => ({ ...prev, about: value }))
|
|
||||||
}
|
|
||||||
value={draftUser.about || ""}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
const { avatar, ...draftUsersWithoutAvatar } = draftUser;
|
|
||||||
const payload = {
|
|
||||||
...draftUsersWithoutAvatar,
|
|
||||||
...(typeof avatar === "string"
|
|
||||||
? {
|
|
||||||
avatar: avatar.startsWith("data:")
|
|
||||||
? (avatar.split(",", 2)[1] ?? "")
|
|
||||||
: avatar,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const validation = ttp.change_user_data.request.safeParse(payload);
|
|
||||||
|
|
||||||
if (!validation.success) {
|
|
||||||
setSaveSucceeded(false);
|
|
||||||
setErrorMessage(
|
|
||||||
validation.error.issues[0]?.message ?? "Invalid profile data",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await send("change_user_data", validation.data);
|
|
||||||
setSaveSucceeded(true);
|
|
||||||
setErrorMessage("");
|
|
||||||
} catch (err) {
|
|
||||||
setSaveSucceeded(false);
|
|
||||||
setErrorMessage("Failed to update profile: " + err);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{saveSucceeded ? (
|
|
||||||
<span className="inline-flex items-center gap-1.5">
|
|
||||||
<Check className="size-4" />
|
|
||||||
Saved
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
"Save"
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="text-sm text-destructive">{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p>Loading...</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import QRCode from "qrcode";
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogAction,
|
|
||||||
AlertDialogCancel,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogFooter,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
Button,
|
|
||||||
} from "@tensamin/ui";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<QrCodeLogin />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// QR Code Login
|
|
||||||
const generateQR = async (text: string): Promise<string> => {
|
|
||||||
try {
|
|
||||||
const url = await QRCode.toDataURL(text, {
|
|
||||||
errorCorrectionLevel: "H",
|
|
||||||
margin: 1,
|
|
||||||
color: {
|
|
||||||
dark: "#000000FF",
|
|
||||||
light: "#FFFFFFFF",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return url;
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
function QrCodeLogin() {
|
|
||||||
const { load } = useStorage();
|
|
||||||
const [userId, setUserId] = useState<number>(0);
|
|
||||||
const [privateKey, setPrivateKey] = useState<string>("");
|
|
||||||
const [qrCodeBase64, setQrCodeBase64] = useState<string | undefined>(
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
const [connectionString, setConnectionString] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load("private_key").then((value) => {
|
|
||||||
if (value) {
|
|
||||||
setPrivateKey(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
load("user_id").then((value) => {
|
|
||||||
if (value) {
|
|
||||||
setUserId(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
load("ttp_url")
|
|
||||||
.then((value) => {
|
|
||||||
if (value) {
|
|
||||||
const url = new URL(value);
|
|
||||||
setConnectionString(`@${url.host}`);
|
|
||||||
} else {
|
|
||||||
setConnectionString("");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => setConnectionString(""));
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (userId && privateKey && connectionString !== null) {
|
|
||||||
generateQR(
|
|
||||||
`tensamin://tu::${userId}${connectionString}::${privateKey}`,
|
|
||||||
).then(setQrCodeBase64);
|
|
||||||
}
|
|
||||||
}, [userId, privateKey, connectionString]);
|
|
||||||
|
|
||||||
const [qrCodeVisible, setQrCodeVisible] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-1 pl-2">
|
|
||||||
<p>Login QR Code</p>
|
|
||||||
<div className="relative rounded-lg w-50 border-3 aspect-square overflow-hidden">
|
|
||||||
{qrCodeBase64 && (
|
|
||||||
<img className="w-full h-full" src={qrCodeBase64} alt="QR Code" />
|
|
||||||
)}
|
|
||||||
{!qrCodeVisible && (
|
|
||||||
<>
|
|
||||||
<div className="absolute top-0 left-0 w-full h-full backdrop-blur-sm bg-background/50" />
|
|
||||||
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger render={<Button>Show QR Code</Button>} />
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
Exposing this QR code is the same as sharing your .tu
|
|
||||||
file. Since changing your private key is quite tedious,
|
|
||||||
only do this when you're confident the key won't be
|
|
||||||
compromised.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
<AlertDialogFooter>
|
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
||||||
<AlertDialogAction
|
|
||||||
render={
|
|
||||||
<Button onClick={() => setQrCodeVisible(true)}>
|
|
||||||
Show QR Code
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</AlertDialogFooter>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import { StylePicker } from "@tensamin/ui";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return <StylePicker />;
|
|
||||||
}
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
|
||||||
import { defineConfig, type Plugin } from "vite";
|
import { defineConfig, type Plugin } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { mtp } from "mtp/vite";
|
||||||
|
|
||||||
const host = process.env.TAURI_DEV_HOST;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
const appDir = dirname(fileURLToPath(import.meta.url));
|
const appDir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
@ -74,6 +75,22 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
dedupe: [
|
dedupe: [
|
||||||
|
"react",
|
||||||
|
"react-dom",
|
||||||
|
"react-redux",
|
||||||
|
"use-sync-external-store",
|
||||||
|
"@tanstack/history",
|
||||||
|
"@tanstack/react-router",
|
||||||
|
"@tanstack/react-store",
|
||||||
|
"@tanstack/router-core",
|
||||||
|
"@tanstack/store",
|
||||||
|
"@tensamin/crypto",
|
||||||
|
"@tensamin/settings",
|
||||||
|
"@tensamin/storage",
|
||||||
|
"@tensamin/mtp",
|
||||||
|
"@tensamin/user",
|
||||||
|
"@tensamin/tauri",
|
||||||
|
"@tensamin/chat",
|
||||||
"@codemirror/commands",
|
"@codemirror/commands",
|
||||||
"@codemirror/lang-markdown",
|
"@codemirror/lang-markdown",
|
||||||
"@codemirror/state",
|
"@codemirror/state",
|
||||||
|
|
@ -93,11 +110,42 @@ export default defineConfig({
|
||||||
: undefined,
|
: undefined,
|
||||||
watch: {
|
watch: {
|
||||||
ignored: ["**/src-tauri/**"],
|
ignored: ["**/src-tauri/**"],
|
||||||
|
usePolling: true,
|
||||||
|
interval: 100,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
envPrefix: ["VITE_", "TAURI_ENV_*"],
|
envPrefix: ["VITE_", "TAURI_ENV_*"],
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
exclude: ["@tensamin/ttp-core"],
|
include: [
|
||||||
|
"react-redux",
|
||||||
|
"use-sync-external-store/shim/with-selector",
|
||||||
|
"use-sync-external-store/with-selector",
|
||||||
|
"decimal.js-light",
|
||||||
|
"eventemitter3",
|
||||||
|
"react-is",
|
||||||
|
],
|
||||||
|
exclude: [
|
||||||
|
"mtp",
|
||||||
|
"mtp/raw",
|
||||||
|
"mtp/type-map",
|
||||||
|
"@tensamin/call",
|
||||||
|
"@tensamin/call/utils",
|
||||||
|
"@tensamin/chat",
|
||||||
|
"@tensamin/crypto",
|
||||||
|
"@tensamin/crypto/context",
|
||||||
|
"@tensamin/markdown",
|
||||||
|
"@tensamin/mtp",
|
||||||
|
"@tensamin/notifications",
|
||||||
|
"@tensamin/shared",
|
||||||
|
"@tensamin/shared/data",
|
||||||
|
"@tensamin/shared/log",
|
||||||
|
"@tensamin/settings",
|
||||||
|
"@tensamin/storage",
|
||||||
|
"@tensamin/storage/context",
|
||||||
|
"@tensamin/tauri",
|
||||||
|
"@tensamin/tauth",
|
||||||
|
"@tensamin/user",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
|
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
|
||||||
|
|
@ -105,6 +153,18 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
deepFilterAssetHeaders(resolve(appDir, "public")),
|
deepFilterAssetHeaders(resolve(appDir, "public")),
|
||||||
|
mtp({ typeMaps: resolve(appDir, "../../type-maps.yaml") }),
|
||||||
|
{
|
||||||
|
name: "workspace-realpath-resolution",
|
||||||
|
enforce: "post",
|
||||||
|
config() {
|
||||||
|
return {
|
||||||
|
resolve: {
|
||||||
|
preserveSymlinks: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
react(),
|
react(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
48
flake.lock
generated
|
|
@ -2,22 +2,58 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1779508470,
|
"lastModified": 1782467914,
|
||||||
"narHash": "sha256-Ap9KJX+5xHIn3bPIpfNgT6MEXdAECECwo4/rmlQD74M=",
|
"narHash": "sha256-pGvFkM8N0xEkIIXDe5YYfbEAvHrk4IxBrjB/x8OomhE=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "29916453413845e54a65b8a1cf996842300cd299",
|
"rev": "e73de5be04e0eff4190a1432b946d469c794e7b4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"id": "nixpkgs",
|
"owner": "NixOS",
|
||||||
"ref": "nixos-unstable",
|
"ref": "nixos-unstable",
|
||||||
"type": "indirect"
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1744536153,
|
||||||
|
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1782616745,
|
||||||
|
"narHash": "sha256-NN5B1cKBXF6h1Ec681gMGZ2o/99d7vKXAAfFNEvyOKA=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "b916d014dc57eb555f84e51172a82f7f6fb560d7",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||