Compare commits

...
Author SHA1 Message Date
15138a30da (fix): outdated tag recognition
All checks were successful
/ build-web (push) Successful in 1m12s
/ build-desktop (push) Successful in 11m50s
/ build-mobile (push) Successful in 16m13s
/ release (push) Successful in 26s
2026-05-25 00:05:48 +02:00
37338f12fb (feat): rename productName to have a uppercase T
Some checks failed
/ build-web (push) Successful in 1m23s
/ release (push) Has been cancelled
/ build-desktop (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-05-25 00:00:33 +02:00
edfb037864 (fix): lowercase name on android
Some checks failed
/ build-desktop (push) Has been cancelled
/ release (push) Has been cancelled
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-05-25 00:00:10 +02:00
d16f260a3a (fix): small desktop app bugs
Some checks failed
/ build-web (push) Successful in 1m12s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (push) Has been cancelled
2026-05-24 23:52:33 +02:00
b0cdf0adf2 (fix): actually fix android icons
All checks were successful
/ build-web (push) Successful in 1m11s
/ build-desktop (push) Successful in 11m40s
/ build-mobile (push) Successful in 16m14s
/ release (push) Successful in 28s
2026-05-24 22:24:38 +02:00
0c933e4890 (fix): messages at the top overlapping
All checks were successful
/ build-web (push) Successful in 1m17s
/ build-desktop (push) Successful in 12m24s
/ build-mobile (push) Successful in 17m15s
/ release (push) Successful in 23s
2026-05-24 21:30:42 +02:00
78f354d0d3 (fix): android icons
All checks were successful
/ build-web (push) Successful in 1m22s
/ build-desktop (push) Successful in 12m30s
/ build-mobile (push) Successful in 17m20s
/ release (push) Successful in 24s
2026-05-24 20:50:28 +02:00
d297282320 (fix): chat message measurement
Some checks failed
/ build-web (push) Successful in 1m16s
/ build-desktop (push) Successful in 12m24s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
(qol): update todos
2026-05-24 20:37:57 +02:00
7ea3b32286 (feat): add native notifications
All checks were successful
/ build-web (push) Successful in 1m9s
/ build-desktop (push) Successful in 11m16s
/ build-mobile (push) Successful in 15m50s
/ release (push) Successful in 23s
(feat): add navbar profile popover
(fix): chat input box click area to slim
(qol): format
2026-05-24 18:31:27 +02:00
67b7926dab (feat): add message states, tweaks chat ui
All checks were successful
/ build-web (push) Successful in 1m10s
/ build-desktop (push) Successful in 11m22s
/ build-mobile (push) Successful in 15m52s
/ release (push) Successful in 23s
(feat): add drag and drop to login page
2026-05-24 14:09:57 +02:00
79beba44a4 (feat): bump version because i am lazy
All checks were successful
/ build-web (push) Successful in 1m14s
/ build-desktop (push) Successful in 11m19s
/ build-mobile (push) Successful in 15m45s
/ release (push) Successful in 23s
2026-05-24 02:18:37 +02:00
809cbb5b5e (feat): update logo
Some checks failed
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 11m16s
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-05-24 02:06:32 +02:00
a0e3e85467 (fix): call being getting initialised even on the login screen
All checks were successful
/ build-web (push) Successful in 1m13s
/ build-desktop (push) Successful in 11m50s
/ build-mobile (push) Successful in 16m27s
/ release (push) Successful in 24s
2026-05-23 21:43:11 +02:00
9e3a6040e3 (fix): add dedicated error_no_iota screen and cancel identification if it occurs
All checks were successful
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 10m54s
/ build-mobile (push) Successful in 15m23s
/ release (push) Successful in 22s
2026-05-23 17:30:55 +02:00
ebd2a364a7 (feat): update android version stuff to better fit obtainium's needs
All checks were successful
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 10m57s
/ build-mobile (push) Successful in 15m24s
/ release (push) Successful in 22s
2026-05-23 17:11:16 +02:00
167036533c (fix): ttp_url missing in qrcode
All checks were successful
/ build-web (push) Successful in 1m7s
/ build-desktop (push) Successful in 11m6s
/ build-mobile (push) Successful in 15m35s
/ release (push) Successful in 23s
2026-05-22 20:34:05 +02:00
fd18ee6079 (feat): remove checks workflow
All checks were successful
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 11m13s
/ build-mobile (push) Successful in 15m42s
/ release (push) Successful in 23s
2026-05-22 14:38:22 +02:00
6f5f88573a (fix): remove the if: from the workflows
Some checks failed
Checks / build-web (pull_request) Has been cancelled
Checks / build-mobile (pull_request) Has been cancelled
Checks / build-desktop (pull_request) Has been cancelled
/ build-desktop (push) Has been cancelled
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
2026-05-22 14:26:21 +02:00
370c3746f7 Added check workflow
Some checks failed
/ build-desktop (push) Has been cancelled
/ release (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-web (push) Has been cancelled
Checks / build-web (pull_request) Successful in 1m5s
Checks / build-desktop (pull_request) Has been cancelled
Checks / build-mobile (pull_request) Has been cancelled
2026-05-22 14:23:25 +02:00
aa811f4277 (fix): workflows running on pull requests
Some checks failed
/ build-web (push) Successful in 1m9s
/ release (push) Has been cancelled
/ build-desktop (push) Has been cancelled
/ build-mobile (push) Has been cancelled
2026-05-22 14:16:57 +02:00
112c9d58da (feat): improved chat ui
All checks were successful
/ build-web (push) Successful in 3m3s
/ build-desktop (push) Successful in 19m37s
/ build-mobile (push) Successful in 27m22s
/ release (push) Successful in 24s
2026-05-21 21:33:34 +02:00
ba59a49f98 (feat): improved conversation adding flow
All checks were successful
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 11m11s
/ build-mobile (push) Successful in 15m40s
/ release (push) Successful in 21s
(feat): visual tweaks for user modal
(feat): now using full width for profile settings page on mobile
(fix): profile not saving with empty avatar
(chore): bump version due to prod release not getting created
(chore): update licenses
2026-05-21 12:15:37 +02:00
4380d664be (chore): update ttp
All checks were successful
/ build-web (push) Successful in 1m7s
/ build-desktop (push) Successful in 17m48s
/ build-mobile (push) Successful in 27m37s
/ release (push) Successful in 37s
2026-05-20 22:09:58 +02:00
55da8026a7 (feat): add profile page
All checks were successful
/ build-web (push) Successful in 1m12s
/ build-desktop (push) Successful in 11m51s
/ build-mobile (push) Successful in 16m33s
/ release (push) Successful in 23s
(feat): show user online-status
(feat): make markdown input more modular
2026-05-16 14:41:43 +02:00
168694ae86 (fix): speaking indicator broken if screensharing, audio was autosubscribed to
All checks were successful
/ build-web (push) Successful in 1m14s
/ build-desktop (push) Successful in 12m29s
/ build-mobile (push) Successful in 17m1s
/ release (push) Successful in 23s
2026-05-15 19:32:35 +02:00
c795f691bf (feat): improved microphone gate
All checks were successful
/ build-web (push) Successful in 1m16s
/ build-desktop (push) Successful in 14m2s
/ build-mobile (push) Successful in 19m13s
/ release (push) Successful in 24s
(fix): weird behaviour related to users that are screensharing
2026-05-15 16:19:03 +02:00
e76c579bb4 (feat): add speaking indicator
All checks were successful
/ build-web (push) Successful in 1m19s
/ build-desktop (push) Successful in 13m14s
/ build-mobile (push) Successful in 18m45s
/ release (push) Successful in 25s
(feat): add microphone gate
(qol): update todo
2026-05-15 15:43:53 +02:00
f624dc7375 (fix): wrong portal for sidebarBox
All checks were successful
/ build-web (push) Successful in 1m10s
/ build-desktop (push) Successful in 11m34s
/ build-mobile (push) Successful in 16m10s
/ release (push) Successful in 22s
(qol): update todo
2026-05-14 16:05:05 +02:00
8954e4193e (fix): tooltips from the sidebarBox hidden behind sidebar
All checks were successful
/ build-web (push) Successful in 1m8s
/ build-desktop (push) Successful in 11m20s
/ build-mobile (push) Successful in 16m6s
/ release (push) Successful in 22s
2026-05-14 15:48:24 +02:00
9641c955fe (feat): revert back to docker runners
All checks were successful
/ build-web (push) Successful in 1m7s
/ build-desktop (push) Successful in 11m8s
/ build-mobile (push) Successful in 15m35s
/ release (push) Successful in 28s
2026-05-14 14:35:17 +02:00
faf695d92c (fix): correctly update workflows for self-hosted runner
Some checks failed
/ build-web (push) Failing after 1s
/ build-mobile (push) Failing after 2s
/ build-desktop (push) Failing after 2s
/ release (push) Has been skipped
2026-05-14 14:32:40 +02:00
39ad421516 (fix): incorrect labels
Some checks failed
/ build-web (push) Failing after 1s
/ build-mobile (push) Failing after 2s
/ build-desktop (push) Failing after 2s
/ release (push) Has been skipped
2026-05-14 14:26:08 +02:00
bb177976c6 (feat): update runners to selfhosted
Some checks failed
/ build-web (push) Has been cancelled
/ build-mobile (push) Has been cancelled
/ build-desktop (push) Has been cancelled
/ release (push) Has been cancelled
2026-05-14 14:25:01 +02:00
726d094bb5 (fix): workflow mobile & desktop failures
Some checks failed
/ build-web (push) Successful in 40s
/ build-mobile (push) Has been cancelled
/ build-desktop (push) Has been cancelled
/ release (push) Has been cancelled
2026-05-14 14:20:26 +02:00
d810f50aab (feat): update workflow
Some checks failed
/ build-web (push) Successful in 40s
/ build-mobile (push) Failing after 11m24s
/ build-desktop (push) Failing after 6m57s
/ release (push) Has been skipped
2026-05-14 13:50:01 +02:00
52918265ea (fix): workflow
All checks were successful
/ deploy (push) Successful in 14m54s
2026-05-14 13:19:15 +02:00
a1af7ce25f (fix): workflow formatting
Some checks failed
deploy-dev.yml / (fix): workflow formatting (push) Failing after 0s
2026-05-14 13:15:59 +02:00
9f83f1e1b7 (fix): release description formatting
All checks were successful
/ deploy (push) Successful in 18m34s
2026-05-14 12:18:18 +02:00
075964a449 (feat): improved mobile sidebar UX
All checks were successful
/ deploy (push) Successful in 18m26s
(feat): update workflows for better releases
2026-05-14 11:35:51 +02:00
fa2b40cbb6 (feat): add logout button
All checks were successful
/ deploy (push) Successful in 15m45s
(qol): update todo
2026-05-13 18:20:01 +02:00
3225b4e652 (feat): add custom ttp urls
All checks were successful
/ deploy (push) Successful in 14m48s
(qol): update todo
2026-05-11 14:36:12 +02:00
125 changed files with 2705 additions and 402 deletions

View file

@ -4,20 +4,31 @@ on:
- dev - dev
jobs: jobs:
deploy: build-web:
runs-on: docker runs-on: docker
steps: steps:
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
- name: Setup Bun - name: Install Packages
run: apt-get update && apt-get install -y sudo curl jq
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v30
- name: Install Bun
uses: oven-sh/setup-bun@v2 uses: oven-sh/setup-bun@v2
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: bun install --frozen-lockfile
- name: Build - name: Copy licenses
run: bun run copy-licenses
- name: Build packages
run: bun run build:packages
- name: Build web
run: bun run build:web run: bun run build:web
- name: Install rsync - name: Install rsync
@ -25,3 +36,204 @@ jobs:
- name: Deploy - name: Deploy
run: rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-dev/ run: rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-dev/
build-mobile:
runs-on: docker
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages
run: apt-get update && apt-get install -y sudo curl jq
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v30
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Copy licenses
run: bun run copy-licenses
- name: Build packages
run: bun run build:packages
- name: Setup Android Keystore
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
run: |
bun -e "require('fs').writeFileSync('keystore.jks', Buffer.from(process.env.KEYSTORE_BASE64.replace(/\s+/g, ''), 'base64'))"
bun -e "const content = process.env.KEYSTORE_PROPERTIES.replace(/\\n/g, '\n').replace(/\r/g, '').split('\n').map(l => l.trim()).filter(l => l).join('\n'); require('fs').writeFileSync('keystore.properties', content)"
- name: Build mobile
run: bun run build:mobile
- name: Upload mobile artifact
uses: https://data.forgejo.org/actions/upload-artifact@v3
with:
name: mobile-apk
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
build-desktop:
runs-on: docker
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages
run: apt-get update && apt-get install -y sudo curl jq
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v30
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Copy licenses
run: bun run copy-licenses
- name: Build packages
run: bun run build:packages
- name: Build desktop
run: bun run build:desktop
- name: Upload desktop artifacts
uses: https://data.forgejo.org/actions/upload-artifact@v3
with:
name: desktop-bundles
path: apps/tauri/src-tauri/target/release/bundle/
release:
runs-on: docker
needs: [build-web, build-mobile, build-desktop]
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
with:
fetch-depth: 0
- name: Install Packages
run: apt-get update && apt-get install -y sudo curl jq
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Download mobile artifact
uses: https://data.forgejo.org/actions/download-artifact@v3
with:
name: mobile-apk
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/
- name: Download desktop artifacts
uses: https://data.forgejo.org/actions/download-artifact@v3
with:
name: desktop-bundles
path: apps/tauri/src-tauri/target/release/bundle/
- name: Copy releases
run: bun --bun run copy-releases
- name: Read version and hash
id: version
run: |
VERSION="$(node -p "require('./package.json').version")"
SHORT_SHA="$(git rev-parse --short HEAD)"
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
echo "short_sha=$SHORT_SHA" >> "$FORGEJO_OUTPUT"
echo "tag=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
echo "title=${VERSION}-dev-${SHORT_SHA}" >> "$FORGEJO_OUTPUT"
- name: Create pre-release and upload files
env:
TOKEN: ${{ forgejo.token }}
API: ${{ forgejo.api_url }}
REPO: ${{ forgejo.repository }}
SHA: ${{ forgejo.sha }}
TAG: ${{ steps.version.outputs.tag }}
TITLE: ${{ steps.version.outputs.title }}
run: |
set -eu
test -d releases
find releases -type f | grep -q .
LATEST_PROD_TAG="$(git tag --sort=-v:refname | head -n 1 || true)"
if [ -n "$LATEST_PROD_TAG" ]; then
RAW_LOG="$(git log "$LATEST_PROD_TAG"..HEAD --pretty=format:'- %s')"
else
RAW_LOG="$(git log --pretty=format:'- %s')"
fi
export RAW_LOG LATEST_PROD_TAG API REPO TAG
BODY="$(node -e '
const raw = process.env.RAW_LOG;
const lines = raw.split("\n");
const commits = [];
for (const line of lines) {
const msg = line.replace(/^- /, "");
const parts = msg.split(/(?=\([^)]+\):)/).filter(Boolean).map(s => s.trim()).filter(s => s);
for (const part of parts) {
commits.push(part);
}
}
const priority = { "(feat):": 1, "(fix):": 2, "(qol):": 3 };
commits.sort((a, b) => {
const tagA = a.match(/^(\([^)]+\):)/)?.[1] || "";
const tagB = b.match(/^(\([^)]+\):)/)?.[1] || "";
return (priority[tagA] || 99) - (priority[tagB] || 99);
});
const log = commits.map(c => "- " + c).join("\n");
const latestTag = process.env.LATEST_PROD_TAG;
if (latestTag) {
const serverUrl = process.env.API.replace(/\/api\/v1.*/, "");
const compareUrl = serverUrl + "/" + process.env.REPO + "/compare/" + latestTag + "..." + process.env.TAG;
console.log(log + "\n\n[View changes](" + compareUrl + ")");
} else {
console.log(log);
}
')"
HTTP_STATUS=$(curl -s -w "%{http_code}" -o release_out.json -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG")
if [ "$HTTP_STATUS" = "200" ]; then
echo "Release $TAG already exists."
RELEASE_ID="$(jq -r .id release_out.json)"
else
echo "Creating new pre-release for $TAG"
RELEASE_JSON="$(curl -f -sS -X POST "$API/repos/$REPO/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg name "$TITLE" \
--arg body "$BODY" \
--arg target "$SHA" \
'{
tag_name: $tag,
name: $name,
body: $body,
target_commitish: $target,
draft: false,
prerelease: true
}')")"
RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)"
fi
find releases -type f -print0 | while IFS= read -r -d '' file; do
name="$(basename "$file")"
curl -fsS -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$name" \
-H "Authorization: token $TOKEN" \
-F "attachment=@$file"
done

