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
0d711ea55c (feat): bump version
All checks were successful
/ build-web (push) Successful in 1m21s
/ build-desktop (linux) (push) Successful in 5m37s
/ build-mobile (push) Successful in 11m48s
/ release (push) Successful in 27s
2026-05-25 20:19:35 +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
2440fedaad (feat): add auto update for dev builds on dev builds
All checks were successful
/ build-web (push) Successful in 1m17s
/ build-desktop (linux) (push) Successful in 5m42s
/ build-mobile (push) Successful in 11m33s
/ release (push) Successful in 29s
2026-05-25 14:05:20 +02:00
e146ae2710 (fix): rpm builds in workflow
All checks were successful
/ build-web (push) Successful in 1m25s
/ build-desktop (linux) (push) Successful in 5m45s
/ build-mobile (push) Successful in 11m39s
/ release (push) Successful in 27s
2026-05-25 13:20:50 +02:00
a209ade10b (feat): migrate experimental tauri cef to electron
Some checks failed
/ build-web (push) Successful in 1m19s
/ build-desktop (linux) (push) Failing after 2m33s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
(feat): add flake to expose tensamin desktop package
(qol): update todo
2026-05-25 13:16:56 +02:00
be80e2f387 (fix): actually fix outdated tag recognition
All checks were successful
/ build-web (push) Successful in 1m13s
/ build-desktop (push) Successful in 12m1s
/ build-mobile (push) Successful in 16m47s
/ release (push) Successful in 22s
2026-05-25 00:58:49 +02:00
15138a30da (fix): outdated tag recognition
All checks were successful
/ build-web (push) Successful in 1m12s
/ build-desktop (push) Successful in 11m50s
/ build-mobile (push) Successful in 16m13s
/ release (push) Successful in 26s
2026-05-25 00:05:48 +02:00
37338f12fb (feat): rename productName to have a uppercase T
Some checks failed
/ build-web (push) Successful in 1m23s
/ release (push) Has been cancelled
/ build-desktop (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-05-25 00:00:33 +02:00
edfb037864 (fix): lowercase name on android
Some checks failed
/ build-desktop (push) Has been cancelled
/ release (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-05-25 00:00:10 +02:00
d16f260a3a (fix): small desktop app bugs
Some checks failed
/ build-web (push) Successful in 1m12s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (push) Has been cancelled
2026-05-24 23:52:33 +02:00
b0cdf0adf2 (fix): actually fix android icons
All checks were successful
/ build-web (push) Successful in 1m11s
/ build-desktop (push) Successful in 11m40s
/ build-mobile (push) Successful in 16m14s
/ release (push) Successful in 28s
2026-05-24 22:24:38 +02:00
0c933e4890 (fix): messages at the top overlapping
All checks were successful
/ build-web (push) Successful in 1m17s
/ build-desktop (push) Successful in 12m24s
/ build-mobile (push) Successful in 17m15s
/ release (push) Successful in 23s
2026-05-24 21:30:42 +02:00
78f354d0d3 (fix): android icons
All checks were successful
/ build-web (push) Successful in 1m22s
/ build-desktop (push) Successful in 12m30s
/ build-mobile (push) Successful in 17m20s
/ release (push) Successful in 24s
2026-05-24 20:50:28 +02:00
d297282320 (fix): chat message measurement
Some checks failed
/ build-web (push) Successful in 1m16s
/ build-desktop (push) Successful in 12m24s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
(qol): update todos
2026-05-24 20:37:57 +02:00
7ea3b32286 (feat): add native notifications
All checks were successful
/ build-web (push) Successful in 1m9s
/ build-desktop (push) Successful in 11m16s
/ build-mobile (push) Successful in 15m50s
/ release (push) Successful in 23s
(feat): add navbar profile popover
(fix): chat input box click area to slim
(qol): format
2026-05-24 18:31:27 +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
67b7926dab (feat): add message states, tweaks chat ui
All checks were successful
/ build-web (push) Successful in 1m10s
/ build-desktop (push) Successful in 11m22s
/ build-mobile (push) Successful in 15m52s
/ release (push) Successful in 23s
(feat): add drag and drop to login page
2026-05-24 14:09:57 +02:00
79beba44a4 (feat): bump version because i am lazy
All checks were successful
/ build-web (push) Successful in 1m14s
/ build-desktop (push) Successful in 11m19s
/ build-mobile (push) Successful in 15m45s
/ release (push) Successful in 23s
2026-05-24 02:18:37 +02:00
809cbb5b5e (feat): update logo
Some checks failed
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 11m16s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-05-24 02:06:32 +02:00
a0e3e85467 (fix): call being getting initialised even on the login screen
All checks were successful
/ build-web (push) Successful in 1m13s
/ build-desktop (push) Successful in 11m50s
/ build-mobile (push) Successful in 16m27s
/ release (push) Successful in 24s
2026-05-23 21:43:11 +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
9e3a6040e3 (fix): add dedicated error_no_iota screen and cancel identification if it occurs
All checks were successful
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 10m54s
/ build-mobile (push) Successful in 15m23s
/ release (push) Successful in 22s
2026-05-23 17:30:55 +02:00
ebd2a364a7 (feat): update android version stuff to better fit obtainium's needs
All checks were successful
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 10m57s
/ build-mobile (push) Successful in 15m24s
/ release (push) Successful in 22s
2026-05-23 17:11:16 +02:00
167036533c (fix): ttp_url missing in qrcode
All checks were successful
/ build-web (push) Successful in 1m7s
/ build-desktop (push) Successful in 11m6s
/ build-mobile (push) Successful in 15m35s
/ release (push) Successful in 23s
2026-05-22 20:34:05 +02:00
fd18ee6079 (feat): remove checks workflow
All checks were successful
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 11m13s
/ build-mobile (push) Successful in 15m42s
/ release (push) Successful in 23s
2026-05-22 14:38:22 +02:00
6f5f88573a (fix): remove the if: from the workflows
Some checks failed
Checks / build-web (pull_request) Has been cancelled
Checks / build-mobile (pull_request) Has been cancelled
Checks / build-desktop (pull_request) Has been cancelled
/ build-desktop (push) Has been cancelled
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
2026-05-22 14:26:21 +02:00
370c3746f7 Added check workflow
Some checks failed
/ build-desktop (push) Has been cancelled
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
Checks / build-web (pull_request) Successful in 1m5s
Checks / build-desktop (pull_request) Has been cancelled
Checks / build-mobile (pull_request) Has been cancelled
2026-05-22 14:23:25 +02:00
aa811f4277 (fix): workflows running on pull requests
Some checks failed
/ build-web (push) Successful in 1m9s
/ release (push) Has been cancelled
/ build-desktop (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-05-22 14:16:57 +02:00
112c9d58da (feat): improved chat ui
All checks were successful
/ build-web (push) Successful in 3m3s
/ build-desktop (push) Successful in 19m37s
/ build-mobile (push) Successful in 27m22s
/ release (push) Successful in 24s
2026-05-21 21:33:34 +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
ba59a49f98 (feat): improved conversation adding flow
All checks were successful
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 11m11s
/ build-mobile (push) Successful in 15m40s
/ release (push) Successful in 21s
(feat): visual tweaks for user modal
(feat): now using full width for profile settings page on mobile
(fix): profile not saving with empty avatar
(chore): bump version due to prod release not getting created
(chore): update licenses
2026-05-21 12:15:37 +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
4380d664be (chore): update ttp
All checks were successful
/ build-web (push) Successful in 1m7s
/ build-desktop (push) Successful in 17m48s
/ build-mobile (push) Successful in 27m37s
/ release (push) Successful in 37s
2026-05-20 22:09:58 +02:00
55da8026a7 (feat): add profile page
All checks were successful
/ build-web (push) Successful in 1m12s
/ build-desktop (push) Successful in 11m51s
/ build-mobile (push) Successful in 16m33s
/ release (push) Successful in 23s
(feat): show user online-status
(feat): make markdown input more modular
2026-05-16 14:41:43 +02:00
168694ae86 (fix): speaking indicator broken if screensharing, audio was autosubscribed to
All checks were successful
/ build-web (push) Successful in 1m14s
/ build-desktop (push) Successful in 12m29s
/ build-mobile (push) Successful in 17m1s
/ release (push) Successful in 23s
2026-05-15 19:32:35 +02:00
c795f691bf (feat): improved microphone gate
All checks were successful
/ build-web (push) Successful in 1m16s
/ build-desktop (push) Successful in 14m2s
/ build-mobile (push) Successful in 19m13s
/ release (push) Successful in 24s
(fix): weird behaviour related to users that are screensharing
2026-05-15 16:19:03 +02:00
e76c579bb4 (feat): add speaking indicator
All checks were successful
/ build-web (push) Successful in 1m19s
/ build-desktop (push) Successful in 13m14s
/ build-mobile (push) Successful in 18m45s
/ release (push) Successful in 25s
(feat): add microphone gate
(qol): update todo
2026-05-15 15:43:53 +02:00
f624dc7375 (fix): wrong portal for sidebarBox
All checks were successful
/ build-web (push) Successful in 1m10s
/ build-desktop (push) Successful in 11m34s
/ build-mobile (push) Successful in 16m10s
/ release (push) Successful in 22s
(qol): update todo
2026-05-14 16:05:05 +02:00
8954e4193e (fix): tooltips from the sidebarBox hidden behind sidebar
All checks were successful
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 11m20s
/ build-mobile (push) Successful in 16m6s
/ release (push) Successful in 22s
2026-05-14 15:48:24 +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
9641c955fe (feat): revert back to docker runners
All checks were successful
/ build-web (push) Successful in 1m7s
/ build-desktop (push) Successful in 11m8s
/ build-mobile (push) Successful in 15m35s
/ release (push) Successful in 28s
2026-05-14 14:35:17 +02:00
faf695d92c (fix): correctly update workflows for self-hosted runner
Some checks failed
/ build-web (push) Failing after 1s
/ build-mobile (push) Failing after 2s
/ build-desktop (push) Failing after 2s
/ release (push) Has been skipped
2026-05-14 14:32:40 +02:00
39ad421516 (fix): incorrect labels
Some checks failed
/ build-web (push) Failing after 1s
/ build-mobile (push) Failing after 2s
/ build-desktop (push) Failing after 2s
/ release (push) Has been skipped
2026-05-14 14:26:08 +02:00
bb177976c6 (feat): update runners to selfhosted
Some checks failed
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (push) Has been cancelled
/ release (push) Has been cancelled
2026-05-14 14:25:01 +02:00
726d094bb5 (fix): workflow mobile & desktop failures
Some checks failed
/ build-web (push) Successful in 40s
/ build-mobile (push) Has been cancelled
/ build-desktop (push) Has been cancelled
/ release (push) Has been cancelled
2026-05-14 14:20:26 +02:00
d810f50aab (feat): update workflow
Some checks failed
/ build-web (push) Successful in 40s
/ build-mobile (push) Failing after 11m24s
/ build-desktop (push) Failing after 6m57s
/ release (push) Has been skipped
2026-05-14 13:50:01 +02:00
52918265ea (fix): workflow
All checks were successful
/ deploy (push) Successful in 14m54s
2026-05-14 13:19:15 +02:00
a1af7ce25f (fix): workflow formatting
Some checks failed
deploy-dev.yml / (fix): workflow formatting (push) Failing after 0s
2026-05-14 13:15:59 +02:00
9f83f1e1b7 (fix): release description formatting
All checks were successful
/ deploy (push) Successful in 18m34s
2026-05-14 12:18:18 +02:00
396 changed files with 40591 additions and 11608 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,56 +1,220 @@
on:
workflow_dispatch:
push:
branches:
- dev
paths-ignore:
- flake.nix
jobs:
deploy:
runs-on: docker
build-web:
runs-on: nixos
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
with:
fetch-depth: 0
- name: Install Packages
run: apt-get update && apt-get install -y sudo curl jq
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v30
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: bun install --frozen-lockfile
run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Copy licenses
run: nix develop .#electron --command pnpm run copy-licenses
- name: Build packages
run: nix develop .#electron --command pnpm run build:packages
- name: Build web
run: nix develop .#electron --command pnpm run build:web
- name: Deploy
run: nix develop .#electron --command rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-dev/
build-mobile:
runs-on: nixos
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
- name: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: nix develop .#tauri --command pnpm install --frozen-lockfile
- name: Copy licenses
run: nix develop .#tauri --command pnpm run copy-licenses
- name: Build packages
run: nix develop .#tauri --command pnpm run build:packages
- name: Setup Android Keystore
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
run: |
bun -e "require('fs').writeFileSync('keystore.jks', Buffer.from(process.env.KEYSTORE_BASE64.replace(/\s+/g, ''), 'base64'))"
bun -e "const content = process.env.KEYSTORE_PROPERTIES.replace(/\\\\n/g, '\n').replace(/\\r/g, '').split('\n').map(l => l.trim()).filter(l => l).join('\n'); require('fs').writeFileSync('keystore.properties', content)"
set -euo pipefail
- name: Build
run: bun run build:apps
if [ -z "$KEYSTORE_BASE64" ]; then
echo "ANDROID_KEYSTORE_BASE64 secret is missing or empty"
exit 1
fi
- name: Install rsync
run: apt-get update && apt-get install -y rsync
if [ -z "$KEYSTORE_PROPERTIES" ]; then
echo "ANDROID_KEYSTORE_PROPERTIES secret is missing or empty"
exit 1
fi
- name: Deploy
run: rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-dev/
printf '%s' "$KEYSTORE_BASE64" \
| tr -d '[:space:]' \
| base64 -d > keystore.jks
printf '%s' "$KEYSTORE_PROPERTIES" \
| sed 's/\\n/\n/g' \
| tr -d '\r' \
| sed 's|^[[:space:]]*storeFile[[:space:]]*=.*|storeFile=keystore.jks|' \
> keystore.properties
grep -q '^[[:space:]]*storeFile[[:space:]]*=' keystore.properties || printf '\nstoreFile=keystore.jks\n' >> keystore.properties
if [ ! -s keystore.jks ]; then
echo "Decoded keystore.jks is missing or empty"
exit 1
fi
if [ ! -s keystore.properties ]; then
echo "Generated keystore.properties is missing or empty"
exit 1
fi
if ! grep -q '^[[:space:]]*keyAlias[[:space:]]*=' keystore.properties; then
echo "keystore.properties is missing keyAlias"
exit 1
fi
if ! grep -Eq '^[[:space:]]*(keyPassword|password)[[:space:]]*=' keystore.properties; then
echo "keystore.properties is missing keyPassword or password"
exit 1
fi
if ! grep -Eq '^[[:space:]]*(storePassword|password)[[:space:]]*=' keystore.properties; then
echo "keystore.properties is missing storePassword or password"
exit 1
fi
- name: Build mobile
run: nix develop .#tauri --command pnpm run build:mobile
- name: Upload mobile artifact
uses: https://data.forgejo.org/actions/upload-artifact@v3
with:
name: mobile-apk
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
build-desktop:
runs-on: nixos
strategy:
matrix:
target: [linux]
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
- name: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Copy licenses
run: nix develop .#electron --command pnpm run copy-licenses
- name: Build packages
run: nix develop .#electron --command pnpm run build:packages
- name: Set Electron dev version
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
VERSION="$(node -p "require('./package.json').version")"
SHORT_SHA="$(git rev-parse --short HEAD)"
DEV_VERSION="$VERSION-dev-$SHORT_SHA"
export DEV_VERSION
node -e '
const fs = require("fs");
const path = "apps/electron/package.json";
const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
pkg.version = process.env.DEV_VERSION;
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
'
EOF
- name: Build Electron desktop
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
cd apps/electron
pnpm run package:raw
EOF
- name: Upload desktop artifacts
uses: https://data.forgejo.org/actions/upload-artifact@v3
with:
name: electron-desktop-${{ matrix.target }}
path: apps/electron/release/
release:
runs-on: nixos
needs: [build-web, build-mobile, build-desktop]
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
with:
fetch-depth: 0
- name: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Download mobile artifact
uses: https://data.forgejo.org/actions/download-artifact@v3
with:
name: mobile-apk
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/
- name: Download desktop artifacts
uses: https://data.forgejo.org/actions/download-artifact@v3
with:
name: electron-desktop-linux
path: apps/electron/release/
- name: Read version and hash
id: version
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
VERSION="$(node -p "require('./package.json').version")"
SHORT_SHA="$(git rev-parse --short HEAD)"
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
echo "short_sha=$SHORT_SHA" >> "$FORGEJO_OUTPUT"
echo "tag=dev-${VERSION}-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
echo "tag=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
echo "title=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
EOF
- name: Copy releases
env:
TENSAMIN_RELEASE_VERSION: ${{ steps.version.outputs.tag }}
TENSAMIN_RELEASE_TAG: ${{ steps.version.outputs.tag }}
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
ASSET_BASE_URL="${{ forgejo.api_url }}"
ASSET_BASE_URL="${ASSET_BASE_URL%/api/v1}/${{ forgejo.repository }}/releases/download/${{ steps.version.outputs.tag }}"
FORGEJO_RELEASE_ASSET_BASE_URL="$ASSET_BASE_URL" pnpm run copy-releases
EOF
- name: Create pre-release and upload files
env:
@ -61,18 +225,49 @@ jobs:
TAG: ${{ steps.version.outputs.tag }}
TITLE: ${{ steps.version.outputs.title }}
run: |
nix develop .#electron --command bash <<'EOF'
set -eu
test -d releases
find releases -type f | grep -q .
LATEST_PROD_TAG="$(git tag --list 'v*' --sort=-v:refname | head -n 1 || true)"
LATEST_PROD_TAG="$(git for-each-ref refs/tags --sort=-creatordate --format='%(refname:short)' | awk '!/-/' | head -n 1 || true)"
if [ -n "$LATEST_PROD_TAG" ]; then
BODY="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s')"
RAW_LOG="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s')"
else
BODY="$(git log --pretty=format:'- %s')"
RAW_LOG="$(git log --pretty=format:'- %s')"
fi
export RAW_LOG LATEST_PROD_TAG API REPO TAG
BODY="$(node -e '
const raw = process.env.RAW_LOG;
const lines = raw.split("\n");
const commits = [];
for (const line of lines) {
const msg = line.replace(/^- /, "");
const parts = msg.split(/(?=\([^)]+\):)/).filter(Boolean).map(s => s.trim()).filter(s => s);
for (const part of parts) {
commits.push(part);
}
}
const priority = { "(feat):": 1, "(fix):": 2, "(qol):": 3 };
commits.sort((a, b) => {
const tagA = a.match(/^(\([^)]+\):)/)?.[1] || "";
const tagB = b.match(/^(\([^)]+\):)/)?.[1] || "";
return (priority[tagA] || 99) - (priority[tagB] || 99);
});
const log = commits.map(c => "- " + c).join("\n");
const latestTag = process.env.LATEST_PROD_TAG;
if (latestTag) {
const serverUrl = process.env.API.replace(/\/api\/v1.*/, "");
const compareUrl = serverUrl + "/" + process.env.REPO + "/compare/" + latestTag + "..." + process.env.TAG;
console.log(log + "\n\n[View changes](" + compareUrl + ")");
} else {
console.log(log);
}
')"
HTTP_STATUS=$(curl -s -w "%{http_code}" -o release_out.json -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG")
if [ "$HTTP_STATUS" = "200" ]; then
@ -99,9 +294,24 @@ jobs:
RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)"
fi
ASSET_BASE_URL="${API%/api/v1}/$REPO/releases/download/$TAG"
export ASSET_BASE_URL
node -e '
const fs = require("fs");
const path = "releases/electron-release-metadata.json";
const metadata = JSON.parse(fs.readFileSync(path, "utf8"));
metadata.version = process.env.TAG;
metadata.tag = process.env.TAG;
for (const artifact of metadata.artifacts || []) {
artifact.url = `${process.env.ASSET_BASE_URL}/${encodeURIComponent(artifact.name)}`;
}
fs.writeFileSync(path, `${JSON.stringify(metadata, null, 2)}\n`);
'
find releases -type f -print0 | while IFS= read -r -d '' file; do
name="$(basename "$file")"
curl -fsS -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$name" \
-H "Authorization: token $TOKEN" \
-F "attachment=@$file"
done
EOF

