Compare commits

...
Author SHA1 Message Date
19ef13f068 (qol): update todo
All checks were successful
/ build-web (push) Successful in 7m27s
/ build-desktop (linux) (push) Successful in 12m7s
/ build-mobile (push) Successful in 19m20s
/ release (push) Successful in 3m3s
2026-07-11 14:54:04 +02:00
7ba08090fe (wip): add reactions
Some checks failed
/ release (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
(qol): update todo
2026-07-11 14:53:12 +02:00
d57940d1b3 (feat): chat improvements
All checks were successful
/ build-web (push) Successful in 7m15s
/ build-desktop (linux) (push) Successful in 11m58s
/ build-mobile (push) Successful in 19m10s
/ release (push) Successful in 2m59s
2026-07-11 12:31:17 +02:00
c11589079b (feat): add message deletions
All checks were successful
/ build-web (push) Successful in 7m26s
/ build-desktop (linux) (push) Successful in 12m17s
/ build-mobile (push) Successful in 19m18s
/ release (push) Successful in 3m0s
2026-07-11 11:25:45 +02:00
943203eeaf (feat): improve tray icon
All checks were successful
/ build-web (push) Successful in 7m20s
/ build-desktop (linux) (push) Successful in 11m59s
/ build-mobile (push) Successful in 19m4s
/ release (push) Successful in 3m0s
(qol): update todo
2026-07-11 01:45:26 +02:00
92e5003989 (feat): improve security page
All checks were successful
/ build-web (push) Successful in 6m51s
/ build-desktop (linux) (push) Successful in 11m48s
/ build-mobile (push) Successful in 19m5s
/ release (push) Successful in 2m54s
(feat): add missing ReplyId type
2026-07-07 23:30:40 +02:00
a08c41067e (feat): unify message patching into a single interface
All checks were successful
/ build-web (push) Successful in 7m11s
/ build-desktop (linux) (push) Successful in 11m52s
/ build-mobile (push) Successful in 19m27s
/ release (push) Successful in 2m53s
2026-07-07 18:21:52 +02:00
8dd73568ce (feat): add message editing
Some checks failed
/ build-web (push) Successful in 6m58s
/ release (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ build-mobile (push) Has been cancelled
(qol): update todos
2026-07-07 18:13:43 +02:00
f61a77af31 (fix): workflows
All checks were successful
/ build-web (push) Successful in 6m58s
/ build-desktop (linux) (push) Successful in 11m40s
/ build-mobile (push) Successful in 18m38s
/ release (push) Successful in 2m53s
2026-07-06 23:50:48 +02:00
0be502cc2e (fix): workflows
Some checks failed
/ build-web (push) Successful in 7m17s
/ build-desktop (linux) (push) Failing after 7m46s
/ build-mobile (push) Successful in 18m5s
/ release (push) Has been skipped
2026-07-06 23:29:53 +02:00
d9bfdb655d (fix): hot-reloading not working
Some checks failed
/ release (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
(fix): other stuff
2026-07-06 23:25:48 +02:00
e0843a11a6 (feat): add replys
Some checks failed
/ build-web (push) Successful in 6m31s
/ build-desktop (linux) (push) Failing after 7m9s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
(feat): move to SenderId
(feat): other chat improvements
2026-07-06 23:08:55 +02:00
95a79892c6 (tmp): disable tauth
Some checks failed
/ build-web (push) Successful in 6m57s
/ build-desktop (linux) (push) Failing after 7m32s
/ build-mobile (push) Successful in 18m13s
/ release (push) Has been skipped
2026-07-06 21:53:06 +02:00
76b64886da (fix): calls
Some checks failed
/ build-desktop (linux) (push) Failing after 5m3s
/ build-web (push) Failing after 5m8s
/ build-mobile (push) Failing after 7m1s
/ release (push) Has been skipped
(fix): deepfilternet3
(feat): remove noise gate logs
(qol): update todo
2026-07-06 21:45:57 +02:00
92dfea7329 Merge branch 'dev' (resolve conflicts in store.tsx)
Some checks failed
/ build-desktop (linux) (push) Failing after 4m52s
/ build-web (push) Failing after 4m56s
/ build-mobile (push) Failing after 7m1s
/ release (push) Has been skipped
2026-07-06 20:04:50 +02:00
27f08e92e5 (wip): call migration stuff 2026-07-06 20:02:21 +02:00
b4fe5edfbb (feat): more crypto migration
Some checks failed
/ build-web (push) Failing after 5m13s
/ build-desktop (linux) (push) Failing after 5m16s
/ build-mobile (push) Failing after 7m3s
/ release (push) Has been skipped
2026-07-06 19:39:23 +02:00
2cd326d0b1 (feat): finish chat crypto migration
Some checks failed
/ build-web (push) Failing after 4m36s
/ build-desktop (linux) (push) Failing after 4m47s
/ build-mobile (push) Failing after 6m34s
/ release (push) Has been skipped
(wip): prep for full ECDH repo migration
2026-07-06 19:23:37 +02:00
ecca5241bc (feat): improve some ui
Some checks failed
/ build-web (push) Successful in 6m28s
/ build-desktop (linux) (push) Failing after 7m1s
/ build-mobile (push) Successful in 17m53s
/ release (push) Has been skipped
(qol): update todo
2026-07-06 18:15:18 +02:00
8ddc8db536 (wip): fix chat crypto
Some checks failed
/ build-web (push) Successful in 7m21s
/ build-desktop (linux) (push) Failing after 7m49s
/ build-mobile (push) Successful in 20m5s
/ release (push) Has been skipped
2026-07-06 17:52:41 +02:00
35414cdfcb (fix): double decryption
Some checks failed
/ build-web (push) Successful in 6m30s
/ build-desktop (linux) (push) Failing after 7m14s
/ build-mobile (push) Successful in 17m51s
/ release (push) Has been skipped
2026-07-06 12:50:05 +02:00
cabe83aa98 (feat): improve chat ui
Some checks failed
/ build-web (push) Successful in 7m3s
/ build-desktop (linux) (push) Failing after 7m41s
/ build-mobile (push) Successful in 18m22s
/ release (push) Has been skipped
2026-07-06 11:20:32 +02:00
de0a153bce (wip): chat decryption stuff
Some checks failed
/ build-web (push) Successful in 6m57s
/ build-desktop (linux) (push) Failing after 7m41s
/ build-mobile (push) Successful in 17m43s
/ release (push) Has been skipped
2026-07-06 00:42:54 +02:00
2777ba34ca (feat): more crypto migration
Some checks failed
/ build-web (push) Failing after 5m33s
/ build-desktop (linux) (push) Failing after 5m46s
/ build-mobile (push) Failing after 7m58s
/ release (push) Has been skipped
2026-07-06 00:13:26 +02:00
f3ecb8f3dd (fix): workflows
Some checks failed
/ build-desktop (linux) (push) Failing after 4m16s
/ build-web (push) Failing after 4m27s
/ build-mobile (push) Failing after 7m4s
/ release (push) Has been skipped
(feat): update mtp
2026-07-05 22:19:03 +02:00
7d6080edbf (fix): stuff
Some checks failed
/ build-web (push) Successful in 6m20s
/ build-desktop (linux) (push) Failing after 7m0s
/ build-mobile (push) Failing after 11m21s
/ release (push) Has been skipped
2026-07-05 21:57:04 +02:00
cd2c2f8167 (feat): crypto migrations
Some checks failed
/ build-web (push) Failing after 6m47s
/ build-desktop (linux) (push) Failing after 7m0s
/ build-mobile (push) Failing after 9m3s
/ release (push) Has been skipped
2026-07-05 21:45:44 +02:00
930663d495 (feat): migrate ttp to mtp
Some checks failed
/ build-desktop (linux) (push) Failing after 3m58s
/ build-web (push) Failing after 4m13s
/ build-mobile (push) Failing after 6m34s
/ release (push) Has been skipped
(wip): crypto migration
2026-07-05 01:43:06 +02:00
4e69b8ef77 (feat): small error screen improvement
Some checks failed
/ build-web (push) Failing after 3m56s
/ build-desktop (linux) (push) Failing after 4m13s
/ build-mobile (push) Failing after 6m43s
/ release (push) Has been skipped
(feat): use correct type maps
(fix): remove sudo from nix flake
(wip): migrate ttp to mtp
2026-07-04 20:26:51 +02:00
fc6d8950d4 (fix): builds
Some checks failed
/ build-web (push) Successful in 6m28s
/ build-desktop (linux) (push) Failing after 7m9s
/ build-mobile (push) Failing after 11m37s
/ release (push) Has been skipped
2026-07-04 01:50:13 +02:00
1b24e7cda1 (fix): integrity stuff
Some checks failed
/ build-web (push) Successful in 6m10s
/ build-desktop (linux) (push) Failing after 7m1s
/ build-mobile (push) Failing after 7m30s
/ release (push) Has been skipped
2026-07-04 01:35:13 +02:00
1621defe34 (fix): build stuff
Some checks failed
/ build-desktop (linux) (push) Failing after 3m32s
/ build-web (push) Failing after 3m33s
/ build-mobile (push) Failing after 6m27s
/ release (push) Has been skipped
2026-07-04 01:28:27 +02:00
82a483d7ab (feat): migrate bun to pnpm
Some checks failed
/ build-web (push) Failing after 3m41s
/ build-desktop (linux) (push) Failing after 3m43s
/ build-mobile (push) Failing after 6m33s
/ release (push) Has been skipped
(feat): some mtp stuff
(wip): electron app tray
2026-07-04 01:18:54 +02:00
69149b73bd (feat): remove mtp loading screen
Some checks failed
/ build-web (push) Failing after 4m35s
/ build-desktop (linux) (push) Failing after 4m37s
/ build-mobile (push) Failing after 6m40s
/ release (push) Has been skipped
(fix): weird dedupe bugs
(qol): update todo
2026-07-03 21:49:13 +02:00
8bff0f503f (fix): desktop uploads in workflow
All checks were successful
/ build-web (push) Successful in 5m47s
/ build-desktop (linux) (push) Successful in 6m52s
/ build-mobile (push) Successful in 20m43s
/ release (push) Successful in 2m44s
2026-07-03 14:39:26 +02:00
25f39ac41d (fix): mobile builds
All checks were successful
/ build-web (push) Successful in 5m38s
/ build-desktop (linux) (push) Successful in 6m34s
/ build-mobile (push) Successful in 18m0s
/ release (push) Successful in 2m42s
2026-07-03 12:14:04 +02:00
4a3be94fef (fix): mobile builds
Some checks failed
/ build-web (push) Successful in 5m45s
/ build-desktop (linux) (push) Successful in 6m45s
/ build-mobile (push) Failing after 9m35s
/ release (push) Has been skipped
2026-07-03 11:41:13 +02:00
7f75a36d73 (wip): migrate ttp to mtp -> snake case to pascal case
Some checks failed
/ build-web (push) Successful in 6m13s
/ build-desktop (linux) (push) Successful in 7m1s
/ build-mobile (push) Failing after 9m20s
/ release (push) Has been skipped
2026-07-03 11:08:38 +02:00
ee5955fc75 (wip): migrate ttp to mtp
(feat): migrate tauth to mtp
(qol): update todo
(qol): add direnv
2026-07-03 10:56:42 +02:00
8e831fd993 (feat): prepare for new ident flow
Some checks failed
/ build-web (push) Failing after 3m21s
/ build-desktop (linux) (push) Failing after 3m37s
/ build-mobile (push) Failing after 11m20s
/ release (push) Has been skipped
(fix): some bugs
2026-07-02 23:56:49 +02:00
20018f09a9 (wip): migrate ttp to mtp
Some checks failed
/ build-web (push) Failing after 3m4s
/ build-desktop (linux) (push) Failing after 3m15s
/ build-mobile (push) Failing after 9m7s
/ release (push) Has been skipped
2026-07-02 21:05:50 +02:00
b1e8af7ec3 (tmp): trigger workflow
All checks were successful
/ build-web (push) Successful in 4m26s
/ build-desktop (linux) (push) Successful in 5m32s
/ build-mobile (push) Successful in 19m43s
/ release (push) Successful in 1m52s
2026-06-28 20:27:08 +02:00
05a94c58f9 (fix): make the flake more robust 2026-06-28 20:26:43 +02:00
eef6c68426 (fix): workflows
Some checks failed
/ build-web (push) Successful in 3m23s
/ build-desktop (linux) (push) Successful in 4m12s
/ build-mobile (push) Failing after 18m40s
/ release (push) Has been skipped
2026-06-28 20:01:55 +02:00
b704e266d6 (fix): workflows
Some checks failed
/ build-web (push) Successful in 4m1s
/ build-desktop (linux) (push) Successful in 5m0s
/ build-mobile (push) Failing after 8m9s
/ release (push) Has been skipped
2026-06-28 19:50:51 +02:00
018571c950 (fix): workflows
Some checks failed
/ build-web (push) Failing after 3s
/ build-desktop (linux) (push) Failing after 3s
/ build-mobile (push) Failing after 3s
/ release (push) Has been skipped
2026-06-28 19:48:40 +02:00
1dd8a979ee (fix): workflows
Some checks failed
/ build-web (push) Failing after 1s
/ build-mobile (push) Failing after 1s
/ build-desktop (linux) (push) Failing after 2s
/ release (push) Has been skipped
2026-06-28 19:47:12 +02:00
70740e31e6 (fix): try workflow fix
Some checks failed
/ build-mobile (push) Failing after 1s
/ build-web (push) Failing after 1s
/ build-desktop (linux) (push) Failing after 1s
/ release (push) Has been skipped
2026-06-28 19:21:21 +02:00
4980ca8f9f (tmp): dubious change to trigger workflow
Some checks failed
/ build-desktop (linux) (push) Failing after 1m55s
/ build-web (push) Failing after 1m58s
/ build-mobile (push) Failing after 5m24s
/ release (push) Has been skipped
2026-06-28 19:14:01 +02:00
692e1da4b0 (feat): add coreutils to dev shell 2026-06-28 19:12:40 +02:00
3c7a28ceb2 Reapply "(fix): now run builds in nix shell"
Some checks failed
/ build-web (push) Failing after 2m19s
/ build-desktop (linux) (push) Failing after 2m7s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
This reverts commit 10da530e9e.
2026-06-28 19:10:23 +02:00
10da530e9e Revert "(fix): now run builds in nix shell"
Some checks failed
/ build-desktop (linux) (push) Has been cancelled
/ release (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
This reverts commit f81235c68d.
2026-06-28 19:09:50 +02:00
f81235c68d (fix): now run builds in nix shell
Some checks failed
/ build-web (push) Failing after 1m53s
/ build-desktop (linux) (push) Failing after 1m56s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-06-28 19:05:22 +02:00
d7aedad9b4 (fix): build issues
Some checks failed
/ build-web (push) Failing after 1m57s
/ build-desktop (linux) (push) Failing after 2m0s
/ build-mobile (push) Failing after 6m2s
/ release (push) Has been skipped
2026-06-28 18:58:55 +02:00
d176de6cb1 (qol): run format
Some checks failed
/ build-desktop (linux) (push) Failing after 1m35s
/ build-web (push) Failing after 1m46s
/ build-mobile (push) Failing after 5m39s
/ release (push) Has been skipped
2026-06-28 18:47:14 +02:00
63abd5697f (fix): nix dev shell 2026-06-28 18:45:58 +02:00
3f2275179b (qol): update readme
Some checks failed
/ build-desktop (linux) (push) Failing after 36s
/ build-web (push) Failing after 36s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-06-28 18:38:13 +02:00
de3150b74b (fix): dep 2026-06-28 18:36:18 +02:00
6df912f428 (feat): merge nix flakes
Some checks failed
/ build-desktop (linux) (push) Failing after 26s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
(fix): workflows
2026-06-28 18:35:11 +02:00
74b152969b (fix): workflows
Some checks failed
/ build-web (push) Failing after 6s
/ build-mobile (push) Failing after 6s
/ build-desktop (linux) (push) Failing after 6s
/ release (push) Has been skipped
2026-06-28 18:03:19 +02:00
e3e90d4779 (feat): update workflows to use nixos runners
Some checks failed
/ build-mobile (push) Failing after 5s
/ build-web (push) Failing after 5s
/ release (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
(wip): add mini hover context menu
(qol): update todo
2026-06-28 18:00:21 +02:00
1828191317 (feat): improve dev releases cleanup
All checks were successful
/ build-web (push) Successful in 1m18s
/ build-desktop (linux) (push) Successful in 6m42s
/ build-mobile (push) Successful in 18m3s
/ release (push) Successful in 30s
2026-06-27 02:20:02 +02:00
de09cf4ebd (feat): make workflows fail early
All checks were successful
/ build-web (push) Successful in 1m20s
/ build-desktop (linux) (push) Successful in 6m49s
/ build-mobile (push) Successful in 17m57s
/ release (push) Successful in 31s
2026-06-27 01:48:21 +02:00
14518978d2 (fix): workflows
Some checks failed
/ release (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
2026-06-27 01:45:19 +02:00
29f1ff2d54 Revert "(fix): workflow"
Some checks failed
/ build-web (push) Successful in 1m26s
/ build-desktop (linux) (push) Successful in 7m32s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
This reverts commit cc2f790c62.
2026-06-27 01:33:20 +02:00
cc2f790c62 (fix): workflow
Some checks failed
/ build-web (push) Successful in 1m28s
/ build-desktop (linux) (push) Successful in 7m1s
/ build-mobile (push) Failing after 14m37s
/ release (push) Has been skipped
2026-06-27 01:11:26 +02:00
0e6b235c1b (feat): minor ui changes
Some checks failed
/ build-web (push) Successful in 1m23s
/ build-desktop (linux) (push) Successful in 6m49s
/ build-mobile (push) Failing after 13m32s
/ release (push) Has been skipped
(qol): update todo
2026-06-27 00:47:26 +02:00
b09ef8a997 (feat): update message state icons
Some checks failed
/ build-web (push) Successful in 1m42s
/ build-desktop (linux) (push) Successful in 7m46s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
(feat): log ttpUrl
2026-06-27 00:13:47 +02:00
c484a3c303 (feat): add message states
Some checks failed
/ build-web (push) Successful in 1m20s
/ build-desktop (linux) (push) Successful in 7m6s
/ build-mobile (push) Failing after 13m56s
/ release (push) Has been skipped
(qol): prep for mtp migration
2026-06-26 10:54:25 +02:00
b100967fda (fix): dumb issue
Some checks failed
/ build-web (push) Successful in 1m20s
/ build-desktop (linux) (push) Successful in 6m54s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-06-21 14:24:06 +02:00
6ed9ee1109 (feat): add custom css
Some checks failed
/ build-desktop (linux) (push) Failing after 52s
/ build-web (push) Failing after 57s
/ build-mobile (push) Failing after 59s
/ release (push) Has been skipped
2026-06-18 22:58:29 +02:00
31e36b2abc (fix): incorrect dep link
Some checks failed
/ build-web (push) Successful in 1m27s
/ build-desktop (linux) (push) Successful in 8m39s
/ build-mobile (push) Failing after 22m35s
/ release (push) Has been cancelled
2026-06-18 15:15:09 +02:00
94e9a67f6e (feat): add border radius option
Some checks failed
/ build-desktop (linux) (push) Failing after 28s
/ build-web (push) Failing after 33s
/ build-mobile (push) Failing after 55s
/ release (push) Has been skipped
2026-06-18 15:14:06 +02:00
d3ab472531 (feat): update logo
Some checks failed
/ build-web (push) Failing after 1m20s
/ build-desktop (linux) (push) Successful in 6m27s
/ build-mobile (push) Failing after 13m20s
/ release (push) Has been skipped
2026-06-16 15:04:30 +02:00
forgejo-actions
f02536ca49 (qol): update release flake hash 2026-06-16 10:07:08 +00:00
292 changed files with 23389 additions and 7492 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

View file

@ -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

View file

@ -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
View file

@ -1,3 +1,4 @@
node_modules node_modules
releases releases
.fallow/ .fallow
.direnv

View file

@ -1,2 +1,2 @@
dist dist
release release

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Before After
Before After

View file

@ -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
}

View file

@ -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'
'';
};
});
}

View file

@ -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",

View file

@ -12,9 +12,11 @@ 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";
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 +190,25 @@ 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.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 +331,7 @@ async function start() {
verboseLog("app ready"); verboseLog("app ready");
registerIpc(); registerIpc();
registerDisplayMediaHandler(); registerDisplayMediaHandler();
initTray(() => mainWindow);
await createWindow(); await createWindow();
} }

