Compare commits

..
Author SHA1 Message Date
ab2ac4813a Merge pull request 'feat(pwa): remove install prompt' (#76) from dev into main
All checks were successful
/ build-web (push) Successful in 5m0s
/ build-desktop (linux) (push) Successful in 11m28s
/ build-mobile (push) Successful in 29m19s
/ release (push) Successful in 2m5s
Reviewed-on: #76
2026-08-30 16:59:43 +03:00
0c0a341f1e
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
All checks were successful
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-web (push) Successful in 5m22s
/ build-desktop (linux) (push) Successful in 10m44s
/ build-mobile (push) Successful in 28m17s
/ release (push) Successful in 2m4s
2026-08-30 15:58:40 +02:00
f74ca52f4f
feat(nix-flake): rework nix package 2026-08-30 15:58:34 +02:00
Alex Emmet
94f11f60f6
Merge remote-tracking branch 'origin/dev' into dev
All checks were successful
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-web (push) Successful in 6m37s
/ build-desktop (linux) (push) Successful in 12m5s
/ build-mobile (push) Successful in 25m44s
/ release (push) Successful in 1m58s
2026-08-29 16:11:40 +02:00
Alex Emmet
f5461bbd42
chore(mtp): update type-maps 2026-08-29 16:11:24 +02:00
7c9d9529b4
feat(workflows): allow manual runs for the dev deploy
All checks were successful
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-web (push) Successful in 7m13s
/ build-desktop (linux) (push) Successful in 14m39s
/ build-mobile (push) Successful in 38m15s
/ release (push) Successful in 3m8s
2026-08-29 12:57:44 +02:00
8a04b327fc
chore(mtp): update type-maps
Some checks failed
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-mobile (push) Failing after 1m44s
/ build-desktop (linux) (push) Failing after 10m56s
/ build-web (push) Failing after 10m56s
/ release (push) Has been cancelled
2026-08-28 20:02:35 +02:00
0a304e44f2
feat(mtp): move useful stuff over to mtp directly
All checks were successful
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-web (push) Successful in 6m14s
/ build-desktop (linux) (push) Successful in 11m36s
/ build-mobile (push) Successful in 24m26s
/ release (push) Successful in 1m39s
2026-08-27 23:30:33 +02:00
094cb910aa
Merge remote-tracking branch 'origin/dev' into dev
Some checks failed
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-web (push) Successful in 7m14s
/ build-mobile (push) Failing after 12m6s
/ build-desktop (linux) (push) Successful in 12m16s
/ release (push) Has been skipped
# Conflicts:
#	apps/pwa/src/runtime.tsx
#	flake.nix
#	packages/mtp/src/context.test.tsx
#	packages/mtp/src/context.tsx
2026-08-27 19:39:52 +02:00
cf17365c8f
Merge branch 'main' into dev 2026-08-27 19:36:41 +02:00
aa34bd962b
temp(mtp): temp commit stuff that needs to get a rework 2026-08-27 19:35:17 +02:00
1d1b304bcf
feat(workflows): remove env thing 2026-08-27 19:32:48 +02:00
forgejo-actions
0fbedf6733 (qol): update release flake hash
Some checks were skipped
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
2026-08-27 12:59:08 +00:00
96b81d30b1
feat(pwa): keep service workers from getting registerd outside the pwa
Some checks failed
/ build-web (push) Successful in 5m32s
/ build-desktop (linux) (push) Successful in 9m55s
/ build-mobile (push) Successful in 18m33s
/ release (push) Failing after 1m42s
2026-08-27 14:38:53 +02:00
edcf0e15dd
chore(mtp): update type-maps
Some checks failed
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-web (push) Successful in 6m3s
/ build-desktop (linux) (push) Successful in 11m9s
/ build-mobile (push) Failing after 11m15s
/ release (push) Has been skipped
2026-08-25 21:00:32 +02:00
5056f084b4
chore(mtp): update type-maps
Some checks failed
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-desktop (linux) (push) Failing after 4s
/ build-web (push) Failing after 4s
/ build-mobile (push) Failing after 5s
/ release (push) Has been skipped
2026-08-25 20:58:32 +02:00
ede0199858
chore(mtp): udpate type maps
Some checks failed
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-web (push) Failing after 3m37s
/ build-desktop (linux) (push) Failing after 3m58s
/ build-mobile (push) Failing after 6m4s
/ release (push) Has been skipped
2026-08-25 20:48:56 +02:00
88a094c8bf
feat(pwa): remove update toast
Some checks failed
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ 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-08-25 20:46:14 +02:00
e43e03ff12
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
Some checks failed
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ build-web (push) Has been cancelled
2026-08-20 20:56:38 +02:00
97270abed8
Update workflows 2026-08-20 20:56:30 +02:00
2a55c87df1
[Updt] Mtp 0.3.0
Some checks failed
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Test native MTP (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-web (push) Failing after 3m32s
/ build-desktop (linux) (push) Failing after 2m47s
/ build-mobile (push) Failing after 5m29s
/ release (push) Has been skipped
2026-08-20 17:05:53 +02:00
forgejo-actions
57c7ceb27a (qol): update release flake hash 2026-08-18 20:46:45 +00:00
forgejo-actions
d0918ff384 (qol): update release flake hash 2026-08-18 20:46:44 +00:00
caa7d65b84
feat(pwa): remove install prompt
All checks were successful
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
/ build-web (push) Successful in 5m40s
/ build-desktop (linux) (push) Successful in 10m33s
/ build-mobile (push) Successful in 23m6s
/ release (push) Successful in 2m15s
2026-08-18 21:40:57 +02:00
0ac20f09e4 Merge pull request 'Add pwa & stuff' (#75) from dev into main
All checks were successful
/ build-web (push) Successful in 5m44s
/ build-desktop (linux) (push) Successful in 10m41s
/ build-mobile (push) Successful in 20m49s
/ release (push) Successful in 1m46s
Reviewed-on: #75
2026-08-18 22:32:16 +03:00
7b36218ffa
feat(pwa): add base
All checks were successful
/ build-web (push) Successful in 5m35s
/ build-desktop (linux) (push) Successful in 9m41s
/ build-mobile (push) Successful in 20m12s
/ release (push) Successful in 1m51s
Dependency builds / Build web (pull_request) Has been skipped
Dependency builds / Build desktop (pull_request) Has been skipped
Dependency builds / Build mobile (pull_request) Has been skipped
2026-08-18 18:44:45 +02:00
73db7e6b2b Update flake.nix 2026-08-12 15:12:05 +02:00
6e977bca7e
feat(mobile): use drawer for gif picker
Some checks failed
/ build-desktop (linux) (push) Failing after 6m36s
/ build-web (push) Failing after 7m7s
/ build-mobile (push) Failing after 9m21s
/ release (push) Has been skipped
fix(mobile): some ui bugs
fix(mobile): disable pinch zooming
2026-08-12 02:16:56 +02:00
eaf3f61ebd
feat(android): add zoom slider
All checks were successful
/ build-web (push) Successful in 8m21s
/ build-desktop (linux) (push) Successful in 18m41s
/ build-mobile (push) Successful in 44m57s
/ release (push) Successful in 9m27s
fix(android): gray screen bug
2026-08-11 15:50:52 +02:00
6f4cc62530
feat(gif-picker): add gif groups
Some checks failed
/ build-web (push) Successful in 8m40s
/ build-mobile (push) Failing after 11m17s
/ build-desktop (linux) (push) Successful in 14m8s
/ release (push) Has been skipped
feat(gif-picker): increase default size
2026-08-11 13:01:09 +02:00
eaeff1b8d5
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
Some checks failed
/ build-web (push) Successful in 8m15s
/ build-mobile (push) Failing after 11m13s
/ build-desktop (linux) (push) Successful in 13m21s
/ release (push) Has been skipped
2026-08-10 15:27:53 +02:00
4016181db9
feat(mtp): update type maps
fix(renovate): workflow not pulling submodules
2026-08-10 15:27:47 +02:00
2299555fe6 Merge pull request 'chore(deps): update dependency com.android.tools.build:gradle to v9' (#73) from renovate/com.android.tools.build-gradle-9.x into dev
Some checks failed
/ build-web (push) Successful in 12m49s
/ build-mobile (push) Failing after 15m57s
/ build-desktop (linux) (push) Successful in 17m37s
/ release (push) Has been skipped
2026-08-10 14:00:35 +02:00
f50b4eaebe chore(deps): update dependency com.android.tools.build:gradle to v9
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
Dependency builds / Build web (pull_request) Failing after 9m12s
Dependency builds / Build desktop (pull_request) Failing after 10m6s
Dependency builds / Build mobile (pull_request) Failing after 15m5s
2026-08-10 14:00:33 +02:00
825be4eb33
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
Some checks failed
/ build-web (push) Successful in 8m17s
/ build-mobile (push) Failing after 10m56s
/ release (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-08-10 13:48:42 +02:00
1c90a0c859
feat(status): make the set-status menu work
feat(user): optimise get() function for react
qol(todos): update todos
2026-08-10 13:48:32 +02:00
6dee7e8dcc Merge pull request 'chore(deps): update rust crate reqwest to 0.13' (#72) from renovate/reqwest-0.x into dev
Some checks failed
/ build-web (push) Successful in 12m8s
/ build-mobile (push) Failing after 15m1s
/ build-desktop (linux) (push) Successful in 17m16s
/ release (push) Has been skipped
2026-08-10 13:00:41 +02:00
41258a8483 chore(deps): update rust crate reqwest to 0.13
Some checks failed
renovate/artifacts Artifact file update failure
renovate/stability-days Updates have met minimum release age requirement
Dependency builds / Build web (pull_request) Failing after 8m47s
Dependency builds / Build desktop (pull_request) Failing after 8m21s
Dependency builds / Build mobile (pull_request) Failing after 13m21s
2026-08-10 13:00:39 +02:00
746f0cc9a4 Merge pull request 'chore(deps): update dependency org.jetbrains.kotlin:kotlin-gradle-plugin to v2.4.10' (#71) from renovate/org.jetbrains.kotlin-kotlin-gradle-plugin-2.x into dev
Some checks failed
/ build-web (push) Successful in 10m21s
/ build-mobile (push) Failing after 19m34s
/ build-desktop (linux) (push) Successful in 19m43s
/ release (push) Has been skipped
2026-08-10 12:00:38 +02:00
c1baf3576d chore(deps): update dependency org.jetbrains.kotlin:kotlin-gradle-plugin to v2.4.10
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
Dependency builds / Build web (pull_request) Failing after 12m5s
Dependency builds / Build mobile (pull_request) Failing after 16m5s
Dependency builds / Build desktop (pull_request) Failing after 16m13s
2026-08-10 12:00:36 +02:00
7ffbab6b08 Merge pull request 'chore(deps): update tauri-build digest to 2f11853' (#70) from renovate/tauri-build-digest into dev
All checks were successful
/ build-web (push) Successful in 13m53s
/ build-desktop (linux) (push) Successful in 19m49s
/ build-mobile (push) Successful in 30m21s
/ release (push) Successful in 3m24s
2026-08-10 11:00:37 +02:00
f1af0f210d chore(deps): update tauri-build digest to e2e585a
Some checks failed
renovate/stability-days Updates have not met minimum release age requirement
Dependency builds / Build web (pull_request) Failing after 10m29s
Dependency builds / Build desktop (pull_request) Failing after 11m47s
Dependency builds / Build mobile (pull_request) Failing after 15m0s
2026-08-10 11:00:32 +02:00
a40bf3a2c2 Merge pull request 'chore(deps): update mtp-transport digest to b067614' (#69) from renovate/mtp-transport-digest into dev
All checks were successful
/ build-web (push) Successful in 8m22s
/ build-desktop (linux) (push) Successful in 13m59s
/ build-mobile (push) Successful in 25m47s
/ release (push) Successful in 3m26s
2026-08-10 00:16:46 +02:00
dbb407606b
feat(markdown): migrate to methanium/ui's markdown
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-08-10 00:16:42 +02:00
f763af13be chore(deps): update mtp-transport digest to b067614
Some checks failed
renovate/stability-days Updates have not met minimum release age requirement
Dependency builds / Build web (pull_request) Failing after 14m21s
Dependency builds / Build desktop (pull_request) Failing after 14m21s
Dependency builds / Build mobile (pull_request) Failing after 15m6s
2026-08-09 21:01:24 +02:00
f83e71be6b
fix(legal): license copy script
Some checks failed
/ build-web (push) Failing after 6m53s
/ build-desktop (linux) (push) Failing after 7m26s
/ build-mobile (push) Failing after 9m16s
/ release (push) Has been skipped
2026-08-09 20:16:24 +02:00
b7c94123fa
feat(chat): add media overlay
Some checks failed
/ build-desktop (linux) (push) Failing after 4m45s
/ build-web (push) Failing after 5m12s
/ build-mobile (push) Failing after 7m34s
/ release (push) Has been skipped
2026-08-09 16:46:37 +02:00
32671f32fd
wip(chat): overall improvements
Some checks failed
/ build-web (push) Failing after 4m55s
/ build-desktop (linux) (push) Failing after 5m0s
/ build-mobile (push) Failing after 7m32s
/ release (push) Has been skipped
2026-08-09 12:05:04 +02:00
2b4207f8fa
fix(deps): checksums
Some checks failed
/ build-desktop (linux) (push) Failing after 4m58s
/ build-web (push) Failing after 5m3s
/ build-mobile (push) Failing after 7m41s
/ release (push) Has been skipped
2026-08-09 11:53:50 +02:00
5f44b89043
chore(deps): update pnpm-lock.yaml
Some checks failed
/ build-web (push) Failing after 7m9s
/ build-desktop (linux) (push) Failing after 7m52s
/ build-mobile (push) Failing after 12m23s
/ release (push) Has been skipped
2026-08-08 23:30:08 +02:00
31ff5a542f
chore(mtp): update type-maps
Some checks failed
/ build-desktop (linux) (push) Failing after 4m37s
/ build-web (push) Failing after 4m37s
/ build-mobile (push) Failing after 7m11s
/ release (push) Has been skipped
2026-08-07 23:42:44 +02:00
99c38162a6
chore(babysit): cleanup slop
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-08-07 23:41:49 +02:00
ef24ea4ca0
Merge branch 'dev' of ssh://git.methanium.net/Tensamin/Client into dev
Some checks failed
/ build-web (push) Failing after 1m11s
/ build-mobile (push) Failing after 1m24s
/ build-desktop (linux) (push) Failing after 1m24s
/ release (push) Has been skipped
2026-08-07 23:18:07 +02:00
45d39cb47c
chore(deps): fully update repo (except rust)
Some checks failed
/ build-web (push) Failing after 5m51s
/ build-desktop (linux) (push) Failing after 5m58s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-08-07 23:10:28 +02:00
c182f2e9ac
Merge branch 'dev' of ssh://git.methanium.net/Tensamin/Client into dev 2026-08-07 23:09:03 +02:00
ec019a4dff
[Upd] User states 2026-08-07 23:05:26 +02:00
b76143e2e2
chore(deps): update everything
Some checks failed
/ build-desktop (linux) (push) Failing after 5m49s
/ build-web (push) Failing after 5m56s
/ build-mobile (push) Failing after 8m38s
/ release (push) Has been skipped
2026-08-07 21:51:35 +02:00
01b68300ab Merge pull request 'chore(deps): update dependency @codemirror/view to v6.43.8' (#60) from renovate/codemirror-view-6.x-lockfile into dev
Some checks failed
/ build-web (push) Failing after 6m18s
/ build-desktop (linux) (push) Failing after 6m25s
/ build-mobile (push) Failing after 9m5s
/ release (push) Has been skipped
2026-08-06 22:00:42 +02:00
3954d776ce chore(deps): update dependency @codemirror/view to v6.43.6
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
2026-08-06 22:00:39 +02:00
860fd2ec08 Merge pull request 'chore(deps): update dependency @codemirror/state to v6.7.1' (#59) from renovate/codemirror-state-6.x-lockfile into dev
Some checks failed
/ build-web (push) Successful in 11m14s
/ build-desktop (linux) (push) Successful in 15m23s
/ build-mobile (push) Failing after 16m18s
/ release (push) Has been skipped
2026-08-06 21:00:47 +02:00
dfb82373c6 chore(deps): update dependency @codemirror/state to v6.7.1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
2026-08-06 21:00:43 +02:00
927a719281 Merge pull request 'chore(deps): update dependency @livekit/components-react to v2.9.23' (#63) from renovate/livekit-components-react-2.x-lockfile into dev
Some checks failed
/ build-web (push) Successful in 10m19s
/ build-desktop (linux) (push) Successful in 15m20s
/ build-mobile (push) Failing after 15m48s
/ release (push) Has been skipped
2026-08-06 20:01:30 +02:00
7f64659e23 Merge pull request 'chore(deps): update dependency @codemirror/lang-markdown to v6.5.2' (#58) from renovate/codemirror-lang-markdown-6.x-lockfile into dev
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-08-06 20:01:27 +02:00
35769b1aae chore(deps): update dependency @codemirror/lang-markdown to v6.5.1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
2026-08-06 20:01:22 +02:00
a731f8ee42 chore(deps): update dependency @livekit/components-react to v2.9.23
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
2026-08-06 19:01:31 +02:00
forgejo-actions
beafb623ba (qol): update release flake hash 2026-08-06 14:47:25 +00:00
forgejo-actions
ac1bdf5ac2 (qol): update release flake hash 2026-08-06 14:47:23 +00:00
22a4cd2f0e
Merge branch 'main' into dev
Some checks failed
/ build-web (push) Failing after 10m19s
/ build-desktop (linux) (push) Failing after 10m19s
/ build-mobile (push) Failing after 12m47s
/ release (push) Has been skipped
2026-08-06 16:19:28 +02:00
3725587232
chore(deps): remove packageManager
Some checks failed
/ build-desktop (linux) (push) Failing after 3m24s
/ build-web (push) Failing after 3m32s
/ build-mobile (push) Failing after 5m15s
/ release (push) Has been skipped
2026-08-06 16:17:12 +02:00
f0dcfbc63e Merge pull request 'chore(deps): update pnpm to v11.20.0' (#54) from renovate/pnpm-11.x into dev
Some checks failed
/ build-web (push) Failing after 3m53s
/ build-desktop (linux) (push) Failing after 3m48s
/ build-mobile (push) Failing after 6m52s
/ release (push) Has been skipped
2026-08-06 14:19:42 +02:00
00ff0b00d4 Merge pull request 'chore(deps): update dependency reselect to v5.2.0' (#53) from renovate/reselect-5.x into dev
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-08-06 14:19:40 +02:00
4d72469806 Merge pull request 'chore(deps): update rust crate jni to 0.22' (#56) from renovate/jni-0.x into dev
Some checks failed
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ release (push) Has been cancelled
2026-08-06 14:19:40 +02:00
forgejo-actions
9ae7dbe35e (qol): update release flake hash 2026-08-06 12:19:37 +00:00
forgejo-actions
2c405af72d (qol): update release flake hash 2026-08-06 12:19:35 +00:00
652dab0351
Merge remote-tracking branch 'refs/remotes/origin/main'
All checks were successful
/ build-web (push) Successful in 8m19s
/ build-desktop (linux) (push) Successful in 14m8s
/ build-mobile (push) Successful in 24m11s
/ release (push) Successful in 3m42s
2026-08-06 13:52:22 +02:00
1ea13acc3b
fix(mobile): build errors 2026-08-06 13:49:16 +02:00
29e80d6d02 chore(deps): update rust crate jni to 0.22 2026-08-06 13:02:05 +02:00
f3ca5d637c chore(deps): update pnpm to v11.20.0
Some checks failed
renovate/artifacts Artifact file update failure
2026-08-06 12:02:46 +02:00
565f39c255 chore(deps): update dependency reselect to v5.2.0
Some checks failed
renovate/artifacts Artifact file update failure
2026-08-06 12:02:38 +02:00
5090b67deb Merge pull request '(feat): dep updates' (#52) from dev into main
Some checks failed
/ build-web (push) Successful in 17m38s
/ build-desktop (linux) (push) Successful in 26m46s
/ build-mobile (push) Failing after 36m56s
/ release (push) Has been skipped
Reviewed-on: #52
2026-08-06 11:53:46 +02:00
2061c3c393
(feat): enable renovate
Some checks failed
/ build-web (push) Successful in 10m15s
/ build-desktop (linux) (push) Successful in 26m4s
/ build-mobile (push) Failing after 35m54s
/ release (push) Has been skipped
2026-08-06 11:53:05 +02:00
83427a344e
(chore): refresh dependency licenses
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-08-06 11:50:56 +02:00
6f9ae31f7d
Merge pull request 'chore(deps): update tauri-build digest to c0bd0d5' (#39) into dev 2026-08-06 11:49:04 +02:00
c442803412
Merge pull request 'chore(deps): update dependency androidx.test.espresso:espresso-core to v3.7.0' (#44) into dev
# Conflicts:
#	apps/tauri/src-tauri/gen/android/app/build.gradle.kts
2026-08-06 11:49:04 +02:00
d59f9455bf
Merge pull request 'chore(deps): update dependency androidx.activity:activity-ktx to v1.13.0' (#43) into dev
# Conflicts:
#	apps/tauri/src-tauri/gen/android/app/build.gradle.kts
2026-08-06 11:48:52 +02:00
6dcec11426
(chore): pause renovate updates 2026-08-06 11:48:34 +02:00
bda3bdcd0a
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-08-06 11:47:30 +02:00
8946da05cc
(temp): go back to 0.0.11 2026-08-06 11:47:22 +02:00
60fbe3c9ec Merge pull request 'chore(deps): update dependency androidx.test.ext:junit to v1.3.0' (#45) from renovate/androidx.test.ext-junit-1.x into dev
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ build-web (push) Has been cancelled
2026-08-06 11:46:58 +02:00
03735f9230 Merge pull request 'chore(deps): update dependency androidx.webkit:webkit to v1.16.0' (#46) from renovate/androidx.webkit-webkit-1.x into dev
Some checks failed
/ release (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-08-06 11:46:56 +02:00
27b60005cc Merge pull request 'chore(deps): update dependency com.android.tools.build:gradle to v8.13.2' (#47) from renovate/com.android.tools.build-gradle-8.x into dev
Some checks failed
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ release (push) Has been cancelled
2026-08-06 11:46:55 +02:00
80d74b3ac7 Merge pull request 'chore(deps): update dependency com.google.android.material:material to v1.14.0' (#48) from renovate/com.google.android.material-material-1.x into dev
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-08-06 11:46:53 +02:00
3e80a37566 Merge pull request 'chore(deps): update dependency deepfilternet3-noise-filter to v1.3.0' (#49) from renovate/deepfilternet3-noise-filter-1.x into dev
Some checks failed
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ release (push) Has been cancelled
2026-08-06 11:46:52 +02:00
2f8bf376b7 Merge pull request 'chore(deps): update dependency esbuild to ^0.28.0' (#50) from renovate/esbuild-0.x into dev
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-08-06 11:46:49 +02:00
cc8cd9c017 Merge pull request 'chore(deps): update dependency recharts to v3.10.1' (#51) from renovate/recharts-3.x into dev
Some checks failed
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ release (push) Has been cancelled
2026-08-06 11:46:49 +02:00
1b17158ad5
(fix): mobile notification showing "Connected" if the device is offline
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-08-06 11:46:44 +02:00
bb25ed83ef chore(deps): update dependency recharts to v3.10.1
Some checks failed
renovate/artifacts Artifact file update failure
2026-08-06 09:01:55 +02:00
959f6638ad chore(deps): update dependency esbuild to ^0.28.0
Some checks failed
renovate/artifacts Artifact file update failure
2026-08-06 08:01:48 +02:00
48f04adfd9 chore(deps): update dependency deepfilternet3-noise-filter to v1.3.0
Some checks failed
renovate/artifacts Artifact file update failure
2026-08-06 08:01:45 +02:00
0e2995454e chore(deps): update dependency com.google.android.material:material to v1.14.0 2026-08-06 07:01:41 +02:00
acc48c1207 chore(deps): update dependency com.android.tools.build:gradle to v8.13.2 2026-08-06 07:01:40 +02:00
d66b404368 chore(deps): update dependency androidx.webkit:webkit to v1.16.0 2026-08-06 06:01:38 +02:00
5914419aee chore(deps): update dependency androidx.test.ext:junit to v1.3.0 2026-08-06 06:01:36 +02:00
625314cb5a chore(deps): update dependency androidx.test.espresso:espresso-core to v3.7.0 2026-08-06 05:01:48 +02:00
6c145d293d chore(deps): update dependency androidx.activity:activity-ktx to v1.13.0 2026-08-06 05:01:47 +02:00
4e97368f2f chore(deps): update tauri-build digest to c0bd0d5 2026-08-06 05:01:02 +02:00
d07202386f Merge pull request 'chore(deps): update dependency @tanstack/react-store to ^0.11.0' (#41) from renovate/tanstack-react-store-0.x into dev
Some checks failed
/ build-web (push) Failing after 5m39s
/ build-desktop (linux) (push) Failing after 6m46s
/ build-mobile (push) Failing after 7m53s
/ release (push) Has been skipped
2026-08-06 04:01:02 +02:00
a71a9effae Merge pull request 'chore(deps): update dependency @tanstack/store to ^0.11.0' (#42) from renovate/tanstack-store-0.x into dev
Some checks failed
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ release (push) Has been cancelled
2026-08-06 04:01:01 +02:00
7b3367cff0 Merge pull request 'chore(deps): update mtp-transport digest to e8b7c43' (#38) from renovate/mtp-transport-digest into dev
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-08-06 04:00:58 +02:00
f14bfe6233 chore(deps): update mtp-transport digest to 7182272 2026-08-06 04:00:54 +02:00
2faf100fa2 chore(deps): update dependency @tanstack/store to ^0.11.0
Some checks failed
renovate/artifacts Artifact file update failure
2026-08-06 03:01:45 +02:00
faebd27286 chore(deps): update dependency @tanstack/react-store to ^0.11.0
Some checks failed
renovate/artifacts Artifact file update failure
2026-08-06 03:01:43 +02:00
5b2ac5315f Merge pull request 'chore(deps): update dependency @tanstack/devtools-event-client to ^0.5.0' (#40) from renovate/tanstack-devtools-event-client-0.x into dev
Some checks failed
/ build-web (push) Failing after 6m7s
/ build-desktop (linux) (push) Failing after 7m5s
/ build-mobile (push) Failing after 9m22s
/ release (push) Has been skipped
2026-08-06 03:01:03 +02:00
e61be9e5e0 Merge pull request 'chore(deps): update mtp digest to e8b7c43' (#37) from renovate/mtp-digest into dev
Some checks failed
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ release (push) Has been cancelled
2026-08-06 03:01:00 +02:00
d30f7100b1 chore(deps): update mtp digest to 7182272 2026-08-06 03:00:56 +02:00
e35fd3d96d chore(deps): update dependency @tanstack/devtools-event-client to ^0.5.0
Some checks failed
renovate/artifacts Artifact file update failure
2026-08-06 02:01:43 +02:00
f2343297e0
(feat): bump version
All checks were successful
/ build-web (push) Successful in 17m57s
/ build-desktop (linux) (push) Successful in 23m37s
/ build-mobile (push) Successful in 33m48s
/ release (push) Successful in 3m32s
2026-08-06 00:53:44 +02:00
5a7d5008a3
Merge remote-tracking branch 'origin/main' into dev
Some checks failed
/ build-web (push) Successful in 16m1s
/ build-mobile (push) Failing after 23m4s
/ build-desktop (linux) (push) Successful in 24m56s
/ release (push) Has been skipped
# Conflicts:
#	apps/tauri/src-tauri/Cargo.lock
#	apps/tauri/src-tauri/Cargo.toml
2026-08-06 00:50:24 +02:00
51e83b8d9e Merge pull request 'chore(deps): update rust crate serde to v1.0.229' (#35) from renovate/serde-1.x-lockfile into dev 2026-08-06 00:01:56 +02:00
9d5517fe30 Merge pull request 'chore(deps): update dependency gradle to v8.14.5' (#34) from renovate/gradle-8.x into dev
All checks were successful
/ build-web (push) Successful in 8m5s
/ build-desktop (linux) (push) Successful in 13m12s
/ build-mobile (push) Successful in 23m53s
/ release (push) Successful in 3m38s
2026-08-06 00:01:53 +02:00
a88bda43eb chore(deps): update rust crate serde to v1.0.229 2026-08-06 00:01:50 +02:00
16a288725d chore(deps): update dependency gradle to v8.14.5 2026-08-06 00:01:37 +02:00
d30de8b404
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
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
# Conflicts:
#	apps/tauri/src-tauri/Cargo.lock
#	apps/tauri/src-tauri/Cargo.toml
2026-08-06 00:01:20 +02:00
be8ea09cd6
(fix): mobile builds 2026-08-05 23:58:50 +02:00
8da83a4900 Merge pull request 'chore(deps): update tauri-build digest to 0aeadb6' (#33) from renovate/tauri-build-digest into dev
Some checks failed
/ build-web (push) Successful in 8m9s
/ build-desktop (linux) (push) Successful in 12m59s
/ build-mobile (push) Failing after 13m30s
/ release (push) Has been skipped
2026-08-05 23:18:44 +02:00
2dc5be85e3
(fix): mobile builds
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-08-05 23:18:39 +02:00
e11f46bd7c chore(deps): update tauri-build digest to 0aeadb6 2026-08-05 23:01:16 +02:00
2ead5e17be Merge pull request '(feat): update deps' (#31) from dev into main
Some checks failed
/ build-web (push) Successful in 8m5s
/ build-desktop (linux) (push) Successful in 12m58s
/ build-mobile (push) Failing after 13m26s
/ release (push) Has been skipped
Reviewed-on: #31
2026-08-05 22:17:26 +02:00
611a4944b6
(feat): update deps
Some checks failed
/ release (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-08-05 22:17:08 +02:00
cf38260a87 Merge pull request 'chore(deps): update mtp-transport digest to a81ac4e' (#29) from renovate/mtp-transport-digest into main
Some checks failed
/ build-web (push) Failing after 6m25s
/ build-desktop (linux) (push) Failing after 6m26s
/ build-mobile (push) Failing after 8m45s
/ release (push) Has been skipped
2026-08-05 22:07:33 +02:00
a6775b1d14 Merge pull request '(feat): update renovate' (#30) from dev into main
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
Reviewed-on: #30
2026-08-05 22:07:30 +02:00
b5c5dd722b
(feat): update renovate
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ build-web (push) Has been cancelled
2026-08-05 22:07:16 +02:00
5fb884edb0 chore(deps): update mtp-transport digest to a81ac4e 2026-08-05 22:00:59 +02:00
52fcf7ca02 Merge pull request 'chore(deps): update dependency @base-ui/utils to v0.3.2' (#25) from renovate/base-ui-utils-0.x into main
Some checks failed
/ build-web (push) Failing after 3m52s
/ build-desktop (linux) (push) Failing after 3m50s
/ build-mobile (push) Failing after 7m7s
/ release (push) Has been skipped
Reviewed-on: #25
2026-08-05 21:46:55 +02:00
014bb0480a Merge pull request 'chore(deps): update dependency @radix-ui/react-use-effect-event to ^0.0.5' (#26) from renovate/radix-ui-react-use-effect-event-0.x into main
Some checks failed
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ release (push) Has been cancelled
Reviewed-on: #26
2026-08-05 21:46:49 +02:00
3bc4f49678 Merge pull request 'dev' (#27) from dev into main
Some checks failed
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
/ release (push) Has been cancelled
/ build-web (push) Has been cancelled
Reviewed-on: #27
2026-08-05 21:46:27 +02:00
089fd152de
Merge remote-tracking branch 'origin/main' into dev
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-08-05 21:45:16 +02:00
5044947525
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-08-05 21:42:02 +02:00
4a841de073
(feat): improved mobile notifications
(feat): add lint rules
(feat): improve markdown inline code box
2026-08-05 21:41:55 +02:00
43fd0beb8e chore(deps): update dependency @radix-ui/react-use-effect-event to ^0.0.5
Some checks failed
renovate/artifacts Artifact file update failure
2026-08-05 21:00:47 +02:00
07e31d1ad6 chore(deps): update dependency @base-ui/utils to v0.3.2
Some checks failed
renovate/artifacts Artifact file update failure
2026-08-05 21:00:39 +02:00
forgejo-actions
4c2b0e4c99 (qol): update release flake hash 2026-08-05 18:45:33 +00:00
forgejo-actions
329a35bcfb (qol): update release flake hash 2026-08-05 18:45:31 +00:00
70932ca135 Merge pull request 'chore: Configure Renovate' (#24) from renovate/configure into main
All checks were successful
/ build-web (push) Successful in 14m4s
/ build-desktop (linux) (push) Successful in 19m0s
/ build-mobile (push) Successful in 26m17s
/ release (push) Successful in 3m31s
Reviewed-on: #24
2026-08-05 20:16:14 +02:00
c1b10ef737 Add renovate.json 2026-08-05 20:14:58 +02:00
336b9464c9
(feat): update mtp
All checks were successful
/ build-web (push) Successful in 8m15s
/ build-desktop (linux) (push) Successful in 13m3s
/ build-mobile (push) Successful in 23m22s
/ release (push) Successful in 3m31s
2026-08-05 16:35:32 +02:00
13fa3d8db7
(feat): add mobile notifications
Some checks failed
/ build-web (push) Successful in 8m31s
/ build-desktop (linux) (push) Successful in 14m5s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
(feat): improve mobile ui & ux
2026-08-05 16:19:46 +02:00
4cb38264d0
(feat): improve chat ui
All checks were successful
/ build-web (push) Successful in 7m57s
/ build-desktop (linux) (push) Successful in 12m32s
/ build-mobile (push) Successful in 20m14s
/ release (push) Successful in 3m38s
2026-08-05 00:30:11 +02:00
560e829cab
(feat): add day date separator
Some checks failed
/ build-web (push) Successful in 8m3s
/ build-mobile (push) Failing after 11m38s
/ build-desktop (linux) (push) Failing after 11m46s
/ release (push) Has been skipped
2026-08-04 23:51:32 +02:00
ab577283f9
(feat): add draft caching
All checks were successful
/ build-web (push) Successful in 8m1s
/ build-desktop (linux) (push) Successful in 12m44s
/ build-mobile (push) Successful in 19m57s
/ release (push) Successful in 3m26s
2026-08-02 01:25:39 +02:00
85633a1c81
(feat): add hotkeys
Some checks failed
/ build-web (push) Successful in 7m53s
/ build-desktop (linux) (push) Successful in 12m24s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-08-02 01:10:05 +02:00
915568f739
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
All checks were successful
/ build-web (push) Successful in 15m25s
/ build-desktop (linux) (push) Successful in 21m27s
/ build-mobile (push) Successful in 32m52s
/ release (push) Successful in 4m5s
2026-08-01 13:44:45 +02:00
b97f554c32
(feat): update version 2026-08-01 13:44:39 +02:00
forgejo-actions
dcb5d43056 (qol): update release flake hash 2026-08-01 00:28:28 +00:00
forgejo-actions
cdc8d31505 (qol): update release flake hash 2026-08-01 00:28:26 +00:00
aee2a0963d Merge pull request '(feat): big call and chatting stuff' (#23) from dev into main
Some checks failed
/ build-web (push) Successful in 13m34s
/ build-desktop (linux) (push) Successful in 20m28s
/ build-mobile (push) Successful in 33m20s
/ release (push) Failing after 3m48s
Reviewed-on: #23
2026-08-01 02:05:39 +02:00
1fc55ea562
(fix): lint issue
All checks were successful
/ build-web (push) Successful in 7m35s
/ build-desktop (linux) (push) Successful in 12m12s
/ build-mobile (push) Successful in 19m35s
/ release (push) Successful in 3m28s
2026-08-01 01:40:36 +02:00
3ef0380122
(feat): update package.json
Some checks failed
/ build-web (push) Failing after 6m42s
/ build-desktop (linux) (push) Failing after 7m17s
/ build-mobile (push) Failing after 9m13s
/ release (push) Has been skipped
2026-08-01 01:25:56 +02:00
62aaa7c01d
(feat): improve mobile
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
(feat): add camera sharing
(fix): vite tauri connection
(fix): methanium/ui theme not applied to call popout
2026-08-01 01:25:41 +02:00
b5ce3c554d
(feat): improve mobile cal ui a bit
All checks were successful
/ build-web (push) Successful in 7m40s
/ build-desktop (linux) (push) Successful in 12m10s
/ build-mobile (push) Successful in 19m34s
/ release (push) Successful in 3m31s
(feat): add popout for call ui on mobile
(fix): fix cache and profile avatar upload stuff
2026-07-31 22:17:08 +02:00
37b2f8747d
(fix): lint issue
All checks were successful
/ build-web (push) Successful in 7m48s
/ build-desktop (linux) (push) Successful in 12m16s
/ build-mobile (push) Successful in 19m51s
/ release (push) Successful in 3m28s
2026-07-31 17:50:49 +02:00
d355c83c02
(feat): add a small zoom to the android app
Some checks failed
/ build-web (push) Failing after 6m40s
/ build-desktop (linux) (push) Failing after 7m12s
/ build-mobile (push) Failing after 9m12s
/ release (push) Has been skipped
(fix): save ReplyId correctly
(qol): update todo
2026-07-31 15:17:30 +02:00
6f03604c90
(fix): small notification logic error
All checks were successful
/ build-web (push) Successful in 7m45s
/ build-desktop (linux) (push) Successful in 12m15s
/ build-mobile (push) Successful in 19m39s
/ release (push) Successful in 3m27s
(fix): call ui
2026-07-31 00:59:57 +02:00
f1874042ba
(fix): live reactions, message and call invites
All checks were successful
/ build-web (push) Successful in 7m40s
/ build-desktop (linux) (push) Successful in 12m10s
/ build-mobile (push) Successful in 19m43s
/ release (push) Successful in 3m35s
(fix): call ui
2026-07-31 00:29:48 +02:00
5a26c2d401
(feat): add preview button to the call settings page
Some checks failed
/ build-desktop (linux) (push) Failing after 6m25s
/ build-web (push) Successful in 7m29s
/ build-mobile (push) Successful in 19m23s
/ release (push) Has been skipped
2026-07-30 23:57:24 +02:00
45e7591e40
(feat): update todos
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-07-30 23:52:02 +02:00
184b85190b
(feat): add sounds
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
2026-07-30 23:51:38 +02:00
6a5236bafe
(feat): add reply box to messages
All checks were successful
/ build-web (push) Successful in 7m40s
/ build-desktop (linux) (push) Successful in 12m6s
/ build-mobile (push) Successful in 19m4s
/ release (push) Successful in 3m25s
(feat): update methanium ui
(fix): user data caching
(fix): other random stuff
(qol): update todo
2026-07-30 23:01:42 +02:00
53728b7436
(fix): add conversation
Some checks failed
/ build-web (push) Failing after 1m2s
/ build-mobile (push) Failing after 1m15s
/ build-desktop (linux) (push) Failing after 1m15s
/ release (push) Has been skipped
2026-07-29 11:46:39 +02:00
5ece90ef4b
(fix): retry when iota is unavailable
Some checks failed
/ build-mobile (push) Failing after 1m0s
/ build-web (push) Failing after 1m13s
/ build-desktop (linux) (push) Failing after 1m13s
/ release (push) Has been skipped
2026-07-29 02:37:18 +02:00
3666902ac9
(fix): connections (again)
Some checks failed
/ build-web (push) Failing after 58s
/ build-mobile (push) Failing after 1m11s
/ build-desktop (linux) (push) Failing after 1m11s
/ release (push) Has been skipped
(qol): update todo
2026-07-28 23:53:53 +02:00
676dea2aa4
(fix): connections
Some checks failed
/ build-web (push) Failing after 1m0s
/ build-desktop (linux) (push) Failing after 1m13s
/ build-mobile (push) Failing after 1m13s
/ release (push) Has been skipped
2026-07-28 12:45:08 +02:00
a43c05f035
(fix): fix connection issues
Some checks failed
/ build-web (push) Failing after 1m14s
/ build-desktop (linux) (push) Failing after 1m14s
/ build-mobile (push) Failing after 1m14s
/ release (push) Has been skipped
2026-07-28 01:02:26 +02:00
781a90d2d1
(feat): update mtp
All checks were successful
/ build-web (push) Successful in 7m33s
/ build-desktop (linux) (push) Successful in 12m9s
/ build-mobile (push) Successful in 18m54s
/ release (push) Successful in 3m29s
2026-07-27 22:54:34 +02:00
0dff15cc28
(fix): broken connections
Some checks failed
/ build-web (push) Failing after 6m34s
/ build-desktop (linux) (push) Failing after 6m44s
/ build-mobile (push) Failing after 8m36s
/ release (push) Has been skipped
2026-07-27 21:37:56 +02:00
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
forgejo-actions
bf17f59a95 (qol): update release flake hash 2026-07-21 10:17:27 +00:00
f41cf7cfa4
Merge 2026-07-21 11:39:28 +02: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
forgejo-actions
41bea6b107 (qol): update release flake hash 2026-07-21 09:19:47 +00:00
e44863e9cb Merge pull request 'TTP -> MTP, A lot of other stuff' (#21) from dev into main
All checks were successful
/ build-web (push) Successful in 6m49s
/ build-desktop (linux) (push) Successful in 11m25s
/ build-mobile (push) Successful in 19m28s
/ release (push) Successful in 3m4s
Reviewed-on: #21
2026-07-21 10:57:32 +02: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
forgejo-actions
4a23f8455f (qol): update release flake hash 2026-06-16 10:07:07 +00:00
37a9f97361 Merge pull request '(feat): add theming for testers' (#20) from dev into main
All checks were successful
/ build-web (push) Successful in 1m19s
/ build-desktop (linux) (push) Successful in 6m37s
/ build-mobile (push) Successful in 15m49s
/ release (push) Successful in 32s
Reviewed-on: #20
2026-06-16 11:50:47 +02:00
464d779569 (chore): bump version
All checks were successful
/ build-web (push) Successful in 1m20s
/ build-desktop (linux) (push) Successful in 6m35s
/ build-mobile (push) Successful in 15m45s
/ release (push) Successful in 29s
2026-06-16 11:12:59 +02:00
3049e090be (fix): nix flake 2026-06-16 11:12:37 +02:00
040d60a35f (feat): add theming
All checks were successful
/ build-web (push) Successful in 1m24s
/ build-desktop (linux) (push) Successful in 6m51s
/ build-mobile (push) Successful in 15m48s
/ release (push) Successful in 31s
2026-06-15 21:13:09 +02:00
87e4a158e3 (feat): add gif picker & media saving
All checks were successful
/ build-web (push) Successful in 1m31s
/ build-desktop (linux) (push) Successful in 7m46s
/ build-mobile (push) Successful in 17m42s
/ release (push) Successful in 34s
2026-06-14 15:25:24 +02:00
4a0f9e3b58 (fix): chat issues when sending messages
All checks were successful
/ build-web (push) Successful in 1m23s
/ build-desktop (linux) (push) Successful in 6m29s
/ build-mobile (push) Successful in 15m9s
/ release (push) Successful in 31s
2026-06-14 01:45:20 +02:00
b4662bc1a4 (feat): improve chat ui
Some checks failed
/ build-web (push) Successful in 1m24s
/ build-desktop (linux) (push) Successful in 6m31s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-06-14 01:36:35 +02:00
30783f8ded (fix): scroll would sometimes lock up when new messages get loaded
Some checks failed
/ build-web (push) Successful in 1m20s
/ build-desktop (linux) (push) Successful in 6m25s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-06-14 01:24:38 +02:00
7dc68d1867 (feat): improve dx for mobile app
All checks were successful
/ build-web (push) Successful in 1m19s
/ build-desktop (linux) (push) Successful in 6m22s
/ build-mobile (push) Successful in 15m12s
/ release (push) Successful in 33s
2026-06-13 13:44:30 +02:00
ff41763d76 (feat): add smooth scrolling
All checks were successful
/ build-web (push) Successful in 1m19s
/ build-desktop (linux) (push) Successful in 6m18s
/ build-mobile (push) Successful in 14m59s
/ release (push) Successful in 30s
2026-06-13 12:10:34 +02:00
f3cf836fcd (fix): a bunch or weird scroll behaviour
All checks were successful
/ build-web (push) Successful in 1m27s
/ build-desktop (linux) (push) Successful in 6m46s
/ build-mobile (push) Successful in 16m16s
/ release (push) Successful in 30s
2026-06-13 11:39:47 +02:00
41517631ee (feat): improve connections on mobile
All checks were successful
/ build-web (push) Successful in 1m30s
/ build-desktop (linux) (push) Successful in 6m52s
/ build-mobile (push) Successful in 17m31s
/ release (push) Successful in 32s
2026-06-09 20:12:32 +02:00
4c0f2fde91 (feat): add ttp-tauri and integrate it
All checks were successful
/ build-web (push) Successful in 1m27s
/ build-desktop (linux) (push) Successful in 6m46s
/ build-mobile (push) Successful in 17m23s
/ release (push) Successful in 32s
(feat): change font to Public Sans
2026-06-09 19:48:18 +02:00
1e90171ee9 (feat): add embeds
All checks were successful
/ build-web (push) Successful in 1m29s
/ build-desktop (linux) (push) Successful in 6m52s
/ build-mobile (push) Successful in 17m8s
/ release (push) Successful in 34s
2026-06-09 18:44:30 +02:00
forgejo-actions
9f3d610bb4 (qol): update release flake hash 2026-06-09 14:39:01 +00:00
forgejo-actions
d545b91844 (qol): update release flake hash 2026-06-09 14:39:00 +00:00
b57bc0683c Merge pull request '(fix): typescript error' (#19) from dev into main
All checks were successful
/ build-web (push) Successful in 1m49s
/ build-desktop (linux) (push) Successful in 9m0s
/ build-mobile (push) Successful in 20m25s
/ release (push) Successful in 32s
Reviewed-on: #19
2026-06-09 16:18:04 +02:00
42dd3a1c49 (fix): typescript error
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-06-09 16:17:40 +02:00
73ff854f0f Merge pull request '(feat): call improvements' (#18) from dev into main
Some checks failed
/ build-desktop (linux) (push) Failing after 1m29s
/ build-web (push) Failing after 1m42s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
Reviewed-on: #18
2026-06-09 16:15:01 +02:00
066db995c1 (feat): add tauri notifications
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
(feat): bump version
2026-06-09 16:14:29 +02:00
4068669704 (feat): add context menu to call users
Some checks failed
/ build-desktop (linux) (push) Failing after 1m9s
/ build-web (push) Failing after 1m25s
/ build-mobile (push) Failing after 4m7s
/ release (push) Has been skipped
(feat): ui changes to invite popup & call ui in general
(feat): make eslint less aggressive
(fix): stop watching button on own screenshares
(qol): update todo
2026-06-08 15:40:04 +02:00
9edd548466 (fix): two small errors
All checks were successful
/ build-web (push) Successful in 1m39s
/ build-desktop (linux) (push) Successful in 7m24s
/ build-mobile (push) Successful in 14m36s
/ release (push) Successful in 32s
2026-06-08 13:43:28 +02:00
28a3d72fad (feat): remove homepage dev buttons
Some checks failed
/ build-desktop (linux) (push) Failing after 1m16s
/ build-web (push) Failing after 1m27s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
(fix): some call related bugs
(qol): update todo
2026-06-08 13:41:36 +02:00
4f82b9c85b (feat): add call popout
Some checks failed
/ build-desktop (linux) (push) Failing after 1m22s
/ build-web (push) Failing after 1m28s
/ build-mobile (push) Failing after 4m10s
/ release (push) Has been skipped
2026-06-08 11:12:44 +02:00
18e507b439 (feat): use Inter instead of the system font for ui
All checks were successful
/ build-web (push) Successful in 1m16s
/ build-desktop (linux) (push) Successful in 6m25s
/ build-mobile (push) Successful in 12m25s
/ release (push) Successful in 32s
2026-06-04 22:29:20 +02:00
f4088691c5 (feat): improve profile popup in the navbar, add shared code to that popup
All checks were successful
/ build-web (push) Successful in 1m20s
/ build-desktop (linux) (push) Successful in 6m18s
/ build-mobile (push) Successful in 12m28s
/ release (push) Successful in 29s
2026-06-03 16:14:39 +02:00
7a7ab3ad2e (fix): dependency issue
All checks were successful
/ build-web (push) Successful in 1m20s
/ build-desktop (linux) (push) Successful in 6m17s
/ build-mobile (push) Successful in 12m24s
/ release (push) Successful in 30s
2026-06-03 15:25:30 +02:00
1a563f63a2 (feat): add lint script to apps/electron/
Some checks failed
/ build-desktop (linux) (push) Failing after 55s
/ build-web (push) Failing after 1m4s
/ build-mobile (push) Failing after 1m6s
/ release (push) Has been skipped
2026-06-03 14:54:34 +02:00
be56080ba1 (fix): remove unused dependencies
Some checks failed
/ build-desktop (linux) (push) Failing after 52s
/ build-web (push) Failing after 1m3s
/ build-mobile (push) Failing after 1m6s
/ release (push) Has been skipped
(fix): remove dead code
(fix): remove unused exports
2026-06-03 14:53:18 +02:00
forgejo-actions
abc0cceb56 (qol): update release flake hash 2026-06-03 07:26:40 +00:00
forgejo-actions
2a8fade420 (qol): update release flake hash 2026-06-03 07:26:40 +00:00
67756255db Merge pull request '(fix): a few mobile related bugs' (#17) from dev into main
All checks were successful
/ build-web (push) Successful in 1m22s
/ build-desktop (linux) (push) Successful in 5m12s
/ build-mobile (push) Successful in 12m0s
/ release (push) Successful in 29s
Reviewed-on: #17
2026-06-03 09:14:14 +02:00
4f58523e02 (feat): bumb version
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-06-03 09:13:22 +02:00
6442de2c30 (feat): remove profile popup from mobile
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
2026-06-03 09:13:07 +02:00
2f9b6edf8c (fix): react rerendered the mobile drawer multiple times causing the drawer to pop out multiple times
All checks were successful
/ build-web (push) Successful in 1m23s
/ build-desktop (linux) (push) Successful in 5m14s
/ build-mobile (push) Successful in 11m54s
/ release (push) Successful in 28s
2026-06-03 08:51:31 +02:00
257ef320d8 Merge pull request '(feat): remove cleanup workflow' (#16) from dev into main
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
Reviewed-on: #16
2026-06-02 22:47:28 +02:00
dce8686c0f (feat): remove cleanup workflow
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
2026-06-02 22:47:13 +02:00
4a53d5c6dc Merge pull request '(fix): cleanup workflow' (#15) from dev into main
Some checks failed
/ release (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
Reviewed-on: #15
2026-06-02 22:33:05 +02:00
5ff85002fd (fix): cleanup workflow
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-02 22:31:18 +02:00
123c0c831e (fix): cleanup workflow
Some checks failed
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-06-02 22:28:50 +02:00
5f92e5e696 (feat): add fallow
All checks were successful
/ build-web (push) Successful in 1m22s
/ build-desktop (linux) (push) Successful in 5m34s
/ build-mobile (push) Successful in 13m2s
/ release (push) Successful in 28s
2026-06-02 21:57:50 +02:00
ed2fd09def (feat): track call invite when it is received
All checks were successful
/ build-web (push) Successful in 3m37s
/ build-desktop (linux) (push) Successful in 8m37s
/ build-mobile (push) Successful in 28m13s
/ release (push) Successful in 28s
(fix): call invites getting sent by non-call-creators
2026-06-01 14:26:13 +02:00
6fda7f1c71 (feat): replace toast with popup for call invites
Some checks failed
/ build-web (push) Successful in 2m37s
/ build-mobile (push) Failing after 5m32s
/ build-desktop (linux) (push) Successful in 6m30s
/ release (push) Has been skipped
(qol): update todo
2026-06-01 14:13:43 +02:00
25e47604b3 (feat): add message context menu
All checks were successful
/ build-web (push) Successful in 2m35s
/ build-desktop (linux) (push) Successful in 7m58s
/ build-mobile (push) Successful in 21m31s
/ release (push) Successful in 45s
2026-06-01 11:13:13 +02:00
forgejo-actions
74eb0d0f91 (qol): update release flake hash 2026-05-29 13:21:14 +00:00
forgejo-actions
089e6c5b28 (qol): update release flake hash 2026-05-29 13:21:12 +00:00
00598348ab Merge pull request '(feat): some workflow updates' (#14) from dev into main
All checks were successful
/ build-web (push) Successful in 2m53s
/ build-desktop (linux) (push) Successful in 7m34s
/ build-mobile (push) Successful in 21m41s
/ release (push) Successful in 54s
Reviewed-on: #14
2026-05-29 14:58:44 +02:00
f30f262751 Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
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-05-29 14:58:04 +02:00
dbd685bd43 (feat): add cleanup for workflow runs and releases
All checks were successful
/ build-web (push) Successful in 3m6s
/ build-desktop (linux) (push) Successful in 8m3s
/ build-mobile (push) Successful in 23m13s
/ release (push) Successful in 52s
2026-05-29 10:37:18 +02:00
a82ee292ce (qol): format 2026-05-28 20:20:43 +02:00
4445852c42 (feat): add icons to the electron app
All checks were successful
/ build-web (push) Successful in 3m55s
/ build-desktop (linux) (push) Successful in 9m5s
/ build-mobile (push) Successful in 25m54s
/ release (push) Successful in 28s
(feat): bump version
2026-05-28 20:16:40 +02:00
forgejo-actions
b5e7b8e2c1 (qol): update release flake hash 2026-05-28 14:09:24 +00:00
forgejo-actions
0c06516984 (qol): update release flake hash 2026-05-28 14:09:23 +00:00
8f4f356746 Merge pull request '(feat): now using the .deb instead of the .AppImage for nixos' (#13) from dev into main
All checks were successful
/ build-web (push) Successful in 3m38s
/ build-desktop (linux) (push) Successful in 9m5s
/ build-mobile (push) Successful in 26m16s
/ release (push) Successful in 55s
Reviewed-on: #13
2026-05-28 15:42:18 +02:00
6225d6f7f0 (feat): now using the .deb instead of the .AppImage for nixos
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
(qol): update hash
2026-05-28 15:41:52 +02:00
forgejo-actions
b0324fafdf (qol): update release flake hash 2026-05-27 18:55:05 +00:00
ed02f6ee6c Merge pull request '(feat): allow manual workflow runs in prod workflow' (#12) from dev into main
All checks were successful
/ build-web (push) Successful in 3m20s
/ build-desktop (linux) (push) Successful in 8m40s
/ build-mobile (push) Successful in 25m33s
/ release (push) Successful in 55s
Reviewed-on: #12
2026-05-27 20:28:42 +02:00
972148f173 (feat): allow manual workflow runs in prod workflow
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-05-27 20:26:26 +02:00
0e703c1913 Merge pull request '(fix): nixos missing .desktop files, binary name from desktop app' (#11) from dev into main
Reviewed-on: #11
2026-05-27 20:19:13 +02:00
e327f47b68 Merge remote-tracking branch 'refs/remotes/origin/main'
Some checks failed
/ release (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
2026-05-27 20:18:39 +02:00
713389227c Merge branch 'main' into dev 2026-05-27 20:16:30 +02:00
c5f0be7af2 Merge branch 'dev' 2026-05-27 20:15:48 +02:00
919814ae0b (fix): nixos missing .desktop files, binary name from desktop app
All checks were successful
/ build-web (push) Successful in 3m17s
/ build-desktop (linux) (push) Successful in 8m41s
/ build-mobile (push) Successful in 25m43s
/ release (push) Successful in 57s
2026-05-27 18:58:05 +02:00
forgejo-actions
f69918d84f (qol): update release flake hash 2026-05-26 10:45:43 +00:00
34da442961 Merge pull request '(feat): bump version' (#10) from dev into main
All checks were successful
/ build-web (push) Successful in 2m48s
/ build-desktop (linux) (push) Successful in 7m28s
/ build-mobile (push) Successful in 14m11s
/ release (push) Successful in 28s
Reviewed-on: #10
2026-05-26 12:31:07 +02:00
forgejo-actions
01e8f9f7dd (qol): update release flake hash 2026-05-25 13:47:36 +00:00
97e6b3d30e Merge pull request '(feat): tauri -> electron desktop app migration' (#9) from dev into main
All checks were successful
/ build-web (push) Successful in 1m18s
/ build-desktop (linux) (push) Successful in 5m45s
/ build-mobile (push) Successful in 11m35s
/ release (push) Successful in 25s
Reviewed-on: #9
2026-05-25 15:35:36 +02:00
1a71e2ddbf Merge pull request '(feat): version dump for testing' (#8) from dev into main
All checks were successful
/ build-web (push) Successful in 1m10s
/ build-desktop (push) Successful in 11m21s
/ build-mobile (push) Successful in 15m54s
/ release (push) Successful in 22s
Reviewed-on: #8
2026-05-24 18:00:32 +02:00
21274991f3 Merge pull request '(feat): added version render script for apps like obtainium' (#7) from dev into main
All checks were successful
/ build-web (push) Successful in 1m9s
/ build-desktop (push) Successful in 11m6s
/ build-mobile (push) Successful in 15m37s
/ release (push) Successful in 22s
Reviewed-on: #7
2026-05-23 18:09:17 +02:00
955b4d53da Merge pull request '(feat): version bump needed for new ttp version' (#4) from dev into main
All checks were successful
/ build-web (push) Successful in 1m7s
/ build-desktop (push) Successful in 17m48s
/ build-mobile (push) Successful in 27m28s
/ release (push) Successful in 32s
Reviewed-on: #4
2026-05-21 21:31:55 +02:00
9f483ce8d3 Merge pull request '(feat): good call progress, mandatory update due to ttp changes' (#3) from dev into main
All checks were successful
/ build-web (push) Successful in 3m17s
/ build-desktop (push) Successful in 20m0s
/ build-mobile (push) Successful in 28m4s
/ release (push) Successful in 22s
Reviewed-on: #3
2026-05-20 22:11:21 +02:00
8b1b34db6a Merge pull request '(feat): various workflow improvements, mobile sidebar improvements' (#2) from dev into main
All checks were successful
/ build-web (push) Successful in 1m9s
/ build-desktop (push) Successful in 11m24s
/ build-mobile (push) Successful in 15m56s
/ release (push) Successful in 22s
Reviewed-on: #2
2026-05-14 15:24:51 +02:00
373 changed files with 38012 additions and 12396 deletions

2
.cargo/config.toml Normal file
View file

@ -0,0 +1,2 @@
[env]
MTP_TYPE_MAPS = { value = "mtp-type-maps/type-maps.yaml", relative = true }

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

18
.fallowrc.json Normal file
View file

@ -0,0 +1,18 @@
{
"$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
"entry": [
"src/index.{ts,tsx,js,jsx}",
"src/main.{ts,tsx,js,jsx}",
"apps/pwa/src/serviceWorker.ts",
"apps/tauri/render-version.ts",
"packages/**/*.test.ts"
],
"workspaces": {
"packages": ["packages/*", "apps/*"]
},
"duplicates": {
"minOccurrences": 3,
"ignore": ["**/scripts/**", "**.test.ts", "licenses/**"]
},
"rules": {}
}

View file

@ -0,0 +1,60 @@
name: Dependency builds
on:
pull_request:
env:
FORGEJO_TOKEN: ""
GITHUB_TOKEN: ""
jobs:
web:
if: ${{ github.actor == 'rasensprenger' }}
name: Build web
runs-on: nixos
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
with:
persist-credentials: false
- run: git submodule update --init --recursive
- run: nix develop .#electron --command pnpm install --frozen-lockfile
- run: nix develop .#electron --command pnpm run build:packages
- run: nix develop .#electron --command pnpm run build:web
desktop:
if: ${{ github.actor == 'rasensprenger' }}
name: Build desktop
runs-on: nixos
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
with:
persist-credentials: false
- run: git submodule update --init --recursive
- run: nix develop .#electron --command pnpm install --frozen-lockfile
- run: nix develop .#electron --command pnpm run build:packages
- run: nix develop .#electron --command pnpm run build:desktop
native-mtp:
if: ${{ github.actor == 'rasensprenger' }}
name: Test native MTP
runs-on: nixos
steps:
- run: nix profile add nixpkgs#nodejs_24
- uses: https://data.forgejo.org/actions/checkout@v4
with:
persist-credentials: false
- run: git submodule update --init --recursive
- run: nix develop .#electron --command bash -lc 'cd apps/tauri/src-tauri && cargo test'
mobile:
if: ${{ github.actor == 'rasensprenger' }}
name: Build mobile
runs-on: nixos
steps:
- uses: https://data.forgejo.org/actions/checkout@v4
with:
persist-credentials: false
- run: git submodule update --init --recursive
- run: nix develop .#tauri --command pnpm install --frozen-lockfile
- run: nix develop .#tauri --command pnpm run build:packages
- run: nix develop .#tauri --command pnpm --dir apps/tauri run build:mobile:ci

View file

@ -1,4 +1,5 @@
on: on:
workflow_dispatch:
push: push:
branches: branches:
- dev - dev
@ -7,72 +8,103 @@ on:
jobs: jobs:
build-web: build-web:
runs-on: docker runs-on: nixos
steps: steps:
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages - name: Pull git submodules
run: apt-get update && apt-get install -y sudo curl jq fakeroot dpkg rpm run: git submodule update --init --recursive
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v30
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Copy licenses - name: Copy licenses
run: bun run copy-licenses run: nix develop .#electron --command pnpm run copy-licenses
- name: Build packages - name: Build packages
run: bun run build:packages run: nix develop .#electron --command pnpm run build:packages
- name: Build web - name: Build web
run: bun run build:web run: nix develop .#electron --command pnpm run build:web
- name: Install rsync
run: apt-get update && apt-get install -y rsync
- name: Deploy - name: Deploy
run: rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-dev/ run: nix develop .#electron --command rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-dev/
build-mobile: build-mobile:
runs-on: docker runs-on: nixos
steps: steps:
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages - name: Pull git submodules
run: apt-get update && apt-get install -y sudo curl jq fakeroot dpkg rpm run: git submodule update --init --recursive
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v30
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: nix develop .#tauri --command pnpm install --frozen-lockfile
- name: Copy licenses - name: Copy licenses
run: bun run copy-licenses run: nix develop .#tauri --command pnpm run copy-licenses
- name: Build packages - name: Build packages
run: bun run build:packages run: nix develop .#tauri --command pnpm run build:packages
- name: Setup Android Keystore - name: Setup Android Keystore
env: env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }} KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
run: | run: |
bun -e "require('fs').writeFileSync('keystore.jks', Buffer.from(process.env.KEYSTORE_BASE64.replace(/\s+/g, ''), 'base64'))" set -euo pipefail
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)"
if [ -z "$KEYSTORE_BASE64" ]; then
echo "ANDROID_KEYSTORE_BASE64 secret is missing or empty"
exit 1
fi
if [ -z "$KEYSTORE_PROPERTIES" ]; then
echo "ANDROID_KEYSTORE_PROPERTIES secret is missing or empty"
exit 1
fi
printf '%s' "$KEYSTORE_BASE64" \
| tr -d '[:space:]' \
| base64 -d > keystore.jks
printf '%s' "$KEYSTORE_PROPERTIES" \
| sed 's/\\n/\n/g' \
| tr -d '\r' \
| sed 's|^[[:space:]]*storeFile[[:space:]]*=.*|storeFile=keystore.jks|' \
> keystore.properties
grep -q '^[[:space:]]*storeFile[[:space:]]*=' keystore.properties || printf '\nstoreFile=keystore.jks\n' >> keystore.properties
if [ ! -s keystore.jks ]; then
echo "Decoded keystore.jks is missing or empty"
exit 1
fi
if [ ! -s keystore.properties ]; then
echo "Generated keystore.properties is missing or empty"
exit 1
fi
if ! grep -q '^[[:space:]]*keyAlias[[:space:]]*=' keystore.properties; then
echo "keystore.properties is missing keyAlias"
exit 1
fi
if ! grep -Eq '^[[:space:]]*(keyPassword|password)[[:space:]]*=' keystore.properties; then
echo "keystore.properties is missing keyPassword or password"
exit 1
fi
if ! grep -Eq '^[[:space:]]*(storePassword|password)[[:space:]]*=' keystore.properties; then
echo "keystore.properties is missing storePassword or password"
exit 1
fi
- name: Build mobile - name: Build mobile
run: bun run build:mobile run: nix develop .#tauri --command pnpm run build:mobile
- name: Upload mobile artifact - name: Upload mobile artifact
uses: https://data.forgejo.org/actions/upload-artifact@v3 uses: https://data.forgejo.org/actions/upload-artifact@v3
@ -81,7 +113,7 @@ jobs:
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
build-desktop: build-desktop:
runs-on: docker runs-on: nixos
strategy: strategy:
matrix: matrix:
target: [linux] target: [linux]
@ -89,23 +121,22 @@ jobs:
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages - name: Pull git submodules
run: apt-get update && apt-get install -y sudo curl jq fakeroot dpkg rpm xz-utils run: git submodule update --init --recursive
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Copy licenses - name: Copy licenses
run: bun run copy-licenses run: nix develop .#electron --command pnpm run copy-licenses
- name: Build packages - name: Build packages
run: bun run build:packages run: nix develop .#electron --command pnpm run build:packages
- name: Set Electron dev version - name: Set Electron dev version
run: | run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
VERSION="$(node -p "require('./package.json').version")" VERSION="$(node -p "require('./package.json').version")"
SHORT_SHA="$(git rev-parse --short HEAD)" SHORT_SHA="$(git rev-parse --short HEAD)"
DEV_VERSION="$VERSION-dev-$SHORT_SHA" DEV_VERSION="$VERSION-dev-$SHORT_SHA"
@ -117,9 +148,15 @@ jobs:
pkg.version = process.env.DEV_VERSION; pkg.version = process.env.DEV_VERSION;
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n"); fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
' '
EOF
- name: Build Electron desktop - name: Build Electron desktop
run: bun run build:desktop run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
cd apps/electron
pnpm run package:raw
EOF
- name: Upload desktop artifacts - name: Upload desktop artifacts
uses: https://data.forgejo.org/actions/upload-artifact@v3 uses: https://data.forgejo.org/actions/upload-artifact@v3
@ -128,7 +165,7 @@ jobs:
path: apps/electron/release/ path: apps/electron/release/
release: release:
runs-on: docker runs-on: nixos
needs: [build-web, build-mobile, build-desktop] needs: [build-web, build-mobile, build-desktop]
steps: steps:
- name: Check out repo - name: Check out repo
@ -136,14 +173,11 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Install Packages - name: Pull git submodules
run: apt-get update && apt-get install -y sudo curl jq run: git submodule update --init --recursive
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Download mobile artifact - name: Download mobile artifact
uses: https://data.forgejo.org/actions/download-artifact@v3 uses: https://data.forgejo.org/actions/download-artifact@v3
@ -160,21 +194,27 @@ jobs:
- name: Read version and hash - name: Read version and hash
id: version id: version
run: | run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
VERSION="$(node -p "require('./package.json').version")" VERSION="$(node -p "require('./package.json').version")"
SHORT_SHA="$(git rev-parse --short HEAD)" SHORT_SHA="$(git rev-parse --short HEAD)"
echo "version=$VERSION" >> "$FORGEJO_OUTPUT" echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
echo "short_sha=$SHORT_SHA" >> "$FORGEJO_OUTPUT" echo "short_sha=$SHORT_SHA" >> "$FORGEJO_OUTPUT"
echo "tag=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT" echo "tag=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
echo "title=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT" echo "title=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
EOF
- name: Copy releases - name: Copy releases
env: env:
TENSAMIN_RELEASE_VERSION: ${{ steps.version.outputs.tag }} TENSAMIN_RELEASE_VERSION: ${{ steps.version.outputs.tag }}
TENSAMIN_RELEASE_TAG: ${{ steps.version.outputs.tag }} TENSAMIN_RELEASE_TAG: ${{ steps.version.outputs.tag }}
run: | run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
ASSET_BASE_URL="${{ forgejo.api_url }}" ASSET_BASE_URL="${{ forgejo.api_url }}"
ASSET_BASE_URL="${ASSET_BASE_URL%/api/v1}/${{ forgejo.repository }}/releases/download/${{ steps.version.outputs.tag }}" ASSET_BASE_URL="${ASSET_BASE_URL%/api/v1}/${{ forgejo.repository }}/releases/download/${{ steps.version.outputs.tag }}"
FORGEJO_RELEASE_ASSET_BASE_URL="$ASSET_BASE_URL" bun --bun run copy-releases FORGEJO_RELEASE_ASSET_BASE_URL="$ASSET_BASE_URL" pnpm run copy-releases
EOF
- name: Create pre-release and upload files - name: Create pre-release and upload files
env: env:
@ -185,6 +225,7 @@ jobs:
TAG: ${{ steps.version.outputs.tag }} TAG: ${{ steps.version.outputs.tag }}
TITLE: ${{ steps.version.outputs.title }} TITLE: ${{ steps.version.outputs.title }}
run: | run: |
nix develop .#electron --command bash <<'EOF'
set -eu set -eu
test -d releases test -d releases
@ -273,3 +314,4 @@ jobs:
-H "Authorization: token $TOKEN" \ -H "Authorization: token $TOKEN" \
-F "attachment=@$file" -F "attachment=@$file"
done done
EOF

View file

@ -1,4 +1,5 @@
on: on:
workflow_dispatch:
push: push:
branches: branches:
- main - main
@ -7,72 +8,103 @@ on:
jobs: jobs:
build-web: build-web:
runs-on: docker runs-on: nixos
steps: steps:
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages - name: Pull git submodules
run: apt-get update && apt-get install -y sudo curl jq fakeroot dpkg rpm run: git submodule update --init --recursive
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v30
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Copy licenses - name: Copy licenses
run: bun run copy-licenses run: nix develop .#electron --command pnpm run copy-licenses
- name: Build packages - name: Build packages
run: bun run build:packages run: nix develop .#electron --command pnpm run build:packages
- name: Build web - name: Build web
run: bun run build:web run: nix develop .#electron --command pnpm run build:web
- name: Install rsync
run: apt-get update && apt-get install -y rsync
- name: Deploy - name: Deploy
run: rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-prod/ run: nix develop .#electron --command rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-prod/
build-mobile: build-mobile:
runs-on: docker runs-on: nixos
steps: steps:
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages - name: Pull git submodules
run: apt-get update && apt-get install -y sudo curl jq fakeroot dpkg rpm run: git submodule update --init --recursive
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v30
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: nix develop .#tauri --command pnpm install --frozen-lockfile
- name: Copy licenses - name: Copy licenses
run: bun run copy-licenses run: nix develop .#tauri --command pnpm run copy-licenses
- name: Build packages - name: Build packages
run: bun run build:packages run: nix develop .#tauri --command pnpm run build:packages
- name: Setup Android Keystore - name: Setup Android Keystore
env: env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }} KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
run: | run: |
bun -e "require('fs').writeFileSync('keystore.jks', Buffer.from(process.env.KEYSTORE_BASE64.replace(/\s+/g, ''), 'base64'))" set -euo pipefail
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)"
if [ -z "$KEYSTORE_BASE64" ]; then
echo "ANDROID_KEYSTORE_BASE64 secret is missing or empty"
exit 1
fi
if [ -z "$KEYSTORE_PROPERTIES" ]; then
echo "ANDROID_KEYSTORE_PROPERTIES secret is missing or empty"
exit 1
fi
printf '%s' "$KEYSTORE_BASE64" \
| tr -d '[:space:]' \
| base64 -d > keystore.jks
printf '%s' "$KEYSTORE_PROPERTIES" \
| sed 's/\\n/\n/g' \
| tr -d '\r' \
| sed 's|^[[:space:]]*storeFile[[:space:]]*=.*|storeFile=keystore.jks|' \
> keystore.properties
grep -q '^[[:space:]]*storeFile[[:space:]]*=' keystore.properties || printf '\nstoreFile=keystore.jks\n' >> keystore.properties
if [ ! -s keystore.jks ]; then
echo "Decoded keystore.jks is missing or empty"
exit 1
fi
if [ ! -s keystore.properties ]; then
echo "Generated keystore.properties is missing or empty"
exit 1
fi
if ! grep -q '^[[:space:]]*keyAlias[[:space:]]*=' keystore.properties; then
echo "keystore.properties is missing keyAlias"
exit 1
fi
if ! grep -Eq '^[[:space:]]*(keyPassword|password)[[:space:]]*=' keystore.properties; then
echo "keystore.properties is missing keyPassword or password"
exit 1
fi
if ! grep -Eq '^[[:space:]]*(storePassword|password)[[:space:]]*=' keystore.properties; then
echo "keystore.properties is missing storePassword or password"
exit 1
fi
- name: Build mobile - name: Build mobile
run: bun run build:mobile run: nix develop .#tauri --command pnpm run build:mobile
- name: Upload mobile artifact - name: Upload mobile artifact
uses: https://data.forgejo.org/actions/upload-artifact@v3 uses: https://data.forgejo.org/actions/upload-artifact@v3
@ -81,7 +113,7 @@ jobs:
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
build-desktop: build-desktop:
runs-on: docker runs-on: nixos
strategy: strategy:
matrix: matrix:
target: [linux] target: [linux]
@ -89,23 +121,22 @@ jobs:
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages - name: Pull git submodules
run: apt-get update && apt-get install -y sudo curl jq fakeroot dpkg rpm xz-utils run: git submodule update --init --recursive
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Copy licenses - name: Copy licenses
run: bun run copy-licenses run: nix develop .#electron --command pnpm run copy-licenses
- name: Build packages - name: Build packages
run: bun run build:packages run: nix develop .#electron --command pnpm run build:packages
- name: Set Electron prod version - name: Set Electron prod version
run: | run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
VERSION="$(node -p "require('./package.json').version")" VERSION="$(node -p "require('./package.json').version")"
export VERSION export VERSION
node -e ' node -e '
@ -115,9 +146,15 @@ jobs:
pkg.version = process.env.VERSION; pkg.version = process.env.VERSION;
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n"); fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
' '
EOF
- name: Build Electron desktop - name: Build Electron desktop
run: bun run build:desktop run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
cd apps/electron
pnpm run package:raw
EOF
- name: Upload desktop artifacts - name: Upload desktop artifacts
uses: https://data.forgejo.org/actions/upload-artifact@v3 uses: https://data.forgejo.org/actions/upload-artifact@v3
@ -126,20 +163,17 @@ jobs:
path: apps/electron/release/ path: apps/electron/release/
release: release:
runs-on: docker runs-on: nixos
needs: [build-web, build-mobile, build-desktop] needs: [build-web, build-mobile, build-desktop]
steps: steps:
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages - name: Pull git submodules
run: apt-get update && apt-get install -y sudo curl jq run: git submodule update --init --recursive
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Download mobile artifact - name: Download mobile artifact
uses: https://data.forgejo.org/actions/download-artifact@v3 uses: https://data.forgejo.org/actions/download-artifact@v3
@ -156,18 +190,24 @@ jobs:
- name: Read version - name: Read version
id: version id: version
run: | run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
VERSION="$(node -p "require('./package.json').version")" VERSION="$(node -p "require('./package.json').version")"
echo "version=$VERSION" >> "$FORGEJO_OUTPUT" echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
echo "tag=$VERSION" >> "$FORGEJO_OUTPUT" echo "tag=$VERSION" >> "$FORGEJO_OUTPUT"
EOF
- name: Copy releases - name: Copy releases
env: env:
TENSAMIN_RELEASE_VERSION: ${{ steps.version.outputs.tag }} TENSAMIN_RELEASE_VERSION: ${{ steps.version.outputs.tag }}
TENSAMIN_RELEASE_TAG: ${{ steps.version.outputs.tag }} TENSAMIN_RELEASE_TAG: ${{ steps.version.outputs.tag }}
run: | run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
ASSET_BASE_URL="${{ forgejo.api_url }}" ASSET_BASE_URL="${{ forgejo.api_url }}"
ASSET_BASE_URL="${ASSET_BASE_URL%/api/v1}/${{ forgejo.repository }}/releases/download/${{ steps.version.outputs.tag }}" ASSET_BASE_URL="${ASSET_BASE_URL%/api/v1}/${{ forgejo.repository }}/releases/download/${{ steps.version.outputs.tag }}"
FORGEJO_RELEASE_ASSET_BASE_URL="$ASSET_BASE_URL" bun --bun run copy-releases FORGEJO_RELEASE_ASSET_BASE_URL="$ASSET_BASE_URL" pnpm run copy-releases
EOF
- name: Create release and upload files - name: Create release and upload files
env: env:
@ -177,6 +217,7 @@ jobs:
SHA: ${{ forgejo.sha }} SHA: ${{ forgejo.sha }}
TAG: ${{ steps.version.outputs.tag }} TAG: ${{ steps.version.outputs.tag }}
run: | run: |
nix develop .#electron --command bash <<'EOF'
set -eu set -eu
test -d releases test -d releases
@ -230,34 +271,43 @@ jobs:
-H "Authorization: token $TOKEN" \ -H "Authorization: token $TOKEN" \
-F "attachment=@$file" -F "attachment=@$file"
done done
EOF
- name: Update root flake release hash - name: Delete dev releases
env: env:
TAG: ${{ steps.version.outputs.tag }} TOKEN: ${{ forgejo.token }}
API: ${{ forgejo.api_url }}
REPO: ${{ forgejo.repository }}
run: | run: |
nix develop .#electron --command bash <<'EOF'
set -eu set -eu
APPIMAGE="$(find releases -maxdepth 1 -type f -name 'Tensamin-*-linux-x86_64.AppImage' -print -quit)" PAGE=1
test -n "$APPIMAGE" DELETE_RELEASES=delete-dev-releases.tsv
HASH="$(node -e 'const fs = require("fs"); const crypto = require("crypto"); const file = process.argv[1]; console.log("sha256-" + crypto.createHash("sha256").update(fs.readFileSync(file)).digest("base64"));' "$APPIMAGE")" : > "$DELETE_RELEASES"
export HASH
node -e ' while :; do
const fs = require("fs"); curl -fsS \
const version = process.env.TAG; -H "Authorization: token $TOKEN" \
const hash = process.env.HASH; "$API/repos/$REPO/releases?page=$PAGE&limit=50&pre-release=true" \
let content = fs.readFileSync("flake.nix", "utf8"); -o releases.json
content = content.replace(/version = "[^"]+";/, `version = "${version}";`);
content = content.replace(/hash = "sha256-[^"]+";/, `hash = "${hash}";`);
fs.writeFileSync("flake.nix", content);
'
if git diff --quiet -- flake.nix; then COUNT="$(jq 'length' releases.json)"
echo "flake.nix already has the current release hash." test "$COUNT" -gt 0 || break
exit 0
fi
git add flake.nix jq -r \
git -c user.name="forgejo-actions" -c user.email="forgejo-actions@localhost" commit -m "(qol): update release flake hash" '.[] | select(.prerelease == true) | select(.tag_name | contains("-dev-")) | [.id, .tag_name] | @tsv' releases.json \
git push >> "$DELETE_RELEASES"
PAGE="$((PAGE + 1))"
done
while IFS="$(printf '\t')" read -r release_id release_tag; do
test -n "$release_id" || continue
echo "Deleting dev release $release_tag"
curl -fsS -X DELETE \
-H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases/$release_id"
done < "$DELETE_RELEASES"
EOF

4
.gitignore vendored
View file

@ -1,2 +1,6 @@
node_modules node_modules
releases releases
.fallow
.direnv
keystore.jks
keystore.properties

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

View file

@ -5,4 +5,5 @@ coverage
*.tsbuildinfo *.tsbuildinfo
bun.lock bun.lock
apps/tauri/src-tauri apps/tauri/src-tauri
apps/electron/release
licenses licenses

25
LICENSE
View file

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

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
- Full accessability
- Add settings saving & update onboarding to use it
- Add onboarding page to load one profile during onboarding
-> Most folders in packages/ have specific todos

View file

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

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,44 +5,48 @@
"description": "Tensamin desktop client", "description": "Tensamin desktop client",
"author": "methanium", "author": "methanium",
"homepage": "https://git.methanium.net/tensamin/client", "homepage": "https://git.methanium.net/tensamin/client",
"desktopName": "Tensamin",
"type": "module", "type": "module",
"main": "dist/main/main.js", "main": "dist/main/main.js",
"scripts": { "scripts": {
"clean": "rm -rf dist release", "clean": "rm -rf dist release",
"build:web": "cd ../.. && bun run build:web", "lint": "eslint src",
"build:web": "cd ../.. && pnpm run build:web",
"build": "tsc -p tsconfig.json && esbuild src/preload/preload.ts --bundle --platform=node --format=cjs --external:electron --outfile=dist/preload/preload.cjs", "build": "tsc -p tsconfig.json && esbuild src/preload/preload.ts --bundle --platform=node --format=cjs --external:electron --outfile=dist/preload/preload.cjs",
"dev:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron . --verbose", "dev:raw": "cd ../.. && pnpm run build:web && cd apps/electron && pnpm run build && electron . --verbose",
"dev": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run dev:raw; else bun run dev:raw; fi", "dev": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run dev:raw; else pnpm run dev:raw; fi",
"start:raw": "bun run build && electron .", "start:raw": "pnpm run build && electron .",
"start": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run start:raw; else bun run start:raw; fi", "start": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run start:raw; else pnpm run start:raw; fi",
"package:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron-builder --publish never", "package:raw": "cd ../.. && pnpm run build:web && cd apps/electron && pnpm run build && electron-builder --publish never",
"package": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run package:raw; else bun run package:raw; fi", "package": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run package:raw; else pnpm run package:raw; fi",
"package:linux:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron-builder --linux --publish never", "package:linux:raw": "cd ../.. && pnpm run build:web && cd apps/electron && pnpm run build && electron-builder --linux --publish never",
"package:linux": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run package:linux:raw; else bun run package:linux:raw; fi", "package:linux": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run package:linux:raw; else pnpm run package:linux:raw; fi",
"package:windows:raw": "cd ../.. && bun run build:web && cd apps/electron && bun run build && electron-builder --win --publish never", "package:windows:raw": "cd ../.. && pnpm run build:web && cd apps/electron && pnpm run build && electron-builder --win --publish never",
"package:windows": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run package:windows:raw; else bun run package:windows:raw; fi", "package:windows": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run package:windows:raw; else pnpm run package:windows:raw; fi",
"checksum": "bun scripts/generate-release-metadata.ts", "checksum": "node scripts/generate-release-metadata.ts",
"generate-signing-key": "bun scripts/generate-signing-key.ts", "generate-signing-key": "node scripts/generate-signing-key.ts",
"validate:raw": "bun run build && bun run package:linux:raw", "validate:raw": "pnpm run build && pnpm run package:linux:raw",
"validate": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop --command bun run validate:raw; else bun run validate:raw; fi" "validate": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run validate:raw; else pnpm run validate:raw; fi"
},
"dependencies": {
"electron-updater": "^6.6.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^25.9.1", "@types/node": "^26.1.2",
"electron": "^39.2.7", "electron": "^43.3.0",
"electron-builder": "^26.0.12", "electron-builder": "^26.15.3",
"esbuild": "^0.25.11", "esbuild": "^0.28.1",
"typescript": "~6.0.3" "typescript": "~6.0.3"
}, },
"build": { "build": {
"appId": "net.tensamin.client", "appId": "net.tensamin.client",
"productName": "Tensamin", "productName": "Tensamin",
"executableName": "tensamin",
"artifactName": "Tensamin-${version}-${os}-${arch}.${ext}", "artifactName": "Tensamin-${version}-${os}-${arch}.${ext}",
"icon": "build/icons/icon.png",
"directories": { "directories": {
"output": "release" "output": "release"
}, },
"toolsets": {
"appimage": "1.0.3"
},
"files": [ "files": [
"dist/**/*", "dist/**/*",
"package.json" "package.json"
@ -51,18 +55,50 @@
{ {
"from": "../web/dist", "from": "../web/dist",
"to": "web" "to": "web"
},
{
"from": "build/icons",
"to": "icons",
"filter": [
"32x32.png",
"icon.png"
]
} }
], ],
"linux": { "linux": {
"target": ["AppImage", "deb", "rpm"], "target": [
"AppImage",
"deb",
"rpm"
],
"icon": "build/icons",
"executableName": "tensamin",
"category": "Network", "category": "Network",
"maintainer": "methanium" "maintainer": "Methanium",
"syncDesktopName": true,
"desktop": {
"entry": {
"Name": "Tensamin",
"StartupWMClass": "Tensamin"
}
}
}, },
"win": { "win": {
"target": ["nsis", "portable"] "target": [
"nsis",
"portable"
],
"icon": "build/icons/icon.ico"
}, },
"mac": { "mac": {
"target": ["dmg"] "target": [
"dmg"
],
"icon": "build/icons/icon.icns",
"extendInfo": {
"NSCameraUsageDescription": "Tensamin uses your camera when you choose to share it in a call.",
"NSMicrophoneUsageDescription": "Tensamin uses your microphone for calls."
}
}, },
"publish": null "publish": null
} }

View file

@ -1,5 +1,11 @@
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { createReadStream, existsSync, readdirSync, statSync, writeFileSync } from "node:fs"; import {
createReadStream,
existsSync,
readdirSync,
statSync,
writeFileSync,
} from "node:fs";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import rootPackage from "../../../package.json" with { type: "json" }; import rootPackage from "../../../package.json" with { type: "json" };
@ -55,7 +61,10 @@ const metadata = {
artifacts, artifacts,
}; };
writeFileSync(join(outDir, "electron-release-metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`); writeFileSync(
join(outDir, "electron-release-metadata.json"),
`${JSON.stringify(metadata, null, 2)}\n`,
);
writeFileSync( writeFileSync(
join(outDir, "SHA256SUMS"), join(outDir, "SHA256SUMS"),
`${artifacts.map((artifact) => `${artifact.sha256} ${artifact.name}`).join("\n")}\n`, `${artifacts.map((artifact) => `${artifact.sha256} ${artifact.name}`).join("\n")}\n`,

View file

@ -1,14 +1,41 @@
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { dirname, join, resolve } from "node:path"; import { dirname, join, resolve } from "node:path";
import { app, BrowserWindow, desktopCapturer, ipcMain, session, shell } from "electron"; import {
app,
BrowserWindow,
desktopCapturer,
globalShortcut,
ipcMain,
session,
shell,
} from "electron";
import { checkForUpdates } from "./updates.js"; import { checkForUpdates } from "./updates.js";
import { ipcChannels, type DesktopScreenShareCapabilities } from "../shared/ipc.js"; import {
ipcChannels,
type DesktopCallStatus,
type DesktopGlobalHotkeyBinding,
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 __dirname = dirname(fileURLToPath(import.meta.url));
const verbose = process.argv.includes("--verbose"); const verbose = process.argv.includes("--verbose");
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow | null = null;
let selectedScreenShareSourceId: string | null = null; let selectedScreenShareSourceId: string | null = null;
let globalHotkeyBindings: DesktopGlobalHotkeyBinding[] = [];
let globalHotkeysSuspended = false;
app.setName("tensamin");
app.setPath("userData", join(app.getPath("appData"), "tensamin", "electron"));
if (verbose) { if (verbose) {
app.commandLine.appendSwitch("enable-logging", "stderr"); app.commandLine.appendSwitch("enable-logging", "stderr");
@ -16,7 +43,25 @@ if (verbose) {
app.commandLine.appendSwitch("log-level", "0"); app.commandLine.appendSwitch("log-level", "0");
} }
if (process.platform === "linux" && process.env.XDG_SESSION_TYPE === "wayland" && !process.env.TENSAMIN_ENABLE_VULKAN) { 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"
) {
app.commandLine.appendSwitch("enable-features", "GlobalShortcutsPortal");
}
if (
process.platform === "linux" &&
process.env.XDG_SESSION_TYPE === "wayland" &&
!process.env.TENSAMIN_ENABLE_VULKAN
) {
app.commandLine.appendSwitch("disable-features", "Vulkan"); app.commandLine.appendSwitch("disable-features", "Vulkan");
} }
@ -34,6 +79,12 @@ function getRendererIndex() {
return join(process.resourcesPath, "web", "index.html"); return join(process.resourcesPath, "web", "index.html");
} }
function getWindowIcon() {
if (process.platform === "darwin") return undefined;
if (app.isPackaged) return join(process.resourcesPath, "icons", "icon.png");
return resolve(__dirname, "../../build/icons/icon.png");
}
function getPlatform(): DesktopScreenShareCapabilities["platform"] { function getPlatform(): DesktopScreenShareCapabilities["platform"] {
if (process.platform === "linux") return "linux"; if (process.platform === "linux") return "linux";
if (process.platform === "darwin") return "macos"; if (process.platform === "darwin") return "macos";
@ -68,7 +119,7 @@ function execJson(command: string, args: string[]) {
}); });
} }
async function listAudioOutputs() { async function listAudioOutputs(): Promise<DesktopScreenShareAudioOutput[]> {
verboseLog("listAudioOutputs", { platform: process.platform }); verboseLog("listAudioOutputs", { platform: process.platform });
if (process.platform !== "linux") return []; if (process.platform !== "linux") return [];
@ -81,11 +132,14 @@ async function listAudioOutputs() {
if (!sink || typeof sink !== "object") return null; if (!sink || typeof sink !== "object") return null;
const record = sink as Record<string, unknown>; const record = sink as Record<string, unknown>;
const id = record.index == null ? undefined : String(record.index); const id = record.index == null ? undefined : String(record.index);
const name = typeof record.description === "string" ? record.description : id; const name =
typeof record.description === "string" ? record.description : id;
if (!id || !name) return null; if (!id || !name) return null;
return { id, name, isDefault: false }; return { id, name, isDefault: false };
}) })
.filter(Boolean); .filter(
(output): output is DesktopScreenShareAudioOutput => output != null,
);
} }
async function listScreenShareSources() { async function listScreenShareSources() {
@ -107,30 +161,57 @@ async function listScreenShareSources() {
} }
function registerDisplayMediaHandler() { function registerDisplayMediaHandler() {
session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => { session.defaultSession.setDisplayMediaRequestHandler(
verboseLog("display media request", { selectedScreenShareSourceId }); async (_request, callback) => {
verboseLog("display media request", { selectedScreenShareSourceId });
const sources = await desktopCapturer.getSources({ const sources = await desktopCapturer.getSources({
types: ["screen", "window"], types: ["screen", "window"],
thumbnailSize: { width: 0, height: 0 }, thumbnailSize: { width: 0, height: 0 },
}); });
const selected = sources.find((source) => source.id === selectedScreenShareSourceId); const selected = sources.find(
selectedScreenShareSourceId = null; (source) => source.id === selectedScreenShareSourceId,
const video = selected ?? sources[0]; );
selectedScreenShareSourceId = null;
const video = selected ?? sources[0];
if (!video) { if (!video) {
callback({}); callback({});
return; return;
}
if (process.platform === "win32") {
callback({ video, audio: "loopback" });
return;
}
callback({ video });
},
);
}
function registerMediaPermissionHandler() {
const isTrustedRenderer = (url: string) => {
try {
const parsed = new URL(url);
return parsed.protocol === "file:";
} catch {
return false;
} }
};
if (process.platform === "win32") { session.defaultSession.setPermissionCheckHandler(
callback({ video, audio: "loopback" }); (_webContents, permission, requestingOrigin) =>
return; permission === "media" && isTrustedRenderer(requestingOrigin),
} );
session.defaultSession.setPermissionRequestHandler(
callback({ video }); (_webContents, permission, callback, details) => {
}); callback(
permission === "media" && isTrustedRenderer(details.requestingUrl),
);
},
);
} }
function registerIpc() { function registerIpc() {
@ -138,18 +219,77 @@ function registerIpc() {
ipcMain.handle(ipcChannels.listScreenShareSources, listScreenShareSources); ipcMain.handle(ipcChannels.listScreenShareSources, listScreenShareSources);
ipcMain.handle(ipcChannels.listScreenShareAudioOutputs, listAudioOutputs); ipcMain.handle(ipcChannels.listScreenShareAudioOutputs, listAudioOutputs);
ipcMain.handle(ipcChannels.getScreenShareCapabilities, getScreenShareCapabilities); ipcMain.handle(
ipcMain.handle(ipcChannels.selectScreenShareSource, (_event, sourceId: unknown) => { ipcChannels.getScreenShareCapabilities,
if (typeof sourceId !== "string" || sourceId.length === 0 || sourceId.length > 256) { getScreenShareCapabilities,
throw new Error("Invalid screen share source id."); );
} ipcMain.handle(
ipcChannels.selectScreenShareSource,
(_event, sourceId: unknown) => {
if (
typeof sourceId !== "string" ||
sourceId.length === 0 ||
sourceId.length > 256
) {
throw new Error("Invalid screen share source id.");
}
selectedScreenShareSourceId = sourceId; selectedScreenShareSourceId = sourceId;
verboseLog("selected screen share source", sourceId); verboseLog("selected screen share source", sourceId);
return true; return true;
}); },
);
ipcMain.handle(ipcChannels.getVersion, () => app.getVersion()); ipcMain.handle(ipcChannels.getVersion, () => app.getVersion());
ipcMain.handle(ipcChannels.checkForUpdates, checkForUpdates); ipcMain.handle(ipcChannels.checkForUpdates, checkForUpdates);
ipcMain.handle(ipcChannels.getSecureStorageStatus, getSecureStorageStatus);
ipcMain.handle(ipcChannels.loadSecureStorage, (_event, key: unknown) =>
loadSecureStorage(key),
);
ipcMain.handle(
ipcChannels.saveSecureStorage,
(_event, key: unknown, value: unknown) => saveSecureStorage(key, value),
);
ipcMain.handle(ipcChannels.deleteSecureStorage, (_event, key: unknown) =>
deleteSecureStorage(key),
);
ipcMain.handle(ipcChannels.clearSecureStorage, clearSecureStorage);
ipcMain.handle(
ipcChannels.setGlobalHotkeyBindings,
(event, bindings: unknown) => {
assertTrustedRenderer(event);
return setGlobalHotkeyBindings(bindings);
},
);
ipcMain.handle(
ipcChannels.setGlobalHotkeysSuspended,
(event, suspended: unknown) => {
assertTrustedRenderer(event);
if (typeof suspended !== "boolean") {
throw new Error("Invalid hotkey suspension state.");
}
globalHotkeysSuspended = suspended;
return applyGlobalHotkeyBindings();
},
);
ipcMain.handle(ipcChannels.setCallStatus, (_event, status: unknown) => {
if (
typeof status !== "object" ||
status === null ||
typeof (status as DesktopCallStatus).inCall !== "boolean" ||
typeof (status as DesktopCallStatus).speaking !== "boolean" ||
((status as DesktopCallStatus).iconDataUrl !== undefined &&
(typeof (status as DesktopCallStatus).iconDataUrl !== "string" ||
!(status as DesktopCallStatus).iconDataUrl?.startsWith(
"data:image/png;base64,",
) ||
(status as DesktopCallStatus).iconDataUrl!.length > 16_384))
) {
throw new Error("Invalid call status.");
}
const { inCall, iconDataUrl } = status as DesktopCallStatus;
setTrayCallStatus(inCall, iconDataUrl);
});
ipcMain.handle(ipcChannels.minimizeWindow, () => { ipcMain.handle(ipcChannels.minimizeWindow, () => {
verboseLog("window:minimize"); verboseLog("window:minimize");
mainWindow?.minimize(); mainWindow?.minimize();
@ -171,6 +311,90 @@ function registerIpc() {
}); });
} }
function assertTrustedRenderer(event: Electron.IpcMainInvokeEvent) {
const target = mainWindow;
if (
!target ||
target.isDestroyed() ||
event.sender !== target.webContents ||
event.senderFrame !== target.webContents.mainFrame
) {
throw new Error("Untrusted hotkey IPC sender.");
}
try {
if (fileURLToPath(event.senderFrame.url) === getRendererIndex()) return;
} catch {
// Fall through to the rejection below.
}
throw new Error("Untrusted hotkey IPC sender.");
}
function validGlobalHotkeyBindings(
value: unknown,
): value is DesktopGlobalHotkeyBinding[] {
return (
Array.isArray(value) &&
value.length <= 64 &&
value.every(
(binding) =>
binding &&
typeof binding === "object" &&
typeof (binding as DesktopGlobalHotkeyBinding).id === "string" &&
/^[a-z0-9.-]+$/i.test((binding as DesktopGlobalHotkeyBinding).id) &&
(binding as DesktopGlobalHotkeyBinding).id.length > 0 &&
(binding as DesktopGlobalHotkeyBinding).id.length <= 128 &&
typeof (binding as DesktopGlobalHotkeyBinding).accelerator ===
"string" &&
(binding as DesktopGlobalHotkeyBinding).accelerator.length > 0 &&
(binding as DesktopGlobalHotkeyBinding).accelerator.length <= 128,
)
);
}
function applyGlobalHotkeyBindings() {
globalShortcut.unregisterAll();
const statuses = Object.fromEntries(
globalHotkeyBindings.map(({ id }) => [id, false]),
);
if (globalHotkeysSuspended) return statuses;
const grouped = new Map<string, string[]>();
for (const { id, accelerator } of globalHotkeyBindings) {
const ids = grouped.get(accelerator) ?? [];
ids.push(id);
grouped.set(accelerator, ids);
}
for (const [accelerator, ids] of grouped) {
let registered = false;
try {
registered = globalShortcut.register(accelerator, () => {
const target = mainWindow;
if (!target || target.isDestroyed()) return;
ids.forEach((id) =>
target.webContents.send(ipcChannels.globalHotkeyTriggered, id),
);
});
} catch (error) {
console.error("Failed to register global hotkey", accelerator, error);
}
ids.forEach((id) => {
statuses[id] = registered;
});
}
return statuses;
}
function setGlobalHotkeyBindings(bindings: unknown) {
if (!validGlobalHotkeyBindings(bindings)) {
throw new Error("Invalid global hotkey bindings.");
}
globalHotkeyBindings = bindings;
return applyGlobalHotkeyBindings();
}
async function createWindow() { async function createWindow() {
const rendererIndex = getRendererIndex(); const rendererIndex = getRendererIndex();
verboseLog("creating main window", { verboseLog("creating main window", {
@ -191,6 +415,7 @@ async function createWindow() {
minWidth: 900, minWidth: 900,
minHeight: 600, minHeight: 600,
title: "Tensamin", title: "Tensamin",
icon: getWindowIcon(),
frame: false, frame: false,
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
@ -210,18 +435,24 @@ async function createWindow() {
}); });
if (verbose) { if (verbose) {
mainWindow.webContents.on("console-message", (_event, level, message, line, sourceId) => { mainWindow.webContents.on(
const target = level >= 2 ? console.error : console.log; "console-message",
target("[tensamin:renderer]", message, { level, line, sourceId }); (_event, level, message, line, sourceId) => {
}); const target = level >= 2 ? console.error : console.log;
target("[tensamin:renderer]", message, { level, line, sourceId });
},
);
mainWindow.webContents.on("did-fail-load", (_event, errorCode, errorDescription, validatedURL) => { mainWindow.webContents.on(
console.error("[tensamin:electron] renderer failed to load", { "did-fail-load",
errorCode, (_event, errorCode, errorDescription, validatedURL) => {
errorDescription, console.error("[tensamin:electron] renderer failed to load", {
validatedURL, errorCode,
}); errorDescription,
}); validatedURL,
});
},
);
mainWindow.webContents.on("did-finish-load", () => { mainWindow.webContents.on("did-finish-load", () => {
verboseLog("renderer finished loading", mainWindow?.webContents.getURL()); verboseLog("renderer finished loading", mainWindow?.webContents.getURL());
@ -249,6 +480,10 @@ app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) void createWindow(); if (BrowserWindow.getAllWindows().length === 0) void createWindow();
}); });
app.on("will-quit", () => {
globalShortcut.unregisterAll();
});
if (verbose) { if (verbose) {
process.on("uncaughtException", (error) => { process.on("uncaughtException", (error) => {
console.error("[tensamin:electron] uncaught exception", error); console.error("[tensamin:electron] uncaught exception", error);
@ -265,6 +500,8 @@ async function start() {
verboseLog("app ready"); verboseLog("app ready");
registerIpc(); registerIpc();
registerDisplayMediaHandler(); registerDisplayMediaHandler();
registerMediaPermissionHandler();
initTray(() => mainWindow);
await createWindow(); 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

@ -3,13 +3,21 @@ import { createReadStream } from "node:fs";
import { mkdir, rm, writeFile } from "node:fs/promises"; import { mkdir, rm, writeFile } from "node:fs/promises";
import { basename, join } from "node:path"; import { basename, join } from "node:path";
import { app, net } from "electron"; import { app, net } from "electron";
import type { ReleaseArtifact, ReleaseMetadata, UpdateCheckResult } from "../shared/ipc.js"; import type {
ReleaseArtifact,
ReleaseMetadata,
UpdateCheckResult,
} from "../shared/ipc.js";
const metadataUrl = process.env.TENSAMIN_UPDATE_METADATA_URL; const metadataUrl = process.env.TENSAMIN_UPDATE_METADATA_URL;
function compareSemver(left: string, right: string) { function compareSemver(left: string, right: string) {
const leftParts = left.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0); const leftParts = left
const rightParts = right.split(/[.-]/).map((part) => Number.parseInt(part, 10) || 0); .split(/[.-]/)
.map((part) => Number.parseInt(part, 10) || 0);
const rightParts = right
.split(/[.-]/)
.map((part) => Number.parseInt(part, 10) || 0);
const length = Math.max(leftParts.length, rightParts.length); const length = Math.max(leftParts.length, rightParts.length);
for (let index = 0; index < length; index += 1) { for (let index = 0; index < length; index += 1) {
@ -42,7 +50,9 @@ function requestText(url: string): Promise<string> {
const request = net.request(url); const request = net.request(url);
request.on("response", (response) => { request.on("response", (response) => {
if (response.statusCode < 200 || response.statusCode >= 300) { if (response.statusCode < 200 || response.statusCode >= 300) {
reject(new Error(`Update request failed with HTTP ${response.statusCode}.`)); reject(
new Error(`Update request failed with HTTP ${response.statusCode}.`),
);
return; return;
} }
@ -86,7 +96,9 @@ async function sha256File(filePath: string) {
return hash.digest("hex"); return hash.digest("hex");
} }
function selectArtifact(metadata: ReleaseMetadata): ReleaseArtifact | undefined { function selectArtifact(
metadata: ReleaseMetadata,
): ReleaseArtifact | undefined {
const platform = platformName(); const platform = platformName();
const arch = archName(); const arch = archName();
@ -102,16 +114,26 @@ export async function checkForUpdates(): Promise<UpdateCheckResult> {
return { available: false, currentVersion, latestVersion: currentVersion }; return { available: false, currentVersion, latestVersion: currentVersion };
} }
const metadata = JSON.parse(await requestText(metadataUrl)) as ReleaseMetadata; const metadata = JSON.parse(
await requestText(metadataUrl),
) as ReleaseMetadata;
const artifact = selectArtifact(metadata); const artifact = selectArtifact(metadata);
if (isDevVersion(metadata.version) !== isDevVersion(currentVersion)) { if (isDevVersion(metadata.version) !== isDevVersion(currentVersion)) {
return { available: false, currentVersion, latestVersion: metadata.version }; return {
available: false,
currentVersion,
latestVersion: metadata.version,
};
} }
if (isDevVersion(currentVersion)) { if (isDevVersion(currentVersion)) {
if (!artifact || metadata.version === currentVersion) { if (!artifact || metadata.version === currentVersion) {
return { available: false, currentVersion, latestVersion: metadata.version }; return {
available: false,
currentVersion,
latestVersion: metadata.version,
};
} }
return { return {
@ -123,7 +145,11 @@ export async function checkForUpdates(): Promise<UpdateCheckResult> {
} }
if (!artifact || compareSemver(metadata.version, currentVersion) <= 0) { if (!artifact || compareSemver(metadata.version, currentVersion) <= 0) {
return { available: false, currentVersion, latestVersion: metadata.version }; return {
available: false,
currentVersion,
latestVersion: metadata.version,
};
} }
return { return {
@ -140,7 +166,9 @@ export async function downloadVerifiedArtifact(artifact: ReleaseArtifact) {
await mkdir(updatesDir, { recursive: true }); await mkdir(updatesDir, { recursive: true });
const destination = join(updatesDir, basename(artifact.name)); const destination = join(updatesDir, basename(artifact.name));
await writeFile(destination, await requestBuffer(artifact.url), { mode: 0o600 }); await writeFile(destination, await requestBuffer(artifact.url), {
mode: 0o600,
});
const actualHash = await sha256File(destination); const actualHash = await sha256File(destination);
if (actualHash !== artifact.sha256) { if (actualHash !== artifact.sha256) {

View file

@ -1,10 +1,24 @@
import { contextBridge, ipcRenderer } from "electron"; import { contextBridge, ipcRenderer } from "electron";
import { ipcChannels, type DesktopScreenShareSource } from "../shared/ipc.js"; import {
ipcChannels,
type DesktopCallStatus,
type DesktopGlobalHotkeyBinding,
type DesktopScreenShareSource,
secureStorageLimits,
} from "../shared/ipc.js";
function windowAction(channel: string) { function windowAction(channel: string) {
return () => ipcRenderer.invoke(channel); return () => ipcRenderer.invoke(channel);
} }
function validKey(key: string) {
return (
typeof key === "string" &&
key.length > 0 &&
Buffer.byteLength(key, "utf8") <= secureStorageLimits.maxKeyBytes
);
}
const desktopApi = { const desktopApi = {
media: { media: {
listScreenShareSources: () => listScreenShareSources: () =>
@ -27,6 +41,56 @@ const desktopApi = {
updates: { updates: {
checkForUpdates: () => ipcRenderer.invoke(ipcChannels.checkForUpdates), checkForUpdates: () => ipcRenderer.invoke(ipcChannels.checkForUpdates),
}, },
call: {
setStatus: (status: DesktopCallStatus) => {
if (
typeof status?.inCall !== "boolean" ||
typeof status?.speaking !== "boolean" ||
(status.iconDataUrl !== undefined &&
(typeof status.iconDataUrl !== "string" ||
!status.iconDataUrl.startsWith("data:image/png;base64,")))
) {
return Promise.reject(new Error("Invalid call status."));
}
return ipcRenderer.invoke(ipcChannels.setCallStatus, status);
},
},
hotkeys: {
setBindings: (bindings: DesktopGlobalHotkeyBinding[]) =>
ipcRenderer.invoke(ipcChannels.setGlobalHotkeyBindings, bindings),
setSuspended: (suspended: boolean) =>
typeof suspended === "boolean"
? ipcRenderer.invoke(ipcChannels.setGlobalHotkeysSuspended, suspended)
: Promise.reject(new Error("Invalid hotkey suspension state.")),
onTriggered: (callback: (id: string) => void) => {
const listener = (_event: Electron.IpcRendererEvent, id: unknown) => {
if (typeof id === "string") callback(id);
};
ipcRenderer.on(ipcChannels.globalHotkeyTriggered, listener);
return () => {
ipcRenderer.removeListener(ipcChannels.globalHotkeyTriggered, listener);
};
},
},
secureStorage: {
getStatus: () => ipcRenderer.invoke(ipcChannels.getSecureStorageStatus),
load: (key: string) =>
validKey(key)
? ipcRenderer.invoke(ipcChannels.loadSecureStorage, key)
: Promise.reject(new Error("Invalid secure storage key.")),
save: (key: string, value: string) =>
validKey(key) &&
typeof value === "string" &&
Buffer.byteLength(value, "utf8") <= secureStorageLimits.maxValueBytes
? ipcRenderer.invoke(ipcChannels.saveSecureStorage, key, value)
: Promise.reject(new Error("Invalid secure storage key or value.")),
delete: (key: string) =>
validKey(key)
? ipcRenderer.invoke(ipcChannels.deleteSecureStorage, key)
: Promise.reject(new Error("Invalid secure storage key.")),
clear: () => ipcRenderer.invoke(ipcChannels.clearSecureStorage),
},
window: { window: {
minimize: () => windowAction(ipcChannels.minimizeWindow), minimize: () => windowAction(ipcChannels.minimizeWindow),
maximize: () => windowAction(ipcChannels.maximizeWindow), maximize: () => windowAction(ipcChannels.maximizeWindow),

View file

@ -20,6 +20,27 @@ export type DesktopScreenShareCapabilities = {
hasReliableSystemAudio: boolean; hasReliableSystemAudio: boolean;
}; };
export type DesktopCallStatus = {
inCall: boolean;
speaking: boolean;
iconDataUrl?: string;
};
export type DesktopSecureStorageStatus = {
available: boolean;
backend: string | null;
};
export type DesktopGlobalHotkeyBinding = {
id: string;
accelerator: string;
};
export const secureStorageLimits = {
maxKeyBytes: 256,
maxValueBytes: 1024 * 1024,
} as const;
export type ReleaseArtifact = { export type ReleaseArtifact = {
name: string; name: string;
platform: string; platform: string;
@ -55,4 +76,13 @@ export const ipcChannels = {
closeWindow: "window:close", closeWindow: "window:close",
getVersion: "app:getVersion", getVersion: "app:getVersion",
checkForUpdates: "updates:checkForUpdates", checkForUpdates: "updates:checkForUpdates",
setCallStatus: "call:setStatus",
getSecureStorageStatus: "secureStorage:getStatus",
loadSecureStorage: "secureStorage:load",
saveSecureStorage: "secureStorage:save",
deleteSecureStorage: "secureStorage:delete",
clearSecureStorage: "secureStorage:clear",
setGlobalHotkeyBindings: "hotkeys:setBindings",
setGlobalHotkeysSuspended: "hotkeys:setSuspended",
globalHotkeyTriggered: "hotkeys:triggered",
} as const; } as const;

36
apps/pwa/package.json Normal file
View file

@ -0,0 +1,36 @@
{
"name": "@tensamin/pwa",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./vite": "./src/vite.ts",
"./runtime": "./src/runtime.tsx"
},
"scripts": {
"format": "pnpm exec prettier --write .",
"lint": "eslint src",
"build": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.worker.json --noEmit"
},
"dependencies": {
"@methanium/ui": "*",
"@tauri-apps/api": "^2.11.1",
"@tensamin/crypto": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/storage": "workspace:*",
"mtp": "*",
"react": "^19.2.8",
"sonner": "^2.0.7",
"vite-plugin-pwa": "^1.1.0",
"workbox-core": "^7.3.0",
"workbox-precaching": "^7.3.0",
"workbox-routing": "^7.3.0",
"workbox-strategies": "^7.3.0"
},
"devDependencies": {
"@types/node": "^26.1.2",
"@types/react": "^19.2.18",
"typescript": "~6.0.3",
"vite": "^8.2.1"
}
}

189
apps/pwa/src/runtime.tsx Normal file
View file

@ -0,0 +1,189 @@
import { useEffect } from "react";
import { toast } from "sonner";
import { setDatabaseEntry } from "@tensamin/shared/indexedDb";
import { isTauri } from "@tauri-apps/api/core";
import "./style.css";
const launchedFiles: File[] = [];
const fileListeners = new Set<(file: File) => void>();
function emitLaunchedFile(file: File) {
if (fileListeners.size === 0) launchedFiles.push(file);
else for (const listener of fileListeners) listener(file);
}
export function subscribeTuFileLaunch(listener: (file: File) => void) {
fileListeners.add(listener);
for (const file of launchedFiles.splice(0)) listener(file);
return () => {
fileListeners.delete(listener);
};
}
function isInstalledPwa() {
return (
window.matchMedia("(display-mode: standalone)").matches ||
window.matchMedia("(display-mode: window-controls-overlay)").matches ||
(navigator as Navigator & { standalone?: boolean }).standalone === true
);
}
function applicationServerKey(value: string) {
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
}
async function enablePush() {
if (!("Notification" in window))
throw new Error("Notifications are not supported by this browser.");
const permission = await Notification.requestPermission();
if (permission !== "granted")
throw new Error("Notification permission was not granted.");
const publicKey = import.meta.env.VITE_WEB_PUSH_PUBLIC_KEY;
if (
!publicKey ||
!("serviceWorker" in navigator) ||
!("PushManager" in window)
) {
return;
}
const registration = await navigator.serviceWorker.ready;
const subscription =
(await registration.pushManager.getSubscription()) ??
(await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: applicationServerKey(publicKey),
}));
await setDatabaseEntry("keys", "push-subscription", subscription.toJSON());
}
function InstalledPwaRuntime() {
useEffect(() => {
if (
!("serviceWorker" in navigator) ||
!["http:", "https:"].includes(window.location.protocol)
) {
return;
}
let reloading = false;
const handleControllerChange = () => {
if (reloading) return;
reloading = true;
// A newly activated service worker must reload the document it controls.
// eslint-disable-next-line tensamin/no-window-location-reload
window.location.reload();
};
navigator.serviceWorker.addEventListener(
"controllerchange",
handleControllerChange,
);
void navigator.serviceWorker
.register(
import.meta.env.DEV ? "/dev-sw.js?dev-sw" : "/serviceWorker.js",
{
type: "module",
},
)
.then((registration) => {
const watchWorker = (worker: ServiceWorker) => {
worker.addEventListener("statechange", () => {
if (worker.state !== "installed") return;
if (!navigator.serviceWorker.controller) {
toast.success("Tensamin is ready for offline startup");
return;
}
toast("A Tensamin update is ready", {
duration: Infinity,
action: {
label: "Update",
onClick: () => worker.postMessage({ type: "SKIP_WAITING" }),
},
});
});
};
if (registration.installing) watchWorker(registration.installing);
registration.addEventListener("updatefound", () => {
if (registration.installing) watchWorker(registration.installing);
});
})
.catch((error: unknown) => {
console.error("Failed to register the Tensamin service worker", error);
});
return () => {
navigator.serviceWorker.removeEventListener(
"controllerchange",
handleControllerChange,
);
};
}, []);
useEffect(() => {
const launchQueue = (
window as Window & {
launchQueue?: {
setConsumer: (
consumer: (params: {
files?: Array<{ getFile: () => Promise<File> }>;
}) => void,
) => void;
};
}
).launchQueue;
launchQueue?.setConsumer((params) => {
for (const handle of params.files ?? []) {
void handle.getFile().then(emitLaunchedFile);
}
});
}, []);
useEffect(() => {
if (
!("Notification" in window) ||
Notification.permission !== "default" ||
localStorage.getItem("pwa-push-hint")
) {
return;
}
localStorage.setItem("pwa-push-hint", "shown");
toast("Enable message notifications", {
duration: Infinity,
action: {
label: "Enable",
onClick: () => {
void enablePush()
.then(() => toast.success("Notifications enabled"))
.catch((error: unknown) =>
toast.error(
error instanceof Error
? error.message
: "Could not enable notifications",
),
);
},
},
});
}, []);
useEffect(() => {
if (
"Notification" in window &&
Notification.permission === "granted" &&
import.meta.env.VITE_WEB_PUSH_PUBLIC_KEY
) {
void enablePush().catch((error: unknown) => {
console.error("Failed to refresh the Web Push subscription", error);
});
}
}, []);
return null;
}
export default function PwaRuntime() {
if (isTauri() || !isInstalledPwa()) return null;
return <InstalledPwaRuntime />;
}

View file

@ -0,0 +1,161 @@
/// <reference lib="webworker" />
import { base64ToBytes } from "mtp";
import { clientsClaim } from "workbox-core";
import { cleanupOutdatedCaches, precacheAndRoute } from "workbox-precaching";
import { NavigationRoute, registerRoute } from "workbox-routing";
import { createHandlerBoundToURL } from "workbox-precaching";
import { CacheFirst } from "workbox-strategies";
import { decryptChatText, unwrapChatSecret } from "@tensamin/crypto/chatSecret";
import { loadSecureBrowserValue } from "@tensamin/storage/browserSecure";
declare let self: ServiceWorkerGlobalScope;
type PushPayload = {
version: 1;
senderId: number;
sender: string;
avatar?: string;
message: { content: string };
secret: {
chatId: string;
secretId: string;
version: number;
encryptedSecret: string;
kemCiphertext: string;
wrappingScheme: string;
};
};
function isPushPayload(value: unknown): value is PushPayload {
if (!value || typeof value !== "object") return false;
const payload = value as Partial<PushPayload>;
const message = payload.message as
Partial<PushPayload["message"]> | undefined;
const secret = payload.secret as Partial<PushPayload["secret"]> | undefined;
const stringValues = [
payload.sender,
message?.content,
secret?.chatId,
secret?.secretId,
secret?.encryptedSecret,
secret?.kemCiphertext,
secret?.wrappingScheme,
];
return (
payload.version === 1 &&
typeof payload.senderId === "number" &&
Number.isSafeInteger(payload.senderId) &&
payload.senderId > 0 &&
typeof secret?.version === "number" &&
stringValues.every((item) => typeof item === "string")
);
}
async function decryptPush(payload: PushPayload) {
const keyring = await loadSecureBrowserValue<string>("mtp_keyring");
if (!keyring) throw new Error("MTP credentials are unavailable.");
const chatSecret = await unwrapChatSecret({
encryptedSecret: base64ToBytes(payload.secret.encryptedSecret),
kemCiphertext: base64ToBytes(payload.secret.kemCiphertext),
keyring,
chatId: payload.secret.chatId,
secretId: payload.secret.secretId,
version: payload.secret.version,
wrappingScheme: payload.secret.wrappingScheme,
});
try {
return await decryptChatText(chatSecret, payload.message.content);
} finally {
chatSecret.fill(0);
}
}
clientsClaim();
cleanupOutdatedCaches();
const precacheManifest = self.__WB_MANIFEST;
precacheAndRoute(precacheManifest);
if (
precacheManifest.some((entry) =>
(typeof entry === "string" ? entry : entry.url).endsWith("index.html"),
)
) {
registerRoute(
new NavigationRoute(createHandlerBoundToURL("index.html"), {
denylist: [/^\/api\//],
}),
);
}
registerRoute(
({ request, url }) =>
url.origin === self.location.origin &&
["font", "image", "style"].includes(request.destination),
new CacheFirst({ cacheName: "tensamin-static-v1" }),
);
self.addEventListener("push", (event) => {
event.waitUntil(
(async () => {
let payload: PushPayload | undefined;
try {
const value = event.data?.json() as unknown;
if (isPushPayload(value)) payload = value;
} catch {
// The generic notification below is safe for malformed payloads.
}
let body = "Open Tensamin to view the encrypted message.";
if (payload) {
try {
body = await decryptPush(payload);
} catch {
// Do not leak credential or decryption failures in the notification.
}
}
await self.registration.showNotification(payload?.sender ?? "Tensamin", {
body,
icon: payload?.avatar || "./icons/icon-192.png",
badge: "./icons/notification-badge.png",
tag: payload ? `message-${payload.senderId}` : "message",
data: { url: payload ? `/chat?id=${payload.senderId}` : "/" },
});
const navigatorWithBadge = self.navigator as WorkerNavigator & {
setAppBadge?: (contents?: number) => Promise<void>;
};
await navigatorWithBadge.setAppBadge?.().catch(() => undefined);
})(),
);
});
self.addEventListener("notificationclick", (event) => {
event.notification.close();
event.waitUntil(
(async () => {
const target = new URL(
String(
(event.notification.data as { url?: string } | undefined)?.url ?? "/",
),
self.location.origin,
);
const windows = await self.clients.matchAll({
type: "window",
includeUncontrolled: true,
});
for (const client of windows) {
if ("navigate" in client) await client.navigate(target.href);
return client.focus();
}
return self.clients.openWindow(target.href);
})(),
);
});
self.addEventListener("message", (event) => {
if ((event.data as { type?: string } | undefined)?.type === "SKIP_WAITING") {
void self.skipWaiting();
}
});

18
apps/pwa/src/style.css Normal file
View file

@ -0,0 +1,18 @@
@media (display-mode: standalone), (display-mode: fullscreen) {
[data-pwa-root] {
padding-top: env(safe-area-inset-top, 0px);
padding-right: env(safe-area-inset-right, 0px);
padding-left: env(safe-area-inset-left, 0px);
}
}
@media (display-mode: window-controls-overlay) and (min-width: 768px) {
[data-pwa-navbar] {
min-height: env(titlebar-area-height, 3.375rem);
padding-left: max(1px, env(titlebar-area-x, 0px));
padding-right: max(
0px,
calc(100vw - env(titlebar-area-x, 0px) - env(titlebar-area-width, 100vw))
);
}
}

218
apps/pwa/src/vite.ts Normal file
View file

@ -0,0 +1,218 @@
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import type { Plugin } from "vite";
import { VitePWA } from "vite-plugin-pwa";
const pwaDirectory = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const tauriIcons = resolve(pwaDirectory, "../tauri/src-tauri/icons");
const androidResources = resolve(
pwaDirectory,
"../tauri/src-tauri/gen/android/app/src/main/res",
);
function emitIcons(): Plugin {
const icons = [
{
fileName: "icons/icon-180.png",
source: resolve(tauriIcons, "ios/AppIcon-60x60@3x.png"),
},
{
fileName: "icons/icon-192.png",
source: resolve(androidResources, "mipmap-xxxhdpi/ic_launcher.png"),
},
{
fileName: "icons/icon-96.png",
source: resolve(androidResources, "mipmap-xhdpi/ic_launcher.png"),
},
{
fileName: "icons/icon-512.png",
source: resolve(tauriIcons, "icon.png"),
},
{
fileName: "icons/icon-maskable-512.png",
source: resolve(tauriIcons, "icon.png"),
},
{
fileName: "icons/icon-monochrome-432.png",
source: resolve(
androidResources,
"mipmap-xxxhdpi/ic_launcher_monochrome.png",
),
},
{
fileName: "icons/notification-badge.png",
source: resolve(androidResources, "drawable/ic_notification_small.png"),
},
];
return {
name: "tensamin-pwa-icons",
configureServer(server) {
server.middlewares.use((request, response, next) => {
const pathname = request.url
? new URL(request.url, "http://localhost").pathname.slice(1)
: "";
const icon = icons.find(({ fileName }) => fileName === pathname);
if (!icon) {
next();
return;
}
response.statusCode = 200;
response.setHeader("Content-Type", "image/png");
response.setHeader("Cache-Control", "no-cache");
response.end(readFileSync(icon.source));
});
},
generateBundle() {
for (const icon of icons) {
this.emitFile({
type: "asset",
fileName: icon.fileName,
source: readFileSync(icon.source),
});
}
},
transformIndexHtml: {
order: "post",
handler() {
return [
{
tag: "link",
attrs: {
rel: "apple-touch-icon",
sizes: "180x180",
href: "./icons/icon-180.png",
},
injectTo: "head",
},
{
tag: "meta",
attrs: { name: "apple-mobile-web-app-capable", content: "yes" },
injectTo: "head",
},
{
tag: "meta",
attrs: {
name: "apple-mobile-web-app-status-bar-style",
content: "black-translucent",
},
injectTo: "head",
},
{
tag: "meta",
attrs: {
name: "apple-mobile-web-app-title",
content: "Tensamin",
},
injectTo: "head",
},
{
tag: "meta",
attrs: { name: "theme-color", content: "#006a67" },
injectTo: "head",
},
];
},
},
};
}
export function tensaminPwa(): Plugin[] {
return [
emitIcons(),
...VitePWA({
strategies: "injectManifest",
srcDir: resolve(pwaDirectory, "src"),
filename: "serviceWorker.ts",
injectRegister: null,
registerType: "prompt",
buildBase: "/",
manifestFilename: "manifest.json",
includeAssets: ["favicon.ico", "icons/*.png"],
manifest: {
id: "/",
name: "Tensamin",
short_name: "Tensamin",
description: "Private messaging and calls with Tensamin.",
start_url: "/",
scope: "/",
display: "standalone",
display_override: ["window-controls-overlay", "standalone"],
background_color: "#001f1e",
theme_color: "#006a67",
categories: ["social", "communication"],
orientation: "any",
launch_handler: { client_mode: "focus-existing" },
icons: [
{
src: "icons/icon-192.png",
sizes: "192x192",
type: "image/png",
purpose: "any",
},
{
src: "icons/icon-512.png",
sizes: "512x512",
type: "image/png",
purpose: "any",
},
{
src: "icons/icon-maskable-512.png",
sizes: "512x512",
type: "image/png",
purpose: "maskable",
},
{
src: "icons/icon-monochrome-432.png",
sizes: "432x432",
type: "image/png",
purpose: "monochrome",
},
],
shortcuts: [
{
name: "Chats",
short_name: "Chats",
url: "/",
icons: [
{
src: "icons/icon-96.png",
sizes: "96x96",
type: "image/png",
},
],
},
{
name: "Settings",
short_name: "Settings",
url: "/settings",
icons: [
{
src: "icons/icon-96.png",
sizes: "96x96",
type: "image/png",
},
],
},
],
file_handlers: [
{
action: "/login",
accept: { "application/x-tensamin-user": [".tu"] },
},
],
},
injectManifest: {
globPatterns: ["**/*.{js,css,html,ico,png,svg,woff2,wasm,mp3,wav}"],
globIgnores: ["assets/v2/**"],
maximumFileSizeToCacheInBytes: 15 * 1024 * 1024,
},
devOptions: {
enabled: true,
type: "module",
},
}),
];
}

33
apps/pwa/todo.md Normal file
View file

@ -0,0 +1,33 @@
# Web Push Backend TODO
The client can subscribe and decrypt version 1 push payloads, but reliable delivery requires backend support.
- Generate and securely store a VAPID key pair. Expose only the public key to the web build as `VITE_WEB_PUSH_PUBLIC_KEY`.
- Add authenticated MTP requests for registering, replacing, and deleting a browser `PushSubscription` per user and installation.
- Persist the endpoint, `p256dh`, `auth`, expiration time, stable installation ID, and last-seen time.
- Remove subscriptions when a push service returns HTTP 404 or 410 and rate-limit registrations per user.
- Send pushes when an encrypted live message cannot be delivered to an active browser client. Define duplicate suppression for clients that receive both MTP and Web Push.
- Keep the JSON payload within push-provider limits and use this version 1 shape:
```json
{
"version": 1,
"senderId": 123,
"sender": "Display name",
"avatar": "https://optional.example/avatar",
"message": { "content": "base64 encrypted message content" },
"secret": {
"chatId": "123:456",
"secretId": "chat:123:456:main",
"version": 1,
"encryptedSecret": "base64 wrapped chat secret",
"kemCiphertext": "base64 KEM ciphertext",
"wrappingScheme": "mtp-chat-secret-kem-chacha20poly1305-hkdf-sha256-v1"
}
}
```
- Ensure the wrapped secret is intended for the receiving user's MTP keyring. The server must never receive plaintext message content or plaintext chat secrets.
- Decide how edits, deletions, reactions, calls, read states, and per-chat notification cancellation map to push events.
- Add subscription rotation handling and unregister subscriptions when a user logs out or clears application data.
- Configure production HTTPS, SPA route fallback, `application/manifest+json` for `manifest.json`, and `Cache-Control: no-cache` for the service worker.

23
apps/pwa/tsconfig.json Normal file
View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client", "vite-plugin-pwa/client", "node"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/runtime.tsx", "src/vite.ts"]
}

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "WebWorker"],
"types": ["vite-plugin-pwa/client"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/serviceWorker.ts"]
}

View file

@ -24,6 +24,3 @@ dist-ssr
*.sw? *.sw?
.android .android
/src-tauri/gen/android/keystore.properties
/src-tauri/gen/android/keystore.jks

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

@ -5,4 +5,4 @@
"android_bg": "./background.png", "android_bg": "./background.png",
"android_fg_scale": 100, "android_fg_scale": 100,
"android_monochrome": "./monochrome.png" "android_monochrome": "./monochrome.png"
} }

View file

@ -2,14 +2,14 @@
<!-- Created with Inkscape (http://www.inkscape.org/) --> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
width="90mm" width="90.476906mm"
height="90mm" height="90.476906mm"
viewBox="0 0 90 90" viewBox="0 0 90.476906 90.476906"
version="1.1" version="1.1"
id="svg1" id="svg1"
xml:space="preserve" xml:space="preserve"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)" inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
sodipodi:docname="logo_raw.svg" sodipodi:docname="logo_square_outline_gen.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xlink="http://www.w3.org/1999/xlink"
@ -24,16 +24,32 @@
inkscape:pagecheckerboard="1" inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050" inkscape:deskcolor="#505050"
inkscape:document-units="mm" inkscape:document-units="mm"
inkscape:zoom="0.99999999" inkscape:zoom="2"
inkscape:cx="450" inkscape:cx="191.5"
inkscape:cy="291.5" inkscape:cy="117.25"
inkscape:window-width="1223" inkscape:window-width="2500"
inkscape:window-height="1369" inkscape:window-height="1403"
inkscape:window-x="26" inkscape:window-x="0"
inkscape:window-y="23" inkscape:window-y="0"
inkscape:window-maximized="0" inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs inkscape:current-layer="layer1" /><defs
id="defs1"><linearGradient id="defs1"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1"
radius="1"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch15" id="swatch15"
inkscape:swatch="solid"><stop inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;" style="stop-color:#000000;stop-opacity:1;"
@ -51,7 +67,7 @@
id="stop3" /></linearGradient><linearGradient id="stop3" /></linearGradient><linearGradient
id="swatch2" id="swatch2"
inkscape:swatch="solid"><stop inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;" style="stop-color:#b8f8ff;stop-opacity:1;"
offset="0" offset="0"
id="stop2" /></linearGradient><linearGradient id="stop2" /></linearGradient><linearGradient
id="swatch1" id="swatch1"
@ -184,13 +200,23 @@
x1="63.191292" x1="63.191292"
y1="148.5" y1="148.5"
x2="146.82355" x2="146.82355"
y2="148.5" /></defs><g y2="148.5" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch2"
id="linearGradient1"
gradientUnits="userSpaceOnUse"
x1="63.191292"
y1="148.5"
x2="146.82355"
y2="148.5"
gradientTransform="matrix(1.0144629,0,0,1.0151924,217.46226,13.830072)" /></defs><g
inkscape:label="Layer 1" inkscape:label="Layer 1"
inkscape:groupmode="layer" inkscape:groupmode="layer"
id="layer1" id="layer1"
transform="translate(-62.941767,-104.13688)"><g transform="translate(-281.06837,-119.33314)"
><g
id="g2" id="g2"
transform="matrix(0.78671048,0,0,0.83608014,-116.36038,-50.640745)" transform="matrix(0.77770172,0,0,0.82714179,104.17421,-33.291714)"
inkscape:label="background" inkscape:label="background"
style="display:inline;stroke:url(#linearGradient15);stroke-width:0.616508;stroke-dasharray:none" style="display:inline;stroke:url(#linearGradient15);stroke-width:0.616508;stroke-dasharray:none"
clip-path="url(#clipPath1)"><path clip-path="url(#clipPath1)"><path
@ -213,11 +239,19 @@
id="rect1-1-3-6-2" id="rect1-1-3-6-2"
style="fill:#12a89e;stroke:url(#linearGradient21);stroke-width:0.616508;stroke-dasharray:none" style="fill:#12a89e;stroke:url(#linearGradient21);stroke-width:0.616508;stroke-dasharray:none"
transform="rotate(-75)" transform="rotate(-75)"
d="m -227.83395,371.18179 h 146.303341 v 24.39636 H -227.83395 Z" /></g><g d="m -227.83395,371.18179 h 146.303341 v 24.39636 H -227.83395 Z" /></g><path
id="path1-5"
style="fill:none;stroke:url(#linearGradient1);stroke-width:2;stroke-dasharray:none"
inkscape:label="glow_outline"
d="m 323.73413,119.91784 -40.80246,15.9513 c -0.13609,0.0532 -0.26542,0.21326 -0.28843,0.35757 -2.90631,18.22859 -0.19736,34.27444 8.28042,47.36302 0.0794,0.12264 0.10916,0.33484 0.0657,0.47436 l -7.08112,22.75509 a 0.11130973,0.11130973 40.101955 0 0 0.15695,0.13218 l 18.7652,-9.59506 c 0.13011,-0.0665 0.32608,-0.044 0.43816,0.0497 5.75202,4.81168 12.45464,8.67546 20.46583,11.84762 0.13586,0.0538 0.3564,0.0538 0.49225,-3e-5 32.16862,-12.74646 46.41741,-39.59704 41.09168,-73.02692 -0.023,-0.14431 -0.15232,-0.30436 -0.28841,-0.35757 l -40.80297,-15.9513 a 0.67679193,0.67679193 179.99988 0 0 -0.49284,0 z"
sodipodi:nodetypes="cccccccc"
inkscape:original-d="m 323.98055,119.8215 -41.2953,16.14398 c -2.99079,18.43973 -0.24934,34.65473 8.38277,47.84598 l -7.23836,23.26035 19.23635,-9.83596 c 5.85568,4.9403 12.69983,8.88741 20.91454,12.11489 32.42602,-12.73985 46.75142,-39.74883 41.29581,-73.38526 z"
inkscape:path-effect="#path-effect1"
transform="matrix(0.98854883,0,0,0.98930922,6.03231,1.7325084)" /><g
id="g3" id="g3"
inkscape:label="foreground" inkscape:label="foreground"
style="stroke:url(#linearGradient16);stroke-width:0.5;stroke-dasharray:none" style="stroke:url(#linearGradient16);stroke-width:0.5;stroke-dasharray:none"
transform="translate(3.3653788,0.84760028)"><g transform="matrix(0.98854883,0,0,0.98930922,222.52896,17.64618)"><g
id="g1" id="g1"
inkscape:label="outline" inkscape:label="outline"
style="stroke:url(#linearGradient23);stroke-width:0.5;stroke-dasharray:none"><path style="stroke:url(#linearGradient23);stroke-width:0.5;stroke-dasharray:none"><path

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -4,41 +4,33 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"exports": { "exports": {
"./controls": {
"types": "./src/windowControls.tsx",
"default": "./src/windowControls.tsx"
},
"./deeplinkHandler": { "./deeplinkHandler": {
"types": "./src/deeplinkHandler.tsx", "types": "./src/deeplinkHandler.tsx",
"default": "./src/deeplinkHandler.tsx" "default": "./src/deeplinkHandler.tsx"
},
"./qrCodeScanner": {
"types": "./src/qrCodeScanner.tsx",
"default": "./src/qrCodeScanner.tsx"
} }
}, },
"scripts": { "scripts": {
"dev:mobile:raw": "tauri android dev", "dev:mobile:raw": "adb reverse tcp:3000 tcp:3000 && tauri android dev --host ${TAURI_DEV_HOST:-127.0.0.1}",
"start-adb:mobile:raw": "adb devices",
"build:mobile:raw": "tauri android build", "build:mobile:raw": "tauri android build",
"dev:mobile": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:mobile:raw; else bun dev:mobile:raw; fi", "build:mobile:ci": "node render-version.ts && trap 'node render-version.ts --unrender' EXIT && tauri android build --debug",
"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", "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",
"gen-icons": "tauri icon ./logo.json", "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",
"format": "bunx prettier --write .", "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" "lint": "eslint src"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2", "@methanium/ui": "*",
"@tauri-apps/plugin-barcode-scanner": "~2", "@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-deep-link": "~2", "@tauri-apps/plugin-deep-link": "~2.4.9",
"@tauri-apps/plugin-opener": "^2",
"@tensamin/ui": "*",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",
"lucide-react": "^1.14.0", "react": "^19.2.8",
"react": "^19.2.0", "react-dom": "^19.2.8"
"react-dom": "^19.2.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2.11.4",
"@types/node": "^25.9.1" "@types/node": "^26.1.2"
} }
} }

View file

@ -1,9 +1,12 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { fileURLToPath } from "url";
// Config // Config
const PLACEHOLDER_VERSION = "0.0.0"; const PLACEHOLDER_VERSION = "0.0.0";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootPackageJsonPath = path.resolve(__dirname, "../../package.json"); const rootPackageJsonPath = path.resolve(__dirname, "../../package.json");
const cargoTomlPath = path.resolve(__dirname, "./src-tauri/Cargo.toml"); const cargoTomlPath = path.resolve(__dirname, "./src-tauri/Cargo.toml");
const tauriConfigPath = path.resolve(__dirname, "./src-tauri/tauri.conf.json"); const tauriConfigPath = path.resolve(__dirname, "./src-tauri/tauri.conf.json");

View file

@ -0,0 +1,53 @@
import { spawnSync } from "node:child_process";
import { createInterface } from "node:readline/promises";
const devicesResult = spawnSync("adb", ["devices", "-l"], {
encoding: "utf8",
});
if (devicesResult.status !== 0) {
process.stderr.write(devicesResult.stderr);
process.exit(devicesResult.status ?? 1);
}
const devices = devicesResult.stdout
.split("\n")
.slice(1)
.map((line) => line.trim())
.filter((line) => /\sdevice(?:\s|$)/.test(line));
if (devices.length === 0) {
console.error("No connected ADB devices found.");
process.exit(1);
}
let selectedDevice = devices[0];
if (devices.length > 1) {
console.log("Select a device:");
devices.forEach((device, index) => console.log(`${index + 1}) ${device}`));
const readline = createInterface({
input: process.stdin,
output: process.stdout,
});
const answer = await readline.question("Device: ");
readline.close();
const selectedIndex = Number(answer) - 1;
if (!Number.isInteger(selectedIndex) || !devices[selectedIndex]) {
console.error("Invalid device selection.");
process.exit(1);
}
selectedDevice = devices[selectedIndex];
}
const serial = selectedDevice.split(/\s+/, 1)[0];
const uninstallResult = spawnSync(
"adb",
["-s", serial, "uninstall", "net.tensamin.client.dev"],
{ stdio: "inherit" },
);
process.exit(uninstallResult.status ?? 1);

View file

@ -0,0 +1,31 @@
import { copyFile, mkdir } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const tauriDir = resolve(scriptDir, "..");
const clientDir = resolve(tauriDir, "../..");
const tauriIconsDir = join(tauriDir, "src-tauri", "icons");
const electronIconsDir = join(clientDir, "apps", "electron", "build", "icons");
const desktopIcons = [
"32x32.png",
"64x64.png",
"128x128.png",
"128x128@2x.png",
"icon.png",
"icon.ico",
"icon.icns",
];
await mkdir(electronIconsDir, { recursive: true });
await Promise.all(
desktopIcons.map((icon) =>
copyFile(join(tauriIconsDir, icon), join(electronIconsDir, icon)),
),
);
console.log(
`Synced ${desktopIcons.length} desktop icons to ${electronIconsDir}`,
);

File diff suppressed because it is too large Load diff

View file

@ -15,23 +15,34 @@ name = "mobile_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { version = "2", features = [] } tauri-build = { git = "https://github.com/tauri-apps/tauri", rev = "4af26a3f7f8b692d62cca549bbacd93f5ce90b41", features = [] }
[dependencies] [dependencies]
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
base64 = "0.22"
reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] }
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "a5c8d4f0c898c78351e9d54124886c86e789a22a", features = ["client", "crypto"] }
webpki-root-certs = "1"
tauri-plugin-deep-link = "2" tauri-plugin-deep-link = "2"
tauri-plugin-notification = "2"
tauri-plugin-log = "2"
[target.'cfg(target_os = "android")'.dependencies.tauri] [target.'cfg(target_os = "android")'.dependencies.tauri]
version = "2" version = "2"
features = [] features = []
default-features = true default-features = true
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] [target.'cfg(not(target_os = "android"))'.dependencies.tauri]
tauri-plugin-barcode-scanner = "2" version = "2"
tauri-plugin-app-events = "0.2" features = []
default-features = true
[target.'cfg(target_os = "android")'.dependencies]
jni = "0.22"
[patch.crates-io.tauri] [patch.crates-io.tauri]
git = "https://github.com/tauri-apps/tauri" git = "https://github.com/tauri-apps/tauri"
branch = "feat/cef" rev = "4af26a3f7f8b692d62cca549bbacd93f5ce90b41"

View file

@ -6,14 +6,9 @@
"main" "main"
], ],
"permissions": [ "permissions": [
"core:default",
"opener:default", "opener:default",
"core:window:default", "deep-link:default",
"core:window:allow-start-dragging", "notification:default",
"core:window:allow-close", "log:default"
"core:window:allow-toggle-maximize",
"core:window:allow-minimize",
"core:event:default",
"deep-link:default"
] ]
} }

View file

@ -1,17 +1,11 @@
{ {
"identifier": "mobile-capability", "identifier": "mobile-capability",
"platforms": [ "platforms": ["android", "iOS"],
"android", "windows": ["main"],
"iOS"
],
"windows": [
"main"
],
"permissions": [ "permissions": [
"core:event:default",
"deep-link:default", "deep-link:default",
"app-events:default", "notification:default",
"barcode-scanner:default", "log:default"
"barcode-scanner:allow-scan",
"barcode-scanner:allow-cancel"
] ]
} }

View file

@ -1,3 +1,4 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties import java.util.Properties
import java.io.FileInputStream import java.io.FileInputStream
@ -40,6 +41,7 @@ android {
} }
buildTypes { buildTypes {
getByName("debug") { getByName("debug") {
applicationIdSuffix = ".dev"
manifestPlaceholders["usesCleartextTraffic"] = "true" manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true isDebuggable = true
isJniDebuggable = true isJniDebuggable = true
@ -60,26 +62,29 @@ android {
) )
} }
} }
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures { buildFeatures {
buildConfig = true buildConfig = true
} }
} }
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
}
}
rust { rust {
rootDirRel = "../../../" rootDirRel = "../../../"
} }
dependencies { dependencies {
implementation("androidx.webkit:webkit:1.14.0") implementation("androidx.webkit:webkit:1.16.0")
implementation("androidx.appcompat:appcompat:1.7.1") implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1") implementation("androidx.activity:activity-ktx:1.13.0")
implementation("com.google.android.material:material:1.12.0") implementation("com.google.android.material:material:1.14.0")
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4") androidTestImplementation("androidx.test.ext:junit:1.3.0")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
} }
apply(from = "tauri.build.gradle.kts") apply(from = "tauri.build.gradle.kts")

View file

@ -18,4 +18,4 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile

View file

@ -1,6 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- AndroidTV support --> <!-- AndroidTV support -->
<uses-feature android:name="android.software.leanback" android:required="false" /> <uses-feature android:name="android.software.leanback" android:required="false" />
@ -39,6 +49,30 @@
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. --> <!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
</activity> </activity>
<service
android:name=".MediaProjectionService"
android:exported="false"
android:foregroundServiceType="mediaProjection" />
<service
android:name=".MtpForegroundService"
android:exported="false"
android:stopWithTask="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Maintains the user-enabled encrypted messaging connection and receives incoming messages" />
</service>
<receiver
android:name=".MtpBootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider" android:authorities="${applicationId}.fileprovider"

View file

@ -1,28 +1,114 @@
package net.tensamin.client package net.tensamin.client
import android.graphics.Rect import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.media.projection.MediaProjectionManager
import android.os.Bundle import android.os.Bundle
import android.view.ViewGroup import android.view.MotionEvent
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.view.WindowManager import android.view.WindowManager
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import org.json.JSONObject
class MainActivity : TauriActivity() { class MainActivity : TauriActivity() {
private var contentRoot: FrameLayout? = null private var contentRoot: FrameLayout? = null
private var contentChild: android.view.View? = null private var contentChild: android.view.View? = null
private var previousUsableHeight = 0 private var previousUsableHeight = 0
private var attachLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null private var attachLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
private var mediaWebView: WebView? = null
private var pendingScreenAudio: Boolean? = null
private val screenCaptureLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult(),
) { result ->
val includeAudio = pendingScreenAudio
pendingScreenAudio = null
if (includeAudio == null) return@registerForActivityResult
val data = result.data
if (result.resultCode != Activity.RESULT_OK || data == null) {
MobileMediaEvents.emitError("Screen capture permission was denied")
return@registerForActivityResult
}
MediaProjectionService.start(this, result.resultCode, data, includeAudio)
}
private val cameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions(),
) {
emitCameraPermission()
}
private val screenAudioPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) {
launchScreenCaptureIntent()
}
override fun onWebViewCreate(webView: WebView) {
webView.settings.apply {
setSupportZoom(false)
builtInZoomControls = false
displayZoomControls = false
}
var blockingMultiTouch = false
webView.setOnTouchListener { _, event ->
val shouldBlock = blockingMultiTouch || event.pointerCount > 1
when (event.actionMasked) {
MotionEvent.ACTION_POINTER_DOWN -> {
// Cancel the one-finger gesture before consuming the rest of the pinch.
MotionEvent.obtain(event).let { cancelEvent ->
cancelEvent.action = MotionEvent.ACTION_CANCEL
webView.onTouchEvent(cancelEvent)
cancelEvent.recycle()
}
blockingMultiTouch = true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> blockingMultiTouch = false
}
shouldBlock
}
NativeAccessibilityBridge.attach(webView)
mediaWebView = webView
MobileMediaEvents.attach(webView)
webView.addJavascriptInterface(MobileMediaJavascriptInterface(), "tensaminMobileMedia")
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, true) WindowCompat.setDecorFitsSystemWindows(window, true)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
NativeMtpBridge.nativeAttach(applicationContext)
if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) {
NativeMtpBridge.startService(this)
}
NativeAccessibilityBridge.nativeAttach(applicationContext)
installKeyboardResizeWorkaround() installKeyboardResizeWorkaround()
} }
override fun onResume() {
super.onResume()
NativeMtpBridge.nativeSetUiState(true)
}
override fun onPause() {
NativeMtpBridge.nativeSetUiState(false)
super.onPause()
}
override fun onDestroy() { override fun onDestroy() {
attachLayoutListener?.let { listener -> attachLayoutListener?.let { listener ->
contentRoot?.viewTreeObserver?.removeOnGlobalLayoutListener(listener) contentRoot?.viewTreeObserver?.removeOnGlobalLayoutListener(listener)
@ -30,9 +116,87 @@ class MainActivity : TauriActivity() {
attachLayoutListener = null attachLayoutListener = null
contentRoot = null contentRoot = null
contentChild = null contentChild = null
mediaWebView?.let {
NativeAccessibilityBridge.detach(it)
it.removeJavascriptInterface("tensaminMobileMedia")
}
mediaWebView = null
MobileMediaEvents.detach()
super.onDestroy() super.onDestroy()
} }
private fun startScreenShare(includeAudio: Boolean) {
runOnUiThread {
if (pendingScreenAudio != null) {
MobileMediaEvents.emitError("Screen capture permission is already pending")
return@runOnUiThread
}
pendingScreenAudio = includeAudio
if (
includeAudio &&
ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) !=
PackageManager.PERMISSION_GRANTED
) {
screenAudioPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
} else {
launchScreenCaptureIntent()
}
}
}
private fun launchScreenCaptureIntent() {
val manager = getSystemService(MediaProjectionManager::class.java)
screenCaptureLauncher.launch(manager.createScreenCaptureIntent())
}
private fun stopScreenShare() {
runOnUiThread {
pendingScreenAudio = null
MediaProjectionService.stop(this)
}
}
private fun requestCameraPermission() {
runOnUiThread {
cameraPermissionLauncher.launch(
arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO),
)
}
}
private fun emitCameraPermission() {
val detail = JSONObject()
.put(
"camera",
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED,
)
.put(
"microphone",
ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED,
)
MobileMediaEvents.emit("tensamin-mobile-camera-permission", detail)
}
private inner class MobileMediaJavascriptInterface {
@JavascriptInterface
fun startScreenShare(includeAudio: Boolean) {
this@MainActivity.startScreenShare(includeAudio)
}
@JavascriptInterface
fun stopScreenShare() {
this@MainActivity.stopScreenShare()
}
@JavascriptInterface
fun requestCameraPermission() {
this@MainActivity.requestCameraPermission()
}
}
private fun installKeyboardResizeWorkaround() { private fun installKeyboardResizeWorkaround() {
val content = window.decorView.findViewById<FrameLayout>(android.R.id.content) val content = window.decorView.findViewById<FrameLayout>(android.R.id.content)
contentRoot = content contentRoot = content
@ -68,16 +232,20 @@ class MainActivity : TauriActivity() {
child: android.view.View, child: android.view.View,
insets: WindowInsetsCompat? = ViewCompat.getRootWindowInsets(child), insets: WindowInsetsCompat? = ViewCompat.getRootWindowInsets(child),
) { ) {
val visibleFrame = Rect()
child.getWindowVisibleDisplayFrame(visibleFrame)
val rootHeight = child.rootView.height val rootHeight = child.rootView.height
if (rootHeight <= 0) return if (rootHeight <= 0) return
val imeHeight = insets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0 val imeVisible = insets?.isVisible(WindowInsetsCompat.Type.ime()) == true
val keyboardHeight = maxOf(imeHeight, rootHeight - visibleFrame.bottom) val imeHeight = if (imeVisible) {
val keyboardVisible = keyboardHeight > rootHeight * 0.15 insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
val usableHeight = if (keyboardVisible) rootHeight - keyboardHeight else ViewGroup.LayoutParams.MATCH_PARENT } else {
0
}
val usableHeight = if (imeHeight in 1 until rootHeight) {
rootHeight - imeHeight
} else {
WindowManager.LayoutParams.MATCH_PARENT
}
if (previousUsableHeight == usableHeight) return if (previousUsableHeight == usableHeight) return

View file

@ -0,0 +1,345 @@
package net.tensamin.client
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.graphics.PixelFormat
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioPlaybackCaptureConfiguration
import android.media.AudioRecord
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.media.ImageReader
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.os.IBinder
import android.util.Base64
import android.util.DisplayMetrics
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import java.io.ByteArrayOutputStream
import java.util.concurrent.atomic.AtomicBoolean
import org.json.JSONObject
class MediaProjectionService : Service() {
private var projection: MediaProjection? = null
private var virtualDisplay: VirtualDisplay? = null
private var imageReader: ImageReader? = null
private var captureThread: HandlerThread? = null
private var audioRecord: AudioRecord? = null
private var audioThread: Thread? = null
private val captureActive = AtomicBoolean(false)
private var lastFrameAt = 0L
private val projectionCallback = object : MediaProjection.Callback() {
override fun onStop() {
stopCapture(stopProjection = false, emitStopped = true)
stopSelf()
}
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == ACTION_STOP) {
stopCapture(stopProjection = true, emitStopped = true)
stopSelf()
return START_NOT_STICKY
}
val permissionData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent?.getParcelableExtra(EXTRA_PERMISSION_DATA, Intent::class.java)
} else {
@Suppress("DEPRECATION")
intent?.getParcelableExtra(EXTRA_PERMISSION_DATA)
}
val resultCode = intent?.getIntExtra(EXTRA_RESULT_CODE, Int.MIN_VALUE) ?: Int.MIN_VALUE
if (permissionData == null || resultCode == Int.MIN_VALUE) {
MobileMediaEvents.emitError("Screen capture permission data is missing")
stopSelf()
return START_NOT_STICKY
}
val includeAudio = intent?.getBooleanExtra(EXTRA_INCLUDE_AUDIO, false) ?: false
try {
startForegroundNotification()
startCapture(resultCode, permissionData, includeAudio)
} catch (error: Throwable) {
stopCapture(stopProjection = true, emitStopped = false)
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
MobileMediaEvents.emitError(error.message ?: "Unable to start screen capture")
}
return START_NOT_STICKY
}
override fun onDestroy() {
stopCapture(stopProjection = true, emitStopped = true)
super.onDestroy()
}
private fun startForegroundNotification() {
val notificationManager = getSystemService(NotificationManager::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationManager.createNotificationChannel(
NotificationChannel(
NOTIFICATION_CHANNEL_ID,
"Screen sharing",
NotificationManager.IMPORTANCE_LOW,
),
)
}
val stopIntent = Intent(this, MediaProjectionService::class.java).setAction(ACTION_STOP)
val stopPendingIntent = PendingIntent.getService(
this,
0,
stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_menu_share)
.setContentTitle("Tensamin is sharing your screen")
.setContentText("Tap Stop to end screen sharing")
.setOngoing(true)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.addAction(android.R.drawable.ic_media_pause, "Stop", stopPendingIntent)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION,
)
} else {
startForeground(NOTIFICATION_ID, notification)
}
}
@Suppress("DEPRECATION")
private fun startCapture(resultCode: Int, permissionData: Intent, includeAudio: Boolean) {
stopCapture(stopProjection = true, emitStopped = false)
val manager = getSystemService(MediaProjectionManager::class.java)
val newProjection = manager.getMediaProjection(resultCode, permissionData)
?: error("Android did not provide a screen capture session")
projection = newProjection
val thread = HandlerThread("tensamin-screen-capture").also { it.start() }
captureThread = thread
val handler = Handler(thread.looper)
newProjection.registerCallback(projectionCallback, handler)
val metrics = DisplayMetrics()
getSystemService(android.view.WindowManager::class.java).defaultDisplay.getRealMetrics(metrics)
val displayWidth = metrics.widthPixels
val displayHeight = metrics.heightPixels
val width = minOf(displayWidth, MAX_FRAME_WIDTH)
val height = (displayHeight.toLong() * width / displayWidth).toInt()
val density = metrics.densityDpi
val reader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2)
imageReader = reader
reader.setOnImageAvailableListener({ source -> captureFrame(source, width, height) }, handler)
virtualDisplay = newProjection.createVirtualDisplay(
"Tensamin screen sharing",
width,
height,
density,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
reader.surface,
null,
handler,
)
captureActive.set(true)
val audioStarted = includeAudio && startAudioCapture(newProjection)
MobileMediaEvents.emit(
"tensamin-mobile-screen-started",
JSONObject().put("includeAudio", audioStarted),
)
}
private fun captureFrame(source: ImageReader, width: Int, height: Int) {
val image = source.acquireLatestImage() ?: return
try {
val now = System.currentTimeMillis()
if (!captureActive.get() || now - lastFrameAt < FRAME_INTERVAL_MS) return
lastFrameAt = now
val plane = image.planes[0]
val paddedWidth = plane.rowStride / plane.pixelStride
val paddedBitmap = Bitmap.createBitmap(paddedWidth, height, Bitmap.Config.ARGB_8888)
paddedBitmap.copyPixelsFromBuffer(plane.buffer)
val croppedBitmap = if (paddedWidth == width) {
paddedBitmap
} else {
Bitmap.createBitmap(paddedBitmap, 0, 0, width, height).also { paddedBitmap.recycle() }
}
val outputBitmap = croppedBitmap
val bytes = ByteArrayOutputStream()
outputBitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, bytes)
val outputWidth = outputBitmap.width
val outputHeight = outputBitmap.height
outputBitmap.recycle()
MobileMediaEvents.emit(
"tensamin-mobile-screen-frame",
JSONObject()
.put("data", Base64.encodeToString(bytes.toByteArray(), Base64.NO_WRAP))
.put("mimeType", "image/jpeg")
.put("width", outputWidth)
.put("height", outputHeight),
)
} catch (error: Throwable) {
if (captureActive.get()) {
MobileMediaEvents.emitError(error.message ?: "Unable to read a screen frame")
}
} finally {
image.close()
}
}
private fun startAudioCapture(activeProjection: MediaProjection): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
MobileMediaEvents.emitError("System audio capture requires Android 10 or newer")
return false
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) !=
PackageManager.PERMISSION_GRANTED
) {
MobileMediaEvents.emitError("Microphone permission is required for system audio capture")
return false
}
return try {
val format = AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(AUDIO_SAMPLE_RATE)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
.build()
val configuration = AudioPlaybackCaptureConfiguration.Builder(activeProjection)
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
.addMatchingUsage(AudioAttributes.USAGE_GAME)
.build()
val minimumBuffer = AudioRecord.getMinBufferSize(
AUDIO_SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
)
check(minimumBuffer > 0) { "Android could not allocate a system audio buffer" }
val bufferSize = maxOf(minimumBuffer * 2, AUDIO_BATCH_BYTES)
val record = AudioRecord.Builder()
.setAudioFormat(format)
.setAudioPlaybackCaptureConfig(configuration)
.setBufferSizeInBytes(bufferSize)
.build()
check(record.state == AudioRecord.STATE_INITIALIZED) {
"Android could not initialize system audio capture"
}
audioRecord = record
record.startRecording()
audioThread = Thread({ readAudio(record) }, "tensamin-audio-capture").also { it.start() }
true
} catch (error: Throwable) {
audioRecord?.release()
audioRecord = null
MobileMediaEvents.emitError(error.message ?: "Unable to capture system audio")
false
}
}
private fun readAudio(record: AudioRecord) {
val buffer = ByteArray(AUDIO_BATCH_BYTES)
while (captureActive.get() && !Thread.currentThread().isInterrupted) {
val read = record.read(buffer, 0, buffer.size, AudioRecord.READ_BLOCKING)
if (read > 0 && captureActive.get()) {
MobileMediaEvents.emit(
"tensamin-mobile-screen-audio",
JSONObject()
.put("data", Base64.encodeToString(buffer, 0, read, Base64.NO_WRAP))
.put("sampleRate", AUDIO_SAMPLE_RATE)
.put("channelCount", 1)
.put("encoding", "pcm16le"),
)
} else if (read < 0 && captureActive.get()) {
MobileMediaEvents.emitError("System audio capture stopped with code $read")
return
}
}
}
@Synchronized
private fun stopCapture(stopProjection: Boolean, emitStopped: Boolean) {
val wasActive = captureActive.getAndSet(false)
val record = audioRecord
audioRecord = null
try {
record?.stop()
} catch (_: IllegalStateException) {
}
record?.release()
audioThread?.interrupt()
audioThread = null
imageReader?.setOnImageAvailableListener(null, null)
virtualDisplay?.release()
virtualDisplay = null
imageReader?.close()
imageReader = null
val oldProjection = projection
projection = null
oldProjection?.unregisterCallback(projectionCallback)
if (stopProjection) oldProjection?.stop()
captureThread?.quitSafely()
captureThread = null
lastFrameAt = 0L
if (wasActive && emitStopped) {
MobileMediaEvents.emit("tensamin-mobile-screen-stopped")
}
}
companion object {
private const val ACTION_STOP = "net.tensamin.client.STOP_SCREEN_SHARE"
private const val EXTRA_RESULT_CODE = "resultCode"
private const val EXTRA_PERMISSION_DATA = "permissionData"
private const val EXTRA_INCLUDE_AUDIO = "includeAudio"
private const val NOTIFICATION_CHANNEL_ID = "screen-sharing"
private const val NOTIFICATION_ID = 7314
private const val FRAME_INTERVAL_MS = 75L
private const val MAX_FRAME_WIDTH = 1280
private const val JPEG_QUALITY = 72
private const val AUDIO_SAMPLE_RATE = 48_000
private const val AUDIO_BATCH_BYTES = 9_600
fun start(context: Context, resultCode: Int, data: Intent, includeAudio: Boolean) {
val intent = Intent(context, MediaProjectionService::class.java)
.putExtra(EXTRA_RESULT_CODE, resultCode)
.putExtra(EXTRA_PERMISSION_DATA, data)
.putExtra(EXTRA_INCLUDE_AUDIO, includeAudio)
ContextCompat.startForegroundService(context, intent)
}
fun stop(context: Context) {
context.startService(
Intent(context, MediaProjectionService::class.java).setAction(ACTION_STOP),
)
}
}
}

View file

@ -0,0 +1,29 @@
package net.tensamin.client
import android.webkit.WebView
import java.lang.ref.WeakReference
import org.json.JSONObject
object MobileMediaEvents {
private var webView = WeakReference<WebView>(null)
fun attach(value: WebView) {
webView = WeakReference(value)
}
fun detach() {
webView.clear()
}
fun emitError(message: String) {
emit("tensamin-mobile-screen-error", JSONObject().put("message", message))
}
fun emit(name: String, detail: JSONObject = JSONObject()) {
val view = webView.get() ?: return
val script = "window.dispatchEvent(new CustomEvent(" +
JSONObject.quote(name) +
", { detail: " + detail.toString() + " }));"
view.post { view.evaluateJavascript(script, null) }
}
}

View file

@ -0,0 +1,17 @@
package net.tensamin.client
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class MtpBootReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (
intent.action == Intent.ACTION_BOOT_COMPLETED &&
MtpSecureStore.isEnabled(context) &&
MtpSecureStore.hasConfig(context)
) {
NativeMtpBridge.startService(context)
}
}
}

View file

@ -0,0 +1,167 @@
package net.tensamin.client
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Build
import android.os.IBinder
import android.os.Process
import android.os.SystemClock
import androidx.core.app.NotificationCompat
class MtpForegroundService : Service() {
private var started = false
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) = refreshNotification()
override fun onLost(network: Network) = refreshNotification()
override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) =
refreshNotification()
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
getSystemService(ConnectivityManager::class.java)
.registerDefaultNetworkCallback(networkCallback)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == ACTION_STOP) {
MtpSecureStore.setEnabled(this, false)
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
return START_NOT_STICKY
}
if (started) return START_STICKY
createChannel(this)
connectionStatus = "Connecting"
val notification = buildNotification(this, displayedStatus(this))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE,
)
} else {
startForeground(NOTIFICATION_ID, notification)
}
val config = MtpSecureStore.loadConfig(this)
if (config == null || !MtpSecureStore.isEnabled(this)) {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
return START_NOT_STICKY
}
try {
NativeMtpBridge.nativeAttach(applicationContext)
NativeMtpBridge.nativeStart(config)
started = true
NativeMtpBridge.log(2, "Started MTP foreground service")
} catch (error: Throwable) {
NativeMtpBridge.log(0, "Failed to start MTP foreground service", error)
updateNotification(this, "Connection failed")
}
return START_STICKY
}
override fun onTaskRemoved(rootIntent: Intent?) {
val preferences = getSharedPreferences(SERVICE_PREFERENCES, Context.MODE_PRIVATE)
val now = SystemClock.elapsedRealtime()
if (now - preferences.getLong(LAST_TASK_RESTART, 0) < TASK_RESTART_COOLDOWN_MS) {
super.onTaskRemoved(rootIntent)
return
}
preferences.edit().putLong(LAST_TASK_RESTART, now).commit()
if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) {
startService(Intent(this, MtpForegroundService::class.java))
}
super.onTaskRemoved(rootIntent)
// Tauri cannot recreate its WebView after the UI task is removed while this process survives.
Process.killProcess(Process.myPid())
}
override fun onDestroy() {
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
if (!MtpSecureStore.isEnabled(this)) NativeMtpBridge.nativeStop()
super.onDestroy()
}
private fun refreshNotification() {
updateNotification(this, connectionStatus)
}
companion object {
private const val CHANNEL_ID = "tensamin-connection"
private const val NOTIFICATION_ID = 2201
private const val ACTION_STOP = "net.tensamin.client.STOP_MTP"
private const val SERVICE_PREFERENCES = "tensamin-service"
private const val LAST_TASK_RESTART = "last-task-restart"
private const val TASK_RESTART_COOLDOWN_MS = 15_000L
@Volatile private var connectionStatus = "Connecting"
fun updateNotification(context: Context, status: String) {
if (!MtpSecureStore.isEnabled(context)) return
connectionStatus = status
createChannel(context)
context.getSystemService(NotificationManager::class.java)
.notify(NOTIFICATION_ID, buildNotification(context, displayedStatus(context)))
}
private fun displayedStatus(context: Context): String {
val connectivity = context.getSystemService(ConnectivityManager::class.java)
val network = connectivity.activeNetwork ?: return "No network"
val capabilities = connectivity.getNetworkCapabilities(network) ?: return "No network"
return if (
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
) connectionStatus else "No network"
}
private fun createChannel(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
context.getSystemService(NotificationManager::class.java).createNotificationChannel(
NotificationChannel(
CHANNEL_ID,
"Background connection",
NotificationManager.IMPORTANCE_LOW,
).apply { description = "Keeps Tensamin connected for incoming messages" },
)
}
private fun buildNotification(context: Context, status: String): Notification {
val openIntent = PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val stopIntent = PendingIntent.getService(
context,
1,
Intent(context, MtpForegroundService::class.java).setAction(ACTION_STOP),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
return NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_notify_sync)
.setContentTitle("Tensamin")
.setContentText(status)
.setContentIntent(openIntent)
.setOngoing(true)
.setCategory(Notification.CATEGORY_SERVICE)
.setPriority(NotificationCompat.PRIORITY_LOW)
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopIntent)
.build()
}
}
}

View file

@ -0,0 +1,74 @@
package net.tensamin.client
import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
object MtpSecureStore {
private const val KEY_ALIAS = "tensamin-mtp-config"
private const val PREFS = "tensamin-mtp"
private const val CONFIG = "config"
private const val ENABLED = "enabled"
fun saveConfig(context: Context, config: String) {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
val encrypted = cipher.doFinal(config.toByteArray(Charsets.UTF_8))
val payload = Base64.encodeToString(cipher.iv + encrypted, Base64.NO_WRAP)
preferences(context).edit().putString(CONFIG, payload).apply()
}
fun loadConfig(context: Context): String? {
val payload = preferences(context).getString(CONFIG, null) ?: return null
return runCatching {
val bytes = Base64.decode(payload, Base64.NO_WRAP)
require(bytes.size > 12)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(
Cipher.DECRYPT_MODE,
getOrCreateKey(),
GCMParameterSpec(128, bytes.copyOfRange(0, 12)),
)
String(cipher.doFinal(bytes.copyOfRange(12, bytes.size)), Charsets.UTF_8)
}.getOrNull()
}
fun hasConfig(context: Context): Boolean = loadConfig(context) != null
fun setEnabled(context: Context, enabled: Boolean) {
preferences(context).edit().putBoolean(ENABLED, enabled).apply()
}
fun isEnabled(context: Context): Boolean =
preferences(context).getBoolean(ENABLED, false)
private fun preferences(context: Context) =
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
private fun getOrCreateKey(): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
(keyStore.getKey(KEY_ALIAS, null) as? SecretKey)?.let { return it }
val generator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore",
)
generator.init(
KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build(),
)
return generator.generateKey()
}
}

View file

@ -0,0 +1,51 @@
package net.tensamin.client
import android.content.Context
import android.webkit.WebView
import androidx.annotation.Keep
import java.lang.ref.WeakReference
@Keep
object NativeAccessibilityBridge {
const val DEFAULT_INITIAL_SCALE = 290
private const val MIN_INITIAL_SCALE = 210
private const val MAX_INITIAL_SCALE = 500
private const val PREFERENCES = "tensamin-accessibility"
private const val INITIAL_SCALE = "initial-scale"
private var webView = WeakReference<WebView>(null)
init {
System.loadLibrary("mobile_lib")
}
@JvmStatic external fun nativeAttach(context: Context)
fun attach(webView: WebView) {
this.webView = WeakReference(webView)
webView.setInitialScale(getInitialScale(webView.context))
}
fun detach(webView: WebView) {
if (this.webView.get() === webView) this.webView.clear()
}
fun getInitialScale(context: Context): Int {
val preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
val storedScale = preferences.getInt(INITIAL_SCALE, DEFAULT_INITIAL_SCALE)
val scale = storedScale.coerceIn(MIN_INITIAL_SCALE, MAX_INITIAL_SCALE)
if (scale != storedScale) preferences.edit().putInt(INITIAL_SCALE, scale).apply()
return scale
}
fun setInitialScale(context: Context, initialScale: Int) {
val nextScale = initialScale.coerceIn(MIN_INITIAL_SCALE, MAX_INITIAL_SCALE)
context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE)
.edit()
.putInt(INITIAL_SCALE, nextScale)
.apply()
webView.get()?.let { currentWebView ->
currentWebView.post { currentWebView.setInitialScale(nextScale) }
}
}
}

View file

@ -0,0 +1,161 @@
package net.tensamin.client
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import android.util.Log
import androidx.annotation.Keep
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import androidx.core.content.LocusIdCompat
import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
@Keep
object NativeMtpBridge {
private const val MESSAGE_CHANNEL = "tensamin-messages"
init {
System.loadLibrary("mobile_lib")
}
@JvmStatic external fun nativeAttach(context: Context)
@JvmStatic external fun nativeStart(config: String)
@JvmStatic external fun nativeStop()
@JvmStatic external fun nativeSetUiState(visible: Boolean)
@JvmStatic external fun nativeLog(level: Int, message: String, details: String)
fun log(level: Int, message: String, error: Throwable? = null) {
val details = error?.stackTraceToString().orEmpty()
Log.println(if (level == 0) Log.ERROR else Log.INFO, "TensaminAndroid", "$message $details")
nativeLog(level, message, details)
}
fun storeConfig(context: Context, config: String) {
try {
MtpSecureStore.saveConfig(context, config)
if (MtpSecureStore.isEnabled(context)) startService(context)
log(2, "Stored native MTP credentials")
} catch (error: Throwable) {
log(0, "Failed to store native MTP credentials", error)
throw error
}
}
fun hasConfig(context: Context): Boolean = MtpSecureStore.hasConfig(context)
fun setServiceEnabled(context: Context, enabled: Boolean) {
MtpSecureStore.setEnabled(context, enabled)
if (enabled && MtpSecureStore.hasConfig(context)) startService(context) else stopService(context)
}
fun isIgnoringBatteryOptimizations(context: Context): Boolean =
context.getSystemService(PowerManager::class.java)
.isIgnoringBatteryOptimizations(context.packageName)
fun requestBatteryExemption(context: Context) {
val intent = Intent(
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
Uri.parse("package:${context.packageName}"),
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
fun startService(context: Context) {
ContextCompat.startForegroundService(
context,
Intent(context, MtpForegroundService::class.java),
)
}
fun stopService(context: Context) {
if (!context.stopService(Intent(context, MtpForegroundService::class.java))) nativeStop()
}
fun updateServiceStatus(context: Context, status: String) {
MtpForegroundService.updateNotification(context, status)
}
fun postMessageNotification(
context: Context,
senderId: Long,
sender: String,
body: String,
avatar: ByteArray,
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.getSystemService(NotificationManager::class.java).createNotificationChannel(
NotificationChannel(
MESSAGE_CHANNEL,
"Messages",
NotificationManager.IMPORTANCE_HIGH,
).apply { description = "Incoming Tensamin messages" },
)
}
val openIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse("tensamin://chat?id=$senderId"),
context,
MainActivity::class.java,
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
val pendingIntent = PendingIntent.getActivity(
context,
senderId.hashCode(),
openIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val avatarBitmap = avatar.takeIf { it.isNotEmpty() }?.let {
BitmapFactory.decodeByteArray(it, 0, it.size)
}
val avatarIcon = avatarBitmap?.let(IconCompat::createWithAdaptiveBitmap)
val person = Person.Builder()
.setName(sender)
.setKey(senderId.toString())
.setIcon(avatarIcon)
.build()
val shortcutId = "chat-$senderId"
val shortcut = ShortcutInfoCompat.Builder(context, shortcutId)
.setShortLabel(sender)
.setLongLived(true)
.setPerson(person)
.setIntent(openIntent)
.apply { if (avatarIcon != null) setIcon(avatarIcon) }
.build()
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
val style = NotificationCompat.MessagingStyle(
Person.Builder().setName("You").build(),
).addMessage(body, System.currentTimeMillis(), person)
val notification = NotificationCompat.Builder(context, MESSAGE_CHANNEL)
.setSmallIcon(R.drawable.ic_notification_small)
.setContentTitle(sender)
.setContentText(body)
.setStyle(style)
.setShortcutId(shortcutId)
.setLocusId(LocusIdCompat(shortcutId))
.setLargeIcon(avatarBitmap)
.setCategory(Notification.CATEGORY_MESSAGE)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build()
context.getSystemService(NotificationManager::class.java)
.notify(senderId.hashCode(), notification)
}
fun cancelMessageNotification(context: Context, senderId: Long) {
context.getSystemService(NotificationManager::class.java)
.cancel(senderId.hashCode())
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

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

View file

@ -1,4 +1,4 @@
import com.android.build.gradle.LibraryExtension import com.android.build.api.dsl.LibraryExtension
buildscript { buildscript {
repositories { repositories {
@ -7,7 +7,7 @@ buildscript {
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:8.11.0") classpath("com.android.tools.build:gradle:8.11.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20")
} }
} }

View file

@ -20,4 +20,3 @@ dependencies {
compileOnly(gradleApi()) compileOnly(gradleApi())
implementation("com.android.tools.build:gradle:8.11.0") implementation("com.android.tools.build:gradle:8.11.0")
} }

View file

@ -5,8 +5,12 @@ import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import org.gradle.process.ExecOperations
import javax.inject.Inject
open class BuildTask : DefaultTask() { open class BuildTask @Inject constructor(
private val execOperations: ExecOperations,
) : DefaultTask() {
@Input @Input
var rootDirRel: String? = null var rootDirRel: String? = null
@Input @Input
@ -50,7 +54,7 @@ open class BuildTask : DefaultTask() {
val release = release ?: throw GradleException("release cannot be null") val release = release ?: throw GradleException("release cannot be null")
val args = listOf("tauri", "android", "android-studio-script"); val args = listOf("tauri", "android", "android-studio-script");
project.exec { execOperations.exec {
workingDir(File(project.projectDir, rootDirRel)) workingDir(File(project.projectDir, rootDirRel))
executable(executable) executable(executable)
args(args) args(args)
@ -65,4 +69,4 @@ open class BuildTask : DefaultTask() {
args(listOf("--target", target)) args(listOf("--target", target))
}.assertNormalExitValue() }.assertNormalExitValue()
} }
} }

View file

@ -1,6 +1,6 @@
#Tue May 10 19:22:52 CST 2022 #Tue May 10 19:22:52 CST 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

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

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