View file

@ -1,51 +1,213 @@
on:
workflow_dispatch:
push:
branches:
- main
paths-ignore:
- flake.nix
jobs:
deploy:
runs-on: docker
build-web:
runs-on: nixos
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages
run: apt-get update && apt-get install -y sudo curl jq
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v30
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: bun install --frozen-lockfile
run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Copy licenses
run: nix develop .#electron --command pnpm run copy-licenses
- name: Build packages
run: nix develop .#electron --command pnpm run build:packages
- name: Build web
run: nix develop .#electron --command pnpm run build:web
- name: Deploy
run: nix develop .#electron --command rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-prod/
build-mobile:
runs-on: nixos
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
- name: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: nix develop .#tauri --command pnpm install --frozen-lockfile
- name: Copy licenses
run: nix develop .#tauri --command pnpm run copy-licenses
- name: Build packages
run: nix develop .#tauri --command pnpm run build:packages
- name: Setup Android Keystore
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
run: |
bun -e "require('fs').writeFileSync('keystore.jks', Buffer.from(process.env.KEYSTORE_BASE64.replace(/\s+/g, ''), 'base64'))"
bun -e "const content = process.env.KEYSTORE_PROPERTIES.replace(/\\\\n/g, '\n').replace(/\\r/g, '').split('\n').map(l => l.trim()).filter(l => l).join('\n'); require('fs').writeFileSync('keystore.properties', content)"
set -euo pipefail
- name: Build
run: bun run build:apps
if [ -z "$KEYSTORE_BASE64" ]; then
echo "ANDROID_KEYSTORE_BASE64 secret is missing or empty"
exit 1
fi
- name: Install rsync
run: apt-get update && apt-get install -y rsync
if [ -z "$KEYSTORE_PROPERTIES" ]; then
echo "ANDROID_KEYSTORE_PROPERTIES secret is missing or empty"
exit 1
fi
- name: Deploy
run: rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-prod/
printf '%s' "$KEYSTORE_BASE64" \
| tr -d '[:space:]' \
| base64 -d > keystore.jks
printf '%s' "$KEYSTORE_PROPERTIES" \
| sed 's/\\n/\n/g' \
| tr -d '\r' \
| sed 's|^[[:space:]]*storeFile[[:space:]]*=.*|storeFile=keystore.jks|' \
> keystore.properties
grep -q '^[[:space:]]*storeFile[[:space:]]*=' keystore.properties || printf '\nstoreFile=keystore.jks\n' >> keystore.properties
if [ ! -s keystore.jks ]; then
echo "Decoded keystore.jks is missing or empty"
exit 1
fi
if [ ! -s keystore.properties ]; then
echo "Generated keystore.properties is missing or empty"
exit 1
fi
if ! grep -q '^[[:space:]]*keyAlias[[:space:]]*=' keystore.properties; then
echo "keystore.properties is missing keyAlias"
exit 1
fi
if ! grep -Eq '^[[:space:]]*(keyPassword|password)[[:space:]]*=' keystore.properties; then
echo "keystore.properties is missing keyPassword or password"
exit 1
fi
if ! grep -Eq '^[[:space:]]*(storePassword|password)[[:space:]]*=' keystore.properties; then
echo "keystore.properties is missing storePassword or password"
exit 1
fi
- name: Build mobile
run: nix develop .#tauri --command pnpm run build:mobile
- name: Upload mobile artifact
uses: https://data.forgejo.org/actions/upload-artifact@v3
with:
name: mobile-apk
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
build-desktop:
runs-on: nixos
strategy:
matrix:
target: [linux]
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
- name: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Copy licenses
run: nix develop .#electron --command pnpm run copy-licenses
- name: Build packages
run: nix develop .#electron --command pnpm run build:packages
- name: Set Electron prod version
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
VERSION="$(node -p "require('./package.json').version")"
export VERSION
node -e '
const fs = require("fs");
const path = "apps/electron/package.json";
const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
pkg.version = process.env.VERSION;
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
'
EOF
- name: Build Electron desktop
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
cd apps/electron
pnpm run package:raw
EOF
- name: Upload desktop artifacts
uses: https://data.forgejo.org/actions/upload-artifact@v3
with:
name: electron-desktop-${{ matrix.target }}
path: apps/electron/release/
release:
runs-on: nixos
needs: [build-web, build-mobile, build-desktop]
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
- name: Pull git submodules
run: git submodule update --init --recursive
- name: Install dependencies
run: nix develop .#electron --command pnpm install --frozen-lockfile
- name: Download mobile artifact
uses: https://data.forgejo.org/actions/download-artifact@v3
with:
name: mobile-apk
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/
- name: Download desktop artifacts
uses: https://data.forgejo.org/actions/download-artifact@v3
with:
name: electron-desktop-linux
path: apps/electron/release/
- name: Read version
id: version
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
VERSION="$(node -p "require('./package.json').version")"
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
echo "tag=v$VERSION" >> "$FORGEJO_OUTPUT"
echo "tag=$VERSION" >> "$FORGEJO_OUTPUT"
EOF
- name: Copy releases
env:
TENSAMIN_RELEASE_VERSION: ${{ steps.version.outputs.tag }}
TENSAMIN_RELEASE_TAG: ${{ steps.version.outputs.tag }}
run: |
nix develop .#electron --command bash <<'EOF'
set -euo pipefail
ASSET_BASE_URL="${{ forgejo.api_url }}"
ASSET_BASE_URL="${ASSET_BASE_URL%/api/v1}/${{ forgejo.repository }}/releases/download/${{ steps.version.outputs.tag }}"
FORGEJO_RELEASE_ASSET_BASE_URL="$ASSET_BASE_URL" pnpm run copy-releases
EOF
- name: Create release and upload files
env:
@ -55,42 +217,97 @@ jobs:
SHA: ${{ forgejo.sha }}
TAG: ${{ steps.version.outputs.tag }}
run: |
nix develop .#electron --command bash <<'EOF'
set -eu
test -d releases
find releases -type f | grep -q .
COMMIT_MSG="$(git log -1 --pretty=%B)"
COMMIT_MSG="$(git log -1 --pretty=%B | sed 's/$/ /')"
HTTP_STATUS=$(curl -s -w "%{http_code}" -o release_out.json -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG")
if [ "$HTTP_STATUS" = "200" ]; then
echo "Release $TAG already exists."
RELEASE_ID="$(jq -r .id release_out.json)"
else
echo "Creating new release for $TAG"
RELEASE_JSON="$(curl -f -sS -X POST "$API/repos/$REPO/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg name "$TAG" \
--arg body "$COMMIT_MSG" \
--arg target "$SHA" \
'{
tag_name: $tag,
name: $name,
body: $body,
target_commitish: $target,
draft: false,
prerelease: false
}')")"
RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)"
exit 0
fi
echo "Creating new release for $TAG"
RELEASE_JSON="$(curl -f -sS -X POST "$API/repos/$REPO/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg name "$TAG" \
--arg body "$COMMIT_MSG" \
--arg target "$SHA" \
'{
tag_name: $tag,
name: $name,
body: $body,
target_commitish: $target,
draft: false,
prerelease: false
}')")"
RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)"
ASSET_BASE_URL="${API%/api/v1}/$REPO/releases/download/$TAG"
export ASSET_BASE_URL
node -e '
const fs = require("fs");
const path = "releases/electron-release-metadata.json";
const metadata = JSON.parse(fs.readFileSync(path, "utf8"));
metadata.version = process.env.TAG;
metadata.tag = process.env.TAG;
for (const artifact of metadata.artifacts || []) {
artifact.url = `${process.env.ASSET_BASE_URL}/${encodeURIComponent(artifact.name)}`;
}
fs.writeFileSync(path, `${JSON.stringify(metadata, null, 2)}\n`);
'
find releases -type f -print0 | while IFS= read -r -d '' file; do
name="$(basename "$file")"
curl -fsS -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$name" \
-H "Authorization: token $TOKEN" \
-F "attachment=@$file"
done
EOF
- name: Delete dev releases
env:
TOKEN: ${{ forgejo.token }}
API: ${{ forgejo.api_url }}
REPO: ${{ forgejo.repository }}
run: |
nix develop .#electron --command bash <<'EOF'
set -eu
PAGE=1
DELETE_RELEASES=delete-dev-releases.tsv
: > "$DELETE_RELEASES"
while :; do
curl -fsS \
-H "Authorization: token $TOKEN" \
"$API/repos/$REPO/releases?page=$PAGE&limit=50&pre-release=true" \
-o releases.json
COUNT="$(jq 'length' releases.json)"
test "$COUNT" -gt 0 || break
jq -r \
'.[] | select(.prerelease == true) | select(.tag_name | contains("-dev-")) | [.id, .tag_name] | @tsv' releases.json \
>> "$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
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
bun.lock
apps/tauri/src-tauri
apps/electron/release
licenses

