Compare commits

..
Author SHA1 Message Date
bbf53e2b30
fix(tauri): build failure
All checks were successful
/ build-web (push) Successful in 4m37s
/ build-desktop (linux) (push) Successful in 8m28s
/ build-mobile (push) Successful in 19m4s
/ release (push) Successful in 2m4s
2026-09-01 08:49:25 +02:00
e0c2331af3
perf(react): remove lazy imports again
Some checks failed
/ build-web (push) Successful in 4m52s
/ build-mobile (push) Failing after 8m36s
/ build-desktop (linux) (push) Successful in 7m46s
/ release (push) Has been skipped
2026-08-31 23:09:02 +02:00
d88c554a2d
fix(storage): circual deps
Some checks failed
/ build-desktop (linux) (push) Failing after 1m57s
/ build-web (push) Failing after 2m1s
/ build-mobile (push) Failing after 3m58s
/ release (push) Has been skipped
2026-08-31 22:05:28 +02:00
0c936718bc
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev 2026-08-31 20:48:34 +02:00
85921a0a08
perf(vite): add dynamic imports 2026-08-31 20:48:32 +02:00
Alex Emmet
6e31989262
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
Some checks failed
/ build-web (push) Failing after 2m6s
/ build-desktop (linux) (push) Failing after 2m10s
/ build-mobile (push) Failing after 3m46s
/ release (push) Has been skipped
2026-08-30 21:28:51 +02:00
Alex Emmet
c155bbb534
[Fix] Durability 2026-08-30 21:28:36 +02:00
78e15f6218
chore(identity): remove dumb test
Some checks failed
/ build-web (push) Successful in 3m53s
/ build-mobile (push) Failing after 7m48s
/ build-desktop (linux) (push) Successful in 7m56s
/ release (push) Has been skipped
2026-08-30 19:34:07 +02:00
042141c781
feat(user): rename to identity
Some checks failed
/ build-web (push) Successful in 4m1s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (linux) (push) Has been cancelled
feat(identity): add getIota function with caching
2026-08-30 19:26:42 +02:00
Alex Emmet
c386e8d5cb
Merge remote-tracking branch 'refs/remotes/origin/dev' into dev
Some checks failed
/ build-web (push) Failing after 1m45s
/ build-desktop (linux) (push) Failing after 1m49s
/ build-mobile (push) Failing after 3m34s
/ release (push) Has been skipped
2026-08-30 18:26:42 +02:00
Alex Emmet
74fb46990e
[Fix] Connections 2026-08-30 18:26:32 +02: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
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
265 changed files with 12916 additions and 13902 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 }

View file

@ -3,6 +3,7 @@
"entry": [ "entry": [
"src/index.{ts,tsx,js,jsx}", "src/index.{ts,tsx,js,jsx}",
"src/main.{ts,tsx,js,jsx}", "src/main.{ts,tsx,js,jsx}",
"apps/pwa/src/serviceWorker.ts",
"apps/tauri/render-version.ts", "apps/tauri/render-version.ts",
"packages/**/*.test.ts" "packages/**/*.test.ts"
], ],

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,20 +1,15 @@
on: on:
workflow_dispatch:
push: push:
branches: branches:
- dev - dev
paths-ignore: paths-ignore:
- flake.nix - flake.nix
env:
NIX_CONFIG: experimental-features = nix-command flakes
jobs: jobs:
build-web: build-web:
runs-on: nixos runs-on: nixos
steps: steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
@ -39,9 +34,6 @@ jobs:
build-mobile: build-mobile:
runs-on: nixos runs-on: nixos
steps: steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
@ -62,8 +54,6 @@ jobs:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }} KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
run: | run: |
nix profile add nixpkgs#gnused
set -euo pipefail set -euo pipefail
if [ -z "$KEYSTORE_BASE64" ]; then if [ -z "$KEYSTORE_BASE64" ]; then
@ -128,9 +118,6 @@ jobs:
matrix: matrix:
target: [linux] target: [linux]
steps: steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
@ -181,9 +168,6 @@ jobs:
runs-on: nixos runs-on: nixos
needs: [build-web, build-mobile, build-desktop] needs: [build-web, build-mobile, build-desktop]
steps: steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
with: with:

View file

@ -6,16 +6,10 @@ on:
paths-ignore: paths-ignore:
- flake.nix - flake.nix
env:
NIX_CONFIG: experimental-features = nix-command flakes
jobs: jobs:
build-web: build-web:
runs-on: nixos runs-on: nixos
steps: steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
@ -40,9 +34,6 @@ jobs:
build-mobile: build-mobile:
runs-on: nixos runs-on: nixos
steps: steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
@ -63,8 +54,6 @@ jobs:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }} KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
run: | run: |
nix profile add nixpkgs#gnused
set -euo pipefail set -euo pipefail
if [ -z "$KEYSTORE_BASE64" ]; then if [ -z "$KEYSTORE_BASE64" ]; then
@ -129,9 +118,6 @@ jobs:
matrix: matrix:
target: [linux] target: [linux]
steps: steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
@ -180,9 +166,6 @@ jobs:
runs-on: nixos runs-on: nixos
needs: [build-web, build-mobile, build-desktop] needs: [build-web, build-mobile, build-desktop]
steps: steps:
- name: Install node
run: nix profile add nixpkgs#nodejs_24
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
@ -328,58 +311,3 @@ jobs:
"$API/repos/$REPO/releases/$release_id" "$API/repos/$REPO/releases/$release_id"
done < "$DELETE_RELEASES" done < "$DELETE_RELEASES"
EOF EOF
- name: Update root flake release hash
env:
TAG: ${{ steps.version.outputs.tag }}
run: |
nix develop .#electron --command bash <<'EOF'
set -eu
DEB="$(find releases -maxdepth 1 -type f -name 'Tensamin-*-linux-amd64.deb' -print -quit)"
test -n "$DEB"
HASH="$(node -e 'const fs = require("fs"); const crypto = require("crypto"); const file = process.argv[1]; console.log("sha256-" + crypto.createHash("sha256").update(fs.readFileSync(file)).digest("base64"));' "$DEB")"
export HASH
node -e '
const fs = require("fs");
const version = process.env.TAG;
const hash = process.env.HASH;
let content = fs.readFileSync("flake.nix", "utf8");
content = content.replace(/version = "[^"]+";/, `version = "${version}";`);
content = content.replace(/x86_64DebHash = "sha256-[^"]+";/, `x86_64DebHash = "${hash}";`);
fs.writeFileSync("flake.nix", content);
'
if git diff --quiet -- flake.nix; then
echo "flake.nix already has the current release hash on main."
else
git add flake.nix
git -c user.name="forgejo-actions" -c user.email="forgejo-actions@localhost" commit -m "(qol): update release flake hash"
git push
fi
git fetch origin dev
git worktree add ../dev-flake-update origin/dev
cd ../dev-flake-update
node -e '
const fs = require("fs");
const version = process.env.TAG;
const hash = process.env.HASH;
let content = fs.readFileSync("flake.nix", "utf8");
content = content.replace(/version = "[^"]+";/, `version = "${version}";`);
content = content.replace(/x86_64DebHash = "sha256-[^"]+";/, `x86_64DebHash = "${hash}";`);
fs.writeFileSync("flake.nix", content);
'
if git diff --quiet -- flake.nix; then
echo "flake.nix already has the current release hash on dev."
exit 0
fi
git add flake.nix
git -c user.name="forgejo-actions" -c user.email="forgejo-actions@localhost" commit -m "(qol): update release flake hash"
git push origin HEAD:dev
EOF

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View file

@ -28,12 +28,11 @@
"validate:raw": "pnpm run build && pnpm run package:linux:raw", "validate:raw": "pnpm run build && pnpm run package:linux:raw",
"validate": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run validate:raw; else pnpm run validate:raw; fi" "validate": "if command -v nix >/dev/null 2>&1 && [ -z \"$IN_NIX_SHELL\" ]; then nix develop ../..#electron --command pnpm run validate:raw; else pnpm run validate:raw; fi"
}, },
"dependencies": {},
"devDependencies": { "devDependencies": {
"@types/node": "^25.9.1", "@types/node": "^26.1.2",
"electron": "^39.2.7", "electron": "^43.3.0",
"electron-builder": "^26.0.12", "electron-builder": "^26.15.3",
"esbuild": "^0.28.0", "esbuild": "^0.28.1",
"typescript": "~6.0.3" "typescript": "~6.0.3"
}, },
"build": { "build": {

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

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

@ -0,0 +1,223 @@
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: "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

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

View file

@ -7,16 +7,13 @@
"./deeplinkHandler": { "./deeplinkHandler": {
"types": "./src/deeplinkHandler.tsx", "types": "./src/deeplinkHandler.tsx",
"default": "./src/deeplinkHandler.tsx" "default": "./src/deeplinkHandler.tsx"
},
"./qrCodeScanner": {
"types": "./src/qrCodeScanner.tsx",
"default": "./src/qrCodeScanner.tsx"
} }
}, },
"scripts": { "scripts": {
"dev:mobile:raw": "tauri android dev --host ${TAURI_DEV_HOST:-127.0.0.1}", "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", "start-adb:mobile:raw": "adb devices",
"build:mobile:raw": "tauri android build", "build:mobile:raw": "tauri android build",
"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", "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", "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", "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",
@ -25,18 +22,15 @@
"lint": "eslint src" "lint": "eslint src"
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-barcode-scanner": "~2",
"@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-log": "~2",
"@tauri-apps/plugin-notification": "~2",
"@tensamin/shared": "workspace:*",
"@methanium/ui": "*", "@methanium/ui": "*",
"react": "^19.2.0", "@tauri-apps/api": "^2.11.1",
"react-dom": "^19.2.0" "@tauri-apps/plugin-deep-link": "~2.4.9",
"@tensamin/shared": "workspace:*",
"react": "^19.2.8",
"react-dom": "^19.2.8"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2.11.4",
"@types/node": "^25.9.1" "@types/node": "^26.1.2"
} }
} }

View file

