Compare commits

...
Author SHA1 Message Date
5602b0a916
(feat): methanium ui stuff
All checks were successful
/ build-web (push) Successful in 8m10s
/ build-desktop (linux) (push) Successful in 12m42s
/ build-mobile (push) Successful in 19m38s
/ release (push) Successful in 3m29s
2026-07-27 16:19:03 +02:00
6d865a93e8
(fix): workflows
All checks were successful
/ build-web (push) Successful in 7m36s
/ build-desktop (linux) (push) Successful in 12m8s
/ build-mobile (push) Successful in 18m57s
/ release (push) Successful in 3m24s
2026-07-26 22:48:54 +02:00
ace7973dff
(feat): big methanium/ui migration
Some checks failed
/ build-web (push) Failing after 5m27s
/ build-desktop (linux) (push) Failing after 5m54s
/ build-mobile (push) Failing after 8m0s
/ release (push) Has been skipped
(feat): update message state icons
(qol): update todo
(qol): formatted
2026-07-26 18:55:46 +02:00
5e79893137
(fix): methanium/ui hash in pnpm lock
All checks were successful
/ build-web (push) Successful in 7m50s
/ build-desktop (linux) (push) Successful in 12m29s
/ build-mobile (push) Successful in 19m23s
/ release (push) Successful in 3m22s
2026-07-25 23:07:28 +02:00
cfabda5d28
(fix): mtp double connections
Some checks failed
/ release (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-07-25 23:05:04 +02:00
25c7ee7c26
(fix): actually migrate all the imports from tensamin/ui to methanium/ui
Some checks failed
/ build-web (push) Failing after 4m33s
/ build-desktop (linux) (push) Failing after 4m36s
/ build-mobile (push) Failing after 7m7s
/ release (push) Has been skipped
(feat): update legal loading screen
2026-07-25 18:12:54 +02:00
f9f8d5c6c1
(feat): migrate tensamin/ui to methanium/ui
Some checks failed
/ build-web (push) Failing after 4m22s
/ build-desktop (linux) (push) Failing after 4m25s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-07-25 18:06:24 +02:00
9e037d8500
(chore): update license
All checks were successful
/ build-web (push) Successful in 7m4s
/ build-desktop (linux) (push) Successful in 11m35s
/ build-mobile (push) Successful in 18m19s
/ release (push) Successful in 2m58s
2026-07-25 13:10:30 +02:00
17db895203
(feat): add custom lint rules
Some checks failed
/ build-web (push) Successful in 7m4s
/ build-desktop (linux) (push) Successful in 11m31s
/ build-mobile (push) Successful in 18m12s
/ release (push) Failing after 3m2s
(qol): update todo
(chore): update licenses
2026-07-24 12:44:08 +02:00
bf8f449f03
(wip): add onboarding
All checks were successful
/ build-web (push) Successful in 7m19s
/ build-desktop (linux) (push) Successful in 11m52s
/ build-mobile (push) Successful in 18m26s
/ release (push) Successful in 2m59s
(fix): login page reloading
2026-07-24 12:18:05 +02:00
b6dfb4d019
(feat): update chromium path
All checks were successful
/ build-web (push) Successful in 8m40s
/ build-desktop (linux) (push) Successful in 13m25s
/ build-mobile (push) Successful in 20m9s
/ release (push) Successful in 3m2s
2026-07-21 13:57:32 +02:00
747b88f177
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
Some checks failed
/ build-web (push) Successful in 8m13s
/ release (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-07-21 13:43:42 +02:00
cda0c48454
(feat): improve secure storage 2026-07-21 13:42:52 +02:00
forgejo-actions
d5a05e3a8b (qol): update release flake hash 2026-07-21 10:17:29 +00:00
9011768a2e
(fix): nix flake 2026-07-21 11:37:44 +02:00
85d97a6969
Merge 2026-07-21 11:36:22 +02:00
9a9dfe54ce
(fix): nix flake 2026-07-21 11:35:36 +02:00
forgejo-actions
bd745a5f13 (qol): update release flake hash 2026-07-21 09:19:49 +00:00
dcca8bd83d Update mtp
All checks were successful
/ build-web (push) Successful in 7m21s
/ build-desktop (linux) (push) Successful in 12m10s
/ build-mobile (push) Successful in 18m31s
/ release (push) Successful in 3m5s
2026-07-20 23:20:26 +02:00
Alex Emmet
fd0e06b71e Merge origin/dev 2026-07-13 17:30:43 +02:00
3ce708b86a (feat): add MessageDeleteLive
All checks were successful
/ build-web (push) Successful in 6m34s
/ build-desktop (linux) (push) Successful in 12m5s
/ build-mobile (push) Successful in 19m39s
/ release (push) Successful in 3m2s
2026-07-11 23:12:42 +02:00
790a1db788 (feat): add cache package
All checks were successful
/ build-web (push) Successful in 7m26s
/ build-desktop (linux) (push) Successful in 11m29s
/ build-mobile (push) Successful in 19m15s
/ release (push) Successful in 3m3s
(feat): improve local storage security
(feat): move settings to dedicated settings package
2026-07-11 22:08:01 +02:00
fb095db7a6 (feat): don't render mini context menu on mobile
All checks were successful
/ build-web (push) Successful in 7m21s
/ build-desktop (linux) (push) Successful in 12m4s
/ build-mobile (push) Successful in 19m8s
/ release (push) Successful in 3m0s
2026-07-11 20:52:54 +02:00
f898da653b (feat): remove mini context menu bounce animation
All checks were successful
/ build-web (push) Successful in 7m14s
/ build-desktop (linux) (push) Successful in 11m57s
/ build-mobile (push) Successful in 19m5s
/ release (push) Successful in 3m2s
2026-07-11 19:57:43 +02:00
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
350 changed files with 26313 additions and 8880 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

View file

@ -5,74 +5,116 @@ on:
paths-ignore:
- flake.nix
env:
NIX_CONFIG: experimental-features = nix-command flakes
jobs:
build-web:
runs-on: docker
runs-on: nixos
steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo
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: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: bun install --frozen-lockfile
run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Copy licenses
run: bun run copy-licenses
run: nix develop .#electron --command pnpm run copy-licenses
- name: Build packages
run: bun run build:packages
run: nix develop .#electron --command pnpm run build:packages
- name: Build web
run: bun run build:web
- name: Install rsync
run: apt-get update && apt-get install -y rsync
run: nix develop .#electron --command pnpm run build:web
- 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:
runs-on: docker
runs-on: nixos
steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo
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: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: bun install --frozen-lockfile
run: nix develop .#tauri --command pnpm install --frozen-lockfile
- name: Copy licenses
run: bun run copy-licenses
run: nix develop .#tauri --command pnpm run copy-licenses
- name: Build packages
run: bun run build:packages
run: nix develop .#tauri --command pnpm run build:packages
- name: Setup Android Keystore
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
run: |
bun -e "require('fs').writeFileSync('keystore.jks', Buffer.from(process.env.KEYSTORE_BASE64.replace(/\s+/g, ''), 'base64'))"
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)"
nix profile add nixpkgs#gnused
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
run: bun run build:mobile
run: nix develop .#tauri --command pnpm run build:mobile
- name: Upload mobile artifact
uses: https://data.forgejo.org/actions/upload-artifact@v3
@ -81,31 +123,33 @@ jobs:
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
build-desktop:
runs-on: docker
runs-on: nixos
strategy:
matrix:
target: [linux]
steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo
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: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: bun install --frozen-lockfile
run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Copy licenses
run: bun run copy-licenses
run: nix develop .#electron --command pnpm run copy-licenses
- name: Build packages
run: bun run build:packages
run: nix develop .#electron --command pnpm run build:packages
- name: Set Electron dev version
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
VERSION="$(node -p "require('./package.json').version")"
SHORT_SHA="$(git rev-parse --short HEAD)"
DEV_VERSION="$VERSION-dev-$SHORT_SHA"
@ -117,9 +161,15 @@ jobs:
pkg.version = process.env.DEV_VERSION;
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
'
EOF
- 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
uses: https://data.forgejo.org/actions/upload-artifact@v3
@ -128,22 +178,22 @@ jobs:
path: apps/electron/release/
release:
runs-on: docker
runs-on: nixos
needs: [build-web, build-mobile, build-desktop]
steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
with:
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: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: bun install --frozen-lockfile
run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Download mobile artifact
uses: https://data.forgejo.org/actions/download-artifact@v3
@ -160,21 +210,27 @@ jobs:
- name: Read version and hash
id: version
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
VERSION="$(node -p "require('./package.json').version")"
SHORT_SHA="$(git rev-parse --short HEAD)"
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
echo "short_sha=$SHORT_SHA" >> "$FORGEJO_OUTPUT"
echo "tag=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
echo "title=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
EOF
- name: Copy releases
env:
TENSAMIN_RELEASE_VERSION: ${{ steps.version.outputs.tag }}
TENSAMIN_RELEASE_TAG: ${{ steps.version.outputs.tag }}
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
ASSET_BASE_URL="${{ forgejo.api_url }}"
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
env:
@ -185,6 +241,7 @@ jobs:
TAG: ${{ steps.version.outputs.tag }}
TITLE: ${{ steps.version.outputs.title }}
run: |
nix develop .#electron --command bash <<'EOF'
set -eu
test -d releases
@ -273,3 +330,4 @@ jobs:
-H "Authorization: token $TOKEN" \
-F "attachment=@$file"
done
EOF

View file

@ -6,74 +6,116 @@ on:
paths-ignore:
- flake.nix
env:
NIX_CONFIG: experimental-features = nix-command flakes
jobs:
build-web:
runs-on: docker
runs-on: nixos
steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo
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: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: bun install --frozen-lockfile
run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Copy licenses
run: bun run copy-licenses
run: nix develop .#electron --command pnpm run copy-licenses
- name: Build packages
run: bun run build:packages
run: nix develop .#electron --command pnpm run build:packages
- name: Build web
run: bun run build:web
- name: Install rsync
run: apt-get update && apt-get install -y rsync
run: nix develop .#electron --command pnpm run build:web
- 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:
runs-on: docker
runs-on: nixos
steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo
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: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: bun install --frozen-lockfile
run: nix develop .#tauri --command pnpm install --frozen-lockfile
- name: Copy licenses
run: bun run copy-licenses
run: nix develop .#tauri --command pnpm run copy-licenses
- name: Build packages
run: bun run build:packages
run: nix develop .#tauri --command pnpm run build:packages
- name: Setup Android Keystore
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
run: |
bun -e "require('fs').writeFileSync('keystore.jks', Buffer.from(process.env.KEYSTORE_BASE64.replace(/\s+/g, ''), 'base64'))"
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)"
nix profile add nixpkgs#gnused
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
run: bun run build:mobile
run: nix develop .#tauri --command pnpm run build:mobile
- name: Upload mobile artifact
uses: https://data.forgejo.org/actions/upload-artifact@v3
@ -82,31 +124,33 @@ jobs:
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
build-desktop:
runs-on: docker
runs-on: nixos
strategy:
matrix:
target: [linux]
steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo
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: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: bun install --frozen-lockfile
run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Copy licenses
run: bun run copy-licenses
run: nix develop .#electron --command pnpm run copy-licenses
- name: Build packages
run: bun run build:packages
run: nix develop .#electron --command pnpm run build:packages
- name: Set Electron prod version
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
VERSION="$(node -p "require('./package.json').version")"
export VERSION
node -e '
@ -116,9 +160,15 @@ jobs:
pkg.version = process.env.VERSION;
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
'
EOF
- 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
uses: https://data.forgejo.org/actions/upload-artifact@v3
@ -127,20 +177,20 @@ jobs:
path: apps/electron/release/
release:
runs-on: docker
runs-on: nixos
needs: [build-web, build-mobile, build-desktop]
steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo
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: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: bun install --frozen-lockfile
run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Download mobile artifact
uses: https://data.forgejo.org/actions/download-artifact@v3
@ -157,18 +207,24 @@ jobs:
- name: Read version
id: version
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
VERSION="$(node -p "require('./package.json').version")"
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
echo "tag=$VERSION" >> "$FORGEJO_OUTPUT"
EOF
- name: Copy releases
env:
TENSAMIN_RELEASE_VERSION: ${{ steps.version.outputs.tag }}
TENSAMIN_RELEASE_TAG: ${{ steps.version.outputs.tag }}
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
ASSET_BASE_URL="${{ forgejo.api_url }}"
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
env:
@ -178,6 +234,7 @@ jobs:
SHA: ${{ forgejo.sha }}
TAG: ${{ steps.version.outputs.tag }}
run: |
nix develop .#electron --command bash <<'EOF'
set -eu
test -d releases
@ -231,18 +288,18 @@ jobs:
-H "Authorization: token $TOKEN" \
-F "attachment=@$file"
done
EOF
- name: Delete dev releases for prod version
- name: Delete dev releases
env:
TOKEN: ${{ forgejo.token }}
API: ${{ forgejo.api_url }}
REPO: ${{ forgejo.repository }}
TAG: ${{ steps.version.outputs.tag }}
run: |
nix develop .#electron --command bash <<'EOF'
set -eu
PAGE=1
PREFIX="$TAG-dev-"
DELETE_RELEASES=delete-dev-releases.tsv
: > "$DELETE_RELEASES"
@ -257,8 +314,7 @@ jobs:
test "$COUNT" -gt 0 || break
jq -r \
--arg prefix "$PREFIX" \
'.[] | select(.prerelease == true) | select(.tag_name | startswith($prefix)) | [.id, .tag_name] | @tsv' releases.json \
'.[] | select(.prerelease == true) | select(.tag_name | contains("-dev-")) | [.id, .tag_name] | @tsv' releases.json \
>> "$DELETE_RELEASES"
PAGE="$((PAGE + 1))"
@ -271,11 +327,13 @@ jobs:
-H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/$release_id"
done < "$DELETE_RELEASES"
EOF
- name: Update root flake release hash
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
nix develop .#electron --command bash <<'EOF'
set -eu
DEB="$(find releases -maxdepth 1 -type f -name 'Tensamin-*-linux-amd64.deb' -print -quit)"
@ -324,3 +382,4 @@ jobs:
git add flake.nix
git -c user.name="forgejo-actions" -c user.email="forgejo-actions@localhost" commit -m "(qol): update release flake hash"
git push origin HEAD:dev
EOF