View file

@ -2,7 +2,6 @@
"recommendations": [
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer",
"bradlc.vscode-tailwindcss",
"antfu.vite"
"bradlc.vscode-tailwindcss"
]
}

25
LICENSE
View file

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

3
README Normal file
View file

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

6
TODO Normal file
View file

@ -0,0 +1,6 @@
- Add a bunch of tests
- 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

2
apps/electron/.gitignore vendored Normal file
View file

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

105
apps/electron/package.json Normal file
View file

@ -0,0 +1,105 @@
{
"name": "@tensamin/electron",
"private": true,
"version": "0.0.3",
"description": "Tensamin desktop client",
"author": "methanium",
"homepage": "https://git.methanium.net/tensamin/client",
"desktopName": "Tensamin",
"type": "module",
"main": "dist/main/main.js",
"scripts": {
"clean": "rm -rf dist release",
"lint": "eslint src",
"build:web": "cd ../.. && pnpm run build:web",
"build": "tsc -p tsconfig.json && esbuild src/preload/preload.ts --bundle --platform=node --format=cjs --external:electron --outfile=dist/preload/preload.cjs",
"dev:raw": "cd ../.. && pnpm run build:web && cd apps/electron && pnpm run build && electron . --verbose",
"dev": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run dev:raw; else pnpm run dev:raw; fi",
"start:raw": "pnpm run build && electron .",
"start": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run start:raw; else pnpm run start:raw; fi",
"package:raw": "cd ../.. && pnpm run build:web && cd apps/electron && pnpm run build && electron-builder --publish never",
"package": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run package:raw; else pnpm run package:raw; fi",
"package:linux:raw": "cd ../.. && pnpm run build:web && cd apps/electron && pnpm run build && electron-builder --linux --publish never",
"package:linux": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run package:linux:raw; else pnpm run package:linux:raw; fi",
"package:windows:raw": "cd ../.. && pnpm run build:web && cd apps/electron && pnpm run build && electron-builder --win --publish never",
"package:windows": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run package:windows:raw; else pnpm run package:windows:raw; fi",
"checksum": "node scripts/generate-release-metadata.ts",
"generate-signing-key": "node scripts/generate-signing-key.ts",
"validate:raw": "pnpm run build && pnpm run package:linux:raw",
"validate": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run validate:raw; else pnpm run validate:raw; fi"
},
"devDependencies": {
"@types/node": "^26.1.2",
"electron": "^43.3.0",
"electron-builder": "^26.15.3",
"esbuild": "^0.28.1",
"typescript": "~6.0.3"
},
"build": {
"appId": "net.tensamin.client",
"productName": "Tensamin",
"executableName": "tensamin",
"artifactName": "Tensamin-${version}-${os}-${arch}.${ext}",
"icon": "build/icons/icon.png",
"directories": {
"output": "release"
},
"toolsets": {
"appimage": "1.0.3"
},
"files": [
"dist/**/*",
"package.json"
],
"extraResources": [
{
"from": "../web/dist",
"to": "web"
},
{
"from": "build/icons",
"to": "icons",
"filter": [
"32x32.png",
"icon.png"
]
}
],
"linux": {
"target": [
"AppImage",
"deb",
"rpm"
],
"icon": "build/icons",
"executableName": "tensamin",
"category": "Network",
"maintainer": "Methanium",
"syncDesktopName": true,
"desktop": {
"entry": {
"Name": "Tensamin",
"StartupWMClass": "Tensamin"
}
}
},
"win": {
"target": [
"nsis",
"portable"
],
"icon": "build/icons/icon.ico"
},
"mac": {
"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
}
}

View file

@ -0,0 +1,71 @@
import { createHash } from "node:crypto";
import {
createReadStream,
existsSync,
readdirSync,
statSync,
writeFileSync,
} from "node:fs";
import { basename, join } from "node:path";
import rootPackage from "../../../package.json" with { type: "json" };
const releaseDir = join(import.meta.dir, "..", "release");
const outDir = join(import.meta.dir, "..", "..", "..", "releases");
function sha256(filePath: string) {
const hash = createHash("sha256");
const stream = createReadStream(filePath);
return new Promise<string>((resolve, reject) => {
stream.on("data", (chunk) => hash.update(chunk));
stream.on("error", reject);
stream.on("end", () => resolve(hash.digest("hex")));
});
}
function platformFor(file: string) {
if (/win|nsis|portable|\.exe$/i.test(file)) return "windows";
if (/mac|darwin|\.dmg$/i.test(file)) return "macos";
return "linux";
}
function archFor(file: string) {
if (/arm64|aarch64/i.test(file)) return "arm64";
return "x64";
}
if (!existsSync(releaseDir)) {
throw new Error(`Missing Electron release directory: ${releaseDir}`);
}
const files = readdirSync(releaseDir)
.filter((file) => !file.endsWith(".blockmap") && !file.endsWith(".yml"))
.map((file) => join(releaseDir, file))
.filter((file) => statSync(file).isFile());
const artifacts = await Promise.all(
files.map(async (filePath) => ({
name: basename(filePath),
platform: platformFor(filePath),
arch: archFor(filePath),
url: `__FORGEJO_RELEASE_ASSET_URL__/${encodeURIComponent(basename(filePath))}`,
sha256: await sha256(filePath),
size: statSync(filePath).size,
})),
);
const metadata = {
version: rootPackage.version,
tag: rootPackage.version,
publishedAt: new Date().toISOString(),
artifacts,
};
writeFileSync(
join(outDir, "electron-release-metadata.json"),
`${JSON.stringify(metadata, null, 2)}\n`,
);
writeFileSync(
join(outDir, "SHA256SUMS"),
`${artifacts.map((artifact) => `${artifact.sha256} ${artifact.name}`).join("\n")}\n`,
);

View file

@ -0,0 +1,11 @@
import { generateKeyPairSync } from "node:crypto";
const { privateKey, publicKey } = generateKeyPairSync("ed25519", {
privateKeyEncoding: { type: "pkcs8", format: "pem" },
publicKeyEncoding: { type: "spki", format: "pem" },
});
console.log("TENSAMIN_UPDATE_PRIVATE_KEY_PEM=");
console.log(privateKey.trim());
console.log("\nTENSAMIN_UPDATE_PUBLIC_KEY_PEM=");
console.log(publicKey.trim());

View file

@ -0,0 +1,511 @@
import { execFile } from "node:child_process";
import { fileURLToPath } from "node:url";
import { dirname, join, resolve } from "node:path";
import {
app,
BrowserWindow,
desktopCapturer,
globalShortcut,
ipcMain,
session,
shell,
} from "electron";
import { checkForUpdates } from "./updates.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 verbose = process.argv.includes("--verbose");
let mainWindow: BrowserWindow | 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) {
app.commandLine.appendSwitch("enable-logging", "stderr");
app.commandLine.appendSwitch("v", "1");
app.commandLine.appendSwitch("log-level", "0");
}
if (
process.platform === "linux" &&
!app.commandLine.hasSwitch("password-store")
) {
app.commandLine.appendSwitch("password-store", "gnome-libsecret");
}
if (
process.platform === "linux" &&
process.env.XDG_SESSION_TYPE === "wayland"
) {
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");
}
function verboseLog(...args: unknown[]) {
if (verbose) {
console.log("[tensamin:electron]", ...args);
}
}
function getRendererIndex() {
if (!app.isPackaged) {
return resolve(__dirname, "../../../web/dist/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"] {
if (process.platform === "linux") return "linux";
if (process.platform === "darwin") return "macos";
if (process.platform === "win32") return "windows";
return "other";
}
function getScreenShareCapabilities(): DesktopScreenShareCapabilities {
const platform = getPlatform();
return {
runtime: "electron",
platform,
showAudioOutputSelector: platform === "linux",
showAudioSwitch: platform === "windows" || platform === "macos",
hasReliableSystemAudio: platform === "windows",
};
}
function execJson(command: string, args: string[]) {
verboseLog("exec", command, args.join(" "));
return new Promise<unknown>((resolvePromise, reject) => {
execFile(command, args, { timeout: 3000 }, (error, stdout, stderr) => {
if (error) {
reject(new Error(stderr.trim() || error.message));
return;
}
resolvePromise(JSON.parse(stdout));
});
});
}
async function listAudioOutputs(): Promise<DesktopScreenShareAudioOutput[]> {
verboseLog("listAudioOutputs", { platform: process.platform });
if (process.platform !== "linux") return [];
const sinks = await execJson("pactl", ["--format=json", "list", "sinks"]);
if (!Array.isArray(sinks)) return [];
return sinks
.map((sink) => {
if (!sink || typeof sink !== "object") return null;
const record = sink as Record<string, unknown>;
const id = record.index == null ? undefined : String(record.index);
const name =
typeof record.description === "string" ? record.description : id;
if (!id || !name) return null;
return { id, name, isDefault: false };
})
.filter(
(output): output is DesktopScreenShareAudioOutput => output != null,
);
}
async function listScreenShareSources() {
verboseLog("listScreenShareSources");
const sources = await desktopCapturer.getSources({
types: ["screen", "window"],
thumbnailSize: { width: 320, height: 180 },
fetchWindowIcons: true,
});
return sources.map((source) => ({
id: source.id,
kind: source.id.startsWith("screen:") ? "screen" : "window",
name: source.name,
subtitle: source.id,
thumbnail: source.thumbnail.isEmpty() ? null : source.thumbnail.toDataURL(),
}));
}
function registerDisplayMediaHandler() {
session.defaultSession.setDisplayMediaRequestHandler(
async (_request, callback) => {
verboseLog("display media request", { selectedScreenShareSourceId });
const sources = await desktopCapturer.getSources({
types: ["screen", "window"],
thumbnailSize: { width: 0, height: 0 },
});
const selected = sources.find(
(source) => source.id === selectedScreenShareSourceId,
);
selectedScreenShareSourceId = null;
const video = selected ?? sources[0];
if (!video) {
callback({});
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;
}
};
session.defaultSession.setPermissionCheckHandler(
(_webContents, permission, requestingOrigin) =>
permission === "media" && isTrustedRenderer(requestingOrigin),
);
session.defaultSession.setPermissionRequestHandler(
(_webContents, permission, callback, details) => {
callback(
permission === "media" && isTrustedRenderer(details.requestingUrl),
);
},
);
}
function registerIpc() {
verboseLog("registering ipc handlers");
ipcMain.handle(ipcChannels.listScreenShareSources, listScreenShareSources);
ipcMain.handle(ipcChannels.listScreenShareAudioOutputs, listAudioOutputs);
ipcMain.handle(
ipcChannels.getScreenShareCapabilities,
getScreenShareCapabilities,
);
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;
verboseLog("selected screen share source", sourceId);
return true;
},
);
ipcMain.handle(ipcChannels.getVersion, () => app.getVersion());
ipcMain.handle(ipcChannels.checkForUpdates, checkForUpdates);
ipcMain.handle(ipcChannels.getSecureStorageStatus, getSecureStorageStatus);
ipcMain.handle(ipcChannels.loadSecureStorage, (_event, key: unknown) =>
loadSecureStorage(key),
);
ipcMain.handle(
ipcChannels.saveSecureStorage,
(_event, key: unknown, value: unknown) => saveSecureStorage(key, value),
);
ipcMain.handle(ipcChannels.deleteSecureStorage, (_event, key: unknown) =>
deleteSecureStorage(key),
);
ipcMain.handle(ipcChannels.clearSecureStorage, clearSecureStorage);
ipcMain.handle(
ipcChannels.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, () => {
verboseLog("window:minimize");
mainWindow?.minimize();
});
ipcMain.handle(ipcChannels.maximizeWindow, () => {
verboseLog("window:maximize");
if (!mainWindow) return;
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
return;
}
mainWindow.maximize();
});
ipcMain.handle(ipcChannels.closeWindow, () => {
verboseLog("window:close");
mainWindow?.close();
});
}
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() {
const rendererIndex = getRendererIndex();
verboseLog("creating main window", {
appVersion: app.getVersion(),
electronVersion: process.versions.electron,
chromeVersion: process.versions.chrome,
nodeVersion: process.versions.node,
platform: process.platform,
arch: process.arch,
isPackaged: app.isPackaged,
rendererIndex,
argv: process.argv,
});
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 900,
minHeight: 600,
title: "Tensamin",
icon: getWindowIcon(),
frame: false,
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, "../preload/preload.cjs"),
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
},
});
mainWindow.setMenuBarVisibility(false);
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
verboseLog("blocked window open", url);
void shell.openExternal(url);
return { action: "deny" };
});
if (verbose) {
mainWindow.webContents.on(
"console-message",
(_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) => {
console.error("[tensamin:electron] renderer failed to load", {
errorCode,
errorDescription,
validatedURL,
});
},
);
mainWindow.webContents.on("did-finish-load", () => {
verboseLog("renderer finished loading", mainWindow?.webContents.getURL());
});
mainWindow.webContents.on("render-process-gone", (_event, details) => {
console.error("[tensamin:electron] renderer process gone", details);
});
mainWindow.on("unresponsive", () => {
console.error("[tensamin:electron] main window became unresponsive");
});
}
await mainWindow.loadFile(rendererIndex);
}
app.on("window-all-closed", () => {
verboseLog("window-all-closed");
if (process.platform !== "darwin") app.quit();
});
app.on("activate", () => {
verboseLog("activate");
if (BrowserWindow.getAllWindows().length === 0) void createWindow();
});
app.on("will-quit", () => {
globalShortcut.unregisterAll();
});
if (verbose) {
process.on("uncaughtException", (error) => {
console.error("[tensamin:electron] uncaught exception", error);
});
process.on("unhandledRejection", (reason) => {
console.error("[tensamin:electron] unhandled rejection", reason);
});
}
async function start() {
verboseLog("waiting for app readiness");
await app.whenReady();
verboseLog("app ready");
registerIpc();
registerDisplayMediaHandler();
registerMediaPermissionHandler();
initTray(() => mainWindow);
await createWindow();
}
void start().catch((error) => {
console.error("[tensamin:electron] failed to start", error);
app.exit(1);
});

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