View file

@ -4,9 +4,8 @@ on:
- main - main
jobs: jobs:
deploy: build-web:
runs-on: docker runs-on: docker
steps: steps:
- name: Check out repo - name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4 uses: https://data.forgejo.org/actions/checkout@v4
@ -23,16 +22,14 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: bun install --frozen-lockfile run: bun install --frozen-lockfile
- name: Setup Android Keystore - name: Copy licenses
env: run: bun run copy-licenses
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
run: |
bun -e "require('fs').writeFileSync('keystore.jks', Buffer.from(process.env.KEYSTORE_BASE64.replace(/\s+/g, ''), 'base64'))"
bun -e "const content = process.env.KEYSTORE_PROPERTIES.replace(/\\\\n/g, '\n').replace(/\\r/g, '').split('\n').map(l => l.trim()).filter(l => l).join('\n'); require('fs').writeFileSync('keystore.properties', content)"
- name: Build - name: Build packages
run: bun run build:apps run: bun run build:packages
- name: Build web
run: bun run build:web
- name: Install rsync - name: Install rsync
run: apt-get update && apt-get install -y rsync run: apt-get update && apt-get install -y rsync
@ -40,12 +37,117 @@ jobs:
- name: Deploy - name: Deploy
run: rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-prod/ run: rsync -a --delete apps/web/dist/ /var/lib/www/tensamin-web-prod/
build-mobile:
runs-on: docker
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages
run: apt-get update && apt-get install -y sudo curl jq
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v30
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Copy licenses
run: bun run copy-licenses
- name: Build packages
run: bun run build:packages
- name: Setup Android Keystore
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
KEYSTORE_PROPERTIES: ${{ secrets.ANDROID_KEYSTORE_PROPERTIES }}
run: |
bun -e "require('fs').writeFileSync('keystore.jks', Buffer.from(process.env.KEYSTORE_BASE64.replace(/\s+/g, ''), 'base64'))"
bun -e "const content = process.env.KEYSTORE_PROPERTIES.replace(/\\n/g, '\n').replace(/\r/g, '').split('\n').map(l => l.trim()).filter(l => l).join('\n'); require('fs').writeFileSync('keystore.properties', content)"
- name: Build mobile
run: bun run build:mobile
- name: Upload mobile artifact
uses: https://data.forgejo.org/actions/upload-artifact@v3
with:
name: mobile-apk
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
build-desktop:
runs-on: docker
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages
run: apt-get update && apt-get install -y sudo curl jq
- name: Install Nix
uses: https://github.com/cachix/install-nix-action@v30
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Copy licenses
run: bun run copy-licenses
- name: Build packages
run: bun run build:packages
- name: Build desktop
run: bun run build:desktop
- name: Upload desktop artifacts
uses: https://data.forgejo.org/actions/upload-artifact@v3
with:
name: desktop-bundles
path: apps/tauri/src-tauri/target/release/bundle/
release:
runs-on: docker
needs: [build-web, build-mobile, build-desktop]
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
- name: Install Packages
run: apt-get update && apt-get install -y sudo curl jq
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Download mobile artifact
uses: https://data.forgejo.org/actions/download-artifact@v3
with:
name: mobile-apk
path: apps/tauri/src-tauri/gen/android/app/build/outputs/apk/universal/release/
- name: Download desktop artifacts
uses: https://data.forgejo.org/actions/download-artifact@v3
with:
name: desktop-bundles
path: apps/tauri/src-tauri/target/release/bundle/
- name: Copy releases
run: bun --bun run copy-releases
- name: Read version - name: Read version
id: version id: version
run: | run: |
VERSION="$(node -p "require('./package.json').version")" VERSION="$(node -p "require('./package.json').version")"
echo "version=$VERSION" >> "$FORGEJO_OUTPUT" echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
echo "tag=v$VERSION" >> "$FORGEJO_OUTPUT" echo "tag=$VERSION" >> "$FORGEJO_OUTPUT"
- name: Create release and upload files - name: Create release and upload files
env: env:
@ -54,39 +156,40 @@ jobs:
REPO: ${{ forgejo.repository }} REPO: ${{ forgejo.repository }}
SHA: ${{ forgejo.sha }} SHA: ${{ forgejo.sha }}
TAG: ${{ steps.version.outputs.tag }} TAG: ${{ steps.version.outputs.tag }}
VERSION: ${{ steps.version.outputs.version }}
run: | run: |
set -eu set -eu
test -d releases test -d releases
find releases -type f | grep -q . find releases -type f | grep -q .
COMMIT_MSG="$(git log -1 --pretty=%B | sed 's/$/ /')"
HTTP_STATUS=$(curl -s -w "%{http_code}" -o release_out.json -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG") HTTP_STATUS=$(curl -s -w "%{http_code}" -o release_out.json -H "Authorization: token $TOKEN" "$API/repos/$REPO/releases/tags/$TAG")
if [ "$HTTP_STATUS" = "200" ]; then if [ "$HTTP_STATUS" = "200" ]; then
echo "Release $TAG already exists." echo "Release $TAG already exists."
RELEASE_ID="$(jq -r .id release_out.json)" exit 0
else
echo "Creating new release for $TAG"
RELEASE_JSON="$(curl -f -sS -X POST "$API/repos/$REPO/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg name "$TAG" \
--arg body "Release $VERSION" \
--arg target "$SHA" \
'{
tag_name: $tag,
name: $name,
body: $body,
target_commitish: $target,
draft: false,
prerelease: false
}')")"
RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)"
fi fi
echo "Creating new release for $TAG"
RELEASE_JSON="$(curl -f -sS -X POST "$API/repos/$REPO/releases" \
-H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg tag "$TAG" \
--arg name "$TAG" \
--arg body "$COMMIT_MSG" \
--arg target "$SHA" \
'{
tag_name: $tag,
name: $name,
body: $body,
target_commitish: $target,
draft: false,
prerelease: false
}')")"
RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)"
find releases -type f -print0 | while IFS= read -r -d '' file; do find releases -type f -print0 | while IFS= read -r -d '' file; do
name="$(basename "$file")" name="$(basename "$file")"
curl -fsS -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$name" \ curl -fsS -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$name" \