View 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();
});
}

View file

@ -1,5 +1,9 @@
import { contextBridge, ipcRenderer } from "electron"; import { contextBridge, ipcRenderer } from "electron";
import { ipcChannels, type DesktopScreenShareSource } from "../shared/ipc.js"; import {
ipcChannels,
type DesktopCallStatus,
type DesktopScreenShareSource,
} from "../shared/ipc.js";
function windowAction(channel: string) { function windowAction(channel: string) {
return () => ipcRenderer.invoke(channel); return () => ipcRenderer.invoke(channel);
@ -27,6 +31,21 @@ 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);
},
},
window: { window: {
minimize: () => windowAction(ipcChannels.minimizeWindow), minimize: () => windowAction(ipcChannels.minimizeWindow),
maximize: () => windowAction(ipcChannels.maximizeWindow), maximize: () => windowAction(ipcChannels.maximizeWindow),

View file

@ -20,6 +20,12 @@ export type DesktopScreenShareCapabilities = {
hasReliableSystemAudio: boolean; hasReliableSystemAudio: boolean;
}; };
export type DesktopCallStatus = {
inCall: boolean;
speaking: boolean;
iconDataUrl?: string;
};
export type ReleaseArtifact = { export type ReleaseArtifact = {
name: string; name: string;
platform: string; platform: string;
@ -55,4 +61,5 @@ export const ipcChannels = {
closeWindow: "window:close", closeWindow: "window:close",
getVersion: "app:getVersion", getVersion: "app:getVersion",
checkForUpdates: "updates:checkForUpdates", checkForUpdates: "updates:checkForUpdates",
setCallStatus: "call:setStatus",
} as const; } as const;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Before After
Before After

96
apps/tauri/flake.lock generated
View file

@ -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
}

View file

@ -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}"
'';
};
}
);
}