@ -0,0 +1,180 @@
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { basename, join } from "node:path";
import { app, net } from "electron";
import type {
ReleaseArtifact,
ReleaseMetadata,
UpdateCheckResult,
} from "../shared/ipc.js";
const metadataUrl = process.env.TENSAMIN_UPDATE_METADATA_URL;
function compareSemver(left: string, right: string) {
const leftParts = left
.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);
for (let index = 0; index < length; index += 1) {
const diff = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
if (diff !== 0) return diff;
}
return 0;
}
function isDevVersion(version: string) {
return /-dev[.-]/.test(version);
}
function platformName() {
if (process.platform === "win32") return "windows";
if (process.platform === "darwin") return "macos";
if (process.platform === "linux") return "linux";
return process.platform;
}
function archName() {
if (process.arch === "x64") return "x64";
if (process.arch === "arm64") return "arm64";
return process.arch;
}
function requestText(url: string): Promise<string> {
return new Promise((resolve, reject) => {
const request = net.request(url);
request.on("response", (response) => {
if (response.statusCode < 200 || response.statusCode >= 300) {
reject(
new Error(`Update request failed with HTTP ${response.statusCode}.`),
);
return;
}
const chunks: Buffer[] = [];
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
response.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
});
request.on("error", reject);
request.end();
});
}
function requestBuffer(url: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
const request = net.request(url);
request.on("response", (response) => {
if (response.statusCode < 200 || response.statusCode >= 300) {
reject(new Error(`Download failed with HTTP ${response.statusCode}.`));
return;
}
const chunks: Buffer[] = [];
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
response.on("end", () => resolve(Buffer.concat(chunks)));
});
request.on("error", reject);
request.end();
});
}
async function sha256File(filePath: string) {
const hash = createHash("sha256");
await new Promise<void>((resolve, reject) => {
const stream = createReadStream(filePath);
stream.on("data", (chunk) => hash.update(chunk));
stream.on("error", reject);
stream.on("end", resolve);
});
return hash.digest("hex");
}
function selectArtifact(
metadata: ReleaseMetadata,
): ReleaseArtifact | undefined {
const platform = platformName();
const arch = archName();
return metadata.artifacts.find(
(artifact) => artifact.platform === platform && artifact.arch === arch,
);
}
export async function checkForUpdates(): Promise<UpdateCheckResult> {
const currentVersion = app.getVersion();
if (!metadataUrl) {
return { available: false, currentVersion, latestVersion: currentVersion };
}
const metadata = JSON.parse(
await requestText(metadataUrl),
) as ReleaseMetadata;
const artifact = selectArtifact(metadata);
if (isDevVersion(metadata.version) !== isDevVersion(currentVersion)) {
return {
available: false,
currentVersion,
latestVersion: metadata.version,
};
}
if (isDevVersion(currentVersion)) {
if (!artifact || metadata.version === currentVersion) {
return {
available: false,
currentVersion,
latestVersion: metadata.version,
};
}
return {
available: true,
currentVersion,
latestVersion: metadata.version,
artifact,
};
}
if (!artifact || compareSemver(metadata.version, currentVersion) <= 0) {
return {
available: false,
currentVersion,
latestVersion: metadata.version,
};
}
return {
available: true,
currentVersion,
latestVersion: metadata.version,
artifact,
};
}
export async function downloadVerifiedArtifact(artifact: ReleaseArtifact) {
const updatesDir = join(app.getPath("userData"), "updates");
await rm(updatesDir, { recursive: true, force: true });
await mkdir(updatesDir, { recursive: true });
const destination = join(updatesDir, basename(artifact.name));
await writeFile(destination, await requestBuffer(artifact.url), {
mode: 0o600,
});
const actualHash = await sha256File(destination);
if (actualHash !== artifact.sha256) {
await rm(destination, { force: true });
throw new Error("Downloaded update failed checksum verification.");
}
return destination;
}