View file

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

BIN
apps/tauri/android.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
apps/tauri/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 997 KiB

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

@ -0,0 +1,237 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="90mm"
height="90mm"
viewBox="0 0 90 90"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
sodipodi:docname="logo_raw.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
inkscape:zoom="0.99999999"
inkscape:cx="450"
inkscape:cy="291.5"
inkscape:window-width="1223"
inkscape:window-height="1369"
inkscape:window-x="26"
inkscape:window-y="23"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" /><defs
id="defs1"><linearGradient
id="swatch15"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop15" /></linearGradient><linearGradient
id="swatch10"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop10" /></linearGradient><linearGradient
id="swatch3"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop3" /></linearGradient><linearGradient
id="swatch2"
inkscape:swatch="solid"><stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop2" /></linearGradient><linearGradient
id="swatch1"
inkscape:swatch="solid"><stop
style="stop-color:#031616;stop-opacity:1;"
offset="0"
id="stop1" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.79374998,0,1 @ F,0,0,1,0,0.79374998,0,1 @ F,0,0,1,0,0.79374998,0,1 @ F,0,0,1,0,0.79374998,0,1 @ F,0,0,1,0,0.79374998,0,1 @ F,0,0,1,0,0.79374998,0,1 @ F,0,0,1,0,0.79374998,0,1"
radius="3"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="bspline"
id="path-effect6"
is_visible="true"
lpeversion="1.3"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false"
uniform="false" /><inkscape:path-effect
effect="spiro"
id="path-effect5"
is_visible="true"
lpeversion="1" /><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1"><path
id="path3"
style="fill:#043b3c;stroke-width:0.320821"
inkscape:label="arrow"
d="m 244.98648,261.60864 -9.19072,26.71639 23.35523,-11.66051 c -4.69011,-5.05439 -10.12762,-9.5023 -14.16451,-15.05588 z m 40.12749,-74.6916 50.72412,18.76364 c 6.70121,39.09457 -10.8946,70.48625 -50.72412,85.29337 z m 0,0 -50.72412,18.76364 c -6.7012,39.09457 10.89461,70.48625 50.72412,85.29337 z"
sodipodi:nodetypes="cccccccccccc" /></clipPath><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient15"
x1="232.99823"
y1="238.94554"
x2="337.22971"
y2="238.94554"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient16"
x1="63.191292"
y1="148.5"
x2="146.82355"
y2="148.5"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient17"
gradientUnits="userSpaceOnUse"
x1="232.99823"
y1="238.94554"
x2="337.22971"
y2="238.94554" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient18"
gradientUnits="userSpaceOnUse"
x1="232.99823"
y1="238.94554"
x2="337.22971"
y2="238.94554" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient19"
gradientUnits="userSpaceOnUse"
x1="232.99823"
y1="238.94554"
x2="337.22971"
y2="238.94554" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient20"
gradientUnits="userSpaceOnUse"
x1="232.99823"
y1="238.94554"
x2="337.22971"
y2="238.94554" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient21"
gradientUnits="userSpaceOnUse"
x1="232.99823"
y1="238.94554"
x2="337.22971"
y2="238.94554" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient22"
gradientUnits="userSpaceOnUse"
x1="63.191292"
y1="148.5"
x2="146.82355"
y2="148.5" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient23"
gradientUnits="userSpaceOnUse"
x1="63.191292"
y1="148.5"
x2="146.82355"
y2="148.5" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient24"
gradientUnits="userSpaceOnUse"
x1="63.191292"
y1="148.5"
x2="146.82355"
y2="148.5" /><linearGradient
inkscape:collect="always"
xlink:href="#swatch1"
id="linearGradient25"
gradientUnits="userSpaceOnUse"
x1="63.191292"
y1="148.5"
x2="146.82355"
y2="148.5" /></defs><g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-62.941767,-104.13688)"><g
id="g2"
transform="matrix(0.78671048,0,0,0.83608014,-116.36038,-50.640745)"
inkscape:label="background"
style="display:inline;stroke:url(#linearGradient15);stroke-width:0.616508;stroke-dasharray:none"
clip-path="url(#clipPath1)"><path
id="rect1"
style="fill:#004b4a;stroke:url(#linearGradient17);stroke-width:0.616508;stroke-dasharray:none"
transform="rotate(-75)"
d="m -227.83395,273.59631 h 146.303341 v 24.39636 H -227.83395 Z" /><path
id="rect1-1"
style="fill:#00524e;stroke:url(#linearGradient18);stroke-width:0.616508;stroke-dasharray:none"
transform="rotate(-75)"
d="m -227.83395,297.99268 h 146.303341 v 24.39636 H -227.83395 Z" /><path
id="rect1-1-3"
style="fill:#006560;stroke:url(#linearGradient19);stroke-width:0.616508;stroke-dasharray:none"
transform="rotate(-75)"
d="m -227.83395,322.3891 h 146.303341 v 24.39636 H -227.83395 Z" /><path
id="rect1-1-3-6"
style="fill:#02857d;stroke:url(#linearGradient20);stroke-width:0.616508;stroke-dasharray:none"
transform="rotate(-75)"
d="m -227.83395,346.78546 h 146.303341 v 24.39636 H -227.83395 Z" /><path
id="rect1-1-3-6-2"
style="fill:#12a89e;stroke:url(#linearGradient21);stroke-width:0.616508;stroke-dasharray:none"
transform="rotate(-75)"
d="m -227.83395,371.18179 h 146.303341 v 24.39636 H -227.83395 Z" /></g><g
id="g3"
inkscape:label="foreground"
style="stroke:url(#linearGradient16);stroke-width:0.5;stroke-dasharray:none"
transform="translate(3.3653788,0.84760028)"><g
id="g1"
inkscape:label="outline"
style="stroke:url(#linearGradient23);stroke-width:0.5;stroke-dasharray:none"><path
id="path15-3-5"
style="opacity:0.352;fill:#000000;fill-opacity:1;stroke:url(#linearGradient22);stroke-width:0.5;stroke-dasharray:none"
inkscape:label="filler"
d="m -54.535435,65.443954 -38.869976,15.14533 c -2.902661,17.83718 0.143161,33.687786 8.852689,46.257116 l -5.823934,18.52239 15.016655,-8.2765 c 5.699447,4.98296 12.651108,9.14382 20.824566,12.34447 30.52177,-11.95203 44.00571,-37.29104 38.8705,-68.847476 z m -0.01808,2.85099 36.24585,14.11438 c 4.61111,37.589746 -14.17784,55.293626 -36.24585,64.158876 -21.698042,-8.5864 -40.914886,-26.53641 -36.245337,-64.158876 z"
transform="translate(159.54375,40.87617)" /></g><path
style="opacity:1;fill:#b8f8ff;fill-opacity:1;stroke:url(#linearGradient24);stroke-width:0.5;stroke-dasharray:none"
d="m 98.103726,126.47621 21.412544,0.16758 a 0.41189664,0.41189664 63.022469 0 1 0.33171,0.65164 l -8.29596,11.58914 a 0.41184923,0.41184923 63.019773 0 0 0.33171,0.65157 l 10.05254,0.0777 a 0.3104736,0.3104736 69.080154 0 1 0.20649,0.54017 l -30.495516,27.73194 a 0.15466019,0.15466019 36.691517 0 1 -0.243455,-0.18141 l 10.172421,-21.16912 a 0.50438266,0.50438266 58.099348 0 0 -0.44993,-0.72282 l -11.312564,-0.10524 a 0.52748832,0.52748832 56.926855 0 1 -0.479488,-0.73628 l 7.661544,-17.7722 a 1.1963277,1.1963277 146.88457 0 1 1.107954,-0.72269 z"
id="path2"
sodipodi:nodetypes="cccccccc"
inkscape:label="bolt" /><path
id="path1"
style="fill:#043b3c;stroke:url(#linearGradient25);stroke-width:0.5;stroke-dasharray:none"
inkscape:label="dark_outline"
d="m 104.99968,104.40528 -40.706558,15.90239 c -2.948156,18.16379 0.05742,34.31077 8.566402,47.3046 l -7.438306,22.73763 18.798853,-9.90998 c 5.772204,4.86637 12.68201,8.97564 20.779609,12.15481 31.96374,-12.54919 46.08489,-39.15399 40.70708,-72.28706 z m 0,1.91926 38.88755,15.13603 c 4.9985,39.45937 -15.37196,59.52899 -38.88755,68.8573 -7.461219,-2.95976 -14.605741,-7.00102 -20.723799,-12.39976 l -1.849499,0.95188 -13.267924,7.37991 5.196582,-16.46463 0.704866,-2.3027 c 0.02259,0.0331 0.04422,0.065 0,-5.2e-4 -0.02016,-0.0296 -0.04036,-0.0594 -0.01292,-0.0196 -7.707836,-11.18647 -11.48502,-25.8661 -8.934338,-46.00184 z m 5.2e-4,2.85151 -36.245852,14.11438 c -4.788427,29.4077 7.785188,53.02119 36.245852,64.15939 28.46066,-11.1382 41.03376,-34.75169 36.24533,-64.15939 z m 0,1.70377 34.62527,13.43381 c 4.45069,35.0227 -13.68688,52.83618 -34.62527,61.11564 -20.938395,-8.27946 -39.076477,-26.09294 -34.625796,-61.11564 z" /></g></g></svg>

After

Width:  |  Height:  |  Size: 10 KiB