3
.gitignore vendored
View file

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

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "mtp-type-maps"]
path = mtp-type-maps
url = https://git.methanium.net/tensamin/mtp-type-maps

25
LICENSE
View file

@ -1,16 +1,15 @@
Copyright (c) [2025] [Methanium]
Copyright (c) 2025 Methanium
All rights reserved.
This software is protected by copyright. Copying, editing,
distributing, publicly performing, or any other use of this software
or its components, in source or binary form, is strictly prohibited without the express
written permission of the copyright holder.
No part of this software, source code, documentation, or
associated materials may be copied, reproduced, modified,
distributed, published, sublicensed, sold, or used to create
derivative works without prior written permission from the
copyright holder.
FUTURE LICENSE ACCEPTANCE:
It is the copyright holder's intention to release this software in the future
under a license yet to be defined, which will, among other things,
allow private, non-commercial use. This statement does not constitute
a current license grant and does not alter the above
prohibition on use, copying, or modification. Until the formal
publication of such a future license, all rights remain
reserved.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY
OF ANY KIND, EXPRESS OR IMPLIED. TO THE MAXIMUM
EXTENT PERMITTED BY LAW, THE COPYRIGHT HOLDER SHALL
NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
LIABILITY ARISING FROM THE SOFTWARE OR ITS USE.

3
README Normal file
View file

@ -0,0 +1,3 @@
# Information
All dev releases get deleted upon creation of the latest prod release.

6
TODO Normal file
View file

@ -0,0 +1,6 @@
- Add a bunch of tests
- Add packages/hotkeys/
- Full accessability
- Add settings saving & update onboarding to use it
-> Most folders in packages/ have specific todos