View file

@ -0,0 +1,107 @@
import { contextBridge, ipcRenderer } from "electron";
import {
ipcChannels,
type DesktopCallStatus,
type DesktopGlobalHotkeyBinding,
type DesktopScreenShareSource,
secureStorageLimits,
} from "../shared/ipc.js";
function windowAction(channel: string) {
return () => ipcRenderer.invoke(channel);
}
function validKey(key: string) {
return (
typeof key === "string" &&
key.length > 0 &&
Buffer.byteLength(key, "utf8") <= secureStorageLimits.maxKeyBytes
);
}
const desktopApi = {
media: {
listScreenShareSources: () =>
ipcRenderer.invoke(ipcChannels.listScreenShareSources),
listScreenShareAudioOutputs: () =>
ipcRenderer.invoke(ipcChannels.listScreenShareAudioOutputs),
getScreenShareCapabilities: () =>
ipcRenderer.invoke(ipcChannels.getScreenShareCapabilities),
selectScreenShareSource: (sourceId: DesktopScreenShareSource["id"]) => {
if (typeof sourceId !== "string" || sourceId.length === 0) {
return Promise.reject(new Error("Invalid screen share source id."));
}
return ipcRenderer.invoke(ipcChannels.selectScreenShareSource, sourceId);
},
},
app: {
getVersion: () => ipcRenderer.invoke(ipcChannels.getVersion),
},
updates: {
checkForUpdates: () => ipcRenderer.invoke(ipcChannels.checkForUpdates),
},
call: {
setStatus: (status: DesktopCallStatus) => {
if (
typeof status?.inCall !== "boolean" ||
typeof status?.speaking !== "boolean" ||
(status.iconDataUrl !== undefined &&
(typeof status.iconDataUrl !== "string" ||
!status.iconDataUrl.startsWith("data:image/png;base64,")))
) {
return Promise.reject(new Error("Invalid call status."));
}
return ipcRenderer.invoke(ipcChannels.setCallStatus, status);
},
},
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: {
minimize: () => windowAction(ipcChannels.minimizeWindow),
maximize: () => windowAction(ipcChannels.maximizeWindow),
close: () => windowAction(ipcChannels.closeWindow),
},
};
contextBridge.exposeInMainWorld("tensaminDesktop", desktopApi);
contextBridge.exposeInMainWorld(
"tensaminShowWindowControls",
process.env.TENSAMIN_HIDE_CONTROLS == null,
);
export type TensaminDesktopApi = typeof desktopApi;