BIN
apps/tauri/monochrome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -25,12 +25,12 @@
"dev:mobile:raw": "tauri android dev", "dev:mobile:raw": "tauri android dev",
"build:mobile:raw": "tauri android build", "build:mobile:raw": "tauri android build",
"dev:mobile": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:mobile:raw; else bun dev:mobile:raw; fi", "dev:mobile": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:mobile:raw; else bun dev:mobile:raw; fi",
"build:mobile": "if command -v nix >/dev/null 2>&1; then nix develop --command bun build:mobile:raw; else bun build:mobile:raw; fi", "build:mobile": "bun run render-version.ts && if command -v nix >/dev/null 2>&1; then nix develop --command bun build:mobile:raw; else bun build:mobile:raw; fi && bun run render-version.ts --unrender",
"dev:desktop:raw": "tauri dev", "dev:desktop:raw": "tauri dev",
"build:desktop:raw": "tauri build", "build:desktop:raw": "tauri build",
"dev:desktop": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:desktop:raw; else bun dev:desktop:raw; fi", "dev:desktop": "if command -v nix >/dev/null 2>&1; then nix develop --command bun dev:desktop:raw; else bun dev:desktop:raw; fi",
"build:desktop": "if command -v nix >/dev/null 2>&1; then nix develop --command bun build:desktop:raw; else bun build:desktop:raw; fi", "build:desktop": "if command -v nix >/dev/null 2>&1; then nix develop --command bun build:desktop:raw; else bun build:desktop:raw; fi",
"gen-icons": "tauri icon ./logo.png", "gen-icons": "tauri icon ./logo.json",
"format": "bunx prettier --write .", "format": "bunx prettier --write .",
"lint": "eslint src" "lint": "eslint src"
}, },
@ -46,6 +46,7 @@
"react-dom": "^19.2.0" "react-dom": "^19.2.0"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2" "@tauri-apps/cli": "^2",
"@types/node": "^25.9.1"
} }
} }

View file

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

View file