View file

@ -1,2 +1,2 @@
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",
"author": "methanium",
"homepage": "https://git.methanium.net/tensamin/client",
"desktopName": "Tensamin",
"type": "module",
"main": "dist/main/main.js",
"scripts": {
"clean": "rm -rf dist release",
"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",
"dev:raw": "cd ../.. && bun run build:web && cd apps/electron && bun 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",
"start:raw": "bun 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",
"package:raw": "cd ../.. && bun run build:web && cd apps/electron && bun 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:linux:raw": "cd ../.. && bun run build:web && cd apps/electron && bun 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:windows:raw": "cd ../.. && bun run build:web && cd apps/electron && bun 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",
"checksum": "bun scripts/generate-release-metadata.ts",
"generate-signing-key": "bun scripts/generate-signing-key.ts",
"validate:raw": "bun run build && bun 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"
"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 ../..#electron --command pnpm run dev:raw; else pnpm run dev:raw; fi",
"start:raw": "pnpm run build && electron .",
"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 ../.. && 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 ../..#electron --command pnpm run package:raw; else pnpm run package:raw; fi",
"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 ../..#electron --command pnpm run package:linux:raw; else pnpm run package:linux:raw; fi",
"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 ../..#electron --command pnpm run package:windows:raw; else pnpm run package:windows:raw; fi",
"checksum": "node scripts/generate-release-metadata.ts",
"generate-signing-key": "node scripts/generate-signing-key.ts",
"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 ../..#electron --command pnpm run validate:raw; else pnpm run validate:raw; fi"
},
"dependencies": {},
"devDependencies": {
@ -44,6 +45,9 @@
"directories": {
"output": "release"
},
"toolsets": {
"appimage": "1.0.3"
},
"files": [
"dist/**/*",
"package.json"
@ -54,8 +58,12 @@
"to": "web"
},
{
"from": "build/icons/icon.png",
"to": "icons/icon.png"
"from": "build/icons",
"to": "icons",
"filter": [
"32x32.png",
"icon.png"
]
}
],
"linux": {
@ -68,6 +76,7 @@
"executableName": "tensamin",
"category": "Network",
"maintainer": "Methanium",
"syncDesktopName": true,
"desktop": {
"entry": {
"Name": "Tensamin",

View file

@ -12,21 +12,40 @@ import {
import { checkForUpdates } from "./updates.js";
import {
ipcChannels,
type DesktopCallStatus,
type DesktopScreenShareAudioOutput,
type DesktopScreenShareCapabilities,
} from "../shared/ipc.js";
import { initTray, setTrayCallStatus } from "./tray.js";
import {
clearSecureStorage,
deleteSecureStorage,
getSecureStorageStatus,
loadSecureStorage,
saveSecureStorage,
} from "./secureStorage.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const verbose = process.argv.includes("--verbose");
let mainWindow: BrowserWindow | null = null;
let selectedScreenShareSourceId: string | null = null;
app.setName("tensamin");
app.setPath("userData", join(app.getPath("appData"), "tensamin", "electron"));
if (verbose) {
app.commandLine.appendSwitch("enable-logging", "stderr");
app.commandLine.appendSwitch("v", "1");
app.commandLine.appendSwitch("log-level", "0");
}
if (
process.platform === "linux" &&
!app.commandLine.hasSwitch("password-store")
) {
app.commandLine.appendSwitch("password-store", "gnome-libsecret");
}
if (
process.platform === "linux" &&
process.env.XDG_SESSION_TYPE === "wayland" &&
@ -188,6 +207,37 @@ function registerIpc() {
);
ipcMain.handle(ipcChannels.getVersion, () => app.getVersion());
ipcMain.handle(ipcChannels.checkForUpdates, checkForUpdates);
ipcMain.handle(ipcChannels.getSecureStorageStatus, getSecureStorageStatus);
ipcMain.handle(ipcChannels.loadSecureStorage, (_event, key: unknown) =>
loadSecureStorage(key),
);
ipcMain.handle(
ipcChannels.saveSecureStorage,
(_event, key: unknown, value: unknown) => saveSecureStorage(key, value),
);
ipcMain.handle(ipcChannels.deleteSecureStorage, (_event, key: unknown) =>
deleteSecureStorage(key),
);
ipcMain.handle(ipcChannels.clearSecureStorage, clearSecureStorage);
ipcMain.handle(ipcChannels.setCallStatus, (_event, status: unknown) => {
if (
typeof status !== "object" ||
status === null ||
typeof (status as DesktopCallStatus).inCall !== "boolean" ||
typeof (status as DesktopCallStatus).speaking !== "boolean" ||
((status as DesktopCallStatus).iconDataUrl !== undefined &&
(typeof (status as DesktopCallStatus).iconDataUrl !== "string" ||
!(status as DesktopCallStatus).iconDataUrl?.startsWith(
"data:image/png;base64,",
) ||
(status as DesktopCallStatus).iconDataUrl!.length > 16_384))
) {
throw new Error("Invalid call status.");
}
const { inCall, iconDataUrl } = status as DesktopCallStatus;
setTrayCallStatus(inCall, iconDataUrl);
});
ipcMain.handle(ipcChannels.minimizeWindow, () => {
verboseLog("window:minimize");
mainWindow?.minimize();
@ -310,6 +360,7 @@ async function start() {
verboseLog("app ready");
registerIpc();
registerDisplayMediaHandler();
initTray(() => mainWindow);
await createWindow();
}

View file

@ -0,0 +1,128 @@
import { app, safeStorage } from "electron";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import {
secureStorageLimits,
type DesktopSecureStorageStatus,
} from "../shared/ipc.js";
type StoredValues = Record<string, string>;
let pendingWrite = Promise.resolve();
function storagePath() {
return join(app.getPath("userData"), "secure-storage.json");
}
function validateKey(key: unknown): asserts key is string {
if (
typeof key !== "string" ||
key.length === 0 ||
Buffer.byteLength(key, "utf8") > secureStorageLimits.maxKeyBytes
) {
throw new Error("Invalid secure storage key.");
}
}
function validateValue(value: unknown): asserts value is string {
if (
typeof value !== "string" ||
Buffer.byteLength(value, "utf8") > secureStorageLimits.maxValueBytes
) {
throw new Error("Invalid secure storage value.");
}
}
export function getSecureStorageStatus(): DesktopSecureStorageStatus {
const backend =
process.platform === "linux"
? safeStorage.getSelectedStorageBackend()
: process.platform === "darwin"
? "keychain"
: process.platform === "win32"
? "dpapi"
: null;
return {
available:
safeStorage.isEncryptionAvailable() &&
(process.platform !== "linux" || backend !== "basic_text"),
backend,
};
}
function requireAvailable() {
if (!getSecureStorageStatus().available) {
throw new Error("Secure storage is unavailable.");
}
}
async function readValues(): Promise<StoredValues> {
try {
const parsed: unknown = JSON.parse(await readFile(storagePath(), "utf8"));
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("Invalid secure storage data.");
}
const values = parsed as Record<string, unknown>;
if (Object.values(values).some((value) => typeof value !== "string")) {
throw new Error("Invalid secure storage data.");
}
return values as StoredValues;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return {};
throw error;
}
}
async function writeValues(values: StoredValues) {
const path = storagePath();
const temporaryPath = `${path}.tmp`;
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
await writeFile(temporaryPath, JSON.stringify(values), { mode: 0o600 });
await rename(temporaryPath, path);
}
function mutateValues(mutation: (values: StoredValues) => void) {
const operation = pendingWrite.then(async () => {
const values = await readValues();
mutation(values);
await writeValues(values);
});
pendingWrite = operation.catch(() => undefined);
return operation;
}
export async function loadSecureStorage(key: unknown): Promise<string | null> {
requireAvailable();
validateKey(key);
await pendingWrite;
const encrypted = (await readValues())[key];
if (encrypted === undefined) return null;
return safeStorage.decryptString(Buffer.from(encrypted, "base64"));
}
export function saveSecureStorage(key: unknown, value: unknown) {
requireAvailable();
validateKey(key);
validateValue(value);
const encrypted = safeStorage.encryptString(value).toString("base64");
return mutateValues((values) => {
values[key] = encrypted;
});
}
export function deleteSecureStorage(key: unknown) {
requireAvailable();
validateKey(key);
return mutateValues((values) => {
delete values[key];
});
}
export function clearSecureStorage() {
requireAvailable();
return mutateValues((values) => {
for (const key of Object.keys(values)) delete values[key];
});
}

View file

@ -0,0 +1,60 @@
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,10 +1,23 @@
import { contextBridge, ipcRenderer } from "electron";
import { ipcChannels, type DesktopScreenShareSource } from "../shared/ipc.js";
import {
ipcChannels,
type DesktopCallStatus,
type DesktopScreenShareSource,
secureStorageLimits,
} from "../shared/ipc.js";
function windowAction(channel: string) {
return () => ipcRenderer.invoke(channel);
}
function validKey(key: string) {
return (
typeof key === "string" &&
key.length > 0 &&
Buffer.byteLength(key, "utf8") <= secureStorageLimits.maxKeyBytes
);
}
const desktopApi = {
media: {
listScreenShareSources: () =>
@ -27,6 +40,39 @@ const desktopApi = {
updates: {
checkForUpdates: () => ipcRenderer.invoke(ipcChannels.checkForUpdates),
},
call: {
setStatus: (status: DesktopCallStatus) => {
if (
typeof status?.inCall !== "boolean" ||
typeof status?.speaking !== "boolean" ||
(status.iconDataUrl !== undefined &&
(typeof status.iconDataUrl !== "string" ||
!status.iconDataUrl.startsWith("data:image/png;base64,")))
) {
return Promise.reject(new Error("Invalid call status."));
}
return ipcRenderer.invoke(ipcChannels.setCallStatus, status);
},
},
secureStorage: {
getStatus: () => ipcRenderer.invoke(ipcChannels.getSecureStorageStatus),
load: (key: string) =>
validKey(key)
? ipcRenderer.invoke(ipcChannels.loadSecureStorage, key)
: Promise.reject(new Error("Invalid secure storage key.")),
save: (key: string, value: string) =>
validKey(key) &&
typeof value === "string" &&
Buffer.byteLength(value, "utf8") <= secureStorageLimits.maxValueBytes
? ipcRenderer.invoke(ipcChannels.saveSecureStorage, key, value)
: Promise.reject(new Error("Invalid secure storage key or value.")),
delete: (key: string) =>
validKey(key)
? ipcRenderer.invoke(ipcChannels.deleteSecureStorage, key)
: Promise.reject(new Error("Invalid secure storage key.")),
clear: () => ipcRenderer.invoke(ipcChannels.clearSecureStorage),
},
window: {
minimize: () => windowAction(ipcChannels.minimizeWindow),
maximize: () => windowAction(ipcChannels.maximizeWindow),

View file

@ -20,6 +20,22 @@ export type DesktopScreenShareCapabilities = {
hasReliableSystemAudio: boolean;
};
export type DesktopCallStatus = {
inCall: boolean;
speaking: boolean;
iconDataUrl?: string;
};
export type DesktopSecureStorageStatus = {
available: boolean;
backend: string | null;
};
export const secureStorageLimits = {
maxKeyBytes: 256,
maxValueBytes: 1024 * 1024,
} as const;
export type ReleaseArtifact = {
name: string;
platform: string;
@ -55,4 +71,10 @@ export const ipcChannels = {
closeWindow: "window:close",
getVersion: "app:getVersion",
checkForUpdates: "updates:checkForUpdates",
setCallStatus: "call:setStatus",
getSecureStorageStatus: "secureStorage:getStatus",
loadSecureStorage: "secureStorage:load",
saveSecureStorage: "secureStorage:save",
deleteSecureStorage: "secureStorage:delete",
clearSecureStorage: "secureStorage:clear",
} 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/) -->
<svg
width="90mm"
height="90mm"
viewBox="0 0 90 90"
width="90.476906mm"
height="90.476906mm"
viewBox="0 0 90.476906 90.476906"
version="1.1"
id="svg1"
xml:space="preserve"
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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
@ -24,16 +24,32 @@
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
inkscape:zoom="0.99999999"
inkscape:cx="450"
inkscape:cy="291.5"
inkscape:window-width="1223"
inkscape:window-height="1369"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:zoom="2"
inkscape:cx="191.5"
inkscape:cy="117.25"
inkscape:window-width="2500"
inkscape:window-height="1403"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
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"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
@ -51,7 +67,7 @@
id="stop3" /></linearGradient><linearGradient
id="swatch2"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
style="stop-color:#b8f8ff;stop-opacity:1;"
offset="0"
id="stop2" /></linearGradient><linearGradient
id="swatch1"
@ -184,13 +200,23 @@
x1="63.191292"
y1="148.5"
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:groupmode="layer"
id="layer1"
transform="translate(-62.941767,-104.13688)"><g
transform="translate(-281.06837,-119.33314)"
><g
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"
style="display:inline;stroke:url(#linearGradient15);stroke-width:0.616508;stroke-dasharray:none"
clip-path="url(#clipPath1)"><path
@ -213,11 +239,19 @@
id="rect1-1-3-6-2"
style="fill:#12a89e;stroke:url(#linearGradient21);stroke-width:0.616508;stroke-dasharray:none"
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"
inkscape:label="foreground"
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"
inkscape:label="outline"
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

@ -4,10 +4,6 @@
"version": "0.0.0",
"type": "module",
"exports": {
"./controls": {
"types": "./src/windowControls.tsx",
"default": "./src/windowControls.tsx"
},
"./deeplinkHandler": {
"types": "./src/deeplinkHandler.tsx",
"default": "./src/deeplinkHandler.tsx"
@ -21,11 +17,11 @@
"dev:mobile:raw": "tauri android dev --host ${TAURI_DEV_HOST:-127.0.0.1}",
"start-adb:mobile:raw": "adb devices",
"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",
"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",
"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",
"gen-icons": "tauri icon ./logo.json && bun scripts/sync-electron-icons.ts",
"format": "bunx prettier --write .",
"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 && [ -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": "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 && node scripts/sync-electron-icons.ts",
"format": "pnpm exec prettier --write .",
"lint": "eslint src"
},
"dependencies": {
@ -35,7 +31,7 @@
"@tauri-apps/plugin-log": "~2",
"@tauri-apps/plugin-notification": "~2",
"@tensamin/shared": "workspace:*",
"@tensamin/ui": "*",
"@methanium/ui": "*",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},

View file

@ -1,9 +1,12 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
// Config
const PLACEHOLDER_VERSION = "0.0.0";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootPackageJsonPath = path.resolve(__dirname, "../../package.json");
const cargoTomlPath = path.resolve(__dirname, "./src-tauri/Cargo.toml");
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"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
tauri-build = { git = "https://github.com/tauri-apps/tauri", branch = "feat/cef", features = [] }
[dependencies]
tauri-plugin-opener = "2"
@ -23,8 +23,6 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri-plugin-deep-link = "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"
[target.'cfg(target_os = "android")'.dependencies.tauri]

View file

@ -9,17 +9,9 @@
"core:default",
"opener: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",
"deep-link:default",
"notification:default",
"log:default",
"ttp-tauri:allow-connect",
"ttp-tauri:allow-send",
"ttp-tauri:allow-close",
"ttp-tauri:allow-ready-state"
"log:default"
]
}

View file

@ -14,10 +14,6 @@
"barcode-scanner:allow-scan",
"barcode-scanner:allow-cancel",
"notification:default",
"log:default",
"ttp-tauri:allow-connect",
"ttp-tauri:allow-send",
"ttp-tauri:allow-close",
"ttp-tauri:allow-ready-state"
"log:default"
]
}

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() {
let builder = tauri::Builder::default()
.plugin(tauri_plugin_log::Builder::new().level(tauri_plugin_log::log::LevelFilter::Info).build())
.plugin(tauri_plugin_notification::init())
.plugin(ttp_tauri::init());
.plugin(tauri_plugin_notification::init());
let builder = builder
.plugin(tauri_plugin_deep_link::init())

View file

@ -5,9 +5,9 @@
"mainBinaryName": "tensamin",
"identifier": "net.tensamin.client",
"build": {
"beforeDevCommand": "cd ../web && bun run dev && cd ../tauri",
"beforeDevCommand": "cd ../web && pnpm run dev && cd ../tauri",
"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"
},
"app": {

View file

@ -7,7 +7,7 @@ import {
} from "react";
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link";
import { isTauri } from "@tauri-apps/api/core";
import { useIsMobile } from "@tensamin/ui";
import { useIsMobile } from "@methanium/ui";
type DeeplinkContextValue = {
deeplinks: readonly string[];

View file

@ -3,7 +3,7 @@ import {
Format,
requestPermissions,
} from "@tauri-apps/plugin-barcode-scanner";
import { Button } from "@tensamin/ui";
import { Button } from "@methanium/ui";
import { toast } from "@tensamin/shared/log";

2
apps/web/.gitignore vendored
View file

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

View file

@ -4,38 +4,129 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"format": "bunx prettier --write .",
"format": "pnpm exec prettier --write .",
"lint": "eslint src",
"dev": "vite --port 3000 --host 0.0.0.0",
"test": "bun test --pass-with-no-tests",
"build": "bun run test && tsc -b && vite build",
"test": "vitest run --passWithNoTests",
"build": "pnpm run test && tsc -b && vite build",
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
},
"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",
"@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",
"@tanstack/devtools-event-client": "^0.3.0",
"@tanstack/query-core": "^5.0.0",
"@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",
"@tauri-apps/api": "^2",
"@tensamin/call": "workspace:*",
"@tensamin/cache": "workspace:*",
"@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/settings": "workspace:*",
"@tensamin/storage": "workspace:*",
"@tensamin/tauri": "workspace:*",
"@tensamin/ttp": "workspace:*",
"@tensamin/mtp": "workspace:*",
"@tensamin/tauth": "workspace:*",
"@tensamin/markdown": "workspace:*",
"@tensamin/ui": "*",
"@methanium/ui": "*",
"@tensamin/user": "workspace:*",
"@tensamin/notifications": "workspace:*",
"@tensamin/onboarding": "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",
"lucide-react": "^1.14.0",
"next-themes": "^0.4.6",
"pngjs": "^5.0.0",
"qrcode": "^1.5.4",
"react": "^19.2.0",
"react-day-picker": "^10.0.1",
"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",
"tailwind-merge": "^3.6.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"
},
"devDependencies": {

View file

@ -6,9 +6,9 @@ import {
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@tensamin/ui";
import { Card, CardHeader } from "@tensamin/ui";
import { Skeleton } from "@tensamin/ui";
} from "@methanium/ui";
import { Card, CardHeader } from "@methanium/ui";
import { Skeleton } from "@methanium/ui";
import { getStatusColor } from "@tensamin/shared/data";
export function Basic({
@ -19,13 +19,13 @@ export function Basic({
extra?: React.ReactNode;
}) {
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">
<div className="relative shrink-0 overflow-visible">
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarImage src={user.Avatar} />
<AvatarFallback>
{user.display.slice(0, 2).toUpperCase()}
{user.Display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<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
style={{
backgroundColor: getStatusColor(user.online_status),
backgroundColor: getStatusColor(user.OnlineStatus),
}}
className="h-2.25 w-2.25 rounded-full"
/>
@ -42,15 +42,14 @@ export function Basic({
}
/>
<TooltipContent>
{user.online_status
.split("_")
{user.OnlineStatus.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")}
</TooltipContent>
</Tooltip>
</div>
<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 className="pr-1">{extra}</div>
</CardHeader>
@ -59,5 +58,5 @@ export function Basic({
}
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 {
Avatar,
AvatarFallback,
AvatarImage,
Button,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@tensamin/ui";
import { toLossySixDigitCode } from "@tensamin/shared/code";
import { Avatar, AvatarFallback, AvatarImage, Button } from "@methanium/ui";
import Text from "@tensamin/markdown/text";
import { ChevronDown, ChevronUp, Info } from "lucide-react";
import { useEffect, useState } from "react";
import { useCrypto } from "@tensamin/crypto/context";
import { useStorage } from "@tensamin/storage/context";
import { useUser } from "@tensamin/user/context";
import { ChevronDown, ChevronUp } from "lucide-react";
import { useState } from "react";
export default function Profile({ user }: { user: User }) {
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 (
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center">
<Avatar className="size-10">
<AvatarImage src={user.avatar} />
<AvatarImage src={user.Avatar} />
<AvatarFallback className="text-lg">
{user.display.slice(0, 2).toUpperCase()}
{user.Display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<p className="text-lg font-semibold">{user.display}</p>
<p className="text-muted-foreground">{user.username}</p>
<p className="text-lg font-semibold">{user.Display}</p>
<p className="text-muted-foreground">{user.Username}</p>
</div>
</div>
<Text value={user.about || ""} />
<Text value={user.About || ""} />
<Button
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"
@ -87,24 +37,14 @@ export default function Profile({ user }: { user: User }) {
</Button>
{showAdvancedInformation && (
<div className="flex flex-col">
<p className="text-white flex gap-1 overflow-hidden text-ellipsis whitespace-nowrap">
Shared Code: <code>{sharedSecretCode}</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 className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
Iota ID: <code>{user.IotaId}</code>
</p>
<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 className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
User ID: <code>{user.user_id}</code>
</p>
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
Public Key: <code>{user.public_key}</code>
Public Key: <code>{user.PublicKey}</code>
</p>
</div>
)}

View file

@ -4,17 +4,22 @@ import {
PopoverContent,
PopoverTrigger,
useIsMobile,
} from "@tensamin/ui";
} from "@methanium/ui";
import { ArrowLeft, House, Phone, Settings, User } from "lucide-react";
import { useLocation, useNavigate, useSearch } from "@tanstack/react-router";
import { joinCall, useCall } from "@tensamin/call/store";
import Wrapper from "@tensamin/user/wrapper";
import { Skeleton } from "@tensamin/ui";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@tensamin/ui";
import { Skeleton } from "@methanium/ui";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "@methanium/ui";
import { displayCallId } from "@tensamin/call/utils";
import { useState } from "react";
import { SidebarTrigger, useSidebar } from "@tensamin/ui";
import { WindowControls as Controls } from "@tensamin/ui";
import { SidebarTrigger, useSidebar } from "@methanium/ui";
import { WindowControls as Controls } from "@methanium/ui";
import { useSession } from "@tensamin/storage/session";
import Profile from "./modals/profile";
@ -28,7 +33,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
const { id } = useSearch({ strict: false });
const currentCalls = calls.filter((call) =>
call.call_members.some((member) => member === id),
call.CallMembers.some((member) => member === id),
);
const isMobile = useIsMobile();
@ -43,7 +48,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
<div className="flex items-center justify-center gap-2">
{forMobile ? (
<SidebarTrigger
className="w-9 h-9 aspect-square rounded-lg ml-2"
className="w-9 h-9! aspect-square rounded-lg ml-2"
variant="outline"
>
<ArrowLeft className="size-4.5" />
@ -52,15 +57,14 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
<>
<Button
onClick={() => navigate({ to: "/" })}
className="w-9 h-9 aspect-square rounded-lg"
className="w-9 h-9! aspect-square rounded-lg"
variant="outline"
>
<House className="size-4.5" />
</Button>
<Button
// @ts-expect-error TanStack router doesn't properly detect the settings route
onClick={() => navigate({ to: "/settings" })}
className="w-9 h-9 aspect-square rounded-lg"
className="w-9 h-9! aspect-square rounded-lg"
variant="outline"
>
<Settings className="size-4.5" />
@ -72,7 +76,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
userId={id}
component={(user) =>
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}>
<PopoverTrigger
@ -85,7 +89,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
}}
>
<p className="font-medium text-[1.07rem]">
{user?.display}
{user?.Display}
</p>
</Button>
}
@ -96,7 +100,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
</Popover>
)
}
loading={<Skeleton className="ml-3 w-40 h-5" />}
loading={<Skeleton className="w-30 h-5" />}
/>
)}
</div>
@ -110,7 +114,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
onClick={() => {
void joinCall(id);
}}
className="w-9 h-9 aspect-square rounded-lg"
className="w-9 h-9! aspect-square rounded-lg"
variant="outline"
>
<Phone />
@ -121,11 +125,11 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
onClick={() => {
void joinCall(
id,
currentCalls[0].call_secret,
currentCalls[0].call_id,
currentCalls[0].CallSecret,
currentCalls[0].CallId,
);
}}
className="w-9 h-9 aspect-square rounded-lg"
className="w-9 h-9! aspect-square rounded-lg"
>
<Phone />
</Button>
@ -147,13 +151,13 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
<SelectContent className="p-1">
{currentCalls.map((call) => (
<SelectItem
value={call.call_id}
key={call.call_id}
value={call.CallId}
key={call.CallId}
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>
))}
</SelectContent>
@ -176,7 +180,7 @@ export function MobileNavbar() {
<div className="z-51 w-full shrink-0 pb-[env(safe-area-inset-bottom)] bg-card border-t">
<div className="z-51 w-full h-15 grid grid-cols-3 items-center place-items-center px-5">
<SidebarTrigger
className="text-foreground w-12! h-12! aspect-square rounded-xl flex flex-col gap-1"
className="text-foreground w-12! h-12! aspect-square rounded-xl flex flex-col gap-1 border-0!"
variant="link"
>
<User className="size-4.5" />
@ -187,7 +191,7 @@ export function MobileNavbar() {
navigate({ to: "/" });
setOpenMobile(false);
}}
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1"
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1 border-0! hover:bg-transparent!"
variant="link"
>
<House className="size-4.5" />
@ -195,11 +199,10 @@ export function MobileNavbar() {
</Button>
<Button
onClick={() => {
// @ts-expect-error TanStack router doesn't properly detect the settings route
navigate({ to: "/settings" });
setOpenMobile(false);
}}
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1"
className="text-foreground w-12 h-12 aspect-square rounded-xl flex flex-col gap-1 border-0! hover:bg-transparent!"
variant="link"
>
<Settings className="size-4.5" />

View file

@ -1,13 +1,21 @@
import { Button, cn, useIsMobile } from "@tensamin/ui";
import { Input } from "@tensamin/ui";
import { Label } from "@tensamin/ui";
import { Button, cn, useIsMobile } from "@methanium/ui";
import { Input } from "@methanium/ui";
import { Label } from "@methanium/ui";
import { useStorage } from "@tensamin/storage/context";
import { log, toast } from "@tensamin/shared/log";
import { File } from "lucide-react";
import * as React from "react";
import {
type ChangeEvent,
type FormEvent,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { z } from "zod";
import { isTauri } from "@tauri-apps/api/core";
import QrCodeScanner from "@tensamin/tauri/qrCodeScanner";
import { useNavigate } from "@tanstack/react-router";
const fetchedUser = z.object({
id: z.uuidv4(),
@ -24,7 +32,7 @@ const fetchedUser = z.object({
const formSchema = z.object({
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 +69,7 @@ function parseTuFileContent(rawFileContent: string): {
? Number(userIdString.split("@")[0])
: Number(userIdString);
const rawDomain = userIdString.includes("@")
? userIdString.split("@")[1]
: null;
const domain = rawDomain
? rawDomain.includes(":")
? rawDomain
: rawDomain + ":1984"
: null;
const domain = userIdString.includes("@") ? userIdString.split("@")[1] : null;
if (!userId || !privateKey) {
throw new Error("Invalid file");
@ -76,7 +77,6 @@ function parseTuFileContent(rawFileContent: string): {
console.log({
domain,
rawDomain,
userId,
});
@ -85,12 +85,32 @@ function parseTuFileContent(rawFileContent: string): {
export default function Form() {
const isMobile = useIsMobile();
const uploadRef = React.useRef<HTMLInputElement | null>(null);
const [isDragging, setIsDragging] = React.useState(false);
const uploadRef = useRef<HTMLInputElement | null>(null);
const [isDragging, setIsDragging] = useState(false);
const { save } = useStorage();
const navigate = useNavigate();
const loginPendingRef = useRef(false);
const persistLogin = useCallback(
async (userId: number, privateKey: string, domain?: string | null) => {
if (loginPendingRef.current) return false;
loginPendingRef.current = true;
try {
if (domain) await save("omega_url", `https://${domain}/`);
await save("mtp_keyring", privateKey, { secure: true });
await save("session_id", Date.now());
await save("user_id", userId);
await navigate({ to: "/", replace: true });
return true;
} finally {
loginPendingRef.current = false;
}
},
[navigate, save],
);
// Process dropped files
const processDroppedFile = React.useCallback(
const processDroppedFile = useCallback(
async (file: globalThis.File): Promise<void> => {
try {
if (!file.name.endsWith(".tu")) {
@ -101,25 +121,18 @@ export default function Form() {
const raw = await file.text();
const parsed = parseTuFileContent(raw);
await save("session_id", Date.now());
await save("user_id", parsed.userId);
await save("private_key", parsed.privateKey);
if (parsed.domain) {
await save("ttp_url", `https://${parsed.domain}/`);
}
location.href = "/";
await persistLogin(parsed.userId, parsed.privateKey, parsed.domain);
} catch (error) {
log(0, "login", "red", error);
toast("error", "Failed to load file");
}
},
[save],
[persistLogin],
);
// Handle .tu files
const handleFileInputChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
const handleFileInputChange = useCallback(
async (event: ChangeEvent<HTMLInputElement>): Promise<void> => {
const file = event.currentTarget.files?.[0];
if (!file) {
@ -133,7 +146,7 @@ export default function Form() {
);
// Drag and drop listener
React.useEffect(() => {
useEffect(() => {
let dragCounter = 0;
const handleDragEnter = (event: DragEvent) => {
@ -203,8 +216,8 @@ export default function Form() {
* @param event Form submit event.
* @returns Promise that resolves after login processing.
*/
const handleCredentialsSubmit = React.useCallback(
async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
const handleCredentialsSubmit = useCallback(
async (event: FormEvent<HTMLFormElement>): Promise<void> => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
@ -220,14 +233,9 @@ export default function Form() {
const actualUsername = inputUsername.includes("@")
? inputUsername.split("@")[0]
: inputUsername;
const rawDomain = inputUsername.includes("@")
const domain = inputUsername.includes("@")
? inputUsername.split("@")[1]
: null;
const domain = rawDomain
? rawDomain.includes(":")
? rawDomain
: rawDomain + ":1984"
: null;
try {
const response = await fetch(
@ -249,20 +257,13 @@ export default function Form() {
const user = parse.data;
await save("session_id", Date.now());
await save("user_id", user.user_id);
await save("private_key", inputParse.data.private_key);
if (domain) {
await save("ttp_url", `https://${domain}/`);
}
location.href = "/";
await persistLogin(user.user_id, inputParse.data.mtp_keyring, domain);
} catch (error) {
log(0, "login", "red", error);
toast("error", "Failed to fetch user data");
}
},
[save],
[persistLogin],
);
return (
@ -282,15 +283,7 @@ export default function Form() {
const { userId, privateKey, domain } =
parseTuFileContent(decoded);
await save("session_id", Date.now());
await save("user_id", userId);
await save("private_key", privateKey);
if (domain) {
await save("ttp_url", `https://${domain}/`);
}
location.href = "/";
await persistLogin(userId, privateKey, domain);
} catch (error) {
log(0, "login", "red", error);
toast("error", "Failed to parse QR code data");
@ -346,8 +339,8 @@ export default function Form() {
<Input required type="text" id="username" name="username" />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="private_key">Private Key</Label>
<Input required type="password" id="private_key" name="private_key" />
<Label htmlFor="mtp_keyring">MTP Keyring</Label>
<Input required type="password" id="mtp_keyring" name="mtp_keyring" />
</div>
<Button className="mt-auto" type="submit">
Login

View file

@ -12,7 +12,6 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuGroup,
DropdownMenuLabel,
Dialog,
DialogContent,
DialogFooter,
@ -27,9 +26,9 @@ import {
SelectValue,
SelectContent,
SelectItem,
} from "@tensamin/ui";
} from "@methanium/ui";
import { isTauri } from "@tauri-apps/api/core";
import { useIsMobile } from "@tensamin/ui";
import { useIsMobile } from "@methanium/ui";
import { MobileNavbar } from "./navbar";
import SidebarBox from "@tensamin/call/sidebarBox";
@ -38,12 +37,10 @@ import { Ellipsis, Check } from "lucide-react";
import { useState } from "react";
import type { User } from "@tensamin/user/context";
import type z from "zod";
import { ttp } from "@tensamin/shared/data";
import { useTTP } from "@tensamin/ttp";
import { mtp } from "@tensamin/shared/data";
import { useMTP } from "@tensamin/mtp";
type OnlineStatus = z.infer<
typeof ttp.get_user_data.response.shape.online_status
>;
type OnlineStatus = z.infer<typeof mtp.GetUserData.response.shape.OnlineStatus>;
const onlineStatusLabels: Record<OnlineStatus, string> = {
user_online: "Online",
@ -74,7 +71,7 @@ function StatusDialog({
user: User;
open: boolean;
onOpenChange: (open: boolean) => void;
send: ReturnType<typeof useTTP>["send"];
send: ReturnType<typeof useMTP>["send"];
draftStatus: string;
setDraftStatus: (value: string) => void;
draftOnlineStatus: OnlineStatus;
@ -89,8 +86,8 @@ function StatusDialog({
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
setDraftStatus(user.status ?? "");
setDraftOnlineStatus(user.online_status);
setDraftStatus(user.Status ?? "");
setDraftOnlineStatus(user.OnlineStatus);
setErrorMessage("");
setSaveSucceeded(false);
}
@ -145,11 +142,10 @@ function StatusDialog({
onClick={async () => {
const payload = {
...(draftStatus && { status: draftStatus }),
online_status: draftOnlineStatus,
OnlineStatus: draftOnlineStatus,
};
const validation =
ttp.change_user_data.request.safeParse(payload);
const validation = mtp.ChangeUserData.request.safeParse(payload);
if (!validation.success) {
setSaveSucceeded(false);
@ -160,7 +156,7 @@ function StatusDialog({
}
try {
await send("change_user_data", validation.data);
await send("ChangeUserData", validation.data);
setSaveSucceeded(true);
setErrorMessage("");
} catch (err) {
@ -195,7 +191,7 @@ export default function Sidebar() {
const [statusErrorMessage, setStatusErrorMessage] = useState("");
const [statusSaveSucceeded, setStatusSaveSucceeded] = useState(false);
const { send } = useTTP();
const { send } = useMTP();
const content = (
<>
@ -228,11 +224,10 @@ export default function Sidebar() {
/>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuLabel>Profile</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => {
setDraftStatus(user.status ?? "");
setDraftOnlineStatus(user.online_status);
setDraftStatus(user.Status ?? "");
setDraftOnlineStatus(user.OnlineStatus);
setStatusErrorMessage("");
setStatusSaveSucceeded(false);
setDialogOpen(true);
@ -250,8 +245,8 @@ export default function Sidebar() {
open={dialogOpen}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
setDraftStatus(user.status ?? "");
setDraftOnlineStatus(user.online_status);
setDraftStatus(user.Status ?? "");
setDraftOnlineStatus(user.OnlineStatus);
setStatusErrorMessage("");
setStatusSaveSucceeded(false);
}

View file

@ -1,4 +1,4 @@
import * as React from "react";
import { useRef, useState } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import Switch from "./switch";
@ -8,14 +8,14 @@ import { Loader2 } from "lucide-react";
import { useSession } from "@tensamin/storage/session";
export default function List() {
const [category, setCategory] = React.useState<
"conversations" | "communities"
>("conversations");
const [category, setCategory] = useState<"conversations" | "communities">(
"conversations",
);
const { contacts, communities } = useSession();
const items = category === "conversations" ? contacts : communities;
const scrollRef = React.useRef<HTMLDivElement | null>(null);
const scrollRef = useRef<HTMLDivElement | null>(null);
// TanStack Virtual is intentionally used here; React Compiler memoization is skipped.
// eslint-disable-next-line react-hooks/incompatible-library
@ -65,7 +65,7 @@ export default function List() {
{category === "conversations" ? (
contacts?.[virtualItem.index] ? (
<ConversationModal
userId={contacts[virtualItem.index].user_id}
userId={contacts[virtualItem.index].UserId}
/>
) : null
) : communities?.[virtualItem.index] ? (

View file

@ -23,10 +23,10 @@ export default function Switch(props: {
return (
<div
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
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={{
left: props.category === "conversations" ? "0.25rem" : "50%",
width: "calc(50% - 0.25rem)",
@ -35,7 +35,7 @@ export default function Switch(props: {
<button
role="tab"
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"
? "text-foreground"
: "text-ring/50 hover:text-ring"
@ -47,7 +47,7 @@ export default function Switch(props: {
<button
role="tab"
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"
? "text-foreground"
: "text-ring/50 hover:text-ring"

View file

@ -6,9 +6,9 @@ import {
ContextMenuGroup,
ContextMenuItem,
ContextMenuTrigger,
} from "@tensamin/ui";
} from "@methanium/ui";
import { useNavigate } from "@tanstack/react-router";
import { useSidebar } from "@tensamin/ui";
import { useSidebar } from "@methanium/ui";
export default function ConversationModal({ userId }: { userId: number }) {
const navigate = useNavigate();

View file

@ -1,247 +0,0 @@
import { useState, useCallback, useEffect } from "react";
import { useStorage } from "@tensamin/storage/context";
import { Button } from "@tensamin/ui";
import { Checkbox } from "@tensamin/ui";
import { z } from "zod";
import { ErrorScreen } from "@tensamin/ui";
import { legalDocsSchema } from "@tensamin/shared/features/legal/schema";
import { log } from "@tensamin/shared/log";
import { Link } from "@tensamin/ui";
import { Label } from "@tensamin/ui";
import { CreateScreen } from "@tensamin/ui";
// Prevents the user from using Tensamin without accepting the privacy policy and terms of service.
export default function Screen(props: { children: React.ReactNode }) {
const { load, save } = useStorage();
const [error, setError] = useState("");
const [errorDescription, setErrorDescription] = useState("");
const [remoteDocs, setRemoteDocs] = useState<
z.infer<typeof legalDocsSchema> | undefined
>(undefined);
const [localDocs, setLocalDocs] = useState<
z.infer<typeof legalDocsSchema> | undefined
>(undefined);
const [loading, setLoading] = useState(true);
const [acceptedPP, acceptPP] = useState(false);
const [acceptedTOS, acceptTOS] = useState(false);
const [hasContinued, setHasContinued] = useState(false);
const [userId, setUserId] = useState<number | undefined>(undefined);
// Saves
const handleContinueLegal = useCallback((): void => {
const currentDocs = remoteDocs;
if (!currentDocs) {
return;
}
save("accepted_privacy_policy", true);
save("accepted_terms_of_service", true);
save("legal_docs", currentDocs);
setHasContinued(true);
}, [remoteDocs, save]);
useEffect(() => {
let active = true;
void (async () => {
try {
const id = await load("user_id");
if (!active) {
return;
}
setUserId(id);
if (id === 0) {
return;
}
const current = await fetch("https://legal.tensamin.net/api/current")
.then((res) => res.json())
.catch((err) => {
if (!active) {
return undefined;
}
setError("Failed to load legal documents");
setErrorDescription(
"An error occurred while fetching the legal documents from the server. Please try again later.",
);
log(0, "Legal", "red", "Failed to fetch legal documents", err);
return undefined;
});
if (!active || current === undefined) {
return;
}
const safeCurrent = legalDocsSchema.safeParse(current);
if (!safeCurrent.success) {
setError("Failed to load legal documents");
setErrorDescription(
"The legal documents data received from the server is invalid. Please try again later.",
);
log(
0,
"Legal",
"red",
"Invalid legal documents data",
safeCurrent.error,
);
return;
}
setRemoteDocs(safeCurrent.data);
const currentLocalDocs = await load("legal_docs");
setLocalDocs(currentLocalDocs);
const [loadedAcceptedPP, loadedAcceptedTOS] = await Promise.all([
load("accepted_privacy_policy"),
load("accepted_terms_of_service"),
]);
if (!active) {
return;
}
acceptPP(
loadedAcceptedPP &&
!!currentLocalDocs &&
currentLocalDocs.pp.hash === safeCurrent.data.pp.hash,
);
acceptTOS(
loadedAcceptedTOS &&
!!currentLocalDocs &&
currentLocalDocs.tos.hash === safeCurrent.data.tos.hash,
);
} finally {
if (active) {
setLoading(false);
}
}
})();
return () => {
active = false;
};
}, [load]);
if (error !== "" && errorDescription !== "") {
return <ErrorScreen error={error} description={errorDescription} />;
}
if (loading || userId === undefined) {
return null;
}
const docsMatch =
localDocs !== undefined &&
remoteDocs !== undefined &&
localDocs.pp.hash === remoteDocs.pp.hash &&
localDocs.tos.hash === remoteDocs.tos.hash;
if (
(acceptedPP && acceptedTOS && (docsMatch || hasContinued)) ||
userId === 0
) {
return <>{props.children}</>;
}
return (
<CreateScreen>
<div className="h-full flex flex-col gap-15 p-10 py-20 md:p-40 w-full lg:w-2/3">
<h1 className="text-3xl md:text-4xl font-bold">
Privacy Policy & ToS
<p className="text-muted-foreground text-[20px] font-normal pt-3">
{remoteDocs?.pp.version} / {remoteDocs?.tos.version}
</p>
</h1>
<div className="w-full h-full flex flex-col items-center justify-center gap-5">
<div className="justify-start items-start flex flex-col gap-2">
<BigCheckbox
id="acceptPP"
checked={acceptedPP}
onChange={acceptPP}
label="I agree to the Privacy Policy"
/>
<BigCheckbox
id="acceptTOS"
checked={acceptedTOS}
onChange={acceptTOS}
label="I agree to the Terms of Service"
/>
<div className="w-full border-t-2" />
<Link
label="Privacy Policy"
link={`https://legal.tensamin.net/pp/${remoteDocs?.pp.version}`}
/>
<Link
label="Terms of Service"
link={`https://legal.tensamin.net/tos/${remoteDocs?.tos.version}`}
/>
</div>
</div>
<ContinueButton
disabled={!acceptedPP || !acceptedTOS}
onClick={handleContinueLegal}
/>
</div>
</CreateScreen>
);
}
// Components
function ContinueButton({
onClick,
disabled,
}: {
onClick: () => void;
disabled?: boolean;
}) {
return (
<div className="w-full flex justify-end">
<Button
size="lg"
className="text-md w-full md:w-auto"
onClick={onClick}
disabled={disabled}
>
Continue
</Button>
</div>
);
}
function BigCheckbox({
id,
label,
checked,
onChange,
}: {
id: string;
label: string;
checked: boolean;
onChange: (checked: boolean) => void;
}) {
return (
<div className="flex items-center space-x-2">
<Checkbox
id={id}
checked={checked}
onCheckedChange={onChange}
className="size-5.5 rounded-md flex items-center justify-center"
/>
<Label htmlFor={id} className="text-lg">
{label}
</Label>
</div>
);
}

View file

@ -1,178 +0,0 @@
import {
Button,
Checkbox,
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
Input,
Label,
Switch as UISwitch,
} from "@tensamin/ui";
import { useEffect, useState } from "react";
import { storageDefaults, type Storage } from "@tensamin/shared/data";
import { settingsStorageDefaults } from "@tensamin/shared/settings";
import { useStorage } from "@tensamin/storage/context";
type BooleanStorageKey = {
[K in keyof Storage]: Storage[K] extends boolean ? K : never;
}[keyof Storage];
type ListStorageKey = {
[K in keyof Storage]: Storage[K] extends (string | number)[] ? K : never;
}[keyof Storage];
type ListStorageItem<K extends ListStorageKey> =
Storage[K] extends Array<infer Item> ? Item : never;
export function Switch({
label,
id,
}: {
label: React.ReactNode;
id: keyof typeof settingsStorageDefaults & BooleanStorageKey;
}) {
const { save, load } = useStorage();
const [value, setValue] = useState<boolean>(settingsStorageDefaults[id]);
useEffect(() => {
load(id).then((value) => setValue(value));
}, [id, load]);
return (
<div className="flex gap-1">
<UISwitch
id={id}
checked={value}
onCheckedChange={(value) => {
setValue(value);
save(id, value);
}}
/>
<Label htmlFor={id}>{label}</Label>
</div>
);
}
export function List<K extends ListStorageKey>({
label,
id,
}: {
label: React.ReactNode;
id: K;
}) {
const { save, load } = useStorage();
const [items, setItems] = useState<Storage[K]>(storageDefaults[id]);
const [inputValue, setInputValue] = useState("");
const [selectedItems, setSelectedItems] = useState<Set<number>>(new Set());
useEffect(() => {
load(id).then((value) => setItems(value));
}, [id, load]);
const persistItems = (nextItems: Storage[K]) => {
setItems(nextItems);
save(id, nextItems);
};
const toStorageItem = (value: string): ListStorageItem<K> => {
const referenceItem = items[0] ?? storageDefaults[id][0];
if (typeof referenceItem === "number") {
return Number(value) as ListStorageItem<K>;
}
return value as ListStorageItem<K>;
};
const addItem = () => {
const trimmedValue = inputValue.trim();
if (!trimmedValue) return;
const nextItem = toStorageItem(trimmedValue);
if (typeof nextItem === "number" && Number.isNaN(nextItem)) return;
persistItems([...items, nextItem] as Storage[K]);
setInputValue("");
};
const deleteItems = (indexes: Set<number>) => {
const nextItems = items.filter(
(_, index) => !indexes.has(index),
) as Storage[K];
setItems(nextItems);
setSelectedItems(new Set());
save(id, nextItems);
};
return (
<div className="flex flex-col gap-2 pt-4">
<Label>{label}</Label>
<div className="flex flex-col gap-0 overflow-hidden p-1 border-2 rounded-xl">
<div className="flex gap-1">
<Input
value={inputValue}
onChange={(event) => setInputValue(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") addItem();
}}
/>
<Button onClick={addItem}>Add item</Button>
</div>
<div className="flex flex-col gap-0">
{items.map((item, index) => {
const labelId = `${String(id)}-${index}`;
const selected = selectedItems.has(index);
const deletingSelectedItems = selectedItems.size > 1;
return (
<ContextMenu key={`${String(item)}-${index}`}>
<ContextMenuTrigger
render={
<div className="grid grid-cols-[auto_auto_1fr] items-center gap-2 border-b px-1 py-2 last:border-b-0">
<Checkbox
id={labelId}
checked={selected}
onCheckedChange={(checked) => {
setSelectedItems((previous) => {
const nextSelected = new Set(previous);
if (checked) {
nextSelected.add(index);
} else {
nextSelected.delete(index);
}
return nextSelected;
});
}}
/>
<Label htmlFor={labelId}>{String(item)}</Label>
<div />
</div>
}
/>
<ContextMenuContent>
<ContextMenuItem
variant="destructive"
onClick={() =>
deleteItems(
deletingSelectedItems
? selectedItems
: new Set([index]),
)
}
>
{deletingSelectedItems ? "Delete Selected" : "Delete"}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
})}
</div>
</div>
</div>
);
}

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