View file

@ -0,0 +1,88 @@
export type DesktopScreenShareSource = {
id: string;
kind: "screen" | "window";
name: string;
subtitle?: string | null;
thumbnail?: string | null;
};
export type DesktopScreenShareAudioOutput = {
id: string;
name: string;
isDefault: boolean;
};
export type DesktopScreenShareCapabilities = {
runtime: "electron";
platform: "linux" | "macos" | "windows" | "other";
showAudioOutputSelector: boolean;
showAudioSwitch: 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 = {
name: string;
platform: string;
arch: string;
url: string;
sha256: string;
size: number;
};
export type ReleaseMetadata = {
version: string;
tag: string;
publishedAt: string;
artifacts: ReleaseArtifact[];
};
export type UpdateCheckResult =
| { available: false; currentVersion: string; latestVersion: string }
| {
available: true;
currentVersion: string;
latestVersion: string;
artifact: ReleaseArtifact;
};
export const ipcChannels = {
listScreenShareSources: "desktopMedia:listScreenShareSources",
listScreenShareAudioOutputs: "desktopMedia:listScreenShareAudioOutputs",
getScreenShareCapabilities: "desktopMedia:getScreenShareCapabilities",
selectScreenShareSource: "desktopMedia:selectScreenShareSource",
minimizeWindow: "window:minimize",
maximizeWindow: "window:maximize",
closeWindow: "window:close",
getVersion: "app:getVersion",
checkForUpdates: "updates:checkForUpdates",
setCallStatus: "call:setStatus",
getSecureStorageStatus: "secureStorage:getStatus",
loadSecureStorage: "secureStorage:load",
saveSecureStorage: "secureStorage:save",
deleteSecureStorage: "secureStorage:delete",
clearSecureStorage: "secureStorage:clear",
setGlobalHotkeyBindings: "hotkeys:setBindings",
setGlobalHotkeysSuspended: "hotkeys:setSuspended",
globalHotkeyTriggered: "hotkeys:triggered",
} as const;

View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"types": ["node", "electron"]
},
"include": ["src/**/*.ts"]
}

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?
.android
/src-tauri/gen/android/keystore.properties
/src-tauri/gen/android/keystore.jks

BIN
apps/tauri/android.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
apps/tauri/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

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,138 +0,0 @@
{
description = "Tauri 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"
];
};
desktopRuntimeLibs = with pkgs; [
glib
gtk3
gdk-pixbuf
pango
cairo
atk
at-spi2-atk
alsa-lib
dbus
cups
expat
libdrm
libgbm
libglvnd
mesa
libxkbcommon
libx11
libxcomposite
libxdamage
libxext
libxfixes
libxrandr
libxcb
systemd
nspr
nss
];
in {
devShells.default = pkgs.mkShell {
buildInputs = with pkgs;
[
jdk17
rustToolchain
gradle
nodejs
pkg-config
]
++ desktopRuntimeLibs
++ [
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}"
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath desktopRuntimeLibs}:$LD_LIBRARY_PATH"
'';
};
}
);
}

8
apps/tauri/logo.json Normal file
View file

