diff --git a/.forgejo/workflows/deploy-dev.yml b/.forgejo/workflows/deploy-dev.yml
index 26053ff..ab7edf1 100644
--- a/.forgejo/workflows/deploy-dev.yml
+++ b/.forgejo/workflows/deploy-dev.yml
@@ -4,20 +4,31 @@ on:
- dev
jobs:
- deploy:
+ build-web:
runs-on: docker
-
steps:
- name: Check out repo
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
- name: Install dependencies
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
- name: Install rsync
@@ -25,3 +36,204 @@ jobs:
- name: Deploy
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 --list '*-prod' --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
diff --git a/.forgejo/workflows/deploy-prod.yml b/.forgejo/workflows/deploy-prod.yml
index 1df5b60..7cd9016 100644
--- a/.forgejo/workflows/deploy-prod.yml
+++ b/.forgejo/workflows/deploy-prod.yml
@@ -4,9 +4,8 @@ on:
- main
jobs:
- deploy:
+ build-web:
runs-on: docker
-
steps:
- name: Check out repo
uses: https://data.forgejo.org/actions/checkout@v4
@@ -23,16 +22,14 @@ jobs:
- name: Install dependencies
run: bun install --frozen-lockfile
- - 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: Copy licenses
+ run: bun run copy-licenses
- - name: Build
- run: bun run build:apps
+ - name: Build packages
+ run: bun run build:packages
+
+ - name: Build web
+ run: bun run build:web
- name: Install rsync
run: apt-get update && apt-get install -y rsync
@@ -40,12 +37,117 @@ jobs:
- name: Deploy
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
id: version
run: |
VERSION="$(node -p "require('./package.json').version")"
echo "version=$VERSION" >> "$FORGEJO_OUTPUT"
- echo "tag=v$VERSION" >> "$FORGEJO_OUTPUT"
+ echo "tag=${VERSION}-prod" >> "$FORGEJO_OUTPUT"
- name: Create release and upload files
env:
@@ -54,39 +156,40 @@ jobs:
REPO: ${{ forgejo.repository }}
SHA: ${{ forgejo.sha }}
TAG: ${{ steps.version.outputs.tag }}
- VERSION: ${{ steps.version.outputs.version }}
run: |
set -eu
test -d releases
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")
if [ "$HTTP_STATUS" = "200" ]; then
echo "Release $TAG already exists."
- RELEASE_ID="$(jq -r .id release_out.json)"
- else
- echo "Creating new release for $TAG"
- RELEASE_JSON="$(curl -f -sS -X POST "$API/repos/$REPO/releases" \
- -H "Authorization: token $TOKEN" \
- -H "Content-Type: application/json" \
- -d "$(jq -n \
- --arg tag "$TAG" \
- --arg name "$TAG" \
- --arg body "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)"
+ exit 0
fi
+ echo "Creating new release for $TAG"
+ RELEASE_JSON="$(curl -f -sS -X POST "$API/repos/$REPO/releases" \
+ -H "Authorization: token $TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "$(jq -n \
+ --arg tag "$TAG" \
+ --arg name "$TAG" \
+ --arg body "$COMMIT_MSG" \
+ --arg target "$SHA" \
+ '{
+ tag_name: $tag,
+ name: $name,
+ body: $body,
+ target_commitish: $target,
+ draft: false,
+ prerelease: false
+ }')")"
+ RELEASE_ID="$(echo "$RELEASE_JSON" | jq -r .id)"
+
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" \
diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx
index ce9bb6b..6b4b6cd 100644
--- a/apps/web/src/components/sidebar.tsx
+++ b/apps/web/src/components/sidebar.tsx
@@ -6,6 +6,7 @@ import {
Sidebar as SidebarRoot,
SidebarContent,
SidebarFooter,
+ useSidebar,
} from "@tensamin/ui";
import { isTauri } from "@tauri-apps/api/core";
import { useIsMobile } from "@tensamin/ui";
@@ -13,17 +14,21 @@ import { MobileNavbar } from "./navbar";
import SidebarBox from "@tensamin/call/sidebarBox";
import { useShowMobileNavbar } from "@/routes/app/layout";
-
/**
* Renders the conversation sidebar with account summary and conversation list.
+ * On mobile the sidebar is always kept in the DOM and hidden via CSS
+ * (opacity + translateX) instead of being unmounted. This keeps the DOM and
+ * React state alive while the drawer is closed, allowing the sidebar to open
+ * instantly on subsequent toggles.
* @returns Sidebar JSX.
*/
export default function Sidebar() {
const isMobile = useIsMobile();
const showMobileNavbar = useShowMobileNavbar();
+ const { openMobile, setOpenMobile } = useSidebar();
- return (
-