@ -560,17 +560,6 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "cfb"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3"
dependencies = [
"fnv",
"uuid",
"web-time",
]
[[package]] [[package]]
name = "cfg-expr" name = "cfg-expr"
version = "0.15.8" version = "0.15.8"
@ -685,12 +674,6 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.10.2" version = "0.10.2"
@ -843,20 +826,7 @@ version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2"
dependencies = [ dependencies = [
"cssparser-macros 0.6.1", "cssparser-macros",
"dtoa-short",
"itoa",
"phf",
"smallvec",
]
[[package]]
name = "cssparser"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98"
dependencies = [
"cssparser-macros 0.7.0",
"dtoa-short", "dtoa-short",
"itoa", "itoa",
"phf", "phf",
@ -873,16 +843,6 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "cssparser-macros"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a2a99df6e410a8ff4245aa2006499ea662245f967cc7c0a38c83ef8eb44dbf"
dependencies = [
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "ctor" name = "ctor"
version = "0.8.0" version = "0.8.0"
@ -923,8 +883,23 @@ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures 0.2.17", "cpufeatures 0.2.17",
"curve25519-dalek-derive", "curve25519-dalek-derive",
"digest 0.10.7", "fiat-crypto 0.2.9",
"fiat-crypto", "rustc_version",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"curve25519-dalek-derive",
"digest 0.11.3",
"fiat-crypto 0.3.0",
"rustc_version", "rustc_version",
"subtle", "subtle",
"zeroize", "zeroize",
@ -992,24 +967,14 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid 0.9.6",
"pem-rfc7468",
"zeroize",
]
[[package]] [[package]]
name = "der" name = "der"
version = "0.8.1" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d"
dependencies = [ dependencies = [
"const-oid 0.10.2", "const-oid",
"pem-rfc7468",
"zeroize", "zeroize",
] ]
@ -1074,7 +1039,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [ dependencies = [
"block-buffer 0.12.1", "block-buffer 0.12.1",
"const-oid 0.10.2", "const-oid",
"crypto-common 0.2.2", "crypto-common 0.2.2",
"ctutils", "ctutils",
] ]
@ -1162,26 +1127,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
dependencies = [ dependencies = [
"bit-set", "bit-set",
"cssparser 0.36.0", "cssparser",
"foldhash", "foldhash",
"html5ever 0.38.0", "html5ever",
"precomputed-hash", "precomputed-hash",
"selectors 0.36.1", "selectors",
"tendril",
]
[[package]]
name = "dom_query"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc"
dependencies = [
"bit-set",
"cssparser 0.37.0",
"foldhash",
"html5ever 0.39.0",
"precomputed-hash",
"selectors 0.38.0",
"tendril", "tendril",
] ]
@ -1238,24 +1188,25 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]] [[package]]
name = "ed25519" name = "ed25519"
version = "2.2.3" version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a"
dependencies = [ dependencies = [
"pkcs8 0.10.2", "pkcs8",
"signature 2.2.0", "signature",
] ]
[[package]] [[package]]
name = "ed25519-dalek" name = "ed25519-dalek"
version = "2.2.0" version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" checksum = "6ebaa1a2bf1290ab3bfe5a7b771d050ebffab2711c19a81691c683a5144a25de"
dependencies = [ dependencies = [
"curve25519-dalek", "curve25519-dalek 5.0.0",
"ed25519", "ed25519",
"serde", "serde",
"sha2 0.10.9", "sha2 0.11.0",
"signature",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@ -1341,7 +1292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -1406,6 +1357,12 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "fiat-crypto"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24"
[[package]] [[package]]
name = "field-offset" name = "field-offset"
version = "0.3.6" version = "0.3.6"
@ -2025,17 +1982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
dependencies = [ dependencies = [
"log", "log",
"markup5ever 0.38.0", "markup5ever",
]
[[package]]
name = "html5ever"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
dependencies = [
"log",
"markup5ever 0.39.0",
] ]
[[package]] [[package]]
@ -2134,7 +2081,6 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tower-service", "tower-service",
"webpki-roots",
] ]
[[package]] [[package]]
@ -2172,7 +2118,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"wasm-bindgen", "wasm-bindgen",
"windows-core 0.62.2", "windows-core",
] ]
[[package]] [[package]]
@ -2332,16 +2278,7 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
dependencies = [ dependencies = [
"cfb 0.7.3", "cfb",
]
[[package]]
name = "infer"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4"
dependencies = [
"cfb 0.14.0",
] ]
[[package]] [[package]]
@ -2704,17 +2641,6 @@ dependencies = [
"web_atoms", "web_atoms",
] ]
[[package]]
name = "markup5ever"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"
dependencies = [
"log",
"tendril",
"web_atoms",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.3" version = "2.8.3"
@ -2769,14 +2695,14 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d"
dependencies = [ dependencies = [
"const-oid 0.10.2", "const-oid",
"crypto-common 0.2.2", "crypto-common 0.2.2",
"ctutils", "ctutils",
"hybrid-array", "hybrid-array",
"module-lattice", "module-lattice",
"pkcs8 0.11.0", "pkcs8",
"shake", "shake",
"signature 3.0.0", "signature",
] ]
[[package]] [[package]]
@ -2818,8 +2744,8 @@ dependencies = [
[[package]] [[package]]
name = "mtp" name = "mtp"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a"
dependencies = [ dependencies = [
"mtp-client", "mtp-client",
"mtp-codec", "mtp-codec",
@ -2833,8 +2759,8 @@ dependencies = [
[[package]] [[package]]
name = "mtp-client" name = "mtp-client"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a"
dependencies = [ dependencies = [
"mtp-codec", "mtp-codec",
"mtp-common", "mtp-common",
@ -2846,8 +2772,8 @@ dependencies = [
[[package]] [[package]]
name = "mtp-codec" name = "mtp-codec"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a"
dependencies = [ dependencies = [
"base64 0.23.1", "base64 0.23.1",
"byteorder", "byteorder",
@ -2855,12 +2781,13 @@ dependencies = [
"mtp-crypto", "mtp-crypto",
"mtp-type-map", "mtp-type-map",
"rand 0.10.2", "rand 0.10.2",
"thiserror 2.0.19",
] ]
[[package]] [[package]]
name = "mtp-common" name = "mtp-common"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a"
dependencies = [ dependencies = [
"quinn", "quinn",
"rustls", "rustls",
@ -2870,10 +2797,10 @@ dependencies = [
[[package]] [[package]]
name = "mtp-crypto" name = "mtp-crypto"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a"
dependencies = [ dependencies = [
"base64 0.23.1", "base64 0.22.1",
"chacha20poly1305", "chacha20poly1305",
"ed25519-dalek", "ed25519-dalek",
"getrandom 0.4.3", "getrandom 0.4.3",
@ -2881,7 +2808,7 @@ dependencies = [
"ml-dsa", "ml-dsa",
"mlkem-tls", "mlkem-tls",
"rand 0.10.2", "rand 0.10.2",
"rand_core 0.10.1", "rand_core 0.6.4",
"rustls", "rustls",
"serde", "serde",
"sha2 0.11.0", "sha2 0.11.0",
@ -2892,14 +2819,15 @@ dependencies = [
[[package]] [[package]]
name = "mtp-host" name = "mtp-host"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a"
dependencies = [ dependencies = [
"mtp-codec", "mtp-codec",
"mtp-common", "mtp-common",
"mtp-crypto", "mtp-crypto",
"mtp-transport", "mtp-transport",
"rand 0.10.2", "rand 0.10.2",
"thiserror 2.0.19",
"tokio", "tokio",
"tracing", "tracing",
"wtransport", "wtransport",
@ -2907,13 +2835,14 @@ dependencies = [
[[package]] [[package]]
name = "mtp-transport" name = "mtp-transport"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"mtp-codec", "mtp-codec",
"mtp-common", "mtp-common",
"mtp-crypto", "mtp-crypto",
"rand 0.10.2",
"rcgen", "rcgen",
"rustls", "rustls",
"rustls-native-certs", "rustls-native-certs",
@ -2921,12 +2850,13 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"wtransport", "wtransport",
"zeroize",
] ]
[[package]] [[package]]
name = "mtp-type-map" name = "mtp-type-map"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a"
dependencies = [ dependencies = [
"serde", "serde",
"serde_yaml", "serde_yaml",
@ -2934,8 +2864,8 @@ dependencies = [
[[package]] [[package]]
name = "mtp-webserver" name = "mtp-webserver"
version = "0.2.0" version = "0.3.0"
source = "git+https://git.methanium.net/methanium/mtp.git?rev=7182272e3edb0079d7b74b45b2f62a298d2fee22#7182272e3edb0079d7b74b45b2f62a298d2fee22" source = "git+https://git.methanium.net/Methanium/mtp.git?rev=a5c8d4f0c898c78351e9d54124886c86e789a22a#a5c8d4f0c898c78351e9d54124886c86e789a22a"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"bytes", "bytes",
@ -2952,7 +2882,6 @@ dependencies = [
"mtp-host", "mtp-host",
"mtp-transport", "mtp-transport",
"quinn", "quinn",
"rand 0.10.2",
"rustls", "rustls",
"thiserror 2.0.19", "thiserror 2.0.19",
"tokio", "tokio",
@ -3456,9 +3385,9 @@ dependencies = [
[[package]] [[package]]
name = "pem-rfc7468" name = "pem-rfc7468"
version = "0.7.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9"
dependencies = [ dependencies = [
"base64ct", "base64ct",
] ]
@ -3539,24 +3468,14 @@ dependencies = [
"futures-io", "futures-io",
] ]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der 0.7.10",
"spki 0.7.3",
]
[[package]] [[package]]
name = "pkcs8" name = "pkcs8"
version = "0.11.0" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7"
dependencies = [ dependencies = [
"der 0.8.1", "der",
"spki 0.8.0", "spki",
] ]
[[package]] [[package]]
@ -3793,7 +3712,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -3973,13 +3892,14 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.28" version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
"futures-core", "futures-core",
"futures-util",
"http", "http",
"http-body", "http-body",
"http-body-util", "http-body-util",
@ -3993,45 +3913,12 @@ dependencies = [
"quinn", "quinn",
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"rustls-platform-verifier",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded",
"sync_wrapper", "sync_wrapper",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots",
]
[[package]]
name = "reqwest"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-util", "tokio-util",
"tower", "tower",
"tower-http", "tower-http",
@ -4101,7 +3988,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -4160,7 +4047,7 @@ dependencies = [
"security-framework", "security-framework",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -4313,26 +4200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
dependencies = [ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.1",
"cssparser 0.36.0", "cssparser",
"derive_more",
"log",
"new_debug_unreachable",
"phf",
"phf_codegen",
"precomputed-hash",
"rustc-hash",
"servo_arc",
"smallvec",
]
[[package]]
name = "selectors"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d"
dependencies = [
"bitflags 2.13.1",
"cssparser 0.37.0",
"derive_more", "derive_more",
"log", "log",
"new_debug_unreachable", "new_debug_unreachable",
@ -4461,18 +4329,6 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "3.21.0" version = "3.21.0"
@ -4608,15 +4464,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"rand_core 0.6.4",
]
[[package]] [[package]]
name = "signature" name = "signature"
version = "3.0.0" version = "3.0.0"
@ -4725,16 +4572,6 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der 0.7.10",
]
[[package]] [[package]]
name = "spki" name = "spki"
version = "0.8.0" version = "0.8.0"
@ -4742,7 +4579,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f"
dependencies = [ dependencies = [
"base64ct", "base64ct",
"der 0.8.1", "der",
] ]
[[package]] [[package]]
@ -4904,7 +4741,7 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
"url", "url",
"windows", "windows",
"windows-core 0.61.2", "windows-core",
"windows-version", "windows-version",
"x11-dl", "x11-dl",
] ]
@ -4955,13 +4792,13 @@ dependencies = [
"percent-encoding", "percent-encoding",
"plist", "plist",
"raw-window-handle", "raw-window-handle",
"reqwest 0.13.4", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"serde_repr", "serde_repr",
"serialize-to-javascript", "serialize-to-javascript",
"swift-rs", "swift-rs",
"tauri-build 2.6.3 (git+https://github.com/tauri-apps/tauri?rev=4af26a3f7f8b692d62cca549bbacd93f5ce90b41)", "tauri-build",
"tauri-macros", "tauri-macros",
"tauri-runtime", "tauri-runtime",
"tauri-runtime-wry", "tauri-runtime-wry",
@ -4996,26 +4833,6 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "tauri-build"
version = "2.6.3"
source = "git+https://github.com/tauri-apps/tauri?rev=c0bd0d5a61eedba5c4783add24455c5028c6f390#c0bd0d5a61eedba5c4783add24455c5028c6f390"
dependencies = [
"anyhow",
"cargo_toml",
"dirs",
"glob",
"heck 0.5.0",
"json-patch 4.2.0",
"schemars 0.8.22",
"semver",
"serde",
"serde_json",
"tauri-utils 2.9.3 (git+https://github.com/tauri-apps/tauri?rev=c0bd0d5a61eedba5c4783add24455c5028c6f390)",
"tauri-winres",
"walkdir",
]
[[package]] [[package]]
name = "tauri-codegen" name = "tauri-codegen"
version = "2.6.3" version = "2.6.3"
@ -5071,20 +4888,6 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "tauri-plugin-barcode-scanner"
version = "2.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3b68f0e3782b61a6e16a67380be40226e2b7233f3e36dadf6e5d03f07d2e7b3"
dependencies = [
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.19",
]
[[package]] [[package]]
name = "tauri-plugin-deep-link" name = "tauri-plugin-deep-link"
version = "2.4.9" version = "2.4.9"
@ -5103,7 +4906,7 @@ dependencies = [
"tracing", "tracing",
"url", "url",
"windows-registry", "windows-registry",
"windows-result 0.3.4", "windows-result",
] ]
[[package]] [[package]]
@ -5224,11 +5027,11 @@ dependencies = [
"anyhow", "anyhow",
"cargo_metadata", "cargo_metadata",
"ctor 0.8.0", "ctor 0.8.0",
"dom_query 0.27.0", "dom_query",
"dunce", "dunce",
"glob", "glob",
"http", "http",
"infer 0.19.0", "infer",
"json-patch 3.0.1", "json-patch 3.0.1",
"log", "log",
"memchr", "memchr",
@ -5260,11 +5063,11 @@ dependencies = [
"brotli", "brotli",
"cargo_metadata", "cargo_metadata",
"ctor 1.0.12", "ctor 1.0.12",
"dom_query 0.27.0", "dom_query",
"dunce", "dunce",
"glob", "glob",
"http", "http",
"infer 0.19.0", "infer",
"json-patch 4.2.0", "json-patch 4.2.0",
"log", "log",
"memchr", "memchr",
@ -5288,42 +5091,6 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "tauri-utils"
version = "2.9.3"
source = "git+https://github.com/tauri-apps/tauri?rev=c0bd0d5a61eedba5c4783add24455c5028c6f390#c0bd0d5a61eedba5c4783add24455c5028c6f390"
dependencies = [
"anyhow",
"cargo_metadata",
"ctor 1.0.12",
"dom_query 0.28.0",
"dunce",
"glob",
"http",
"infer 0.22.0",
"json-patch 4.2.0",
"log",
"memchr",
"phf",
"plist",
"proc-macro2",
"quote",
"regex",
"schemars 0.8.22",
"semver",
"serde",
"serde-untagged",
"serde_json",
"serde_with",
"swift-rs",
"thiserror 2.0.19",
"toml 1.1.4+spec-1.1.0",
"url",
"urlpattern 0.6.0",
"uuid",
"walkdir",
]
[[package]] [[package]]
name = "tauri-winres" name = "tauri-winres"
version = "0.3.6" version = "0.3.6"
@ -5353,10 +5120,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom 0.4.3", "getrandom 0.3.4",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -5370,18 +5137,16 @@ dependencies = [
[[package]] [[package]]
name = "tensamin" name = "tensamin"
version = "0.0.11" version = "0.0.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"jni 0.21.1", "jni 0.22.4",
"mtp", "mtp",
"mtp-transport", "reqwest",
"reqwest 0.12.28",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build 2.6.3 (git+https://github.com/tauri-apps/tauri?rev=c0bd0d5a61eedba5c4783add24455c5028c6f390)", "tauri-build",
"tauri-plugin-barcode-scanner",
"tauri-plugin-deep-link", "tauri-plugin-deep-link",
"tauri-plugin-log", "tauri-plugin-log",
"tauri-plugin-notification", "tauri-plugin-notification",
@ -6138,15 +5903,6 @@ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
[[package]]
name = "webpki-roots"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
dependencies = [
"rustls-pki-types",
]
[[package]] [[package]]
name = "webview2-com" name = "webview2-com"
version = "0.38.2" version = "0.38.2"
@ -6156,7 +5912,7 @@ dependencies = [
"webview2-com-macros", "webview2-com-macros",
"webview2-com-sys", "webview2-com-sys",
"windows", "windows",
"windows-core 0.61.2", "windows-core",
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
] ]
@ -6180,7 +5936,7 @@ checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
dependencies = [ dependencies = [
"thiserror 2.0.19", "thiserror 2.0.19",
"windows", "windows",
"windows-core 0.61.2", "windows-core",
] ]
[[package]] [[package]]
@ -6205,7 +5961,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -6236,7 +5992,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [ dependencies = [
"windows-collections", "windows-collections",
"windows-core 0.61.2", "windows-core",
"windows-future", "windows-future",
"windows-link 0.1.3", "windows-link 0.1.3",
"windows-numerics", "windows-numerics",
@ -6248,7 +6004,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [ dependencies = [
"windows-core 0.61.2", "windows-core",
] ]
[[package]] [[package]]
@ -6260,21 +6016,8 @@ dependencies = [
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
"windows-link 0.1.3", "windows-link 0.1.3",
"windows-result 0.3.4", "windows-result",
"windows-strings 0.4.2", "windows-strings",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
] ]
[[package]] [[package]]
@ -6283,7 +6026,7 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [ dependencies = [
"windows-core 0.61.2", "windows-core",
"windows-link 0.1.3", "windows-link 0.1.3",
"windows-threading", "windows-threading",
] ]
@ -6328,7 +6071,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [ dependencies = [
"windows-core 0.61.2", "windows-core",
"windows-link 0.1.3", "windows-link 0.1.3",
] ]
@ -6339,8 +6082,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [ dependencies = [
"windows-link 0.1.3", "windows-link 0.1.3",
"windows-result 0.3.4", "windows-result",
"windows-strings 0.4.2", "windows-strings",
] ]
[[package]] [[package]]
@ -6352,15 +6095,6 @@ dependencies = [
"windows-link 0.1.3", "windows-link 0.1.3",
] ]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link 0.2.1",
]
[[package]] [[package]]
name = "windows-strings" name = "windows-strings"
version = "0.4.2" version = "0.4.2"
@ -6370,15 +6104,6 @@ dependencies = [
"windows-link 0.1.3", "windows-link 0.1.3",
] ]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link 0.2.1",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.45.0" version = "0.45.0"
@ -6605,7 +6330,7 @@ dependencies = [
"cookie", "cookie",
"crossbeam-channel", "crossbeam-channel",
"dirs", "dirs",
"dom_query 0.27.0", "dom_query",
"dpi", "dpi",
"dunce", "dunce",
"gdkx11", "gdkx11",
@ -6633,7 +6358,7 @@ dependencies = [
"webkit2gtk-sys", "webkit2gtk-sys",
"webview2-com", "webview2-com",
"windows", "windows",
"windows-core 0.61.2", "windows-core",
"windows-version", "windows-version",
"x11-dl", "x11-dl",
] ]
@ -6701,7 +6426,7 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
dependencies = [ dependencies = [
"curve25519-dalek", "curve25519-dalek 4.1.3",
"rand_core 0.6.4", "rand_core 0.6.4",
"serde", "serde",
"zeroize", "zeroize",

View file

@ -15,17 +15,16 @@ name = "mobile_lib"
crate-type = ["staticlib", "cdylib", "rlib"] crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies] [build-dependencies]
tauri-build = { git = "https://github.com/tauri-apps/tauri", rev = "c0bd0d5a61eedba5c4783add24455c5028c6f390", features = [] } tauri-build = { git = "https://github.com/tauri-apps/tauri", rev = "4af26a3f7f8b692d62cca549bbacd93f5ce90b41", features = [] }
[dependencies] [dependencies]
tauri-plugin-opener = "2" tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
base64 = "0.22" base64 = "0.22"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] }
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
mtp = { git = "https://git.methanium.net/methanium/mtp.git", rev = "7182272e3edb0079d7b74b45b2f62a298d2fee22", features = ["client", "crypto"] } mtp = { git = "https://git.methanium.net/Methanium/mtp.git", rev = "a5c8d4f0c898c78351e9d54124886c86e789a22a", features = ["client", "crypto"] }
mtp-transport = { git = "https://git.methanium.net/methanium/mtp.git", rev = "7182272e3edb0079d7b74b45b2f62a298d2fee22" }
webpki-root-certs = "1" webpki-root-certs = "1"
tauri-plugin-deep-link = "2" tauri-plugin-deep-link = "2"
tauri-plugin-notification = "2" tauri-plugin-notification = "2"
@ -36,11 +35,13 @@ version = "2"
features = [] features = []
default-features = true default-features = true
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(not(target_os = "android"))'.dependencies.tauri]
jni = "0.21" version = "2"
features = []
default-features = true
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
tauri-plugin-barcode-scanner = "2" jni = "0.22"
[patch.crates-io.tauri] [patch.crates-io.tauri]
git = "https://github.com/tauri-apps/tauri" git = "https://github.com/tauri-apps/tauri"

View file

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

View file

@ -1,3 +1,4 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import java.util.Properties import java.util.Properties
import java.io.FileInputStream import java.io.FileInputStream
@ -61,14 +62,17 @@ android {
) )
} }
} }
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures { buildFeatures {
buildConfig = true buildConfig = true
} }
} }
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_1_8
}
}
rust { rust {
rootDirRel = "../../../" rootDirRel = "../../../"
} }

View file

@ -3,10 +3,9 @@ package net.tensamin.client
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Rect
import android.media.projection.MediaProjectionManager import android.media.projection.MediaProjectionManager
import android.os.Bundle import android.os.Bundle
import android.view.ViewGroup import android.view.MotionEvent
import android.view.ViewTreeObserver import android.view.ViewTreeObserver
import android.view.WindowManager import android.view.WindowManager
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
@ -56,7 +55,33 @@ class MainActivity : TauriActivity() {
} }
override fun onWebViewCreate(webView: WebView) { override fun onWebViewCreate(webView: WebView) {
webView.setInitialScale(290) 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 mediaWebView = webView
MobileMediaEvents.attach(webView) MobileMediaEvents.attach(webView)
webView.addJavascriptInterface(MobileMediaJavascriptInterface(), "tensaminMobileMedia") webView.addJavascriptInterface(MobileMediaJavascriptInterface(), "tensaminMobileMedia")
@ -65,11 +90,12 @@ class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, true) WindowCompat.setDecorFitsSystemWindows(window, true)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
super.onCreate(savedInstanceState)
NativeMtpBridge.nativeAttach(applicationContext)
if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) { if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) {
NativeMtpBridge.startService(this) NativeMtpBridge.startService(this)
} }
super.onCreate(savedInstanceState) NativeAccessibilityBridge.nativeAttach(applicationContext)
NativeMtpBridge.nativeAttach(applicationContext)
installKeyboardResizeWorkaround() installKeyboardResizeWorkaround()
} }
@ -90,7 +116,10 @@ class MainActivity : TauriActivity() {
attachLayoutListener = null attachLayoutListener = null
contentRoot = null contentRoot = null
contentChild = null contentChild = null
mediaWebView?.removeJavascriptInterface("tensaminMobileMedia") mediaWebView?.let {
NativeAccessibilityBridge.detach(it)
it.removeJavascriptInterface("tensaminMobileMedia")
}
mediaWebView = null mediaWebView = null
MobileMediaEvents.detach() MobileMediaEvents.detach()
super.onDestroy() super.onDestroy()
@ -203,16 +232,20 @@ class MainActivity : TauriActivity() {
child: android.view.View, child: android.view.View,
insets: WindowInsetsCompat? = ViewCompat.getRootWindowInsets(child), insets: WindowInsetsCompat? = ViewCompat.getRootWindowInsets(child),
) { ) {
val visibleFrame = Rect()
child.getWindowVisibleDisplayFrame(visibleFrame)
val rootHeight = child.rootView.height val rootHeight = child.rootView.height
if (rootHeight <= 0) return if (rootHeight <= 0) return
val imeHeight = insets?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0 val imeVisible = insets?.isVisible(WindowInsetsCompat.Type.ime()) == true
val keyboardHeight = maxOf(imeHeight, rootHeight - visibleFrame.bottom) val imeHeight = if (imeVisible) {
val keyboardVisible = keyboardHeight > rootHeight * 0.15 insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
val usableHeight = if (keyboardVisible) rootHeight - keyboardHeight else ViewGroup.LayoutParams.MATCH_PARENT } else {
0
}
val usableHeight = if (imeHeight in 1 until rootHeight) {
rootHeight - imeHeight
} else {
WindowManager.LayoutParams.MATCH_PARENT
}
if (previousUsableHeight == usableHeight) return if (previousUsableHeight == usableHeight) return

View file

@ -26,6 +26,7 @@ import android.os.HandlerThread
import android.os.IBinder import android.os.IBinder
import android.util.Base64 import android.util.Base64
import android.util.DisplayMetrics import android.util.DisplayMetrics
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -107,16 +108,12 @@ class MediaProjectionService : Service() {
stopIntent, stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
) )
val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
android.app.Notification.Builder(this, NOTIFICATION_CHANNEL_ID)
} else {
android.app.Notification.Builder(this)
}
.setSmallIcon(android.R.drawable.ic_menu_share) .setSmallIcon(android.R.drawable.ic_menu_share)
.setContentTitle("Tensamin is sharing your screen") .setContentTitle("Tensamin is sharing your screen")
.setContentText("Tap Stop to end screen sharing") .setContentText("Tap Stop to end screen sharing")
.setOngoing(true) .setOngoing(true)
.setCategory(android.app.Notification.CATEGORY_SERVICE) .setCategory(NotificationCompat.CATEGORY_SERVICE)
.addAction(android.R.drawable.ic_media_pause, "Stop", stopPendingIntent) .addAction(android.R.drawable.ic_media_pause, "Stop", stopPendingIntent)
.build() .build()

View file

@ -13,6 +13,8 @@ import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.Process
import android.os.SystemClock
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
class MtpForegroundService : Service() { class MtpForegroundService : Service() {
@ -74,10 +76,19 @@ class MtpForegroundService : Service() {
} }
override fun onTaskRemoved(rootIntent: Intent?) { 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)) { if (MtpSecureStore.isEnabled(this) && MtpSecureStore.hasConfig(this)) {
startService(Intent(this, MtpForegroundService::class.java)) startService(Intent(this, MtpForegroundService::class.java))
} }
super.onTaskRemoved(rootIntent) 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() { override fun onDestroy() {
@ -94,6 +105,9 @@ class MtpForegroundService : Service() {
private const val CHANNEL_ID = "tensamin-connection" private const val CHANNEL_ID = "tensamin-connection"
private const val NOTIFICATION_ID = 2201 private const val NOTIFICATION_ID = 2201
private const val ACTION_STOP = "net.tensamin.client.STOP_MTP" 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" @Volatile private var connectionStatus = "Connecting"
fun updateNotification(context: Context, status: String) { fun updateNotification(context: Context, status: String) {

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

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

View file

@ -18,6 +18,5 @@ repositories {
dependencies { dependencies {
compileOnly(gradleApi()) compileOnly(gradleApi())
implementation("com.android.tools.build:gradle:8.13.2") implementation("com.android.tools.build:gradle:8.11.0")
} }

View file

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

View file

@ -0,0 +1,135 @@
#[cfg(not(target_os = "android"))]
const DEFAULT_INITIAL_SCALE: i32 = 290;
const MIN_INITIAL_SCALE: i32 = 210;
const MAX_INITIAL_SCALE: i32 = 500;
#[tauri::command]
pub fn accessibility_get_initial_scale() -> Result<i32, String> {
android_get_initial_scale()
}
#[tauri::command]
pub fn accessibility_set_initial_scale(initial_scale: i32) -> Result<(), String> {
if !(MIN_INITIAL_SCALE..=MAX_INITIAL_SCALE).contains(&initial_scale) {
return Err(format!(
"Initial scale must be between {MIN_INITIAL_SCALE} and {MAX_INITIAL_SCALE}"
));
}
android_set_initial_scale(initial_scale)
}
#[cfg(not(target_os = "android"))]
fn android_get_initial_scale() -> Result<i32, String> {
Ok(DEFAULT_INITIAL_SCALE)
}
#[cfg(not(target_os = "android"))]
fn android_set_initial_scale(_: i32) -> Result<(), String> {
Ok(())
}
#[cfg(target_os = "android")]
mod android {
use std::sync::OnceLock;
use jni::{
jni_sig, jni_str,
objects::{Global, JClass, JObject, JValue},
Env, EnvUnowned, JavaVM,
};
struct Host {
vm: JavaVM,
context: Global<JObject<'static>>,
bridge: Global<JObject<'static>>,
}
static HOST: OnceLock<Host> = OnceLock::new();
fn attach(env: &mut Env, context: JObject) -> Result<(), String> {
if HOST.get().is_some() {
return Ok(());
}
let class = env
.find_class(jni_str!("net/tensamin/client/NativeAccessibilityBridge"))
.map_err(|error| error.to_string())?;
let bridge = env
.get_static_field(
class,
jni_str!("INSTANCE"),
jni_sig!("Lnet/tensamin/client/NativeAccessibilityBridge;"),
)
.and_then(|value| value.l())
.map_err(|error| error.to_string())?;
HOST.set(Host {
vm: env.get_java_vm().map_err(|error| error.to_string())?,
context: env
.new_global_ref(context)
.map_err(|error| error.to_string())?,
bridge: env
.new_global_ref(bridge)
.map_err(|error| error.to_string())?,
})
.map_err(|_| "Android accessibility host is already attached".to_string())
}
fn with_env<T>(call: impl FnOnce(&mut Env, &Host) -> Result<T, String>) -> Result<T, String> {
let host = HOST
.get()
.ok_or("Android accessibility host is not attached")?;
host.vm
.attach_current_thread(|env| Ok::<_, jni::errors::Error>(call(env, host)))
.map_err(|error| error.to_string())?
}
pub fn get_initial_scale() -> Result<i32, String> {
with_env(|env, host| {
env.call_method(
host.bridge.as_obj(),
jni_str!("getInitialScale"),
jni_sig!("(Landroid/content/Context;)I"),
&[JValue::Object(host.context.as_obj())],
)
.and_then(|value| value.i())
.map_err(|error| error.to_string())
})
}
pub fn set_initial_scale(initial_scale: i32) -> Result<(), String> {
with_env(|env, host| {
env.call_method(
host.bridge.as_obj(),
jni_str!("setInitialScale"),
jni_sig!("(Landroid/content/Context;I)V"),
&[
JValue::Object(host.context.as_obj()),
JValue::Int(initial_scale),
],
)
.map_err(|error| error.to_string())?;
Ok(())
})
}
#[no_mangle]
pub extern "system" fn Java_net_tensamin_client_NativeAccessibilityBridge_nativeAttach<
'caller,
>(
mut env: EnvUnowned<'caller>,
_class: JClass,
context: JObject<'caller>,
) {
let _ = env.with_env(|env| {
let _ = attach(env, context);
Ok::<_, jni::errors::Error>(())
});
}
}
#[cfg(target_os = "android")]
use android::{
get_initial_scale as android_get_initial_scale, set_initial_scale as android_set_initial_scale,
};

View file

@ -1,3 +1,4 @@
mod accessibility_backend;
mod mtp_backend; mod mtp_backend;
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
@ -14,12 +15,12 @@ pub fn run() {
.plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_opener::init()); .plugin(tauri_plugin_opener::init());
#[cfg(any(target_os = "ios", target_os = "android"))]
let builder = builder.plugin(tauri_plugin_barcode_scanner::init());
let app = builder let app = builder
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
accessibility_backend::accessibility_get_initial_scale,
accessibility_backend::accessibility_set_initial_scale,
mtp_backend::mtp_request, mtp_backend::mtp_request,
mtp_backend::mtp_send_sealed_relay,
mtp_backend::mtp_status, mtp_backend::mtp_status,
mtp_backend::mtp_store_credentials, mtp_backend::mtp_store_credentials,
mtp_backend::mtp_has_credentials, mtp_backend::mtp_has_credentials,

View file

@ -1,9 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!! // Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use lib::log;
fn main() { fn main() {
mobile_lib::run(); mobile_lib::run();
log("Test test 123")
} }

File diff suppressed because it is too large Load diff

View file

@ -1,38 +0,0 @@
import {
scan,
Format,
requestPermissions,
} from "@tauri-apps/plugin-barcode-scanner";
import { Button } from "@methanium/ui";
import { toast } from "@tensamin/shared/log";
export default function QrCodeScanner({
onData,
}: {
onData: (data: string) => void;
}) {
return (
<Button
onClick={() => {
requestPermissions()
.catch((err) => {
toast("error", err.message);
})
.then(() =>
scan({ windowed: false, formats: [Format.QRCode] })
.catch((err) => {
toast("error", err.message);
})
.then((data) => {
if (data) {
onData(data.content);
}
}),
);
}}
>
Open QR Code Scanner
</Button>
);
}

1
apps/web/.gitignore vendored
View file

@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules node_modules
.mtp
dist dist
dist-ssr dist-ssr
*.local *.local

View file

@ -2,8 +2,11 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" /> <link rel="icon" href="./favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
<title>Tensamin</title> <title>Tensamin</title>
</head> </head>
<body> <body>

View file

@ -6,141 +6,59 @@
"scripts": { "scripts": {
"format": "pnpm exec prettier --write .", "format": "pnpm exec prettier --write .",
"lint": "eslint src", "lint": "eslint src",
"dev": "vite --port 3000 --host 0.0.0.0", "dev": "vite",
"test": "vitest run --passWithNoTests", "test": "vitest run --passWithNoTests",
"build": "pnpm run test && tsc -b && vite build", "build": "pnpm --filter @tensamin/pwa build && pnpm run test && tsc -b && vite build",
"preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .." "preview": "cd dist && nix-shell -p python3 --run 'python3 -m http.server 3000' && cd .."
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.0.0", "@fontsource-variable/public-sans": "^5.3.0",
"@base-ui/utils": "0.3.2", "@methanium/ui": "*",
"@babel/runtime": "^7.29.2", "@tailwindcss/vite": "^4.3.3",
"@fontsource-variable/inter": "^5.2.8", "@tanstack/react-router": "^1.170.21",
"@fontsource-variable/public-sans": "^5.2.7", "@tanstack/react-virtual": "^3.14.9",
"@floating-ui/core": "^1.7.0", "@tauri-apps/api": "^2.11.1",
"@floating-ui/dom": "^1.7.0", "@tensamin/pwa": "workspace:*",
"@floating-ui/react-dom": "^2.1.8",
"@floating-ui/utils": "^0.2.11",
"@radix-ui/primitive": "^1.1.0",
"@radix-ui/react-compose-refs": "^1.1.1",
"@radix-ui/react-context": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dismissable-layer": "^1.1.11",
"@radix-ui/react-focus-guards": "^1.1.4",
"@radix-ui/react-focus-scope": "^1.1.11",
"@radix-ui/react-id": "^1.1.0",
"@radix-ui/react-portal": "^1.1.13",
"@radix-ui/react-presence": "^1.1.6",
"@radix-ui/react-primitive": "^2.0.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-use-callback-ref": "^1.1.1",
"@radix-ui/react-use-controllable-state": "^1.2.3",
"@radix-ui/react-use-effect-event": "^0.0.5",
"@radix-ui/react-use-layout-effect": "^1.1.0",
"@reduxjs/toolkit": "^2.0.0",
"@tailwindcss/vite": "^4.2.4",
"@tanstack/devtools-event-client": "^0.5.0",
"@tanstack/query-core": "^5.0.0",
"@tanstack/react-router": "^1.169.1",
"@tanstack/history": "1.162.0",
"@tanstack/react-store": "^0.11.0",
"@tanstack/router-core": "^1.169.1",
"@tanstack/store": "^0.11.0",
"@tanstack/virtual-core": "^3.13.24",
"@tanstack/react-virtual": "^3.13.24",
"@tauri-apps/api": "^2",
"@tensamin/call": "workspace:*",
"@tensamin/cache": "workspace:*", "@tensamin/cache": "workspace:*",
"@tensamin/call": "workspace:*",
"@tensamin/chat": "workspace:*", "@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
"@tensamin/hotkeys": "workspace:*", "@tensamin/hotkeys": "workspace:*",
"@tensamin/shared": "workspace:*",
"@tensamin/settings": "workspace:*",
"@tensamin/storage": "workspace:*",
"@tensamin/tauri": "workspace:*",
"@tensamin/mtp": "workspace:*",
"@tensamin/tauth": "workspace:*",
"@tensamin/markdown": "workspace:*", "@tensamin/markdown": "workspace:*",
"@methanium/ui": "*", "@tensamin/mtp": "workspace:*",
"@tensamin/user": "workspace:*",
"@tensamin/notifications": "workspace:*", "@tensamin/notifications": "workspace:*",
"@tensamin/onboarding": "workspace:*", "@tensamin/onboarding": "workspace:*",
"aria-hidden": "^1.2.4", "@tensamin/settings": "workspace:*",
"class-variance-authority": "^0.7.1", "@tensamin/shared": "workspace:*",
"clsx": "^2.1.1", "@tensamin/storage": "workspace:*",
"cmdk": "^1.1.1", "@tensamin/tauri": "workspace:*",
"cookie-es": "^3.0.0", "@tensamin/tauth": "workspace:*",
"d3-array": "^3.1.6", "@tensamin/identity": "workspace:*",
"d3-color": "^3.1.0",
"d3-ease": "^3.0.1",
"d3-format": "^3.1.0",
"d3-interpolate": "^3.0.1",
"d3-path": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-time-format": "^4.1.0",
"d3-timer": "^3.0.1",
"date-fns": "^4.4.0",
"decimal.js-light": "^2.5.1", "decimal.js-light": "^2.5.1",
"detect-node-es": "^1.1.0", "eventemitter3": "^5.0.4",
"dijkstrajs": "^1.0.1", "lucide-react": "^1.29.0",
"embla-carousel": "8.6.0", "react": "^19.2.8",
"embla-carousel-react": "^8.6.0", "react-dom": "^19.2.8",
"embla-carousel-reactive-utils": "8.6.0", "react-is": "^19.2.8",
"es-toolkit": "^1.39.3", "react-redux": "^9.3.0",
"eventemitter3": "^5.0.1",
"get-nonce": "^1.0.1",
"immer": "^10.1.1",
"input-otp": "^1.4.2",
"internmap": "^2.0.3",
"tw-animate-css": "^1.4.0",
"lucide-react": "^1.14.0",
"next-themes": "^0.4.6",
"pngjs": "^5.0.0",
"qrcode": "^1.5.4",
"react": "^19.2.0",
"react-day-picker": "^10.0.1",
"react-dom": "^19.2.0",
"react-is": "^19.0.0",
"react-redux": "^9.0.0",
"react-remove-scroll": "^2.7.2",
"react-remove-scroll-bar": "^2.3.7",
"react-resizable-panels": "^4.11.2",
"react-style-singleton": "^2.2.3",
"recharts": "3.10.1",
"redux": "^5.0.0",
"redux-thunk": "^3.1.0",
"reselect": "5.1.1",
"scheduler": "^0.27.0",
"seroval": "^1.5.4",
"seroval-plugins": "^1.5.4",
"shadcn": "^4.11.0",
"sonner": "^2.0.7",
"tailwindcss": "^4.2.4",
"tailwind-merge": "^3.6.0",
"tailwind-scrollbar-hide": "^4.0.0", "tailwind-scrollbar-hide": "^4.0.0",
"tiny-invariant": "^1.3.3", "tailwindcss": "^4.3.3",
"tslib": "^2.8.1", "tw-animate-css": "^1.4.0",
"use-callback-ref": "^1.3.3", "use-sync-external-store": "^1.6.0",
"use-sidecar": "^1.1.3", "zod": "^4.4.3"
"use-sync-external-store": "^1.2.2",
"vaul": "^1.1.2",
"victory-vendor": "^37.0.2",
"yargs": "^15.3.1",
"zod": "^4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@types/qrcode": "^1.5.6", "@types/qrcode": "^1.5.6",
"@types/react": "^19.2.2", "@types/react": "^19.2.18",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.0.1", "@vitejs/plugin-react": "^6.0.5",
"esbuild": "^0.28.0", "esbuild": "^0.28.1",
"eslint": "^10.0.3", "eslint": "^10.8.0",
"globals": "^17.4.0", "globals": "^17.9.0",
"mtp": "*",
"typescript": "~6.0.3", "typescript": "~6.0.3",
"typescript-eslint": "^8.57.0", "typescript-eslint": "^8.66.0",
"vite": "^8.0.10" "vite": "^8.2.1"
} }
} }

View file

@ -0,0 +1,8 @@
import { useCall } from "@tensamin/call/state";
import Popout from "@tensamin/call/popout";
export default function CallPopout() {
const active = useCall((state) => state.state !== "closed");
return active ? <Popout /> : null;
}

View file

@ -0,0 +1,95 @@
import { useEffect, useState } from "react";
import { useTheme } from "@methanium/ui";
import { useIsSpeaking } from "@tensamin/call/speakingState";
import { useCall, useInitializeCall } from "@tensamin/call/store";
import { useStorage } from "@tensamin/storage/context";
function createCallTrayIcon(color: string, speaking: boolean) {
const canvas = document.createElement("canvas");
canvas.width = 32;
canvas.height = 32;
const context = canvas.getContext("2d");
if (!context) return undefined;
context.globalAlpha = speaking ? 1 : 0.55;
context.fillStyle = color;
context.beginPath();
context.arc(16, 16, 13, 0, Math.PI * 2);
context.fill();
if (speaking) {
context.globalAlpha = 0.3;
context.fillStyle = "#ffffff";
context.fill();
}
return canvas.toDataURL("image/png");
}
export default function CallRuntimeInit() {
const callInvitePopup = useInitializeCall();
const { load } = useStorage();
const {
themeColor,
themePalette,
themePrimaryColor,
themePolarity,
themeTint,
themeCustomCss,
} = useTheme();
const [localUserId, setLocalUserId] = useState(-1);
const [primaryColor, setPrimaryColor] = useState("");
const inCall = useCall((state) => state.state === "open");
const speaking = useIsSpeaking(localUserId);
useEffect(() => {
let active = true;
load("user_id").then((userId) => {
if (active) setLocalUserId(userId);
});
return () => {
active = false;
};
}, [load]);
useEffect(() => {
const frame = requestAnimationFrame(() => {
setPrimaryColor(
getComputedStyle(document.documentElement)
.getPropertyValue("--primary")
.trim(),
);
});
return () => cancelAnimationFrame(frame);
}, [
themeColor,
themeCustomCss,
themePalette,
themePolarity,
themePrimaryColor,
themeTint,
]);
useEffect(() => {
const iconDataUrl = primaryColor
? createCallTrayIcon(primaryColor, speaking)
: undefined;
void window.tensaminDesktop?.call
?.setStatus?.({
inCall,
speaking: inCall && speaking,
iconDataUrl: inCall ? iconDataUrl : undefined,
})
.catch((error: unknown) => {
console.error("Failed to update desktop call status", error);
});
}, [inCall, primaryColor, speaking]);
return callInvitePopup;
}

View file

@ -0,0 +1,8 @@
import { useCall } from "@tensamin/call/state";
import SidebarBox from "@tensamin/call/sidebarBox";
export default function CallSidebarBox() {
const active = useCall((state) => state.state !== "closed");
return active ? <SidebarBox /> : null;
}

View file

@ -1,4 +1,4 @@
import type { User } from "@tensamin/user/context"; import type { User } from "@tensamin/identity/context";
import { import {
Avatar, Avatar,
AvatarImage, AvatarImage,
@ -15,7 +15,10 @@ export function Basic({
user, user,
extra, extra,
}: { }: {
user: User; user: Pick<
User,
"Display" | "Username" | "Avatar" | "OnlineStatus" | "Status"
>;
extra?: React.ReactNode; extra?: React.ReactNode;
}) { }) {
const display = user.Display || user.Username || "Unknown"; const display = user.Display || user.Username || "Unknown";
@ -57,8 +60,9 @@ export function Basic({
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
<div className="flex w-full flex-col items-start justify-center gap-1 text-[15px]"> <div className="flex w-full flex-col items-start justify-center text-[15px]">
<p>{display}</p> <p>{display}</p>
<p className="text-xs text-muted-foreground">{user.Status}</p>
</div> </div>
<div className="pr-1">{extra}</div> <div className="pr-1">{extra}</div>
</Button> </Button>

View file

@ -1,10 +1,23 @@
import type { User } from "@tensamin/user/context"; import type { User } from "@tensamin/identity/context";
import { Avatar, AvatarFallback, AvatarImage, Button } from "@methanium/ui"; import { Avatar, AvatarFallback, AvatarImage, Button } from "@methanium/ui";
import Text from "@tensamin/markdown/text"; import Text from "@tensamin/markdown/text";
import { ChevronDown, ChevronUp } from "lucide-react"; import { ChevronDown, ChevronUp } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
export default function Profile({ user }: { user: User }) { export default function Profile({
user,
}: {
user: Pick<
User,
| "UserId"
| "Display"
| "Username"
| "Avatar"
| "About"
| "IotaId"
| "PublicKey"
>;
}) {
const [showAdvancedInformation, setShowAdvancedInformation] = useState(false); const [showAdvancedInformation, setShowAdvancedInformation] = useState(false);
return ( return (

View file

@ -14,8 +14,8 @@ import {
User, User,
} from "lucide-react"; } from "lucide-react";
import { useLocation, useNavigate, useSearch } from "@tanstack/react-router"; import { useLocation, useNavigate, useSearch } from "@tanstack/react-router";
import { joinCall, useCall } from "@tensamin/call/store"; import { useCall } from "@tensamin/call/state";
import Wrapper from "@tensamin/user/wrapper"; import Wrapper from "@tensamin/identity/wrapper";
import { Skeleton } from "@methanium/ui"; import { Skeleton } from "@methanium/ui";
import { import {
Select, Select,
@ -27,7 +27,8 @@ import { displayCallId } from "@tensamin/call/utils";
import { useState } from "react"; import { useState } from "react";
import { SidebarTrigger, useSidebar } from "@methanium/ui"; import { SidebarTrigger, useSidebar } from "@methanium/ui";
import { WindowControls as Controls } from "@methanium/ui"; import { WindowControls as Controls } from "@methanium/ui";
import { useSession } from "@tensamin/storage/session"; import { useSession } from "@tensamin/identity/session";
import { joinCall } from "@tensamin/call/store";
import Profile from "./modals/profile"; import Profile from "./modals/profile";
export default function Navbar({ forMobile }: { forMobile: boolean }) { export default function Navbar({ forMobile }: { forMobile: boolean }) {
@ -52,6 +53,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
return ( return (
<div <div
data-tauri-drag-region data-tauri-drag-region
data-pwa-navbar
className={`${forMobile && "border-b"} pl-px w-full shrink-0 gap-2 h-13.5 flex items-center justify-between`} className={`${forMobile && "border-b"} pl-px w-full shrink-0 gap-2 h-13.5 flex items-center justify-between`}
> >
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
@ -91,6 +93,15 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
{pathname === "/chat" && id && ( {pathname === "/chat" && id && (
<Wrapper <Wrapper
userId={id} userId={id}
fields={[
"UserId",
"Display",
"Username",
"Avatar",
"About",
"IotaId",
"PublicKey",
]}
component={(user) => component={(user) =>
isMobile ? ( isMobile ? (
<p className="font-medium text-[1.07rem]">{user?.Display}</p> <p className="font-medium text-[1.07rem]">{user?.Display}</p>
@ -129,9 +140,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
{currentCalls.length === 0 ? ( {currentCalls.length === 0 ? (
<Button <Button
disabled={callState !== "closed"} disabled={callState !== "closed"}
onClick={() => { onClick={() => void joinCall(id)}
void joinCall(id);
}}
className="w-9 h-9! aspect-square rounded-lg" className="w-9 h-9! aspect-square rounded-lg"
variant="outline" variant="outline"
> >
@ -171,9 +180,9 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
<SelectItem <SelectItem
value={call.CallId} value={call.CallId}
key={call.CallId} key={call.CallId}
onSelect={() => { onSelect={() =>
void joinCall(id, call.CallSecret, call.CallId); void joinCall(id, call.CallSecret, call.CallId)
}} }
> >
{displayCallId(call.CallId)} {displayCallId(call.CallId)}
</SelectItem> </SelectItem>

View file

@ -0,0 +1,15 @@
import { Loader2 } from "lucide-react";
export default function RouteLoader() {
return (
<div
className="flex h-full min-h-40 w-full items-center justify-center bg-background"
role="status"
>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Loader2 className="size-5 animate-spin" />
<span>Loading...</span>
</div>
</div>
);
}

View file

@ -13,8 +13,11 @@ import {
useState, useState,
} from "react"; } from "react";
import { z } from "zod"; import { z } from "zod";
import { invoke, isTauri } from "@tauri-apps/api/core"; import { subscribeTuFileLaunch } from "@tensamin/pwa/runtime";
import QrCodeScanner from "@tensamin/tauri/qrCodeScanner"; import {
parseTuFileContent,
persistMtpCredentials,
} from "@tensamin/storage/credentials";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
const fetchedUser = z.object({ const fetchedUser = z.object({
@ -35,55 +38,6 @@ const formSchema = z.object({
mtp_keyring: z.string().min(1).max(92), mtp_keyring: z.string().min(1).max(92),
}); });
/**
* Parses a .tu file payload into credentials.
* @param rawFileContent UTF-8 file content from an uploaded .tu file.
* @returns Parsed user id and private key credentials.
*/
function parseTuFileContent(rawFileContent: string): {
userId: number;
privateKey: string;
domain: string | null;
} {
if (rawFileContent.trim().length === 0) {
throw new Error("File is empty");
} else if (!rawFileContent.includes("::")) {
throw new Error("Invalid file");
} else if (rawFileContent.split("::").length !== 2) {
throw new Error("Invalid file");
}
const left = rawFileContent.split("::")[0];
const right = rawFileContent.split("::")[1];
if (left.length === 0) {
throw new Error("Invalid file");
} else if (right.length === 0) {
throw new Error("Invalid file");
} else if (isNaN(Number(left)) && !left.includes("@")) {
throw new Error("Invalid file");
}
const [userIdString, privateKeyValue] = rawFileContent.split("::");
const privateKey = privateKeyValue.trim();
const userId = isNaN(Number(userIdString))
? Number(userIdString.split("@")[0])
: Number(userIdString);
const domain = userIdString.includes("@") ? userIdString.split("@")[1] : null;
if (!userId || !privateKey) {
throw new Error("Invalid file");
}
console.log({
domain,
userId,
});
return { userId, privateKey, domain };
}
export default function Form() { export default function Form() {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const uploadRef = useRef<HTMLInputElement | null>(null); const uploadRef = useRef<HTMLInputElement | null>(null);
@ -97,27 +51,12 @@ export default function Form() {
if (loginPendingRef.current) return false; if (loginPendingRef.current) return false;
loginPendingRef.current = true; loginPendingRef.current = true;
try { try {
if (domain) await save("omega_url", `https://${domain}/`); await persistMtpCredentials({
if (isTauri()) { storage: { load, save },
const [omegaUrl, forcedOmikronUrl, forcedOmikronPublicKey] = userId,
await Promise.all([ keyring: privateKey,
domain ? `https://${domain}/` : load("omega_url"), domain,
load("forced_omikron_url"), });
load("forced_omikron_public_key"),
]);
await invoke("mtp_store_credentials", {
config: {
userId,
keyring: privateKey,
omegaUrl,
forcedOmikronUrl,
forcedOmikronPublicKey,
},
});
}
await save("mtp_keyring", privateKey, { secure: true });
await save("session_id", Date.now());
await save("user_id", userId);
await navigate({ to: "/", replace: true }); await navigate({ to: "/", replace: true });
return true; return true;
} finally { } finally {
@ -148,6 +87,11 @@ export default function Form() {
[persistLogin], [persistLogin],
); );
useEffect(
() => subscribeTuFileLaunch(processDroppedFile),
[processDroppedFile],
);
// Handle .tu files // Handle .tu files
const handleFileInputChange = useCallback( const handleFileInputChange = useCallback(
async (event: ChangeEvent<HTMLInputElement>): Promise<void> => { async (event: ChangeEvent<HTMLInputElement>): Promise<void> => {
@ -286,32 +230,10 @@ export default function Form() {
return ( return (
<div className="relative flex md:flex-row flex-col gap-15"> <div className="relative flex md:flex-row flex-col gap-15">
{isTauri() && isMobile ? ( {isMobile ? (
<> <Button onClick={() => uploadRef.current?.click()}>
<QrCodeScanner Select .tu file
onData={async (data) => { </Button>
if (!data.startsWith("tensamin://tu::")) {
toast("error", "Invalid QR code");
return;
}
const decoded = data.replace("tensamin://tu::", "");
try {
const { userId, privateKey, domain } =
parseTuFileContent(decoded);
await persistLogin(userId, privateKey, domain);
} catch (error) {
log(0, "login", "red", error);
toast("error", "Failed to parse QR code data");
}
}}
/>
<Button onClick={() => uploadRef.current?.click()}>
Select .tu file
</Button>
</>
) : ( ) : (
<div <div
onClick={() => uploadRef.current?.click()} onClick={() => uploadRef.current?.click()}

View file

@ -1,5 +1,6 @@
import Wrapper from "@tensamin/user/wrapper"; import Wrapper from "@tensamin/identity/wrapper";
import { Basic, Loading } from "./modals/basic"; import { Basic, Loading } from "./modals/basic";
import CallSidebarBox from "./callSidebarBox";
import List from "@/features/conversation/list/body"; import List from "@/features/conversation/list/body";
import { import {
@ -27,32 +28,26 @@ import {
SelectContent, SelectContent,
SelectItem, SelectItem,
} from "@methanium/ui"; } from "@methanium/ui";
import { isTauri } from "@tauri-apps/api/core";
import { useIsMobile } from "@methanium/ui"; import { useIsMobile } from "@methanium/ui";
import { MobileNavbar } from "./navbar"; import { MobileNavbar } from "./navbar";
import SidebarBox from "@tensamin/call/sidebarBox";
import { useShowMobileNavbar } from "@/routes/app/useShowMobileNavbar"; import { useShowMobileNavbar } from "@/routes/app/useShowMobileNavbar";
import { Ellipsis, Check } from "lucide-react"; import { Ellipsis, Check } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import type { User } from "@tensamin/user/context"; import { useUser, type User } from "@tensamin/identity/context";
import type z from "zod"; import { mtp, userPresencePreferenceSchema } from "@tensamin/shared/data";
import { mtp } from "@tensamin/shared/data";
import { useMTP } from "@tensamin/mtp"; import { useMTP } from "@tensamin/mtp";
import {
onlineStatusLabels,
onlineStatusOptions,
type OnlineStatus,
} from "./status-options";
type OnlineStatus = z.infer<typeof mtp.GetUserData.response.shape.OnlineStatus>; function accountPreference(status: User["OnlineStatus"]): OnlineStatus {
return userPresencePreferenceSchema.safeParse(status).success
const onlineStatusLabels: Record<OnlineStatus, string> = { ? (status as OnlineStatus)
user_online: "Online", : "user_online";
user_offline: "Offline", }
user_dnd: "Do not disturb",
user_idle: "Idle",
user_wc: "Away",
user_borked: "Borked",
iota_offline: "Iota offline",
iota_online: "Iota online",
iota_borked: "Iota borked",
};
function StatusDialog({ function StatusDialog({
user, user,
@ -68,7 +63,7 @@ function StatusDialog({
saveSucceeded, saveSucceeded,
setSaveSucceeded, setSaveSucceeded,
}: { }: {
user: User; user: Pick<User, "UserId" | "OnlineStatus" | "Status">;
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
send: ReturnType<typeof useMTP>["send"]; send: ReturnType<typeof useMTP>["send"];
@ -81,13 +76,15 @@ function StatusDialog({
saveSucceeded: boolean; saveSucceeded: boolean;
setSaveSucceeded: (value: boolean) => void; setSaveSucceeded: (value: boolean) => void;
}) { }) {
const { updateProfile, updateState } = useUser();
return ( return (
<Dialog <Dialog
open={open} open={open}
onOpenChange={(nextOpen) => { onOpenChange={(nextOpen) => {
if (!nextOpen) { if (!nextOpen) {
setDraftStatus(user.Status ?? ""); setDraftStatus(user.Status ?? "");
setDraftOnlineStatus(user.OnlineStatus); setDraftOnlineStatus(accountPreference(user.OnlineStatus));
setErrorMessage(""); setErrorMessage("");
setSaveSucceeded(false); setSaveSucceeded(false);
} }
@ -126,10 +123,11 @@ function StatusDialog({
</SelectValue> </SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent className="p-1"> <SelectContent className="p-1">
<SelectItem value="user_online">Online</SelectItem> {onlineStatusOptions.map((option) => (
<SelectItem value="user_offline">Offline</SelectItem> <SelectItem key={option.value} value={option.value}>
<SelectItem value="user_idle">Idle</SelectItem> {option.label}
<SelectItem value="user_dnd">Do not disturb</SelectItem> </SelectItem>
))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -140,12 +138,10 @@ function StatusDialog({
<DialogClose render={<Button variant="destructive">Cancel</Button>} /> <DialogClose render={<Button variant="destructive">Cancel</Button>} />
<Button <Button
onClick={async () => { onClick={async () => {
const payload = { const validation = mtp.ChangeUserData.request.safeParse({
...(draftStatus && { status: draftStatus }),
OnlineStatus: draftOnlineStatus, OnlineStatus: draftOnlineStatus,
}; Status: draftStatus,
});
const validation = mtp.ChangeUserData.request.safeParse(payload);
if (!validation.success) { if (!validation.success) {
setSaveSucceeded(false); setSaveSucceeded(false);
@ -157,6 +153,8 @@ function StatusDialog({
try { try {
await send("ChangeUserData", validation.data); await send("ChangeUserData", validation.data);
updateState(user.UserId, draftOnlineStatus);
await updateProfile(user.UserId, { Status: draftStatus });
setSaveSucceeded(true); setSaveSucceeded(true);
setErrorMessage(""); setErrorMessage("");
} catch (err) { } catch (err) {
@ -195,16 +193,20 @@ export default function Sidebar() {
const content = ( const content = (
<> <>
<SidebarContent <SidebarContent className="pt-2">
className={
isTauri() && isMobile ? "pt-[env(safe-area-inset-top)]" : "pt-2"
}
>
<div className="h-full w-full flex flex-col gap-3 p-2 pt-0!"> <div className="h-full w-full flex flex-col gap-3 p-2 pt-0!">
<div> <div>
<Wrapper <Wrapper
loading={<Loading />} loading={<Loading />}
userId={"own"} userId={"own"}
fields={[
"UserId",
"Display",
"Username",
"Avatar",
"OnlineStatus",
"Status",
]}
component={(user) => ( component={(user) => (
<> <>
<Basic <Basic
@ -227,7 +229,9 @@ export default function Sidebar() {
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
setDraftStatus(user.Status ?? ""); setDraftStatus(user.Status ?? "");
setDraftOnlineStatus(user.OnlineStatus); setDraftOnlineStatus(
accountPreference(user.OnlineStatus),
);
setStatusErrorMessage(""); setStatusErrorMessage("");
setStatusSaveSucceeded(false); setStatusSaveSucceeded(false);
setDialogOpen(true); setDialogOpen(true);
@ -246,7 +250,9 @@ export default function Sidebar() {
onOpenChange={(nextOpen) => { onOpenChange={(nextOpen) => {
if (!nextOpen) { if (!nextOpen) {
setDraftStatus(user.Status ?? ""); setDraftStatus(user.Status ?? "");
setDraftOnlineStatus(user.OnlineStatus); setDraftOnlineStatus(
accountPreference(user.OnlineStatus),
);
setStatusErrorMessage(""); setStatusErrorMessage("");
setStatusSaveSucceeded(false); setStatusSaveSucceeded(false);
} }
@ -279,7 +285,7 @@ export default function Sidebar() {
)} )}
{!isMobile && ( {!isMobile && (
<SidebarFooter> <SidebarFooter>
<SidebarBox /> <CallSidebarBox />
</SidebarFooter> </SidebarFooter>
)} )}
</> </>
@ -298,7 +304,7 @@ export default function Sidebar() {
data-sidebar="sidebar" data-sidebar="sidebar"
data-slot="sidebar" data-slot="sidebar"
data-mobile="true" data-mobile="true"
className="fixed inset-y-0 left-0 z-50 w-screen bg-sidebar p-0 text-sidebar-foreground" className="fixed inset-y-0 left-0 z-50 w-screen bg-sidebar pt-[var(--ui-safe-area-top)] pr-[var(--ui-safe-area-right)] pl-[var(--ui-safe-area-left)] text-sidebar-foreground"
style={{ style={{
transform: openMobile ? "translateX(0)" : "translateX(-100%)", transform: openMobile ? "translateX(0)" : "translateX(-100%)",
opacity: openMobile ? 1 : 0, opacity: openMobile ? 1 : 0,

View file

@ -0,0 +1,13 @@
export const onlineStatusOptions = [
{ label: "Online", value: "user_online" },
{ label: "Idle", value: "user_idle" },
{ label: "Do not disturb", value: "user_dnd" },
{ label: "On the toilet", value: "user_wc" },
{ label: "Offline", value: "user_invisible" },
] as const;
export type OnlineStatus = (typeof onlineStatusOptions)[number]["value"];
export const onlineStatusLabels = Object.fromEntries(
onlineStatusOptions.map((option) => [option.value, option.label]),
) as Record<OnlineStatus, string>;

View file

@ -5,7 +5,7 @@ import Switch from "./switch";
import ConversationModal from "../modal/conversation"; import ConversationModal from "../modal/conversation";
import CommunityModal from "../modal/community"; import CommunityModal from "../modal/community";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { useSession } from "@tensamin/storage/session"; import { useSession } from "@tensamin/identity/session";
export default function List() { export default function List() {
const [category, setCategory] = useState<"conversations" | "communities">( const [category, setCategory] = useState<"conversations" | "communities">(

View file

@ -1,5 +1,5 @@
import { Basic, Loading } from "@/components/modals/basic"; import { Basic, Loading } from "@/components/modals/basic";
import Wrapper from "@tensamin/user/wrapper"; import Wrapper from "@tensamin/identity/wrapper";
import { import {
ContextMenu, ContextMenu,
ContextMenuContent, ContextMenuContent,
@ -28,6 +28,13 @@ export default function ConversationModal({ userId }: { userId: number }) {
<Wrapper <Wrapper
loading={<Loading />} loading={<Loading />}
userId={userId} userId={userId}
fields={[
"Display",
"Username",
"Avatar",
"OnlineStatus",
"Status",
]}
component={(user) => <Basic user={user} />} component={(user) => <Basic user={user} />}
/> />
</div> </div>

View file

@ -12,25 +12,17 @@ import "./index.css";
import "@methanium/ui/index.css"; import "@methanium/ui/index.css";
import NotFound from "@/routes/404"; import NotFound from "@/routes/404";
import RouteLoader from "@/components/routeLoader";
import AppLayout from "@/routes/app/layout";
import { createSettingsRoute } from "@tensamin/settings";
import OnboardingGate from "@tensamin/onboarding";
import Home from "@/routes/app/home"; import Home from "@/routes/app/home";
import ChatScreen from "@tensamin/chat/screen"; import AppShell from "@/routes/app/shell";
import CallScreen from "@tensamin/call/screen";
import Login from "@/routes/screens/login"; import Login from "@/routes/screens/login";
import ChatContext from "@tensamin/chat/context"; import { createSettingsRoute } from "@tensamin/settings";
import { useCall, useInitializeCall } from "@tensamin/call/store"; import ChatScreen from "@tensamin/chat/screen";
import { useIsSpeaking } from "@tensamin/call/speakingState"; import CallScreen from "@tensamin/call/screen";
import { Provider as MTPProvider } from "@tensamin/mtp";
import UserProvider from "@tensamin/user/context";
import DeeplinkContext, { useDeeplinks } from "@tensamin/tauri/deeplinkHandler";
import NotificationsProvider from "@tensamin/notifications/context";
import TAuthWrapper from "@tensamin/tauth/context"; import DeeplinkContext from "@tensamin/tauri/deeplinkHandler";
import PwaRuntime from "@tensamin/pwa/runtime";
import { ErrorScreen, ThemeProvider, useTheme } from "@methanium/ui"; import { ErrorScreen, ThemeProvider, useTheme } from "@methanium/ui";
import z from "zod"; import z from "zod";
@ -38,17 +30,11 @@ import z from "zod";
import { useEffect, useRef, useState, type ReactNode } from "react"; import { useEffect, useRef, useState, type ReactNode } from "react";
import Storage from "@tensamin/storage/context"; import Storage from "@tensamin/storage/context";
import Session from "@tensamin/storage/session";
import Crypto from "@tensamin/crypto/context";
import DesktopMediaProvider from "@tensamin/shared/desktopMedia";
import { log } from "@tensamin/shared/log"; import { log } from "@tensamin/shared/log";
import CacheSync from "@tensamin/cache/sync";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
import { useLocation, useNavigate } from "@tanstack/react-router"; import { useLocation, useNavigate } from "@tanstack/react-router";
import { useIsMobile, Toaster, TooltipProvider } from "@methanium/ui"; import { useIsMobile, Toaster, TooltipProvider } from "@methanium/ui";
import { isTauri } from "@tauri-apps/api/core";
import { HotkeysProvider } from "@tensamin/hotkeys"; import { HotkeysProvider } from "@tensamin/hotkeys";
const wrapper = document.getElementById("root"); const wrapper = document.getElementById("root");
@ -108,7 +94,7 @@ function LoginWrapper({ children }: { children: ReactNode }) {
}, [load, location.pathname, navigate, secureStorage]); }, [load, location.pathname, navigate, secureStorage]);
if (loggedIn !== true && location.pathname !== "/login") { if (loggedIn !== true && location.pathname !== "/login") {
return null; return <RouteLoader />;
} }
return children; return children;
@ -254,19 +240,14 @@ function RootShell() {
parentThemeStorageKey={null} parentThemeStorageKey={null}
designStorageKey={null} designStorageKey={null}
> >
<div className="w-screen h-dvh overflow-hidden"> <div
<Toaster data-pwa-root
position={isMobile ? "top-center" : "bottom-right"} className="box-border w-screen h-dvh overflow-hidden"
{...(isTauri() && isMobile >
? { <Toaster position={isMobile ? "top-center" : "bottom-right"} />
mobileOffset: {
top: "env(safe-area-inset-top)",
},
}
: {})}
/>
<TooltipProvider> <TooltipProvider>
<Storage> <Storage>
<PwaRuntime />
<HotkeysProvider> <HotkeysProvider>
<ThemeStorageBridge /> <ThemeStorageBridge />
<LoginWrapper> <LoginWrapper>
@ -281,155 +262,6 @@ function RootShell() {
); );
} }
function AppShell() {
return (
<OnboardingGate>
<Crypto>
<DesktopMediaProvider>
<MTPProvider>
<CacheSync />
<DeeplinkNavigator />
<Session>
<UserProvider>
<CallInit />
<TAuthWrapper>
<AppLayout>
<ChatContext>
<NotificationsProvider>
<Outlet />
</NotificationsProvider>
</ChatContext>
</AppLayout>
</TAuthWrapper>
</UserProvider>
</Session>
</MTPProvider>
</DesktopMediaProvider>
</Crypto>
</OnboardingGate>
);
}
function DeeplinkNavigator() {
const { deeplinks } = useDeeplinks();
const navigate = useNavigate();
const handledCount = useRef(0);
useEffect(() => {
const links = deeplinks.slice(handledCount.current);
handledCount.current = deeplinks.length;
for (const link of links) {
try {
const url = new URL(link);
const id = Number(url.searchParams.get("id"));
if (
url.protocol === "tensamin:" &&
url.hostname === "chat" &&
Number.isSafeInteger(id) &&
id > 0
) {
void navigate({ to: "/chat", search: { id } });
}
} catch {
// Ignore malformed URLs delivered by the platform.
}
}
}, [deeplinks, navigate]);
return null;
}
function createCallTrayIcon(color: string, speaking: boolean) {
const canvas = document.createElement("canvas");
canvas.width = 32;
canvas.height = 32;
const context = canvas.getContext("2d");
if (!context) return undefined;
context.globalAlpha = speaking ? 1 : 0.55;
context.fillStyle = color;
context.beginPath();
context.arc(16, 16, 13, 0, Math.PI * 2);
context.fill();
if (speaking) {
context.globalAlpha = 0.3;
context.fillStyle = "#ffffff";
context.fill();
}
return canvas.toDataURL("image/png");
}
function CallInit() {
const callInvitePopup = useInitializeCall();
const { load } = useStorage();
const {
themeColor,
themePalette,
themePrimaryColor,
themePolarity,
themeTint,
themeCustomCss,
} = useTheme();
const [localUserId, setLocalUserId] = useState(-1);
const [primaryColor, setPrimaryColor] = useState("");
const inCall = useCall((state) => state.state === "open");
const speaking = useIsSpeaking(localUserId);
useEffect(() => {
let active = true;
load("user_id").then((userId) => {
if (active) setLocalUserId(userId);
});
return () => {
active = false;
};
}, [load]);
useEffect(() => {
const frame = requestAnimationFrame(() => {
setPrimaryColor(
getComputedStyle(document.documentElement)
.getPropertyValue("--primary")
.trim(),
);
});
return () => cancelAnimationFrame(frame);
}, [
themeColor,
themeCustomCss,
themePalette,
themePolarity,
themePrimaryColor,
themeTint,
]);
useEffect(() => {
const iconDataUrl = primaryColor
? createCallTrayIcon(primaryColor, speaking)
: undefined;
void window.tensaminDesktop?.call
?.setStatus?.({
inCall,
speaking: inCall && speaking,
iconDataUrl: inCall ? iconDataUrl : undefined,
})
.catch((error: unknown) => {
console.error("Failed to update desktop call status", error);
});
}, [inCall, primaryColor, speaking]);
return callInvitePopup;
}
const rootRoute = createRootRoute({ const rootRoute = createRootRoute({
component: RootShell, component: RootShell,
errorComponent: ({ error }: { error: Error }) => ( errorComponent: ({ error }: { error: Error }) => (

View file

@ -11,13 +11,16 @@ import {
useIsMobile, useIsMobile,
} from "@methanium/ui"; } from "@methanium/ui";
import z from "zod"; import z from "zod";
import { useMTP } from "@tensamin/mtp"; import { MTPProtocolError } from "mtp";
import { RelayRejectedError, requireRelaySuccess, useMTP } from "@tensamin/mtp";
import { log } from "@tensamin/shared/log";
import { useState } from "react"; import { useState } from "react";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
import { isTauri } from "@tauri-apps/api/core"; import { isTauri } from "@tauri-apps/api/core";
import { useSession } from "@tensamin/storage/session"; import { useSession } from "@tensamin/identity/session";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
import { ShieldAlert } from "lucide-react"; import { ShieldAlert } from "lucide-react";
import { useUser } from "@tensamin/identity/context";
// The page // The page
export default function Page() { export default function Page() {
@ -51,8 +54,10 @@ export default function Page() {
// Add Conversation Button Component // Add Conversation Button Component
function AddConversationButton() { function AddConversationButton() {
const { send } = useMTP(); const { send, sendSealedRelay } = useMTP();
const { contacts, insertContact } = useSession(); const { contacts, insertContact } = useSession();
const { load } = useStorage();
const { getIota } = useUser();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -64,7 +69,7 @@ function AddConversationButton() {
// username check // username check
const schema = z const schema = z
.string() .string()
.min(1, "Username is too short") .regex(/^[a-z0-9]+$/, "Username must use lowercase letters and numbers")
.max(15, "Username is too long"); .max(15, "Username is too long");
const result = schema.safeParse(username?.toLowerCase().trim()); const result = schema.safeParse(username?.toLowerCase().trim());
@ -74,22 +79,27 @@ function AddConversationButton() {
return; return;
} }
// user existence check let user;
const user = await send("GetUserData", { try {
Username: result.data, user = await send("GetUserData", { Username: result.data });
}) } catch (error) {
.then((data) => { if (error instanceof MTPProtocolError && error.type === "ErrorNotFound") {
if (data.type === "ErrorNotFound" || data.data.UserId === 0) {
throw new Error();
}
return data;
})
.catch(() => {
setError("User not found"); setError("User not found");
return; } else if (error instanceof MTPProtocolError) {
}); setError(`User lookup failed: ${error.type}`);
if (!user) return; } else {
setError("User lookup failed: connection error");
}
return;
}
if (user.type === "ErrorNotFound") {
setError("User not found");
return;
}
if (user.type !== "GetUserData") {
setError(`User lookup failed: ${user.type}`);
return;
}
// alrady added check // alrady added check
if (contacts.some((contact) => contact.UserId === user.data.UserId)) { if (contacts.some((contact) => contact.UserId === user.data.UserId)) {
@ -100,25 +110,36 @@ function AddConversationButton() {
// add the conv // add the conv
const timeout = setTimeout(() => setLoading(true), 500); const timeout = setTimeout(() => setLoading(true), 500);
send("AddConversation", { try {
ChatPartnerId: user.data.UserId, const userId = await load("user_id");
}) const iota = await getIota(userId);
.then(() => { const response = await sendSealedRelay(
insertContact(user.data.UserId); "AddConversation",
setOpen(false); { ChatPartnerId: user.data.UserId },
}) {
.catch((error) => { nextHop: { kind: "iota", id: iota.IotaId },
if (String(error).includes("error_not_found")) { finalRecipientId: userId,
setError("User not found"); metadataRecipients: [{ value: iota.PublicKey, encoding: "base64" }],
return; contentRecipients: [{ value: iota.PublicKey, encoding: "base64" }],
} },
);
setError(String(error)); requireRelaySuccess(response);
}) insertContact(user.data.UserId);
.finally(() => { setOpen(false);
clearTimeout(timeout); } catch (error) {
setLoading(false); if (error instanceof RelayRejectedError) {
}); setError(
`Could not route the request to your Iota: ${error.responseType}`,
);
} else {
log(1, "mtp", "red", "Add conversation failed", error);
const detail = error instanceof Error ? error.message : "unknown error";
setError(`Add conversation failed: ${detail}`);
}
} finally {
clearTimeout(timeout);
setLoading(false);
}
} }
return ( return (

View file

@ -2,8 +2,8 @@ import { type ReactNode } from "react";
import Sidebar from "@/components/sidebar"; import Sidebar from "@/components/sidebar";
import Navbar, { MobileNavbar } from "@/components/navbar"; import Navbar, { MobileNavbar } from "@/components/navbar";
import CallPopout from "@/components/callPopout";
import { useShowMobileNavbar } from "./useShowMobileNavbar"; import { useShowMobileNavbar } from "./useShowMobileNavbar";
import CallPopout from "@tensamin/call/popout";
import { useIsMobile, cn, SidebarProvider } from "@methanium/ui"; import { useIsMobile, cn, SidebarProvider } from "@methanium/ui";
@ -19,6 +19,7 @@ export default function Layout({ children }: { children: ReactNode }) {
<Sidebar /> <Sidebar />
<CallPopout /> <CallPopout />
<div <div
data-app-layout
// Background of ui that is overlapping with the system ui // Background of ui that is overlapping with the system ui
className={cn( className={cn(
"w-full h-full min-h-0 flex flex-col overflow-hidden", "w-full h-full min-h-0 flex flex-col overflow-hidden",

View file

@ -0,0 +1,75 @@
import { useEffect, useRef } from "react";
import { Outlet, useNavigate } from "@tanstack/react-router";
import AppLayout from "./layout";
import CallInit from "@/components/callRuntimeInit";
import CacheSync from "@tensamin/cache/sync";
import ChatContext from "@tensamin/chat/context";
import Crypto from "@tensamin/crypto/context";
import UserProvider from "@tensamin/identity/context";
import { Provider as MTPProvider } from "@tensamin/mtp";
import NotificationsProvider from "@tensamin/notifications/context";
import OnboardingGate from "@tensamin/onboarding";
import DesktopMediaProvider from "@tensamin/shared/desktopMedia";
import Session from "@tensamin/identity/session";
import { useDeeplinks } from "@tensamin/tauri/deeplinkHandler";
import TAuthWrapper from "@tensamin/tauth/context";
function DeeplinkNavigator() {
const { deeplinks } = useDeeplinks();
const navigate = useNavigate();
const handledCount = useRef(0);
useEffect(() => {
const links = deeplinks.slice(handledCount.current);
handledCount.current = deeplinks.length;
for (const link of links) {
try {
const url = new URL(link);
const id = Number(url.searchParams.get("id"));
if (
url.protocol === "tensamin:" &&
url.hostname === "chat" &&
Number.isSafeInteger(id) &&
id > 0
) {
void navigate({ to: "/chat", search: { id } });
}
} catch {
// Ignore malformed URLs delivered by the platform.
}
}
}, [deeplinks, navigate]);
return null;
}
export default function AppShell() {
return (
<OnboardingGate>
<Crypto>
<DesktopMediaProvider>
<MTPProvider>
<CacheSync />
<DeeplinkNavigator />
<Session>
<UserProvider>
<CallInit />
<TAuthWrapper>
<AppLayout>
<ChatContext>
<NotificationsProvider>
<Outlet />
</NotificationsProvider>
</ChatContext>
</AppLayout>
</TAuthWrapper>
</UserProvider>
</Session>
</MTPProvider>
</DesktopMediaProvider>
</Crypto>
</OnboardingGate>
);
}

View file

@ -1,4 +1,4 @@
import { createReadStream, realpathSync, statSync } from "node:fs"; import { createReadStream, statSync } from "node:fs";
import type { IncomingMessage, ServerResponse } from "node:http"; import type { IncomingMessage, ServerResponse } from "node:http";
import { dirname, resolve } from "node:path"; import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@ -8,14 +8,10 @@ import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import { mtp } from "mtp/vite"; import { mtp } from "mtp/vite";
import { methaniumUi } from "@methanium/ui/vite"; import { methaniumUi } from "@methanium/ui/vite";
import { tensaminPwa } from "@tensamin/pwa/vite";
const host = process.env.TAURI_DEV_HOST; const host = process.env.TAURI_DEV_HOST;
const appDir = dirname(fileURLToPath(import.meta.url)); const appDir = dirname(fileURLToPath(import.meta.url));
const markdownPackageDir = resolve(appDir, "../../packages/markdown");
function resolveMarkdownDependency(packageName: string): string {
return realpathSync(resolve(markdownPackageDir, "node_modules", packageName));
}
function deepFilterAssetHeaders(rootDir: string): Plugin { function deepFilterAssetHeaders(rootDir: string): Plugin {
const serveModel = ( const serveModel = (
@ -57,24 +53,6 @@ export default defineConfig({
clearScreen: false, clearScreen: false,
resolve: { resolve: {
tsconfigPaths: true, tsconfigPaths: true,
alias: [
{
find: "@codemirror/commands",
replacement: resolveMarkdownDependency("@codemirror/commands"),
},
{
find: "@codemirror/lang-markdown",
replacement: resolveMarkdownDependency("@codemirror/lang-markdown"),
},
{
find: "@codemirror/state",
replacement: resolveMarkdownDependency("@codemirror/state"),
},
{
find: "@codemirror/view",
replacement: resolveMarkdownDependency("@codemirror/view"),
},
],
dedupe: [ dedupe: [
"react", "react",
"react-dom", "react-dom",
@ -87,19 +65,15 @@ export default defineConfig({
"@tensamin/settings", "@tensamin/settings",
"@tensamin/storage", "@tensamin/storage",
"@tensamin/mtp", "@tensamin/mtp",
"@tensamin/user", "@tensamin/identity",
"@tensamin/tauri", "@tensamin/tauri",
"@tensamin/chat", "@tensamin/chat",
"@codemirror/commands",
"@codemirror/lang-markdown",
"@codemirror/state",
"@codemirror/view",
], ],
}, },
server: { server: {
port: 5173, port: 3000,
strictPort: true, strictPort: true,
host: host || "0.0.0.0", host: "0.0.0.0",
hmr: host hmr: host
? { ? {
protocol: "ws", protocol: "ws",
@ -133,7 +107,6 @@ export default defineConfig({
"@tensamin/crypto", "@tensamin/crypto",
"@tensamin/crypto/context", "@tensamin/crypto/context",
"@tensamin/hotkeys", "@tensamin/hotkeys",
"@tensamin/markdown",
"@tensamin/mtp", "@tensamin/mtp",
"@tensamin/notifications", "@tensamin/notifications",
"@tensamin/onboarding", "@tensamin/onboarding",
@ -145,17 +118,50 @@ export default defineConfig({
"@tensamin/storage/context", "@tensamin/storage/context",
"@tensamin/tauri", "@tensamin/tauri",
"@tensamin/tauth", "@tensamin/tauth",
"@tensamin/user", "@tensamin/identity",
], ],
}, },
build: { build: {
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false, minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
sourcemap: !!process.env.TAURI_ENV_DEBUG, sourcemap: !!process.env.TAURI_ENV_DEBUG,
chunkSizeWarningLimit: 550,
rolldownOptions: {
output: {
codeSplitting: {
groups: [
{
name: "codemirror-core",
test: /node_modules\/.pnpm\/@codemirror\+(?:state|view|language)@/,
priority: 20,
},
{
name: "codemirror-editor",
test: /node_modules\/.pnpm\/@codemirror\+(?:autocomplete|commands|lang-markdown)@|node_modules\/.pnpm\/codemirror@/,
priority: 20,
},
{
name: "livekit-client",
test: /node_modules\/.pnpm\/livekit-client@/,
priority: 20,
},
{
name: "livekit-support",
test: /node_modules\/.pnpm\/@livekit\+/,
priority: 20,
},
],
},
},
},
}, },
plugins: [ plugins: [
...tensaminPwa(),
methaniumUi({ defaultThemeId: "tensamin" }), methaniumUi({ defaultThemeId: "tensamin" }),
deepFilterAssetHeaders(resolve(appDir, "public")), deepFilterAssetHeaders(resolve(appDir, "public")),
mtp({ typeMaps: resolve(appDir, "../../mtp-type-maps/type-maps.yaml") }), mtp({
typeMaps: resolve(appDir, "../../mtp-type-maps/type-maps.yaml"),
outDir: ".mtp",
}),
{ {
name: "workspace-realpath-resolution", name: "workspace-realpath-resolution",
enforce: "post", enforce: "post",

183
flake.nix
View file

@ -16,9 +16,7 @@
let let
systems = [ "x86_64-linux" ]; systems = [ "x86_64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs systems; forAllSystems = nixpkgs.lib.genAttrs systems;
version = "0.0.10"; version = "0.0.11";
x86_64DebHash = "sha256-R7ufXBPL2+jsgtbcIjPg55RU6m39odaC5IeLI1nYU48=";
forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}";
in in
{ {
packages = forAllSystems ( packages = forAllSystems (
@ -32,8 +30,6 @@
android_sdk.accept_license = true; android_sdk.accept_license = true;
}; };
}; };
debArtifact = "Tensamin-${version}-linux-amd64.deb";
electronRuntimeLibs = with pkgs; [ electronRuntimeLibs = with pkgs; [
alsa-lib alsa-lib
at-spi2-atk at-spi2-atk
@ -75,71 +71,130 @@
libXtst libXtst
libxcb libxcb
]; ];
packageDeb = electron = pkgs.electron;
src: pnpm = pkgs.pnpm;
pkgs.stdenv.mkDerivation { mtpTypeMaps = pkgs.fetchgit {
pname = "tensamin"; url = "https://git.methanium.net/tensamin/mtp-type-maps";
inherit version src; rev = "6e5122fe44f793c0e0d3229b3d34145ce17c2d31";
hash = "sha256-/4n8F0YLJaLncefKL907P5l+en1CTjpgdFmLBF3gbiQ=";
nativeBuildInputs = with pkgs; [ };
autoPatchelfHook mtpSource = pkgs.fetchzip {
dpkg url = "https://git.methanium.net/methanium/mtp/releases/download/0.3.0-dev-c7c7afe/mtp-0.3.0.tgz";
makeWrapper hash = "sha256-XLTa8DxP93Q4hBHRCLUzCPOqkbdb4V3aZxE5Iuq+kW0=";
]; };
buildInputs = electronRuntimeLibs; mtpCargoDeps = pkgs.rustPlatform.fetchCargoVendor {
src = mtpSource;
dontConfigure = true; hash = "sha256-8MZ65N/EtWPAggal0JkGDx3WSn+LxWQORinkqVbsrys=";
dontBuild = true; };
wasmBindgenCliSource = pkgs.fetchCrate {
unpackPhase = '' pname = "wasm-bindgen-cli";
runHook preUnpack version = "0.2.127";
dpkg-deb -x "$src" . hash = "sha256-di+qBAdd7pENLiIB9CoZoab+W5xeDoByMREcCGTSzWo=";
runHook postUnpack };
''; wasmBindgenCli = pkgs.buildWasmBindgenCli {
src = wasmBindgenCliSource;
installPhase = '' cargoDeps = pkgs.rustPlatform.fetchCargoVendor {
runHook preInstall src = wasmBindgenCliSource;
hash = "sha256-FTv2GZIAQs0ePdIZXIXil7JbZ6kIT05VG6vqC1qNFxQ=";
mkdir -p "$out"
cp -r opt "$out/"
cp -r usr/* "$out/"
mkdir -p "$out/bin"
makeWrapper "$out/opt/Tensamin/tensamin" "$out/bin/tensamin" \
--prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath electronRuntimeLibs}"
substituteInPlace "$out/share/applications/Tensamin.desktop" \
--replace-fail "Exec=/opt/Tensamin/tensamin" "Exec=tensamin"
runHook postInstall
'';
}; };
defaultPackage = packageDeb ( };
pkgs.fetchurl { desktopItem = pkgs.makeDesktopItem {
url = "${forgejoBaseUrl}/${debArtifact}"; name = "tensamin";
hash = x86_64DebHash; desktopName = "Tensamin";
} exec = "tensamin";
); icon = "tensamin";
localDebPath = builtins.getEnv "TENSAMIN_DEB"; startupWMClass = "Tensamin";
localPathPackage = categories = [ "Network" ];
if localDebPath == "" then };
pkgs.writeShellScriptBin "tensamin" '' defaultPackage = pkgs.stdenv.mkDerivation (finalAttrs: {
echo "Set TENSAMIN_DEB to a local .deb path and run with --impure." >&2 pname = "tensamin";
exit 1 inherit version;
'' src = self;
else
packageDeb ( pnpmDeps = pkgs.fetchPnpmDeps {
builtins.path { inherit (finalAttrs) pname version src;
path = localDebPath; inherit pnpm;
name = debArtifact; fetcherVersion = 4;
} hash = "sha256-imP3MTr1YLc28Z9n617m0Wt/6vPirFznzoriKzojlEg=";
); };
nativeBuildInputs = with pkgs; [
copyDesktopItems
makeWrapper
nodejs_22
pnpm
pnpmConfigHook
cargo
lld
rustc
wasm-pack
wasmBindgenCli
binaryen
];
env.ELECTRON_SKIP_BINARY_DOWNLOAD = 1;
postPatch = ''
rm -rf mtp-type-maps
ln -s ${mtpTypeMaps} mtp-type-maps
node -e '
const fs = require("fs");
const path = "apps/electron/package.json";
const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
pkg.version = "${version}";
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
'
'';
buildPhase = ''
runHook preBuild
mkdir -p "$HOME/.cargo"
substitute ${mtpCargoDeps}/.cargo/config.toml "$HOME/.cargo/config.toml" \
--replace-fail @vendor@ ${mtpCargoDeps}
pnpm run copy-licenses
pnpm run build:packages
pnpm run build:web
pnpm --dir apps/tauri run gen-icons
pnpm --dir apps/electron run build
pnpm --dir apps/electron exec electron-builder --dir --linux --publish never \
--config.electronDist=${electron.dist} \
--config.electronVersion=${electron.version}
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p "$out/lib/tensamin" "$out/bin"
cp -r apps/electron/release/linux-unpacked/. "$out/lib/tensamin/"
makeWrapper "$out/lib/tensamin/tensamin" "$out/bin/tensamin" \
--prefix LD_LIBRARY_PATH : "${pkgs.lib.makeLibraryPath electronRuntimeLibs}"
install -Dm644 apps/electron/build/icons/icon.png \
"$out/share/icons/hicolor/512x512/apps/tensamin.png"
runHook postInstall
'';
desktopItems = [ desktopItem ];
meta = {
description = "Tensamin desktop client";
homepage = "https://git.methanium.net/tensamin/client";
mainProgram = "tensamin";
platforms = pkgs.lib.platforms.linux;
};
});
in in
{ {
default = defaultPackage; default = defaultPackage;
tensamin = defaultPackage; tensamin = defaultPackage;
electron = defaultPackage; electron = defaultPackage;
localPathForDev = localPathPackage;
} }
); );

View file

@ -1,22 +0,0 @@
MIT License
Copyright (c) 2014-present Sebastian McKenzie and other contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 Material-UI SAS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 Material-UI SAS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (C) 2018-2021 by Marijn Haverbeke <marijn@haverbeke.berlin> and others
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -1,20 +0,0 @@
MIT License
Copyright (c) 2021-present Floating UI contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,20 +0,0 @@
MIT License
Copyright (c) 2021-present Floating UI contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,20 +0,0 @@
MIT License
Copyright (c) 2021-present Floating UI contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,20 +0,0 @@
MIT License
Copyright (c) 2021-present Floating UI contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,93 +0,0 @@
Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 WorkOS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2018 Mark Erikson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2025 TanStack
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021-present Tanner Linsley
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Tanner Linsley
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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