@ -0,0 +1,8 @@
{
"default": "./logo.svg",
"android_fg": "./android.png",
"android_bg": "./background.png",
"android_fg_scale": 100,
"android_monochrome": "./monochrome.png"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 997 KiB

271
apps/tauri/logo.svg Normal file
View file

@ -0,0 +1,271 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="90.476906mm"
height="90.476906mm"
viewBox="0 0 90.476906 90.476906"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
sodipodi:docname="logo_square_outline_gen.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
inkscape:zoom="2"
inkscape:cx="191.5"
inkscape:cy="117.25"
inkscape:window-width="2500"
inkscape:window-height="1403"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" /><defs
id="defs1"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1 @ F,0,0,1,0,0.26458333,0,1"
radius="1"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch15"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop15" /></linearGradient><linearGradient
id="swatch10"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop10" /></linearGradient><linearGradient
id="swatch3"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3" /></linearGradient><linearGradient
id="swatch2"
inkscape:swatch="solid"><stop
style="stop-color:#b8f8ff;stop-opacity:1;"
offset="0"
id="stop2" /></linearGradient><linearGradient
id="swatch1"
inkscape:swatch="solid"><stop
style="stop-color:#031616;stop-opacity:1;"
offset="0"
id="stop1" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.79374998,0,1 @ F,0,0,1,0,0.79374998,0,1 @ F,0,0,1,0,0.79374998,0,1 @ F,0,0,1,0,0.79374998,0,1 @ F,0,0,1,0,0.79374998,0,1 @ F,0,0,1,0,0.79374998,0,1 @ F,0,0,1,0,0.79374998,0,1"
radius="3"
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" /><inkscape:path-effect
effect="bspline"
id="path-effect6"
is_visible="true"
lpeversion="1.3"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false"
uniform="false" /><inkscape:path-effect
effect="spiro"
id="path-effect5"
is_visible="true"
lpeversion="1" /><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1"><path
id="path3"
style="fill:#043b3c;stroke-width:0.320821"
inkscape:label="arrow"
d="m 244.98648,261.60864 -9.19072,26.71639 23.35523,-11.66051 c -4.69011,-5.05439 -10.12762,-9.5023 -14.16451,-15.05588 z m 40.12749,-74.6916 50.72412,18.76364 c 6.70121,39.09457 -10.8946,70.48625 -50.72412,85.29337 z m 0,0 -50.72412,18.76364 c -6.7012,39.09457 10.89461,70.48625 50.72412,85.29337 z"
sodipodi:nodetypes="cccccccccccc" /></clipPath><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient15"
x1="232.99823"
y1="238.94554"
x2="337.22971"
y2="238.94554"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient16"
x1="63.191292"
y1="148.5"
x2="146.82355"
y2="148.5"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient17"
gradientUnits="userSpaceOnUse"
x1="232.99823"
y1="238.94554"
x2="337.22971"
y2="238.94554" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient18"
gradientUnits="userSpaceOnUse"
x1="232.99823"
y1="238.94554"
x2="337.22971"
y2="238.94554" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient19"
gradientUnits="userSpaceOnUse"
x1="232.99823"
y1="238.94554"
x2="337.22971"
y2="238.94554" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient20"
gradientUnits="userSpaceOnUse"
x1="232.99823"
y1="238.94554"
x2="337.22971"
y2="238.94554" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient21"
gradientUnits="userSpaceOnUse"
x1="232.99823"
y1="238.94554"
x2="337.22971"
y2="238.94554" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient22"
gradientUnits="userSpaceOnUse"
x1="63.191292"
y1="148.5"
x2="146.82355"
y2="148.5" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient23"
gradientUnits="userSpaceOnUse"
x1="63.191292"
y1="148.5"
x2="146.82355"
y2="148.5" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient24"
gradientUnits="userSpaceOnUse"
x1="63.191292"
y1="148.5"
x2="146.82355"
y2="148.5" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient25"
gradientUnits="userSpaceOnUse"
x1="63.191292"
y1="148.5"
x2="146.82355"
y2="148.5" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch2"
id="linearGradient1"
gradientUnits="userSpaceOnUse"
x1="63.191292"
y1="148.5"
x2="146.82355"
y2="148.5"
gradientTransform="matrix(1.0144629,0,0,1.0151924,217.46226,13.830072)" /></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-281.06837,-119.33314)"
><g
id="g2"
transform="matrix(0.77770172,0,0,0.82714179,104.17421,-33.291714)"
inkscape:label="background"
style="display:inline;stroke:url(#linearGradient15);stroke-width:0.616508;stroke-dasharray:none"
clip-path="url(#clipPath1)"><path
id="rect1"
style="fill:#004b4a;stroke:url(#linearGradient17);stroke-width:0.616508;stroke-dasharray:none"
transform="rotate(-75)"
d="m -227.83395,273.59631 h 146.303341 v 24.39636 H -227.83395 Z" /><path
id="rect1-1"
style="fill:#00524e;stroke:url(#linearGradient18);stroke-width:0.616508;stroke-dasharray:none"
transform="rotate(-75)"
d="m -227.83395,297.99268 h 146.303341 v 24.39636 H -227.83395 Z" /><path
id="rect1-1-3"
style="fill:#006560;stroke:url(#linearGradient19);stroke-width:0.616508;stroke-dasharray:none"
transform="rotate(-75)"
d="m -227.83395,322.3891 h 146.303341 v 24.39636 H -227.83395 Z" /><path
id="rect1-1-3-6"
style="fill:#02857d;stroke:url(#linearGradient20);stroke-width:0.616508;stroke-dasharray:none"
transform="rotate(-75)"
d="m -227.83395,346.78546 h 146.303341 v 24.39636 H -227.83395 Z" /><path
id="rect1-1-3-6-2"
style="fill:#12a89e;stroke:url(#linearGradient21);stroke-width:0.616508;stroke-dasharray:none"
transform="rotate(-75)"
d="m -227.83395,371.18179 h 146.303341 v 24.39636 H -227.83395 Z" /></g><path
id="path1-5"
style="fill:none;stroke:url(#linearGradient1);stroke-width:2;stroke-dasharray:none"
inkscape:label="glow_outline"
d="m 323.73413,119.91784 -40.80246,15.9513 c -0.13609,0.0532 -0.26542,0.21326 -0.28843,0.35757 -2.90631,18.22859 -0.19736,34.27444 8.28042,47.36302 0.0794,0.12264 0.10916,0.33484 0.0657,0.47436 l -7.08112,22.75509 a 0.11130973,0.11130973 40.101955 0 0 0.15695,0.13218 l 18.7652,-9.59506 c 0.13011,-0.0665 0.32608,-0.044 0.43816,0.0497 5.75202,4.81168 12.45464,8.67546 20.46583,11.84762 0.13586,0.0538 0.3564,0.0538 0.49225,-3e-5 32.16862,-12.74646 46.41741,-39.59704 41.09168,-73.02692 -0.023,-0.14431 -0.15232,-0.30436 -0.28841,-0.35757 l -40.80297,-15.9513 a 0.67679193,0.67679193 179.99988 0 0 -0.49284,0 z"
sodipodi:nodetypes="cccccccc"
inkscape:original-d="m 323.98055,119.8215 -41.2953,16.14398 c -2.99079,18.43973 -0.24934,34.65473 8.38277,47.84598 l -7.23836,23.26035 19.23635,-9.83596 c 5.85568,4.9403 12.69983,8.88741 20.91454,12.11489 32.42602,-12.73985 46.75142,-39.74883 41.29581,-73.38526 z"
inkscape:path-effect="#path-effect1"
transform="matrix(0.98854883,0,0,0.98930922,6.03231,1.7325084)" /><g
id="g3"
inkscape:label="foreground"
style="stroke:url(#linearGradient16);stroke-width:0.5;stroke-dasharray:none"
transform="matrix(0.98854883,0,0,0.98930922,222.52896,17.64618)"><g
id="g1"
inkscape:label="outline"
style="stroke:url(#linearGradient23);stroke-width:0.5;stroke-dasharray:none"><path
id="path15-3-5"
style="opacity:0.352;fill:#000000;fill-opacity:1;stroke:url(#linearGradient22);stroke-width:0.5;stroke-dasharray:none"
inkscape:label="filler"
d="m -54.535435,65.443954 -38.869976,15.14533 c -2.902661,17.83718 0.143161,33.687786 8.852689,46.257116 l -5.823934,18.52239 15.016655,-8.2765 c 5.699447,4.98296 12.651108,9.14382 20.824566,12.34447 30.52177,-11.95203 44.00571,-37.29104 38.8705,-68.847476 z m -0.01808,2.85099 36.24585,14.11438 c 4.61111,37.589746 -14.17784,55.293626 -36.24585,64.158876 -21.698042,-8.5864 -40.914886,-26.53641 -36.245337,-64.158876 z"
transform="translate(159.54375,40.87617)" /></g><path
style="opacity:1;fill:#b8f8ff;fill-opacity:1;stroke:url(#linearGradient24);stroke-width:0.5;stroke-dasharray:none"
d="m 98.103726,126.47621 21.412544,0.16758 a 0.41189664,0.41189664 63.022469 0 1 0.33171,0.65164 l -8.29596,11.58914 a 0.41184923,0.41184923 63.019773 0 0 0.33171,0.65157 l 10.05254,0.0777 a 0.3104736,0.3104736 69.080154 0 1 0.20649,0.54017 l -30.495516,27.73194 a 0.15466019,0.15466019 36.691517 0 1 -0.243455,-0.18141 l 10.172421,-21.16912 a 0.50438266,0.50438266 58.099348 0 0 -0.44993,-0.72282 l -11.312564,-0.10524 a 0.52748832,0.52748832 56.926855 0 1 -0.479488,-0.73628 l 7.661544,-17.7722 a 1.1963277,1.1963277 146.88457 0 1 1.107954,-0.72269 z"
id="path2"
sodipodi:nodetypes="cccccccc"
inkscape:label="bolt" /><path
id="path1"
style="fill:#043b3c;stroke:url(#linearGradient25);stroke-width:0.5;stroke-dasharray:none"
inkscape:label="dark_outline"
d="m 104.99968,104.40528 -40.706558,15.90239 c -2.948156,18.16379 0.05742,34.31077 8.566402,47.3046 l -7.438306,22.73763 18.798853,-9.90998 c 5.772204,4.86637 12.68201,8.97564 20.779609,12.15481 31.96374,-12.54919 46.08489,-39.15399 40.70708,-72.28706 z m 0,1.91926 38.88755,15.13603 c 4.9985,39.45937 -15.37196,59.52899 -38.88755,68.8573 -7.461219,-2.95976 -14.605741,-7.00102 -20.723799,-12.39976 l -1.849499,0.95188 -13.267924,7.37991 5.196582,-16.46463 0.704866,-2.3027 c 0.02259,0.0331 0.04422,0.065 0,-5.2e-4 -0.02016,-0.0296 -0.04036,-0.0594 -0.01292,-0.0196 -7.707836,-11.18647 -11.48502,-25.8661 -8.934338,-46.00184 z m 5.2e-4,2.85151 -36.245852,14.11438 c -4.788427,29.4077 7.785188,53.02119 36.245852,64.15939 28.46066,-11.1382 41.03376,-34.75169 36.24533,-64.15939 z m 0,1.70377 34.62527,13.43381 c 4.45069,35.0227 -13.68688,52.83618 -34.62527,61.11564 -20.938395,-8.27946 -39.076477,-26.09294 -34.625796,-61.11564 z" /></g></g></svg>