View file

@ -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

Before After
Before After

View file

@ -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": {

View file

@ -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");

File diff suppressed because it is too large Load diff

View file

@ -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]

View file

@ -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"
] ]
} }

View file

@ -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"
] ]
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

View file

@ -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())

View file

@ -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
View file

@ -22,3 +22,5 @@ dist-ssr
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
.android

View file

@ -4,17 +4,49 @@
"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:*",
@ -23,19 +55,75 @@
"@tensamin/shared": "workspace:*", "@tensamin/shared": "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": {

View file

@ -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" />;
} }

View file

@ -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>
)} )}

View file

@ -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();
@ -72,7 +72,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 +85,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 +96,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 +121,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 +147,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>

View file

@ -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,
}); });
@ -103,9 +95,9 @@ export default function Form() {
await save("session_id", Date.now()); await save("session_id", Date.now());
await save("user_id", parsed.userId); await save("user_id", parsed.userId);
await save("private_key", parsed.privateKey); await save("mtp_keyring", parsed.privateKey);
if (parsed.domain) { if (parsed.domain) {
await save("ttp_url", `https://${parsed.domain}/`); await save("omega_url", `https://${parsed.domain}/`);
} }
location.href = "/"; location.href = "/";
@ -220,14 +212,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(
@ -251,9 +238,9 @@ export default function Form() {
await save("session_id", Date.now()); await save("session_id", Date.now());
await save("user_id", user.user_id); await save("user_id", user.user_id);
await save("private_key", inputParse.data.private_key); await save("mtp_keyring", inputParse.data.mtp_keyring);
if (domain) { if (domain) {
await save("ttp_url", `https://${domain}/`); await save("omega_url", `https://${domain}/`);
} }
location.href = "/"; location.href = "/";
@ -284,10 +271,10 @@ export default function Form() {
await save("session_id", Date.now()); await save("session_id", Date.now());
await save("user_id", userId); await save("user_id", userId);
await save("private_key", privateKey); await save("mtp_keyring", privateKey);
if (domain) { if (domain) {
await save("ttp_url", `https://${domain}/`); await save("omega_url", `https://${domain}/`);
} }
location.href = "/"; location.href = "/";
@ -346,8 +333,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

View file

@ -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);
} }

View file

@ -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] ? (

View file

@ -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"

View file

@ -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 {

View file

@ -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";
@ -119,6 +120,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 +136,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 +178,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 +219,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 +255,9 @@ function RootShell() {
function AppShell() { function AppShell() {
return ( return (
<TTPProvider> <MTPProvider>
<Session> <Session>
<UserContext> <UserProvider>
<CallInit /> <CallInit />
<CallPopout /> <CallPopout />
<TAuthWrapper> <TAuthWrapper>
@ -258,18 +269,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({

View file

@ -11,7 +11,7 @@ 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";
@ -31,7 +31,7 @@ export default function Page() {
// 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 +55,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 +72,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 +80,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) => {

View file

@ -1,7 +1,11 @@
import { List, Switch } from "@/features/settings/components"; import { List, Switch } from "@/features/settings/components";
import { Kbd } from "@tensamin/ui"; import { Button, Kbd } from "@tensamin/ui";
import { useStorage } from "@tensamin/storage/context";
import { storageDefaults } from "@tensamin/shared/data";
export default function Page() { export default function Page() {
const { save } = useStorage();
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Switch <Switch
@ -29,6 +33,12 @@ export default function Page() {
Trusted embed domains can get your IP-Address! Only add domains if you Trusted embed domains can get your IP-Address! Only add domains if you
really trust them! really trust them!
</p> </p>
<Button
variant="outline"
onClick={() => void save("reactions", storageDefaults.reactions)}
>
Reset Emoji Ranks
</Button>
<List label="Trusted embed domains" id="chat_trusted_domains" /> <List label="Trusted embed domains" id="chat_trusted_domains" />
</div> </div>
); );

View file

@ -11,8 +11,8 @@ import {
import { useUser, type User } from "@tensamin/user/context"; import { useUser, type User } from "@tensamin/user/context";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import MDInput from "@tensamin/markdown/input"; import MDInput from "@tensamin/markdown/input";
import { ttp } from "@tensamin/shared/data"; import { mtp } from "@tensamin/shared/data";
import { useTTP } from "@tensamin/ttp"; import { useMTP } from "@tensamin/mtp";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
async function prepImage( async function prepImage(
@ -43,7 +43,7 @@ async function prepImage(
export default function Page() { export default function Page() {
const { get } = useUser(); const { get } = useUser();
const { load } = useStorage(); const { load } = useStorage();
const { send } = useTTP(); const { send } = useMTP();
const [currentUser, setCurrentUser] = useState<User | null>(null); const [currentUser, setCurrentUser] = useState<User | null>(null);
const [draftUser, setDraftUser] = useState<Partial<User>>({}); const [draftUser, setDraftUser] = useState<Partial<User>>({});
const [errorMessage, setErrorMessage] = useState(""); const [errorMessage, setErrorMessage] = useState("");
@ -51,7 +51,7 @@ export default function Page() {
const avatarUploadRef = useRef<HTMLInputElement>(null); const avatarUploadRef = useRef<HTMLInputElement>(null);
const draftInitializedRef = useRef(false); const draftInitializedRef = useRef(false);
const effectiveAvatar = const effectiveAvatar =
draftUser.avatar === "none" ? undefined : draftUser.avatar; draftUser.Avatar === "none" ? undefined : draftUser.Avatar;
const updateDraftUser = ( const updateDraftUser = (
updater: (previous: Partial<User>) => Partial<User>, updater: (previous: Partial<User>) => Partial<User>,
@ -102,8 +102,8 @@ export default function Page() {
<Avatar className="size-14"> <Avatar className="size-14">
<AvatarImage src={effectiveAvatar} /> <AvatarImage src={effectiveAvatar} />
<AvatarFallback className="text-2xl"> <AvatarFallback className="text-2xl">
{draftUser.display?.slice(0, 2).toUpperCase() || {draftUser.Display?.slice(0, 2).toUpperCase() ||
currentUser.display.slice(0, 2).toUpperCase()} currentUser.Display.slice(0, 2).toUpperCase()}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
@ -140,7 +140,7 @@ export default function Page() {
})) }))
} }
placeholder="Display Name" placeholder="Display Name"
value={draftUser.display || ""} value={draftUser.Display || ""}
/> />
<Input <Input
className="w-full" className="w-full"
@ -151,7 +151,7 @@ export default function Page() {
})) }))
} }
placeholder="Username" placeholder="Username"
value={draftUser.username || ""} value={draftUser.Username || ""}
/> />
<MDInput <MDInput
styled styled
@ -162,23 +162,23 @@ export default function Page() {
setValue={(value) => setValue={(value) =>
updateDraftUser((prev) => ({ ...prev, about: value })) updateDraftUser((prev) => ({ ...prev, about: value }))
} }
value={draftUser.about || ""} value={draftUser.About || ""}
/> />
<Button <Button
onClick={async () => { onClick={async () => {
const { avatar, ...draftUsersWithoutAvatar } = draftUser; const { Avatar, ...draftUsersWithoutAvatar } = draftUser;
const payload = { const payload = {
...draftUsersWithoutAvatar, ...draftUsersWithoutAvatar,
...(typeof avatar === "string" ...(typeof Avatar === "string"
? { ? {
avatar: avatar.startsWith("data:") avatar: Avatar.startsWith("data:")
? (avatar.split(",", 2)[1] ?? "") ? (Avatar.split(",", 2)[1] ?? "")
: avatar, : Avatar,
} }
: {}), : {}),
}; };
const validation = ttp.change_user_data.request.safeParse(payload); const validation = mtp.ChangeUserData.request.safeParse(payload);
if (!validation.success) { if (!validation.success) {
setSaveSucceeded(false); setSaveSucceeded(false);
@ -189,7 +189,7 @@ export default function Page() {
} }
try { try {
await send("change_user_data", validation.data); await send("ChangeUserData", validation.data);
setSaveSucceeded(true); setSaveSucceeded(true);
setErrorMessage(""); setErrorMessage("");
} catch (err) { } catch (err) {

View file

@ -1,124 +1,92 @@
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
import { Button, Input, Label } from "@tensamin/ui";
import { useEffect, useState } from "react"; 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() { export default function Page() {
return ( const { save, load } = useStorage();
<div> const [draftOmegaUrl, setDraftOmegaUrl] = useState("");
<QrCodeLogin /> const [currentOmegaUrl, setCurrentOmegaUrl] = useState("");
</div>
);
}
// QR Code Login const [draftForcedOmikronUrl, setDraftForcedOmikronUrl] = useState("");
const generateQR = async (text: string): Promise<string> => { const [currentForcedOmikronUrl, setForcedForcedOmikronUrl] = useState("");
try { const [draftForcedOmikronPublicKey, setDraftForcedOmikronPublicKey] =
const url = await QRCode.toDataURL(text, { useState("");
errorCorrectionLevel: "H", const [currentForcedOmikronPublicKey, setForcedForcedOmikronPublicKey] =
margin: 1, useState("");
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(() => { useEffect(() => {
load("private_key").then((value) => { load("omega_url").then((value) => {
if (value) { setDraftOmegaUrl(value);
setPrivateKey(value); setCurrentOmegaUrl(value);
}
}); });
load("user_id").then((value) => { load("forced_omikron_url").then((value) => {
if (value) { setDraftForcedOmikronUrl(value || "");
setUserId(value); setForcedForcedOmikronUrl(value || "");
} });
load("forced_omikron_public_key").then((value) => {
setDraftForcedOmikronPublicKey(value || "");
setForcedForcedOmikronPublicKey(value || "");
}); });
load("ttp_url")
.then((value) => {
if (value) {
const url = new URL(value);
setConnectionString(`@${url.host}`);
} else {
setConnectionString("");
}
})
.catch(() => setConnectionString(""));
}, [load]); }, [load]);
useEffect(() => {
if (userId && privateKey && connectionString !== null) {
generateQR(
`tensamin://tu::${userId}${connectionString}::${privateKey}`,
).then(setQrCodeBase64);
}
}, [userId, privateKey, connectionString]);
const [qrCodeVisible, setQrCodeVisible] = useState(false);
return ( return (
<div className="flex flex-col gap-1 pl-2"> <div className="flex flex-col gap-8">
<p>Login QR Code</p> <p className="text-destructive">
<div className="relative rounded-lg w-50 border-3 aspect-square overflow-hidden"> It's best not to touch these! They can be exploited to gain access to
{qrCodeBase64 && ( your account!
<img className="w-full h-full" src={qrCodeBase64} alt="QR Code" /> </p>
)} <div className="flex flex-col gap-2">
{!qrCodeVisible && ( <Label>Omega Url</Label>
<> <div className="flex gap-1">
<div className="absolute top-0 left-0 w-full h-full backdrop-blur-sm bg-background/50" /> <Input
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center"> value={draftOmegaUrl}
<AlertDialog> onChange={(e) => setDraftOmegaUrl(e.target.value)}
<AlertDialogTrigger render={<Button>Show QR Code</Button>} /> />
<AlertDialogContent> <Button
<AlertDialogHeader> disabled={currentOmegaUrl === draftOmegaUrl}
<AlertDialogTitle>Are you sure?</AlertDialogTitle> onClick={() => {
<AlertDialogDescription> save("omega_url", draftOmegaUrl).then(() =>
Exposing this QR code is the same as sharing your .tu setCurrentOmegaUrl(draftOmegaUrl),
file. Since changing your private key is quite tedious, );
only do this when you're confident the key won't be }}
compromised. >
</AlertDialogDescription> Save
</AlertDialogHeader> </Button>
<AlertDialogFooter> </div>
<AlertDialogCancel>Cancel</AlertDialogCancel> </div>
<AlertDialogAction <div className="flex flex-col gap-2">
render={ <Label>Forced Omikron</Label>
<Button onClick={() => setQrCodeVisible(true)}> <div className="flex gap-1">
Show QR Code <Input
</Button> placeholder="URL..."
} value={draftForcedOmikronUrl || ""}
/> onChange={(e) => setDraftForcedOmikronUrl(e.target.value)}
</AlertDialogFooter> />
</AlertDialogContent> <Input
</AlertDialog> placeholder="Public Key..."
</div> value={draftForcedOmikronPublicKey || ""}
</> onChange={(e) => setDraftForcedOmikronPublicKey(e.target.value)}
)} />
<Button
disabled={
currentForcedOmikronUrl === draftForcedOmikronUrl &&
currentForcedOmikronPublicKey === draftForcedOmikronPublicKey
}
onClick={() => {
save("forced_omikron_url", draftForcedOmikronUrl).then(() =>
setForcedForcedOmikronUrl(draftForcedOmikronUrl),
);
save(
"forced_omikron_public_key",
draftForcedOmikronPublicKey,
).then(() =>
setForcedForcedOmikronPublicKey(draftForcedOmikronPublicKey),
);
}}
>
Save
</Button>
</div>
</div> </div>
</div> </div>
); );

View file

@ -1,5 +1,20 @@
import { StylePicker } from "@tensamin/ui"; import { StylePicker } from "@tensamin/ui";
export default function Page() { export default function Page() {
return <StylePicker />; return (
<div className="overflow-y-auto">
<StylePicker />
<div className="absolute bottom-0 right-0 mb-3 mr-2">
<a
className="block w-60 text-xs whitespace-pre-wrap"
href="https://git.methanium.net/tensamin/client/issues/new"
target="_blank"
rel="noreferrer"
>
Please open a Git issue to help us improve this feature. We want to
get it right.
</a>
</div>
</div>
);
} }

View file

@ -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,21 @@ 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/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 +109,41 @@ 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/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 +151,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(),
], ],

2216
bun.lock

File diff suppressed because it is too large Load diff

48
flake.lock generated
View file

@ -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"
} }
} }
}, },

Some files were not shown because too many files have changed in this diff Show more