@ -4838,7 +4838,7 @@ dependencies = [
[[package]] [[package]]
name = "tensamin" name = "tensamin"
version = "0.1.0" version = "0.0.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"image 0.25.10", "image 0.25.10",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "tensamin" name = "tensamin"
version = "0.1.0" version = "0.0.0"
description = "Privacy focused messanger" description = "Privacy focused messanger"
authors = ["methanium"] authors = ["methanium"]
edition = "2021" edition = "2021"

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 6 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before After
Before After

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,001 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before After
Before After

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Tensamin", "productName": "Tensamin",
"version": "0.1.0", "version": "0.0.0",
"mainBinaryName": "tensamin", "mainBinaryName": "tensamin",
"identifier": "net.tensamin.client", "identifier": "net.tensamin.client",
"build": { "build": {

View file

@ -7,6 +7,7 @@ import {
} from "react"; } from "react";
import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link"; import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link";
import { isTauri } from "@tauri-apps/api/core"; import { isTauri } from "@tauri-apps/api/core";
import { useIsMobile } from "@tensamin/ui";
type DeeplinkContextValue = { type DeeplinkContextValue = {
deeplinks: readonly string[]; deeplinks: readonly string[];
@ -31,10 +32,10 @@ export default function DeeplinkProvider({
children: ReactNode; children: ReactNode;
}) { }) {
const [deeplinks, setDeeplinks] = useState<string[]>([]); const [deeplinks, setDeeplinks] = useState<string[]>([]);
const isTauriEnv = isTauri(); const isMobile = useIsMobile();
useEffect(() => { useEffect(() => {
if (!isTauriEnv) return; if (!isTauri() || !isMobile) return;
let mounted = true; let mounted = true;
let unlisten: (() => void) | undefined; let unlisten: (() => void) | undefined;
@ -57,7 +58,7 @@ export default function DeeplinkProvider({
mounted = false; mounted = false;
unlisten?.(); unlisten?.();
}; };
}, [isTauriEnv]); }, [isMobile]);
return ( return (
<deeplinkContext.Provider <deeplinkContext.Provider

View file

@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"types": ["node"],
"target": "ES2022", "target": "ES2022",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
@ -8,5 +9,5 @@
"skipLibCheck": true, "skipLibCheck": true,
"noEmit": true "noEmit": true
}, },
"include": ["src"] "include": ["src", "./render-version.ts"]
} }

View file

@ -26,6 +26,7 @@
"@tensamin/tauri": "workspace:*", "@tensamin/tauri": "workspace:*",
"@tensamin/ttp": "workspace:*", "@tensamin/ttp": "workspace:*",
"@tensamin/tauth": "workspace:*", "@tensamin/tauth": "workspace:*",
"@tensamin/markdown": "workspace:*",
"@tensamin/ui": "*", "@tensamin/ui": "*",
"@tensamin/user": "workspace:*", "@tensamin/user": "workspace:*",
"@tensamin/notifications": "workspace:*", "@tensamin/notifications": "workspace:*",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Before After
Before After

View file

@ -1,20 +1,58 @@
import type { User } from "@tensamin/user/context"; import type { User } from "@tensamin/user/context";
import { Avatar, AvatarImage, AvatarFallback } from "@tensamin/ui"; import {
import { reduceDisplay } from "./utils"; Avatar,
AvatarImage,
AvatarFallback,
Tooltip,
TooltipTrigger,
TooltipContent,
} from "@tensamin/ui";
import { Card, CardHeader } from "@tensamin/ui"; import { Card, CardHeader } from "@tensamin/ui";
import { Skeleton } from "@tensamin/ui"; import { Skeleton } from "@tensamin/ui";
import { getStatusColor } from "@tensamin/shared/data";
export function Basic(props: { user: User }) { export function Basic({
user,
extra,
}: {
user: User;
extra?: React.ReactNode;
}) {
return ( return (
<Card className="animate-in fade-in duration-300 rounded-2xl py-0 m-px"> <Card className="animate-in fade-in duration-300 rounded-2xl py-0 m-px">
<CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2"> <CardHeader className="flex flex-row gap-2.5 items-center justify-start p-2">
<Avatar> <div className="relative shrink-0 overflow-visible">
<AvatarImage src={props.user.avatar} /> <Avatar>
<AvatarFallback>{reduceDisplay(props.user.display)}</AvatarFallback> <AvatarImage src={user.avatar} />
</Avatar> <AvatarFallback>
<div className="flex flex-col gap-1 w-full items-start justify-center text-[15px]"> {user.display.slice(0, 2).toUpperCase()}
<p>{props.user.display}</p> </AvatarFallback>
</Avatar>
<Tooltip>
<TooltipTrigger
render={
<div className="absolute -bottom-0.5 -right-0.5 z-10 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-card">
<div
style={{
backgroundColor: getStatusColor(user.online_status),
}}
className="h-2.25 w-2.25 rounded-full"
/>
</div>
}
/>
<TooltipContent>
{user.online_status
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")}
</TooltipContent>
</Tooltip>
</div> </div>
<div className="flex flex-col gap-1 w-full items-start justify-center text-[15px]">
<p>{user.display}</p>
</div>
<div className="pr-1">{extra}</div>
</CardHeader> </CardHeader>
</Card> </Card>
); );

View file

@ -0,0 +1,34 @@
import type { User } from "@tensamin/user/context";
import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui";
import Text from "@tensamin/markdown/text";
export default function Profile({ user }: { user: User }) {
return (
<div className="flex flex-col gap-2">
<div className="flex gap-2 items-center">
<Avatar className="size-10">
<AvatarImage src={user.avatar} />
<AvatarFallback className="text-lg">
{user.display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<p className="text-lg font-semibold">{user.display}</p>
<p className="text-muted-foreground">{user.username}</p>
</div>
</div>
<Text value={user.about || ""} />
<div className="flex flex-col">
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
iota: {user.iota_id}
</p>
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
user: {user.user_id}
</p>
<p className="text-muted-foreground text-xs overflow-hidden text-ellipsis whitespace-nowrap">
pub key: {user.public_key}
</p>
</div>
</div>
);
}

View file

@ -1,13 +0,0 @@
/**
* Executes reduceDisplay.
* @param display Parameter display.
* @returns unknown.
*/
export function reduceDisplay(display: string) {
const words = display.split(" ");
if (words.length === 1) {
return display.slice(0, 2).toUpperCase();
} else {
return words[0].charAt(0).toUpperCase() + words[1].charAt(0).toUpperCase();
}
}

View file

@ -1,5 +1,13 @@
import { Button } from "@tensamin/ui"; import { Button, Popover, PopoverContent, PopoverTrigger } from "@tensamin/ui";
import { ArrowLeft, House, Phone, Settings, User } from "lucide-react"; import {
ArrowLeft,
ChevronDown,
ChevronUp,
House,
Phone,
Settings,
User,
} 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 { joinCall, useCall } from "@tensamin/call/store";
import Wrapper from "@tensamin/user/wrapper"; import Wrapper from "@tensamin/user/wrapper";
@ -10,6 +18,7 @@ import { useState } from "react";
import { SidebarTrigger, useSidebar } from "@tensamin/ui"; import { SidebarTrigger, useSidebar } from "@tensamin/ui";
import { WindowControls as Controls } from "@tensamin/ui"; import { WindowControls as Controls } from "@tensamin/ui";
import { useSession } from "@tensamin/storage/session"; import { useSession } from "@tensamin/storage/session";
import Profile from "./modals/profile";
export default function Navbar({ forMobile }: { forMobile: boolean }) { export default function Navbar({ forMobile }: { forMobile: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
@ -23,10 +32,11 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
const currentCalls = calls.filter((call) => const currentCalls = calls.filter((call) =>
call.call_members.some((member) => member === id), call.call_members.some((member) => member === id),
); );
//const currentCalls = ["leck", "schleck", "und", "eck"];
const [selectOpen, setSelectOpen] = useState(false); const [selectOpen, setSelectOpen] = useState(false);
const [userInfoOpen, setUserInfoOpen] = useState(false);
return ( return (
<div <div
data-tauri-drag-region data-tauri-drag-region
@ -63,7 +73,27 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
<Wrapper <Wrapper
userId={id} userId={id}
component={(user) => ( component={(user) => (
<p className="font-medium text-md">{user?.display}</p> <>
<Popover open={userInfoOpen} onOpenChange={setUserInfoOpen}>
<PopoverTrigger
render={
<Button
variant="link"
className="px-0! text-foreground"
style={{
textDecorationLine: "none",
}}
>
<p className="font-medium text-md">{user?.display}</p>
{userInfoOpen ? <ChevronUp /> : <ChevronDown />}
</Button>
}
/>
<PopoverContent side="bottom">
<Profile user={user} />
</PopoverContent>
</Popover>
</>
)} )}
loading={<Skeleton className="ml-3 w-40 h-5" />} loading={<Skeleton className="ml-3 w-40 h-5" />}
/> />
@ -126,7 +156,7 @@ export default function Navbar({ forMobile }: { forMobile: boolean }) {
)} )}
</> </>
)} )}
<Controls className="mr-2 ring-border" /> <Controls className="mr-2" />
</div> </div>
</div> </div>
); );

View file

@ -1,4 +1,4 @@
import { Button } from "@tensamin/ui"; import { Button, cn, useIsMobile } from "@tensamin/ui";
import { Input } from "@tensamin/ui"; import { Input } from "@tensamin/ui";
import { Label } from "@tensamin/ui"; import { Label } from "@tensamin/ui";
import { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
@ -23,7 +23,7 @@ const fetchedUser = z.object({
}); });
const formSchema = z.object({ const formSchema = z.object({
username: z.string().min(1).max(15), username: z.string().min(1).max(255),
private_key: z.string().min(1).max(92), private_key: z.string().min(1).max(92),
}); });
@ -35,6 +35,7 @@ const formSchema = z.object({
function parseTuFileContent(rawFileContent: string): { function parseTuFileContent(rawFileContent: string): {
userId: number; userId: number;
privateKey: string; privateKey: string;
domain: string | null;
} { } {
if (rawFileContent.trim().length === 0) { if (rawFileContent.trim().length === 0) {
throw new Error("File is empty"); throw new Error("File is empty");
@ -42,42 +43,59 @@ function parseTuFileContent(rawFileContent: string): {
throw new Error("Invalid file"); throw new Error("Invalid file");
} else if (rawFileContent.split("::").length !== 2) { } else if (rawFileContent.split("::").length !== 2) {
throw new Error("Invalid file"); throw new Error("Invalid file");
} else if (rawFileContent.split("::")[0].length === 0) { }
const left = rawFileContent.split("::")[0];
const right = rawFileContent.split("::")[1];
if (left.length === 0) {
throw new Error("Invalid file"); throw new Error("Invalid file");
} else if (rawFileContent.split("::")[1].length === 0) { } else if (right.length === 0) {
throw new Error("Invalid file"); throw new Error("Invalid file");
} else if (isNaN(Number(rawFileContent.split("::")[0]))) { } else if (isNaN(Number(left)) && !left.includes("@")) {
throw new Error("Invalid file"); throw new Error("Invalid file");
} }
const [userIdString, privateKey] = rawFileContent.split("::"); const [userIdString, privateKey] = rawFileContent.split("::");
const userId = Number(userIdString); const userId = isNaN(Number(userIdString))
? Number(userIdString.split("@")[0])
: Number(userIdString);
const rawDomain = userIdString.includes("@")
? userIdString.split("@")[1]
: null;
const domain = rawDomain
? rawDomain.includes(":")
? rawDomain
: rawDomain + ":1984"
: null;
if (!userId || !privateKey) { if (!userId || !privateKey) {
throw new Error("Invalid file"); throw new Error("Invalid file");
} }
return { userId, privateKey }; console.log({
domain,
rawDomain,
userId,
});
return { userId, privateKey, domain };
} }
/**
* Renders the login form for file upload and manual credential login.
* @returns Login form JSX.
*/
export default function Form() { export default function Form() {
const isMobile = useIsMobile();
const uploadRef = React.useRef<HTMLInputElement | null>(null); const uploadRef = React.useRef<HTMLInputElement | null>(null);
const [isDragging, setIsDragging] = React.useState(false);
const { save } = useStorage(); const { save } = useStorage();
/** // Process dropped files
* Handles uploaded .tu files and stores resolved credentials. const processDroppedFile = React.useCallback(
* @param event Change event from the hidden file input. async (file: globalThis.File): Promise<void> => {
* @returns Promise that resolves when processing has finished.
*/
const handleFileInputChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
try { try {
const file = event.currentTarget.files?.[0]; if (!file.name.endsWith(".tu")) {
if (!file) { toast("error", "Please upload a .tu file");
throw new Error("No file selected"); return;
} }
const raw = await file.text(); const raw = await file.text();
@ -86,6 +104,9 @@ export default function Form() {
await save("session_id", Date.now()); await save("session_id", Date.now());
await save("user_id", parsed.userId); await save("user_id", parsed.userId);
await save("private_key", parsed.privateKey); await save("private_key", parsed.privateKey);
if (parsed.domain) {
await save("ttp_url", `https://${parsed.domain}/`);
}
location.href = "/"; location.href = "/";
} catch (error) { } catch (error) {
@ -96,13 +117,94 @@ export default function Form() {
[save], [save],
); );
// Handle .tu files
const handleFileInputChange = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
const file = event.currentTarget.files?.[0];
if (!file) {
toast("error", "No file selected");
return;
}
await processDroppedFile(file);
},
[processDroppedFile],
);
// Drag and drop listener
React.useEffect(() => {
let dragCounter = 0;
const handleDragEnter = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
dragCounter++;
if (event.dataTransfer?.items?.length) {
setIsDragging(true);
}
};
const handleDragLeave = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
dragCounter--;
if (dragCounter <= 0) {
setIsDragging(false);
}
};
const handleDragOver = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "copy";
}
};
const handleDrop = async (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
dragCounter = 0;
setIsDragging(false);
const file = event.dataTransfer?.files?.[0];
if (!file) {
toast("error", "No file dropped");
return;
}
await processDroppedFile(file);
};
window.addEventListener("dragenter", handleDragEnter);
window.addEventListener("dragleave", handleDragLeave);
window.addEventListener("dragover", handleDragOver);
window.addEventListener("drop", handleDrop);
return () => {
window.removeEventListener("dragenter", handleDragEnter);
window.removeEventListener("dragleave", handleDragLeave);
window.removeEventListener("dragover", handleDragOver);
window.removeEventListener("drop", handleDrop);
};
}, [processDroppedFile]);
/** /**
* Handles username and private key login submission. * Handles username and private key login submission.
* @param event Form submit event. * @param event Form submit event.
* @returns Promise that resolves after login processing. * @returns Promise that resolves after login processing.
*/ */
const handleCredentialsSubmit = React.useCallback( const handleCredentialsSubmit = React.useCallback(
async (event: React.SubmitEvent<HTMLFormElement>): Promise<void> => { async (event: React.FormEvent<HTMLFormElement>): Promise<void> => {
event.preventDefault(); event.preventDefault();
const formData = new FormData(event.currentTarget); const formData = new FormData(event.currentTarget);
@ -114,9 +216,22 @@ export default function Form() {
return; return;
} }
const inputUsername = inputParse.data.username;
const actualUsername = inputUsername.includes("@")
? inputUsername.split("@")[0]
: inputUsername;
const rawDomain = inputUsername.includes("@")
? inputUsername.split("@")[1]
: null;
const domain = rawDomain
? rawDomain.includes(":")
? rawDomain
: rawDomain + ":1984"
: null;
try { try {
const response = await fetch( const response = await fetch(
`https://omega.tensamin.net/api/get/id/${inputParse.data.username}`, `https://omega.tensamin.net/api/get/id/${actualUsername}`,
); );
const rawData = await response.arrayBuffer(); const rawData = await response.arrayBuffer();
@ -137,6 +252,9 @@ export default function Form() {
await save("session_id", Date.now()); await save("session_id", Date.now());
await save("user_id", user.user_id); await save("user_id", user.user_id);
await save("private_key", inputParse.data.private_key); await save("private_key", inputParse.data.private_key);
if (domain) {
await save("ttp_url", `https://${domain}/`);
}
location.href = "/"; location.href = "/";
} catch (error) { } catch (error) {
@ -147,30 +265,35 @@ export default function Form() {
[save], [save],
); );
const isTauriEnv = isTauri();
return ( return (
<div className="flex md:flex-row flex-col gap-15"> <div className="relative flex md:flex-row flex-col gap-15">
{isTauriEnv ? ( {isTauri() && isMobile ? (
<> <>
<QrCodeScanner <QrCodeScanner
onData={(data) => { onData={async (data) => {
if (!data.startsWith("tensamin://tu::")) { if (!data.startsWith("tensamin://tu::")) {
toast("error", "Invalid QR code"); toast("error", "Invalid QR code");
return; return;
} else { }
const decoded = data.replace("tensamin://tu::", "");
try { const decoded = data.replace("tensamin://tu::", "");
const { userId, privateKey } = parseTuFileContent(decoded);
save("session_id", Date.now()); try {
save("user_id", userId); const { userId, privateKey, domain } =
save("private_key", privateKey); parseTuFileContent(decoded);
location.href = "/";
} catch (error) { await save("session_id", Date.now());
log(0, "login", "red", error); await save("user_id", userId);
toast("error", "Failed to parse QR code data"); await save("private_key", privateKey);
if (domain) {
await save("ttp_url", `https://${domain}/`);
} }
location.href = "/";
} catch (error) {
log(0, "login", "red", error);
toast("error", "Failed to parse QR code data");
} }
}} }}
/> />
@ -181,10 +304,30 @@ export default function Form() {
) : ( ) : (
<div <div
onClick={() => uploadRef.current?.click()} onClick={() => uploadRef.current?.click()}
className="flex flex-col gap-3 cursor-pointer w-55 aspect-square bg-input/13 hover:bg-input/30 transition-all duration-300 ease-in-out border-3 items-center justify-center rounded-lg" className={cn(
"flex flex-col gap-3 cursor-pointer w-55 aspect-square",
"border-3 items-center justify-center rounded-lg",
"transition-all duration-300 ease-in-out",
"border-input",
"bg-input/13 hover:bg-input/30",
isDragging ? "animate-wiggle" : "",
)}
> >
<File className="text-foreground" size={27} /> <File
<p className="text-md">Select .tu file</p> className={["transition-all duration-300 text-foreground"].join(
" ",
)}
size={27}
/>
<p
className={[
"text-md transition-all duration-300",
isDragging ? "font-medium" : "",
].join(" ")}
>
Select .tu file
</p>
</div> </div>
)} )}
<input <input

View file

@ -6,6 +6,27 @@ import {
Sidebar as SidebarRoot, Sidebar as SidebarRoot,
SidebarContent, SidebarContent,
SidebarFooter, SidebarFooter,
useSidebar,
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuGroup,
DropdownMenuLabel,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
DialogClose,
Button,
Label,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@tensamin/ui"; } from "@tensamin/ui";
import { isTauri } from "@tauri-apps/api/core"; import { isTauri } from "@tauri-apps/api/core";
import { useIsMobile } from "@tensamin/ui"; import { useIsMobile } from "@tensamin/ui";
@ -13,17 +34,171 @@ import { MobileNavbar } from "./navbar";
import SidebarBox from "@tensamin/call/sidebarBox"; import SidebarBox from "@tensamin/call/sidebarBox";
import { useShowMobileNavbar } from "@/routes/app/layout"; import { useShowMobileNavbar } from "@/routes/app/layout";
import { Ellipsis, Check } from "lucide-react";
import { useState } from "react";
import type { User } from "@tensamin/user/context";
import type z from "zod";
import { ttp } from "@tensamin/shared/data";
import { useTTP } from "@tensamin/ttp";
type OnlineStatus = z.infer<
typeof ttp.get_user_data.response.shape.online_status
>;
const onlineStatusLabels: Record<OnlineStatus, string> = {
user_online: "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({
user,
open,
onOpenChange,
send,
draftStatus,
setDraftStatus,
draftOnlineStatus,
setDraftOnlineStatus,
errorMessage,
setErrorMessage,
saveSucceeded,
setSaveSucceeded,
}: {
user: User;
open: boolean;
onOpenChange: (open: boolean) => void;
send: ReturnType<typeof useTTP>["send"];
draftStatus: string;
setDraftStatus: (value: string) => void;
draftOnlineStatus: OnlineStatus;
setDraftOnlineStatus: (value: OnlineStatus) => void;
errorMessage: string;
setErrorMessage: (value: string) => void;
saveSucceeded: boolean;
setSaveSucceeded: (value: boolean) => void;
}) {
return (
<Dialog
open={open}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
setDraftStatus(user.status ?? "");
setDraftOnlineStatus(user.online_status);
setErrorMessage("");
setSaveSucceeded(false);
}
onOpenChange(nextOpen);
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Update Status</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-2">
<Label htmlFor="user-status">Status message</Label>
<Input
id="user-status"
placeholder="Getting snacks..."
value={draftStatus}
onChange={(e) => {
setSaveSucceeded(false);
setErrorMessage("");
setDraftStatus(e.target.value);
}}
/>
<Label htmlFor="user-online-status">Online status</Label>
<Select
value={draftOnlineStatus}
onValueChange={(value) => {
setSaveSucceeded(false);
setErrorMessage("");
setDraftOnlineStatus(value ?? "user_online");
}}
>
<SelectTrigger className="w-full" id="user-online-status">
<SelectValue placeholder="Online">
{onlineStatusLabels[draftOnlineStatus]}
</SelectValue>
</SelectTrigger>
<SelectContent className="p-1">
<SelectItem value="user_online">Online</SelectItem>
<SelectItem value="user_offline">Offline</SelectItem>
<SelectItem value="user_idle">Idle</SelectItem>
<SelectItem value="user_dnd">Do not disturb</SelectItem>
</SelectContent>
</Select>
</div>
{errorMessage && (
<p className="text-sm text-destructive">{errorMessage}</p>
)}
<DialogFooter>
<DialogClose render={<Button variant="destructive">Cancel</Button>} />
<Button
onClick={async () => {
const payload = {
...(draftStatus && { status: draftStatus }),
online_status: draftOnlineStatus,
};
const validation =
ttp.change_user_data.request.safeParse(payload);
if (!validation.success) {
setSaveSucceeded(false);
setErrorMessage(
validation.error.issues[0]?.message ?? "Invalid status data",
);
return;
}
try {
await send("change_user_data", validation.data);
setSaveSucceeded(true);
setErrorMessage("");
} catch (err) {
setSaveSucceeded(false);
setErrorMessage("Failed to update status: " + err);
}
}}
>
{saveSucceeded ? (
<span className="inline-flex items-center gap-1.5">
<Check className="size-4" />
Saved
</span>
) : (
"Save"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
/**
* Renders the conversation sidebar with account summary and conversation list.
* @returns Sidebar JSX.
*/
export default function Sidebar() { export default function Sidebar() {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const showMobileNavbar = useShowMobileNavbar(); const showMobileNavbar = useShowMobileNavbar();
const { openMobile, setOpenMobile } = useSidebar();
const [draftStatus, setDraftStatus] = useState("");
const [draftOnlineStatus, setDraftOnlineStatus] =
useState<OnlineStatus>("user_online");
const [dialogOpen, setDialogOpen] = useState(false);
const [statusErrorMessage, setStatusErrorMessage] = useState("");
const [statusSaveSucceeded, setStatusSaveSucceeded] = useState(false);
return ( const { send } = useTTP();
<SidebarRoot className="border-0!">
const content = (
<>
<SidebarContent <SidebarContent
className={ className={
isTauri() && isMobile ? "pt-[env(safe-area-inset-top)]" : "pt-2" isTauri() && isMobile ? "pt-[env(safe-area-inset-top)]" : "pt-2"
@ -34,7 +209,67 @@ export default function Sidebar() {
<Wrapper <Wrapper
loading={<Loading />} loading={<Loading />}
userId={"own"} userId={"own"}
component={(user) => <Basic user={user} />} component={(user) => (
<>
<Basic
user={user}
extra={
<DropdownMenu>
<DropdownMenuTrigger
render={
<button
type="button"
aria-label="Open profile menu"
className="cursor-pointer inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
>
<Ellipsis />
</button>
}
/>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuLabel>Profile</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => {
setDraftStatus(user.status ?? "");
setDraftOnlineStatus(user.online_status);
setStatusErrorMessage("");
setStatusSaveSucceeded(false);
setDialogOpen(true);
}}
>
Set Status
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
}
/>
<StatusDialog
user={user}
open={dialogOpen}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
setDraftStatus(user.status ?? "");
setDraftOnlineStatus(user.online_status);
setStatusErrorMessage("");
setStatusSaveSucceeded(false);
}
setDialogOpen(nextOpen);
}}
send={send}
draftStatus={draftStatus}
setDraftStatus={setDraftStatus}
draftOnlineStatus={draftOnlineStatus}
setDraftOnlineStatus={setDraftOnlineStatus}
errorMessage={statusErrorMessage}
setErrorMessage={setStatusErrorMessage}
saveSucceeded={statusSaveSucceeded}
setSaveSucceeded={setStatusSaveSucceeded}
/>
</>
)}
/> />
</div> </div>
<div className="h-full"> <div className="h-full">
@ -52,6 +287,34 @@ export default function Sidebar() {
<SidebarBox /> <SidebarBox />
</SidebarFooter> </SidebarFooter>
)} )}
</SidebarRoot> </>
); );
if (isMobile) {
return (
<>
{openMobile && (
<div
className="fixed inset-0 z-40 bg-black/10"
onClick={() => setOpenMobile(false)}
/>
)}
<div
data-sidebar="sidebar"
data-slot="sidebar"
data-mobile="true"
className="fixed inset-y-0 left-0 z-50 w-screen bg-sidebar p-0 text-sidebar-foreground transition-[transform,opacity] duration-150 ease-linear"
style={{
transform: openMobile ? "translateX(0)" : "translateX(-100%)",
opacity: openMobile ? 1 : 0,
pointerEvents: openMobile ? "auto" : "none",
}}
>
<div className="flex h-full w-full flex-col">{content}</div>
</div>
</>
);
}
return <SidebarRoot className="border-0!">{content}</SidebarRoot>;
} }

View file

@ -76,6 +76,16 @@ export default function List() {
</div> </div>
); );
})} })}
<p
className="text-center text-sm px-2 text-muted-foreground transition-all duration-200 ease-in-out"
hidden={contacts.length !== 0 || category !== "conversations"}
style={{
opacity:
contacts.length === 0 && category === "conversations" ? 1 : 0,
}}
>
Get started by adding a conversation
</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -36,7 +36,7 @@ export function Switch({
label, label,
id, id,
}: { }: {
label: string; label: React.ReactNode;
id: keyof typeof settingsStorageDefaults & BooleanStorageKey; id: keyof typeof settingsStorageDefaults & BooleanStorageKey;
}) { }) {
const { save, load } = useStorage(); const { save, load } = useStorage();

View file

@ -1,4 +1,4 @@
import { Button } from "@tensamin/ui"; import { Button, ClearStorageButton } from "@tensamin/ui";
import options from "@tensamin/shared/settings"; import options from "@tensamin/shared/settings";
import { cn, useIsMobile } from "@tensamin/ui"; import { cn, useIsMobile } from "@tensamin/ui";
@ -69,6 +69,9 @@ export function SettingsSidebar() {
))} ))}
</div> </div>
))} ))}
<div className="mt-auto">
<ClearStorageButton className="w-full" />
</div>
</div> </div>
); );
} }