After

Width:  |  Height:  |  Size: 13 KiB

BIN
apps/tauri/monochrome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

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

View file

@ -1,5 +0,0 @@
# Outputs
- ./src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
- ./src-tauri/target/release/bundle/deb/Tensamin*{version}*{arch}.deb
- ./src-tauri/target/release/bundle/rpm/Tensamin-{version}-1.{arch}.rpm

View file

@ -0,0 +1,68 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
// Config
const PLACEHOLDER_VERSION = "0.0.0";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootPackageJsonPath = path.resolve(__dirname, "../../package.json");
const cargoTomlPath = path.resolve(__dirname, "./src-tauri/Cargo.toml");
const tauriConfigPath = path.resolve(__dirname, "./src-tauri/tauri.conf.json");
// Args
const isUnrender = process.argv.includes("--unrender");
// Version Source
const packageJson = JSON.parse(fs.readFileSync(rootPackageJsonPath, "utf8"));
const packageVersion: string = packageJson.version;
if (!packageVersion && !isUnrender) {
throw new Error("No version found in package.json");
}
const targetVersion = isUnrender ? PLACEHOLDER_VERSION : packageVersion;
// Helpers
function updateCargoToml(content: string): string {
const regex = /^version\s*=\s*".*"$/m;
if (!regex.test(content)) {
throw new Error("Could not find version field in Cargo.toml");
}
return content.replace(regex, `version = "${targetVersion}"`);
}
function updateTauriConfig(content: string): string {
const regex = /"version"\s*:\s*".*"/;
if (!regex.test(content)) {
throw new Error("Could not find version field in tauri.conf.json");
}
return content.replace(regex, `"version": "${targetVersion}"`);
}
// Update Cargo.toml
const cargoToml = fs.readFileSync(cargoTomlPath, "utf8");
const updatedCargoToml = updateCargoToml(cargoToml);
fs.writeFileSync(cargoTomlPath, updatedCargoToml, "utf8");
// Update tauri.conf.json
const tauriConfig = fs.readFileSync(tauriConfigPath, "utf8");
const updatedTauriConfig = updateTauriConfig(tauriConfig);
fs.writeFileSync(tauriConfigPath, updatedTauriConfig, "utf8");
// Finished
if (isUnrender) {
console.log(`Unrendered versions back to ${PLACEHOLDER_VERSION}`);
} else {
console.log(`Rendered version ${targetVersion}`);
}

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

@ -1,6 +1,6 @@
[package]
name = "tensamin"
version = "0.1.0"
version = "0.0.0"
description = "Privacy focused messanger"
authors = ["methanium"]
edition = "2021"
@ -15,43 +15,34 @@ name = "mobile_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
tauri-build = { git = "https://github.com/tauri-apps/tauri", rev = "4af26a3f7f8b692d62cca549bbacd93f5ce90b41", features = [] }
[dependencies]
base64 = "0.22"
image = { version = "0.25", default-features = false, features = ["jpeg"] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
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"
[target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))'.dependencies]
xcap = "0.4.1"
tauri-plugin-notification = "2"
tauri-plugin-log = "2"
[target.'cfg(target_os = "android")'.dependencies.tauri]
version = "2"
features = []
default-features = true
[target.'cfg(target_os = "windows")'.dependencies.tauri]
[target.'cfg(not(target_os = "android"))'.dependencies.tauri]
version = "2"
features = ["compression", "common-controls-v6", "dynamic-acl"]
features = []
default-features = true
[target.'cfg(target_os = "linux")'.dependencies.tauri]
version = "2"
features = ["common-controls-v6", "cef", "compression", "dynamic-acl", "x11"]
default-features = false
[target.'cfg(target_os = "macos")'.dependencies.tauri]
version = "2"
features = ["x11", "common-controls-v6", "cef", "compression", "dynamic-acl"]
default-features = false
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
tauri-plugin-barcode-scanner = "2"
tauri-plugin-app-events = "0.2"
[target.'cfg(target_os = "android")'.dependencies]
jni = "0.22"
[patch.crates-io.tauri]
git = "https://github.com/tauri-apps/tauri"
branch = "feat/cef"
rev = "4af26a3f7f8b692d62cca549bbacd93f5ce90b41"

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties
import java.io.FileInputStream
@ -40,6 +41,7 @@ android {
}
buildTypes {
getByName("debug") {
applicationIdSuffix = ".dev"
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
@ -60,26 +62,29 @@ android {
)
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
}
}
rust {
rootDirRel = "../../../"
}
dependencies {
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.webkit:webkit:1.16.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.activity:activity-ktx:1.13.0")
implementation("com.google.android.material:material:1.14.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
androidTestImplementation("androidx.test.ext:junit:1.3.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
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile

View file

@ -1,6 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<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 -->
<uses-feature android:name="android.software.leanback" android:required="false" />
@ -39,6 +49,30 @@
<!-- DEEP LINK PLUGIN. AUTO-GENERATED. DO NOT REMOVE. -->
</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
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View file

@ -1,28 +1,114 @@
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.view.ViewGroup
import android.view.MotionEvent
import android.view.ViewTreeObserver
import android.view.WindowManager
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.widget.FrameLayout
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import org.json.JSONObject
class MainActivity : TauriActivity() {
private var contentRoot: FrameLayout? = null
private var contentChild: android.view.View? = null
private var previousUsableHeight = 0
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?) {
WindowCompat.setDecorFitsSystemWindows(window, true)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
super.onCreate(savedInstanceState)
NativeMtpBridge.nativeAttach(applicationContext)
if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) {
NativeMtpBridge.startService(this)
}
NativeAccessibilityBridge.nativeAttach(applicationContext)
installKeyboardResizeWorkaround()
}
override fun onResume() {
super.onResume()
NativeMtpBridge.nativeSetUiState(true)
}
override fun onPause() {
NativeMtpBridge.nativeSetUiState(false)
super.onPause()
}
override fun onDestroy() {
attachLayoutListener?.let { listener ->
contentRoot?.viewTreeObserver?.removeOnGlobalLayoutListener(listener)
@ -30,9 +116,87 @@ class MainActivity : TauriActivity() {
attachLayoutListener = null
contentRoot = null
contentChild = null
mediaWebView?.let {
NativeAccessibilityBridge.detach(it)
it.removeJavascriptInterface("tensaminMobileMedia")
}
mediaWebView = null
MobileMediaEvents.detach()
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() {
val content = window.decorView.findViewById<FrameLayout>(android.R.id.content)
contentRoot = content
@ -68,16 +232,20 @@ class MainActivity : TauriActivity() {
child: android.view.View,
insets: WindowInsetsCompat? = ViewCompat.getRootWindowInsets(child),
) {
val visibleFrame = Rect()
child.getWindowVisibleDisplayFrame(visibleFrame)
val rootHeight = child.rootView.height
if (rootHeight <= 0) return
val imeHeight = insets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0
val keyboardHeight = maxOf(imeHeight, rootHeight - visibleFrame.bottom)
val keyboardVisible = keyboardHeight > rootHeight * 0.15
val usableHeight = if (keyboardVisible) rootHeight - keyboardHeight else ViewGroup.LayoutParams.MATCH_PARENT
val imeVisible = insets?.isVisible(WindowInsetsCompat.Type.ime()) == true
val imeHeight = if (imeVisible) {
insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
} else {
0
}
val usableHeight = if (imeHeight in 1 until rootHeight) {
rootHeight - imeHeight
} else {
WindowManager.LayoutParams.MATCH_PARENT
}
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

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
<background android:drawable="@mipmap/ic_launcher_background"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon>

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.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Before After
Before After

View file

@ -1,4 +1,4 @@
<resources>
<string name="app_name">tensamin</string>
<string name="main_activity_title">tensamin</string>
<string name="app_name">Tensamin</string>
<string name="main_activity_title">Tensamin</string>
</resources>

View file

@ -1,4 +1,4 @@
import com.android.build.gradle.LibraryExtension
import com.android.build.api.dsl.LibraryExtension
buildscript {
repositories {
@ -7,7 +7,7 @@ buildscript {
}
dependencies {
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())
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.tasks.Input
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
var rootDirRel: String? = null
@Input
@ -50,7 +54,7 @@ open class BuildTask : DefaultTask() {
val release = release ?: throw GradleException("release cannot be null")
val args = listOf("tauri", "android", "android-studio-script");
project.exec {
execOperations.exec {
workingDir(File(project.projectDir, rootDirRel))
executable(executable)
args(args)
@ -65,4 +69,4 @@ open class BuildTask : DefaultTask() {
args(listOf("--target", target))
}.assertNormalExitValue()
}
}
}

View file

@ -1,6 +1,6 @@
#Tue May 10 19:22:52 CST 2022
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
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 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: 15 KiB

Before After
Before After

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