Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
bbf53e2b30 |
|||
|
e0c2331af3 |
|||
|
d88c554a2d |
|||
|
0c936718bc |
|||
|
85921a0a08 |
|||
|
|
6e31989262 |
||
|
|
c155bbb534 |
||
|
78e15f6218 |
|||
|
042141c781 |
|||
|
|
c386e8d5cb |
||
|
|
74fb46990e |
||
|
0c0a341f1e |
|||
|
f74ca52f4f |
|||
|
|
94f11f60f6 |
||
|
|
f5461bbd42 |
||
|
7c9d9529b4 |
|||
|
8a04b327fc |
|||
|
0a304e44f2 |
|||
|
094cb910aa |
|||
|
cf17365c8f |
|||
|
aa34bd962b |
|||
|
1d1b304bcf |
|||
|
|
0fbedf6733 | ||
|
96b81d30b1 |
|||
|
edcf0e15dd |
|||
|
5056f084b4 |
|||
|
ede0199858 |
|||
|
88a094c8bf |
|||
|
e43e03ff12 |
|||
|
97270abed8 |
|||
|
2a55c87df1 |
|||
|
|
57c7ceb27a | ||
|
|
d0918ff384 | ||
|
caa7d65b84 |
|||
| 0ac20f09e4 | |||
|
7b36218ffa |
|||
| 73db7e6b2b | |||
|
6e977bca7e |
|||
|
eaf3f61ebd |
|||
|
6f4cc62530 |
|||
|
eaeff1b8d5 |
|||
|
4016181db9 |
|||
| 2299555fe6 | |||
| f50b4eaebe | |||
|
825be4eb33 |
|||
|
1c90a0c859 |
|||
| 6dee7e8dcc | |||
| 41258a8483 | |||
| 746f0cc9a4 | |||
| c1baf3576d | |||
| 7ffbab6b08 | |||
| f1af0f210d | |||
| a40bf3a2c2 | |||
|
dbb407606b |
|||
| f763af13be | |||
|
f83e71be6b |
|||
|
b7c94123fa |
|||
|
32671f32fd |
|||
|
2b4207f8fa |
|||
|
5f44b89043 |
|||
|
31ff5a542f |
|||
|
99c38162a6 |
|||
|
ef24ea4ca0 |
|||
|
45d39cb47c |
|||
|
c182f2e9ac |
|||
|
ec019a4dff |
|||
|
b76143e2e2 |
|||
| 01b68300ab | |||
| 3954d776ce | |||
| 860fd2ec08 | |||
| dfb82373c6 | |||
| 927a719281 | |||
| 7f64659e23 | |||
| 35769b1aae | |||
| a731f8ee42 | |||
|
|
beafb623ba | ||
|
|
ac1bdf5ac2 | ||
|
22a4cd2f0e |
|||
|
3725587232 |
|||
| f0dcfbc63e | |||
| 00ff0b00d4 | |||
| 4d72469806 | |||
|
|
9ae7dbe35e | ||
|
|
2c405af72d | ||
| 29e80d6d02 | |||
| f3ca5d637c | |||
| 565f39c255 |
265 changed files with 12916 additions and 13902 deletions
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[env]
|
||||||
|
MTP_TYPE_MAPS = { value = "mtp-type-maps/type-maps.yaml", relative = true }
|
||||||
|
|
@ -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"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
60
.forgejo/workflows/dependency-builds.yml
Normal file
60
.forgejo/workflows/dependency-builds.yml
Normal 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
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
|
@ -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
36
apps/pwa/package.json
Normal 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
189
apps/pwa/src/runtime.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
161
apps/pwa/src/serviceWorker.ts
Normal file
161
apps/pwa/src/serviceWorker.ts
Normal 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
18
apps/pwa/src/style.css
Normal 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
223
apps/pwa/src/vite.ts
Normal 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
33
apps/pwa/todo.md
Normal 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
23
apps/pwa/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
21
apps/pwa/tsconfig.worker.json
Normal file
21
apps/pwa/tsconfig.worker.json
Normal 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"]
|
||||||
|
}
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[env]
|
|
||||||
MTP_TYPE_MAPS = { value = "../../mtp-type-maps/type-maps.yaml", relative = true }
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
497
apps/tauri/src-tauri/Cargo.lock
generated
497
apps/tauri/src-tauri/Cargo.lock
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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 = "../../../"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
135
apps/tauri/src-tauri/src/accessibility_backend.rs
Normal file
135
apps/tauri/src-tauri/src/accessibility_backend.rs
Normal 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,
|
||||||
|
};
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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
1
apps/web/.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
8
apps/web/src/components/callPopout.tsx
Normal file
8
apps/web/src/components/callPopout.tsx
Normal 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;
|
||||||
|
}
|
||||||
95
apps/web/src/components/callRuntimeInit.tsx
Normal file
95
apps/web/src/components/callRuntimeInit.tsx
Normal 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;
|
||||||
|
}
|
||||||
8
apps/web/src/components/callSidebarBox.tsx
Normal file
8
apps/web/src/components/callSidebarBox.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
15
apps/web/src/components/routeLoader.tsx
Normal file
15
apps/web/src/components/routeLoader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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()}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
13
apps/web/src/components/status-options.ts
Normal file
13
apps/web/src/components/status-options.ts
Normal 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>;
|
||||||
|
|
@ -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">(
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 }) => (
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
75
apps/web/src/routes/app/shell.tsx
Normal file
75
apps/web/src/routes/app/shell.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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
183
flake.nix
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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
Loading…
Reference in a new issue