View file

@ -6,6 +6,20 @@
@source "./**/*.{ts,tsx}"; @source "./**/*.{ts,tsx}";
@source "../../../packages/**/src/**/*.{ts,tsx}"; @source "../../../packages/**/src/**/*.{ts,tsx}";
@theme {
--animate-wiggle: wiggle 0.5s ease-in-out infinite;
@keyframes wiggle {
0%,
100% {
transform: rotate(-2deg);
}
50% {
transform: rotate(2deg);
}
}
}
html, html,
body, body,
#root { #root {
@ -17,3 +31,32 @@ body,
#root { #root {
min-height: 0; min-height: 0;
} }
@layer base {
* {
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--border);
border-radius: 9999px;
}
::-webkit-scrollbar-thumb:hover {
opacity: 0.8;
}
::-webkit-scrollbar-corner {
background: transparent;
}
}

View file

@ -11,12 +11,12 @@ import {
useIsMobile, useIsMobile,
} from "@tensamin/ui"; } from "@tensamin/ui";
import z from "zod"; import z from "zod";
import { toast } from "@tensamin/shared/log";
import { useTTP } from "@tensamin/ttp"; import { useTTP } from "@tensamin/ttp";
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 { useStorage } from "@tensamin/storage/context"; import { useStorage } from "@tensamin/storage/context";
import { useSession } from "@tensamin/storage/session";
// The page // The page
export default function Page() { export default function Page() {
@ -43,11 +43,16 @@ export default function Page() {
// Add Conversation Button Component // Add Conversation Button Component
function AddConversationButton() { function AddConversationButton() {
const { send } = useTTP(); const { send } = useTTP();
const { contacts, insertContact } = useSession();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [error, setError] = useState<string | null>(null);
function submit(username: string | null) { async function submit(username: string | null) {
if (loading) return; if (loading) return;
setError(null);
// username check
const schema = z const schema = z
.string() .string()
.min(1, "Username is too short") .min(1, "Username is too short")
@ -56,26 +61,50 @@ function AddConversationButton() {
const result = schema.safeParse(username?.toLowerCase().trim()); const result = schema.safeParse(username?.toLowerCase().trim());
if (!result.success) { if (!result.success) {
toast("error", result.error.issues[0].message); setError(result.error.issues[0].message);
return; return;
} }
// user existence check
const user = await send("get_user_data", {
username: result.data,
})
.then((data) => {
if (data.data.user_id === 0) {
throw new Error();
}
return data;
})
.catch(() => {
setError("User not found");
return;
});
if (!user) return;
// alrady added check
if (contacts.some((contact) => contact.user_id === user.data.user_id)) {
setError("Conversation already exists");
return;
}
// add the conv
const timeout = setTimeout(() => setLoading(true), 500); const timeout = setTimeout(() => setLoading(true), 500);
send("add_conversation", { send("add_conversation", {
chat_partner_name: result.data, chat_partner_name: result.data,
}) })
.then(() => { .then(() => {
toast("success", "Conversation added"); insertContact(user.data.user_id);
setOpen(false);
}) })
.catch((error) => { .catch((error) => {
if (String(error).includes("error_not_found")) { if (String(error).includes("error_not_found")) {
toast("error", "User not found"); setError("User not found");
return; return;
} }
toast("error", "Failed to add conversation, check console for details"); setError(String(error));
console.error("Failed to add conversation", error);
}) })
.finally(() => { .finally(() => {
clearTimeout(timeout); clearTimeout(timeout);
@ -84,7 +113,15 @@ function AddConversationButton() {
} }
return ( return (
<Dialog> <Dialog
open={open}
onOpenChange={(value) => {
if (value) {
setError(null);
}
setOpen(value);
}}
>
<DialogTrigger render={<Button>Add Conversation</Button>} /> <DialogTrigger render={<Button>Add Conversation</Button>} />
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
@ -110,7 +147,10 @@ function AddConversationButton() {
name="username" name="username"
placeholder="Enter username..." placeholder="Enter username..."
/> />
<div className="flex w-full justify-end gap-1"> <div className="flex w-full justify-end items-center gap-1">
{error && (
<p className="text-sm text-destructive w-full">{error}</p>
)}
<DialogClose render={<Button variant="outline">Cancel</Button>} /> <DialogClose render={<Button variant="outline">Cancel</Button>} />
<Button disabled={loading} type="submit"> <Button disabled={loading} type="submit">
{loading && <Loader2 className="animate-spin" />} Continue {loading && <Loader2 className="animate-spin" />} Continue

View file

@ -1,12 +1,30 @@
import { Switch } from "@/features/settings/components"; import { Switch } from "@/features/settings/components";
import { Kbd } from "@tensamin/ui";
export default function Page() { export default function Page() {
return ( return (
<div> <div className="flex flex-col gap-2">
<Switch <Switch
label="Reverse Enter Key Behavior" label={
<p>
Change <Kbd>Enter</Kbd> behavior to <Kbd>Shift</Kbd> +{" "}
<Kbd>Enter</Kbd>
</p>
}
id="settings.reverse_enter_behavior" id="settings.reverse_enter_behavior"
/> />
<Switch
label="Enable read confirmations"
id="settings.read_confirmations"
/>
<Switch
label="Enable receive confirmations"
id="settings.receive_confirmations"
/>
<Switch
label="Sidebar message preview"
id="settings.show_start_of_last_message_in_sidebar"
/>
</div> </div>
); );
} }

View file

@ -1,3 +1,218 @@
import { useStorage } from "@tensamin/storage/context";
import {
Avatar,
AvatarFallback,
AvatarImage,
Button,
cn,
Input,
useIsMobile,
} from "@tensamin/ui";
import { useUser, type User } from "@tensamin/user/context";
import { useEffect, useRef, useState } from "react";
import MDInput from "@tensamin/markdown/input";
import { ttp } from "@tensamin/shared/data";
import { useTTP } from "@tensamin/ttp";
import { Check } from "lucide-react";
async function prepImage(
file: File,
size = 300,
quality = 0.8,
): Promise<string> {
const bitmap = await createImageBitmap(file);
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Could not get canvas context");
const scale = Math.max(size / bitmap.width, size / bitmap.height);
const width = bitmap.width * scale;
const height = bitmap.height * scale;
const x = (size - width) / 2;
const y = (size - height) / 2;
ctx.drawImage(bitmap, x, y, width, height);
return canvas.toDataURL("image/webp", quality);
}
export default function Page() { export default function Page() {
return <div></div>; const { get } = useUser();
const { load } = useStorage();
const { send } = useTTP();
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [draftUser, setDraftUser] = useState<Partial<User>>({});
const [errorMessage, setErrorMessage] = useState("");
const [saveSucceeded, setSaveSucceeded] = useState(false);
const avatarUploadRef = useRef<HTMLInputElement>(null);
const draftInitializedRef = useRef(false);
const effectiveAvatar =
draftUser.avatar === "none" ? undefined : draftUser.avatar;
const updateDraftUser = (
updater: (previous: Partial<User>) => Partial<User>,
) => {
setSaveSucceeded(false);
setErrorMessage("");
setDraftUser(updater);
};
useEffect(() => {
const fetchUser = async () => {
const user = await get(await load("user_id"));
setCurrentUser(user);
};
fetchUser();
}, [load, get]);
useEffect(() => {
if (!currentUser || draftInitializedRef.current) return;
setDraftUser(currentUser);
draftInitializedRef.current = true;
}, [currentUser]);
const handleAvatarUpload = async (file: File) => {
const final = await prepImage(file);
updateDraftUser((prev) => ({ ...prev, avatar: final }));
if (avatarUploadRef.current) {
avatarUploadRef.current.value = "";
}
};
const isMobile = useIsMobile();
return currentUser ? (
<>
<input
ref={avatarUploadRef}
hidden
onChange={(e) =>
e.target.files?.[0] && handleAvatarUpload(e.target.files[0])
}
type="file"
/>
<div className={cn("flex flex-col gap-5", isMobile ? "w-full" : "w-80")}>
<div className="flex items-center gap-3">
<Avatar className="size-14">
<AvatarImage src={effectiveAvatar} />
<AvatarFallback className="text-2xl">
{draftUser.display?.slice(0, 2).toUpperCase() ||
currentUser.display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1">
<p>Avatar</p>
<div className="flex gap-1">
<Button onClick={() => avatarUploadRef.current?.click()}>
Upload avatar
</Button>
<Button
onClick={() => {
updateDraftUser((prev) => {
return { ...prev, avatar: "none" };
});
}}
variant="destructive"
disabled={effectiveAvatar === undefined}
>
Remove
</Button>
</div>
<p className="text-sm text-muted-foreground">
GIFs are supported in decentralised mode or with Tensamin Premium.
<br />
Maximum file size is 16mb.
</p>
</div>
</div>
<Input
className="w-full"
onChange={(event) =>
updateDraftUser((prev) => ({
...prev,
display: event.target.value,
}))
}
placeholder="Display Name"
value={draftUser.display || ""}
/>
<Input
className="w-full"
onChange={(event) =>
updateDraftUser((prev) => ({
...prev,
username: event.target.value,
}))
}
placeholder="Username"
value={draftUser.username || ""}
/>
<MDInput
styled
paddingY="4px"
paddingX="10px"
fontSize=".875rem"
placeholder="About Me"
setValue={(value) =>
updateDraftUser((prev) => ({ ...prev, about: value }))
}
value={draftUser.about || ""}
/>
<Button
onClick={async () => {
const { avatar, ...draftUsersWithoutAvatar } = draftUser;
const payload = {
...draftUsersWithoutAvatar,
...(typeof avatar === "string"
? {
avatar: avatar.startsWith("data:")
? (avatar.split(",", 2)[1] ?? "")
: avatar,
}
: {}),
};
const validation = ttp.change_user_data.request.safeParse(payload);
if (!validation.success) {
setSaveSucceeded(false);
setErrorMessage(
validation.error.issues[0]?.message ?? "Invalid profile data",
);
return;
}
try {
await send("change_user_data", validation.data);
setSaveSucceeded(true);
setErrorMessage("");
} catch (err) {
setSaveSucceeded(false);
setErrorMessage("Failed to update profile: " + err);
}
}}
>
{saveSucceeded ? (
<span className="inline-flex items-center gap-1.5">
<Check className="size-4" />
Saved
</span>
) : (
"Save"
)}
</Button>
{errorMessage && (
<p className="text-sm text-destructive">{errorMessage}</p>
)}
</div>
</>
) : (
<p>Loading...</p>
);
} }

View file

@ -46,6 +46,7 @@ function QrCodeLogin() {
const [qrCodeBase64, setQrCodeBase64] = useState<string | undefined>( const [qrCodeBase64, setQrCodeBase64] = useState<string | undefined>(
undefined, undefined,
); );
const [connectionString, setConnectionString] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
load("private_key").then((value) => { load("private_key").then((value) => {
@ -58,15 +59,25 @@ function QrCodeLogin() {
setUserId(value); setUserId(value);
} }
}); });
load("ttp_url")
.then((value) => {
if (value) {
const url = new URL(value);
setConnectionString(`@${url.host}`);
} else {
setConnectionString("");
}
})
.catch(() => setConnectionString(""));
}, [load]); }, [load]);
useEffect(() => { useEffect(() => {
if (userId && privateKey) { if (userId && privateKey && connectionString !== null) {
generateQR(`tensamin://tu::${userId}::${privateKey}`).then( generateQR(
setQrCodeBase64, `tensamin://tu::${userId}${connectionString}::${privateKey}`,
); ).then(setQrCodeBase64);
} }
}, [userId, privateKey]); }, [userId, privateKey, connectionString]);
const [qrCodeVisible, setQrCodeVisible] = useState(false); const [qrCodeVisible, setQrCodeVisible] = useState(false);

View file

@ -40,6 +40,7 @@
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2", "@tauri-apps/cli": "^2",
"@types/node": "^25.9.1",
}, },
}, },
"apps/web": { "apps/web": {
@ -55,6 +56,7 @@
"@tensamin/call": "workspace:*", "@tensamin/call": "workspace:*",
"@tensamin/chat": "workspace:*", "@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
"@tensamin/markdown": "workspace:*",
"@tensamin/notifications": "workspace:*", "@tensamin/notifications": "workspace:*",
"@tensamin/shared": "workspace:*", "@tensamin/shared": "workspace:*",
"@tensamin/storage": "workspace:*", "@tensamin/storage": "workspace:*",
@ -169,6 +171,7 @@
"name": "@tensamin/notifications", "name": "@tensamin/notifications",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@tanstack/react-router": "^1.169.1",
"@tauri-apps/api": "^2.11.0", "@tauri-apps/api": "^2.11.0",
"@tensamin/chat": "workspace:*", "@tensamin/chat": "workspace:*",
"@tensamin/crypto": "workspace:*", "@tensamin/crypto": "workspace:*",
@ -258,8 +261,8 @@
}, },
}, },
"overrides": { "overrides": {
"@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.17.tar.gz", "@tensamin/ttp-core": "https://git.methanium.net/tensamin/ttp/archive/0.0.19.tar.gz",
"@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.31.tar.gz", "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.34.tar.gz",
}, },
"packages": { "packages": {
"@antfu/ni": ["@antfu/ni@25.0.0", "", { "dependencies": { "ansis": "^4.0.0", "fzf": "^0.5.2", "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" }, "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs", "nup": "bin/nup.mjs" } }, "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA=="], "@antfu/ni": ["@antfu/ni@25.0.0", "", { "dependencies": { "ansis": "^4.0.0", "fzf": "^0.5.2", "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" }, "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs", "nup": "bin/nup.mjs" } }, "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA=="],
@ -698,9 +701,9 @@
"@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"], "@tensamin/ttp": ["@tensamin/ttp@workspace:packages/ttp"],
"@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.17.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.59.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "zod": "^4.4.1" } }, "sha512-smyx+04hSnWM1oyWJfJrRWmxu7OInwyHeIlaD1h3tUrkrSxiKWHl1qCmxVO1bC+cDwyUXhJ5HUnNCbx1aWx7Dg=="], "@tensamin/ttp-core": ["@tensamin/ttp-core@https://git.methanium.net/tensamin/ttp/archive/0.0.19.tar.gz", { "dependencies": { "@eslint/js": "^10.0.1", "@typescript-eslint/parser": "^8.59.1", "@webtransport-bun/webtransport": "^0.3.0", "globals": "^17.5.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.1", "zod": "^4.4.1" } }, "sha512-La9VqXqJFtzzsRQotXVp+3Vr6u8kj4mQ4wTlSIMRDxKFBnbCvtZyB3V/f8HiIlf7FlZ0Xg0suUUpnamvtcvs9w=="],
"@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.31.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "@tauri-apps/api": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^1.8.0", "next-themes": "^0.4.6", "react-day-picker": "^9.14.0", "react-resizable-panels": "^4.10.0", "recharts": "3.8.0", "shadcn": "^3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.3.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } }, "sha512-I2FhsDtR5ElHwCasf6CfFCiyr5T2Cp9MQ2uAhcMnoNZ0tsK0mJhs+Rz9nUDmysydNyqOj8j/uC0CYXnnG1pCxQ=="], "@tensamin/ui": ["@tensamin/ui@https://git.methanium.net/tensamin/ui/archive/0.0.34.tar.gz", { "dependencies": { "@base-ui/react": "^1.3.0", "@fontsource-variable/inter": "^5.2.6", "@tauri-apps/api": "^2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "lucide-react": "^1.8.0", "next-themes": "^0.4.6", "react-day-picker": "^9.14.0", "react-resizable-panels": "^4.10.0", "recharts": "3.8.0", "shadcn": "^3.5.0", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tw-animate-css": "^1.3.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.2.0", "react-dom": "^19.2.0" } }, "sha512-hp7rV0a0gfD/rNw9et+PqM1PPkgFC6/Z7eYfza6NIp/m+a8/r5Um+S/8tzBDCgZmQC9Y1sJsjesH7mXZz6Jmuw=="],
"@tensamin/user": ["@tensamin/user@workspace:packages/user"], "@tensamin/user": ["@tensamin/user@workspace:packages/user"],
@ -1660,6 +1663,8 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tensamin/tauri/@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
"@tensamin/ui/recharts": ["recharts@3.8.0", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ=="], "@tensamin/ui/recharts": ["recharts@3.8.0", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ=="],
"@tensamin/ui/shadcn": ["shadcn@3.8.5", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA=="], "@tensamin/ui/shadcn": ["shadcn@3.8.5", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-jPRx44e+eyeV7xwY3BLJXcfrks00+M0h5BGB9l6DdcBW4BpAj4x3lVmVy0TXPEs2iHEisxejr62sZAAw6B1EVA=="],
@ -1720,6 +1725,8 @@
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"@tensamin/tauri/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
"@tensamin/ui/shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "@tensamin/ui/shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],

View file

@ -163,18 +163,18 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/@tauri-apps_plugin-opener@2.5.4` - Folder: `licenses/@tauri-apps_plugin-opener@2.5.4`
- Source package dir: `apps/tauri/node_modules/@tauri-apps/plugin-opener` - Source package dir: `apps/tauri/node_modules/@tauri-apps/plugin-opener`
## @tensamin/ttp-core@0.0.15 ## @tensamin/ttp-core@0.0.19
- License: UNKNOWN - License: UNKNOWN
- Included files: LICENSE - Included files: LICENSE
- Folder: `licenses/@tensamin_ttp-core@0.0.15` - Folder: `licenses/@tensamin_ttp-core@0.0.19`
- Source package dir: `packages/ttp/node_modules/@tensamin/ttp-core` - Source package dir: `packages/ttp/node_modules/@tensamin/ttp-core`
## @tensamin/ui@0.0.30 ## @tensamin/ui@0.0.34
- License: UNKNOWN - License: UNKNOWN
- Included files: none found - Included files: none found
- Folder: `licenses/@tensamin_ui@0.0.30` - Folder: `licenses/@tensamin_ui@0.0.34`
- Source package dir: `apps/tauri/node_modules/@tensamin/ui` - Source package dir: `apps/tauri/node_modules/@tensamin/ui`
## @types/node@25.6.0 ## @types/node@25.6.0
@ -449,6 +449,16 @@ Generated from bun.lock and installed packages in workspace node_modules folders
- Folder: `licenses/tailwindcss@4.2.4` - Folder: `licenses/tailwindcss@4.2.4`
- Source package dir: `apps/web/node_modules/tailwindcss` - Source package dir: `apps/web/node_modules/tailwindcss`
## tauri-plugin-app-events-api@0.2.0
- License: MIT
- Homepage: https://github.com/wtto00/tauri-plugin-app-events#readme
- Repository: git+https://github.com/wtto00/tauri-plugin-app-events.git
- Description: A plugin for tauri@v2 to listen some events on iOS and Android.
- Included files: LICENSE
- Folder: `licenses/tauri-plugin-app-events-api@0.2.0`
- Source package dir: `apps/web/node_modules/tauri-plugin-app-events-api`
## tw-animate-css@1.4.0 ## tw-animate-css@1.4.0
- License: MIT - License: MIT

View file

@ -3,7 +3,7 @@
"specVersion": "1.5", "specVersion": "1.5",
"version": 1, "version": 1,
"metadata": { "metadata": {
"timestamp": "2026-05-04T13:05:17.472Z", "timestamp": "2026-05-21T10:15:15.541Z",
"tools": [ "tools": [
{ {
"vendor": "OpenAI", "vendor": "OpenAI",
@ -580,15 +580,15 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40tensamin/ttp-core@0.0.15", "bomRef": "pkg:npm/%40tensamin/ttp-core@0.0.19",
"name": "@tensamin/ttp-core", "name": "@tensamin/ttp-core",
"version": "0.0.15", "version": "0.0.19",
"purl": "pkg:npm/%40tensamin/ttp-core@0.0.15", "purl": "pkg:npm/%40tensamin/ttp-core@0.0.19",
"externalReferences": [], "externalReferences": [],
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@tensamin_ttp-core@0.0.15" "value": "licenses/@tensamin_ttp-core@0.0.19"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -598,15 +598,15 @@
}, },
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/%40tensamin/ui@0.0.30", "bomRef": "pkg:npm/%40tensamin/ui@0.0.34",
"name": "@tensamin/ui", "name": "@tensamin/ui",
"version": "0.0.30", "version": "0.0.34",
"purl": "pkg:npm/%40tensamin/ui@0.0.30", "purl": "pkg:npm/%40tensamin/ui@0.0.34",
"externalReferences": [], "externalReferences": [],
"properties": [ "properties": [
{ {
"name": "local:licenseFolder", "name": "local:licenseFolder",
"value": "licenses/@tensamin_ui@0.0.30" "value": "licenses/@tensamin_ui@0.0.34"
}, },
{ {
"name": "local:sourcePackageDir", "name": "local:sourcePackageDir",
@ -1562,6 +1562,41 @@
} }
] ]
}, },
{
"type": "library",
"bomRef": "pkg:npm/tauri-plugin-app-events-api@0.2.0",
"name": "tauri-plugin-app-events-api",
"version": "0.2.0",
"purl": "pkg:npm/tauri-plugin-app-events-api@0.2.0",
"description": "A plugin for tauri@v2 to listen some events on iOS and Android.",
"licenses": [
{
"license": {
"id": "MIT"
}
}
],
"externalReferences": [
{
"type": "website",
"url": "https://github.com/wtto00/tauri-plugin-app-events#readme"
},
{
"type": "vcs",
"url": "git+https://github.com/wtto00/tauri-plugin-app-events.git"
}
],
"properties": [
{
"name": "local:licenseFolder",
"value": "licenses/tauri-plugin-app-events-api@0.2.0"
},
{
"name": "local:sourcePackageDir",
"value": "apps/web/node_modules/tauri-plugin-app-events-api"
}
]
},
{ {
"type": "library", "type": "library",
"bomRef": "pkg:npm/tw-animate-css@1.4.0", "bomRef": "pkg:npm/tw-animate-css@1.4.0",

View file

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

View file

@ -1,6 +1,6 @@
{ {
"generatedAt": "2026-05-04T13:05:17.471Z", "generatedAt": "2026-05-21T10:15:15.540Z",
"packageCount": 53, "packageCount": 54,
"packages": [ "packages": [
{ {
"name": "@codemirror/commands", "name": "@codemirror/commands",
@ -227,7 +227,7 @@
}, },
{ {
"name": "@tensamin/ttp-core", "name": "@tensamin/ttp-core",
"version": "0.0.15", "version": "0.0.19",
"license": "UNKNOWN", "license": "UNKNOWN",
"homepage": null, "homepage": null,
"repository": null, "repository": null,
@ -235,18 +235,18 @@
"files": [ "files": [
"LICENSE" "LICENSE"
], ],
"licenseFolder": "licenses/@tensamin_ttp-core@0.0.15", "licenseFolder": "licenses/@tensamin_ttp-core@0.0.19",
"sourcePackageDir": "packages/ttp/node_modules/@tensamin/ttp-core" "sourcePackageDir": "packages/ttp/node_modules/@tensamin/ttp-core"
}, },
{ {
"name": "@tensamin/ui", "name": "@tensamin/ui",
"version": "0.0.30", "version": "0.0.34",
"license": "UNKNOWN", "license": "UNKNOWN",
"homepage": null, "homepage": null,
"repository": null, "repository": null,
"description": null, "description": null,
"files": [], "files": [],
"licenseFolder": "licenses/@tensamin_ui@0.0.30", "licenseFolder": "licenses/@tensamin_ui@0.0.34",
"sourcePackageDir": "apps/tauri/node_modules/@tensamin/ui" "sourcePackageDir": "apps/tauri/node_modules/@tensamin/ui"
}, },
{ {
@ -614,6 +614,19 @@
"licenseFolder": "licenses/tailwindcss@4.2.4", "licenseFolder": "licenses/tailwindcss@4.2.4",
"sourcePackageDir": "apps/web/node_modules/tailwindcss" "sourcePackageDir": "apps/web/node_modules/tailwindcss"
}, },
{
"name": "tauri-plugin-app-events-api",
"version": "0.2.0",
"license": "MIT",
"homepage": "https://github.com/wtto00/tauri-plugin-app-events#readme",
"repository": "git+https://github.com/wtto00/tauri-plugin-app-events.git",
"description": "A plugin for tauri@v2 to listen some events on iOS and Android.",
"files": [
"LICENSE"
],
"licenseFolder": "licenses/tauri-plugin-app-events-api@0.2.0",
"sourcePackageDir": "apps/web/node_modules/tauri-plugin-app-events-api"
},
{ {
"name": "tw-animate-css", "name": "tw-animate-css",
"version": "1.4.0", "version": "1.4.0",

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