diff --git a/.github/workflows/cd-macmini.yml b/.github/workflows/cd-macmini.yml
index 35728bef7..fee1c4df9 100644
--- a/.github/workflows/cd-macmini.yml
+++ b/.github/workflows/cd-macmini.yml
@@ -24,7 +24,6 @@ on:
type: choice
options:
- all
- - matrix-web
- mana-auth
- mana-sync
- mana-media
@@ -33,7 +32,6 @@ on:
- mana-crawler
- mana-credits
- mana-search
- - mana-matrix-bot
- chat-backend
- chat-web
- todo-backend
@@ -59,8 +57,6 @@ env:
PROJECT_DIR: /Users/mana/projects/mana-monorepo
COMPOSE_FILE: docker-compose.macmini.yml
ENV_FILE: .env.macmini
- DEPLOY_NOTIFY_ROOM_ID: ${{ secrets.DEPLOY_NOTIFY_ROOM_ID }}
- DEPLOY_NOTIFY_BOT_TOKEN: ${{ secrets.DEPLOY_NOTIFY_BOT_TOKEN }}
DOCKER_BUILDKIT: 1
PATH: /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin
@@ -73,7 +69,6 @@ jobs:
runs-on: self-hosted
if: github.event_name == 'push'
outputs:
- matrix-web: ${{ steps.changes.outputs.matrix-web }}
mana-auth: ${{ steps.changes.outputs.mana-auth }}
mana-sync: ${{ steps.changes.outputs.mana-sync }}
mana-media: ${{ steps.changes.outputs.mana-media }}
@@ -98,7 +93,6 @@ jobs:
memoro-server: ${{ steps.changes.outputs.memoro-server }}
memoro-audio-server: ${{ steps.changes.outputs.memoro-audio-server }}
memoro-web: ${{ steps.changes.outputs.memoro-web }}
- mana-matrix-bot: ${{ steps.changes.outputs.mana-matrix-bot }}
any-changes: ${{ steps.changes.outputs.any-changes }}
steps:
- name: Check for changes
@@ -139,7 +133,6 @@ jobs:
echo "Shared packages changed: $SHARED_CHANGED"
echo ""
- check_changes "matrix-web" "apps/matrix/apps/web/" "apps/matrix/packages/"
check_changes "mana-auth" "services/mana-auth/"
check_changes "mana-sync" "services/mana-sync/" "packages/shared-go/"
check_changes "mana-media" "services/mana-media/"
@@ -165,12 +158,11 @@ jobs:
check_changes "memoro-server" "apps/memoro/apps/server/" "apps/memoro/packages/"
check_changes "memoro-audio-server" "apps/memoro/apps/audio-server/"
check_changes "memoro-web" "apps/memoro/apps/web/" "apps/memoro/packages/"
- check_changes "mana-matrix-bot" "services/mana-matrix-bot/"
check_changes "mana-landing-builder" "services/mana-landing-builder/" "packages/shared-types/" "packages/shared-landing-ui/"
# Check if anything needs deploying
ANY="false"
- for svc in matrix-web mana-auth mana-sync mana-media mana-notify mana-api-gateway mana-crawler mana-credits mana-search chat-backend chat-web todo-backend todo-web calendar-backend calendar-web clock-web contacts-backend contacts-web music-backend music-web storage-backend storage-web memoro-server memoro-audio-server memoro-web mana-matrix-bot mana-landing-builder; do
+ for svc in mana-auth mana-sync mana-media mana-notify mana-api-gateway mana-crawler mana-credits mana-search chat-backend chat-web todo-backend todo-web calendar-backend calendar-web clock-web contacts-backend contacts-web music-backend music-web storage-backend storage-web memoro-server memoro-audio-server memoro-web mana-landing-builder; do
val=$(grep "^$svc=" $GITHUB_OUTPUT | tail -1 | cut -d= -f2)
if [ "$val" == "true" ]; then
ANY="true"
@@ -231,7 +223,6 @@ jobs:
fi
else
# Build list from detected changes
- if [ "${{ needs.detect-changes.outputs.matrix-web }}" == "true" ]; then SERVICES="$SERVICES matrix-web"; fi
if [ "${{ needs.detect-changes.outputs.mana-auth }}" == "true" ]; then SERVICES="$SERVICES mana-auth"; fi
if [ "${{ needs.detect-changes.outputs.mana-sync }}" == "true" ]; then SERVICES="$SERVICES mana-sync"; fi
if [ "${{ needs.detect-changes.outputs.mana-media }}" == "true" ]; then SERVICES="$SERVICES mana-media"; fi
@@ -257,7 +248,6 @@ jobs:
if [ "${{ needs.detect-changes.outputs.memoro-server }}" == "true" ]; then SERVICES="$SERVICES memoro-server"; fi
if [ "${{ needs.detect-changes.outputs.memoro-audio-server }}" == "true" ]; then SERVICES="$SERVICES memoro-audio-server"; fi
if [ "${{ needs.detect-changes.outputs.memoro-web }}" == "true" ]; then SERVICES="$SERVICES memoro-web"; fi
- if [ "${{ needs.detect-changes.outputs.mana-matrix-bot }}" == "true" ]; then SERVICES="$SERVICES mana-matrix-bot"; fi
if [ "${{ needs.detect-changes.outputs.mana-landing-builder }}" == "true" ]; then SERVICES="$SERVICES mana-landing-builder"; fi
fi
@@ -344,7 +334,6 @@ jobs:
health_url_for() {
case "$1" in
mana-auth) echo "http://localhost:3001/health" ;;
- matrix-web) echo "http://localhost:5180/health" ;;
chat-backend) echo "http://localhost:3030/health" ;;
chat-web) echo "http://localhost:5010/health" ;;
todo-backend) echo "http://localhost:3031/health" ;;
@@ -371,7 +360,7 @@ jobs:
SERVICES="${{ steps.services.outputs.services }}"
if [ "$DEPLOY_ALL" == "true" ]; then
- SERVICES="mana-auth matrix-web chat-backend chat-web todo-backend todo-web calendar-backend calendar-web clock-web contacts-backend contacts-web music-backend music-web storage-backend storage-web memoro-server memoro-audio-server memoro-web"
+ SERVICES="mana-auth chat-backend chat-web todo-backend todo-web calendar-backend calendar-web clock-web contacts-backend contacts-web music-backend music-web storage-backend storage-web memoro-server memoro-audio-server memoro-web"
fi
HEALTH_RESULTS=""
@@ -499,23 +488,8 @@ jobs:
COMMIT_MSG=$(git log -1 --pretty=%s 2>/dev/null | head -c 100)
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
- MSG="⚠️ **Deploy failed**\n\n**Services:** ${SERVICES}\n**Commit:** ${COMMIT_MSG}\n**By:** ${{ github.actor }}\n**[View logs](${RUN_URL})**"
-
- # Send to Matrix deploy-notifications room via Synapse API
- ROOM_ID="${DEPLOY_NOTIFY_ROOM_ID:-}"
- BOT_TOKEN="${DEPLOY_NOTIFY_BOT_TOKEN:-}"
- if [ -n "$ROOM_ID" ] && [ -n "$BOT_TOKEN" ]; then
- TXN_ID="deploy-$(date +%s)"
- curl -s -X PUT \
- "http://localhost:8008/_matrix/client/v3/rooms/${ROOM_ID}/send/m.room.message/${TXN_ID}" \
- -H "Authorization: Bearer ${BOT_TOKEN}" \
- -H "Content-Type: application/json" \
- -d "{\"msgtype\":\"m.text\",\"body\":\"Deploy failed: ${SERVICES}\",\"format\":\"org.matrix.custom.html\",\"formatted_body\":\"$(echo -e "$MSG" | sed 's/"/\\"/g')\"}" \
- || true
- echo "Matrix notification sent"
- else
- echo "Matrix notification skipped (DEPLOY_NOTIFY_ROOM_ID or DEPLOY_NOTIFY_BOT_TOKEN not set)"
- fi
+ MSG="⚠️ Deploy failed: ${SERVICES} (commit ${COMMIT_MSG} by ${{ github.actor }}) — ${RUN_URL}"
+ echo "$MSG"
- name: Cleanup old images
if: always()
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 632e4bc99..d61c830b1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -74,7 +74,6 @@ jobs:
nutriphi-backend: ${{ steps.changes.outputs.nutriphi-backend }}
nutriphi-web: ${{ steps.changes.outputs.nutriphi-web }}
skilltree-web: ${{ steps.changes.outputs.skilltree-web }}
- mana-matrix-bot: ${{ steps.changes.outputs.mana-matrix-bot }}
any-changes: ${{ steps.changes.outputs.any-changes }}
steps:
- name: Checkout code
@@ -113,7 +112,6 @@ jobs:
echo "nutriphi-backend=true" >> $GITHUB_OUTPUT
echo "nutriphi-web=true" >> $GITHUB_OUTPUT
echo "skilltree-web=true" >> $GITHUB_OUTPUT
- echo "mana-matrix-bot=true" >> $GITHUB_OUTPUT
echo "any-changes=true" >> $GITHUB_OUTPUT
exit 0
fi
@@ -156,7 +154,6 @@ jobs:
echo "nutriphi-backend=true" >> $GITHUB_OUTPUT
echo "nutriphi-web=true" >> $GITHUB_OUTPUT
echo "skilltree-web=true" >> $GITHUB_OUTPUT
- echo "mana-matrix-bot=true" >> $GITHUB_OUTPUT
echo "any-changes=true" >> $GITHUB_OUTPUT
exit 0
fi
@@ -370,14 +367,6 @@ jobs:
echo "skilltree-web=false" >> $GITHUB_OUTPUT
fi
- # mana-matrix-bot (consolidated Go bot)
- MANA_MATRIX_BOT_CHANGED=$(check_pattern "services/mana-matrix-bot/")
- if [ "$MANA_MATRIX_BOT_CHANGED" == "true" ]; then
- echo "mana-matrix-bot=true" >> $GITHUB_OUTPUT
- else
- echo "mana-matrix-bot=false" >> $GITHUB_OUTPUT
- fi
-
# zitare-backend: REMOVED — migrated to local-first
# Check if any service needs building
@@ -1221,38 +1210,5 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
- # ===========================================
- # Matrix Bots
- # ===========================================
-
- build-mana-matrix-bot:
- name: Build mana-matrix-bot (Go)
- runs-on: ubuntu-latest
- needs: detect-changes
- if: needs.detect-changes.outputs.mana-matrix-bot == 'true'
- steps:
- - uses: actions/checkout@v4
- - uses: docker/setup-qemu-action@v3
- - uses: docker/setup-buildx-action@v3
- - uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - uses: docker/metadata-action@v5
- id: meta
- with:
- images: ghcr.io/${{ github.repository_owner }}/mana-matrix-bot
- tags: type=raw,value=latest
- - uses: docker/build-push-action@v5
- with:
- context: .
- file: services/mana-matrix-bot/Dockerfile
- platforms: linux/amd64,linux/arm64
- push: true
- tags: ${{ steps.meta.outputs.tags }}
- cache-from: type=gha
- cache-to: type=gha,mode=max
-
# ===========================================
# Zitare Backend: REMOVED — migrated to local-first
diff --git a/CLAUDE.md b/CLAUDE.md
index 324548a32..6dd80ea0c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -23,7 +23,7 @@ apps/
├── mana/ # Unified frontend (SvelteKit web + Expo mobile + Astro landing)
├── api/ # Unified backend API (Hono/Bun) — @mana/api
├── {product}/ # Per-product landing pages, mobile apps, packages
-│ # Standalone (own container, not unified): matrix, manavoxel
+│ # Standalone (own container, not unified): manavoxel
games/ # arcade, voxelava, whopixels, worldream
services/ # Backend services (Hono/Bun, Go, Python) — see list below
packages/ # Shared workspace packages (@mana/*)
@@ -33,7 +33,7 @@ docs/ # Long-form docs (deployment, hardware, postmortems, etc.)
### Active services (`services/`)
-`mana-auth` (3001), `mana-sync` (3050), `mana-credits`, `mana-user`, `mana-subscriptions`, `mana-analytics`, `mana-search` (3021), `mana-crawler`, `mana-api-gateway`, `mana-notify`, `mana-matrix-bot`, `mana-media`, `mana-llm`, `mana-image-gen`, `mana-video-gen`, `mana-stt`, `mana-tts`, `mana-voice-bot`, `mana-events`, `mana-landing-builder`. Each non-trivial service has its own `CLAUDE.md`.
+`mana-auth` (3001), `mana-sync` (3050), `mana-credits`, `mana-user`, `mana-subscriptions`, `mana-analytics`, `mana-search` (3021), `mana-crawler`, `mana-api-gateway`, `mana-notify`, `mana-media`, `mana-llm`, `mana-image-gen`, `mana-video-gen`, `mana-stt`, `mana-tts`, `mana-voice-bot`, `mana-events`, `mana-landing-builder`. Each non-trivial service has its own `CLAUDE.md`.
## Coding Guidelines
diff --git a/apps/mana/apps/landing/src/components/navigation/Footer.astro b/apps/mana/apps/landing/src/components/navigation/Footer.astro
index d3fc41eac..689d92cb7 100644
--- a/apps/mana/apps/landing/src/components/navigation/Footer.astro
+++ b/apps/mana/apps/landing/src/components/navigation/Footer.astro
@@ -28,7 +28,6 @@ const ecosystemApps = [
{ label: 'Taktik', href: 'https://times.mana.how', status: 'alpha' },
{ label: 'uLoad', href: 'https://ulo.ad', status: 'alpha' },
{ label: 'Playground', href: 'https://playground.mana.how', status: 'alpha' },
- { label: 'Matrix', href: 'https://matrix.mana.how', status: 'alpha' },
];
const platformLinks = [
@@ -36,7 +35,6 @@ const platformLinks = [
{ label: 'Observatory', href: 'https://mana.how/observatory' },
{ label: 'Alle Apps', href: '/apps' },
{ label: 'Preise', href: '/pricing' },
- { label: 'Matrix Chat', href: 'https://element.mana.how' },
];
const monitoringLinks = [
@@ -223,11 +221,6 @@ function statusColor(status: string) {
GitHub
-
-
-
diff --git a/apps/mana/apps/landing/src/pages/apps/index.astro b/apps/mana/apps/landing/src/pages/apps/index.astro
index 2e54a2966..93f628fd3 100644
--- a/apps/mana/apps/landing/src/pages/apps/index.astro
+++ b/apps/mana/apps/landing/src/pages/apps/index.astro
@@ -67,12 +67,6 @@ const sections: Section[] = [
{ name: 'Traces', icon: 'ph:map-trifold-bold', tagline: 'Stadt-Erkundung', url: 'https://traces.mana.how' },
],
},
- {
- label: 'Kommunikation',
- apps: [
- { name: 'Matrix', icon: 'ph:chats-bold', tagline: 'Matrix Chat', url: 'https://matrix.mana.how' },
- ],
- },
];
---
diff --git a/apps/mana/apps/web/src/lib/api/gifts.ts b/apps/mana/apps/web/src/lib/api/gifts.ts
index e80e6da1d..9be0bd733 100644
--- a/apps/mana/apps/web/src/lib/api/gifts.ts
+++ b/apps/mana/apps/web/src/lib/api/gifts.ts
@@ -71,7 +71,6 @@ export interface CreateGiftRequest {
type?: 'simple' | 'personalized' | 'split' | 'first_come' | 'riddle';
portions?: number;
targetEmail?: string;
- targetMatrixId?: string;
riddleQuestion?: string;
riddleAnswer?: string;
message?: string;
diff --git a/apps/mana/apps/web/src/lib/components/observatory/data/layout.ts b/apps/mana/apps/web/src/lib/components/observatory/data/layout.ts
index b620a8d95..ae6038274 100644
--- a/apps/mana/apps/web/src/lib/components/observatory/data/layout.ts
+++ b/apps/mana/apps/web/src/lib/components/observatory/data/layout.ts
@@ -163,7 +163,6 @@ export const APP_POSITIONS: Record Application > Manifest for errors
-
-## Related Documentation
-
-- [Matrix Client-Server API](https://spec.matrix.org/latest/client-server-api/)
-- [matrix-js-sdk docs](https://matrix-org.github.io/matrix-js-sdk/)
-- [Synapse Admin API](https://element-hq.github.io/synapse/latest/admin_api/)
-- [Vite PWA Plugin](https://vite-pwa-org.netlify.app/frameworks/sveltekit.html)
diff --git a/apps/matrix/apps/mobile/.gitignore b/apps/matrix/apps/mobile/.gitignore
deleted file mode 100644
index 526492a93..000000000
--- a/apps/matrix/apps/mobile/.gitignore
+++ /dev/null
@@ -1,21 +0,0 @@
-node_modules/
-.expo/
-dist/
-build/
-ios/
-android/
-*.jks
-*.p8
-*.p12
-*.key
-*.mobileprovision
-*.orig.*
-web-build/
-.env
-.env.local
-
-# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
-# The following patterns were generated by expo-cli
-
-expo-env.d.ts
-# @end expo-cli
\ No newline at end of file
diff --git a/apps/matrix/apps/mobile/app.json b/apps/matrix/apps/mobile/app.json
deleted file mode 100644
index f16f88740..000000000
--- a/apps/matrix/apps/mobile/app.json
+++ /dev/null
@@ -1,82 +0,0 @@
-{
- "expo": {
- "name": "Manalink",
- "slug": "manalink",
- "owner": "tilljs",
- "version": "1.0.0",
- "scheme": "manalink",
- "orientation": "portrait",
- "icon": "./assets/icon.png",
- "userInterfaceStyle": "automatic",
- "splash": {
- "image": "./assets/splash.png",
- "resizeMode": "contain",
- "backgroundColor": "#0f0f0f"
- },
- "assetBundlePatterns": ["**/*"],
- "ios": {
- "supportsTablet": true,
- "bundleIdentifier": "how.mana.manalink",
- "infoPlist": {
- "ITSAppUsesNonExemptEncryption": false
- }
- },
- "android": {
- "adaptiveIcon": {
- "foregroundImage": "./assets/adaptive-icon.png",
- "backgroundColor": "#0f0f0f"
- },
- "package": "how.mana.manalink",
- "permissions": [
- "android.permission.RECORD_AUDIO",
- "android.permission.MODIFY_AUDIO_SETTINGS",
- "android.permission.READ_EXTERNAL_STORAGE",
- "android.permission.WRITE_EXTERNAL_STORAGE"
- ]
- },
- "web": {
- "bundler": "metro",
- "output": "static",
- "favicon": "./assets/favicon.png"
- },
- "plugins": [
- "expo-router",
- "expo-secure-store",
- "expo-audio",
- [
- "expo-image-picker",
- {
- "photosPermission": "Allow Manalink to select photos for sharing.",
- "cameraPermission": "Allow Manalink to take photos for sharing."
- }
- ],
- [
- "expo-media-library",
- {
- "photosPermission": "Allow Manalink to save images to your library.",
- "savePhotosPermission": "Allow Manalink to save images."
- }
- ],
- [
- "expo-notifications",
- {
- "icon": "./assets/notification-icon.png",
- "color": "#7c6bff",
- "sounds": []
- }
- ]
- ],
- "experiments": {
- "typedRoutes": true,
- "tsconfigPaths": true
- },
- "extra": {
- "router": {
- "origin": false
- },
- "eas": {
- "projectId": "a4c5098c-fcae-474e-95b2-13394d8b323d"
- }
- }
- }
-}
diff --git a/apps/matrix/apps/mobile/app/(app)/_layout.tsx b/apps/matrix/apps/mobile/app/(app)/_layout.tsx
deleted file mode 100644
index e030b0517..000000000
--- a/apps/matrix/apps/mobile/app/(app)/_layout.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { View, Text } from 'react-native';
-import { Tabs } from 'expo-router';
-import { ChatCircle, User, Bell, GearSix } from 'phosphor-react-native';
-import { useMatrixStore } from '~/src/matrix/store';
-
-const BG = '#0f0f0f';
-const BORDER = '#2a2a2a';
-const ACTIVE = '#7c6bff';
-const INACTIVE = '#6b7280';
-const SIZE = 22;
-
-function InviteBadge({ count }: { count: number }) {
- if (count === 0) return null;
- return (
-
-
- {count > 9 ? '9+' : count}
-
-
- );
-}
-
-export default function AppLayout() {
- const invites = useMatrixStore((s) => s.invites);
-
- return (
-
- (
-
- ),
- }}
- />
- (
-
- ),
- }}
- />
- (
-
-
-
-
- ),
- }}
- />
- (
-
- ),
- }}
- />
-
- );
-}
diff --git a/apps/matrix/apps/mobile/app/(app)/dms.tsx b/apps/matrix/apps/mobile/app/(app)/dms.tsx
deleted file mode 100644
index 7ca1ad97c..000000000
--- a/apps/matrix/apps/mobile/app/(app)/dms.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { useState, useMemo } from 'react';
-import { View, Text, FlatList, Pressable, ActivityIndicator, TextInput } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
-import { useRouter } from 'expo-router';
-import { Plus, MagnifyingGlass } from 'phosphor-react-native';
-import { useMatrixStore } from '~/src/matrix/store';
-import RoomListItem from '~/src/components/RoomListItem';
-import SyncStatusBar from '~/src/components/SyncStatusBar';
-
-export default function DMsScreen() {
- const { rooms, syncState, isReady, selectRoom } = useMatrixStore();
- const router = useRouter();
- const [search, setSearch] = useState('');
-
- const dmRooms = useMemo(() => {
- const base = rooms.filter((r) => r.isDirect && r.membership === 'join');
- if (!search.trim()) return base;
- const q = search.toLowerCase();
- return base.filter((r) => r.name.toLowerCase().includes(q));
- }, [rooms, search]);
-
- const dmInvites = useMemo(
- () => rooms.filter((r) => r.membership === 'invite' && r.isDirect),
- [rooms]
- );
-
- const handleRoomPress = (roomId: string) => {
- selectRoom(roomId);
- router.push(`/room/${roomId}`);
- };
-
- return (
-
-
-
-
- Direct Messages
- router.push('/room/new')}
- className="w-9 h-9 bg-primary rounded-full items-center justify-center active:opacity-70"
- >
-
-
-
-
- {(dmRooms.length > 0 || search.length > 0) && (
-
-
-
-
- )}
-
- {!isReady && syncState === 'STOPPED' ? (
-
-
-
- ) : (
- item.id}
- renderItem={({ item }) => (
- handleRoomPress(item.id)} />
- )}
- contentContainerStyle={{ paddingBottom: 16 }}
- ListHeaderComponent={
- dmInvites.length > 0 ? (
-
-
- {dmInvites.length} pending invite{dmInvites.length !== 1 ? 's' : ''}
-
-
- ) : null
- }
- ListEmptyComponent={
-
-
- {search ? 'No people found' : 'No direct messages'}
-
- {!search && (
-
- Tap + to start a conversation
-
- )}
-
- }
- />
- )}
-
- );
-}
diff --git a/apps/matrix/apps/mobile/app/(app)/index.tsx b/apps/matrix/apps/mobile/app/(app)/index.tsx
deleted file mode 100644
index cb30e8bbb..000000000
--- a/apps/matrix/apps/mobile/app/(app)/index.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { useState, useMemo } from 'react';
-import { View, Text, FlatList, Pressable, ActivityIndicator, TextInput } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
-import { useRouter } from 'expo-router';
-import { Plus, MagnifyingGlass, Compass } from 'phosphor-react-native';
-import { useMatrixStore } from '~/src/matrix/store';
-import RoomListItem from '~/src/components/RoomListItem';
-import SyncStatusBar from '~/src/components/SyncStatusBar';
-
-export default function ChatsScreen() {
- const { rooms, syncState, isReady, selectRoom } = useMatrixStore();
- const router = useRouter();
- const [search, setSearch] = useState('');
-
- const groupRooms = useMemo(() => {
- const base = rooms.filter((r) => !r.isDirect && r.membership === 'join');
- if (!search.trim()) return base;
- const q = search.toLowerCase();
- return base.filter((r) => r.name.toLowerCase().includes(q));
- }, [rooms, search]);
-
- // Pending invites
- const invites = useMemo(
- () => rooms.filter((r) => r.membership === 'invite' && !r.isDirect),
- [rooms]
- );
-
- const handleRoomPress = (roomId: string) => {
- selectRoom(roomId);
- router.push(`/room/${roomId}`);
- };
-
- return (
-
-
-
- {/* Header */}
-
- Chats
-
- router.push('/search')}
- className="w-9 h-9 bg-surface border border-border rounded-full items-center justify-center active:opacity-70"
- >
-
-
- router.push('/room/new')}
- className="w-9 h-9 bg-primary rounded-full items-center justify-center active:opacity-70"
- >
-
-
-
-
-
- {/* Search */}
- {(groupRooms.length > 0 || search.length > 0) && (
-
-
-
-
- )}
-
- {/* Loading state */}
- {!isReady && syncState === 'STOPPED' ? (
-
-
- Connecting...
-
- ) : (
- item.id}
- renderItem={({ item }) => (
- handleRoomPress(item.id)} />
- )}
- contentContainerStyle={{ paddingBottom: 16 }}
- ListHeaderComponent={
- invites.length > 0 ? (
-
-
- {invites.length} pending invite{invites.length !== 1 ? 's' : ''}
-
-
- ) : null
- }
- ListEmptyComponent={
-
-
- {search ? 'No rooms found' : 'No group chats yet'}
-
- {!search && (
-
- Tap + to create or join a room
-
- )}
-
- }
- />
- )}
-
- );
-}
diff --git a/apps/matrix/apps/mobile/app/(app)/invites.tsx b/apps/matrix/apps/mobile/app/(app)/invites.tsx
deleted file mode 100644
index 7307f3836..000000000
--- a/apps/matrix/apps/mobile/app/(app)/invites.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { View, Text, FlatList, Pressable, ActivityIndicator, Alert } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
-import { Image } from 'expo-image';
-import { useMatrixStore } from '~/src/matrix/store';
-import type { SimpleRoom } from '~/src/matrix/types';
-
-function InviteCard({
- room,
- onAccept,
- onDecline,
-}: {
- room: SimpleRoom;
- onAccept: () => void;
- onDecline: () => void;
-}) {
- return (
-
-
- {/* Avatar */}
-
- {room.avatar ? (
-
- ) : (
-
- {(room.name ?? '?')[0].toUpperCase()}
-
- )}
-
-
- {/* Info */}
-
-
- {room.name}
-
- {room.topic && (
-
- {room.topic}
-
- )}
- {room.inviter && (
- Invited by {room.inviter}
- )}
-
-
- {room.isDirect
- ? 'Direct message'
- : `${room.memberCount} member${room.memberCount !== 1 ? 's' : ''}`}
-
- {room.isEncrypted && · 🔒 Encrypted }
-
-
-
-
- {/* Actions */}
-
-
- Decline
-
-
- Accept
-
-
-
- );
-}
-
-export default function InvitesScreen() {
- const { invites, acceptInvite, declineInvite, isReady } = useMatrixStore();
-
- const handleAccept = async (roomId: string) => {
- try {
- await acceptInvite(roomId);
- } catch (err) {
- Alert.alert('Error', err instanceof Error ? err.message : 'Could not join room');
- }
- };
-
- const handleDecline = (roomId: string, roomName: string) => {
- Alert.alert(`Decline invite`, `Decline invite to "${roomName}"?`, [
- { text: 'Cancel', style: 'cancel' },
- {
- text: 'Decline',
- style: 'destructive',
- onPress: () => declineInvite(roomId).catch(() => {}),
- },
- ]);
- };
-
- return (
-
-
- Invites
- {invites.length > 0 && (
-
- {invites.length}
-
- )}
-
-
- {!isReady ? (
-
-
-
- ) : (
- item.id}
- renderItem={({ item }) => (
- handleAccept(item.id)}
- onDecline={() => handleDecline(item.id, item.name)}
- />
- )}
- contentContainerStyle={{ paddingTop: 8, paddingBottom: 24 }}
- ListEmptyComponent={
-
- ✉️
- No pending invites
-
- Room invites will appear here
-
-
- }
- />
- )}
-
- );
-}
diff --git a/apps/matrix/apps/mobile/app/(app)/settings.tsx b/apps/matrix/apps/mobile/app/(app)/settings.tsx
deleted file mode 100644
index 2f2dc515b..000000000
--- a/apps/matrix/apps/mobile/app/(app)/settings.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import { useState } from 'react';
-import {
- View,
- Text,
- Pressable,
- Alert,
- ScrollView,
- TextInput,
- ActivityIndicator,
- Modal,
-} from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
-import { Image } from 'expo-image';
-import * as ImagePicker from 'expo-image-picker';
-import { PencilSimple, X } from 'phosphor-react-native';
-import { useMatrixStore } from '~/src/matrix/store';
-import { uploadMedia, getMimetypeFromFilename } from '~/src/matrix/upload';
-
-function ProfileAvatar({ displayName, avatarUrl }: { displayName: string; avatarUrl?: string }) {
- const initial = displayName[0]?.toUpperCase() ?? '?';
- return (
-
- {avatarUrl ? (
-
- ) : (
- {initial}
- )}
-
- );
-}
-
-export default function SettingsScreen() {
- const { client, syncState, credentials, logout } = useMatrixStore();
-
- const userId = client?.getUserId() ?? credentials?.userId ?? '';
- const homeserver = client?.baseUrl ?? credentials?.homeserver ?? '';
-
- const [editingName, setEditingName] = useState(false);
- const [newDisplayName, setNewDisplayName] = useState('');
- const [savingName, setSavingName] = useState(false);
- const [uploadingAvatar, setUploadingAvatar] = useState(false);
-
- // Get current profile from client
- const profileInfo = client
- ? (() => {
- try {
- const user = client.getUser(userId);
- return {
- displayName: user?.displayName ?? userId.split(':')[0].slice(1),
- avatarUrl: user?.avatarUrl ?? undefined,
- };
- } catch {
- return { displayName: userId.split(':')[0].slice(1), avatarUrl: undefined };
- }
- })()
- : { displayName: '', avatarUrl: undefined };
-
- const handleEditName = () => {
- setNewDisplayName(profileInfo.displayName);
- setEditingName(true);
- };
-
- const handleSaveName = async () => {
- if (!client || !newDisplayName.trim()) return;
- setSavingName(true);
- try {
- await client.setDisplayName(newDisplayName.trim());
- setEditingName(false);
- } catch (err) {
- Alert.alert('Error', err instanceof Error ? err.message : 'Could not update name');
- } finally {
- setSavingName(false);
- }
- };
-
- const handleChangeAvatar = async () => {
- const result = await ImagePicker.launchImageLibraryAsync({
- mediaTypes: ImagePicker.MediaTypeOptions.Images,
- quality: 0.8,
- allowsEditing: true,
- aspect: [1, 1],
- });
- if (result.canceled || !result.assets[0] || !client) return;
- const asset = result.assets[0];
- const filename = asset.fileName ?? `avatar_${Date.now()}.jpg`;
- const mimetype = asset.mimeType ?? getMimetypeFromFilename(filename);
-
- setUploadingAvatar(true);
- try {
- const uploaded = await uploadMedia(client, asset.uri, filename, mimetype);
- await client.setAvatarUrl(uploaded.mxcUrl);
- } catch (err) {
- Alert.alert('Error', err instanceof Error ? err.message : 'Could not update avatar');
- } finally {
- setUploadingAvatar(false);
- }
- };
-
- const handleLogout = () => {
- Alert.alert('Sign out', 'Are you sure you want to sign out?', [
- { text: 'Cancel', style: 'cancel' },
- { text: 'Sign out', style: 'destructive', onPress: logout },
- ]);
- };
-
- return (
-
-
- Settings
-
-
-
- {/* Profile card */}
-
- {/* Avatar */}
-
-
-
- {uploadingAvatar ? (
-
- ) : (
-
- )}
-
-
-
- {/* Display name */}
-
-
-
- {profileInfo.displayName}
-
-
-
-
-
-
- {userId}
-
-
-
-
- {/* Connection info */}
-
-
-
- Connection
-
-
-
-
- Homeserver
-
- {homeserver || '—'}
-
-
-
- Sync status
-
-
-
- {syncState.toLowerCase()}
-
-
-
-
-
-
- {/* About */}
-
-
- About
-
-
-
- App
- Manalink
-
-
- Version
- 1.0.0
-
-
- Protocol
- Matrix
-
-
-
-
- {/* Sign out */}
-
- Sign out
-
-
-
- {/* Edit display name modal */}
- setEditingName(false)}
- >
-
-
-
- Display name
- setEditingName(false)}>
-
-
-
-
-
- {savingName ? (
-
- ) : (
- Save
- )}
-
-
-
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/app/(auth)/_layout.tsx b/apps/matrix/apps/mobile/app/(auth)/_layout.tsx
deleted file mode 100644
index 819279f22..000000000
--- a/apps/matrix/apps/mobile/app/(auth)/_layout.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Stack } from 'expo-router';
-
-export default function AuthLayout() {
- return (
-
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/app/(auth)/login.tsx b/apps/matrix/apps/mobile/app/(auth)/login.tsx
deleted file mode 100644
index 252aaa79e..000000000
--- a/apps/matrix/apps/mobile/app/(auth)/login.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-import { useState } from 'react';
-import {
- View,
- Text,
- TextInput,
- Pressable,
- ScrollView,
- KeyboardAvoidingView,
- Platform,
- ActivityIndicator,
-} from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
-import * as WebBrowser from 'expo-web-browser';
-import * as Linking from 'expo-linking';
-import { loginWithPassword, loginWithToken, checkHomeserver } from '~/src/matrix/client';
-import { useMatrixStore } from '~/src/matrix/store';
-
-WebBrowser.maybeCompleteAuthSession();
-
-export default function LoginScreen() {
- const [homeserver, setHomeserver] = useState('matrix.mana.how');
- const [username, setUsername] = useState('');
- const [password, setPassword] = useState('');
- const [error, setError] = useState(null);
- const [loading, setLoading] = useState(false);
- const [ssoLoading, setSsoLoading] = useState(false);
- const [checkingServer, setCheckingServer] = useState(false);
- const [serverOk, setServerOk] = useState(null);
-
- const { initialize } = useMatrixStore();
-
- const normalizeHs = (hs: string) => {
- let url = hs.trim();
- if (!url.startsWith('http://') && !url.startsWith('https://')) url = `https://${url}`;
- return url.replace(/\/$/, '');
- };
-
- const handleCheckServer = async () => {
- setCheckingServer(true);
- setServerOk(null);
- const result = await checkHomeserver(homeserver);
- setServerOk(result.ok);
- setError(result.ok ? null : (result.error ?? 'Server not reachable'));
- setCheckingServer(false);
- };
-
- const handleLogin = async () => {
- if (!homeserver.trim() || !username.trim() || !password.trim()) {
- setError('Please fill in all fields');
- return;
- }
- setLoading(true);
- setError(null);
- const result = await loginWithPassword(homeserver, username, password);
- if (result.success && result.credentials) {
- await initialize(result.credentials);
- } else {
- setError(result.error ?? 'Login failed');
- setLoading(false);
- }
- };
-
- const handleSSO = async () => {
- setSsoLoading(true);
- setError(null);
- try {
- const base = normalizeHs(homeserver);
- const redirectUri = Linking.createURL('sso-callback');
- const ssoUrl = `${base}/_matrix/client/v3/login/sso/redirect?redirectUrl=${encodeURIComponent(redirectUri)}`;
-
- const result = await WebBrowser.openAuthSessionAsync(ssoUrl, redirectUri);
-
- if (result.type === 'success') {
- const url = result.url;
- const parsed = new URL(url);
- const loginToken = parsed.searchParams.get('loginToken');
- if (!loginToken) {
- setError('SSO login failed: no token received');
- return;
- }
-
- // Exchange token for credentials
- await import('~/src/matrix/polyfills');
- const { createClient } = await import('matrix-js-sdk');
- const tempClient = createClient({ baseUrl: base });
- const response = await tempClient.login('m.login.token', {
- token: loginToken,
- initial_device_display_name: 'Manalink Mobile',
- });
-
- const loginResult = await loginWithToken(
- base,
- response.access_token,
- response.user_id,
- response.device_id
- );
- if (loginResult.success && loginResult.credentials) {
- await initialize(loginResult.credentials);
- }
- }
- } catch (err) {
- setError(err instanceof Error ? err.message : 'SSO failed');
- } finally {
- setSsoLoading(false);
- }
- };
-
- return (
-
-
-
- {/* Logo */}
-
-
- ⬡
-
- Manalink
- Secure Matrix messaging
-
-
-
- {/* Homeserver */}
-
-
- Homeserver
-
-
- {
- setHomeserver(v);
- setServerOk(null);
- }}
- autoCapitalize="none"
- autoCorrect={false}
- keyboardType="url"
- placeholder="matrix.example.com"
- placeholderTextColor="#6b7280"
- onBlur={handleCheckServer}
- />
- {checkingServer && }
- {serverOk === true && ✓ }
- {serverOk === false && ✗ }
-
-
-
- {/* Username */}
-
-
- Username
-
-
-
-
- {/* Password */}
-
-
- Password
-
-
-
-
- {error && {error} }
-
- {/* Password login */}
-
- {loading ? (
-
- ) : (
- Sign in
- )}
-
-
- {/* Divider */}
-
-
- or
-
-
-
- {/* SSO */}
-
- {ssoLoading ? (
-
- ) : (
- Sign in with SSO
- )}
-
-
-
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/app/+not-found.tsx b/apps/matrix/apps/mobile/app/+not-found.tsx
deleted file mode 100644
index 7ed3a4281..000000000
--- a/apps/matrix/apps/mobile/app/+not-found.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { View, Text } from 'react-native';
-import { Link } from 'expo-router';
-
-export default function NotFound() {
- return (
-
- Screen not found
-
- Go home
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/app/_layout.tsx b/apps/matrix/apps/mobile/app/_layout.tsx
deleted file mode 100644
index a9f058e0a..000000000
--- a/apps/matrix/apps/mobile/app/_layout.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import '../global.css';
-import { useEffect, useState } from 'react';
-import { Stack, useRouter, useSegments } from 'expo-router';
-import { GestureHandlerRootView } from 'react-native-gesture-handler';
-import { StatusBar } from 'expo-status-bar';
-import { useMatrixStore } from '~/src/matrix/store';
-import {
- requestNotificationPermissions,
- setupNotificationNavigation,
-} from '~/src/notifications';
-
-function AuthGuard({ children }: { children: React.ReactNode }) {
- const [checking, setChecking] = useState(true);
- const segments = useSegments();
- const router = useRouter();
- const { isReady, restoreSession } = useMatrixStore();
-
- useEffect(() => {
- restoreSession().finally(() => setChecking(false));
- }, []);
-
- useEffect(() => {
- if (checking) return;
- const inAuthGroup = segments[0] === '(auth)';
- if (!isReady && !inAuthGroup) router.replace('/(auth)/login');
- else if (isReady && inAuthGroup) router.replace('/(app)');
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isReady, checking, segments]);
-
- if (checking) return null;
- return <>{children}>;
-}
-
-export default function RootLayout() {
- useEffect(() => {
- // Request notification permissions (non-blocking)
- requestNotificationPermissions().catch(() => {});
- // Set up navigation from notification taps
- const cleanup = setupNotificationNavigation();
- return cleanup;
- }, []);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/app/room/[id].tsx b/apps/matrix/apps/mobile/app/room/[id].tsx
deleted file mode 100644
index 35c10e986..000000000
--- a/apps/matrix/apps/mobile/app/room/[id].tsx
+++ /dev/null
@@ -1,557 +0,0 @@
-import { useEffect, useRef, useState, useCallback, useMemo } from 'react';
-import {
- View,
- FlatList,
- Text,
- TextInput,
- Pressable,
- ActivityIndicator,
- Modal,
- ScrollView,
- Alert,
- ActionSheetIOS,
- Platform,
-} from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
-import { useLocalSearchParams, useRouter } from 'expo-router';
-import { ArrowLeft, Lock, DotsThreeVertical, X } from 'phosphor-react-native';
-import { Image } from 'expo-image';
-import * as ImagePicker from 'expo-image-picker';
-import * as DocumentPicker from 'expo-document-picker';
-import { useMatrixStore } from '~/src/matrix/store';
-import MessageBubble from '~/src/components/MessageBubble';
-import MessageInput from '~/src/components/MessageInput';
-import TypingIndicator from '~/src/components/TypingIndicator';
-import DateSeparator from '~/src/components/DateSeparator';
-import ImageViewer from '~/src/components/ImageViewer';
-import UserProfileModal from '~/src/components/UserProfileModal';
-import VoiceRecorder from '~/src/components/VoiceRecorder';
-import UnreadSeparator from '~/src/components/UnreadSeparator';
-import { getMimetypeFromFilename } from '~/src/matrix/upload';
-import type { SimpleMessage, SimpleRoom, RoomMember } from '~/src/matrix/types';
-
-type ListItem =
- | { type: 'message'; data: SimpleMessage }
- | { type: 'date'; timestamp: number; key: string }
- | { type: 'unread'; key: string };
-
-function isSameDay(a: number, b: number) {
- const da = new Date(a),
- db = new Date(b);
- return (
- da.getFullYear() === db.getFullYear() &&
- da.getMonth() === db.getMonth() &&
- da.getDate() === db.getDate()
- );
-}
-
-function buildListItems(messages: SimpleMessage[], firstUnreadEventId: string | null): ListItem[] {
- const items: ListItem[] = [];
- let unreadInserted = false;
- for (let i = 0; i < messages.length; i++) {
- const msg = messages[i];
- if (!messages[i - 1] || !isSameDay(messages[i - 1].timestamp, msg.timestamp)) {
- items.push({ type: 'date', timestamp: msg.timestamp, key: `date_${msg.timestamp}_${i}` });
- }
- if (!unreadInserted && firstUnreadEventId && msg.id === firstUnreadEventId) {
- items.push({ type: 'unread', key: 'unread_separator' });
- unreadInserted = true;
- }
- items.push({ type: 'message', data: msg });
- }
- return items;
-}
-
-function MemberRow({ member, onClose }: { member: RoomMember; onClose: () => void }) {
- const [showProfile, setShowProfile] = useState(false);
- return (
- <>
- setShowProfile(true)}
- className="flex-row items-center gap-3 px-4 py-3 active:bg-surface/60"
- >
-
- {member.avatarUrl ? (
-
- ) : (
-
- {member.displayName[0]?.toUpperCase() ?? '?'}
-
- )}
-
-
- {member.displayName}
-
- {member.userId}
-
-
- {member.powerLevel >= 100 && (
-
- Admin
-
- )}
- {member.powerLevel >= 50 && member.powerLevel < 100 && (
-
- Mod
-
- )}
-
- {
- setShowProfile(false);
- onClose();
- }}
- />
- >
- );
-}
-
-export default function RoomScreen() {
- const { id } = useLocalSearchParams<{ id: string }>();
- const router = useRouter();
- const listRef = useRef>(null);
-
- const [loadingMore, setLoadingMore] = useState(false);
- const [uploading, setUploading] = useState(false);
- const [showVoiceRecorder, setShowVoiceRecorder] = useState(false);
- const [replyTo, setReplyTo] = useState(null);
- const [editingMessage, setEditingMessage] = useState(null);
- const [showMembers, setShowMembers] = useState(false);
- const [viewingImage, setViewingImage] = useState(null);
- const [profileUserId, setProfileUserId] = useState(null);
- const [forwardingMessage, setForwardingMessage] = useState(null);
- const [forwardSearch, setForwardSearch] = useState('');
-
- const {
- rooms,
- messages,
- firstUnreadEventId,
- typingUsers,
- roomMembers,
- client,
- credentials,
- selectRoom,
- loadRoomMembers,
- sendMessage,
- editMessage,
- sendReaction,
- redactMessage,
- sendTyping,
- sendImage,
- sendFile,
- sendVoice,
- forwardMessage,
- leaveRoom,
- } = useMatrixStore();
-
- const room = rooms.find((r) => r.id === id);
- const isAdmin = useMemo(() => {
- if (!client || !id) return false;
- const matrixRoom = client.getRoom(id);
- const userId = client.getUserId() ?? '';
- return (matrixRoom?.getMember(userId)?.powerLevel ?? 0) >= 100;
- }, [client, id]);
-
- useEffect(() => {
- if (id) selectRoom(id);
- }, [id]);
-
- const listItems = useMemo(
- () => buildListItems(messages, firstUnreadEventId),
- [messages, firstUnreadEventId]
- );
-
- // Scroll to first unread message on initial load
- useEffect(() => {
- if (!firstUnreadEventId || listItems.length === 0) return;
- const unreadIndex = listItems.findIndex((item) => item.type === 'unread');
- if (unreadIndex > 0) {
- setTimeout(() => {
- listRef.current?.scrollToIndex({ index: unreadIndex, animated: true, viewPosition: 0 });
- }, 300);
- }
- }, [firstUnreadEventId]);
-
- const handleLoadMore = async () => {
- if (!client || !id || loadingMore) return;
- const matrixRoom = client.getRoom(id);
- if (!matrixRoom) return;
- setLoadingMore(true);
- try {
- await client.scrollback(matrixRoom, 30);
- } finally {
- setLoadingMore(false);
- }
- };
-
- const handleRoomOptions = () => {
- const options = ['Cancel', 'Members', ...(isAdmin ? ['Room settings'] : []), 'Leave room'];
- const destructiveIndex = options.length - 1;
-
- if (Platform.OS === 'ios') {
- ActionSheetIOS.showActionSheetWithOptions(
- { options, cancelButtonIndex: 0, destructiveButtonIndex: destructiveIndex },
- (index) => {
- if (index === 1) {
- loadRoomMembers(id!);
- setShowMembers(true);
- }
- if (isAdmin && index === 2) {
- router.push({ pathname: '/room/settings', params: { id } });
- }
- if (index === options.length - 1) handleLeave();
- }
- );
- } else {
- Alert.alert(room?.name ?? 'Room', undefined, [
- {
- text: 'Members',
- onPress: () => {
- loadRoomMembers(id!);
- setShowMembers(true);
- },
- },
- ...(isAdmin
- ? [
- {
- text: 'Room settings',
- onPress: () => router.push({ pathname: '/room/settings', params: { id } }),
- },
- ]
- : []),
- { text: 'Leave room', style: 'destructive' as const, onPress: handleLeave },
- { text: 'Cancel', style: 'cancel' as const },
- ]);
- }
- };
-
- const handleLeave = () => {
- Alert.alert('Leave room', `Leave "${room?.name ?? id}"?`, [
- { text: 'Cancel', style: 'cancel' },
- {
- text: 'Leave',
- style: 'destructive',
- onPress: async () => {
- await leaveRoom(id!);
- router.replace('/(app)');
- },
- },
- ]);
- };
-
- const handleAttach = () => {
- if (Platform.OS === 'ios') {
- ActionSheetIOS.showActionSheetWithOptions(
- { options: ['Cancel', 'Photo Library', 'Camera', 'File'], cancelButtonIndex: 0 },
- (index) => {
- if (index === 1) pickImage('library');
- if (index === 2) pickImage('camera');
- if (index === 3) pickDocument();
- }
- );
- } else {
- Alert.alert('Attach', undefined, [
- { text: 'Photo Library', onPress: () => pickImage('library') },
- { text: 'Camera', onPress: () => pickImage('camera') },
- { text: 'File', onPress: pickDocument },
- { text: 'Cancel', style: 'cancel' },
- ]);
- }
- };
-
- const pickImage = async (source: 'library' | 'camera') => {
- const fn =
- source === 'camera' ? ImagePicker.launchCameraAsync : ImagePicker.launchImageLibraryAsync;
- const result = await fn({ mediaTypes: ImagePicker.MediaTypeOptions.Images, quality: 0.85 });
- if (result.canceled || !result.assets[0]) return;
- const asset = result.assets[0];
- const filename = asset.fileName ?? `image_${Date.now()}.jpg`;
- setUploading(true);
- try {
- await sendImage(
- asset.uri,
- filename,
- asset.mimeType ?? getMimetypeFromFilename(filename),
- asset.width,
- asset.height
- );
- } catch (err) {
- Alert.alert('Upload failed', err instanceof Error ? err.message : 'Unknown error');
- } finally {
- setUploading(false);
- }
- };
-
- const pickDocument = async () => {
- const result = await DocumentPicker.getDocumentAsync({ copyToCacheDirectory: true });
- if (result.canceled || !result.assets[0]) return;
- const asset = result.assets[0];
- setUploading(true);
- try {
- await sendFile(asset.uri, asset.name, asset.mimeType ?? getMimetypeFromFilename(asset.name));
- } catch (err) {
- Alert.alert('Upload failed', err instanceof Error ? err.message : 'Unknown error');
- } finally {
- setUploading(false);
- }
- };
-
- const handleForward = useCallback((msg: SimpleMessage) => {
- setForwardingMessage(msg);
- setForwardSearch('');
- }, []);
-
- const handleForwardToRoom = useCallback(
- async (targetRoom: SimpleRoom) => {
- if (!forwardingMessage) return;
- try {
- await forwardMessage(forwardingMessage.id, targetRoom.id);
- setForwardingMessage(null);
- } catch (err) {
- Alert.alert('Forward failed', err instanceof Error ? err.message : 'Unknown error');
- }
- },
- [forwardingMessage, forwardMessage]
- );
-
- const handleEdit = useCallback((msg: SimpleMessage) => {
- setReplyTo(null);
- setEditingMessage(msg);
- }, []);
-
- const handleSend = useCallback(
- async (body: string, replyToEventId?: string) => {
- await sendMessage(body, replyToEventId);
- },
- [sendMessage]
- );
-
- const handleEditSave = useCallback(
- async (eventId: string, newBody: string) => {
- await editMessage(eventId, newBody);
- },
- [editMessage]
- );
-
- const renderItem = ({ item, index }: { item: ListItem; index: number }) => {
- if (item.type === 'date') return ;
- if (item.type === 'unread') return ;
- const msgIndex = messages.indexOf(item.data);
- return (
- {
- setEditingMessage(null);
- setReplyTo(msg);
- }}
- onEdit={handleEdit}
- onReact={sendReaction}
- onDelete={redactMessage}
- onForward={handleForward}
- onImagePress={setViewingImage}
- onAvatarPress={setProfileUserId}
- />
- );
- };
-
- return (
-
- {/* Header */}
-
- router.back()} className="p-1 active:opacity-50">
-
-
-
-
-
- {room?.name ?? id}
-
- {room?.isEncrypted && }
-
- {room?.topic ? (
-
- {room.topic}
-
- ) : room?.memberCount != null ? (
-
- {room.memberCount} member{room.memberCount !== 1 ? 's' : ''}
-
- ) : null}
-
-
-
-
-
-
- {(loadingMore || uploading) && (
-
-
- {uploading ? 'Uploading...' : 'Loading...'}
-
- )}
-
- (item.type === 'message' ? item.data.id : item.key)}
- renderItem={renderItem}
- contentContainerStyle={{ paddingHorizontal: 0, paddingVertical: 8 }}
- onEndReached={handleLoadMore}
- onEndReachedThreshold={0.15}
- onContentSizeChange={() => listRef.current?.scrollToEnd({ animated: false })}
- maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
- keyboardDismissMode="interactive"
- ListEmptyComponent={
-
- No messages yet
-
- }
- />
-
- {typingUsers.length > 0 && }
-
- {showVoiceRecorder ? (
- {
- setUploading(true);
- try {
- await sendVoice(uri, durationMs);
- } catch (err) {
- Alert.alert('Upload failed', err instanceof Error ? err.message : 'Unknown error');
- } finally {
- setUploading(false);
- setShowVoiceRecorder(false);
- }
- }}
- onCancel={() => setShowVoiceRecorder(false)}
- />
- ) : (
- setShowVoiceRecorder(true)}
- replyTo={replyTo}
- onCancelReply={() => setReplyTo(null)}
- editingMessage={editingMessage}
- onCancelEdit={() => setEditingMessage(null)}
- />
- )}
-
- {/* Members modal */}
- setShowMembers(false)}
- >
-
-
-
- Members{room?.memberCount != null ? ` (${room.memberCount})` : ''}
-
- setShowMembers(false)} className="p-1 active:opacity-50">
-
-
-
-
- {roomMembers.length === 0 ? (
-
-
-
- ) : (
- roomMembers.map((member) => (
- setShowMembers(false)}
- />
- ))
- )}
-
-
-
-
- setViewingImage(null)} />
-
- setProfileUserId(null)} />
-
- {/* Forward message modal */}
- setForwardingMessage(null)}
- >
-
-
- Forward to...
- setForwardingMessage(null)} className="p-1 active:opacity-50">
-
-
-
-
-
-
- {forwardingMessage && (
-
- Message:
-
- {forwardingMessage.body}
-
-
- )}
-
- {rooms
- .filter(
- (r) => r.id !== id && r.name.toLowerCase().includes(forwardSearch.toLowerCase())
- )
- .map((r) => (
- handleForwardToRoom(r)}
- className="flex-row items-center gap-3 px-4 py-3 active:bg-surface/60"
- >
-
- {r.avatar ? (
-
- ) : (
-
- {r.name[0]?.toUpperCase() ?? '?'}
-
- )}
-
-
-
- {r.name}
-
- {r.isDirect && (
- Direct message
- )}
-
-
- ))}
-
-
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/app/room/new.tsx b/apps/matrix/apps/mobile/app/room/new.tsx
deleted file mode 100644
index c2040a9f0..000000000
--- a/apps/matrix/apps/mobile/app/room/new.tsx
+++ /dev/null
@@ -1,209 +0,0 @@
-import { useState } from 'react';
-import {
- View,
- Text,
- TextInput,
- Pressable,
- ScrollView,
- KeyboardAvoidingView,
- Platform,
- ActivityIndicator,
- Switch,
-} from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
-import { useRouter } from 'expo-router';
-import { ArrowLeft, Users, ChatCircle } from 'phosphor-react-native';
-import { useMatrixStore } from '~/src/matrix/store';
-
-type Mode = 'room' | 'dm';
-
-export default function NewRoomScreen() {
- const router = useRouter();
- const { client, selectRoom } = useMatrixStore();
-
- const [mode, setMode] = useState('room');
- const [name, setName] = useState('');
- const [topic, setTopic] = useState('');
- const [dmTarget, setDmTarget] = useState('');
- const [isPrivate, setIsPrivate] = useState(true);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
-
- const handleCreate = async () => {
- if (!client) return;
- setError(null);
- setLoading(true);
-
- try {
- if (mode === 'dm') {
- let userId = dmTarget.trim();
- if (!userId) {
- setError('Enter a Matrix user ID');
- return;
- }
- // Ensure proper format
- if (!userId.startsWith('@')) userId = `@${userId}`;
- if (!userId.includes(':')) {
- const hs = new URL(client.baseUrl).hostname;
- userId = `${userId}:${hs}`;
- }
-
- const room = await client.createRoom({
- is_direct: true,
- invite: [userId],
- preset: 'trusted_private_chat' as any,
- });
-
- selectRoom(room.room_id);
- router.replace(`/room/${room.room_id}`);
- } else {
- if (!name.trim()) {
- setError('Enter a room name');
- return;
- }
-
- const room = await client.createRoom({
- name: name.trim(),
- topic: topic.trim() || undefined,
- preset: isPrivate ? ('private_chat' as any) : ('public_chat' as any),
- visibility: isPrivate ? ('private' as any) : ('public' as any),
- });
-
- selectRoom(room.room_id);
- router.replace(`/room/${room.room_id}`);
- }
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Failed to create room');
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
- {/* Header */}
-
- router.back()} className="p-1 active:opacity-50">
-
-
- New conversation
-
-
-
-
- {/* Mode toggle */}
-
- {(['dm', 'room'] as Mode[]).map((m) => (
- {
- setMode(m);
- setError(null);
- }}
- className={`flex-1 flex-row items-center justify-center gap-2 py-2.5 rounded-xl ${mode === m ? 'bg-primary' : ''}`}
- >
- {m === 'dm' ? (
-
- ) : (
-
- )}
-
- {m === 'dm' ? 'Direct message' : 'Group room'}
-
-
- ))}
-
-
- {/* DM form */}
- {mode === 'dm' && (
-
-
-
- User ID
-
-
-
-
- )}
-
- {/* Room form */}
- {mode === 'room' && (
-
-
-
- Room name
-
-
-
-
-
- Topic (optional)
-
-
-
-
-
- Private room
-
- Only invited members can join
-
-
-
-
-
- )}
-
- {/* Error */}
- {error && {error} }
-
- {/* Create button */}
-
- {loading ? (
-
- ) : (
-
- {mode === 'dm' ? 'Start conversation' : 'Create room'}
-
- )}
-
-
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/app/room/settings.tsx b/apps/matrix/apps/mobile/app/room/settings.tsx
deleted file mode 100644
index afd30e9e2..000000000
--- a/apps/matrix/apps/mobile/app/room/settings.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-import { useState, useEffect } from 'react';
-import {
- View,
- Text,
- TextInput,
- Pressable,
- ScrollView,
- Alert,
- ActivityIndicator,
-} from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
-import { useLocalSearchParams, useRouter } from 'expo-router';
-import { ArrowLeft, Camera } from 'phosphor-react-native';
-import { Image } from 'expo-image';
-import * as ImagePicker from 'expo-image-picker';
-import { useMatrixStore } from '~/src/matrix/store';
-import { uploadMedia } from '~/src/matrix/upload';
-import { resolveMxcThumbnail } from '~/src/matrix/media';
-
-export default function RoomSettingsScreen() {
- const { id } = useLocalSearchParams<{ id: string }>();
- const router = useRouter();
- const { client, credentials, rooms } = useMatrixStore();
-
- const room = rooms.find((r) => r.id === id);
-
- const [name, setName] = useState(room?.name ?? '');
- const [topic, setTopic] = useState(room?.topic ?? '');
- const [avatarUri, setAvatarUri] = useState(room?.avatar ?? null);
- const [saving, setSaving] = useState(false);
- const [uploadingAvatar, setUploadingAvatar] = useState(false);
- const [newAvatarMxc, setNewAvatarMxc] = useState(null);
-
- useEffect(() => {
- if (room) {
- setName(room.name);
- setTopic(room.topic ?? '');
- setAvatarUri(room.avatar ?? null);
- }
- }, [room?.id]);
-
- const handlePickAvatar = async () => {
- const result = await ImagePicker.launchImageLibraryAsync({
- mediaTypes: ImagePicker.MediaTypeOptions.Images,
- allowsEditing: true,
- aspect: [1, 1],
- quality: 0.85,
- });
- if (result.canceled || !result.assets[0] || !client) return;
- const asset = result.assets[0];
- setUploadingAvatar(true);
- try {
- const filename = `avatar_${Date.now()}.jpg`;
- const uploaded = await uploadMedia(client, asset.uri, filename, 'image/jpeg');
- setNewAvatarMxc(uploaded.mxcUrl);
- setAvatarUri(
- credentials
- ? (resolveMxcThumbnail(uploaded.mxcUrl, credentials.homeserver, 128, 128) ?? asset.uri)
- : asset.uri
- );
- } catch (err) {
- Alert.alert('Upload failed', err instanceof Error ? err.message : 'Unknown error');
- } finally {
- setUploadingAvatar(false);
- }
- };
-
- const handleSave = async () => {
- if (!client || !id) return;
- setSaving(true);
- try {
- const trimmedName = name.trim();
- const trimmedTopic = topic.trim();
-
- if (trimmedName && trimmedName !== room?.name) {
- await client.setRoomName(id, trimmedName);
- }
- if (trimmedTopic !== (room?.topic ?? '')) {
- await (client as any).sendStateEvent(id, 'm.room.topic', { topic: trimmedTopic }, '');
- }
- if (newAvatarMxc) {
- await (client as any).sendStateEvent(id, 'm.room.avatar', { url: newAvatarMxc }, '');
- }
- router.back();
- } catch (err) {
- Alert.alert('Save failed', err instanceof Error ? err.message : 'Unknown error');
- } finally {
- setSaving(false);
- }
- };
-
- const hasChanges =
- name.trim() !== room?.name || topic.trim() !== (room?.topic ?? '') || newAvatarMxc !== null;
-
- return (
-
- {/* Header */}
-
- router.back()} className="p-1 active:opacity-50">
-
-
- Room Settings
-
- {saving ? (
-
- ) : (
-
- Save
-
- )}
-
-
-
-
- {/* Avatar */}
-
-
-
- {uploadingAvatar ? (
-
- ) : avatarUri ? (
-
- ) : (
-
- {room?.name?.[0]?.toUpperCase() ?? '#'}
-
- )}
-
-
-
-
-
- Tap to change room avatar
-
-
- {/* Name */}
-
- Room name
-
-
-
- {/* Topic */}
-
- Topic
-
-
-
- {/* Room ID info */}
-
- Room ID
-
-
- {id}
-
-
-
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/app/search.tsx b/apps/matrix/apps/mobile/app/search.tsx
deleted file mode 100644
index 483931045..000000000
--- a/apps/matrix/apps/mobile/app/search.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-import { useState, useCallback } from 'react';
-import { View, Text, TextInput, FlatList, Pressable, ActivityIndicator, Alert } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
-import { useRouter } from 'expo-router';
-import { ArrowLeft, MagnifyingGlass, Lock, Users } from 'phosphor-react-native';
-import { Image } from 'expo-image';
-import { useMatrixStore } from '~/src/matrix/store';
-
-interface PublicRoom {
- room_id: string;
- name?: string;
- topic?: string;
- avatar_url?: string;
- num_joined_members: number;
- world_readable: boolean;
- guest_can_join: boolean;
- join_rule?: string;
-}
-
-export default function SearchScreen() {
- const router = useRouter();
- const { client, credentials, selectRoom, acceptInvite } = useMatrixStore();
-
- const [query, setQuery] = useState('');
- const [results, setResults] = useState([]);
- const [loading, setLoading] = useState(false);
- const [joiningId, setJoiningId] = useState(null);
- const [nextBatch, setNextBatch] = useState();
- const [hasMore, setHasMore] = useState(false);
-
- const search = useCallback(
- async (q: string, since?: string) => {
- if (!client || !credentials) return;
- setLoading(true);
- try {
- const response = await (client as any).publicRooms({
- limit: 20,
- filter: { generic_search_term: q },
- since,
- server: new URL(credentials.homeserver).hostname,
- });
- const rooms: PublicRoom[] = response.chunk ?? [];
- setResults((prev) => (since ? [...prev, ...rooms] : rooms));
- setNextBatch(response.next_batch);
- setHasMore(!!response.next_batch);
- } catch (err) {
- Alert.alert('Search failed', err instanceof Error ? err.message : 'Unknown error');
- } finally {
- setLoading(false);
- }
- },
- [client, credentials]
- );
-
- const handleSearch = (text: string) => {
- setQuery(text);
- setNextBatch(undefined);
- if (text.length >= 2 || text.length === 0) {
- search(text);
- }
- };
-
- const handleLoadMore = () => {
- if (hasMore && nextBatch && !loading) {
- search(query, nextBatch);
- }
- };
-
- const handleJoin = async (room: PublicRoom) => {
- if (!client) return;
- setJoiningId(room.room_id);
- try {
- await client.joinRoom(room.room_id);
- selectRoom(room.room_id);
- router.replace(`/room/${room.room_id}`);
- } catch (err) {
- Alert.alert('Could not join', err instanceof Error ? err.message : 'Unknown error');
- } finally {
- setJoiningId(null);
- }
- };
-
- const renderRoom = ({ item }: { item: PublicRoom }) => {
- const name = item.name ?? item.room_id;
- const initial = name[0]?.toUpperCase() ?? '#';
- const isJoining = joiningId === item.room_id;
-
- return (
-
- {/* Avatar */}
-
- {item.avatar_url ? (
-
- ) : (
- {initial}
- )}
-
-
- {/* Info */}
-
-
-
- {name}
-
- {item.join_rule === 'public' ? null : }
-
- {item.topic && (
-
- {item.topic}
-
- )}
-
-
- {item.num_joined_members}
-
-
-
- {/* Join button */}
- handleJoin(item)}
- disabled={isJoining}
- className="bg-primary rounded-lg px-3 py-1.5 shrink-0 active:opacity-60"
- >
- {isJoining ? (
-
- ) : (
- Join
- )}
-
-
- );
- };
-
- return (
-
- {/* Header */}
-
- router.back()} className="p-1 active:opacity-50">
-
-
- Explore rooms
-
-
- {/* Search bar */}
-
-
-
- {loading && }
-
-
- item.room_id}
- renderItem={renderRoom}
- onEndReached={handleLoadMore}
- onEndReachedThreshold={0.2}
- ListEmptyComponent={
- !loading ? (
-
-
- {query.length > 0 ? 'No rooms found' : 'Search for public rooms'}
-
-
- ) : null
- }
- ListFooterComponent={
- hasMore && !loading ? (
-
- Load more
-
- ) : null
- }
- />
-
- );
-}
diff --git a/apps/matrix/apps/mobile/assets/adaptive-icon.png b/apps/matrix/apps/mobile/assets/adaptive-icon.png
deleted file mode 100644
index b825c132e..000000000
Binary files a/apps/matrix/apps/mobile/assets/adaptive-icon.png and /dev/null differ
diff --git a/apps/matrix/apps/mobile/assets/favicon.png b/apps/matrix/apps/mobile/assets/favicon.png
deleted file mode 100644
index c352c323c..000000000
Binary files a/apps/matrix/apps/mobile/assets/favicon.png and /dev/null differ
diff --git a/apps/matrix/apps/mobile/assets/icon.png b/apps/matrix/apps/mobile/assets/icon.png
deleted file mode 100644
index b825c132e..000000000
Binary files a/apps/matrix/apps/mobile/assets/icon.png and /dev/null differ
diff --git a/apps/matrix/apps/mobile/assets/notification-icon.png b/apps/matrix/apps/mobile/assets/notification-icon.png
deleted file mode 100644
index dec4ef1d0..000000000
Binary files a/apps/matrix/apps/mobile/assets/notification-icon.png and /dev/null differ
diff --git a/apps/matrix/apps/mobile/assets/splash.png b/apps/matrix/apps/mobile/assets/splash.png
deleted file mode 100644
index b825c132e..000000000
Binary files a/apps/matrix/apps/mobile/assets/splash.png and /dev/null differ
diff --git a/apps/matrix/apps/mobile/babel.config.js b/apps/matrix/apps/mobile/babel.config.js
deleted file mode 100644
index d830ae815..000000000
--- a/apps/matrix/apps/mobile/babel.config.js
+++ /dev/null
@@ -1,7 +0,0 @@
-module.exports = function (api) {
- api.cache(true);
- return {
- presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
- plugins: ['react-native-reanimated/plugin'],
- };
-};
diff --git a/apps/matrix/apps/mobile/eas-build-pre-install.sh b/apps/matrix/apps/mobile/eas-build-pre-install.sh
deleted file mode 100755
index 6a507a497..000000000
--- a/apps/matrix/apps/mobile/eas-build-pre-install.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-# EAS Build pre-install hook
-# Creates .npmrc with node-linker=hoisted at the monorepo root
-# so pnpm uses a flat node_modules structure compatible with
-# React Native / Metro bundler module resolution.
-
-MONOREPO_ROOT="$EAS_BUILD_WORKINGDIR"
-if [ -z "$MONOREPO_ROOT" ]; then
- MONOREPO_ROOT=$(cd "$(dirname "$0")/../../../.." && pwd)
-fi
-
-echo "node-linker=hoisted" > "$MONOREPO_ROOT/.npmrc"
-echo "Created .npmrc with node-linker=hoisted at $MONOREPO_ROOT"
diff --git a/apps/matrix/apps/mobile/eas.json b/apps/matrix/apps/mobile/eas.json
deleted file mode 100644
index a799e8c93..000000000
--- a/apps/matrix/apps/mobile/eas.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "cli": {
- "version": ">= 15.0.15",
- "appVersionSource": "remote"
- },
- "build": {
- "base": {
- "node": "22.22.1",
- "pnpm": "9.15.0",
- "env": {
- "EAS_BUILD_RUNNER": "eas-build-on-success"
- }
- },
- "development": {
- "extends": "base",
- "developmentClient": true,
- "distribution": "internal"
- },
- "preview": {
- "extends": "base",
- "distribution": "internal",
- "ios": {
- "simulator": true
- }
- },
- "production": {
- "extends": "base",
- "autoIncrement": true,
- "ios": {
- "image": "sdk-55"
- }
- }
- },
- "submit": {
- "production": {}
- }
-}
diff --git a/apps/matrix/apps/mobile/eslint.config.js b/apps/matrix/apps/mobile/eslint.config.js
deleted file mode 100644
index 9be743690..000000000
--- a/apps/matrix/apps/mobile/eslint.config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-const { defineConfig } = require('eslint/config');
-const expoConfig = require('eslint-config-expo/flat');
-
-module.exports = defineConfig([
- ...expoConfig,
- {
- ignores: ['dist/**', 'build/**', '.expo/**', 'node_modules/**'],
- },
-]);
diff --git a/apps/matrix/apps/mobile/expo-env.d.ts b/apps/matrix/apps/mobile/expo-env.d.ts
deleted file mode 100644
index bf3c1693a..000000000
--- a/apps/matrix/apps/mobile/expo-env.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-///
-
-// NOTE: This file should not be edited and should be in your git ignore
diff --git a/apps/matrix/apps/mobile/global.css b/apps/matrix/apps/mobile/global.css
deleted file mode 100644
index b5c61c956..000000000
--- a/apps/matrix/apps/mobile/global.css
+++ /dev/null
@@ -1,3 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
diff --git a/apps/matrix/apps/mobile/metro.config.js b/apps/matrix/apps/mobile/metro.config.js
deleted file mode 100644
index d285173b2..000000000
--- a/apps/matrix/apps/mobile/metro.config.js
+++ /dev/null
@@ -1,51 +0,0 @@
-const { getDefaultConfig } = require('expo/metro-config');
-const { withNativeWind } = require('nativewind/metro');
-const path = require('path');
-
-// Monorepo root where hoisted node_modules live
-const monorepoRoot = path.resolve(__dirname, '../../../..');
-
-const config = getDefaultConfig(__dirname);
-
-// Polyfills for matrix-js-sdk (browser-oriented SDK)
-config.resolver.extraNodeModules = {
- ...config.resolver.extraNodeModules,
- events: require.resolve('events'),
- buffer: require.resolve('buffer'),
- stream: require.resolve('stream-browserify'),
-};
-
-// In pnpm monorepos with node-linker=hoisted, pnpm may place a different version of
-// react-native-css-interop in the app's local node_modules vs the monorepo root.
-// This causes module duplication in the Metro bundle: the transformer's injectData()
-// writes styles to one module instance while the JSX runtime's getStyle() reads from
-// another (empty) instance, resulting in no styles being applied.
-//
-// Fix: intercept react-native-css-interop imports and resolve them from the monorepo
-// root node_modules, bypassing any local copy.
-config.resolver.resolveRequest = (context, moduleName, platform) => {
- // Block matrix-sdk-crypto-wasm (uses import.meta, not compatible with Hermes)
- if (moduleName === '@matrix-org/matrix-sdk-crypto-wasm') {
- return { type: 'empty' };
- }
-
- // Deduplicate react-native-css-interop by resolving from monorepo root
- if (
- moduleName === 'react-native-css-interop' ||
- moduleName.startsWith('react-native-css-interop/')
- ) {
- return context.resolveRequest({ ...context, originDir: monorepoRoot }, moduleName, platform);
- }
-
- return context.resolveRequest(context, moduleName, platform);
-};
-
-// In a pnpm monorepo with node-linker=hoisted, the virtual module system used by
-// react-native-css-interop can fail because node_modules are at the monorepo root,
-// not inside the app directory. Using forceWriteFileSystem bypasses virtual modules
-// and writes CSS data directly to the cache files on disk, which Metro then reads
-// and the transformer wraps with injectData().
-module.exports = withNativeWind(config, {
- input: './global.css',
- forceWriteFileSystem: true,
-});
diff --git a/apps/matrix/apps/mobile/nativewind-env.d.ts b/apps/matrix/apps/mobile/nativewind-env.d.ts
deleted file mode 100644
index 5d7befc24..000000000
--- a/apps/matrix/apps/mobile/nativewind-env.d.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-///
-
-import 'react-native';
-
-declare module 'react-native' {
- interface PressableProps {
- className?: string | ((state: { pressed: boolean }) => string);
- cssInterop?: boolean;
- }
-}
diff --git a/apps/matrix/apps/mobile/package.json b/apps/matrix/apps/mobile/package.json
deleted file mode 100644
index c299d7255..000000000
--- a/apps/matrix/apps/mobile/package.json
+++ /dev/null
@@ -1,67 +0,0 @@
-{
- "name": "@matrix/mobile",
- "version": "0.2.0",
- "main": "expo-router/entry",
- "scripts": {
- "dev": "expo start",
- "ios": "expo run:ios",
- "android": "expo run:android",
- "build:dev": "eas build --profile development --platform ios",
- "build:preview": "eas build --profile preview --platform ios",
- "build:prod": "eas build --profile production --platform ios",
- "submit:ios": "eas submit --platform ios --profile production",
- "build:testflight": "eas build --profile production --platform ios --auto-submit",
- "prebuild": "expo prebuild",
- "type-check": "tsc --noEmit",
- "lint": "eslint .",
- "format": "eslint . --fix"
- },
- "dependencies": {
- "@react-native-async-storage/async-storage": "2.2.0",
- "babel-preset-expo": "~55.0.8",
- "buffer": "^6.0.3",
- "events": "^3.3.0",
- "expo": "~55.0.5",
- "expo-audio": "~55.0.8",
- "expo-constants": "~55.0.7",
- "expo-document-picker": "~55.0.8",
- "expo-file-system": "~55.0.10",
- "expo-haptics": "~55.0.8",
- "expo-image": "~55.0.6",
- "expo-image-picker": "~55.0.12",
- "expo-linking": "~55.0.7",
- "expo-media-library": "~55.0.9",
- "expo-notifications": "~55.0.12",
- "expo-router": "~55.0.5",
- "expo-secure-store": "~55.0.8",
- "expo-status-bar": "~55.0.4",
- "expo-system-ui": "~55.0.9",
- "expo-web-browser": "~55.0.9",
- "matrix-js-sdk": "^37.1.0",
- "nativewind": "~4.2.3",
- "phosphor-react-native": "^2.3.0",
- "react": "19.2.0",
- "react-dom": "19.2.0",
- "react-native": "0.83.2",
- "react-native-css-interop": "0.2.3",
- "react-native-gesture-handler": "~2.30.0",
- "react-native-reanimated": "~4.2.1",
- "react-native-safe-area-context": "~5.6.2",
- "react-native-screens": "~4.23.0",
- "react-native-web": "~0.21.2",
- "react-native-worklets": "~0.7.2",
- "stream-browserify": "^3.0.0",
- "zustand": "^4.5.1"
- },
- "devDependencies": {
- "@babel/core": "^7.20.0",
- "@types/react": "~19.2.14",
- "eslint": "^9.39.1",
- "eslint-config-expo": "^55.0.0",
- "prettier": "^3.2.5",
- "prettier-plugin-tailwindcss": "^0.5.11",
- "tailwindcss": "^3.4.0",
- "typescript": "~5.9.3"
- },
- "private": true
-}
diff --git a/apps/matrix/apps/mobile/prettier.config.js b/apps/matrix/apps/mobile/prettier.config.js
deleted file mode 100644
index 698fc9790..000000000
--- a/apps/matrix/apps/mobile/prettier.config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-module.exports = {
- semi: true,
- singleQuote: true,
- trailingComma: 'all',
- printWidth: 100,
- useTabs: true,
- plugins: ['prettier-plugin-tailwindcss'],
-};
diff --git a/apps/matrix/apps/mobile/src/components/DateSeparator.tsx b/apps/matrix/apps/mobile/src/components/DateSeparator.tsx
deleted file mode 100644
index 437094e73..000000000
--- a/apps/matrix/apps/mobile/src/components/DateSeparator.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { View, Text } from 'react-native';
-
-interface Props {
- timestamp: number;
-}
-
-function formatDate(timestamp: number): string {
- const date = new Date(timestamp);
- const now = new Date();
- const diffDays = Math.floor((now.getTime() - date.getTime()) / 86400000);
-
- if (diffDays === 0) return 'Today';
- if (diffDays === 1) return 'Yesterday';
- if (diffDays < 7) {
- return date.toLocaleDateString([], { weekday: 'long' });
- }
- return date.toLocaleDateString([], { day: 'numeric', month: 'long', year: 'numeric' });
-}
-
-export default function DateSeparator({ timestamp }: Props) {
- return (
-
-
- {formatDate(timestamp)}
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/src/components/ImageViewer.tsx b/apps/matrix/apps/mobile/src/components/ImageViewer.tsx
deleted file mode 100644
index a44885ce2..000000000
--- a/apps/matrix/apps/mobile/src/components/ImageViewer.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { Modal, View, Pressable, StatusBar, Dimensions } from 'react-native';
-import { Image } from 'expo-image';
-import { X, DownloadSimple } from 'phosphor-react-native';
-import { File, Paths } from 'expo-file-system';
-import * as MediaLibrary from 'expo-media-library';
-import { useState } from 'react';
-
-interface Props {
- uri: string | null;
- onClose: () => void;
-}
-
-const { width: SCREEN_W, height: SCREEN_H } = Dimensions.get('window');
-
-export default function ImageViewer({ uri, onClose }: Props) {
- const [saving, setSaving] = useState(false);
-
- const handleSave = async () => {
- if (!uri || saving) return;
- setSaving(true);
- try {
- const { status } = await MediaLibrary.requestPermissionsAsync();
- if (status !== 'granted') return;
-
- const filename = `manalink_${Date.now()}.jpg`;
- const downloaded = await File.downloadFileAsync(uri, Paths.cache);
- await MediaLibrary.saveToLibraryAsync(downloaded.uri);
- } finally {
- setSaving(false);
- }
- };
-
- return (
-
-
-
- {/* Controls */}
-
-
-
-
-
-
-
-
-
- {/* Image */}
- {uri && (
-
-
-
- )}
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/src/components/MessageBubble.tsx b/apps/matrix/apps/mobile/src/components/MessageBubble.tsx
deleted file mode 100644
index d7fbfe09a..000000000
--- a/apps/matrix/apps/mobile/src/components/MessageBubble.tsx
+++ /dev/null
@@ -1,448 +0,0 @@
-import { useState } from 'react';
-import {
- View,
- Text,
- Pressable,
- ActionSheetIOS,
- Platform,
- Alert,
- Clipboard,
- Modal,
- ScrollView,
-} from 'react-native';
-import { Swipeable } from 'react-native-gesture-handler';
-import Animated, {
- useAnimatedStyle,
- interpolate,
- Extrapolation,
- type SharedValue,
-} from 'react-native-reanimated';
-import { Image } from 'expo-image';
-import { ArrowBendUpLeft } from 'phosphor-react-native';
-import type { SimpleMessage, MessageReaction } from '~/src/matrix/types';
-import MessageText from './MessageText';
-import VoiceMessage from './VoiceMessage';
-
-interface Props {
- message: SimpleMessage;
- prevMessage: SimpleMessage | null;
- onReply?: (message: SimpleMessage) => void;
- onEdit?: (message: SimpleMessage) => void;
- onReact?: (eventId: string, emoji: string) => void;
- onDelete?: (eventId: string) => void;
- onForward?: (message: SimpleMessage) => void;
- onImagePress?: (uri: string) => void;
- onAvatarPress?: (userId: string) => void;
-}
-
-function formatTime(ts: number) {
- return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
-}
-
-function AvatarCircle({
- name,
- url,
- onPress,
- size = 28,
-}: {
- name: string;
- url?: string;
- onPress?: () => void;
- size?: number;
-}) {
- const inner = (
-
- {url ? (
-
- ) : (
-
- {name[0]?.toUpperCase() ?? '?'}
-
- )}
-
- );
- if (!onPress) return inner;
- return (
-
- {inner}
-
- );
-}
-
-const QUICK_REACTIONS = ['👍', '❤️', '😂', '😮', '😢'];
-
-function SwipeReplyAction({ progress }: { progress: SharedValue }) {
- const style = useAnimatedStyle(() => ({
- opacity: interpolate(progress.value, [0, 0.5, 1], [0, 0.6, 1], Extrapolation.CLAMP),
- transform: [{ scale: interpolate(progress.value, [0, 1], [0.5, 1], Extrapolation.CLAMP) }],
- }));
- return (
-
-
-
-
-
- );
-}
-
-function ReactionDetailsModal({
- reactions,
- visible,
- onClose,
-}: {
- reactions: MessageReaction[];
- visible: boolean;
- onClose: () => void;
-}) {
- const [selectedKey, setSelectedKey] = useState(null);
- const selected = selectedKey ? reactions.find((r) => r.key === selectedKey) : reactions[0];
-
- return (
-
-
-
- Reactions
-
- Done
-
-
-
- {reactions.map((r) => (
- setSelectedKey(r.key)}
- className={`flex-row items-center gap-1 px-3 py-1.5 rounded-full border ${
- selected?.key === r.key
- ? 'bg-primary/20 border-primary/40'
- : 'bg-surface border-border'
- }`}
- >
- {r.key}
-
- {r.count}
-
-
- ))}
-
-
- {selected?.users.map((userId) => (
-
-
-
- {userId.replace(/^@/, '')[0]?.toUpperCase() ?? '?'}
-
-
-
- {userId}
-
-
- ))}
-
-
-
- );
-}
-
-export default function MessageBubble({
- message,
- prevMessage,
- onReply,
- onEdit,
- onReact,
- onDelete,
- onForward,
- onImagePress,
- onAvatarPress,
-}: Props) {
- const [showReactionDetails, setShowReactionDetails] = useState(false);
- const isOwn = message.isOwn;
- const isGrouped =
- !message.redacted &&
- prevMessage !== null &&
- prevMessage.sender === message.sender &&
- message.timestamp - prevMessage.timestamp < 300_000;
- const showAvatar = !isOwn && !isGrouped;
- const showSenderName = !isOwn && !isGrouped;
-
- const handleLongPress = () => {
- const extraOptions = isOwn && !message.redacted ? ['Edit', 'Delete'] : [];
- const options = [
- 'Cancel',
- 'Reply',
- 'Forward',
- ...QUICK_REACTIONS,
- 'Copy text',
- ...extraOptions,
- ];
- const destructiveIndex = isOwn && !message.redacted ? options.length - 1 : undefined;
-
- if (Platform.OS === 'ios') {
- ActionSheetIOS.showActionSheetWithOptions(
- { options, cancelButtonIndex: 0, destructiveButtonIndex: destructiveIndex },
- (index) => {
- if (index === 0) return;
- if (index === 1) {
- onReply?.(message);
- return;
- }
- if (index === 2) {
- onForward?.(message);
- return;
- }
- const ri = index - 3;
- if (ri < QUICK_REACTIONS.length) {
- onReact?.(message.id, QUICK_REACTIONS[ri]);
- return;
- }
- const ai = index - 3 - QUICK_REACTIONS.length;
- if (ai === 0) {
- Clipboard.setString(message.body);
- return;
- }
- if (ai === 1 && isOwn) {
- onEdit?.(message);
- return;
- }
- if (ai === 2 && isOwn) {
- onDelete?.(message.id);
- }
- }
- );
- } else {
- Alert.alert('Message', undefined, [
- { text: 'Reply', onPress: () => onReply?.(message) },
- { text: 'Forward', onPress: () => onForward?.(message) },
- ...(isOwn ? [{ text: 'Edit', onPress: () => onEdit?.(message) }] : []),
- { text: 'Copy text', onPress: () => Clipboard.setString(message.body) },
- ...(isOwn && !message.redacted
- ? [
- {
- text: 'Delete',
- style: 'destructive' as const,
- onPress: () => onDelete?.(message.id),
- },
- ]
- : []),
- { text: 'Cancel', style: 'cancel' as const },
- ]);
- }
- };
-
- if (message.redacted) {
- return (
-
-
- Message deleted
-
-
- );
- }
-
- const renderLeftActions = isOwn
- ? undefined
- : (progress: SharedValue) => ;
-
- const renderRightActions = isOwn
- ? (progress: SharedValue) =>
- : undefined;
-
- // Type assertion needed: react-native-gesture-handler Swipeable types expect old Animated API
- // but we use Reanimated v3 SharedValue which is what actually works at runtime
- const swipeableProps = {
- renderLeftActions: renderLeftActions as any,
- renderRightActions: renderRightActions as any,
- };
-
- return (
- {
- if ((direction === 'left' && !isOwn) || (direction === 'right' && isOwn)) {
- onReply?.(message);
- }
- }}
- friction={2}
- overshootFriction={8}
- >
-
- {/* Left avatar */}
- {!isOwn && (
-
- {showAvatar && (
- onAvatarPress(message.sender) : undefined}
- />
- )}
-
- )}
-
-
- {showSenderName && (
- onAvatarPress(message.sender) : undefined}>
-
- {message.senderName}
-
-
- )}
-
-
- {/* Reply preview */}
- {message.replyTo && (
-
-
- {message.replyToSenderName ?? 'Unknown'}
-
-
- {message.replyToBody ?? '…'}
-
-
- )}
-
- {message.type === 'm.image' && message.media?.thumbnailUrl && (
- onImagePress?.(message.media!.thumbnailUrl!)}>
-
-
- )}
-
- {message.type === 'm.file' && (
-
- 📎
-
- {message.media?.filename ?? message.body}
-
-
- )}
-
- {message.type === 'm.audio' && message.media?.downloadUrl && (
-
- )}
-
- {(message.type === 'm.text' ||
- message.type === 'm.notice' ||
- message.type === 'm.emote') && (
-
- )}
-
-
- {/* Reactions */}
- {message.reactions && message.reactions.length > 0 && (
-
- {message.reactions.map((r) => (
- onReact?.(message.id, r.key)}
- onLongPress={() => setShowReactionDetails(true)}
- className={`flex-row items-center gap-0.5 px-2 py-0.5 rounded-full border ${
- r.includesMe ? 'bg-primary/20 border-primary/40' : 'bg-surface border-border'
- }`}
- >
- {r.key}
- {r.count > 1 && (
-
- {r.count}
-
- )}
-
- ))}
-
- )}
-
- {/* Timestamp + Read receipts */}
-
-
- {formatTime(message.timestamp)}
- {message.edited && ' · edited'}
-
- {message.readBy && message.readBy.length > 0 && (
-
- {message.readBy.slice(0, 3).map((r, i) => (
- 0 ? { marginLeft: -3 } : undefined}
- >
-
- {r.userName[0]?.toUpperCase() ?? '?'}
-
-
- ))}
- {message.readBy.length > 3 && (
-
- +{message.readBy.length - 3}
-
- )}
-
- )}
-
-
-
-
- {/* Reaction details modal */}
- {message.reactions && message.reactions.length > 0 && (
- setShowReactionDetails(false)}
- />
- )}
-
- );
-}
diff --git a/apps/matrix/apps/mobile/src/components/MessageInput.tsx b/apps/matrix/apps/mobile/src/components/MessageInput.tsx
deleted file mode 100644
index 2d6cb023d..000000000
--- a/apps/matrix/apps/mobile/src/components/MessageInput.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-import { useState, useRef, useEffect } from 'react';
-import { View, TextInput, Pressable, Text } from 'react-native';
-import * as Haptics from 'expo-haptics';
-import { ArrowUp, Paperclip, Microphone, X, PencilSimple } from 'phosphor-react-native';
-import type { SimpleMessage } from '~/src/matrix/types';
-
-interface Props {
- onSend: (body: string, replyToEventId?: string) => Promise;
- onEdit?: (eventId: string, newBody: string) => Promise;
- onTyping: (typing: boolean) => Promise;
- onAttach?: () => void;
- onVoiceRecord?: () => void;
- replyTo?: SimpleMessage | null;
- onCancelReply?: () => void;
- editingMessage?: SimpleMessage | null;
- onCancelEdit?: () => void;
-}
-
-export default function MessageInput({
- onSend,
- onEdit,
- onTyping,
- onAttach,
- onVoiceRecord,
- replyTo,
- onCancelReply,
- editingMessage,
- onCancelEdit,
-}: Props) {
- const [text, setText] = useState('');
- const [sending, setSending] = useState(false);
- const typingTimer = useRef | null>(null);
-
- // Pre-fill text when entering edit mode
- useEffect(() => {
- if (editingMessage) setText(editingMessage.body);
- else setText('');
- }, [editingMessage?.id]);
-
- const handleChangeText = (value: string) => {
- setText(value);
- if (!editingMessage) {
- onTyping(true);
- if (typingTimer.current) clearTimeout(typingTimer.current);
- typingTimer.current = setTimeout(() => onTyping(false), 3000);
- }
- };
-
- const handleSubmit = async () => {
- const body = text.trim();
- if (!body || sending) return;
- setSending(true);
- if (typingTimer.current) clearTimeout(typingTimer.current);
- onTyping(false);
- await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
- try {
- if (editingMessage) {
- await onEdit?.(editingMessage.id, body);
- onCancelEdit?.();
- } else {
- await onSend(body, replyTo?.id);
- onCancelReply?.();
- }
- setText('');
- } finally {
- setSending(false);
- }
- };
-
- const canSend = text.trim().length > 0 && !sending;
- const isEditing = !!editingMessage;
- const showMic = !canSend && !isEditing && !!onVoiceRecord;
-
- return (
-
- {/* Context banner: Reply or Edit */}
- {(replyTo || isEditing) && (
-
-
-
-
- {isEditing ? 'Editing message' : `Reply to ${replyTo!.senderName}`}
-
-
- {isEditing ? editingMessage!.body : replyTo!.body}
-
-
-
-
-
-
- )}
-
- {/* Input row */}
-
- {onAttach && !isEditing && (
-
-
-
- )}
-
-
-
- {showMic ? (
-
-
-
- ) : (
-
- {isEditing ? (
-
- ) : (
-
- )}
-
- )}
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/src/components/MessageText.tsx b/apps/matrix/apps/mobile/src/components/MessageText.tsx
deleted file mode 100644
index 7f34d25c0..000000000
--- a/apps/matrix/apps/mobile/src/components/MessageText.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import { Text, Linking } from 'react-native';
-
-const URL_REGEX = /(https?:\/\/[^\s<>[\]{}|\\^`"]+)/g;
-const MENTION_REGEX = /(@[\w.-]+:[\w.-]+)/g;
-
-interface Segment {
- text: string;
- type: 'text' | 'url' | 'mention';
-}
-
-function parseSegments(body: string): Segment[] {
- const segments: Segment[] = [];
- // Split on URLs first, then handle mentions
- const parts = body.split(URL_REGEX);
- for (const part of parts) {
- if (URL_REGEX.test(part)) {
- segments.push({ text: part, type: 'url' });
- URL_REGEX.lastIndex = 0;
- } else {
- // Split on @mentions
- const mentionParts = part.split(MENTION_REGEX);
- for (const mp of mentionParts) {
- if (MENTION_REGEX.test(mp)) {
- segments.push({ text: mp, type: 'mention' });
- MENTION_REGEX.lastIndex = 0;
- } else if (mp) {
- segments.push({ text: mp, type: 'text' });
- }
- }
- }
- }
- return segments;
-}
-
-interface Props {
- body: string;
- isOwn: boolean;
- className?: string;
-}
-
-export default function MessageText({ body, isOwn, className }: Props) {
- const segments = parseSegments(body);
- const baseColor = isOwn ? 'rgba(255,255,255,0.95)' : undefined;
-
- return (
-
- {segments.map((seg, i) => {
- if (seg.type === 'url') {
- return (
- Linking.openURL(seg.text).catch(() => {})}
- >
- {seg.text}
-
- );
- }
- if (seg.type === 'mention') {
- return (
-
- {seg.text}
-
- );
- }
- return (
-
- {seg.text}
-
- );
- })}
-
- );
-}
diff --git a/apps/matrix/apps/mobile/src/components/RoomListItem.tsx b/apps/matrix/apps/mobile/src/components/RoomListItem.tsx
deleted file mode 100644
index df065d175..000000000
--- a/apps/matrix/apps/mobile/src/components/RoomListItem.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import { View, Text, Pressable } from 'react-native';
-import { Image } from 'expo-image';
-import type { SimpleRoom } from '~/src/matrix/types';
-
-interface Props {
- room: SimpleRoom;
- onPress: () => void;
-}
-
-function formatTime(timestamp?: number): string {
- if (!timestamp) return '';
- const date = new Date(timestamp);
- const now = new Date();
- const diffDays = Math.floor((now.getTime() - date.getTime()) / 86400000);
- if (diffDays === 0) return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
- if (diffDays === 1) return 'Yesterday';
- if (diffDays < 7) return date.toLocaleDateString([], { weekday: 'short' });
- return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
-}
-
-function PresenceDot({ presence }: { presence?: string }) {
- if (!presence || presence === 'offline') return null;
- return (
-
- );
-}
-
-export default function RoomListItem({ room, onPress }: Props) {
- const hasHighlight = room.highlightCount > 0;
- const hasUnread = room.unreadCount > 0;
- const displayName = room.name ?? room.id;
- const initial = displayName[0]?.toUpperCase() ?? '?';
-
- return (
-
- {/* Avatar */}
-
-
- {room.avatar ? (
-
- ) : (
- {initial}
- )}
-
- {room.isDirect && }
-
-
- {/* Content */}
-
-
-
- {displayName}
-
-
- {formatTime(room.lastMessageTime)}
-
-
-
-
-
- {room.lastMessage
- ? (room.lastMessageSender && !room.isDirect
- ? `${room.lastMessageSender.split(':')[0].slice(1)}: `
- : '') + room.lastMessage
- : room.isEncrypted
- ? '🔒 Encrypted'
- : 'No messages'}
-
-
- {/* Badge */}
- {(hasUnread || hasHighlight) && (
-
-
- {hasHighlight
- ? room.highlightCount
- : room.unreadCount > 99
- ? '99+'
- : room.unreadCount}
-
-
- )}
-
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/src/components/SyncStatusBar.tsx b/apps/matrix/apps/mobile/src/components/SyncStatusBar.tsx
deleted file mode 100644
index 0d1f1f1b6..000000000
--- a/apps/matrix/apps/mobile/src/components/SyncStatusBar.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { View, Text } from 'react-native';
-import type { SyncState } from '~/src/matrix/types';
-
-interface Props {
- syncState: SyncState;
-}
-
-const statusConfig: Record = {
- STOPPED: { label: 'Disconnected', color: 'bg-destructive/80' },
- ERROR: { label: 'Connection error', color: 'bg-destructive/80' },
- RECONNECTING: { label: 'Reconnecting...', color: 'bg-yellow-500/80' },
- CATCHUP: { label: 'Catching up...', color: 'bg-yellow-500/80' },
- PREPARED: null,
- SYNCING: null,
-};
-
-export default function SyncStatusBar({ syncState }: Props) {
- const config = statusConfig[syncState];
- if (!config) return null;
-
- return (
-
- {config.label}
-
- );
-}
diff --git a/apps/matrix/apps/mobile/src/components/TypingIndicator.tsx b/apps/matrix/apps/mobile/src/components/TypingIndicator.tsx
deleted file mode 100644
index 7828c94e2..000000000
--- a/apps/matrix/apps/mobile/src/components/TypingIndicator.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { View, Text } from 'react-native';
-
-interface Props {
- users: string[];
-}
-
-export default function TypingIndicator({ users }: Props) {
- if (users.length === 0) return null;
-
- const label =
- users.length === 1
- ? `${users[0]} is typing...`
- : users.length === 2
- ? `${users[0]} and ${users[1]} are typing...`
- : 'Several people are typing...';
-
- return (
-
- {label}
-
- );
-}
diff --git a/apps/matrix/apps/mobile/src/components/UnreadSeparator.tsx b/apps/matrix/apps/mobile/src/components/UnreadSeparator.tsx
deleted file mode 100644
index e439eaaa3..000000000
--- a/apps/matrix/apps/mobile/src/components/UnreadSeparator.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import { View, Text } from 'react-native';
-
-export default function UnreadSeparator() {
- return (
-
-
- New messages
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/src/components/UserProfileModal.tsx b/apps/matrix/apps/mobile/src/components/UserProfileModal.tsx
deleted file mode 100644
index 837a95f76..000000000
--- a/apps/matrix/apps/mobile/src/components/UserProfileModal.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-import { Modal, View, Text, Pressable, ActivityIndicator, ScrollView } from 'react-native';
-import { SafeAreaView } from 'react-native-safe-area-context';
-import { Image } from 'expo-image';
-import { X, ChatCircle } from 'phosphor-react-native';
-import { useEffect, useState } from 'react';
-import { useMatrixStore } from '~/src/matrix/store';
-import { resolveMxcThumbnail } from '~/src/matrix/media';
-import { useRouter } from 'expo-router';
-
-interface UserProfile {
- userId: string;
- displayName: string;
- avatarUrl?: string;
-}
-
-interface Props {
- userId: string | null;
- onClose: () => void;
-}
-
-export default function UserProfileModal({ userId, onClose }: Props) {
- const { client, credentials, rooms, selectRoom } = useMatrixStore();
- const router = useRouter();
- const [profile, setProfile] = useState(null);
- const [loading, setLoading] = useState(false);
-
- useEffect(() => {
- if (!userId || !client || !credentials) return;
- setLoading(true);
- setProfile(null);
-
- client
- .getProfileInfo(userId)
- .then((info) => {
- const rawAvatar = info.avatar_url ?? null;
- setProfile({
- userId,
- displayName: info.displayname ?? userId.split(':')[0].slice(1),
- avatarUrl: rawAvatar
- ? (resolveMxcThumbnail(rawAvatar, credentials.homeserver, 160, 160) ?? undefined)
- : undefined,
- });
- })
- .catch(() => {
- setProfile({
- userId,
- displayName: userId.split(':')[0].slice(1),
- });
- })
- .finally(() => setLoading(false));
- }, [userId]);
-
- // Find an existing DM room with this user
- const existingDM = userId ? rooms.find((r) => r.isDirect && r.dmUserId === userId) : null;
-
- const handleStartDM = async () => {
- if (!client || !userId || !credentials) return;
-
- if (existingDM) {
- selectRoom(existingDM.id);
- router.push(`/room/${existingDM.id}`);
- onClose();
- return;
- }
-
- try {
- const room = await client.createRoom({
- is_direct: true,
- invite: [userId],
- preset: 'trusted_private_chat' as any,
- });
- selectRoom(room.room_id);
- router.push(`/room/${room.room_id}`);
- onClose();
- } catch {
- // ignore
- }
- };
-
- const initial = profile?.displayName[0]?.toUpperCase() ?? '?';
-
- return (
-
-
-
- e.stopPropagation()}>
-
- {/* Handle */}
-
-
-
-
- {/* Close */}
-
-
-
-
-
-
-
- {loading ? (
-
- ) : profile ? (
- <>
- {/* Avatar */}
-
- {profile.avatarUrl ? (
-
- ) : (
- {initial}
- )}
-
-
- {/* Name */}
-
-
- {profile.displayName}
-
-
- {profile.userId}
-
-
-
- {/* Actions */}
- {profile.userId !== credentials?.userId && (
-
-
-
- {existingDM ? 'Open conversation' : 'Send message'}
-
-
- )}
- >
- ) : null}
-
-
-
-
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/src/components/VoiceMessage.tsx b/apps/matrix/apps/mobile/src/components/VoiceMessage.tsx
deleted file mode 100644
index 945b8a7f4..000000000
--- a/apps/matrix/apps/mobile/src/components/VoiceMessage.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { useState, useCallback } from 'react';
-import { View, Text, Pressable, ActivityIndicator } from 'react-native';
-import { useAudioPlayer, useAudioPlayerStatus, setAudioModeAsync } from 'expo-audio';
-import { Play, Pause } from 'phosphor-react-native';
-
-interface Props {
- uri: string;
- duration?: number;
- isOwn: boolean;
-}
-
-function formatDuration(ms: number): string {
- const secs = Math.floor(ms / 1000);
- const m = Math.floor(secs / 60);
- const s = secs % 60;
- return `${m}:${s.toString().padStart(2, '0')}`;
-}
-
-export default function VoiceMessage({ uri, duration, isOwn }: Props) {
- const player = useAudioPlayer(uri);
- const status = useAudioPlayerStatus(player);
- const [initialized, setInitialized] = useState(false);
-
- const currentTimeMs = (status.currentTime ?? 0) * 1000;
- const durationMs = (status.duration ?? 0) * 1000 || duration || 0;
- const playing = status.playing;
- const progress = durationMs > 0 ? currentTimeMs / durationMs : 0;
-
- const handleToggle = useCallback(async () => {
- if (!initialized) {
- await setAudioModeAsync({ playsInSilentMode: true });
- setInitialized(true);
- }
-
- if (playing) {
- player.pause();
- } else {
- player.play();
- }
- }, [player, playing, initialized]);
-
- const iconColor = isOwn ? '#fff' : '#7c6bff';
- const barColor = isOwn ? 'rgba(255,255,255,0.5)' : '#2a2a2a';
- const fillColor = isOwn ? '#fff' : '#7c6bff';
-
- return (
-
-
- {status.isBuffering ? (
-
- ) : playing ? (
-
- ) : (
-
- )}
-
-
- {/* Waveform / progress bar */}
-
-
-
-
-
- {formatDuration(playing || currentTimeMs > 0 ? currentTimeMs : durationMs)}
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/src/components/VoiceRecorder.tsx b/apps/matrix/apps/mobile/src/components/VoiceRecorder.tsx
deleted file mode 100644
index 6ed38c28a..000000000
--- a/apps/matrix/apps/mobile/src/components/VoiceRecorder.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-import { useState, useEffect, useRef } from 'react';
-import { View, Text, Pressable, Animated, Alert } from 'react-native';
-import {
- useAudioRecorder,
- RecordingPresets,
- requestRecordingPermissionsAsync,
- setAudioModeAsync,
-} from 'expo-audio';
-import { Trash, PaperPlaneRight } from 'phosphor-react-native';
-
-interface Props {
- onSend: (uri: string, durationMs: number) => Promise;
- onCancel: () => void;
-}
-
-export default function VoiceRecorder({ onSend, onCancel }: Props) {
- const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
- const [duration, setDuration] = useState(0);
- const [sending, setSending] = useState(false);
- const pulseAnim = useRef(new Animated.Value(1)).current;
- const timerRef = useRef | null>(null);
-
- useEffect(() => {
- startRecording();
- const pulse = Animated.loop(
- Animated.sequence([
- Animated.timing(pulseAnim, { toValue: 1.3, duration: 600, useNativeDriver: true }),
- Animated.timing(pulseAnim, { toValue: 1, duration: 600, useNativeDriver: true }),
- ])
- );
- pulse.start();
- return () => {
- pulse.stop();
- stopRecordingCleanup();
- };
- }, []);
-
- const startRecording = async () => {
- try {
- const { granted } = await requestRecordingPermissionsAsync();
- if (!granted) {
- Alert.alert('Permission required', 'Microphone access is needed to record voice messages.');
- onCancel();
- return;
- }
- await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true });
- await recorder.prepareToRecordAsync();
- recorder.record();
- timerRef.current = setInterval(() => setDuration((d) => d + 1), 1000);
- } catch (err) {
- Alert.alert('Error', 'Could not start recording');
- onCancel();
- }
- };
-
- const stopRecordingCleanup = async () => {
- if (timerRef.current) clearInterval(timerRef.current);
- try {
- await recorder.stop();
- } catch {
- /* ignore */
- }
- };
-
- const handleSend = async () => {
- if (sending) return;
- setSending(true);
- if (timerRef.current) clearInterval(timerRef.current);
- try {
- await recorder.stop();
- const uri = recorder.uri;
- if (!uri) throw new Error('No recording URI');
- await onSend(uri, duration * 1000);
- } catch (err) {
- Alert.alert('Error', err instanceof Error ? err.message : 'Send failed');
- } finally {
- setSending(false);
- }
- };
-
- const handleDiscard = async () => {
- await stopRecordingCleanup();
- onCancel();
- };
-
- const formatDuration = (secs: number) => {
- const m = Math.floor(secs / 60);
- const s = secs % 60;
- return `${m}:${s.toString().padStart(2, '0')}`;
- };
-
- return (
-
- {/* Discard */}
-
-
-
-
- {/* Recording indicator */}
-
-
- {formatDuration(duration)}
- Recording...
-
-
- {/* Send */}
- = 1 ? 'bg-primary' : 'bg-surface border border-border'} active:opacity-60`}
- >
- = 1 ? '#fff' : '#6b7280'} weight="fill" />
-
-
- );
-}
diff --git a/apps/matrix/apps/mobile/src/matrix/client.ts b/apps/matrix/apps/mobile/src/matrix/client.ts
deleted file mode 100644
index 9b99c8e06..000000000
--- a/apps/matrix/apps/mobile/src/matrix/client.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import type { MatrixCredentials, LoginResult } from './types';
-
-function normalizeHomeserver(homeserver: string): string {
- let url = homeserver.trim();
- if (!url.startsWith('http://') && !url.startsWith('https://')) {
- url = `https://${url}`;
- }
- return url.replace(/\/$/, '');
-}
-
-export async function loginWithPassword(
- homeserver: string,
- username: string,
- password: string,
-): Promise {
- await import('./polyfills');
- const { createClient } = await import('matrix-js-sdk');
-
- const baseUrl = normalizeHomeserver(homeserver);
- const tempClient = createClient({ baseUrl });
-
- try {
- const response = await tempClient.login('m.login.password', {
- user: username,
- password,
- initial_device_display_name: 'Manalink Mobile',
- });
-
- return {
- success: true,
- credentials: {
- homeserver: baseUrl,
- accessToken: response.access_token,
- userId: response.user_id,
- deviceId: response.device_id,
- },
- };
- } catch (err) {
- const message = err instanceof Error ? err.message : 'Login failed';
- if (message.includes('M_FORBIDDEN')) return { success: false, error: 'Invalid username or password' };
- if (message.includes('M_USER_DEACTIVATED')) return { success: false, error: 'Account is deactivated' };
- if (message.includes('Failed to fetch') || message.includes('Network')) {
- return { success: false, error: 'Could not connect to homeserver' };
- }
- return { success: false, error: message };
- }
-}
-
-export async function loginWithToken(
- homeserver: string,
- accessToken: string,
- userId: string,
- deviceId?: string,
-): Promise {
- const baseUrl = normalizeHomeserver(homeserver);
- return {
- success: true,
- credentials: {
- homeserver: baseUrl,
- accessToken,
- userId,
- deviceId: deviceId ?? `MANALINK_${Date.now()}`,
- },
- };
-}
-
-export async function checkHomeserver(homeserver: string): Promise<{ ok: boolean; error?: string }> {
- const baseUrl = normalizeHomeserver(homeserver);
- try {
- const response = await fetch(`${baseUrl}/_matrix/client/versions`);
- if (response.ok) return { ok: true };
- return { ok: false, error: `Server returned ${response.status}` };
- } catch (err) {
- return { ok: false, error: err instanceof Error ? err.message : 'Could not connect' };
- }
-}
-
-export async function discoverHomeserver(userIdOrDomain: string): Promise {
- let domain = userIdOrDomain;
- if (userIdOrDomain.startsWith('@')) {
- const parts = userIdOrDomain.split(':');
- if (parts.length < 2) return null;
- domain = parts[1];
- }
- domain = domain.replace(/^https?:\/\//, '');
-
- try {
- const response = await fetch(`https://${domain}/.well-known/matrix/client`);
- if (response.ok) {
- const data = await response.json();
- const baseUrl = data['m.homeserver']?.base_url;
- if (baseUrl) return baseUrl.replace(/\/$/, '');
- }
- } catch {
- // .well-known not available
- }
-
- return `https://${domain}`;
-}
diff --git a/apps/matrix/apps/mobile/src/matrix/index.ts b/apps/matrix/apps/mobile/src/matrix/index.ts
deleted file mode 100644
index b831d9e4b..000000000
--- a/apps/matrix/apps/mobile/src/matrix/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from './types';
-export * from './client';
-export * from './media';
-export { useMatrixStore } from './store';
diff --git a/apps/matrix/apps/mobile/src/matrix/media.ts b/apps/matrix/apps/mobile/src/matrix/media.ts
deleted file mode 100644
index a9a188ce4..000000000
--- a/apps/matrix/apps/mobile/src/matrix/media.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Resolve Matrix mxc:// URLs to HTTPS URLs for display.
- * mxc://server/mediaId → https://server/_matrix/media/v3/download/server/mediaId
- */
-export function resolveMxcUrl(mxcUrl: string, homeserverUrl: string): string | null {
- if (!mxcUrl?.startsWith('mxc://')) return null;
-
- const withoutProtocol = mxcUrl.slice('mxc://'.length);
- const slashIndex = withoutProtocol.indexOf('/');
- if (slashIndex === -1) return null;
-
- const server = withoutProtocol.slice(0, slashIndex);
- const mediaId = withoutProtocol.slice(slashIndex + 1);
-
- // Use the homeserver as proxy (handles auth and federation)
- const base = homeserverUrl.replace(/\/$/, '');
- return `${base}/_matrix/media/v3/download/${server}/${mediaId}`;
-}
-
-/**
- * Resolve mxc:// to a thumbnail URL via the homeserver.
- */
-export function resolveMxcThumbnail(
- mxcUrl: string,
- homeserverUrl: string,
- width = 96,
- height = 96,
- method: 'crop' | 'scale' = 'crop',
-): string | null {
- if (!mxcUrl?.startsWith('mxc://')) return null;
-
- const withoutProtocol = mxcUrl.slice('mxc://'.length);
- const slashIndex = withoutProtocol.indexOf('/');
- if (slashIndex === -1) return null;
-
- const server = withoutProtocol.slice(0, slashIndex);
- const mediaId = withoutProtocol.slice(slashIndex + 1);
-
- const base = homeserverUrl.replace(/\/$/, '');
- return `${base}/_matrix/media/v3/thumbnail/${server}/${mediaId}?width=${width}&height=${height}&method=${method}`;
-}
diff --git a/apps/matrix/apps/mobile/src/matrix/polyfills.ts b/apps/matrix/apps/mobile/src/matrix/polyfills.ts
deleted file mode 100644
index c769201c5..000000000
--- a/apps/matrix/apps/mobile/src/matrix/polyfills.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-// Polyfills required for matrix-js-sdk in React Native
-import { Buffer } from 'buffer';
-
-const g = globalThis as any;
-
-g.Buffer = Buffer;
-
-// process stub (Expo provides process.env but not all fields)
-if (typeof g.process === 'undefined') {
- g.process = { env: {}, nextTick: setImmediate };
-} else if (typeof g.process.nextTick === 'undefined') {
- g.process.nextTick = setImmediate;
-}
-
-export {};
diff --git a/apps/matrix/apps/mobile/src/matrix/store.ts b/apps/matrix/apps/mobile/src/matrix/store.ts
deleted file mode 100644
index 8f1dafc5a..000000000
--- a/apps/matrix/apps/mobile/src/matrix/store.ts
+++ /dev/null
@@ -1,596 +0,0 @@
-import { create } from 'zustand';
-import * as SecureStore from 'expo-secure-store';
-import AsyncStorage from '@react-native-async-storage/async-storage';
-import type { MatrixClient, Room, MatrixEvent } from 'matrix-js-sdk';
-import { NotificationCountType } from 'matrix-js-sdk/lib/models/room';
-import type {
- MatrixCredentials,
- SimpleRoom,
- SimpleMessage,
- SyncState,
- MessageType,
- MessageReaction,
- ReadReceipt,
- RoomMember,
-} from './types';
-import { resolveMxcThumbnail, resolveMxcUrl } from './media';
-import { uploadMedia } from './upload';
-import { showMessageNotification, setBadgeCount } from '../notifications';
-
-const CREDENTIALS_KEY = 'manalink_credentials';
-const LAST_ROOM_KEY = 'manalink_last_room';
-const ROOMS_CACHE_KEY = 'manalink_rooms_cache';
-
-interface MatrixState {
- client: MatrixClient | null;
- credentials: MatrixCredentials | null;
- syncState: SyncState;
- rooms: SimpleRoom[];
- invites: SimpleRoom[];
- currentRoomId: string | null;
- messages: SimpleMessage[];
- firstUnreadEventId: string | null;
- typingUsers: string[];
- roomMembers: RoomMember[];
- error: string | null;
- isReady: boolean;
-
- initialize: (credentials: MatrixCredentials) => Promise;
- restoreSession: () => Promise;
- selectRoom: (roomId: string) => void;
- loadRoomMembers: (roomId: string) => void;
- sendMessage: (body: string, replyToEventId?: string) => Promise;
- sendReaction: (eventId: string, key: string) => Promise;
- redactMessage: (eventId: string) => Promise;
- sendTyping: (typing: boolean) => Promise;
- sendImage: (
- fileUri: string,
- filename: string,
- mimetype: string,
- width?: number,
- height?: number
- ) => Promise;
- sendFile: (fileUri: string, filename: string, mimetype: string) => Promise;
- editMessage: (eventId: string, newBody: string) => Promise;
- sendVoice: (fileUri: string, durationMs: number) => Promise;
- forwardMessage: (eventId: string, targetRoomId: string) => Promise;
- acceptInvite: (roomId: string) => Promise;
- declineInvite: (roomId: string) => Promise;
- leaveRoom: (roomId: string) => Promise;
- logout: () => Promise;
-}
-
-function roomToSimple(room: Room, userId: string, baseUrl: string): SimpleRoom {
- const timeline = room.getLiveTimeline().getEvents();
- const lastMsg = timeline.findLast((e) => e.getType() === 'm.room.message');
-
- const dmUserId = (() => {
- const members = room.getJoinedMembers();
- if (members.length === 2) return members.find((m) => m.userId !== userId)?.userId;
- return undefined;
- })();
-
- const rawAvatar = room.getMxcAvatarUrl?.() ?? null;
- const avatar = rawAvatar
- ? (resolveMxcThumbnail(rawAvatar, baseUrl, 96, 96) ?? undefined)
- : undefined;
-
- return {
- id: room.roomId,
- name: room.name || room.roomId,
- topic: room.currentState.getStateEvents('m.room.topic', '')?.getContent()?.topic,
- avatar,
- lastMessage: lastMsg?.getContent()?.body,
- lastMessageSender: lastMsg?.getSender() ?? undefined,
- lastMessageTime: room.getLastActiveTimestamp?.() ?? undefined,
- unreadCount: room.getUnreadNotificationCount(NotificationCountType.Total) ?? 0,
- highlightCount: room.getUnreadNotificationCount(NotificationCountType.Highlight) ?? 0,
- isDirect: !!dmUserId,
- isEncrypted: room.hasEncryptionStateEvent(),
- memberCount: room.getJoinedMemberCount(),
- membership: (room.getMyMembership() as SimpleRoom['membership']) ?? 'leave',
- inviter: room.getDMInviter?.() ?? undefined,
- dmUserId,
- };
-}
-
-function eventToMessage(
- event: MatrixEvent,
- userId: string,
- baseUrl: string,
- room?: Room
-): SimpleMessage | null {
- if (event.getType() !== 'm.room.message') return null;
-
- const content = event.getContent();
- const msgtype = content.msgtype as MessageType;
-
- // Resolve media if present
- let media = undefined;
- if (['m.image', 'm.file', 'm.audio', 'm.video'].includes(msgtype) && content.url) {
- const mxcUrl = content.url as string;
- const isAudio = msgtype === 'm.audio';
- media = {
- mxcUrl,
- mimetype: content.info?.mimetype,
- size: content.info?.size,
- width: content.info?.w,
- height: content.info?.h,
- filename: content.body,
- thumbnailUrl: isAudio
- ? undefined
- : (resolveMxcThumbnail(mxcUrl, baseUrl, 400, 300) ?? undefined),
- downloadUrl: resolveMxcUrl(mxcUrl, baseUrl) ?? undefined,
- duration: content.info?.duration,
- };
- }
-
- // Resolve sender avatar
- const senderMember = event.sender;
- const rawSenderAvatar = senderMember?.getMxcAvatarUrl?.() ?? null;
- const senderAvatar = rawSenderAvatar
- ? (resolveMxcThumbnail(rawSenderAvatar, baseUrl, 64, 64) ?? undefined)
- : undefined;
-
- // Reply-to
- const replyRelation = content['m.relates_to']?.['m.in_reply_to'];
- const replyToId: string | undefined = replyRelation?.event_id;
- let replyToBody: string | undefined;
- let replyToSenderName: string | undefined;
- if (replyToId && room) {
- const replyEvent = room.findEventById(replyToId);
- if (replyEvent) {
- replyToBody = replyEvent.getContent()?.body;
- replyToSenderName = replyEvent.sender?.name ?? replyEvent.getSender() ?? undefined;
- }
- }
-
- // Reactions
- let reactions: MessageReaction[] | undefined;
- if (room) {
- const eventId = event.getId();
- const reactionEvents = room
- .getLiveTimeline()
- .getEvents()
- .filter(
- (e) =>
- e.getType() === 'm.reaction' &&
- e.getContent()?.['m.relates_to']?.event_id === eventId &&
- e.getContent()?.['m.relates_to']?.rel_type === 'm.annotation'
- );
- if (reactionEvents.length > 0) {
- const grouped = new Map();
- for (const re of reactionEvents) {
- const key = re.getContent()?.['m.relates_to']?.key as string;
- if (!grouped.has(key)) grouped.set(key, { users: [], includesMe: false });
- const entry = grouped.get(key)!;
- const sender = re.getSender() ?? '';
- entry.users.push(sender);
- if (sender === userId) entry.includesMe = true;
- }
- reactions = Array.from(grouped.entries()).map(([key, { users, includesMe }]) => ({
- key,
- count: users.length,
- users,
- includesMe,
- }));
- }
- }
-
- // Read receipts
- let readBy: ReadReceipt[] | undefined;
- if (room) {
- const eventId = event.getId();
- if (eventId) {
- const receipts: ReadReceipt[] = [];
- const members = room.getMembersWithMembership('join');
- for (const member of members) {
- if (member.userId === userId) continue;
- const readUpTo = (room as any).getEventReadUpTo?.(member.userId) as string | null;
- if (readUpTo === eventId) {
- receipts.push({
- userId: member.userId,
- userName: member.name || member.userId,
- timestamp: 0,
- });
- }
- }
- if (receipts.length > 0) readBy = receipts;
- }
- }
-
- return {
- id: event.getId() ?? `${event.getTs()}_${event.getSender()}`,
- sender: event.getSender() ?? '',
- senderName: senderMember?.name ?? event.getSender() ?? '',
- senderAvatar,
- body: content.body ?? '',
- formattedBody: content.formatted_body,
- timestamp: event.getTs(),
- type: msgtype,
- isOwn: event.getSender() === userId,
- replyTo: replyToId,
- replyToBody,
- replyToSenderName,
- edited: !!event.replacingEvent(),
- redacted: event.isRedacted(),
- media,
- reactions,
- readBy,
- };
-}
-
-function buildSimpleRooms(client: MatrixClient, userId: string, baseUrl: string): SimpleRoom[] {
- return client
- .getRooms()
- .filter((r) => r.getMyMembership() === 'join')
- .map((r) => roomToSimple(r, userId, baseUrl))
- .sort((a, b) => (b.lastMessageTime ?? 0) - (a.lastMessageTime ?? 0));
-}
-
-function buildInvites(client: MatrixClient, userId: string, baseUrl: string): SimpleRoom[] {
- return client
- .getRooms()
- .filter((r) => r.getMyMembership() === 'invite')
- .map((r) => roomToSimple(r, userId, baseUrl));
-}
-
-function buildMessages(room: Room, userId: string, baseUrl: string): SimpleMessage[] {
- return room
- .getLiveTimeline()
- .getEvents()
- .map((e) => eventToMessage(e, userId, baseUrl, room))
- .filter((m): m is SimpleMessage => m !== null);
-}
-
-export const useMatrixStore = create((set, get) => ({
- client: null,
- credentials: null,
- syncState: 'STOPPED',
- rooms: [],
- invites: [],
- currentRoomId: null,
- messages: [],
- firstUnreadEventId: null,
- typingUsers: [],
- roomMembers: [],
- error: null,
- isReady: false,
-
- initialize: async (credentials: MatrixCredentials) => {
- const existing = get().client;
- if (existing) existing.stopClient();
-
- await import('./polyfills');
- const { createClient } = await import('matrix-js-sdk');
-
- const client = createClient({
- baseUrl: credentials.homeserver,
- accessToken: credentials.accessToken,
- userId: credentials.userId,
- deviceId: credentials.deviceId,
- });
-
- await SecureStore.setItemAsync(CREDENTIALS_KEY, JSON.stringify(credentials));
- set({ client, credentials });
-
- const { userId, homeserver: baseUrl } = credentials;
-
- // Load cached rooms immediately for fast startup
- try {
- const cached = await AsyncStorage.getItem(ROOMS_CACHE_KEY);
- if (cached) set({ rooms: JSON.parse(cached) });
- } catch {
- /* ignore cache errors */
- }
-
- const refresh = () => {
- const rooms = buildSimpleRooms(client, userId, baseUrl);
- const invites = buildInvites(client, userId, baseUrl);
- set({ rooms, invites });
- // Update badge count
- const totalUnread = rooms.reduce((n, r) => n + r.highlightCount, 0);
- setBadgeCount(totalUnread).catch(() => {});
- // Persist rooms cache
- AsyncStorage.setItem(ROOMS_CACHE_KEY, JSON.stringify(rooms)).catch(() => {});
- };
-
- const refreshMessages = (room: Room) => {
- const { currentRoomId } = get();
- if (room.roomId !== currentRoomId) return;
- set({ messages: buildMessages(room, userId, baseUrl) });
- };
-
- client.on('sync' as any, (state: SyncState) => {
- set({ syncState: state });
- if (state === 'PREPARED' || state === 'SYNCING') {
- refresh();
- set({ isReady: true, error: null });
- }
- if (state === 'ERROR') set({ error: 'Sync error — reconnecting...' });
- });
-
- client.on('Room.timeline' as any, (event: MatrixEvent, room: Room) => {
- refresh();
- refreshMessages(room);
-
- // Foreground notification for incoming messages
- const { currentRoomId } = get();
- if (
- event.getType() === 'm.room.message' &&
- event.getSender() !== userId &&
- room.roomId !== currentRoomId
- ) {
- const senderName = event.sender?.name ?? event.getSender() ?? 'Someone';
- const body = event.getContent()?.body ?? 'New message';
- showMessageNotification(senderName, room.name, body, room.roomId).catch(() => {});
- }
- });
-
- client.on('Room.redaction' as any, (_: unknown, room: Room) => {
- refresh();
- refreshMessages(room);
- });
-
- client.on('Room.name' as any, () => refresh());
- client.on('RoomState.events' as any, () => refresh());
- client.on('Room.myMembership' as any, () => refresh());
- client.on('Room.receipt' as any, (_: unknown, room: Room) => {
- refreshMessages(room);
- });
-
- client.on('RoomMember.typing' as any, (_: unknown, member: any) => {
- const { currentRoomId } = get();
- if (!currentRoomId || member.roomId !== currentRoomId) return;
- const room = client.getRoom(currentRoomId);
- if (!room) return;
- const typing = room
- .getMembersWithMembership('join')
- .filter((m: any) => m.typing && m.userId !== userId)
- .map((m: any) => m.name || m.userId);
- set({ typingUsers: typing });
- });
-
- await client.startClient({ initialSyncLimit: 50 });
- },
-
- restoreSession: async () => {
- try {
- const stored = await SecureStore.getItemAsync(CREDENTIALS_KEY);
- if (!stored) return false;
- const credentials: MatrixCredentials = JSON.parse(stored);
- await get().initialize(credentials);
- return true;
- } catch {
- return false;
- }
- },
-
- selectRoom: (roomId: string) => {
- const { client, credentials } = get();
- set({
- currentRoomId: roomId,
- typingUsers: [],
- messages: [],
- roomMembers: [],
- firstUnreadEventId: null,
- });
- if (!client || !credentials) return;
-
- const room = client.getRoom(roomId);
- if (!room) return;
-
- // Capture first unread event before marking as read
- const { userId, homeserver: baseUrl } = credentials;
- let firstUnreadEventId: string | null = null;
- const unreadCount = room.getUnreadNotificationCount(NotificationCountType.Total) ?? 0;
- if (unreadCount > 0) {
- const lastReadEventId = (room as any).getEventReadUpTo?.(userId) as string | null;
- if (lastReadEventId) {
- const timeline = room.getLiveTimeline().getEvents();
- const lastReadIdx = timeline.findIndex((e) => e.getId() === lastReadEventId);
- if (lastReadIdx >= 0) {
- const firstUnread = timeline
- .slice(lastReadIdx + 1)
- .find((e) => e.getType() === 'm.room.message');
- firstUnreadEventId = firstUnread?.getId() ?? null;
- }
- }
- }
-
- set({ messages: buildMessages(room, userId, baseUrl), firstUnreadEventId });
-
- SecureStore.setItemAsync(LAST_ROOM_KEY, roomId).catch(() => {});
-
- const lastEvent = room.getLiveTimeline().getEvents().at(-1);
- if (lastEvent) client.sendReadReceipt(lastEvent).catch(() => {});
- },
-
- loadRoomMembers: (roomId: string) => {
- const { client, credentials } = get();
- if (!client || !credentials) return;
- const room = client.getRoom(roomId);
- if (!room) return;
-
- const members: RoomMember[] = room
- .getMembersWithMembership('join')
- .map((m: any) => {
- const rawAvatar = m.getMxcAvatarUrl?.() ?? null;
- return {
- userId: m.userId,
- displayName: m.name || m.userId,
- avatarUrl: rawAvatar
- ? (resolveMxcThumbnail(rawAvatar, credentials.homeserver, 64, 64) ?? undefined)
- : undefined,
- membership: 'join' as const,
- powerLevel: m.powerLevel ?? 0,
- };
- })
- .sort((a: RoomMember, b: RoomMember) => b.powerLevel - a.powerLevel);
-
- set({ roomMembers: members });
- },
-
- sendMessage: async (body: string, replyToEventId?: string) => {
- const { client, currentRoomId } = get();
- if (!client || !currentRoomId) return;
-
- if (replyToEventId) {
- const room = client.getRoom(currentRoomId);
- const replyEvent = room?.findEventById(replyToEventId);
- if (replyEvent) {
- await (client as any).sendMessage(currentRoomId, {
- msgtype: 'm.text',
- body,
- 'm.relates_to': {
- 'm.in_reply_to': { event_id: replyToEventId },
- },
- });
- return;
- }
- }
-
- await client.sendTextMessage(currentRoomId, body);
- },
-
- sendReaction: async (eventId: string, key: string) => {
- const { client, currentRoomId } = get();
- if (!client || !currentRoomId) return;
- await (client as any).sendEvent(currentRoomId, 'm.reaction', {
- 'm.relates_to': { rel_type: 'm.annotation', event_id: eventId, key },
- });
- },
-
- redactMessage: async (eventId: string) => {
- const { client, currentRoomId } = get();
- if (!client || !currentRoomId) return;
- await client.redactEvent(currentRoomId, eventId);
- },
-
- sendTyping: async (typing: boolean) => {
- const { client, currentRoomId } = get();
- if (!client || !currentRoomId) return;
- client.sendTyping(currentRoomId, typing, 4000).catch(() => {});
- },
-
- sendImage: async (fileUri, filename, mimetype, width, height) => {
- const { client, currentRoomId } = get();
- if (!client || !currentRoomId) return;
- const uploaded = await uploadMedia(client, fileUri, filename, mimetype);
- await (client as any).sendMessage(currentRoomId, {
- msgtype: 'm.image',
- body: filename,
- url: uploaded.mxcUrl,
- info: {
- mimetype,
- size: uploaded.size,
- ...(width ? { w: width } : {}),
- ...(height ? { h: height } : {}),
- },
- });
- },
-
- sendFile: async (fileUri, filename, mimetype) => {
- const { client, currentRoomId } = get();
- if (!client || !currentRoomId) return;
- const uploaded = await uploadMedia(client, fileUri, filename, mimetype);
- await (client as any).sendMessage(currentRoomId, {
- msgtype: 'm.file',
- body: filename,
- url: uploaded.mxcUrl,
- info: { mimetype, size: uploaded.size },
- });
- },
-
- editMessage: async (eventId: string, newBody: string) => {
- const { client, currentRoomId } = get();
- if (!client || !currentRoomId) return;
- await (client as any).sendMessage(currentRoomId, {
- msgtype: 'm.text',
- body: `* ${newBody}`,
- 'm.new_content': { msgtype: 'm.text', body: newBody },
- 'm.relates_to': { rel_type: 'm.replace', event_id: eventId },
- });
- },
-
- sendVoice: async (fileUri: string, durationMs: number) => {
- const { client, currentRoomId } = get();
- if (!client || !currentRoomId) return;
- const filename = `voice_${Date.now()}.m4a`;
- const uploaded = await uploadMedia(client, fileUri, filename, 'audio/m4a');
- await (client as any).sendMessage(currentRoomId, {
- msgtype: 'm.audio',
- body: filename,
- url: uploaded.mxcUrl,
- info: { mimetype: 'audio/m4a', size: uploaded.size, duration: durationMs },
- });
- },
-
- forwardMessage: async (eventId: string, targetRoomId: string) => {
- const { client, currentRoomId } = get();
- if (!client || !currentRoomId) return;
- const room = client.getRoom(currentRoomId);
- const event = room?.findEventById(eventId);
- if (!event) return;
- const content = event.getContent();
- const msgtype = content.msgtype ?? 'm.text';
- // Forward as a fresh message (strip reply relations)
- const forwarded: Record = { msgtype, body: content.body };
- if (content.url) forwarded.url = content.url;
- if (content.info) forwarded.info = content.info;
- if (content.formatted_body) {
- forwarded.format = content.format;
- forwarded.formatted_body = content.formatted_body;
- }
- await (client as any).sendMessage(targetRoomId, forwarded);
- },
-
- leaveRoom: async (roomId: string) => {
- const { client } = get();
- if (!client) return;
- await client.leave(roomId);
- // If we left the current room, clear it
- const { currentRoomId } = get();
- if (currentRoomId === roomId) {
- set({ currentRoomId: null, messages: [], roomMembers: [] });
- }
- },
-
- acceptInvite: async (roomId: string) => {
- const { client } = get();
- if (!client) return;
- await client.joinRoom(roomId);
- },
-
- declineInvite: async (roomId: string) => {
- const { client } = get();
- if (!client) return;
- await client.leave(roomId);
- },
-
- logout: async () => {
- const { client } = get();
- try {
- await client?.logout();
- } catch {
- // non-fatal
- }
- client?.stopClient();
- await SecureStore.deleteItemAsync(CREDENTIALS_KEY).catch(() => {});
- await SecureStore.deleteItemAsync(LAST_ROOM_KEY).catch(() => {});
- await AsyncStorage.removeItem(ROOMS_CACHE_KEY).catch(() => {});
- await setBadgeCount(0).catch(() => {});
- set({
- client: null,
- credentials: null,
- syncState: 'STOPPED',
- rooms: [],
- invites: [],
- currentRoomId: null,
- messages: [],
- firstUnreadEventId: null,
- error: null,
- isReady: false,
- });
- },
-}));
diff --git a/apps/matrix/apps/mobile/src/matrix/types.ts b/apps/matrix/apps/mobile/src/matrix/types.ts
deleted file mode 100644
index 3f80f26b4..000000000
--- a/apps/matrix/apps/mobile/src/matrix/types.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-export type SyncState = 'STOPPED' | 'PREPARED' | 'SYNCING' | 'ERROR' | 'RECONNECTING' | 'CATCHUP';
-
-export interface MatrixCredentials {
- homeserver: string;
- accessToken: string;
- userId: string;
- deviceId: string;
-}
-
-export interface LoginResult {
- success: boolean;
- credentials?: MatrixCredentials;
- error?: string;
-}
-
-export type PresenceState = 'online' | 'offline' | 'unavailable';
-export type RoomMembership = 'join' | 'invite' | 'leave' | 'ban' | 'knock';
-export type MessageType = 'm.text' | 'm.image' | 'm.file' | 'm.audio' | 'm.video' | 'm.emote' | 'm.notice';
-
-export interface SimpleRoom {
- id: string;
- name: string;
- topic?: string;
- avatar?: string;
- lastMessage?: string;
- lastMessageSender?: string;
- lastMessageTime?: number;
- unreadCount: number;
- highlightCount: number;
- isDirect: boolean;
- isEncrypted: boolean;
- memberCount: number;
- membership: RoomMembership;
- inviter?: string;
- dmUserId?: string;
- presence?: PresenceState;
- lastActiveAgo?: number;
-}
-
-export interface MediaInfo {
- mxcUrl: string;
- mimetype?: string;
- size?: number;
- width?: number;
- height?: number;
- filename?: string;
- thumbnailUrl?: string;
- downloadUrl?: string;
- duration?: number;
-}
-
-export interface MessageReaction {
- key: string;
- count: number;
- users: string[];
- includesMe: boolean;
-}
-
-export interface ReadReceipt {
- userId: string;
- userName: string;
- timestamp: number;
-}
-
-export interface SimpleMessage {
- id: string;
- sender: string;
- senderName: string;
- senderAvatar?: string;
- body: string;
- formattedBody?: string;
- timestamp: number;
- type: MessageType;
- isOwn: boolean;
- replyTo?: string;
- replyToBody?: string;
- replyToSenderName?: string;
- edited?: boolean;
- redacted?: boolean;
- media?: MediaInfo;
- reactions?: MessageReaction[];
- readBy?: ReadReceipt[];
-}
-
-export interface RoomMember {
- userId: string;
- displayName: string;
- avatarUrl?: string;
- membership: 'join' | 'invite' | 'leave' | 'ban' | 'knock';
- powerLevel: number;
-}
diff --git a/apps/matrix/apps/mobile/src/matrix/upload.ts b/apps/matrix/apps/mobile/src/matrix/upload.ts
deleted file mode 100644
index 40c7eb046..000000000
--- a/apps/matrix/apps/mobile/src/matrix/upload.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import type { MatrixClient } from 'matrix-js-sdk';
-
-export interface UploadResult {
- mxcUrl: string;
- mimetype: string;
- size: number;
- width?: number;
- height?: number;
- filename?: string;
-}
-
-/**
- * Upload a local file URI to the Matrix homeserver.
- * Returns the mxc:// URL and metadata.
- */
-export async function uploadMedia(
- client: MatrixClient,
- fileUri: string,
- filename: string,
- mimetype: string,
-): Promise {
- // Fetch the file as a blob
- const response = await fetch(fileUri);
- const blob = await response.blob();
-
- // Use the matrix-js-sdk upload endpoint
- const uploadResponse = await (client as any).uploadContent(blob, {
- name: filename,
- type: mimetype,
- rawResponse: false,
- });
-
- const mxcUrl: string = uploadResponse?.content_uri ?? uploadResponse;
-
- return {
- mxcUrl,
- mimetype,
- size: blob.size,
- filename,
- };
-}
-
-export function getMimetypeFromFilename(filename: string): string {
- const ext = filename.split('.').pop()?.toLowerCase() ?? '';
- const map: Record = {
- jpg: 'image/jpeg',
- jpeg: 'image/jpeg',
- png: 'image/png',
- gif: 'image/gif',
- webp: 'image/webp',
- heic: 'image/heic',
- mp4: 'video/mp4',
- mov: 'video/quicktime',
- mp3: 'audio/mpeg',
- m4a: 'audio/mp4',
- ogg: 'audio/ogg',
- pdf: 'application/pdf',
- doc: 'application/msword',
- docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- zip: 'application/zip',
- };
- return map[ext] ?? 'application/octet-stream';
-}
diff --git a/apps/matrix/apps/mobile/src/notifications/index.ts b/apps/matrix/apps/mobile/src/notifications/index.ts
deleted file mode 100644
index b41647985..000000000
--- a/apps/matrix/apps/mobile/src/notifications/index.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import * as Notifications from 'expo-notifications';
-import { router } from 'expo-router';
-import type { MatrixClient } from 'matrix-js-sdk';
-
-// Show notifications even when app is in foreground
-Notifications.setNotificationHandler({
- handleNotification: async () => ({
- shouldShowAlert: true,
- shouldPlaySound: true,
- shouldSetBadge: true,
- shouldShowBanner: true,
- shouldShowList: true,
- }),
-});
-
-export async function requestNotificationPermissions(): Promise {
- const { status: existing } = await Notifications.getPermissionsAsync();
- if (existing === 'granted') return true;
- const { status } = await Notifications.requestPermissionsAsync();
- return status === 'granted';
-}
-
-/**
- * Get the Expo push token for this device.
- * projectId from app.json extra.eas.projectId.
- */
-export async function getExpoPushToken(projectId?: string): Promise {
- try {
- const token = await Notifications.getExpoPushTokenAsync(
- projectId ? { projectId } : undefined,
- );
- return token.data;
- } catch {
- return null;
- }
-}
-
-/**
- * Register a Matrix HTTP pusher pointing to the Expo push proxy.
- * This requires a compatible Matrix push gateway (e.g. a custom proxy or sygnal).
- * For development, this is optional — sync keeps the app connected.
- */
-export async function registerMatrixPusher(
- client: MatrixClient,
- pushToken: string,
- appId: string,
- appDisplayName: string,
- deviceDisplayName: string,
- pushGatewayUrl: string,
-): Promise {
- await (client as any).setPusher({
- pushkey: pushToken,
- kind: 'http',
- app_id: appId,
- app_display_name: appDisplayName,
- device_display_name: deviceDisplayName,
- lang: 'en',
- data: {
- url: `${pushGatewayUrl}/_matrix/push/v1/notify`,
- format: 'event_id_only',
- },
- });
-}
-
-/**
- * Display a local notification for an incoming message.
- * Called from the Matrix sync event handler for messages
- * while the app is in the foreground.
- */
-export async function showMessageNotification(
- senderName: string,
- roomName: string,
- body: string,
- roomId: string,
-): Promise {
- await Notifications.scheduleNotificationAsync({
- content: {
- title: `${senderName} in ${roomName}`,
- body,
- data: { roomId },
- sound: true,
- },
- trigger: null, // fire immediately
- });
-}
-
-/**
- * Set the app badge count.
- */
-export async function setBadgeCount(count: number): Promise {
- await Notifications.setBadgeCountAsync(count);
-}
-
-/**
- * Listen for notification taps and navigate to the room.
- * Returns a cleanup function.
- */
-export function setupNotificationNavigation(): () => void {
- const subscription = Notifications.addNotificationResponseReceivedListener((response) => {
- const roomId = response.notification.request.content.data?.roomId as string | undefined;
- if (roomId) {
- router.push(`/room/${roomId}`);
- }
- });
-
- return () => subscription.remove();
-}
diff --git a/apps/matrix/apps/mobile/tailwind.config.js b/apps/matrix/apps/mobile/tailwind.config.js
deleted file mode 100644
index cd7c4ef81..000000000
--- a/apps/matrix/apps/mobile/tailwind.config.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- content: ['./app/**/*.{js,ts,tsx}', './src/**/*.{js,ts,tsx}'],
- presets: [require('nativewind/preset')],
- theme: {
- extend: {
- colors: {
- background: '#0f0f0f',
- surface: '#1a1a1a',
- border: '#2a2a2a',
- primary: '#7c6bff',
- 'primary-foreground': '#ffffff',
- muted: '#6b7280',
- foreground: '#f9fafb',
- 'muted-foreground': '#9ca3af',
- destructive: '#ef4444',
- },
- },
- },
- plugins: [],
-};
diff --git a/apps/matrix/apps/mobile/tsconfig.json b/apps/matrix/apps/mobile/tsconfig.json
deleted file mode 100644
index 7bef4d58e..000000000
--- a/apps/matrix/apps/mobile/tsconfig.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "extends": "expo/tsconfig.base",
- "compilerOptions": {
- "strict": true,
- "jsx": "react-jsx",
- "baseUrl": ".",
- "paths": {
- "~/*": ["./*"]
- }
- },
- "include": [
- "**/*.ts",
- "**/*.tsx",
- ".expo/types/**/*.ts",
- "expo-env.d.ts",
- "nativewind-env.d.ts"
- ]
-}
diff --git a/apps/matrix/apps/web/Dockerfile b/apps/matrix/apps/web/Dockerfile
deleted file mode 100644
index ec6bf4df0..000000000
--- a/apps/matrix/apps/web/Dockerfile
+++ /dev/null
@@ -1,32 +0,0 @@
-# syntax=docker/dockerfile:1
-FROM sveltekit-base:local AS builder
-
-ARG PUBLIC_BACKEND_URL=http://mana-auth
-ARG PUBLIC_MANA_AUTH_URL=http://mana-auth:3001
-ENV PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL
-ENV PUBLIC_MANA_AUTH_URL=$PUBLIC_MANA_AUTH_URL
-
-COPY apps/matrix/packages/shared ./apps/matrix/packages/shared
-COPY apps/matrix/apps/web ./apps/matrix/apps/web
-
-RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
- pnpm install --no-frozen-lockfile --ignore-scripts
-
-WORKDIR /app/apps/matrix/apps/web
-RUN pnpm exec svelte-kit sync
-RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build
-
-FROM node:20-alpine AS production
-WORKDIR /app/apps/matrix/apps/web
-COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
-COPY --from=builder /app/apps/matrix/apps/web/node_modules ./node_modules
-COPY --from=builder /app/apps/matrix/apps/web/build ./build
-COPY --from=builder /app/apps/matrix/apps/web/package.json ./
-
-EXPOSE 5024
-ENV NODE_ENV=production PORT=5024 HOST=0.0.0.0
-
-HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
- CMD wget --no-verbose --tries=1 --spider http://localhost:5024/health || exit 1
-
-CMD ["node", "build"]
diff --git a/apps/matrix/apps/web/package.json b/apps/matrix/apps/web/package.json
deleted file mode 100644
index 58e8d4940..000000000
--- a/apps/matrix/apps/web/package.json
+++ /dev/null
@@ -1,60 +0,0 @@
-{
- "name": "@matrix/web",
- "version": "0.2.0",
- "private": true,
- "type": "module",
- "scripts": {
- "dev": "vite dev",
- "build": "vite build",
- "preview": "vite preview",
- "prepare": "svelte-kit sync || echo ''",
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
- "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
- "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
- "format": "prettier --write .",
- "lint": "eslint .",
- "test": "vitest run",
- "test:watch": "vitest"
- },
- "devDependencies": {
- "@mana/shared-pwa": "workspace:*",
- "@sveltejs/adapter-node": "^5.2.12",
- "@sveltejs/kit": "^2.47.1",
- "@sveltejs/vite-plugin-svelte": "^5.0.3",
- "@tailwindcss/vite": "^4.1.7",
- "@types/node": "^22.15.21",
- "@vite-pwa/sveltekit": "^1.1.0",
- "prettier": "^3.5.3",
- "prettier-plugin-svelte": "^3.4.0",
- "sharp": "^0.33.5",
- "svelte": "^5.41.0",
- "svelte-check": "^4.3.3",
- "tailwindcss": "^4.1.7",
- "tslib": "^2.8.1",
- "typescript": "^5.9.3",
- "vite": "^6.3.5",
- "vite-plugin-pwa": "^1.2.0",
- "vitest": "^4.1.2",
- "workbox-window": "^7.4.0"
- },
- "dependencies": {
- "@mana/shared-auth": "workspace:*",
- "@mana/shared-branding": "workspace:*",
- "@mana/shared-error-tracking": "workspace:*",
- "@mana/feedback": "workspace:*",
- "@mana/help": "workspace:*",
- "@mana/shared-i18n": "workspace:*",
- "@mana/shared-icons": "workspace:*",
- "@mana/shared-tailwind": "workspace:*",
- "@mana/shared-theme": "workspace:*",
- "@mana/shared-stores": "workspace:*",
- "@mana/shared-tags": "workspace:*",
- "@mana/shared-ui": "workspace:*",
- "@matrix-org/matrix-sdk-crypto-wasm": "^13.0.0",
- "buffer": "^6.0.3",
- "date-fns": "^4.1.0",
- "events": "^3.3.0",
- "matrix-js-sdk": "^37.1.0",
- "svelte-i18n": "^4.0.1"
- }
-}
diff --git a/apps/matrix/apps/web/scripts/generate-icons.mjs b/apps/matrix/apps/web/scripts/generate-icons.mjs
deleted file mode 100644
index 4a0e0f23d..000000000
--- a/apps/matrix/apps/web/scripts/generate-icons.mjs
+++ /dev/null
@@ -1,50 +0,0 @@
-#!/usr/bin/env node
-/**
- * Generate PWA icons from SVG favicon
- * Run: node scripts/generate-icons.mjs
- * Requires: npm install -D sharp
- */
-
-import { readFileSync, writeFileSync } from 'fs';
-import { join, dirname } from 'path';
-import { fileURLToPath } from 'url';
-
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const staticDir = join(__dirname, '..', 'static');
-
-// Icon sizes to generate
-const sizes = [
- { name: 'favicon.png', size: 32 },
- { name: 'pwa-192x192.png', size: 192 },
- { name: 'pwa-512x512.png', size: 512 },
- { name: 'apple-touch-icon.png', size: 180 },
-];
-
-async function generateIcons() {
- try {
- const sharp = (await import('sharp')).default;
- const svgPath = join(staticDir, 'favicon.svg');
- const svgBuffer = readFileSync(svgPath);
-
- for (const { name, size } of sizes) {
- const outputPath = join(staticDir, name);
- await sharp(svgBuffer).resize(size, size).png().toFile(outputPath);
- console.log(`Generated: ${name} (${size}x${size})`);
- }
-
- console.log('\nAll icons generated successfully!');
- } catch (error) {
- if (error.code === 'ERR_MODULE_NOT_FOUND') {
- console.error('Sharp is not installed. Run: pnpm add -D sharp');
- console.log('\nAlternatively, use an online tool to convert the SVG:');
- console.log('1. Open static/favicon.svg in a browser');
- console.log('2. Use https://realfavicongenerator.net/ to generate icons');
- console.log('3. Replace the placeholder PNGs in static/');
- } else {
- console.error('Error generating icons:', error);
- }
- process.exit(1);
- }
-}
-
-generateIcons();
diff --git a/apps/matrix/apps/web/src/app.css b/apps/matrix/apps/web/src/app.css
deleted file mode 100644
index 6c38be0dd..000000000
--- a/apps/matrix/apps/web/src/app.css
+++ /dev/null
@@ -1,127 +0,0 @@
-@import 'tailwindcss';
-@import '@mana/shared-tailwind/themes.css';
-
-/* Scan shared packages for Tailwind classes */
-@source '../../../packages/shared/src';
-@source '../../../../../packages/shared-ui/src';
-@source '../../../../../packages/shared-icons/src';
-@source '../../../../../packages/shared-auth-ui/src';
-@source '../../../../../packages/shared-theme-ui/src';
-@source '../../../../../packages/shared-branding/src';
-
-@layer base {
- :root {
- --spacing-xs: 0.25rem;
- --spacing-sm: 0.5rem;
- --spacing-md: 1rem;
- --spacing-lg: 1.5rem;
- --spacing-xl: 2rem;
- }
-
- body {
- @apply bg-background text-foreground;
- }
-}
-
-/* Fade-in animation */
-@keyframes fade-in {
- from {
- opacity: 0;
- transform: translateY(8px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.animate-fade-in {
- animation: fade-in 0.3s ease-out;
-}
-
-/* Slide-up animation for bottom sheets */
-@keyframes slide-up {
- from {
- transform: translateY(100%);
- }
- to {
- transform: translateY(0);
- }
-}
-
-.animate-slide-up {
- animation: slide-up 0.25s ease-out;
-}
-
-/* Custom scrollbar for chat */
-.chat-scrollbar::-webkit-scrollbar {
- width: 6px;
-}
-
-.chat-scrollbar::-webkit-scrollbar-track {
- background: transparent;
-}
-
-.chat-scrollbar::-webkit-scrollbar-thumb {
- background: rgba(0, 0, 0, 0.15);
- border-radius: 3px;
-}
-
-.chat-scrollbar::-webkit-scrollbar-thumb:hover {
- background: rgba(0, 0, 0, 0.25);
-}
-
-.dark .chat-scrollbar::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.15);
-}
-
-.dark .chat-scrollbar::-webkit-scrollbar-thumb:hover {
- background: rgba(255, 255, 255, 0.25);
-}
-
-/* Elevation utilities - semantic surface styles */
-.glass {
- @apply bg-surface-elevated border border-border;
-}
-
-.glass-card {
- @apply bg-surface border border-border;
-}
-
-.glass-button {
- @apply bg-surface border border-border hover:bg-surface-hover hover:shadow-lg transition-all duration-200;
-}
-
-/* iOS Safe Area Insets for PWA */
-.safe-area-top {
- padding-top: env(safe-area-inset-top, 0px);
-}
-
-.safe-area-bottom {
- padding-bottom: env(safe-area-inset-bottom, 0px);
-}
-
-.safe-area-left {
- padding-left: env(safe-area-inset-left, 0px);
-}
-
-.safe-area-right {
- padding-right: env(safe-area-inset-right, 0px);
-}
-
-.safe-area-x {
- padding-left: env(safe-area-inset-left, 0px);
- padding-right: env(safe-area-inset-right, 0px);
-}
-
-.safe-area-y {
- padding-top: env(safe-area-inset-top, 0px);
- padding-bottom: env(safe-area-inset-bottom, 0px);
-}
-
-.safe-area-all {
- padding-top: env(safe-area-inset-top, 0px);
- padding-bottom: env(safe-area-inset-bottom, 0px);
- padding-left: env(safe-area-inset-left, 0px);
- padding-right: env(safe-area-inset-right, 0px);
-}
diff --git a/apps/matrix/apps/web/src/app.d.ts b/apps/matrix/apps/web/src/app.d.ts
deleted file mode 100644
index db149168c..000000000
--- a/apps/matrix/apps/web/src/app.d.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-declare const __BUILD_HASH__: string;
-declare const __BUILD_TIME__: string;
-
-// See https://svelte.dev/docs/kit/types#app.d.ts
-// for information about these interfaces
-declare global {
- namespace App {
- // interface Error {}
- // interface Locals {}
- // interface PageData {}
- // interface PageState {}
- // interface Platform {}
- }
-
- // Polyfills for matrix-js-sdk
- interface Window {
- global: typeof globalThis;
- Buffer: typeof import('buffer').Buffer;
- process: { env: Record };
- }
-}
-
-export {};
diff --git a/apps/matrix/apps/web/src/app.html b/apps/matrix/apps/web/src/app.html
deleted file mode 100644
index cbb299c88..000000000
--- a/apps/matrix/apps/web/src/app.html
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Manalink
- %sveltekit.head%
-
-
- %sveltekit.body%
-
-
diff --git a/apps/matrix/apps/web/src/hooks.client.ts b/apps/matrix/apps/web/src/hooks.client.ts
deleted file mode 100644
index a7b632d03..000000000
--- a/apps/matrix/apps/web/src/hooks.client.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { initErrorTracking, handleSvelteError } from '@mana/shared-error-tracking/browser';
-import type { HandleClientError } from '@sveltejs/kit';
-
-initErrorTracking({
- serviceName: 'matrix-web',
- dsn: (window as any).__PUBLIC_GLITCHTIP_DSN__,
- environment: import.meta.env.MODE,
-});
-
-export const handleError: HandleClientError = ({ error }) => {
- handleSvelteError(error);
-};
diff --git a/apps/matrix/apps/web/src/hooks.server.ts b/apps/matrix/apps/web/src/hooks.server.ts
deleted file mode 100644
index 7351c9016..000000000
--- a/apps/matrix/apps/web/src/hooks.server.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { Handle } from '@sveltejs/kit';
-
-export const handle: Handle = async ({ event, resolve }) => {
- const response = await resolve(event);
-
- response.headers.set('X-Frame-Options', 'SAMEORIGIN');
- response.headers.set('X-Content-Type-Options', 'nosniff');
- response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
- response.headers.set('Permissions-Policy', 'camera=(self), microphone=(self), geolocation=()');
- // COEP/COOP required for WASM (matrix-sdk-crypto)
- response.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
- response.headers.set('Cross-Origin-Embedder-Policy', 'require-corp');
-
- return response;
-};
diff --git a/apps/matrix/apps/web/src/lib/components/bots/BotCard.svelte b/apps/matrix/apps/web/src/lib/components/bots/BotCard.svelte
deleted file mode 100644
index 5f9fcee30..000000000
--- a/apps/matrix/apps/web/src/lib/components/bots/BotCard.svelte
+++ /dev/null
@@ -1,166 +0,0 @@
-
-
-
-
-
(expanded = !expanded)}
- >
-
-
-
-
-
-
-
{bot.name}
- {#if bot.isGateway}
-
- Gateway
-
- {/if}
-
-
{bot.description}
-
-
-
-
-
- {#if bot.requiresAuth}
-
- Login
-
- {:else}
-
-
- {$t('bots.free')}
-
- {/if}
-
- {bot.commands.length}
- {$t('bots.commands')}
-
-
-
-
-
- {#if expanded}
-
-
- {#if bot.longDescription}
-
{bot.longDescription}
- {/if}
-
-
-
-
{$t('bots.commands')}
-
- {#each bot.commands as cmd}
-
-
{cmd.command}
- {#if cmd.aliases?.length}
-
({cmd.aliases.join(', ')})
- {/if}
-
- {cmd.description}
- {#if cmd.example}
-
- {$t('bots.example')}: {cmd.example}
-
- {/if}
-
- {/each}
-
-
-
-
-
- Matrix:
- {bot.matrixUserId}
-
-
-
-
{
- e.stopPropagation();
- onStartChat();
- }}
- >
-
- {$t('bots.startChat')}
-
-
- {/if}
-
diff --git a/apps/matrix/apps/web/src/lib/components/call/CallView.svelte b/apps/matrix/apps/web/src/lib/components/call/CallView.svelte
deleted file mode 100644
index b4c4ad4ac..000000000
--- a/apps/matrix/apps/web/src/lib/components/call/CallView.svelte
+++ /dev/null
@@ -1,224 +0,0 @@
-
-
-
-
-
-
- {#if call.opponentAvatar}
-
- {:else}
-
-
-
- {/if}
-
-
{call.opponentName || 'Unbekannt'}
-
- {call.type === 'video' ? 'Videoanruf' : 'Sprachanruf'} · {getStateText(call.state)}
- {#if call.isScreenSharing}
-
-
- Bildschirmfreigabe
-
- {/if}
-
-
-
-
-
-
-
- {#if call.type === 'video'}
-
-
-
-
-
-
-
-
- {:else}
-
-
- {#if call.opponentAvatar}
-
- {:else}
-
-
-
- {/if}
-
{call.opponentName || 'Unbekannt'}
-
{getStateText(call.state)}
-
- {/if}
-
-
-
-
-
-
- {#if call.isMicMuted}
-
- {:else}
-
- {/if}
-
-
-
- {#if call.type === 'video'}
-
- {#if call.isCameraMuted}
-
- {:else}
-
- {/if}
-
-
-
-
-
-
- {/if}
-
-
-
-
-
-
-
diff --git a/apps/matrix/apps/web/src/lib/components/call/IncomingCallDialog.svelte b/apps/matrix/apps/web/src/lib/components/call/IncomingCallDialog.svelte
deleted file mode 100644
index 069a5e99f..000000000
--- a/apps/matrix/apps/web/src/lib/components/call/IncomingCallDialog.svelte
+++ /dev/null
@@ -1,123 +0,0 @@
-
-
-
-
-
-
- {#if call.opponentAvatar}
-
- {:else}
-
-
-
- {/if}
-
{call.opponentName || 'Unbekannt'}
-
- {call.type === 'video' ? 'Eingehender Videoanruf' : 'Eingehender Sprachanruf'}
-
-
-
-
-
- {#if call.type === 'video'}
-
-
Video
- {:else}
-
-
Audio
- {/if}
-
-
-
-
-
-
-
-
diff --git a/apps/matrix/apps/web/src/lib/components/call/index.ts b/apps/matrix/apps/web/src/lib/components/call/index.ts
deleted file mode 100644
index 2245f06a3..000000000
--- a/apps/matrix/apps/web/src/lib/components/call/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as CallView } from './CallView.svelte';
-export { default as IncomingCallDialog } from './IncomingCallDialog.svelte';
diff --git a/apps/matrix/apps/web/src/lib/components/chat/CreateRoomDialog.svelte b/apps/matrix/apps/web/src/lib/components/chat/CreateRoomDialog.svelte
deleted file mode 100644
index 4fa6e9a17..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/CreateRoomDialog.svelte
+++ /dev/null
@@ -1,313 +0,0 @@
-
-
-{#if open}
-
-
-
-
e.stopPropagation()}
- role="dialog"
- aria-modal="true"
- >
-
-
-
Neuer Chat
-
-
-
-
-
-
-
-
-
- (isDirect = true)}
- >
-
- Direktnachricht
-
- (isDirect = false)}
- >
-
- Gruppenraum
-
-
-
-
- {#if !isDirect}
-
- Raumname
-
-
-
-
- Beschreibung (optional)
-
-
-
-
-
-
- {#if isPrivate}
-
- Privater Raum
- {:else}
-
- Öffentlicher Raum
- {/if}
-
- (isPrivate = !isPrivate)}
- >
-
-
-
-
- {isPrivate
- ? 'Nur eingeladene Benutzer können beitreten'
- : 'Jeder kann diesen Raum finden und beitreten'}
-
- {/if}
-
-
-
-
- {isDirect ? 'Mit wem möchtest du chatten?' : 'Benutzer einladen (optional)'}
-
-
-
- {#if searching}
-
- {/if}
-
-
-
- {#if searchResults.length > 0}
-
- {#each searchResults as user}
-
selectUser(user)}
- >
-
- {#if user.avatarUrl}
-
- {:else}
- {user.displayName?.[0] || user.userId[1]}
- {/if}
-
-
-
- {user.displayName || user.userId}
-
- {#if user.displayName}
-
{user.userId}
- {/if}
-
-
- {/each}
-
- {/if}
-
-
-
- {#if selectedUsers.length > 0}
-
- {#each selectedUsers as user}
-
- {user.displayName || user.userId}
- removeUser(user.userId)}
- >
-
-
-
- {/each}
-
- {/if}
-
-
- {#if error}
-
- {error}
-
- {/if}
-
-
-
-
-
- Abbrechen
-
-
- {#if loading}
-
- {/if}
- {isDirect ? 'Chat starten' : 'Raum erstellen'}
-
-
-
-
-{/if}
diff --git a/apps/matrix/apps/web/src/lib/components/chat/DropZoneOverlay.svelte b/apps/matrix/apps/web/src/lib/components/chat/DropZoneOverlay.svelte
deleted file mode 100644
index d6d5c2622..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/DropZoneOverlay.svelte
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-{#if visible}
-
-
-
-
-
-
-
Datei hier ablegen
-
Bilder, Videos oder Dateien
-
-
-
-{/if}
diff --git a/apps/matrix/apps/web/src/lib/components/chat/ForwardMessageDialog.svelte b/apps/matrix/apps/web/src/lib/components/chat/ForwardMessageDialog.svelte
deleted file mode 100644
index e71940ff8..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/ForwardMessageDialog.svelte
+++ /dev/null
@@ -1,179 +0,0 @@
-
-
-{#if open && message}
-
-
-
-
e.stopPropagation()}
- >
-
-
-
Nachricht weiterleiten
-
-
-
-
-
-
-
-
Von {message.senderName}
-
{message.body}
-
-
-
-
-
-
-
- {#if filteredRooms.length === 0}
-
Keine Chats gefunden
- {:else}
- {#each filteredRooms as room (room.id)}
-
toggleRoom(room.id)}
- >
-
-
- {#if selectedRooms.has(room.id)}
-
- {/if}
-
-
-
-
- {#if room.avatar}
-
- {:else if room.isDirect}
-
- {:else}
-
- {/if}
-
-
-
-
-
{room.name}
-
- {room.isDirect ? 'Direktnachricht' : `${room.memberCount} Mitglieder`}
-
-
-
- {/each}
- {/if}
-
-
-
-
-
- {selectedRooms.size} ausgewählt
-
-
-
- {sending ? 'Sende...' : 'Weiterleiten'}
-
-
-
-
-{/if}
diff --git a/apps/matrix/apps/web/src/lib/components/chat/Message.svelte b/apps/matrix/apps/web/src/lib/components/chat/Message.svelte
deleted file mode 100644
index 6e64baf8e..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/Message.svelte
+++ /dev/null
@@ -1,845 +0,0 @@
-
-
-
-{#if showTimestamp}
-
-
-
{formattedDate()}
-
-
-{/if}
-
-
- (showActions = true)}
- onmouseleave={() => (showActions = false)}
- ontouchstart={handleTouchStart}
- ontouchmove={handleTouchMove}
- ontouchend={handleTouchEnd}
- ontouchcancel={handleTouchEnd}
->
-
- {#if showAvatar}
-
- {initials}
-
- {:else}
-
- {/if}
-
-
-
-
- {#if showAvatar && !message.isOwn}
-
{message.senderName}
- {/if}
-
-
- {#if message.replyTo && message.replyToBody}
-
-
-
{message.replyToBody}
-
- {/if}
-
-
-
- {#if message.redacted}
-
Nachricht wurde gelöscht
- {:else if isDecryptionError}
-
-
-
- Kann nicht entschlüsselt werden
-
- {:else if message.type === 'm.image' && thumbnailUrl}
-
-
- {#if imageLoading}
-
-
-
- {/if}
- {#if imageError}
-
-
Bild konnte nicht geladen werden
-
- {:else}
-
(imageLoading = false)}
- onerror={() => {
- imageLoading = false;
- imageError = true;
- }}
- onclick={() => mediaUrl && window.open(mediaUrl, '_blank')}
- />
- {/if}
-
- {:else if message.type === 'm.video' && thumbnailUrl}
-
-
-
-
-
-
- {#if message.media?.duration}
-
- {Math.floor(message.media.duration / 60)}:{(message.media.duration % 60)
- .toString()
- .padStart(2, '0')}
-
- {/if}
-
- {:else if message.type === 'm.audio'}
-
-
-
- {#if mediaUrl}
-
(isPlaying = true)}
- onpause={() => (isPlaying = false)}
- ontimeupdate={handleAudioTimeUpdate}
- onloadedmetadata={handleAudioLoadedMetadata}
- onended={handleAudioEnded}
- >
- {/if}
-
-
-
- {#if isPlaying}
-
- {:else}
-
- {/if}
-
-
-
-
-
-
-
-
-
-
- {formatAudioTime(audioProgress)}
- {formatAudioTime(audioDuration || message.media?.duration || 0)}
-
-
-
- {:else if message.type === 'm.file'}
-
-
-
-
-
-
-
{message.media?.filename || message.body}
-
- {formatFileSize(message.media?.size)}
- {#if message.media?.mimetype}
- • {message.media.mimetype.split('/')[1]?.toUpperCase()}
- {/if}
-
-
-
-
- {:else if message.type === 'm.emote'}
-
- * {message.senderName}
- {message.body}
-
- {:else if message.type === 'm.notice'}
-
- {message.body}
-
- {:else}
-
-
- {@html formatMessageBody(message.body, message.isOwn)}
-
-
-
- {#if firstUrl()}
-
- ((e.currentTarget as HTMLImageElement).style.display = 'none')}
- />
-
- {getDomain(firstUrl() || '')}
-
-
- {/if}
- {/if}
-
- {#if message.edited}
-
(bearbeitet)
- {/if}
-
- {#if showEncryptionBadge}
-
- {/if}
-
-
-
- {#if message.reactions && message.reactions.length > 0}
-
- {#each message.reactions as reaction}
- handleReaction(reaction.key)}
- >
- {reaction.key}
- {reaction.count}
-
- {/each}
-
- {/if}
-
-
-
- {formattedTime()}
-
- {#if message.isOwn}
- {#if message.readBy && message.readBy.length > 0}
-
-
-
- {:else}
-
-
-
- {/if}
- {/if}
-
-
-
- {#if showActions && !message.redacted}
-
-
-
-
(showEmojiPicker = !showEmojiPicker)}
- >
-
-
- {#if showEmojiPicker}
-
-
{
- showEmojiPicker = false;
- showFullPicker = false;
- }}
- aria-label="Schließen"
- >
-
-
- {#if showFullPicker}
-
-
-
-
- {#each emojiCategories as category, i}
- (selectedCategory = i)}
- >
- {category.name}
-
- {/each}
-
-
-
- {#each emojiCategories[selectedCategory].emojis as emoji}
- handleReaction(emoji)}
- >
- {emoji}
-
- {/each}
-
-
- {:else}
-
-
- {#each quickEmojis as emoji}
- handleReaction(emoji)}
- >
- {emoji}
-
- {/each}
-
- (showFullPicker = true)}
- title="Mehr Emojis"
- >
-
-
-
- {/if}
-
- {/if}
-
-
onReply?.(message)}
- >
-
-
-
onForward?.(message)}
- >
-
-
- {#if message.isOwn && message.type === 'm.text'}
-
onEdit?.(message)}
- >
-
-
- {/if}
- {#if message.isOwn}
-
-
-
- {/if}
-
- {/if}
-
-
-
-
-{#if showMobileActions}
-
-
-
-
- {#each quickEmojis as emoji}
- {
- handleReaction(emoji);
- closeMobileActions();
- }}
- >
- {emoji}
-
- {/each}
-
-
-
-
-
-
-
{
- onReply?.(message);
- closeMobileActions();
- }}
- >
-
- Antworten
-
-
{
- onForward?.(message);
- closeMobileActions();
- }}
- >
-
- Weiterleiten
-
- {#if message.isOwn && message.type === 'm.text'}
-
{
- onEdit?.(message);
- closeMobileActions();
- }}
- >
-
- Bearbeiten
-
- {/if}
- {#if message.isOwn}
-
{
- handleDelete();
- closeMobileActions();
- }}
- >
-
- Löschen
-
- {/if}
-
-
-{/if}
diff --git a/apps/matrix/apps/web/src/lib/components/chat/MessageInput.svelte b/apps/matrix/apps/web/src/lib/components/chat/MessageInput.svelte
deleted file mode 100644
index 9d2da49cb..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/MessageInput.svelte
+++ /dev/null
@@ -1,846 +0,0 @@
-
-
-
-
- {#if replyTo || editMessage}
-
-
- {#if editMessage}
-
Nachricht bearbeiten
-
{editMessage.body}
- {:else if replyTo}
-
- Antwort auf {replyTo.senderName}
-
-
{replyTo.body}
- {/if}
-
-
{
- if (editMessage) {
- onCancelEdit?.();
- message = '';
- } else {
- onCancelReply?.();
- }
- }}
- >
-
-
-
- {/if}
-
-
- {#if uploading}
-
-
-
-
{uploadProgress}%
-
- {/if}
-
-
- {#if isRecording}
-
-
-
Aufnahme...
-
{formatDuration(recordingDuration)}
-
-
-
-
- {/if}
-
-
- {#if showMentionPicker && mentionResults.length > 0}
-
-
- Erwähne jemanden
-
- {#each mentionResults as member, i}
-
insertMention(member)}
- >
-
- {#if member.avatarUrl}
-
- {:else}
-
-
-
- {/if}
-
-
-
{member.displayName}
-
{member.userId}
-
-
- {/each}
-
- {/if}
-
-
-
-
-
-
(showAttachMenu = !showAttachMenu)}
- >
-
-
-
- {#if showAttachMenu}
-
-
(showAttachMenu = false)}
- aria-label="Menü schließen"
- >
-
-
- {
- openFilePicker();
- showAttachMenu = false;
- }}
- class="flex items-center gap-2 w-full px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors text-sm"
- >
-
- Bild oder Video
-
- {
- openFilePicker();
- showAttachMenu = false;
- }}
- class="flex items-center gap-2 w-full px-3 py-2 rounded-lg hover:bg-surface-hover transition-colors text-sm"
- >
-
- Datei
-
-
-
-
-
- {
- openFilePicker();
- showAttachMenu = false;
- }}
- class="flex items-center gap-3 w-full px-4 py-3.5 rounded-xl active:bg-surface-hover transition-colors"
- >
-
- Bild oder Video
-
- {
- openFilePicker();
- showAttachMenu = false;
- }}
- class="flex items-center gap-3 w-full px-4 py-3.5 rounded-xl active:bg-surface-hover transition-colors"
- >
-
- Datei
-
-
-
- {/if}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {#if showEmojiPicker}
-
-
(showEmojiPicker = false)}
- aria-label="Emoji-Picker schließen"
- >
-
-
- {#if recentEmojis.length > 0}
-
-
- Häufig benutzt
-
-
- {#each recentEmojis as emoji}
- insertEmoji(emoji)}
- >
- {emoji}
-
- {/each}
-
-
-
- {/if}
-
- {#each commonEmojis as emoji}
- insertEmoji(emoji)}
- >
- {emoji}
-
- {/each}
-
-
-
-
-
- {#if recentEmojis.length > 0}
-
-
- Häufig benutzt
-
-
- {#each recentEmojis as emoji}
- insertEmoji(emoji)}
- >
- {emoji}
-
- {/each}
-
-
-
- {/if}
-
- {#each commonEmojis as emoji}
- insertEmoji(emoji)}
- >
- {emoji}
-
- {/each}
-
-
-
- {/if}
-
-
-
- {#if isRecording}
-
-
-
- {:else if message.trim()}
-
-
-
- {:else}
-
-
-
- {/if}
-
-
-
-
- {#if editMessage}
- Enter = Speichern · Escape = Abbrechen
- {:else}
- Enter = Senden · Shift+Enter = Neue Zeile
- {/if}
-
-
diff --git a/apps/matrix/apps/web/src/lib/components/chat/RoomHeader.svelte b/apps/matrix/apps/web/src/lib/components/chat/RoomHeader.svelte
deleted file mode 100644
index 89b1f3a77..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/RoomHeader.svelte
+++ /dev/null
@@ -1,203 +0,0 @@
-
-
-{#if room}
-
-{/if}
diff --git a/apps/matrix/apps/web/src/lib/components/chat/RoomItem.svelte b/apps/matrix/apps/web/src/lib/components/chat/RoomItem.svelte
deleted file mode 100644
index ad20d11ac..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/RoomItem.svelte
+++ /dev/null
@@ -1,130 +0,0 @@
-
-
-
-
-
-
- {#if room.avatar}
-
- {:else}
-
{initials}
- {/if}
-
-
- {#if room.isDirect}
-
- {/if}
-
-
-
-
-
- {room.name}
- {#if room.isEncrypted}
-
- {/if}
- {#if !room.isDirect && room.memberCount > 2}
-
-
- {room.memberCount}
-
- {/if}
-
- {#if room.lastMessage}
-
- {#if room.lastMessageSender && !room.isDirect}
- {room.lastMessageSender}:
- {/if}
- {room.lastMessage}
-
- {/if}
-
-
-
-
- {#if timeAgo()}
- {timeAgo()}
- {/if}
- {#if room.unreadCount > 0}
-
- {room.unreadCount > 99 ? '99+' : room.unreadCount}
-
- {/if}
-
-
diff --git a/apps/matrix/apps/web/src/lib/components/chat/RoomList.svelte b/apps/matrix/apps/web/src/lib/components/chat/RoomList.svelte
deleted file mode 100644
index 2735c8a15..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/RoomList.svelte
+++ /dev/null
@@ -1,184 +0,0 @@
-
-
-
diff --git a/apps/matrix/apps/web/src/lib/components/chat/RoomSettingsPanel.svelte b/apps/matrix/apps/web/src/lib/components/chat/RoomSettingsPanel.svelte
deleted file mode 100644
index ebde6703d..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/RoomSettingsPanel.svelte
+++ /dev/null
@@ -1,325 +0,0 @@
-
-
-{#if open && room}
-
-
-
-
-
-
-
-
-
-
-
- {#if room.avatar}
-
- {:else}
-
- {room.name.charAt(0).toUpperCase()}
-
- {/if}
-
-
{room.name}
- {#if room.topic}
-
{room.topic}
- {/if}
-
- {room.memberCount} Mitglieder
- {#if room.isEncrypted}
- • Verschlüsselt
- {/if}
-
-
-
-
-
- (activeTab = 'members')}
- >
-
- Mitglieder
-
- (activeTab = 'widgets')}
- >
-
- Widgets
- {#if widgets.length > 0}
-
- {widgets.length}
-
- {/if}
-
- (activeTab = 'settings')}
- >
-
- Einstellungen
-
-
-
-
-
-
-{/if}
diff --git a/apps/matrix/apps/web/src/lib/components/chat/SearchDialog.svelte b/apps/matrix/apps/web/src/lib/components/chat/SearchDialog.svelte
deleted file mode 100644
index fb363d82a..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/SearchDialog.svelte
+++ /dev/null
@@ -1,187 +0,0 @@
-
-
-{#if open}
-
-
-
-
e.stopPropagation()}
- role="document"
- >
-
-
-
-
- {#if searching}
-
- {/if}
-
-
-
-
-
-
-
- (searchScope = 'room')}
- >
- Aktueller Raum
-
- (searchScope = 'all')}
- >
- Alle Räume
-
-
-
-
-
- {#if searching}
-
-
- Suche läuft...
-
- {:else if searchResults.length > 0}
-
- {#each searchResults as result}
-
handleSelectResult(result)}
- >
-
- {result.senderName}
- {#if searchScope === 'all'}
- in {result.roomName}
- {/if}
- {formatTime(result.timestamp)}
-
-
- {@html highlightMatch(result.body, query)}
-
-
- {/each}
-
- {:else if hasSearched && query.trim()}
-
-
-
Keine Nachrichten gefunden
-
Versuche es mit anderen Suchbegriffen
-
- {:else}
-
-
-
Gib einen Suchbegriff ein
-
Drücke Enter zum Suchen
-
- {/if}
-
-
-
-{/if}
diff --git a/apps/matrix/apps/web/src/lib/components/chat/Timeline.svelte b/apps/matrix/apps/web/src/lib/components/chat/Timeline.svelte
deleted file mode 100644
index 4ac34cb82..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/Timeline.svelte
+++ /dev/null
@@ -1,186 +0,0 @@
-
-
-
-
-
-
- {#if showScrollButton}
-
-
-
- {/if}
-
diff --git a/apps/matrix/apps/web/src/lib/components/chat/TypingIndicator.svelte b/apps/matrix/apps/web/src/lib/components/chat/TypingIndicator.svelte
deleted file mode 100644
index 3dcb7c9e1..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/TypingIndicator.svelte
+++ /dev/null
@@ -1,70 +0,0 @@
-
-
-{#if users.length > 0}
-
-
-
- {#each typingUsers().slice(0, 3) as user, i}
- {#if user.avatarUrl}
-
- {:else}
-
-
-
- {/if}
- {/each}
-
-
-
-
-
-
-
-
-
-
-
{text()}
-
-{/if}
diff --git a/apps/matrix/apps/web/src/lib/components/chat/index.ts b/apps/matrix/apps/web/src/lib/components/chat/index.ts
deleted file mode 100644
index 06e82b842..000000000
--- a/apps/matrix/apps/web/src/lib/components/chat/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export { default as RoomList } from './RoomList.svelte';
-export { default as RoomItem } from './RoomItem.svelte';
-export { default as RoomHeader } from './RoomHeader.svelte';
-export { default as Timeline } from './Timeline.svelte';
-export { default as Message } from './Message.svelte';
-export { default as MessageInput } from './MessageInput.svelte';
-export { default as TypingIndicator } from './TypingIndicator.svelte';
-export { default as CreateRoomDialog } from './CreateRoomDialog.svelte';
-export { default as RoomSettingsPanel } from './RoomSettingsPanel.svelte';
-export { default as DropZoneOverlay } from './DropZoneOverlay.svelte';
diff --git a/apps/matrix/apps/web/src/lib/components/crypto/EmojiVerification.svelte b/apps/matrix/apps/web/src/lib/components/crypto/EmojiVerification.svelte
deleted file mode 100644
index da44d6d9a..000000000
--- a/apps/matrix/apps/web/src/lib/components/crypto/EmojiVerification.svelte
+++ /dev/null
@@ -1,158 +0,0 @@
-
-
-
- {#if phase === 'waiting'}
-
-
-
Warte auf Antwort vom anderen Gerät...
-
- Öffne die Verifizierungsanfrage auf deinem anderen Gerät.
-
-
- {:else if phase === 'emojis'}
-
-
- Vergleiche die folgenden Emojis mit deinem anderen Gerät:
-
-
-
-
- {#each emojis as item}
-
- {item.emoji}
- {item.description}
-
- {/each}
-
-
-
- Stimmen die Emojis auf beiden Geräten überein?
-
-
-
-
-
-
- Nein, stimmen nicht
-
-
-
- Ja, stimmen überein
-
-
-
- {:else if phase === 'confirming'}
-
-
-
Bestätige Verifizierung...
-
- {:else if phase === 'done'}
-
-
-
-
-
Verifizierung erfolgreich!
-
- Das Gerät wurde erfolgreich verifiziert.
-
-
- {:else if phase === 'error'}
-
-
-
-
-
Verifizierung fehlgeschlagen
- {#if error}
-
- {error}
-
- {/if}
-
Schließen
-
- {/if}
-
diff --git a/apps/matrix/apps/web/src/lib/components/crypto/RecoveryKeyDialog.svelte b/apps/matrix/apps/web/src/lib/components/crypto/RecoveryKeyDialog.svelte
deleted file mode 100644
index eb696016f..000000000
--- a/apps/matrix/apps/web/src/lib/components/crypto/RecoveryKeyDialog.svelte
+++ /dev/null
@@ -1,374 +0,0 @@
-
-
-{#if open}
-
-
e.stopPropagation()}
- role="dialog"
- aria-modal="true"
- >
-
-
-
-
-
- {currentMode === 'setup' ? 'Verschlüsselung einrichten' : 'Schlüssel wiederherstellen'}
-
-
-
-
-
-
-
-
-
- {#if step === 'intro'}
-
- {#if currentMode === 'setup'}
-
- Richte einen Recovery Key ein, um deine verschlüsselten Nachrichten auf anderen
- Geräten wiederherzustellen.
-
-
-
-
-
- Ohne Recovery Key verlierst du den Zugriff auf deine verschlüsselten Nachrichten,
- wenn du dich abmeldest.
-
-
-
-
-
-
-
Mit Passphrase sichern (optional)
-
- Du kannst zusätzlich eine Passphrase festlegen, um den Recovery Key zu schützen.
-
-
-
- {:else}
-
- Gib deinen Recovery Key ein, um auf deine verschlüsselten Nachrichten zugreifen zu
- können.
-
-
-
- Recovery Key
-
-
- {/if}
-
- {#if error}
-
- {error}
-
- {/if}
-
- {:else if step === 'passphrase'}
-
-
- Gib eine sichere Passphrase ein, die du dir merken kannst.
-
-
-
- Passphrase
-
-
-
-
-
- Passphrase bestätigen
-
-
-
-
- {#if error}
-
- {error}
-
- {/if}
-
- {:else if step === 'show-key'}
-
-
-
-
- Speichere diesen Schlüssel an einem sicheren Ort. Du benötigst ihn, um deine
- Nachrichten wiederherzustellen.
-
-
-
-
-
Dein Recovery Key:
-
- {formatRecoveryKey(recoveryKey)}
-
-
-
-
-
- {#if keyCopied}
-
- Kopiert!
- {:else}
-
- Kopieren
- {/if}
-
-
-
- Herunterladen
-
-
-
- {:else if step === 'done'}
-
-
-
-
-
- {currentMode === 'setup'
- ? 'Verschlüsselung eingerichtet!'
- : 'Schlüssel wiederhergestellt!'}
-
-
- {currentMode === 'setup'
- ? 'Deine Nachrichten sind jetzt sicher verschlüsselt.'
- : 'Du kannst jetzt auf deine verschlüsselten Nachrichten zugreifen.'}
-
-
- {/if}
-
-
-
-
- {#if step === 'intro'}
- Abbrechen
- {#if currentMode === 'setup'}
-
- {#if loading}
-
- {/if}
- Weiter
-
- {:else}
-
- {#if loading}
-
- {/if}
- Wiederherstellen
-
- {/if}
- {:else if step === 'passphrase'}
- (step = 'intro')}>Zurück
-
- {#if loading}
-
- {/if}
- Schlüssel erstellen
-
- {:else if step === 'show-key'}
-
- Ich habe den Schlüssel gespeichert
-
- {:else if step === 'done'}
- Fertig
- {/if}
-
-
-
-{/if}
diff --git a/apps/matrix/apps/web/src/lib/components/crypto/VerificationDialog.svelte b/apps/matrix/apps/web/src/lib/components/crypto/VerificationDialog.svelte
deleted file mode 100644
index 17b904abe..000000000
--- a/apps/matrix/apps/web/src/lib/components/crypto/VerificationDialog.svelte
+++ /dev/null
@@ -1,244 +0,0 @@
-
-
-{#if open}
-
-
e.stopPropagation()}
- role="dialog"
- aria-modal="true"
- >
-
-
-
-
-
Geräte-Verifizierung
-
-
-
-
-
-
-
-
- {#if verificationStarted && activeVerification}
-
-
- {:else}
-
-
-
- Verifiziere deine Geräte um sicherzustellen, dass du der einzige bist, der auf deine
- verschlüsselten Nachrichten zugreifen kann.
-
-
- {#if error}
-
- {error}
-
- {/if}
-
- {#if loading}
-
-
-
- {:else if devices.length === 0}
-
-
Keine Geräte gefunden
-
- {:else}
-
- {#each devices as device}
- {@const DeviceIcon = getDeviceIcon(device)}
-
-
- {#if device.verified}
-
-
-
-
- {:else if device.blocked}
-
-
-
-
- {:else}
-
-
-
-
- {/if}
-
-
-
-
-
- {formatDeviceName(device.displayName, device.deviceId)}
-
- {#if device.isCurrentDevice}
- Dieses Gerät
- {/if}
-
-
- {device.deviceId}
-
-
- {#if device.verified}
- Verifiziert
- {:else if device.blocked}
- Blockiert
- {:else}
- Nicht verifiziert
- {/if}
-
-
-
- {#if !device.isCurrentDevice && !device.verified}
-
startVerification(device)}>
- Verifizieren
-
- {/if}
-
- {/each}
-
- {/if}
-
-
-
-
- {/if}
-
-
-
-
-
- {verificationStarted ? 'Abbrechen' : 'Schließen'}
-
-
-
-
-{/if}
diff --git a/apps/matrix/apps/web/src/lib/components/crypto/index.ts b/apps/matrix/apps/web/src/lib/components/crypto/index.ts
deleted file mode 100644
index f81f9effb..000000000
--- a/apps/matrix/apps/web/src/lib/components/crypto/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// Crypto component exports
-export { default as VerificationDialog } from './VerificationDialog.svelte';
-export { default as EmojiVerification } from './EmojiVerification.svelte';
-export { default as RecoveryKeyDialog } from './RecoveryKeyDialog.svelte';
diff --git a/apps/matrix/apps/web/src/lib/content/help/index.test.ts b/apps/matrix/apps/web/src/lib/content/help/index.test.ts
deleted file mode 100644
index dd2e0830d..000000000
--- a/apps/matrix/apps/web/src/lib/content/help/index.test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { getManalinkHelpContent } from './index';
-
-describe('Manalink Help Content', () => {
- it('returns valid German content', () => {
- const content = getManalinkHelpContent('de');
-
- expect(content.faq.length).toBeGreaterThan(0);
- content.faq.forEach((faq) => {
- expect(faq.id).toBeTruthy();
- expect(faq.question).toBeTruthy();
- expect(faq.answer).toBeTruthy();
- });
-
- expect(content.features).toBeDefined();
- expect(content.contact).toBeDefined();
- expect(content.contact.supportEmail).toBe('support@mana.how');
- });
-
- it('returns valid English content', () => {
- const content = getManalinkHelpContent('en');
-
- expect(content.faq.length).toBeGreaterThan(0);
- content.faq.forEach((faq) => {
- expect(faq.id).toBeTruthy();
- expect(faq.question).toBeTruthy();
- expect(faq.answer).toBeTruthy();
- });
-
- expect(content.features).toBeDefined();
- expect(content.contact).toBeDefined();
- });
-
- it('returns same number of FAQ items for both languages', () => {
- const de = getManalinkHelpContent('de');
- const en = getManalinkHelpContent('en');
-
- expect(de.faq.length).toBe(en.faq.length);
- expect(de.features.length).toBe(en.features.length);
- });
-
- it('has unique FAQ IDs', () => {
- const content = getManalinkHelpContent('de');
- const ids = content.faq.map((f) => f.id);
- expect(new Set(ids).size).toBe(ids.length);
- });
-});
diff --git a/apps/matrix/apps/web/src/lib/content/help/index.ts b/apps/matrix/apps/web/src/lib/content/help/index.ts
deleted file mode 100644
index e7a3d2e0d..000000000
--- a/apps/matrix/apps/web/src/lib/content/help/index.ts
+++ /dev/null
@@ -1,276 +0,0 @@
-/**
- * Help content for Manalink (Matrix) app
- */
-
-import type { HelpContent } from '@mana/help';
-import { getPrivacyFAQs } from '@mana/help';
-
-export function getManalinkHelpContent(locale: string): HelpContent {
- const isDE = locale === 'de';
-
- return {
- faq: [
- {
- id: 'faq-what-is-matrix',
- question: isDE ? 'Was ist Matrix und Manalink?' : 'What is Matrix and Manalink?',
- answer: isDE
- ? 'Manalink ist ein sicherer Messenger auf Basis des Matrix-Protokolls — einem dezentralen, offenen Standard für Kommunikation:
Ende-zu-Ende-verschlüsselt Dezentral — kein einzelner Server kontrolliert deine Daten Kompatibel mit anderen Matrix-Clients (Element, FluffyChat, etc.) Der Standard-Homeserver ist matrix.mana.how '
- : 'Manalink is a secure messenger based on the Matrix protocol — a decentralized, open standard for communication:
End-to-end encrypted Decentralized — no single server controls your data Compatible with other Matrix clients (Element, FluffyChat, etc.) The default homeserver is matrix.mana.how ',
- category: 'features',
- order: 1,
- language: isDE ? 'de' : 'en',
- tags: isDE ? ['matrix', 'protokoll', 'dezentral'] : ['matrix', 'protocol', 'decentralized'],
- },
- {
- id: 'faq-login',
- question: isDE ? 'Wie melde ich mich an?' : 'How do I log in?',
- answer: isDE
- ? 'Du kannst dich auf zwei Wegen anmelden:
Matrix-Konto — Benutzername und Passwort eines Matrix-HomeserversSSO über Mana Core — Anmeldung mit deinem Mana-KontoDer Standard-Homeserver ist matrix.mana.how, aber du kannst jeden beliebigen Matrix-Homeserver verwenden.
'
- : 'You can log in two ways:
Matrix account — Username and password from a Matrix homeserverSSO via Mana Core — Login with your Mana accountThe default homeserver is matrix.mana.how, but you can use any Matrix homeserver.
',
- category: 'getting-started',
- order: 2,
- language: isDE ? 'de' : 'en',
- tags: isDE ? ['anmeldung', 'login', 'sso'] : ['login', 'auth', 'sso'],
- },
- {
- id: 'faq-rooms',
- question: isDE ? 'Wie funktionieren Räume?' : 'How do rooms work?',
- answer: isDE
- ? 'Räume sind Chatgruppen in Matrix:
Direktnachrichten — 1:1 GesprächeGruppenräume — Mehrere TeilnehmerDu kannst Räume erstellen, beitreten und verwalten Räume zeigen ungelesene Nachrichten und Highlight-Zähler an '
- : 'Rooms are chat groups in Matrix:
Direct messages — 1:1 conversationsGroup rooms — Multiple participantsYou can create, join, and manage rooms Rooms show unread message and highlight counters ',
- category: 'features',
- order: 3,
- language: isDE ? 'de' : 'en',
- tags: isDE ? ['räume', 'chat', 'gruppen'] : ['rooms', 'chat', 'groups'],
- },
- {
- id: 'faq-messaging',
- question: isDE
- ? 'Welche Nachrichtenfunktionen gibt es?'
- : 'What messaging features are available?',
- answer: isDE
- ? 'Manalink bietet umfangreiche Nachrichtenfunktionen:
Textnachrichten senden und empfangenTipp-Indikatoren — Sieh, wenn jemand gerade tipptLesebestätigungen — Wisse, wann deine Nachricht gelesen wurdeNachrichtensuche — Durchsuche den ChatverlaufPaginierung — Lade ältere Nachrichten nach '
- : 'Manalink offers comprehensive messaging features:
Text messages — send and receiveTyping indicators — See when someone is typingRead receipts — Know when your message was readMessage search — Search through chat historyPagination — Load older messages ',
- category: 'features',
- order: 4,
- language: isDE ? 'de' : 'en',
- tags: isDE
- ? ['nachrichten', 'tippen', 'lesen', 'suche']
- : ['messages', 'typing', 'read', 'search'],
- },
- {
- id: 'faq-encryption',
- question: isDE ? 'Sind meine Nachrichten verschlüsselt?' : 'Are my messages encrypted?',
- answer: isDE
- ? 'Ja, Manalink unterstützt Ende-zu-Ende-Verschlüsselung (E2EE) über das Matrix-Protokoll:
Verschlüsselte Räume sind nur für die Teilnehmer lesbar Nicht einmal der Server kann die Nachrichten lesen Die Verschlüsselung verwendet bewährte kryptographische Verfahren (Olm/Megolm) '
- : 'Yes, Manalink supports end-to-end encryption (E2EE) via the Matrix protocol:
Encrypted rooms are only readable by participants Not even the server can read the messages Encryption uses proven cryptographic methods (Olm/Megolm) ',
- category: 'privacy',
- order: 5,
- language: isDE ? 'de' : 'en',
- tags: isDE ? ['verschlüsselung', 'e2ee', 'sicherheit'] : ['encryption', 'e2ee', 'security'],
- },
- {
- id: 'faq-room-settings',
- question: isDE ? 'Wie verwalte ich Raumeinstellungen?' : 'How do I manage room settings?',
- answer: isDE
- ? 'Jeder Raum hat eigene Einstellungen:
Name und Thema des Raums ändernMitglieder einladen und verwaltenBenachrichtigungen pro Raum konfigurieren '
- : 'Each room has its own settings:
Change the name and topic of the room Invite and manage membersConfigure notifications per room ',
- category: 'features',
- order: 6,
- language: isDE ? 'de' : 'en',
- tags: isDE ? ['einstellungen', 'raum', 'mitglieder'] : ['settings', 'room', 'members'],
- },
- {
- id: 'faq-bots',
- question: isDE ? 'Was sind Bots?' : 'What are bots?',
- answer: isDE
- ? 'Unter Bots findest du automatisierte Assistenten, die in Räumen helfen können. Bots können Aufgaben automatisieren, Informationen bereitstellen und den Chat bereichern.
'
- : 'Under Bots you can find automated assistants that can help in rooms. Bots can automate tasks, provide information, and enrich the chat experience.
',
- category: 'features',
- order: 7,
- language: isDE ? 'de' : 'en',
- tags: isDE
- ? ['bots', 'automatisierung', 'assistenten']
- : ['bots', 'automation', 'assistants'],
- },
- {
- id: 'faq-pwa',
- question: isDE
- ? 'Kann ich Manalink auf dem Handy nutzen?'
- : 'Can I use Manalink on my phone?',
- answer: isDE
- ? 'Ja! Manalink ist eine Progressive Web App (PWA) :
Öffne die App im Browser und tippe auf "Zum Startbildschirm hinzufügen" Funktioniert auch offline dank Service Worker Caching Push-Benachrichtigungen für neue Nachrichten Vollbild-App-Erfahrung ohne Browser-Leiste '
- : 'Yes! Manalink is a Progressive Web App (PWA) :
Open the app in your browser and tap "Add to Home Screen" Works offline thanks to service worker caching Push notifications for new messages Fullscreen app experience without browser bar ',
- category: 'getting-started',
- order: 8,
- language: isDE ? 'de' : 'en',
- tags: isDE ? ['pwa', 'mobil', 'installieren'] : ['pwa', 'mobile', 'install'],
- },
- {
- id: 'faq-feedback',
- question: isDE ? 'Wie kann ich Feedback geben?' : 'How can I give feedback?',
- answer: isDE
- ? 'Dein Feedback hilft uns, Manalink zu verbessern:
Nutze die Feedback-Seite im Menü, um Verbesserungsvorschläge, Fehlermeldungen oder Feature-Wünsche einzureichen Wir lesen jedes Feedback und arbeiten kontinuierlich an Verbesserungen '
- : 'Your feedback helps us improve Manalink:
Use the Feedback page in the menu to submit improvement suggestions, bug reports, or feature requests We read every piece of feedback and continuously work on improvements ',
- category: 'general',
- order: 9,
- language: isDE ? 'de' : 'en',
- tags: isDE
- ? ['feedback', 'verbesserung', 'kontakt']
- : ['feedback', 'improvement', 'contact'],
- },
- ...getPrivacyFAQs(locale, {
- dataTypeDE: 'Nachrichten',
- dataTypeEN: 'messages',
- extraBulletsDE: [
- 'Ende-zu-Ende-Verschlüsselung : Verschlüsselte Räume sind nur für Teilnehmer lesbar — nicht einmal der Server kann mitlesen',
- ],
- extraBulletsEN: [
- 'End-to-end encryption : Encrypted rooms are only readable by participants — not even the server can read them',
- ],
- }),
- ],
- features: [
- {
- id: 'feature-messaging',
- title: isDE ? 'Sichere Nachrichten' : 'Secure Messaging',
- description: isDE
- ? 'Ende-zu-Ende-verschlüsselte Nachrichten über das dezentrale Matrix-Protokoll'
- : 'End-to-end encrypted messaging via the decentralized Matrix protocol',
- icon: '🔒',
- category: 'core',
- highlights: isDE
- ? [
- 'E2E-Verschlüsselung',
- 'Direktnachrichten & Gruppen',
- 'Lesebestätigungen',
- 'Tipp-Indikatoren',
- ]
- : ['E2E encryption', 'Direct messages & groups', 'Read receipts', 'Typing indicators'],
- content: '',
- order: 1,
- language: isDE ? 'de' : 'en',
- },
- {
- id: 'feature-rooms',
- title: isDE ? 'Räume' : 'Rooms',
- description: isDE
- ? 'Erstelle und verwalte Räume für Direktnachrichten und Gruppen'
- : 'Create and manage rooms for direct messages and groups',
- icon: '💬',
- category: 'core',
- highlights: isDE
- ? ['Raum erstellen', 'Raumeinstellungen', 'Mitgliederverwaltung', 'Ungelesen-Zähler']
- : ['Create rooms', 'Room settings', 'Member management', 'Unread counter'],
- content: '',
- order: 2,
- language: isDE ? 'de' : 'en',
- },
- {
- id: 'feature-search',
- title: isDE ? 'Nachrichtensuche' : 'Message Search',
- description: isDE
- ? 'Durchsuche den Chatverlauf nach Nachrichten und Inhalten'
- : 'Search through chat history for messages and content',
- icon: '🔍',
- category: 'core',
- highlights: isDE
- ? ['Volltextsuche', 'Raum-übergreifend', 'Schnelle Ergebnisse']
- : ['Full-text search', 'Cross-room', 'Quick results'],
- content: '',
- order: 3,
- language: isDE ? 'de' : 'en',
- },
- {
- id: 'feature-sso',
- title: isDE ? 'SSO-Anmeldung' : 'SSO Login',
- description: isDE
- ? 'Melde dich mit deinem Mana-Konto an — kein separates Matrix-Passwort nötig'
- : 'Sign in with your Mana account — no separate Matrix password needed',
- icon: '🔐',
- category: 'core',
- highlights: isDE
- ? ['Mana SSO', 'Ein-Klick-Login', 'Sicher', 'Passwort-Login']
- : ['Mana SSO', 'One-click login', 'Secure', 'Password login'],
- content: '',
- order: 4,
- language: isDE ? 'de' : 'en',
- },
- {
- id: 'feature-bots',
- title: 'Bots',
- description: isDE
- ? 'Automatisierte Assistenten für Aufgaben und Informationen in Räumen'
- : 'Automated assistants for tasks and information in rooms',
- icon: '🤖',
- category: 'advanced',
- highlights: isDE
- ? ['Chat-Assistenten', 'Automatisierung', 'Informationsdienste']
- : ['Chat assistants', 'Automation', 'Information services'],
- content: '',
- order: 5,
- language: isDE ? 'de' : 'en',
- },
- {
- id: 'feature-pwa',
- title: isDE ? 'Progressive Web App' : 'Progressive Web App',
- description: isDE
- ? 'Installierbar auf jedem Gerät — offline-fähig und mit Push-Benachrichtigungen'
- : 'Installable on any device — works offline and supports push notifications',
- icon: '📱',
- category: 'core',
- highlights: isDE
- ? ['Installierbar', 'Offline-fähig', 'Push-Benachrichtigungen', 'Vollbild']
- : ['Installable', 'Works offline', 'Push notifications', 'Fullscreen'],
- content: '',
- order: 6,
- language: isDE ? 'de' : 'en',
- },
- {
- id: 'feature-federation',
- title: isDE ? 'Föderation' : 'Federation',
- description: isDE
- ? 'Kommuniziere mit Nutzern auf anderen Matrix-Homeservern'
- : 'Communicate with users on other Matrix homeservers',
- icon: '🌐',
- category: 'advanced',
- highlights: isDE
- ? ['Server-übergreifend', 'Offenes Protokoll', 'Element-kompatibel']
- : ['Cross-server', 'Open protocol', 'Element-compatible'],
- content: '',
- order: 7,
- language: isDE ? 'de' : 'en',
- },
- {
- id: 'feature-settings',
- title: isDE ? 'Einstellungen' : 'Settings',
- description: isDE
- ? 'Passe Manalink an — Themes, Benachrichtigungen und Kontoeinstellungen'
- : 'Customize Manalink — themes, notifications, and account settings',
- icon: '⚙️',
- category: 'core',
- highlights: isDE
- ? ['Hell/Dunkel-Modus', 'Benachrichtigungen', 'Kontoeinstellungen']
- : ['Light/Dark mode', 'Notifications', 'Account settings'],
- content: '',
- order: 8,
- language: isDE ? 'de' : 'en',
- },
- ],
- shortcuts: [],
- gettingStarted: [],
- changelog: [],
- contact: {
- id: 'contact-support',
- title: isDE ? 'Support kontaktieren' : 'Contact Support',
- content: isDE
- ? 'Unser Support-Team hilft dir bei allen Fragen rund um Manalink. Nutze auch die Feedback-Funktion im Menü, um uns direkt Verbesserungsvorschläge zu schicken.
'
- : 'Our support team is here to help you with any questions about Manalink. You can also use the feedback feature in the menu to send us improvement suggestions directly.
',
- language: isDE ? 'de' : 'en',
- order: 1,
- supportEmail: 'support@mana.how',
- documentationUrl: 'https://mana.how/docs',
- responseTime: isDE ? 'Normalerweise innerhalb von 24 Stunden' : 'Usually within 24 hours',
- },
- };
-}
diff --git a/apps/matrix/apps/web/src/lib/data/bots.ts b/apps/matrix/apps/web/src/lib/data/bots.ts
deleted file mode 100644
index c4fee52ce..000000000
--- a/apps/matrix/apps/web/src/lib/data/bots.ts
+++ /dev/null
@@ -1,424 +0,0 @@
-export interface BotCommand {
- command: string;
- aliases?: string[];
- description: string;
- example?: string;
-}
-
-export interface BotInfo {
- id: string;
- name: string;
- matrixUserId: string;
- description: string;
- longDescription?: string;
- icon: string;
- color: string;
- commands: BotCommand[];
- category: 'productivity' | 'ai' | 'media' | 'lifestyle' | 'tools';
- requiresAuth: boolean;
- isGateway?: boolean;
-}
-
-export const BOTS: BotInfo[] = [
- // AI & Chat
- {
- id: 'mana-bot',
- name: 'Mana Bot',
- matrixUserId: '@mana-bot:matrix.mana.how',
- description: 'All-in-One Gateway zu allen Mana-Services',
- longDescription:
- 'Der zentrale Hub, der alle anderen Bots vereint. Starte Chats, erstelle Bilder, verwalte Aufgaben und mehr - alles in einem Bot.',
- icon: 'Sparkle',
- color: 'from-violet-500 to-purple-600',
- category: 'ai',
- requiresAuth: true,
- isGateway: true,
- commands: [
- { command: '!help', aliases: ['!hilfe'], description: 'Zeigt alle verfügbaren Befehle' },
- { command: '!chat', description: 'Startet einen KI-Chat' },
- { command: '!image', aliases: ['!bild'], description: 'Generiert ein Bild mit KI' },
- { command: '!todo', description: 'Verwaltet Aufgaben' },
- { command: '!calendar', aliases: ['!cal'], description: 'Kalender-Operationen' },
- ],
- },
- {
- id: 'chat-bot',
- name: 'Chat Bot',
- matrixUserId: '@chat-bot:matrix.mana.how',
- description: 'KI-Assistent powered by Claude und anderen LLMs',
- longDescription:
- 'Dein persönlicher KI-Assistent für Fragen, Texterstellung, Zusammenfassungen und kreative Aufgaben. Nutzt verschiedene KI-Modelle.',
- icon: 'ChatCircle',
- color: 'from-blue-500 to-cyan-500',
- category: 'ai',
- requiresAuth: true,
- commands: [
- {
- command: '!chat',
- description: 'Startet eine Konversation',
- example: '!chat Erkläre mir Quantencomputing',
- },
- { command: '!model', description: 'Wechselt das KI-Modell', example: '!model gpt-4' },
- { command: '!clear', description: 'Löscht den Chat-Verlauf' },
- { command: '!system', description: 'Setzt einen System-Prompt' },
- ],
- },
- {
- id: 'ollama-bot',
- name: 'Ollama Bot',
- matrixUserId: '@ollama-bot:matrix.mana.how',
- description: 'Lokale KI-Modelle via Ollama',
- longDescription:
- 'Chatte mit lokal gehosteten Open-Source KI-Modellen. Vollständig privat, keine Daten verlassen den Server.',
- icon: 'Robot',
- color: 'from-emerald-500 to-teal-600',
- category: 'ai',
- requiresAuth: false,
- commands: [
- { command: '!ollama', description: 'Startet einen Chat', example: '!ollama Hallo!' },
- { command: '!models', description: 'Listet verfügbare Modelle' },
- { command: '!switch', description: 'Wechselt das Modell', example: '!switch llama3' },
- ],
- },
-
- // Productivity
- {
- id: 'todo-bot',
- name: 'Todo Bot',
- matrixUserId: '@todo-bot:matrix.mana.how',
- description: 'Aufgabenverwaltung und To-Do Listen',
- longDescription:
- 'Verwalte deine Aufgaben direkt im Chat. Erstelle, bearbeite und erledige Todos mit einfachen Befehlen.',
- icon: 'CheckSquare',
- color: 'from-green-500 to-emerald-600',
- category: 'productivity',
- requiresAuth: true,
- commands: [
- {
- command: '!add',
- description: 'Fügt eine neue Aufgabe hinzu',
- example: '!add Einkaufen gehen',
- },
- { command: '!list', aliases: ['!todos'], description: 'Zeigt alle Aufgaben' },
- { command: '!done', description: 'Markiert Aufgabe als erledigt', example: '!done 1' },
- {
- command: '!delete',
- aliases: ['!del'],
- description: 'Löscht eine Aufgabe',
- example: '!delete 1',
- },
- { command: '!clear', description: 'Löscht alle erledigten Aufgaben' },
- ],
- },
- {
- id: 'calendar-bot',
- name: 'Calendar Bot',
- matrixUserId: '@calendar-bot:matrix.mana.how',
- description: 'Terminverwaltung und Erinnerungen',
- longDescription:
- 'Plane Termine, setze Erinnerungen und behalte deinen Zeitplan im Blick - alles per Chat-Befehl.',
- icon: 'CalendarBlank',
- color: 'from-orange-500 to-amber-600',
- category: 'productivity',
- requiresAuth: true,
- commands: [
- {
- command: '!event',
- description: 'Erstellt einen Termin',
- example: '!event Meeting morgen 14:00',
- },
- { command: '!today', description: 'Zeigt heutige Termine' },
- { command: '!week', description: 'Zeigt Termine dieser Woche' },
- { command: '!remind', description: 'Setzt eine Erinnerung', example: '!remind 30min Anruf' },
- ],
- },
- {
- id: 'contacts-bot',
- name: 'Contacts Bot',
- matrixUserId: '@contacts-bot:matrix.mana.how',
- description: 'Kontaktverwaltung und Adressbuch',
- longDescription:
- 'Speichere und finde Kontaktinformationen schnell. Durchsuche dein Adressbuch direkt im Chat.',
- icon: 'AddressBook',
- color: 'from-indigo-500 to-blue-600',
- category: 'productivity',
- requiresAuth: true,
- commands: [
- { command: '!find', description: 'Sucht nach Kontakten', example: '!find Max Mustermann' },
- { command: '!add', description: 'Fügt einen Kontakt hinzu' },
- { command: '!all', description: 'Listet alle Kontakte' },
- ],
- },
- {
- id: 'project-doc-bot',
- name: 'Project Doc Bot',
- matrixUserId: '@project-doc-bot:matrix.mana.how',
- description: 'Projektdokumentation und Wissensbasis',
- longDescription:
- 'Durchsuche Projektdokumentationen, finde Code-Beispiele und erhalte Antworten basierend auf deiner Wissensbasis.',
- icon: 'Folders',
- color: 'from-purple-500 to-violet-600',
- category: 'productivity',
- requiresAuth: true,
- commands: [
- {
- command: '!search',
- description: 'Durchsucht die Dokumentation',
- example: '!search API authentication',
- },
- { command: '!projects', description: 'Listet verfügbare Projekte' },
- { command: '!select', description: 'Wählt ein Projekt aus', example: '!select mana' },
- ],
- },
-
- // Media
- {
- id: 'picture-bot',
- name: 'Picture Bot',
- matrixUserId: '@picture-bot:matrix.mana.how',
- description: 'KI-Bildgenerierung mit verschiedenen Modellen',
- longDescription:
- 'Erstelle beeindruckende Bilder mit KI. Unterstützt verschiedene Stile und Modelle wie Stable Diffusion und DALL-E.',
- icon: 'Image',
- color: 'from-pink-500 to-rose-600',
- category: 'media',
- requiresAuth: true,
- commands: [
- {
- command: '!image',
- aliases: ['!bild'],
- description: 'Generiert ein Bild',
- example: '!image Ein Sonnenuntergang am Meer',
- },
- { command: '!style', description: 'Wählt einen Stil', example: '!style anime' },
- { command: '!size', description: 'Setzt die Bildgröße', example: '!size 1024x1024' },
- ],
- },
- {
- id: 'tts-bot',
- name: 'TTS Bot',
- matrixUserId: '@tts-bot:matrix.mana.how',
- description: 'Text-to-Speech Sprachausgabe',
- longDescription:
- 'Wandle Text in natürlich klingende Sprache um. Unterstützt verschiedene Stimmen und Sprachen.',
- icon: 'SpeakerHigh',
- color: 'from-cyan-500 to-sky-600',
- category: 'media',
- requiresAuth: true,
- commands: [
- {
- command: '!speak',
- aliases: ['!say'],
- description: 'Spricht Text vor',
- example: '!speak Hallo Welt',
- },
- { command: '!voice', description: 'Wählt eine Stimme', example: '!voice nova' },
- { command: '!lang', description: 'Setzt die Sprache', example: '!lang de' },
- ],
- },
- {
- id: 'storage-bot',
- name: 'Storage Bot',
- matrixUserId: '@storage-bot:matrix.mana.how',
- description: 'Cloud-Speicher und Dateiverwaltung',
- longDescription:
- 'Verwalte deine Dateien in der Cloud. Lade hoch, teile und organisiere direkt aus dem Chat.',
- icon: 'CloudArrowUp',
- color: 'from-slate-500 to-zinc-600',
- category: 'media',
- requiresAuth: true,
- commands: [
- { command: '!upload', description: 'Lädt eine Datei hoch' },
- { command: '!files', description: 'Listet deine Dateien' },
- { command: '!share', description: 'Teilt eine Datei', example: '!share document.pdf' },
- { command: '!delete', description: 'Löscht eine Datei' },
- ],
- },
-
- // Lifestyle
- {
- id: 'nutriphi-bot',
- name: 'NutriPhi Bot',
- matrixUserId: '@nutriphi-bot:matrix.mana.how',
- description: 'Ernährungstracking und Mahlzeiten-Analyse',
- longDescription:
- 'Tracke deine Ernährung, analysiere Mahlzeiten per Foto und erhalte Nährwertinformationen.',
- icon: 'ForkKnife',
- color: 'from-lime-500 to-green-600',
- category: 'lifestyle',
- requiresAuth: true,
- commands: [
- {
- command: '!log',
- description: 'Protokolliert eine Mahlzeit',
- example: '!log 2 Äpfel, 1 Sandwich',
- },
- { command: '!today', description: 'Zeigt heutige Kalorien' },
- { command: '!analyze', description: 'Analysiert ein Essens-Foto' },
- ],
- },
- {
- id: 'planta-bot',
- name: 'Planta Bot',
- matrixUserId: '@planta-bot:matrix.mana.how',
- description: 'Pflanzenidentifikation und Pflege-Tipps',
- longDescription:
- 'Identifiziere Pflanzen per Foto und erhalte Pflege-Anleitungen. Perfekt für Hobbygärtner.',
- icon: 'Plant',
- color: 'from-green-600 to-emerald-700',
- category: 'lifestyle',
- requiresAuth: true,
- commands: [
- { command: '!identify', description: 'Identifiziert eine Pflanze per Foto' },
- { command: '!care', description: 'Zeigt Pflegetipps', example: '!care Monstera' },
- { command: '!water', description: 'Erinnerung zum Gießen setzen' },
- ],
- },
- {
- id: 'zitare-bot',
- name: 'Zitare Bot',
- matrixUserId: '@zitare-bot:matrix.mana.how',
- description: 'Tägliche Inspiration und Weisheiten',
- longDescription:
- 'Erhalte inspirierende Zitate und Weisheiten. Perfekt für den täglichen Motivationsschub.',
- icon: 'Quotes',
- color: 'from-amber-500 to-orange-600',
- category: 'lifestyle',
- requiresAuth: true,
- commands: [
- { command: '!quote', aliases: ['!zitat'], description: 'Zeigt ein zufälliges Zitat' },
- { command: '!daily', description: 'Aktiviert tägliche Zitate' },
- { command: '!topic', description: 'Zitat zu einem Thema', example: '!topic Erfolg' },
- ],
- },
- {
- id: 'skilltree-bot',
- name: 'SkillTree Bot',
- matrixUserId: '@skilltree-bot:matrix.mana.how',
- description: 'Fähigkeiten-Tracking und Lernfortschritt',
- longDescription:
- 'Verfolge deinen Lernfortschritt, setze Ziele und entwickle deine Fähigkeiten systematisch weiter.',
- icon: 'TreeStructure',
- color: 'from-yellow-500 to-amber-600',
- category: 'lifestyle',
- requiresAuth: true,
- commands: [
- { command: '!skills', description: 'Zeigt deine Fähigkeiten' },
- { command: '!add', description: 'Fügt eine Fähigkeit hinzu', example: '!add TypeScript' },
- {
- command: '!progress',
- description: 'Protokolliert Fortschritt',
- example: '!progress TypeScript +2h',
- },
- ],
- },
-
- // Tools
- {
- id: 'clock-bot',
- name: 'Clock Bot',
- matrixUserId: '@clock-bot:matrix.mana.how',
- description: 'Zeiterfassung und Zeitzonen',
- longDescription:
- 'Tracke Arbeitszeiten, konvertiere Zeitzonen und setze Timer für Fokus-Sessions.',
- icon: 'Clock',
- color: 'from-blue-600 to-indigo-700',
- category: 'tools',
- requiresAuth: true,
- commands: [
- { command: '!start', description: 'Startet Zeiterfassung' },
- { command: '!stop', description: 'Stoppt Zeiterfassung' },
- { command: '!time', description: 'Zeigt aktuelle Zeit in Zonen', example: '!time NYC' },
- { command: '!timer', description: 'Setzt einen Timer', example: '!timer 25m Pomodoro' },
- ],
- },
- {
- id: 'stats-bot',
- name: 'Stats Bot',
- matrixUserId: '@stats-bot:matrix.mana.how',
- description: 'Nutzungsstatistiken und Analytics',
- longDescription:
- 'Erhalte Einblicke in deine Nutzung der Mana-Services. Statistiken, Trends und Zusammenfassungen.',
- icon: 'ChartBar',
- color: 'from-fuchsia-500 to-pink-600',
- category: 'tools',
- requiresAuth: true,
- commands: [
- { command: '!stats', description: 'Zeigt Nutzungsstatistiken' },
- { command: '!usage', description: 'Credits-Verbrauch diese Woche' },
- { command: '!top', description: 'Meistgenutzte Features' },
- ],
- },
- {
- id: 'questions-bot',
- name: 'Questions Bot',
- matrixUserId: '@questions-bot:matrix.mana.how',
- description: 'Websuche und Fakten-Recherche',
- longDescription:
- 'Durchsuche das Web und erhalte fundierte Antworten mit Quellenangaben. Perfekt für Recherchen.',
- icon: 'MagnifyingGlass',
- color: 'from-teal-500 to-cyan-600',
- category: 'tools',
- requiresAuth: true,
- commands: [
- {
- command: '!search',
- aliases: ['!q'],
- description: 'Sucht im Web',
- example: '!search Wetter Berlin',
- },
- { command: '!wiki', description: 'Sucht auf Wikipedia', example: '!wiki Photosynthese' },
- { command: '!news', description: 'Aktuelle Nachrichten' },
- ],
- },
- {
- id: 'cards-bot',
- name: 'Cards Bot',
- matrixUserId: '@cards-bot:matrix.mana.how',
- description: 'Lernkarten und Spaced Repetition',
- longDescription:
- 'Erstelle und lerne mit Karteikarten. Nutzt Spaced Repetition für optimales Lernen.',
- icon: 'Cards',
- color: 'from-violet-600 to-purple-700',
- category: 'tools',
- requiresAuth: true,
- commands: [
- { command: '!learn', description: 'Startet eine Lernsession' },
- { command: '!add', description: 'Fügt eine Karte hinzu', example: '!add Frage | Antwort' },
- { command: '!decks', description: 'Listet deine Decks' },
- { command: '!stats', description: 'Zeigt Lernfortschritt' },
- ],
- },
- {
- id: 'presi-bot',
- name: 'Presi Bot',
- matrixUserId: '@presi-bot:matrix.mana.how',
- description: 'Präsentationen erstellen mit KI',
- longDescription:
- 'Erstelle professionelle Präsentationen mit KI-Unterstützung. Generiere Folien aus Text oder Themen.',
- icon: 'PresentationChart',
- color: 'from-red-500 to-rose-600',
- category: 'tools',
- requiresAuth: true,
- commands: [
- {
- command: '!create',
- description: 'Erstellt eine Präsentation',
- example: '!create Thema: KI im Alltag',
- },
- { command: '!slides', description: 'Zeigt deine Präsentationen' },
- { command: '!export', description: 'Exportiert als PDF/PPTX' },
- ],
- },
-];
-
-export const CATEGORIES = [
- { id: 'all', label: 'Alle' },
- { id: 'productivity', label: 'Produktivität' },
- { id: 'ai', label: 'KI & Chat' },
- { id: 'media', label: 'Medien' },
- { id: 'lifestyle', label: 'Lifestyle' },
- { id: 'tools', label: 'Tools' },
-] as const;
-
-export type BotCategory = (typeof CATEGORIES)[number]['id'];
diff --git a/apps/matrix/apps/web/src/lib/i18n/index.ts b/apps/matrix/apps/web/src/lib/i18n/index.ts
deleted file mode 100644
index 9dba087ec..000000000
--- a/apps/matrix/apps/web/src/lib/i18n/index.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { browser } from '$app/environment';
-import { init, register, locale, waitLocale } from 'svelte-i18n';
-
-// List of supported locales
-export const supportedLocales = ['de', 'en'] as const;
-export type SupportedLocale = (typeof supportedLocales)[number];
-
-// Default locale
-const defaultLocale = 'de';
-
-// Register all available locales
-register('de', () => import('./locales/de.json'));
-register('en', () => import('./locales/en.json'));
-
-// Get initial locale from browser or localStorage
-function getInitialLocale(): SupportedLocale {
- if (browser) {
- // Check localStorage first
- const stored = localStorage.getItem('matrix_locale');
- if (stored && supportedLocales.includes(stored as SupportedLocale)) {
- return stored as SupportedLocale;
- }
-
- // Fall back to browser language
- const browserLang = navigator.language.split('-')[0];
- if (supportedLocales.includes(browserLang as SupportedLocale)) {
- return browserLang as SupportedLocale;
- }
- }
-
- return defaultLocale;
-}
-
-// Initialize i18n at module scope (required for SSR)
-init({
- fallbackLocale: defaultLocale,
- initialLocale: getInitialLocale(),
-});
-
-// Set locale and persist to localStorage
-export function setLocale(newLocale: SupportedLocale) {
- locale.set(newLocale);
- if (browser) {
- localStorage.setItem('matrix_locale', newLocale);
- }
-}
-
-// Wait for locale to be loaded (useful for SSR)
-export { waitLocale };
diff --git a/apps/matrix/apps/web/src/lib/i18n/locales/de.json b/apps/matrix/apps/web/src/lib/i18n/locales/de.json
deleted file mode 100644
index 6d6198aa3..000000000
--- a/apps/matrix/apps/web/src/lib/i18n/locales/de.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "app": {
- "name": "Manalink",
- "description": "Sicherer Matrix-Chat"
- },
- "nav": {
- "chat": "Chat",
- "bots": "Bots",
- "settings": "Einstellungen"
- },
- "auth": {
- "login": "Anmelden",
- "logout": "Abmelden",
- "connecting": "Verbinde mit Matrix...",
- "connectionFailed": "Verbindung fehlgeschlagen",
- "retry": "Erneut versuchen"
- },
- "chat": {
- "newChat": "Neuer Chat",
- "createRoom": "Raum erstellen",
- "sendMessage": "Nachricht senden",
- "typeMessage": "Nachricht schreiben...",
- "noRooms": "Noch keine Räume",
- "noMessages": "Noch keine Nachrichten"
- },
- "bots": {
- "title": "Bots",
- "subtitle": "Entdecke alle verfügbaren Bot-Assistenten",
- "search": "Bot suchen...",
- "startChat": "Chat starten",
- "commands": "Befehle",
- "example": "Beispiel",
- "free": "Frei",
- "requiresLogin": "Erfordert Anmeldung",
- "noResults": "Keine Bots gefunden",
- "found": "gefunden",
- "categories": {
- "all": "Alle",
- "productivity": "Produktivität",
- "ai": "KI & Chat",
- "media": "Medien",
- "lifestyle": "Lifestyle",
- "tools": "Tools"
- }
- }
-}
diff --git a/apps/matrix/apps/web/src/lib/i18n/locales/en.json b/apps/matrix/apps/web/src/lib/i18n/locales/en.json
deleted file mode 100644
index c7c415a00..000000000
--- a/apps/matrix/apps/web/src/lib/i18n/locales/en.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "app": {
- "name": "Manalink",
- "description": "Secure Matrix chat"
- },
- "nav": {
- "chat": "Chat",
- "bots": "Bots",
- "settings": "Settings"
- },
- "auth": {
- "login": "Sign in",
- "logout": "Sign out",
- "connecting": "Connecting to Matrix...",
- "connectionFailed": "Connection failed",
- "retry": "Retry"
- },
- "chat": {
- "newChat": "New Chat",
- "createRoom": "Create Room",
- "sendMessage": "Send message",
- "typeMessage": "Type a message...",
- "noRooms": "No rooms yet",
- "noMessages": "No messages yet"
- },
- "bots": {
- "title": "Bots",
- "subtitle": "Discover all available bot assistants",
- "search": "Search bots...",
- "startChat": "Start Chat",
- "commands": "Commands",
- "example": "Example",
- "free": "Free",
- "requiresLogin": "Requires login",
- "noResults": "No bots found",
- "found": "found",
- "categories": {
- "all": "All",
- "productivity": "Productivity",
- "ai": "AI & Chat",
- "media": "Media",
- "lifestyle": "Lifestyle",
- "tools": "Tools"
- }
- }
-}
diff --git a/apps/matrix/apps/web/src/lib/matrix/client.test.ts b/apps/matrix/apps/web/src/lib/matrix/client.test.ts
deleted file mode 100644
index 8c2f8f38c..000000000
--- a/apps/matrix/apps/web/src/lib/matrix/client.test.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { discoverHomeserver, checkHomeserver, loginWithToken } from './client';
-
-// Mock matrix-js-sdk to avoid importing the full SDK in tests
-vi.mock('matrix-js-sdk', () => ({
- createClient: vi.fn(),
-}));
-
-vi.mock('./polyfills', () => ({}));
-
-describe('discoverHomeserver', () => {
- beforeEach(() => {
- vi.restoreAllMocks();
- });
-
- it('extracts domain from Matrix user ID', async () => {
- // Mock .well-known failing so we get the fallback
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network error')));
-
- const result = await discoverHomeserver('@user:example.com');
- expect(result).toBe('https://example.com');
- });
-
- it('returns null for invalid user ID without domain', async () => {
- const result = await discoverHomeserver('@user');
- expect(result).toBeNull();
- });
-
- it('uses domain directly when no @ prefix', async () => {
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network error')));
-
- const result = await discoverHomeserver('matrix.org');
- expect(result).toBe('https://matrix.org');
- });
-
- it('strips protocol prefix from domain', async () => {
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network error')));
-
- const result = await discoverHomeserver('https://matrix.org');
- expect(result).toBe('https://matrix.org');
- });
-
- it('uses .well-known base_url when available', async () => {
- vi.stubGlobal(
- 'fetch',
- vi.fn().mockResolvedValue({
- ok: true,
- json: async () => ({
- 'm.homeserver': { base_url: 'https://synapse.example.com/' },
- }),
- })
- );
-
- const result = await discoverHomeserver('example.com');
- expect(result).toBe('https://synapse.example.com');
- });
-});
-
-describe('checkHomeserver', () => {
- beforeEach(() => {
- vi.restoreAllMocks();
- });
-
- it('returns ok for reachable server', async () => {
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }));
-
- const result = await checkHomeserver('matrix.mana.how');
- expect(result).toEqual({ ok: true });
- });
-
- it('prepends https:// if missing', async () => {
- const mockFetch = vi.fn().mockResolvedValue({ ok: true });
- vi.stubGlobal('fetch', mockFetch);
-
- await checkHomeserver('matrix.mana.how');
- expect(mockFetch).toHaveBeenCalledWith('https://matrix.mana.how/_matrix/client/versions', {
- method: 'GET',
- });
- });
-
- it('does not double-prepend https://', async () => {
- const mockFetch = vi.fn().mockResolvedValue({ ok: true });
- vi.stubGlobal('fetch', mockFetch);
-
- await checkHomeserver('https://matrix.mana.how');
- expect(mockFetch).toHaveBeenCalledWith('https://matrix.mana.how/_matrix/client/versions', {
- method: 'GET',
- });
- });
-
- it('returns error for non-ok response', async () => {
- vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 502 }));
-
- const result = await checkHomeserver('matrix.mana.how');
- expect(result).toEqual({ ok: false, error: 'Server returned 502' });
- });
-
- it('returns error for network failure', async () => {
- vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Failed to fetch')));
-
- const result = await checkHomeserver('matrix.mana.how');
- expect(result).toEqual({ ok: false, error: 'Failed to fetch' });
- });
-});
-
-describe('loginWithToken', () => {
- it('normalizes homeserver URL', async () => {
- const result = await loginWithToken('matrix.mana.how', 'token123', '@user:matrix.mana.how');
- expect(result.success).toBe(true);
- expect(result.credentials?.homeserver).toBe('https://matrix.mana.how');
- });
-
- it('removes trailing slash from homeserver', async () => {
- const result = await loginWithToken(
- 'https://matrix.mana.how/',
- 'token123',
- '@user:matrix.mana.how'
- );
- expect(result.credentials?.homeserver).toBe('https://matrix.mana.how');
- });
-
- it('preserves provided deviceId', async () => {
- const result = await loginWithToken(
- 'matrix.mana.how',
- 'token123',
- '@user:matrix.mana.how',
- 'MYDEVICE'
- );
- expect(result.credentials?.deviceId).toBe('MYDEVICE');
- });
-
- it('generates deviceId when not provided', async () => {
- const result = await loginWithToken('matrix.mana.how', 'token123', '@user:matrix.mana.how');
- expect(result.credentials?.deviceId).toMatch(/^MANA_\d+$/);
- });
-});
diff --git a/apps/matrix/apps/web/src/lib/matrix/client.ts b/apps/matrix/apps/web/src/lib/matrix/client.ts
deleted file mode 100644
index d73c969c8..000000000
--- a/apps/matrix/apps/web/src/lib/matrix/client.ts
+++ /dev/null
@@ -1,252 +0,0 @@
-import type { MatrixCredentials, LoginResult } from './types';
-
-/**
- * Login with username and password
- */
-export async function loginWithPassword(
- homeserver: string,
- username: string,
- password: string
-): Promise {
- // Load polyfills first
- await import('./polyfills');
- const { createClient } = await import('matrix-js-sdk');
-
- // Normalize homeserver URL
- let baseUrl = homeserver.trim();
- if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {
- baseUrl = `https://${baseUrl}`;
- }
- // Remove trailing slash
- baseUrl = baseUrl.replace(/\/$/, '');
-
- const tempClient = createClient({ baseUrl });
-
- try {
- const response = await tempClient.login('m.login.password', {
- user: username,
- password: password,
- initial_device_display_name: 'Manalink',
- });
-
- return {
- success: true,
- credentials: {
- homeserver: baseUrl,
- accessToken: response.access_token,
- userId: response.user_id,
- deviceId: response.device_id,
- },
- };
- } catch (err) {
- const message = err instanceof Error ? err.message : 'Login failed';
-
- // Provide more helpful error messages
- if (message.includes('M_FORBIDDEN')) {
- return { success: false, error: 'Invalid username or password' };
- }
- if (message.includes('M_USER_DEACTIVATED')) {
- return { success: false, error: 'This account has been deactivated' };
- }
- if (message.includes('Failed to fetch') || message.includes('NetworkError')) {
- return { success: false, error: 'Could not connect to homeserver' };
- }
-
- return { success: false, error: message };
- }
-}
-
-/**
- * Login with an existing access token (for SSO/OAuth flows)
- */
-export async function loginWithToken(
- homeserver: string,
- accessToken: string,
- userId: string,
- deviceId?: string
-): Promise {
- // Normalize homeserver URL
- let baseUrl = homeserver.trim();
- if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {
- baseUrl = `https://${baseUrl}`;
- }
- baseUrl = baseUrl.replace(/\/$/, '');
-
- return {
- success: true,
- credentials: {
- homeserver: baseUrl,
- accessToken,
- userId,
- deviceId: deviceId || `MANA_${Date.now()}`,
- },
- };
-}
-
-/**
- * Discover homeserver from user ID or domain
- * Uses .well-known discovery
- */
-export async function discoverHomeserver(userIdOrDomain: string): Promise {
- // Extract domain from user ID if provided
- let domain = userIdOrDomain;
- if (userIdOrDomain.startsWith('@')) {
- const parts = userIdOrDomain.split(':');
- if (parts.length < 2) return null;
- domain = parts[1];
- }
-
- // Remove any protocol prefix
- domain = domain.replace(/^https?:\/\//, '');
-
- try {
- // Try .well-known discovery
- const wellKnownUrl = `https://${domain}/.well-known/matrix/client`;
- const response = await fetch(wellKnownUrl);
-
- if (response.ok) {
- const data = await response.json();
- const baseUrl = data['m.homeserver']?.base_url;
- if (baseUrl) {
- return baseUrl.replace(/\/$/, '');
- }
- }
- } catch {
- // .well-known not available
- }
-
- // Fallback: assume homeserver is at the domain
- return `https://${domain}`;
-}
-
-/**
- * Check if a homeserver is reachable
- */
-export async function checkHomeserver(
- homeserver: string
-): Promise<{ ok: boolean; error?: string }> {
- let baseUrl = homeserver.trim();
- if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {
- baseUrl = `https://${baseUrl}`;
- }
-
- try {
- const response = await fetch(`${baseUrl}/_matrix/client/versions`, {
- method: 'GET',
- });
-
- if (response.ok) {
- return { ok: true };
- }
-
- return { ok: false, error: `Server returned ${response.status}` };
- } catch (err) {
- return {
- ok: false,
- error: err instanceof Error ? err.message : 'Could not connect to server',
- };
- }
-}
-
-/**
- * Login with a Matrix SSO login token (for SSO/OAuth callback)
- * This exchanges the loginToken from SSO redirect for proper credentials
- */
-export async function loginWithLoginToken(
- homeserver: string,
- loginToken: string
-): Promise {
- // Load polyfills first
- await import('./polyfills');
- const { createClient } = await import('matrix-js-sdk');
-
- // Normalize homeserver URL
- let baseUrl = homeserver.trim();
- if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {
- baseUrl = `https://${baseUrl}`;
- }
- // Remove trailing slash
- baseUrl = baseUrl.replace(/\/$/, '');
-
- const tempClient = createClient({ baseUrl });
-
- try {
- const response = await tempClient.login('m.login.token', {
- token: loginToken,
- initial_device_display_name: 'Manalink',
- });
-
- return {
- success: true,
- credentials: {
- homeserver: baseUrl,
- accessToken: response.access_token,
- userId: response.user_id,
- deviceId: response.device_id,
- },
- };
- } catch (err) {
- const message = err instanceof Error ? err.message : 'Login failed';
-
- // Provide more helpful error messages
- if (message.includes('M_UNKNOWN_TOKEN') || message.includes('M_FORBIDDEN')) {
- return { success: false, error: 'Login token expired or invalid. Please try again.' };
- }
- if (message.includes('Failed to fetch') || message.includes('NetworkError')) {
- return { success: false, error: 'Could not connect to homeserver' };
- }
-
- return { success: false, error: message };
- }
-}
-
-/**
- * Register a new account (if registration is open)
- */
-export async function register(
- homeserver: string,
- username: string,
- password: string
-): Promise {
- await import('./polyfills');
- const { createClient } = await import('matrix-js-sdk');
-
- let baseUrl = homeserver.trim();
- if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {
- baseUrl = `https://${baseUrl}`;
- }
- baseUrl = baseUrl.replace(/\/$/, '');
-
- const tempClient = createClient({ baseUrl });
-
- try {
- const response = await tempClient.register(username, password, null, {
- initial_device_display_name: 'Manalink',
- } as any);
-
- return {
- success: true,
- credentials: {
- homeserver: baseUrl,
- accessToken: response.access_token!,
- userId: response.user_id,
- deviceId: response.device_id!,
- },
- };
- } catch (err: unknown) {
- const message = err instanceof Error ? err.message : 'Registration failed';
-
- // Check for common errors
- if (message.includes('M_USER_IN_USE')) {
- return { success: false, error: 'Username is already taken' };
- }
- if (message.includes('M_INVALID_USERNAME')) {
- return { success: false, error: 'Invalid username format' };
- }
- if (message.includes('M_FORBIDDEN')) {
- return { success: false, error: 'Registration is disabled on this server' };
- }
-
- return { success: false, error: message };
- }
-}
diff --git a/apps/matrix/apps/web/src/lib/matrix/crypto.ts b/apps/matrix/apps/web/src/lib/matrix/crypto.ts
deleted file mode 100644
index bc04d22c3..000000000
--- a/apps/matrix/apps/web/src/lib/matrix/crypto.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * Crypto utilities for Matrix E2EE
- */
-
-import type { MatrixClient } from 'matrix-js-sdk';
-
-/**
- * SAS Emoji data type from matrix-js-sdk
- */
-export interface SasEmoji {
- emoji: string;
- description: string;
-}
-
-/**
- * Verification emoji set (7 emojis)
- */
-export type EmojiSet = [SasEmoji, SasEmoji, SasEmoji, SasEmoji, SasEmoji, SasEmoji, SasEmoji];
-
-/**
- * Format device name for display
- */
-export function formatDeviceName(displayName?: string, deviceId?: string): string {
- if (displayName) return displayName;
- if (deviceId) {
- // Show first 8 characters of device ID
- return `Device ${deviceId.substring(0, 8)}...`;
- }
- return 'Unknown Device';
-}
-
-/**
- * Format timestamp for device last seen
- */
-export function formatLastSeen(timestamp?: number): string {
- if (!timestamp) return 'Unknown';
-
- const date = new Date(timestamp);
- const now = new Date();
- const diffMs = now.getTime() - date.getTime();
- const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
-
- if (diffDays === 0) return 'Today';
- if (diffDays === 1) return 'Yesterday';
- if (diffDays < 7) return `${diffDays} days ago`;
- if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks ago`;
- if (diffDays < 365) return `${Math.floor(diffDays / 30)} months ago`;
- return date.toLocaleDateString();
-}
-
-/**
- * Check if recovery key format is valid
- * Recovery keys are base58 encoded, 28-32 characters
- */
-export function isValidRecoveryKey(key: string): boolean {
- const trimmed = key.trim().replace(/\s+/g, '');
- // Recovery keys are typically ~59 characters, space-separated into groups
- // Valid characters are Base58 (no 0, O, I, l)
- const base58Regex = /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/;
- return trimmed.length >= 28 && base58Regex.test(trimmed);
-}
-
-/**
- * Format recovery key for display (add spaces every 4 chars)
- */
-export function formatRecoveryKey(key: string): string {
- const trimmed = key.replace(/\s+/g, '');
- return trimmed.match(/.{1,4}/g)?.join(' ') || key;
-}
-
-/**
- * Get encryption warning level for a room
- */
-export function getEncryptionWarningLevel(
- encrypted: boolean,
- allVerified: boolean
-): 'none' | 'warning' | 'secure' {
- if (!encrypted) return 'none';
- return allVerified ? 'secure' : 'warning';
-}
-
-/**
- * Generate a device display name based on browser/OS info
- */
-export function generateDeviceName(): string {
- if (typeof navigator === 'undefined') return 'Manalink';
-
- const ua = navigator.userAgent;
- let browser = 'Browser';
- let os = 'Desktop';
-
- // Detect browser
- if (ua.includes('Firefox')) browser = 'Firefox';
- else if (ua.includes('Edg')) browser = 'Edge';
- else if (ua.includes('Chrome')) browser = 'Chrome';
- else if (ua.includes('Safari')) browser = 'Safari';
-
- // Detect OS
- if (ua.includes('Windows')) os = 'Windows';
- else if (ua.includes('Mac')) os = 'macOS';
- else if (ua.includes('Linux')) os = 'Linux';
- else if (ua.includes('Android')) os = 'Android';
- else if (ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS';
-
- return `Manalink (${browser} on ${os})`;
-}
-
-/**
- * Check if cross-signing should be bootstrapped
- */
-export async function shouldBootstrapCrossSigning(client: MatrixClient): Promise {
- const crypto = client.getCrypto();
- if (!crypto) return false;
-
- try {
- const status = await crypto.getCrossSigningStatus();
- // Should bootstrap if we don't have keys on device
- return !status.publicKeysOnDevice;
- } catch {
- return true;
- }
-}
-
-/**
- * Check if key backup should be setup
- */
-export async function shouldSetupKeyBackup(client: MatrixClient): Promise {
- const crypto = client.getCrypto();
- if (!crypto) return false;
-
- try {
- const backupVersion = await crypto.getActiveSessionBackupVersion();
- return backupVersion === null;
- } catch {
- return true;
- }
-}
diff --git a/apps/matrix/apps/web/src/lib/matrix/index.ts b/apps/matrix/apps/web/src/lib/matrix/index.ts
deleted file mode 100644
index 971c9cd11..000000000
--- a/apps/matrix/apps/web/src/lib/matrix/index.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// Matrix client exports
-export { matrixStore } from './store.svelte';
-export {
- loginWithPassword,
- loginWithToken,
- loginWithLoginToken,
- discoverHomeserver,
- checkHomeserver,
- register,
-} from './client';
-export * from './types';
-export * from './crypto';
diff --git a/apps/matrix/apps/web/src/lib/matrix/polyfills.ts b/apps/matrix/apps/web/src/lib/matrix/polyfills.ts
deleted file mode 100644
index 5d1648ebc..000000000
--- a/apps/matrix/apps/web/src/lib/matrix/polyfills.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Polyfills required for matrix-js-sdk to work in browser environment
- * Must be imported before any matrix-js-sdk imports
- */
-import { Buffer } from 'buffer';
-
-if (typeof window !== 'undefined') {
- // Global object polyfill
- window.global = window.globalThis;
-
- // Buffer polyfill (used by matrix-js-sdk for binary data)
- (window as Window).Buffer = Buffer;
-
- // Process polyfill (some dependencies check process.env)
- (window as Window).process = { env: {} };
-}
-
-export {};
diff --git a/apps/matrix/apps/web/src/lib/matrix/store.svelte.ts b/apps/matrix/apps/web/src/lib/matrix/store.svelte.ts
deleted file mode 100644
index 85c3944c9..000000000
--- a/apps/matrix/apps/web/src/lib/matrix/store.svelte.ts
+++ /dev/null
@@ -1,2019 +0,0 @@
-import { browser } from '$app/environment';
-import type { MatrixClient, Room, MatrixEvent, RoomMember as SDKRoomMember } from 'matrix-js-sdk';
-import {
- showMessageNotification,
- canShowNotifications,
- isDocumentFocused,
-} from '$lib/notifications';
-import type {
- SyncState,
- MatrixCredentials,
- SimpleRoom,
- SimpleMessage,
- MessageType,
- MessageReaction,
- ReadReceipt,
- RoomMember,
- VerificationStatus,
- DeviceInfo,
- VerificationRequest,
- CryptoCallbacks,
- CrossSigningStatus,
- PresenceState,
- UserPresence,
- SimpleCall,
- CallCallbacks,
- CallState as CallStateType,
- CallType,
- CallDirection,
- RoomWidget,
-} from './types';
-
-const STORAGE_KEY = 'matrix_credentials';
-const LAST_ROOM_KEY = 'matrix_last_room';
-
-/**
- * Reactive Matrix store using Svelte 5 runes
- */
-class MatrixStore {
- // ─────────────────────────────────────────────────────────
- // Private State
- // ─────────────────────────────────────────────────────────
- private _client = $state(null);
- private _syncState = $state('STOPPED');
- private _rooms = $state([]);
- private _currentRoomId = $state(null);
- private _timeline = $state([]);
- private _typingUsers = $state>(new Map());
- private _userPresence = $state>(new Map());
- private _error = $state(null);
- private _initialized = $state(false);
-
- // Crypto State
- private _cryptoReady = $state(false);
- private _verificationStatus = $state('unknown');
- private _activeVerification = $state(null);
- private _keyBackupEnabled = $state(false);
- private _crossSigningReady = $state(false);
- private _cryptoCallbacks: CryptoCallbacks = {};
-
- // VoIP / Call State
- private _activeCall = $state(null);
- private _incomingCall = $state(null);
- private _callCallbacks: CallCallbacks = {};
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- private _matrixCall: any = null; // The actual MatrixCall object
-
- // ─────────────────────────────────────────────────────────
- // Public Getters
- // ─────────────────────────────────────────────────────────
- get client() {
- return this._client;
- }
- get syncState() {
- return this._syncState;
- }
- get error() {
- return this._error;
- }
- get initialized() {
- return this._initialized;
- }
- get currentRoomId() {
- return this._currentRoomId;
- }
-
- // Crypto Getters
- get cryptoReady() {
- return this._cryptoReady;
- }
- get verificationStatus() {
- return this._verificationStatus;
- }
- get activeVerification() {
- return this._activeVerification;
- }
- get keyBackupEnabled() {
- return this._keyBackupEnabled;
- }
- get crossSigningReady() {
- return this._crossSigningReady;
- }
-
- // VoIP Getters
- get activeCall() {
- return this._activeCall;
- }
- get incomingCall() {
- return this._incomingCall;
- }
- get hasActiveCall() {
- return this._activeCall !== null;
- }
- get hasIncomingCall() {
- return this._incomingCall !== null;
- }
-
- /**
- * Get presence for a specific user
- */
- getUserPresence(userId: string): UserPresence | undefined {
- return this._userPresence.get(userId);
- }
-
- /**
- * Check if a user is currently online
- */
- isUserOnline(userId: string): boolean {
- const presence = this._userPresence.get(userId);
- return presence?.presence === 'online' || presence?.currentlyActive === true;
- }
-
- // ─────────────────────────────────────────────────────────
- // Derived State
- // ─────────────────────────────────────────────────────────
-
- /** Is the client ready to use? */
- isReady = $derived(this._syncState === 'PREPARED' || this._syncState === 'SYNCING');
-
- /** Is currently syncing? */
- isSyncing = $derived(this._syncState === 'SYNCING' || this._syncState === 'CATCHUP');
-
- /** Current user ID */
- userId = $derived(this._client?.getUserId() || null);
-
- /** Simplified room list sorted by last activity */
- rooms = $derived(
- this._rooms
- .map((room) => this.roomToSimpleRoom(room))
- .sort((a, b) => (b.lastMessageTime || 0) - (a.lastMessageTime || 0))
- );
-
- /** Joined rooms only */
- joinedRooms = $derived(this.rooms.filter((r) => r.membership === 'join'));
-
- /** Invited rooms */
- invitedRooms = $derived(this.rooms.filter((r) => r.membership === 'invite'));
-
- /** Direct message rooms (joined only) */
- directRooms = $derived(this.joinedRooms.filter((r) => r.isDirect));
-
- /** Group rooms (non-DM, joined only) */
- groupRooms = $derived(this.joinedRooms.filter((r) => !r.isDirect));
-
- /** Current selected room */
- currentRoom = $derived(
- this._currentRoomId ? this._rooms.find((r) => r.roomId === this._currentRoomId) || null : null
- );
-
- /** Current room as SimpleRoom */
- currentSimpleRoom = $derived(this.currentRoom ? this.roomToSimpleRoom(this.currentRoom) : null);
-
- /** Messages in current room */
- messages = $derived(
- this._timeline
- .filter((e) => e.getType() === 'm.room.message')
- .map((e) => this.eventToSimpleMessage(e, this._timeline))
- );
-
- /** Users currently typing in current room */
- currentRoomTyping = $derived(
- this._currentRoomId ? this._typingUsers.get(this._currentRoomId) || [] : []
- );
-
- /** Total unread count across all rooms */
- totalUnreadCount = $derived(this.rooms.reduce((sum, r) => sum + r.unreadCount, 0));
-
- // ─────────────────────────────────────────────────────────
- // Initialization
- // ─────────────────────────────────────────────────────────
-
- /**
- * Initialize the Matrix client
- * @param credentials Optional credentials, will load from storage if not provided
- */
- async initialize(credentials?: MatrixCredentials): Promise {
- if (!browser) return false;
- if (this._initialized && this._client) return true;
-
- // Load polyfills first
- await import('./polyfills');
-
- // Get credentials
- const creds = credentials || this.loadCredentials();
- if (!creds) {
- this._error = 'No credentials available';
- return false;
- }
-
- try {
- const sdk = await import('matrix-js-sdk');
-
- this._client = sdk.createClient({
- baseUrl: creds.homeserver,
- accessToken: creds.accessToken,
- userId: creds.userId,
- deviceId: creds.deviceId,
- timelineSupport: true,
- });
-
- this.setupEventHandlers(sdk);
-
- // Initialize Rust Crypto
- try {
- await this._client.initRustCrypto();
- this._cryptoReady = true;
-
- // Setup crypto event handlers
- this.setupCryptoEventHandlers(sdk);
- } catch {
- this._cryptoReady = false;
- }
-
- await this._client.startClient({
- initialSyncLimit: 20,
- lazyLoadMembers: true,
- });
-
- this.saveCredentials(creds);
- this._initialized = true;
- this._error = null;
-
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to initialize Matrix client';
- console.error('Matrix initialization error:', err);
- return false;
- }
- }
-
- /**
- * Setup event handlers for Matrix SDK events
- */
- private setupEventHandlers(sdk: typeof import('matrix-js-sdk')) {
- if (!this._client) return;
-
- // Sync state changes
- this._client.on(sdk.ClientEvent.Sync, (state, prevState) => {
- this._syncState = state as SyncState;
-
- if (state === 'PREPARED') {
- this._rooms = this._client!.getRooms();
- }
-
- if (state === 'ERROR') {
- this._error = 'Sync error occurred';
- }
- });
-
- // Room timeline updates (new messages)
- this._client.on(sdk.RoomEvent.Timeline, (event, room, toStartOfTimeline) => {
- // Skip historical events from pagination
- if (toStartOfTimeline) return;
-
- // Update rooms list
- this._rooms = this._client!.getRooms();
-
- // Update timeline if we're in this room
- if (room?.roomId === this._currentRoomId) {
- this._timeline = [...(room.getLiveTimeline().getEvents() || [])];
- }
-
- // Show browser notification for new messages from others
- if (
- browser &&
- event.getType() === 'm.room.message' &&
- event.getSender() !== this._client!.getUserId() &&
- !isDocumentFocused()
- ) {
- const content = event.getContent();
- const body = content?.body || '';
- const senderName = this.getSenderName(event);
- const roomName = room?.name || 'Unbekannt';
-
- showMessageNotification(senderName, body, roomName, {
- onClick: () => {
- if (room) {
- this.selectRoom(room.roomId);
- }
- },
- });
- }
- });
-
- // Typing indicators
- this._client.on(sdk.RoomMemberEvent.Typing, (event, member) => {
- const roomId = event.getRoomId();
- if (!roomId) return;
-
- const room = this._client!.getRoom(roomId);
- const typingMembers =
- room
- ?.getMembersWithMembership('join')
- .filter((m) => m.typing && m.userId !== this._client!.getUserId())
- .map((m) => m.name || m.userId) || [];
-
- // Trigger reactivity by creating new Map
- const newMap = new Map(this._typingUsers);
- newMap.set(roomId, typingMembers);
- this._typingUsers = newMap;
- });
-
- // Room membership changes (invites, joins, leaves)
- this._client.on(sdk.RoomEvent.MyMembership, (room, membership, prevMembership) => {
- this._rooms = this._client!.getRooms();
- });
-
- // Room name/state changes
- this._client.on(sdk.RoomStateEvent.Events, (event, state, prevEvent) => {
- // Trigger reactivity for room updates
- this._rooms = this._client!.getRooms();
- });
-
- // User presence changes
- this._client.on(sdk.UserEvent.Presence, (event, user) => {
- if (!user) return;
-
- const userId = user.userId;
- const presence: UserPresence = {
- userId,
- presence: (user.presence as PresenceState) || 'offline',
- lastActiveAgo: user.lastActiveAgo,
- statusMessage: user.presenceStatusMsg,
- currentlyActive: user.currentlyActive,
- };
-
- // Trigger reactivity by creating new Map
- const newMap = new Map(this._userPresence);
- newMap.set(userId, presence);
- this._userPresence = newMap;
-
- // Also trigger room list update for DMs
- this._rooms = this._client!.getRooms();
- });
-
- // Read receipt updates
- this._client.on(sdk.RoomEvent.Receipt, (event, room) => {
- // Update timeline if we're in this room to refresh read receipts
- if (room.roomId === this._currentRoomId) {
- this._timeline = [...(room.getLiveTimeline().getEvents() || [])];
- }
- });
-
- // Incoming calls
- // CallEvent is exported from matrix-js-sdk, but CallState needs dynamic import from webrtc module
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- this._client.on('Call.incoming' as any, async (call: any) => {
- try {
- const webrtc = await import('matrix-js-sdk/lib/webrtc/call');
- this.handleIncomingCall(call, webrtc.CallEvent, webrtc.CallState);
- } catch (err) {
- console.error('Error handling incoming call:', err);
- }
- });
- }
-
- /**
- * Setup crypto event handlers
- * Note: Uses loose typing due to matrix-js-sdk type complexity
- */
- private async setupCryptoEventHandlers(_sdk: typeof import('matrix-js-sdk')) {
- if (!this._client || !this._cryptoReady) return;
-
- const crypto = this._client.getCrypto();
- if (!crypto) return;
-
- try {
- // Import CryptoEvent separately - types may vary by SDK version
- const cryptoApi = await import('matrix-js-sdk/lib/crypto-api');
- const CryptoEvent = cryptoApi.CryptoEvent;
-
- // Verification request received
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (this._client as any).on(CryptoEvent.VerificationRequestReceived, (request: unknown) => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const req = request as any;
- const verificationRequest: VerificationRequest = {
- requestId: req.transactionId || req.id || '',
- otherUserId: req.otherUserId || '',
- otherDeviceId: req.otherDeviceId,
- phase: this.mapVerificationPhase(req.phase ?? 0),
- isSelfVerification: req.isSelfVerification ?? false,
- methods: (req.methods || []) as VerificationRequest['methods'],
- };
-
- this._activeVerification = verificationRequest;
- this._cryptoCallbacks.onVerificationRequest?.(verificationRequest);
- });
-
- // Keys changed (e.g., new device added)
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (this._client as any).on(CryptoEvent.KeysChanged, () => {
- this.checkVerificationStatus();
- });
-
- // Key backup status - check if event exists
- if ('KeyBackupStatus' in CryptoEvent) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (this._client as any).on((CryptoEvent as any).KeyBackupStatus, (enabled: boolean) => {
- this._keyBackupEnabled = enabled;
- this._cryptoCallbacks.onKeyBackupStatus?.(enabled);
- });
- }
- } catch {
- // Crypto event handlers not fully supported in this SDK version
- }
-
- // Initial status check
- this.checkVerificationStatus();
- this.checkKeyBackupStatus();
- }
-
- /**
- * Map SDK verification phase to our type
- */
- private mapVerificationPhase(phase: number): VerificationRequest['phase'] {
- // Phase values from matrix-js-sdk VerificationPhase enum
- const phaseMap: Record = {
- 0: 'created',
- 1: 'requested',
- 2: 'ready',
- 3: 'started',
- 4: 'done',
- 5: 'cancelled',
- };
- return phaseMap[phase] || 'created';
- }
-
- // ─────────────────────────────────────────────────────────
- // Room Actions
- // ─────────────────────────────────────────────────────────
-
- /**
- * Select a room to view
- */
- selectRoom(roomId: string) {
- this._currentRoomId = roomId;
- const room = this._client?.getRoom(roomId);
-
- if (room) {
- this._timeline = room.getLiveTimeline().getEvents() || [];
-
- // Mark as read
- const lastEvent = this._timeline[this._timeline.length - 1];
- if (lastEvent) {
- this._client?.sendReadReceipt(lastEvent).catch(console.error);
- }
-
- // Save last room to localStorage
- if (browser) {
- localStorage.setItem(LAST_ROOM_KEY, roomId);
- }
- } else {
- this._timeline = [];
- }
- }
-
- /**
- * Clear current room selection
- */
- clearRoom() {
- this._currentRoomId = null;
- this._timeline = [];
- }
-
- /**
- * Join a room by ID or alias
- */
- async joinRoom(roomIdOrAlias: string): Promise {
- if (!this._client) return false;
-
- try {
- await this._client.joinRoom(roomIdOrAlias);
- this._rooms = this._client.getRooms();
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to join room';
- return false;
- }
- }
-
- /**
- * Leave a room
- */
- async leaveRoom(roomId: string): Promise {
- if (!this._client) return false;
-
- try {
- await this._client.leave(roomId);
-
- if (this._currentRoomId === roomId) {
- this.clearRoom();
- }
-
- this._rooms = this._client.getRooms();
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to leave room';
- return false;
- }
- }
-
- /**
- * Create a new room
- */
- async createRoom(options: {
- name?: string;
- topic?: string;
- isDirect?: boolean;
- invite?: string[];
- }): Promise {
- if (!this._client) return null;
-
- try {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const result = await this._client.createRoom({
- name: options.name,
- topic: options.topic,
- is_direct: options.isDirect,
- invite: options.invite,
- preset: (options.isDirect ? 'trusted_private_chat' : 'private_chat') as any,
- });
-
- this._rooms = this._client.getRooms();
- return result.room_id;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to create room';
- return null;
- }
- }
-
- // ─────────────────────────────────────────────────────────
- // Message Actions
- // ─────────────────────────────────────────────────────────
-
- /**
- * Send a text message to current room
- */
- async sendMessage(body: string): Promise {
- if (!this._client || !this._currentRoomId) return false;
-
- try {
- await this._client.sendTextMessage(this._currentRoomId, body);
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to send message';
- return false;
- }
- }
-
- /**
- * Send a message to a specific room (for forwarding)
- */
- async sendMessageToRoom(roomId: string, body: string): Promise {
- if (!this._client) return false;
-
- try {
- await this._client.sendTextMessage(roomId, body);
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to send message';
- return false;
- }
- }
-
- /**
- * Send typing indicator
- */
- async sendTyping(typing: boolean): Promise {
- if (!this._client || !this._currentRoomId) return;
-
- try {
- await this._client.sendTyping(this._currentRoomId, typing, typing ? 30000 : 0);
- } catch (err) {
- // Ignore typing errors
- }
- }
-
- /**
- * Load more messages (pagination)
- */
- async loadMoreMessages(limit = 50): Promise {
- if (!this._client || !this._currentRoomId) return false;
-
- const room = this._client.getRoom(this._currentRoomId);
- if (!room) return false;
-
- try {
- await this._client.scrollback(room, limit);
- this._timeline = room.getLiveTimeline().getEvents() || [];
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to load messages';
- return false;
- }
- }
-
- /**
- * Send a file/image to current room
- */
- async sendFile(file: File, onProgress?: (progress: number) => void): Promise {
- if (!this._client || !this._currentRoomId) return false;
-
- try {
- // Upload to Matrix media repo
- const uploadResponse = await this._client.uploadContent(file, {
- progressHandler: (progress) => {
- if (onProgress) {
- onProgress(Math.round((progress.loaded / progress.total) * 100));
- }
- },
- });
-
- const mxcUrl = uploadResponse.content_uri;
-
- // Determine message type based on MIME type
- const isImage = file.type.startsWith('image/');
- const isVideo = file.type.startsWith('video/');
- const isAudio = file.type.startsWith('audio/');
-
- let msgtype = 'm.file';
- if (isImage) msgtype = 'm.image';
- if (isVideo) msgtype = 'm.video';
- if (isAudio) msgtype = 'm.audio';
-
- // Build content based on type
- const content: Record = {
- msgtype,
- body: file.name,
- filename: file.name,
- info: {
- mimetype: file.type,
- size: file.size,
- },
- url: mxcUrl,
- };
-
- // Add dimensions for images
- if (isImage) {
- const dimensions = await this.getImageDimensions(file);
- if (dimensions) {
- (content.info as Record).w = dimensions.width;
- (content.info as Record).h = dimensions.height;
- }
- }
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- await this._client.sendMessage(this._currentRoomId, content as any);
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to send file';
- return false;
- }
- }
-
- /**
- * Get image dimensions
- */
- private getImageDimensions(file: File): Promise<{ width: number; height: number } | null> {
- return new Promise((resolve) => {
- if (!file.type.startsWith('image/')) {
- resolve(null);
- return;
- }
-
- const img = new Image();
- img.onload = () => {
- resolve({ width: img.width, height: img.height });
- URL.revokeObjectURL(img.src);
- };
- img.onerror = () => resolve(null);
- img.src = URL.createObjectURL(file);
- });
- }
-
- /**
- * Get HTTP URL for Matrix media (mxc:// URLs)
- */
- getMediaUrl(mxcUrl: string, width?: number, height?: number): string | null {
- if (!this._client || !mxcUrl?.startsWith('mxc://')) return null;
-
- if (width && height) {
- return this._client.mxcUrlToHttp(mxcUrl, width, height, 'scale') || null;
- }
- return this._client.mxcUrlToHttp(mxcUrl) || null;
- }
-
- /**
- * Reply to a message
- */
- async replyToMessage(eventId: string, body: string): Promise {
- if (!this._client || !this._currentRoomId) return false;
-
- const room = this._client.getRoom(this._currentRoomId);
- const originalEvent = room?.findEventById(eventId);
- if (!originalEvent) return false;
-
- try {
- const content = {
- msgtype: 'm.text',
- body: `> <${originalEvent.getSender()}> ${originalEvent.getContent().body}\n\n${body}`,
- format: 'org.matrix.custom.html',
- formatted_body: `In reply to ${originalEvent.getSender()} ${originalEvent.getContent().body} ${body}`,
- 'm.relates_to': {
- 'm.in_reply_to': {
- event_id: eventId,
- },
- },
- };
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- await this._client.sendMessage(this._currentRoomId, content as any);
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to send reply';
- return false;
- }
- }
-
- /**
- * Edit a message
- */
- async editMessage(eventId: string, newBody: string): Promise {
- if (!this._client || !this._currentRoomId) return false;
-
- try {
- const content = {
- msgtype: 'm.text',
- body: `* ${newBody}`,
- 'm.new_content': {
- msgtype: 'm.text',
- body: newBody,
- },
- 'm.relates_to': {
- rel_type: 'm.replace',
- event_id: eventId,
- },
- };
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- await this._client.sendMessage(this._currentRoomId, content as any);
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to edit message';
- return false;
- }
- }
-
- /**
- * Delete (redact) a message
- */
- async deleteMessage(eventId: string, reason?: string): Promise {
- if (!this._client || !this._currentRoomId) return false;
-
- try {
- await this._client.redactEvent(this._currentRoomId, eventId, undefined, { reason });
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to delete message';
- return false;
- }
- }
-
- /**
- * React to a message with an emoji
- */
- async reactToMessage(eventId: string, emoji: string): Promise {
- if (!this._client || !this._currentRoomId) return false;
-
- try {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- await (this._client as any).sendEvent(this._currentRoomId, 'm.reaction', {
- 'm.relates_to': {
- rel_type: 'm.annotation',
- event_id: eventId,
- key: emoji,
- },
- });
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to react';
- return false;
- }
- }
-
- // ─────────────────────────────────────────────────────────
- // User Actions
- // ─────────────────────────────────────────────────────────
-
- /**
- * Invite a user to a room
- */
- async inviteUser(roomId: string, userId: string): Promise {
- if (!this._client) return false;
-
- try {
- await this._client.invite(roomId, userId);
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to invite user';
- return false;
- }
- }
-
- /**
- * Kick a user from a room
- */
- async kickUser(roomId: string, userId: string, reason?: string): Promise {
- if (!this._client) return false;
-
- try {
- await this._client.kick(roomId, userId, reason);
- return true;
- } catch (err) {
- this._error = err instanceof Error ? err.message : 'Failed to kick user';
- return false;
- }
- }
-
- /**
- * Search for users by name or ID
- */
- async searchUsers(
- query: string,
- limit = 10
- ): Promise<{ userId: string; displayName?: string; avatarUrl?: string }[]> {
- if (!this._client || !query.trim()) return [];
-
- try {
- const result = await this._client.searchUserDirectory({ term: query, limit });
- return result.results.map((user) => ({
- userId: user.user_id,
- displayName: user.display_name,
- avatarUrl: user.avatar_url
- ? this.getMediaUrl(user.avatar_url, 40, 40) || undefined
- : undefined,
- }));
- } catch {
- return [];
- }
- }
-
- /**
- * Search messages in the current room
- */
- async searchMessages(
- query: string,
- roomId?: string
- ): Promise<
- {
- eventId: string;
- sender: string;
- senderName: string;
- body: string;
- timestamp: number;
- roomId: string;
- roomName: string;
- }[]
- > {
- if (!this._client || !query.trim()) return [];
-
- const targetRoomId = roomId || this._currentRoomId;
-
- try {
- // Use Matrix search API
- const searchResult = await this._client.searchRoomEvents({
- term: query,
- filter: targetRoomId
- ? {
- rooms: [targetRoomId],
- }
- : undefined,
- });
-
- const results: {
- eventId: string;
- sender: string;
- senderName: string;
- body: string;
- timestamp: number;
- roomId: string;
- roomName: string;
- }[] = [];
-
- // Process search results - cast to any since SDK types are incomplete
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const searchData = searchResult as any;
- const searchResults = searchData?.search_categories?.room_events?.results || [];
- for (const result of searchResults) {
- const event = result.result;
- if (!event) continue;
-
- const eventRoomId = event.room_id;
- const room = this._client.getRoom(eventRoomId);
- const content = event.content as { body?: string };
-
- results.push({
- eventId: event.event_id || '',
- sender: event.sender || '',
- senderName: room?.getMember(event.sender || '')?.name || event.sender || 'Unbekannt',
- body: content?.body || '',
- timestamp: event.origin_server_ts || 0,
- roomId: eventRoomId || '',
- roomName: room?.name || 'Unbekannt',
- });
- }
-
- return results;
- } catch (e) {
- console.error('Search failed:', e);
- return [];
- }
- }
-
- /**
- * Get room members
- */
- getRoomMembers(roomId?: string): RoomMember[] {
- const id = roomId || this._currentRoomId;
- if (!this._client || !id) return [];
-
- const room = this._client.getRoom(id);
- if (!room) return [];
-
- // Get power levels from room state
- const powerLevelsEvent = room.currentState.getStateEvents('m.room.power_levels', '');
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const powerLevels = (powerLevelsEvent as any)?.getContent?.()?.users || {};
- const defaultPowerLevel = (powerLevelsEvent as any)?.getContent?.()?.users_default || 0;
-
- return room.getMembersWithMembership('join').map((member) => ({
- userId: member.userId,
- displayName: member.name || member.userId,
- avatarUrl:
- member.getAvatarUrl(this._client!.baseUrl, 40, 40, 'scale', false, false) || undefined,
- membership: member.membership as RoomMember['membership'],
- powerLevel: powerLevels[member.userId] ?? defaultPowerLevel,
- }));
- }
-
- /**
- * Get widgets in a room
- */
- getRoomWidgets(roomId?: string): RoomWidget[] {
- const id = roomId || this._currentRoomId;
- if (!this._client || !id) return [];
-
- const room = this._client.getRoom(id);
- if (!room) return [];
-
- const widgets: RoomWidget[] = [];
-
- // Get all widget state events (im.vector.modular.widgets is the standard type)
- const widgetEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
-
- for (const event of widgetEvents) {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const content = (event as any).getContent?.();
- if (!content || !content.url) continue; // Skip removed widgets
-
- widgets.push({
- id: content.id || (event as any).getStateKey?.() || '',
- type: content.type || 'm.custom',
- name: content.name || 'Widget',
- url: content.url,
- creatorUserId: content.creatorUserId || (event as any).getSender?.() || '',
- data: content.data,
- });
- }
-
- return widgets;
- }
-
- /**
- * Build widget URL with Matrix variable substitution
- */
- buildWidgetUrl(widget: RoomWidget, roomId?: string): string {
- const id = roomId || this._currentRoomId;
- const userId = this._client?.getUserId() || '';
-
- let url = widget.url;
-
- // Substitute Matrix variables
- url = url.replace(/\$matrix_user_id/g, encodeURIComponent(userId));
- url = url.replace(/\$matrix_room_id/g, encodeURIComponent(id || ''));
- url = url.replace(
- /\$matrix_display_name/g,
- encodeURIComponent(userId.split(':')[0].substring(1))
- );
- url = url.replace(/\$matrix_avatar_url/g, '');
-
- return url;
- }
-
- // ─────────────────────────────────────────────────────────
- // Crypto Actions
- // ─────────────────────────────────────────────────────────
-
- /**
- * Set crypto callbacks for UI notifications
- */
- setCryptoCallbacks(callbacks: CryptoCallbacks) {
- this._cryptoCallbacks = callbacks;
- }
-
- /**
- * Check current verification status
- */
- async checkVerificationStatus(): Promise {
- if (!this._client || !this._cryptoReady) {
- this._verificationStatus = 'unknown';
- return;
- }
-
- try {
- const crypto = this._client.getCrypto();
- if (!crypto) {
- this._verificationStatus = 'unknown';
- return;
- }
-
- const crossSigningStatus = await crypto.getCrossSigningStatus();
- if (crossSigningStatus.publicKeysOnDevice && crossSigningStatus.privateKeysCachedLocally) {
- this._verificationStatus = 'verified';
- this._crossSigningReady = true;
- } else {
- this._verificationStatus = 'unverified';
- this._crossSigningReady = false;
- }
- } catch (err) {
- console.error('Error checking verification status:', err);
- this._verificationStatus = 'unknown';
- }
- }
-
- /**
- * Check key backup status
- */
- async checkKeyBackupStatus(): Promise {
- if (!this._client || !this._cryptoReady) {
- this._keyBackupEnabled = false;
- return;
- }
-
- try {
- const crypto = this._client.getCrypto();
- if (!crypto) return;
-
- const backupInfo = await crypto.getActiveSessionBackupVersion();
- this._keyBackupEnabled = backupInfo !== null;
- } catch (err) {
- console.error('Error checking key backup status:', err);
- this._keyBackupEnabled = false;
- }
- }
-
- /**
- * Get current device ID
- */
- getDeviceId(): string | null {
- return this._client?.getDeviceId() || null;
- }
-
- /**
- * Get all devices for a user
- */
- async getDevices(userId?: string): Promise {
- if (!this._client || !this._cryptoReady) return [];
-
- const targetUserId = userId || this._client.getUserId();
- if (!targetUserId) return [];
-
- try {
- const crypto = this._client.getCrypto();
- if (!crypto) return [];
-
- const deviceMap = await crypto.getUserDeviceInfo([targetUserId]);
- const devices = deviceMap.get(targetUserId);
- if (!devices) return [];
-
- const currentDeviceId = this._client.getDeviceId();
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return Array.from(devices.values()).map((device: any) => ({
- deviceId: device.deviceId,
- displayName: device.displayName,
- // DeviceVerification enum values may vary - check for Verified state
- verified:
- device.verified === 1 || device.verified === 'Verified' || device.isVerified?.() === true,
- blocked: device.verified === 2 || device.verified === 'Blocked',
- isCurrentDevice: device.deviceId === currentDeviceId,
- }));
- } catch (err) {
- console.error('Error getting devices:', err);
- return [];
- }
- }
-
- /**
- * Start verification with another device
- * Note: Verification flow varies by SDK version - this is a simplified approach
- */
- async startVerification(targetUserId?: string, _targetDeviceId?: string): Promise {
- if (!this._client || !this._cryptoReady) return false;
-
- const userId = targetUserId || this._client.getUserId();
- if (!userId) return false;
-
- try {
- const crypto = this._client.getCrypto();
- if (!crypto) return false;
-
- // Use requestOwnUserVerification for self-verification
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const cryptoAny = crypto as any;
- if (userId === this._client.getUserId() && cryptoAny.requestOwnUserVerification) {
- await cryptoAny.requestOwnUserVerification();
- } else if (cryptoAny.requestVerificationDM) {
- await cryptoAny.requestVerificationDM(userId);
- } else {
- return false;
- }
-
- return true;
- } catch (err) {
- console.error('Error starting verification:', err);
- this._error = 'Failed to start verification';
- return false;
- }
- }
-
- /**
- * Accept incoming verification request
- */
- async acceptVerification(_requestId: string): Promise {
- if (!this._client || !this._cryptoReady) return false;
-
- try {
- // Verification request handling varies by SDK version - handled automatically
- return true;
- } catch (err) {
- console.error('Error accepting verification:', err);
- return false;
- }
- }
-
- /**
- * Confirm SAS verification (emoji match)
- */
- async confirmSasVerification(_requestId: string): Promise {
- if (!this._client || !this._cryptoReady) return false;
-
- try {
- // In newer SDK versions, verification is handled via verifier events
- return true;
- } catch (err) {
- console.error('Error confirming SAS verification:', err);
- return false;
- }
- }
-
- /**
- * Cancel verification
- */
- async cancelVerification(_requestId: string): Promise {
- if (!this._client || !this._cryptoReady) return;
-
- try {
- this._activeVerification = null;
- } catch {
- // Cancellation is best-effort
- }
- }
-
- /**
- * Bootstrap secret storage and cross-signing
- */
- async bootstrapSecretStorage(passphrase?: string): Promise<{ recoveryKey: string } | null> {
- if (!this._client || !this._cryptoReady) return null;
-
- try {
- const crypto = this._client.getCrypto();
- if (!crypto) return null;
-
- let recoveryKey = '';
-
- // Bootstrap cross-signing first
- await crypto.bootstrapCrossSigning({
- authUploadDeviceSigningKeys: async (makeRequest) => {
- // This callback is called when we need to authenticate for uploading keys
- // In a real app, this might show a UIA (User Interactive Auth) dialog
- await makeRequest({});
- },
- });
-
- // Bootstrap secret storage
- await crypto.bootstrapSecretStorage({
- createSecretStorageKey: async () => {
- // Generate a new recovery key
- const keyInfo = await crypto.createRecoveryKeyFromPassphrase(passphrase);
- recoveryKey = keyInfo.encodedPrivateKey || '';
- return keyInfo;
- },
- });
-
- // Reset key backup
- await crypto.resetKeyBackup();
-
- this._crossSigningReady = true;
- this._keyBackupEnabled = true;
- this._verificationStatus = 'verified';
-
- return { recoveryKey };
- } catch (err) {
- console.error('Error bootstrapping secret storage:', err);
- this._error = 'Failed to setup encryption keys';
- return null;
- }
- }
-
- /**
- * Restore keys from recovery key
- */
- async restoreFromRecoveryKey(recoveryKey: string): Promise {
- if (!this._client || !this._cryptoReady) return false;
-
- try {
- const crypto = this._client.getCrypto();
- if (!crypto) return false;
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const cryptoAny = crypto as any;
- const clientAny = this._client as any;
-
- // Restore from backup using recovery key
- // Method names may vary by SDK version
- if (cryptoAny.restoreKeyBackupWithRecoveryKey) {
- await cryptoAny.restoreKeyBackupWithRecoveryKey(recoveryKey);
- } else if (clientAny.restoreKeyBackupWithRecoveryKey) {
- const backupInfo = await clientAny.getKeyBackupVersion?.();
- if (backupInfo) {
- await clientAny.restoreKeyBackupWithRecoveryKey(
- recoveryKey,
- undefined,
- undefined,
- backupInfo
- );
- }
- } else {
- console.warn('Key backup restore not available in this SDK version');
- return false;
- }
-
- this._keyBackupEnabled = true;
- await this.checkVerificationStatus();
- return true;
- } catch (err) {
- console.error('Error restoring from recovery key:', err);
- this._error = 'Failed to restore encryption keys';
- return false;
- }
- }
-
- /**
- * Get cross-signing status
- */
- async getCrossSigningStatus(): Promise {
- if (!this._client || !this._cryptoReady) return null;
-
- try {
- const crypto = this._client.getCrypto();
- if (!crypto) return null;
-
- const status = await crypto.getCrossSigningStatus();
- // Status properties may be booleans or objects depending on SDK version
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const statusAny = status as any;
- return {
- publicKeysOnDevice: !!statusAny.publicKeysOnDevice,
- privateKeysInSecretStorage: !!statusAny.privateKeysInSecretStorage,
- privateKeysCachedLocally: !!statusAny.privateKeysCachedLocally,
- };
- } catch (err) {
- console.error('Error getting cross-signing status:', err);
- return null;
- }
- }
-
- /**
- * Check if a room is encrypted
- */
- isRoomEncrypted(roomId?: string): boolean {
- const id = roomId || this._currentRoomId;
- if (!this._client || !id) return false;
-
- const room = this._client.getRoom(id);
- return room?.hasEncryptionStateEvent() ?? false;
- }
-
- /**
- * Get room encryption status with details
- */
- async getRoomEncryptionStatus(roomId?: string): Promise<{
- encrypted: boolean;
- allDevicesVerified: boolean;
- unverifiedDevices: number;
- }> {
- const id = roomId || this._currentRoomId;
- if (!this._client || !id) {
- return { encrypted: false, allDevicesVerified: false, unverifiedDevices: 0 };
- }
-
- const room = this._client.getRoom(id);
- if (!room) {
- return { encrypted: false, allDevicesVerified: false, unverifiedDevices: 0 };
- }
-
- const encrypted = room.hasEncryptionStateEvent();
- if (!encrypted || !this._cryptoReady) {
- return { encrypted, allDevicesVerified: false, unverifiedDevices: 0 };
- }
-
- try {
- const crypto = this._client.getCrypto();
- if (!crypto) {
- return { encrypted, allDevicesVerified: false, unverifiedDevices: 0 };
- }
-
- // Get all members and their devices
- const members = room.getMembersWithMembership('join');
- const userIds = members.map((m) => m.userId);
- const deviceMap = await crypto.getUserDeviceInfo(userIds);
-
- let unverifiedCount = 0;
- for (const [userId, devices] of deviceMap) {
- for (const device of devices.values()) {
- if (device.verified !== 1) {
- // Not verified
- unverifiedCount++;
- }
- }
- }
-
- return {
- encrypted,
- allDevicesVerified: unverifiedCount === 0,
- unverifiedDevices: unverifiedCount,
- };
- } catch {
- return { encrypted, allDevicesVerified: false, unverifiedDevices: 0 };
- }
- }
-
- // ─────────────────────────────────────────────────────────
- // VoIP / Call Actions
- // ─────────────────────────────────────────────────────────
-
- /**
- * Set call callbacks for UI notifications
- */
- setCallCallbacks(callbacks: CallCallbacks) {
- this._callCallbacks = callbacks;
- }
-
- /**
- * Place a voice call to the current room
- */
- async placeVoiceCall(roomId?: string): Promise {
- const targetRoomId = roomId || this._currentRoomId;
- if (!this._client || !targetRoomId) return false;
-
- try {
- // Import WebRTC types from the submodule
- const webrtc = await import('matrix-js-sdk/lib/webrtc/call');
- const { CallEvent, CallState } = webrtc;
-
- // Create the call
- const call = this._client.createCall(targetRoomId);
- if (!call) {
- console.error('Failed to create call');
- return false;
- }
-
- this._matrixCall = call;
-
- // Set up event handlers
- this.setupCallEventHandlers(call, CallEvent, CallState);
-
- // Place the voice call
- await call.placeVoiceCall();
-
- // Update active call state
- this._activeCall = this.matrixCallToSimpleCall(call, 'voice', 'outbound');
-
- return true;
- } catch (err) {
- console.error('Error placing voice call:', err);
- this._error = 'Failed to start voice call';
- return false;
- }
- }
-
- /**
- * Place a video call to the current room
- */
- async placeVideoCall(roomId?: string): Promise {
- const targetRoomId = roomId || this._currentRoomId;
- if (!this._client || !targetRoomId) return false;
-
- try {
- // Import WebRTC types from the submodule
- const webrtc = await import('matrix-js-sdk/lib/webrtc/call');
- const { CallEvent, CallState } = webrtc;
-
- // Create the call
- const call = this._client.createCall(targetRoomId);
- if (!call) {
- console.error('Failed to create call');
- return false;
- }
-
- this._matrixCall = call;
-
- // Set up event handlers
- this.setupCallEventHandlers(call, CallEvent, CallState);
-
- // Place the video call
- await call.placeVideoCall();
-
- // Update active call state
- this._activeCall = this.matrixCallToSimpleCall(call, 'video', 'outbound');
-
- return true;
- } catch (err) {
- console.error('Error placing video call:', err);
- this._error = 'Failed to start video call';
- return false;
- }
- }
-
- /**
- * Answer an incoming call
- */
- async answerCall(): Promise {
- if (!this._matrixCall || !this._incomingCall) return false;
-
- try {
- await this._matrixCall.answer();
- this._activeCall = { ...this._incomingCall };
- this._incomingCall = null;
- return true;
- } catch (err) {
- console.error('Error answering call:', err);
- return false;
- }
- }
-
- /**
- * Reject an incoming call
- */
- rejectCall(): boolean {
- if (!this._matrixCall || !this._incomingCall) return false;
-
- try {
- this._matrixCall.reject();
- this._incomingCall = null;
- this._matrixCall = null;
- return true;
- } catch (err) {
- console.error('Error rejecting call:', err);
- return false;
- }
- }
-
- /**
- * Hang up the current call
- */
- hangupCall(): boolean {
- if (!this._matrixCall) return false;
-
- try {
- this._matrixCall.hangup('user_hangup', false);
- this._activeCall = null;
- this._matrixCall = null;
- return true;
- } catch (err) {
- console.error('Error hanging up call:', err);
- return false;
- }
- }
-
- /**
- * Toggle microphone mute
- */
- toggleMicMute(): boolean {
- if (!this._matrixCall || !this._activeCall) return false;
-
- try {
- const muted = this._matrixCall.isMicrophoneMuted();
- this._matrixCall.setMicrophoneMuted(!muted);
- this._activeCall = { ...this._activeCall, isMicMuted: !muted };
- return true;
- } catch (err) {
- console.error('Error toggling mic mute:', err);
- return false;
- }
- }
-
- /**
- * Toggle camera mute (for video calls)
- */
- toggleCameraMute(): boolean {
- if (!this._matrixCall || !this._activeCall) return false;
-
- try {
- const muted = this._matrixCall.isLocalVideoMuted();
- this._matrixCall.setLocalVideoMuted(!muted);
- this._activeCall = { ...this._activeCall, isCameraMuted: !muted };
- return true;
- } catch (err) {
- console.error('Error toggling camera mute:', err);
- return false;
- }
- }
-
- /**
- * Toggle screen sharing
- */
- async toggleScreenShare(): Promise {
- if (!this._matrixCall || !this._activeCall) return false;
-
- try {
- const isSharing = this._activeCall.isScreenSharing;
-
- if (isSharing) {
- // Stop screen sharing - switch back to camera
- await this._matrixCall.setScreensharingEnabled(false);
- this._activeCall = { ...this._activeCall, isScreenSharing: false };
- } else {
- // Start screen sharing
- const success = await this._matrixCall.setScreensharingEnabled(true, {
- audio: true, // Include system audio if available
- });
- if (success) {
- this._activeCall = { ...this._activeCall, isScreenSharing: true };
- } else {
- console.warn('Screen sharing was denied or failed');
- return false;
- }
- }
- return true;
- } catch (err) {
- console.error('Error toggling screen share:', err);
- this._error = 'Bildschirmfreigabe fehlgeschlagen';
- return false;
- }
- }
-
- /**
- * Set up call event handlers
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- private setupCallEventHandlers(call: any, CallEvent: any, CallState: any) {
- // State changes
- call.on(CallEvent.State, (state: string, oldState: string) => {
- if (this._activeCall) {
- this._activeCall = {
- ...this._activeCall,
- state: state as CallStateType,
- };
- this._callCallbacks.onCallStateChange?.(this._activeCall);
- }
-
- // Handle call ending
- if (state === CallState.Ended) {
- const reason = call.hangupReason;
- const endedCall = this._activeCall;
- this._activeCall = null;
- this._matrixCall = null;
- if (endedCall) {
- this._callCallbacks.onCallEnded?.(endedCall, reason);
- }
- }
- });
-
- // Feeds changed (audio/video streams)
- call.on(CallEvent.FeedsChanged, (feeds: any[]) => {
- if (this._activeCall) {
- const localFeed = feeds.find((f) => f.isLocal());
- const remoteFeed = feeds.find((f) => !f.isLocal());
-
- this._activeCall = {
- ...this._activeCall,
- localStream: localFeed?.stream,
- remoteStream: remoteFeed?.stream,
- };
- }
- });
-
- // Error handling
- call.on(CallEvent.Error, (error: any) => {
- console.error('Call error:', error);
- this._error = `Call error: ${error.message || 'Unknown error'}`;
- });
-
- // Hangup
- call.on(CallEvent.Hangup, () => {
- const endedCall = this._activeCall;
- this._activeCall = null;
- this._matrixCall = null;
- if (endedCall) {
- this._callCallbacks.onCallEnded?.(endedCall, call.hangupReason);
- }
- });
- }
-
- /**
- * Handle incoming call from the SDK
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- private handleIncomingCall(call: any, CallEvent: any, CallState: any) {
- this._matrixCall = call;
-
- // Determine call type from the call object
- const callType: CallType = call.type === 'video' ? 'video' : 'voice';
-
- // Create simple call representation
- const simpleCall = this.matrixCallToSimpleCall(call, callType, 'inbound');
- this._incomingCall = simpleCall;
-
- // Set up event handlers
- this.setupCallEventHandlers(call, CallEvent, CallState);
-
- // Notify UI
- this._callCallbacks.onIncomingCall?.(simpleCall);
- }
-
- /**
- * Convert MatrixCall to SimpleCall
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- private matrixCallToSimpleCall(call: any, type: CallType, direction: CallDirection): SimpleCall {
- const opponent = call.getOpponentMember?.();
- const room = this._client?.getRoom(call.roomId);
-
- return {
- callId: call.callId,
- roomId: call.roomId,
- state: (call.state || 'fledgling') as CallStateType,
- type,
- direction,
- opponentUserId: opponent?.userId,
- opponentName: opponent?.name || room?.name || 'Unbekannt',
- opponentAvatar: opponent?.getAvatarUrl?.(this._client?.baseUrl || '', 48, 48, 'scale'),
- isMicMuted: call.isMicrophoneMuted?.() || false,
- isCameraMuted: call.isLocalVideoMuted?.() || false,
- isScreenSharing: false,
- isRemoteOnHold: call.isRemoteOnHold?.() || false,
- };
- }
-
- // ─────────────────────────────────────────────────────────
- // Cleanup
- // ─────────────────────────────────────────────────────────
-
- /**
- * Stop the client and clean up
- */
- destroy() {
- // Hang up any active call
- if (this._matrixCall) {
- try {
- this._matrixCall.hangup('user_hangup', false);
- } catch {
- // Ignore errors during cleanup
- }
- }
-
- this._client?.stopClient();
- this._client = null;
- this._syncState = 'STOPPED';
- this._rooms = [];
- this._timeline = [];
- this._currentRoomId = null;
- this._typingUsers = new Map();
- this._initialized = false;
- // Reset crypto state
- this._cryptoReady = false;
- this._verificationStatus = 'unknown';
- this._activeVerification = null;
- this._keyBackupEnabled = false;
- this._crossSigningReady = false;
- this._cryptoCallbacks = {};
- // Reset call state
- this._activeCall = null;
- this._incomingCall = null;
- this._matrixCall = null;
- this._callCallbacks = {};
- }
-
- /**
- * Logout and clear credentials
- */
- logout() {
- this.destroy();
- if (browser) {
- localStorage.removeItem(STORAGE_KEY);
- localStorage.removeItem(LAST_ROOM_KEY);
- }
- }
-
- // ─────────────────────────────────────────────────────────
- // Helper Methods
- // ─────────────────────────────────────────────────────────
-
- /**
- * Convert SDK Room to SimpleRoom
- */
- private roomToSimpleRoom(room: Room): SimpleRoom {
- const lastEvent = room
- .getLiveTimeline()
- .getEvents()
- .filter((e) => e.getType() === 'm.room.message')
- .pop();
-
- // Get topic from state event
- const topicEvent = room.currentState.getStateEvents('m.room.topic', '');
- const topic = (topicEvent as MatrixEvent | null)?.getContent()?.topic;
-
- // Get membership status
- const myUserId = this._client?.getUserId();
- const myMember = myUserId ? room.getMember(myUserId) : null;
- const membership = (myMember?.membership || 'leave') as SimpleRoom['membership'];
-
- // Get inviter if this is an invite
- let inviter: string | undefined;
- if (membership === 'invite' && myMember) {
- // The events array contains the invite event
- const inviteEvent = room.currentState.getStateEvents('m.room.member', myUserId || '');
- if (inviteEvent) {
- const sender = (inviteEvent as MatrixEvent).getSender();
- if (sender) {
- const senderMember = room.getMember(sender);
- inviter = senderMember?.name || sender;
- }
- }
- }
-
- // Get DM user presence info
- const isDirect = this.isDirectRoom(room);
- let dmUserId: string | undefined;
- let presence: SimpleRoom['presence'];
- let lastActiveAgo: number | undefined;
-
- if (isDirect && myUserId) {
- // Find the other user in the DM
- const members = room.getJoinedMembers();
- const otherMember = members.find((m) => m.userId !== myUserId);
- if (otherMember) {
- dmUserId = otherMember.userId;
- const userPresence = this._userPresence.get(dmUserId);
- if (userPresence) {
- presence = userPresence.presence;
- lastActiveAgo = userPresence.lastActiveAgo;
- } else {
- // Try to get from user object directly
- const user = this._client?.getUser(dmUserId);
- if (user) {
- presence = (user.presence as SimpleRoom['presence']) || 'offline';
- lastActiveAgo = user.lastActiveAgo;
- }
- }
- }
- }
-
- return {
- id: room.roomId,
- name: room.name || 'Unnamed Room',
- topic,
- avatar: room.getAvatarUrl(this._client?.baseUrl || '', 48, 48, 'scale') || undefined,
- lastMessage: lastEvent?.getContent()?.body,
- lastMessageSender: lastEvent ? this.getSenderName(lastEvent) : undefined,
- lastMessageTime: room.getLastActiveTimestamp() || undefined,
- unreadCount: room.getUnreadNotificationCount('total' as any) || 0,
- highlightCount: room.getUnreadNotificationCount('highlight' as any) || 0,
- isDirect,
- isEncrypted: room.hasEncryptionStateEvent(),
- memberCount: room.getJoinedMemberCount(),
- membership,
- inviter,
- dmUserId,
- presence,
- lastActiveAgo,
- };
- }
-
- /**
- * Convert SDK MatrixEvent to SimpleMessage
- */
- private eventToSimpleMessage(event: MatrixEvent, timeline?: MatrixEvent[]): SimpleMessage {
- const content = event.getContent();
- const relatesTo = content['m.relates_to'];
- const msgtype = content.msgtype || 'm.text';
-
- // Check if message was redacted
- const isRedacted = event.isRedacted();
-
- // Extract media info for file/image/video/audio messages
- let media: SimpleMessage['media'] = undefined;
- if (['m.image', 'm.file', 'm.video', 'm.audio'].includes(msgtype) && content.url) {
- const info = content.info || {};
- media = {
- mxcUrl: content.url,
- mimetype: info.mimetype,
- size: info.size,
- width: info.w,
- height: info.h,
- filename: content.filename || content.body,
- thumbnailUrl: info.thumbnail_url,
- duration: info.duration,
- };
- }
-
- // Get reply-to body if this is a reply
- let replyToBody: string | undefined;
- const replyToId = relatesTo?.['m.in_reply_to']?.event_id;
- if (replyToId) {
- const room = this._client?.getRoom(event.getRoomId() || '');
- const replyEvent = room?.findEventById(replyToId);
- if (replyEvent) {
- replyToBody = replyEvent.getContent().body;
- }
- }
-
- // Collect reactions for this message
- const reactions = this.getReactionsForEvent(event.getId() || '', timeline);
-
- // Get read receipts for this message (only for own messages)
- const isOwn = event.getSender() === this._client?.getUserId();
- const readBy = isOwn ? this.getReadReceiptsForEvent(event) : undefined;
-
- return {
- id: event.getId() || '',
- sender: event.getSender() || '',
- senderName: this.getSenderName(event),
- body: isRedacted ? 'Message deleted' : content.body || '',
- formattedBody: content.formatted_body,
- timestamp: event.getTs(),
- type: msgtype as MessageType,
- isOwn,
- replyTo: replyToId,
- replyToBody,
- edited: !!event.replacingEvent(),
- redacted: isRedacted,
- media,
- reactions: reactions.length > 0 ? reactions : undefined,
- readBy: readBy && readBy.length > 0 ? readBy : undefined,
- };
- }
-
- /**
- * Get reactions for a specific event
- */
- private getReactionsForEvent(eventId: string, timeline?: MatrixEvent[]): MessageReaction[] {
- if (!timeline || !eventId) return [];
-
- const myUserId = this._client?.getUserId();
- const reactionMap = new Map }>();
-
- // Find all m.reaction events that relate to this event
- for (const event of timeline) {
- if (event.getType() !== 'm.reaction') continue;
-
- const content = event.getContent();
- const relatesTo = content['m.relates_to'];
-
- if (
- relatesTo?.rel_type === 'm.annotation' &&
- relatesTo?.event_id === eventId &&
- relatesTo?.key
- ) {
- const emoji = relatesTo.key;
- const sender = event.getSender() || '';
-
- if (!reactionMap.has(emoji)) {
- reactionMap.set(emoji, { users: [], senders: new Set() });
- }
-
- const entry = reactionMap.get(emoji)!;
- // Avoid duplicates from same user
- if (!entry.senders.has(sender)) {
- entry.senders.add(sender);
- entry.users.push(sender);
- }
- }
- }
-
- // Convert to MessageReaction array
- const reactions: MessageReaction[] = [];
- for (const [key, data] of reactionMap) {
- reactions.push({
- key,
- count: data.users.length,
- users: data.users,
- includesMe: myUserId ? data.senders.has(myUserId) : false,
- });
- }
-
- // Sort by count descending
- return reactions.sort((a, b) => b.count - a.count);
- }
-
- /**
- * Get read receipts for a specific event
- */
- private getReadReceiptsForEvent(event: MatrixEvent): ReadReceipt[] {
- const eventId = event.getId();
- const roomId = event.getRoomId();
- if (!eventId || !roomId || !this._client) return [];
-
- const room = this._client.getRoom(roomId);
- if (!room) return [];
-
- const myUserId = this._client.getUserId();
- const receipts: ReadReceipt[] = [];
-
- // Get all members who have read up to or past this event
- const members = room.getJoinedMembers();
- for (const member of members) {
- // Skip self
- if (member.userId === myUserId) continue;
-
- // Get the user's read receipt
- const receiptEvent = room.getEventReadUpTo(member.userId);
- if (!receiptEvent) continue;
-
- // Check if their read receipt is at or after this event
- const receiptEventObj = room.findEventById(receiptEvent);
- if (receiptEventObj && receiptEventObj.getTs() >= event.getTs()) {
- receipts.push({
- userId: member.userId,
- userName: member.name || member.userId.split(':')[0].substring(1),
- timestamp: receiptEventObj.getTs(),
- });
- }
- }
-
- return receipts;
- }
-
- /**
- * Get display name for message sender
- */
- private getSenderName(event: MatrixEvent): string {
- const room = this._client?.getRoom(event.getRoomId() || '');
- const member = room?.getMember(event.getSender() || '');
- return member?.name || event.getSender()?.split(':')[0].substring(1) || 'Unknown';
- }
-
- /**
- * Check if room is a direct message room
- */
- private isDirectRoom(room: Room): boolean {
- const dmContent = this._client?.getAccountData('m.direct' as any)?.getContent() || {};
- return Object.values(dmContent).flat().includes(room.roomId);
- }
-
- /**
- * Load credentials from localStorage
- */
- private loadCredentials(): MatrixCredentials | null {
- if (!browser) return null;
-
- try {
- const stored = localStorage.getItem(STORAGE_KEY);
- return stored ? JSON.parse(stored) : null;
- } catch {
- return null;
- }
- }
-
- /**
- * Save credentials to localStorage
- */
- private saveCredentials(creds: MatrixCredentials) {
- if (browser) {
- localStorage.setItem(STORAGE_KEY, JSON.stringify(creds));
- }
- }
-
- /**
- * Check if credentials exist in storage
- */
- hasStoredCredentials(): boolean {
- return this.loadCredentials() !== null;
- }
-}
-
-// Export singleton instance
-export const matrixStore = new MatrixStore();
diff --git a/apps/matrix/apps/web/src/lib/matrix/types.ts b/apps/matrix/apps/web/src/lib/matrix/types.ts
deleted file mode 100644
index d4b7f1d1e..000000000
--- a/apps/matrix/apps/web/src/lib/matrix/types.ts
+++ /dev/null
@@ -1,324 +0,0 @@
-import type { Room, MatrixEvent, MatrixClient } from 'matrix-js-sdk';
-
-/**
- * Matrix sync states
- */
-export type SyncState = 'STOPPED' | 'PREPARED' | 'SYNCING' | 'ERROR' | 'RECONNECTING' | 'CATCHUP';
-
-/**
- * Credentials for Matrix authentication
- */
-export interface MatrixCredentials {
- homeserver: string;
- accessToken: string;
- userId: string;
- deviceId: string;
-}
-
-/**
- * Media info for files/images
- */
-export interface MediaInfo {
- mxcUrl: string;
- mimetype?: string;
- size?: number;
- width?: number;
- height?: number;
- filename?: string;
- thumbnailUrl?: string;
- duration?: number; // For audio/video
-}
-
-/**
- * Reaction on a message
- */
-export interface MessageReaction {
- key: string; // The emoji
- count: number;
- users: string[]; // User IDs who reacted
- includesMe: boolean;
-}
-
-/**
- * Read receipt info for a message
- */
-export interface ReadReceipt {
- userId: string;
- userName: string;
- timestamp: number;
-}
-
-/**
- * Simplified message for UI rendering
- */
-export interface SimpleMessage {
- id: string;
- sender: string;
- senderName: string;
- body: string;
- formattedBody?: string;
- timestamp: number;
- type: MessageType;
- isOwn: boolean;
- replyTo?: string;
- replyToBody?: string;
- edited?: boolean;
- redacted?: boolean;
- media?: MediaInfo;
- reactions?: MessageReaction[];
- // Read receipts
- readBy?: ReadReceipt[];
-}
-
-export type MessageType =
- | 'm.text'
- | 'm.image'
- | 'm.file'
- | 'm.audio'
- | 'm.video'
- | 'm.emote'
- | 'm.notice';
-
-/**
- * Room membership status
- */
-export type RoomMembership = 'join' | 'invite' | 'leave' | 'ban' | 'knock';
-
-/**
- * User presence state
- */
-export type PresenceState = 'online' | 'offline' | 'unavailable';
-
-/**
- * User presence info
- */
-export interface UserPresence {
- userId: string;
- presence: PresenceState;
- lastActiveAgo?: number; // milliseconds since last active
- statusMessage?: string;
- currentlyActive?: boolean;
-}
-
-/**
- * Simplified room for UI rendering
- */
-export interface SimpleRoom {
- id: string;
- name: string;
- topic?: string;
- avatar?: string;
- lastMessage?: string;
- lastMessageSender?: string;
- lastMessageTime?: number;
- unreadCount: number;
- highlightCount: number;
- isDirect: boolean;
- isEncrypted: boolean;
- memberCount: number;
- membership: RoomMembership;
- inviter?: string; // User who sent the invite
- // Presence info for DMs
- dmUserId?: string; // The other user's ID in a DM
- presence?: PresenceState;
- lastActiveAgo?: number;
-}
-
-/**
- * Room member info
- */
-export interface RoomMember {
- userId: string;
- displayName: string;
- avatarUrl?: string;
- membership: 'join' | 'invite' | 'leave' | 'ban' | 'knock';
- powerLevel: number;
-}
-
-/**
- * Login result
- */
-export interface LoginResult {
- success: boolean;
- credentials?: MatrixCredentials;
- error?: string;
-}
-
-/**
- * Matrix store state (for debugging)
- */
-export interface MatrixStoreState {
- syncState: SyncState;
- roomCount: number;
- currentRoomId: string | null;
- messageCount: number;
- error: string | null;
-}
-
-// ─────────────────────────────────────────────────────────
-// Crypto Types
-// ─────────────────────────────────────────────────────────
-
-/**
- * Device verification status
- */
-export type VerificationStatus = 'unverified' | 'verified' | 'unknown';
-
-/**
- * Device info for crypto
- */
-export interface DeviceInfo {
- deviceId: string;
- displayName?: string;
- lastSeenIp?: string;
- lastSeenTs?: number;
- verified: boolean;
- blocked: boolean;
- isCurrentDevice: boolean;
-}
-
-/**
- * User device list
- */
-export interface UserDevices {
- userId: string;
- devices: DeviceInfo[];
-}
-
-/**
- * Verification request state
- */
-export type VerificationRequestState =
- | 'created'
- | 'requested'
- | 'ready'
- | 'started'
- | 'done'
- | 'cancelled';
-
-/**
- * Verification method
- */
-export type VerificationMethod = 'sas' | 'reciprocate' | 'show_qr' | 'scan_qr';
-
-/**
- * SAS (Short Authentication String) verification data
- */
-export interface SasVerification {
- emoji?: { emoji: string; description: string }[];
- decimal?: [number, number, number];
-}
-
-/**
- * Crypto event callbacks for UI handling
- */
-export interface CryptoCallbacks {
- onVerificationRequest?: (request: VerificationRequest) => void;
- onDeviceVerified?: (userId: string, deviceId: string) => void;
- onKeyBackupStatus?: (enabled: boolean) => void;
-}
-
-/**
- * Verification request wrapper
- */
-export interface VerificationRequest {
- requestId: string;
- otherUserId: string;
- otherDeviceId?: string;
- phase: VerificationRequestState;
- isSelfVerification: boolean;
- methods: VerificationMethod[];
-}
-
-/**
- * Extended SimpleMessage with crypto info
- */
-export interface SimpleMessageWithCrypto extends SimpleMessage {
- encrypted?: boolean;
- decryptionError?: string;
- senderVerified?: boolean;
-}
-
-/**
- * Cross-signing status
- */
-export interface CrossSigningStatus {
- publicKeysOnDevice: boolean;
- privateKeysInSecretStorage: boolean;
- privateKeysCachedLocally: boolean;
-}
-
-// ─────────────────────────────────────────────────────────
-// VoIP / Call Types
-// ─────────────────────────────────────────────────────────
-
-/**
- * Call state
- */
-export type CallState =
- | 'fledgling'
- | 'invite_sent'
- | 'wait_local_media'
- | 'create_offer'
- | 'create_answer'
- | 'connecting'
- | 'connected'
- | 'ringing'
- | 'ended';
-
-/**
- * Call type (voice or video)
- */
-export type CallType = 'voice' | 'video';
-
-/**
- * Call direction
- */
-export type CallDirection = 'inbound' | 'outbound';
-
-/**
- * Simplified call info for UI
- */
-export interface SimpleCall {
- callId: string;
- roomId: string;
- state: CallState;
- type: CallType;
- direction: CallDirection;
- opponentUserId?: string;
- opponentName?: string;
- opponentAvatar?: string;
- startTime?: number;
- duration?: number;
- isMicMuted: boolean;
- isCameraMuted: boolean;
- isScreenSharing: boolean;
- isRemoteOnHold: boolean;
- localStream?: MediaStream;
- remoteStream?: MediaStream;
-}
-
-/**
- * Call event callbacks for UI handling
- */
-export interface CallCallbacks {
- onIncomingCall?: (call: SimpleCall) => void;
- onCallStateChange?: (call: SimpleCall) => void;
- onCallEnded?: (call: SimpleCall, reason?: string) => void;
-}
-
-// ─────────────────────────────────────────────────────────
-// Widget Types
-// ─────────────────────────────────────────────────────────
-
-/**
- * Matrix widget info
- */
-export interface RoomWidget {
- id: string;
- type: string;
- name: string;
- url: string;
- creatorUserId: string;
- data?: Record;
-}
diff --git a/apps/matrix/apps/web/src/lib/notifications/index.ts b/apps/matrix/apps/web/src/lib/notifications/index.ts
deleted file mode 100644
index 05d18f1c5..000000000
--- a/apps/matrix/apps/web/src/lib/notifications/index.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-import { browser } from '$app/environment';
-
-/**
- * Browser Notification Service for Matrix Chat
- */
-
-// Notification settings stored in localStorage
-const SETTINGS_KEY = 'matrix_notification_settings';
-
-interface NotificationSettings {
- enabled: boolean;
- sound: boolean;
- showPreview: boolean;
-}
-
-const defaultSettings: NotificationSettings = {
- enabled: true,
- sound: true,
- showPreview: true,
-};
-
-/**
- * Get notification settings from localStorage
- */
-export function getNotificationSettings(): NotificationSettings {
- if (!browser) return defaultSettings;
-
- try {
- const stored = localStorage.getItem(SETTINGS_KEY);
- if (stored) {
- return { ...defaultSettings, ...JSON.parse(stored) };
- }
- } catch (e) {
- console.warn('Failed to parse notification settings:', e);
- }
- return defaultSettings;
-}
-
-/**
- * Save notification settings to localStorage
- */
-export function saveNotificationSettings(settings: Partial): void {
- if (!browser) return;
-
- const current = getNotificationSettings();
- const updated = { ...current, ...settings };
- localStorage.setItem(SETTINGS_KEY, JSON.stringify(updated));
-}
-
-/**
- * Check if browser notifications are supported
- */
-export function isNotificationSupported(): boolean {
- return browser && 'Notification' in window;
-}
-
-/**
- * Get current notification permission status
- */
-export function getNotificationPermission(): NotificationPermission | 'unsupported' {
- if (!isNotificationSupported()) return 'unsupported';
- return Notification.permission;
-}
-
-/**
- * Request notification permission
- */
-export async function requestNotificationPermission(): Promise {
- if (!isNotificationSupported()) return 'unsupported';
-
- try {
- const permission = await Notification.requestPermission();
- return permission;
- } catch (e) {
- console.error('Failed to request notification permission:', e);
- return 'denied';
- }
-}
-
-/**
- * Check if notifications are enabled and permitted
- */
-export function canShowNotifications(): boolean {
- if (!isNotificationSupported()) return false;
- if (Notification.permission !== 'granted') return false;
-
- const settings = getNotificationSettings();
- return settings.enabled;
-}
-
-/**
- * Check if the document is currently focused
- */
-export function isDocumentFocused(): boolean {
- if (!browser) return true;
- return document.hasFocus();
-}
-
-/**
- * Show a browser notification for a new message
- */
-export function showMessageNotification(
- senderName: string,
- messageBody: string,
- roomName: string,
- options?: {
- onClick?: () => void;
- icon?: string;
- }
-): void {
- if (!canShowNotifications()) return;
- if (isDocumentFocused()) return;
-
- const settings = getNotificationSettings();
-
- const title = roomName ? `${senderName} in ${roomName}` : senderName;
- const body = settings.showPreview ? messageBody : 'Neue Nachricht';
-
- try {
- const notification = new Notification(title, {
- body: body.slice(0, 200), // Limit body length
- icon: options?.icon || '/favicon.png',
- tag: 'matrix-message', // Group notifications
- silent: !settings.sound,
- } as NotificationOptions);
-
- notification.onclick = () => {
- window.focus();
- notification.close();
- options?.onClick?.();
- };
-
- // Auto-close after 5 seconds
- setTimeout(() => {
- notification.close();
- }, 5000);
- } catch (e) {
- console.error('Failed to show notification:', e);
- }
-}
-
-/**
- * Play notification sound
- */
-export function playNotificationSound(): void {
- if (!browser) return;
-
- const settings = getNotificationSettings();
- if (!settings.sound) return;
-
- try {
- // Create a simple beep using Web Audio API
- const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
- const oscillator = audioContext.createOscillator();
- const gainNode = audioContext.createGain();
-
- oscillator.connect(gainNode);
- gainNode.connect(audioContext.destination);
-
- oscillator.frequency.value = 800;
- oscillator.type = 'sine';
-
- gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
- gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
-
- oscillator.start(audioContext.currentTime);
- oscillator.stop(audioContext.currentTime + 0.3);
- } catch (e) {
- // Ignore audio errors (common when user hasn't interacted yet)
- }
-}
diff --git a/apps/matrix/apps/web/src/lib/stores/navigation.svelte.ts b/apps/matrix/apps/web/src/lib/stores/navigation.svelte.ts
deleted file mode 100644
index 9603b24a1..000000000
--- a/apps/matrix/apps/web/src/lib/stores/navigation.svelte.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-// Local implementation of navigation stores
-// Previously imported from @mana/shared-stores, now inlined to avoid dependency issues
-
-import { browser } from '$app/environment';
-
-// Create reactive stores using Svelte 5 runes
-let _isNavCollapsed = $state(false);
-
-export const isNavCollapsed = {
- get value() {
- return _isNavCollapsed;
- },
- toggle() {
- _isNavCollapsed = !_isNavCollapsed;
- },
- set(value: boolean) {
- _isNavCollapsed = value;
- },
-};
diff --git a/apps/matrix/apps/web/src/lib/stores/tags.svelte.ts b/apps/matrix/apps/web/src/lib/stores/tags.svelte.ts
deleted file mode 100644
index ca1ed13fc..000000000
--- a/apps/matrix/apps/web/src/lib/stores/tags.svelte.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Tag Store - Uses shared createTagStore backed by central mana-auth
- *
- * Matrix uses its own auth (Matrix homeserver), not mana-auth directly.
- * The mana-auth token is obtained via session-to-token exchange and stored
- * in localStorage. Tags will work when user has a mana-auth session.
- */
-import { browser } from '$app/environment';
-import { createTagStore } from '@mana/shared-stores';
-import { loadStoredAccessToken } from '$lib/stores/userSettings.svelte';
-
-const AUTH_URL = import.meta.env.VITE_MANA_AUTH_URL || 'https://auth.mana.how';
-
-function getAuthUrl(): string {
- return AUTH_URL;
-}
-
-export const tagStore = createTagStore({
- authUrl: getAuthUrl(),
- getToken: () => {
- if (!browser) return null;
- return loadStoredAccessToken();
- },
-});
diff --git a/apps/matrix/apps/web/src/lib/stores/theme.ts b/apps/matrix/apps/web/src/lib/stores/theme.ts
deleted file mode 100644
index 0240fb257..000000000
--- a/apps/matrix/apps/web/src/lib/stores/theme.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { createThemeStore } from '@mana/shared-theme';
-
-export const theme = createThemeStore({
- appId: 'matrix',
- defaultVariant: 'lavender',
- primaryColor: {
- light: '270 70% 60%', // Purple/violet
- dark: '270 70% 60%',
- },
-});
diff --git a/apps/matrix/apps/web/src/lib/stores/userSettings.svelte.ts b/apps/matrix/apps/web/src/lib/stores/userSettings.svelte.ts
deleted file mode 100644
index cfde6047e..000000000
--- a/apps/matrix/apps/web/src/lib/stores/userSettings.svelte.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { createUserSettingsStore } from '@mana/shared-theme';
-import { browser } from '$app/environment';
-
-const AUTH_URL = import.meta.env.VITE_MANA_AUTH_URL || 'https://auth.mana.how';
-const TOKEN_STORAGE_KEY = 'mana_core_access_token';
-
-// Internal access token state
-let accessToken: string | null = null;
-
-/**
- * Set the access token (called after SSO token exchange)
- */
-export function setAccessToken(token: string): void {
- accessToken = token;
- if (browser) {
- try {
- localStorage.setItem(TOKEN_STORAGE_KEY, token);
- } catch {
- // Ignore storage errors
- }
- }
-}
-
-/**
- * Clear the access token (called on logout)
- */
-export function clearAccessToken(): void {
- accessToken = null;
- if (browser) {
- try {
- localStorage.removeItem(TOKEN_STORAGE_KEY);
- } catch {
- // Ignore storage errors
- }
- }
-}
-
-/**
- * Load access token from localStorage (for page reloads)
- */
-export function loadStoredAccessToken(): string | null {
- if (!browser) return null;
- try {
- const stored = localStorage.getItem(TOKEN_STORAGE_KEY);
- if (stored) {
- accessToken = stored;
- return stored;
- }
- } catch {
- // Ignore storage errors
- }
- return null;
-}
-
-/**
- * Get the current access token
- */
-async function getAccessToken(): Promise {
- // If we have a token in memory, return it
- if (accessToken) return accessToken;
-
- // Try to load from storage
- return loadStoredAccessToken();
-}
-
-/**
- * User settings store for the Matrix app
- *
- * This store syncs settings with mana-auth and provides:
- * - Global settings (including recentEmojis)
- * - localStorage caching for offline support
- */
-export const userSettings = createUserSettingsStore({
- appId: 'matrix',
- authUrl: AUTH_URL,
- getAccessToken,
-});
diff --git a/apps/matrix/apps/web/src/lib/version.ts b/apps/matrix/apps/web/src/lib/version.ts
deleted file mode 100644
index d63b4cfef..000000000
--- a/apps/matrix/apps/web/src/lib/version.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const APP_VERSION = '0.2.0';
-export const BUILD_TIME: string =
- typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : new Date().toISOString();
-export const BUILD_HASH: string = typeof __BUILD_HASH__ !== 'undefined' ? __BUILD_HASH__ : 'dev';
diff --git a/apps/matrix/apps/web/src/routes/(app)/+layout.svelte b/apps/matrix/apps/web/src/routes/(app)/+layout.svelte
deleted file mode 100644
index a13bafec7..000000000
--- a/apps/matrix/apps/web/src/routes/(app)/+layout.svelte
+++ /dev/null
@@ -1,586 +0,0 @@
-
-
-
-
-{#if loading}
-
-
-
-
-
Connecting to Matrix...
-
- {#if matrixStore.syncState === 'PREPARED'}
- Preparing sync...
- {:else if matrixStore.syncState === 'SYNCING'}
- Syncing messages...
- {:else if matrixStore.syncState === 'CATCHUP'}
- Catching up...
- {:else}
- Initializing...
- {/if}
-
-
-
-{:else if initError}
-
-
-
-
-
-
-
Connection Failed
-
{initError}
-
-
-
-
- Retry
-
-
- Sign Out
-
-
-
-{:else if matrixStore.isReady}
-
-
-
- {#if !isMobileRoomView}
-
- {/if}
-
-
- {#if isTagStripVisible && !isMobileRoomView}
-
({
- id: t.id,
- name: t.name,
- color: t.color || '#3b82f6',
- }))}
- selectedIds={[]}
- onToggle={() => {}}
- onClear={() => {}}
- managementHref="/tags"
- loading={tagStore.loading}
- />
- {/if}
-
-
-
- {@render children()}
-
-
-
- {#if showCommandPalette}
-
-
-
-
-
-
-
-
-
-
- ESC
-
-
-
- {#if commandQuery.trim()}
-
- {#if commandResults.length === 0}
-
- Keine Räume gefunden
-
- {:else}
- {#each commandResults as item, index (item.id)}
-
handleCommandSelect(item)}
- onmouseenter={() => (commandSelectedIndex = index)}
- >
-
- {item.title
- .split(' ')
- .map((w) => w[0])
- .join('')
- .substring(0, 2)
- .toUpperCase()}
-
-
-
{item.title}
- {#if item.subtitle}
-
{item.subtitle}
- {/if}
-
-
- {/each}
- {/if}
-
- {:else}
-
- Tippe um Räume und Kontakte zu finden
-
- {/if}
-
-
- {/if}
-
-
- {#if !isMobileRoomView}
-
- {/if}
-
-{:else}
-
-
-{/if}
-
-
diff --git a/apps/matrix/apps/web/src/routes/(app)/+layout.ts b/apps/matrix/apps/web/src/routes/(app)/+layout.ts
deleted file mode 100644
index 517d3e87a..000000000
--- a/apps/matrix/apps/web/src/routes/(app)/+layout.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-// Disable SSR for all (app) routes
-// matrix-js-sdk requires browser APIs and shared-ui uses Svelte 5 runes
-// that need client-side compilation
-export const ssr = false;
diff --git a/apps/matrix/apps/web/src/routes/(app)/bots/+page.svelte b/apps/matrix/apps/web/src/routes/(app)/bots/+page.svelte
deleted file mode 100644
index 53d620b44..000000000
--- a/apps/matrix/apps/web/src/routes/(app)/bots/+page.svelte
+++ /dev/null
@@ -1,158 +0,0 @@
-
-
-
- {$t('bots.title')} - Manalink
-
-
-
-
-
-
-
-
-
-
-
-
{$t('bots.title')}
-
{$t('bots.subtitle')}
-
-
-
-
-
-
-
-
-
-
-
- {#if filteredBots.length === 0}
-
-
-
{$t('bots.noResults')}
-
- {:else}
-
- {#each filteredBots as bot (bot.id)}
-
-
startChat(bot)} />
- {#if startingChat === bot.id}
-
-
-
- {/if}
-
- {/each}
-
- {/if}
-
-
-
- {filteredBots.length}
- {filteredBots.length === 1 ? 'Bot' : 'Bots'}
- {#if selectedCategory !== 'all' || search}
- {$t('bots.found')}
- {/if}
-
-
-
-
-
diff --git a/apps/matrix/apps/web/src/routes/(app)/chat/+page.svelte b/apps/matrix/apps/web/src/routes/(app)/chat/+page.svelte
deleted file mode 100644
index 81db66581..000000000
--- a/apps/matrix/apps/web/src/routes/(app)/chat/+page.svelte
+++ /dev/null
@@ -1,385 +0,0 @@
-
-
-{#if isMobile}
-
-
-
-
-
-
-
Manalink
-
-
- {matrixStore.syncState === 'SYNCING' ? 'Verbunden' : matrixStore.syncState}
-
- {#if matrixStore.totalUnreadCount > 0}
-
- {matrixStore.totalUnreadCount}
-
- {/if}
-
-
-
-
-
-
(showCreateRoom = true)}
- >
-
-
-
-
-
-
- {#if !matrixStore.cryptoReady}
-
-
- Verschlüsselung nicht verfügbar
-
- {/if}
-
-
-
- (showCreateRoom = true)} onSelectRoom={handleSelectRoom} />
-
-
-{:else}
-
-
-
-
-
-
-
-
{matrixStore.userId}
-
-
-
-
-
(showCreateRoom = true)}
- >
-
-
-
-
-
-
- {matrixStore.syncState === 'SYNCING' ? 'Verbunden' : matrixStore.syncState}
- {#if matrixStore.totalUnreadCount > 0}
-
- {matrixStore.totalUnreadCount}
-
- {/if}
-
-
-
- {#if !matrixStore.cryptoReady}
-
-
- Verschlüsselung nicht verfügbar
-
- {/if}
-
-
-
- (showCreateRoom = true)} onSelectRoom={handleSelectRoom} />
-
-
-
-
-
-
-
-
- {#if matrixStore.currentRoom}
-
- (showRoomSettings = true)}
- onSearchClick={() => (showSearch = true)}
- onVoiceCall={handleVoiceCall}
- onVideoCall={handleVideoCall}
- />
-
-
-
-
-
- (replyTo = null)}
- onCancelEdit={() => (editMessage = null)}
- />
- {:else}
-
-
-
-
-
-
-
Willkommen bei Manalink
-
- Wähle eine Unterhaltung aus der Seitenleiste oder starte einen neuen Chat
-
-
-
-
(showCreateRoom = true)}
- >
-
- Neuer Chat
-
-
-
-
-
-
{matrixStore.rooms.length}
-
Räume
-
-
-
{matrixStore.totalUnreadCount}
-
Ungelesen
-
-
-
- {/if}
-
-
-
-
(showRoomSettings = false)} />
-
-{/if}
-
-
- (showCreateRoom = false)}
- onCreated={handleRoomCreated}
-/>
-
-
- (showSearch = false)} />
-
-
-{#if activeCall}
-
-{/if}
-
-
-{#if incomingCall && !activeCall}
-
-{/if}
-
-
- {
- showForward = false;
- forwardMessage = null;
- }}
-/>
diff --git a/apps/matrix/apps/web/src/routes/(app)/chat/[roomId]/+page.svelte b/apps/matrix/apps/web/src/routes/(app)/chat/[roomId]/+page.svelte
deleted file mode 100644
index db1da1aa1..000000000
--- a/apps/matrix/apps/web/src/routes/(app)/chat/[roomId]/+page.svelte
+++ /dev/null
@@ -1,315 +0,0 @@
-
-
-
-
-
-
-
-
-
- {#if isSwiping && swipeProgress > 0}
-
-
- {/if}
- {#if matrixStore.currentRoom}
-
-
(showRoomSettings = true)}
- onSearchClick={() => (showSearch = true)}
- onVoiceCall={handleVoiceCall}
- onVideoCall={handleVideoCall}
- />
-
-
-
-
-
- (replyTo = null)}
- onCancelEdit={() => (editMessage = null)}
- />
- {:else}
-
-
- {/if}
-
-
-
- (showRoomSettings = false)} />
-
-
- (showSearch = false)} />
-
-
-{#if activeCall}
-
-{/if}
-
-
-{#if incomingCall && !activeCall}
-
-{/if}
-
-
- {
- showForward = false;
- forwardMessage = null;
- }}
-/>
diff --git a/apps/matrix/apps/web/src/routes/(app)/feedback/+page.svelte b/apps/matrix/apps/web/src/routes/(app)/feedback/+page.svelte
deleted file mode 100644
index 7c95d3ef4..000000000
--- a/apps/matrix/apps/web/src/routes/(app)/feedback/+page.svelte
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
diff --git a/apps/matrix/apps/web/src/routes/(app)/help/+page.svelte b/apps/matrix/apps/web/src/routes/(app)/help/+page.svelte
deleted file mode 100644
index 6906477fa..000000000
--- a/apps/matrix/apps/web/src/routes/(app)/help/+page.svelte
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
- {translations.title} | Manalink
-
-
- goto('/')}
- showGettingStarted={false}
- showChangelog={false}
- defaultSection="faq"
-/>
diff --git a/apps/matrix/apps/web/src/routes/(app)/settings/+page.svelte b/apps/matrix/apps/web/src/routes/(app)/settings/+page.svelte
deleted file mode 100644
index 103bb7f15..000000000
--- a/apps/matrix/apps/web/src/routes/(app)/settings/+page.svelte
+++ /dev/null
@@ -1,410 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Profil
-
-
-
-
- {matrixStore.userId?.charAt(1).toUpperCase() || '?'}
-
-
-
-
{matrixStore.userId}
-
Matrix ID
-
-
-
-
-
-
-
-
-
-
- Server
-
-
-
- Homeserver
- {matrixStore.client?.getHomeserverUrl() || 'Unbekannt'}
-
-
- Sync Status
-
- {matrixStore.syncState}
-
-
-
- Räume
- {matrixStore.rooms.length}
-
-
-
-
-
-
-
-
-
-
- Sicherheit & Verschlüsselung
-
-
- {#if !cryptoReady}
-
-
- Verschlüsselung wird initialisiert...
-
- {:else}
-
-
-
-
- {#if verificationStatus === 'verified'}
-
- {:else}
-
- {/if}
-
-
- {verificationStatus === 'verified' ? 'Verifiziert' : 'Nicht verifiziert'}
-
-
- {verificationStatus === 'verified'
- ? 'Dein Gerät ist verifiziert'
- : 'Verifiziere dein Gerät für bessere Sicherheit'}
-
-
-
-
(verificationDialogOpen = true)}
- >
-
- Geräte
-
-
-
-
-
-
- Geräte-ID
- {deviceId || 'Unbekannt'}
-
-
-
-
-
-
-
-
-
- {keyBackupEnabled ? 'Schlüssel-Backup aktiv' : 'Kein Schlüssel-Backup'}
-
-
- {keyBackupEnabled
- ? 'Deine Nachrichten werden gesichert'
- : 'Richte ein Backup ein, um Nachrichten wiederherzustellen'}
-
-
-
- {#if keyBackupEnabled}
-
openRecoveryDialog('restore')}>
- Wiederherstellen
-
- {:else}
-
openRecoveryDialog('setup')}>
- Einrichten
-
- {/if}
-
-
- {/if}
-
-
-
-
-
-
-
-
- Erscheinungsbild
-
-
-
-
Wähle dein bevorzugtes Farbschema
-
-
-
- theme.setMode('light')}
- >
-
- Hell
-
-
- theme.setMode('dark')}
- >
-
- Dunkel
-
-
- theme.setMode('system')}
- >
-
- System
-
-
-
-
-
- {#if theme.mode === 'system'}
- Aktuell: {theme.isDark ? 'Dunkel' : 'Hell'} (basierend auf System)
- {:else}
- Aktuell: {theme.isDark ? 'Dunkel' : 'Hell'}
- {/if}
-
-
-
-
-
-
-
-
-
-
- Benachrichtigungen
-
-
- {#if !notificationsSupported}
-
- Dein Browser unterstützt keine Benachrichtigungen.
-
- {:else if notificationPermission === 'denied'}
-
-
Benachrichtigungen blockiert
-
- Du hast Benachrichtigungen für diese Seite blockiert. Bitte ändere die Einstellung
- in deinem Browser.
-
-
- {:else if notificationPermission === 'default'}
-
-
-
-
-
Benachrichtigungen aktivieren
-
- Erhalte Benachrichtigungen für neue Nachrichten
-
-
-
-
- Erlauben
-
-
- {:else}
-
-
-
-
-
-
-
Benachrichtigungen
-
- Desktop-Benachrichtigungen für neue Nachrichten
-
-
-
-
- updateNotificationSetting('enabled', !notificationSettings.enabled)}
- />
-
-
-
-
-
-
-
-
Ton
-
Ton bei neuen Nachrichten abspielen
-
-
- updateNotificationSetting('sound', !notificationSettings.sound)}
- />
-
-
-
-
-
-
-
-
Vorschau anzeigen
-
- Nachrichteninhalt in Benachrichtigung anzeigen
-
-
-
-
- updateNotificationSetting('showPreview', !notificationSettings.showPreview)}
- />
-
-
- {/if}
-
-
-
-
-
-
-
-
v{APP_VERSION}
-
-
-
-
- (verificationDialogOpen = false)}
-/>
- (recoveryDialogOpen = false)}
-/>
diff --git a/apps/matrix/apps/web/src/routes/(app)/tags/+page.svelte b/apps/matrix/apps/web/src/routes/(app)/tags/+page.svelte
deleted file mode 100644
index a137c3930..000000000
--- a/apps/matrix/apps/web/src/routes/(app)/tags/+page.svelte
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
- Tags | Manalink
-
-
-
-
Tags verwalten
-
- Tags sind app-übergreifend — Änderungen gelten in allen Mana-Apps.
-
-
- {#if tagStore.loading}
-
Lädt...
- {:else if tagStore.tags.length === 0}
-
Keine Tags vorhanden.
- {:else}
-
- {#each tagStore.tags as tag}
-
-
- {tag.name}
-
- {/each}
-
- {/if}
-
-
-
diff --git a/apps/matrix/apps/web/src/routes/(auth)/+layout.svelte b/apps/matrix/apps/web/src/routes/(auth)/+layout.svelte
deleted file mode 100644
index 9091dc9bf..000000000
--- a/apps/matrix/apps/web/src/routes/(auth)/+layout.svelte
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
- {@render children()}
-
-
-
diff --git a/apps/matrix/apps/web/src/routes/(auth)/login/+page.svelte b/apps/matrix/apps/web/src/routes/(auth)/login/+page.svelte
deleted file mode 100644
index e6d515f4b..000000000
--- a/apps/matrix/apps/web/src/routes/(auth)/login/+page.svelte
+++ /dev/null
@@ -1,849 +0,0 @@
-
-
-
- Login - Manalink
-
-
-
-
-
- {#if isDark}
-
- {:else}
-
- {/if}
-
-
-
-
-
-
- {#if showSuccess}
-
- {:else}
-
- {/if}
-
-
Manalink
-
Sichere Matrix-Kommunikation
-
-
-
-
-
-
-
-
diff --git a/apps/matrix/apps/web/src/routes/+error.svelte b/apps/matrix/apps/web/src/routes/+error.svelte
deleted file mode 100644
index f64842b92..000000000
--- a/apps/matrix/apps/web/src/routes/+error.svelte
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
- {$page.status} - Manalink
-
-
-
-
-
-
-
-
-
{$page.status}
-
- {#if $page.status === 404}
- Diese Seite wurde nicht gefunden.
- {:else}
- Ein unerwarteter Fehler ist aufgetreten.
- {/if}
-
- {#if $page.error?.message}
-
{$page.error.message}
- {/if}
-
-
-
-
history.back()}
- >
-
- Zurück
-
-
goto('/chat')}
- >
-
- Startseite
-
-
-
diff --git a/apps/matrix/apps/web/src/routes/+layout.svelte b/apps/matrix/apps/web/src/routes/+layout.svelte
deleted file mode 100644
index c4ec76918..000000000
--- a/apps/matrix/apps/web/src/routes/+layout.svelte
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
- {$i18nLoading ? 'Matrix Chat' : $t('app.name')}
-
-
-
-{#if $i18nLoading}
-
-
-{:else}
-
- {@render children()}
-
-
-
-
-{/if}
diff --git a/apps/matrix/apps/web/src/routes/+page.svelte b/apps/matrix/apps/web/src/routes/+page.svelte
deleted file mode 100644
index ac828c7d4..000000000
--- a/apps/matrix/apps/web/src/routes/+page.svelte
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
diff --git a/apps/matrix/apps/web/src/routes/health/+server.ts b/apps/matrix/apps/web/src/routes/health/+server.ts
deleted file mode 100644
index 0b1fdd950..000000000
--- a/apps/matrix/apps/web/src/routes/health/+server.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { RequestHandler } from '@sveltejs/kit';
-
-export const GET: RequestHandler = async () => {
- return new Response(
- JSON.stringify({
- status: 'ok',
- timestamp: new Date().toISOString(),
- service: 'matrix-web',
- }),
- {
- status: 200,
- headers: {
- 'Content-Type': 'application/json',
- },
- }
- );
-};
diff --git a/apps/matrix/apps/web/src/routes/offline/+page.svelte b/apps/matrix/apps/web/src/routes/offline/+page.svelte
deleted file mode 100644
index f10f1803f..000000000
--- a/apps/matrix/apps/web/src/routes/offline/+page.svelte
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/apps/matrix/apps/web/src/routes/offline/+page.ts b/apps/matrix/apps/web/src/routes/offline/+page.ts
deleted file mode 100644
index 189f71e2e..000000000
--- a/apps/matrix/apps/web/src/routes/offline/+page.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const prerender = true;
diff --git a/apps/matrix/apps/web/src/test/mocks/app-environment.ts b/apps/matrix/apps/web/src/test/mocks/app-environment.ts
deleted file mode 100644
index 7e7ecc8aa..000000000
--- a/apps/matrix/apps/web/src/test/mocks/app-environment.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export const browser = false;
-export const building = false;
-export const dev = true;
-export const version = 'test';
diff --git a/apps/matrix/apps/web/static/apple-touch-icon.png b/apps/matrix/apps/web/static/apple-touch-icon.png
deleted file mode 100644
index a56722250..000000000
Binary files a/apps/matrix/apps/web/static/apple-touch-icon.png and /dev/null differ
diff --git a/apps/matrix/apps/web/static/favicon.png b/apps/matrix/apps/web/static/favicon.png
deleted file mode 100644
index c352c323c..000000000
Binary files a/apps/matrix/apps/web/static/favicon.png and /dev/null differ
diff --git a/apps/matrix/apps/web/static/favicon.svg b/apps/matrix/apps/web/static/favicon.svg
deleted file mode 100644
index 07d21b48e..000000000
--- a/apps/matrix/apps/web/static/favicon.svg
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/matrix/apps/web/static/pwa-192x192.png b/apps/matrix/apps/web/static/pwa-192x192.png
deleted file mode 100644
index e69962d63..000000000
Binary files a/apps/matrix/apps/web/static/pwa-192x192.png and /dev/null differ
diff --git a/apps/matrix/apps/web/static/pwa-512x512.png b/apps/matrix/apps/web/static/pwa-512x512.png
deleted file mode 100644
index 1d596ae7c..000000000
Binary files a/apps/matrix/apps/web/static/pwa-512x512.png and /dev/null differ
diff --git a/apps/matrix/apps/web/svelte.config.js b/apps/matrix/apps/web/svelte.config.js
deleted file mode 100644
index a7a917e4c..000000000
--- a/apps/matrix/apps/web/svelte.config.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import adapter from '@sveltejs/adapter-node';
-import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
-
-/** @type {import('@sveltejs/kit').Config} */
-const config = {
- preprocess: vitePreprocess(),
- kit: {
- adapter: adapter({
- out: 'build',
- }),
- },
-};
-
-export default config;
diff --git a/apps/matrix/apps/web/tsconfig.json b/apps/matrix/apps/web/tsconfig.json
deleted file mode 100644
index 942bcc11c..000000000
--- a/apps/matrix/apps/web/tsconfig.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "extends": "./.svelte-kit/tsconfig.json",
- "compilerOptions": {
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
- "module": "ES2022",
- "target": "ES2022",
- "moduleResolution": "bundler",
- "strict": true,
- "skipLibCheck": true,
- "esModuleInterop": true,
- "resolveJsonModule": true,
- "declaration": true,
- "declarationMap": true,
- "sourceMap": true,
- "allowSyntheticDefaultImports": true,
- "types": ["node"]
- },
- "include": ["src/**/*", "src/**/*.svelte"]
-}
diff --git a/apps/matrix/apps/web/vite.config.ts b/apps/matrix/apps/web/vite.config.ts
deleted file mode 100644
index 2112353f6..000000000
--- a/apps/matrix/apps/web/vite.config.ts
+++ /dev/null
@@ -1,188 +0,0 @@
-import { sveltekit } from '@sveltejs/kit/vite';
-import tailwindcss from '@tailwindcss/vite';
-import { defineConfig, type UserConfig } from 'vite';
-import { SvelteKitPWA } from '@vite-pwa/sveltekit';
-
-// Mana shared packages that need SSR configuration
-const MANA_SHARED_PACKAGES = [
- '@mana/shared-icons',
- '@mana/shared-ui',
- '@mana/shared-tailwind',
- '@mana/shared-theme',
- '@mana/shared-theme-ui',
- '@mana/feedback',
- '@mana/feedback',
- '@mana/feedback',
- '@mana/shared-auth',
- '@mana/shared-auth-ui',
- '@mana/shared-branding',
- '@mana/subscriptions',
- '@mana/shared-profile-ui',
- '@mana/shared-i18n',
- '@mana/shared-api-client',
- '@mana/shared-splitscreen',
- '@mana/shared-utils',
- '@mana/shared-tags',
- '@mana/help',
- '@mana/help',
- '@mana/help',
-];
-
-const noExternal = [...MANA_SHARED_PACKAGES, '@matrix/shared'];
-const exclude = [...MANA_SHARED_PACKAGES];
-
-const baseConfig: Partial = {
- server: {
- port: 5180,
- strictPort: true,
- },
- ssr: {
- noExternal,
- },
- optimizeDeps: {
- exclude,
- },
-};
-
-export default defineConfig({
- ...baseConfig,
- plugins: [
- tailwindcss(),
- sveltekit(),
- SvelteKitPWA({
- srcDir: 'src',
- registerType: 'autoUpdate',
- strategies: 'generateSW',
- scope: '/',
- base: '/',
- manifest: {
- name: 'Manalink',
- short_name: 'Manalink',
- description: 'Secure Matrix messaging client',
- theme_color: '#8b5cf6',
- background_color: '#09090b',
- display: 'standalone',
- orientation: 'portrait',
- start_url: '/',
- scope: '/',
- categories: ['communication', 'social'],
- icons: [
- {
- src: 'pwa-192x192.png',
- sizes: '192x192',
- type: 'image/png',
- },
- {
- src: 'pwa-512x512.png',
- sizes: '512x512',
- type: 'image/png',
- },
- {
- src: 'pwa-512x512.png',
- sizes: '512x512',
- type: 'image/png',
- purpose: 'maskable',
- },
- ],
- shortcuts: [
- {
- name: 'New Chat',
- short_name: 'New Chat',
- url: '/chat?action=new',
- icons: [{ src: 'pwa-192x192.png', sizes: '192x192' }],
- },
- ],
- },
- injectManifest: {
- globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2}'],
- },
- workbox: {
- globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2}'],
- cleanupOutdatedCaches: true,
- clientsClaim: true,
- skipWaiting: true,
- // Cache strategies
- runtimeCaching: [
- {
- // Cache Matrix API responses (short TTL)
- urlPattern: /^https:\/\/matrix\.mana\.how\/_matrix\/.*/i,
- handler: 'NetworkFirst',
- options: {
- cacheName: 'matrix-api-cache',
- expiration: {
- maxEntries: 100,
- maxAgeSeconds: 60 * 5, // 5 minutes
- },
- cacheableResponse: {
- statuses: [0, 200],
- },
- },
- },
- {
- // Cache images and avatars
- urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/i,
- handler: 'CacheFirst',
- options: {
- cacheName: 'image-cache',
- expiration: {
- maxEntries: 200,
- maxAgeSeconds: 60 * 60 * 24 * 30, // 30 days
- },
- },
- },
- {
- // Cache fonts
- urlPattern: /\.(?:woff|woff2|ttf|otf)$/i,
- handler: 'CacheFirst',
- options: {
- cacheName: 'font-cache',
- expiration: {
- maxEntries: 20,
- maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
- },
- },
- },
- ],
- },
- devOptions: {
- enabled: process.env.NODE_ENV !== 'production',
- type: 'module',
- navigateFallback: '/',
- },
- kit: {
- includeVersionFile: true,
- },
- }),
- ],
- server: {
- ...baseConfig.server,
- headers: {
- // Required for WASM module loading
- 'Cross-Origin-Opener-Policy': 'same-origin',
- 'Cross-Origin-Embedder-Policy': 'require-corp',
- },
- },
- ssr: {
- ...baseConfig.ssr,
- },
- define: {
- global: 'globalThis',
- },
- optimizeDeps: {
- ...baseConfig.optimizeDeps,
- include: ['buffer', 'events'],
- // WASM modules cannot be pre-bundled
- exclude: [...exclude, '@matrix-org/matrix-sdk-crypto-wasm'],
- esbuildOptions: {
- define: {
- global: 'globalThis',
- },
- },
- },
- worker: {
- format: 'es',
- },
- build: {
- target: 'esnext',
- },
-});
diff --git a/apps/matrix/apps/web/vitest.config.ts b/apps/matrix/apps/web/vitest.config.ts
deleted file mode 100644
index 942d7fd60..000000000
--- a/apps/matrix/apps/web/vitest.config.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { defineConfig } from 'vitest/config';
-import { resolve } from 'path';
-
-export default defineConfig({
- test: {
- include: ['src/**/*.{test,spec}.{js,ts}'],
- globals: true,
- environment: 'node',
- },
- resolve: {
- alias: {
- $lib: resolve('./src/lib'),
- '$app/environment': resolve('./src/test/mocks/app-environment.ts'),
- },
- },
-});
diff --git a/apps/matrix/package.json b/apps/matrix/package.json
deleted file mode 100644
index 2f824a585..000000000
--- a/apps/matrix/package.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "name": "matrix",
- "version": "0.2.0",
- "private": true,
- "scripts": {
- "dev": "turbo run dev",
- "dev:web": "pnpm --filter @matrix/web dev",
- "dev:mobile": "pnpm --filter @matrix/mobile dev"
- }
-}
diff --git a/apps/matrix/packages/shared/package.json b/apps/matrix/packages/shared/package.json
deleted file mode 100644
index 44bd1ac34..000000000
--- a/apps/matrix/packages/shared/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "@matrix/shared",
- "version": "0.2.0",
- "private": true,
- "type": "module",
- "main": "./src/index.ts",
- "types": "./src/index.ts",
- "exports": {
- ".": "./src/index.ts",
- "./types": "./src/types.ts"
- },
- "scripts": {
- "type-check": "tsc --noEmit"
- },
- "devDependencies": {
- "typescript": "^5.8.3"
- }
-}
diff --git a/apps/matrix/packages/shared/src/index.ts b/apps/matrix/packages/shared/src/index.ts
deleted file mode 100644
index a5980435f..000000000
--- a/apps/matrix/packages/shared/src/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-// Re-export all types
-export * from './types';
diff --git a/apps/matrix/packages/shared/src/types.ts b/apps/matrix/packages/shared/src/types.ts
deleted file mode 100644
index 66f9e5942..000000000
--- a/apps/matrix/packages/shared/src/types.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Shared types for Matrix client
- */
-
-export type SyncState = 'STOPPED' | 'PREPARED' | 'SYNCING' | 'ERROR' | 'RECONNECTING' | 'CATCHUP';
-
-export interface MatrixCredentials {
- homeserver: string;
- accessToken: string;
- userId: string;
- deviceId: string;
-}
-
-export type MessageType = 'm.text' | 'm.image' | 'm.file' | 'm.audio' | 'm.video' | 'm.emote' | 'm.notice';
-
-export interface SimpleMessage {
- id: string;
- sender: string;
- senderName: string;
- body: string;
- formattedBody?: string;
- timestamp: number;
- type: MessageType;
- isOwn: boolean;
- replyTo?: string;
- edited?: boolean;
-}
-
-export interface SimpleRoom {
- id: string;
- name: string;
- topic?: string;
- avatar?: string;
- lastMessage?: string;
- lastMessageSender?: string;
- lastMessageTime?: number;
- unreadCount: number;
- highlightCount: number;
- isDirect: boolean;
- isEncrypted: boolean;
- memberCount: number;
-}
diff --git a/apps/matrix/packages/shared/tsconfig.json b/apps/matrix/packages/shared/tsconfig.json
deleted file mode 100644
index 882ac919d..000000000
--- a/apps/matrix/packages/shared/tsconfig.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "compilerOptions": {
- "lib": ["ES2022"],
- "module": "ES2022",
- "target": "ES2022",
- "moduleResolution": "bundler",
- "strict": true,
- "skipLibCheck": true,
- "esModuleInterop": true,
- "resolveJsonModule": true,
- "declaration": true,
- "declarationMap": true,
- "sourceMap": true
- },
- "include": ["src/**/*"]
-}
diff --git a/cloudflared-config.yml b/cloudflared-config.yml
index a2a71cf6d..acef7bf2a 100644
--- a/cloudflared-config.yml
+++ b/cloudflared-config.yml
@@ -43,14 +43,6 @@ ingress:
- hostname: stats.mana.how
service: http://localhost:8010
- # Matrix (DSGVO-konformes Messaging)
- - hostname: matrix.mana.how
- service: http://localhost:4000
- - hostname: element.mana.how
- service: http://localhost:4010
- - hostname: link.mana.how
- service: http://localhost:4090
-
# GlitchTip Error Tracking
- hostname: glitchtip.mana.how
service: http://localhost:8020
diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml
index 2ed8479ca..41d8192db 100644
--- a/docker-compose.macmini.yml
+++ b/docker-compose.macmini.yml
@@ -579,8 +579,6 @@ services:
SMTP_FROM: "Mana "
SMTP_INSECURE_TLS: "true"
EXPO_ACCESS_TOKEN: ${EXPO_ACCESS_TOKEN:-}
- MATRIX_HOMESERVER_URL: http://mana-matrix-synapse:8008
- MATRIX_ACCESS_TOKEN: ${MATRIX_NOTIFY_BOT_TOKEN:-}
ports:
- "3013:3013"
healthcheck:
@@ -671,7 +669,6 @@ services:
S3_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin}
S3_BUCKET: mana-media
S3_PUBLIC_URL: https://media.mana.how
- MATRIX_HOMESERVER_URL: https://matrix.mana.how
PUBLIC_URL: https://media.mana.how/api/v1
CORS_ORIGINS: https://mana.how,https://nutriphi.mana.how,https://contacts.mana.how,https://chat.mana.how,https://storage.mana.how,https://photos.mana.how
ports:
@@ -728,173 +725,6 @@ services:
# Requires: Dockerfile.bun in apps/ root (FROM oven/bun:1, COPY, CMD bun run src/index.ts)
- # ============================================
- # Tier 4: Matrix Stack (Ports 4000-4099)
- # ============================================
-
- synapse:
- image: matrixdotorg/synapse:latest
- container_name: mana-matrix-synapse
- restart: always
- mem_limit: 512m
- depends_on:
- postgres:
- condition: service_healthy
- entrypoint: ["sh", "-c", "mkdir -p /config && cp /mnt/synapse-config/*.yaml /config/ 2>/dev/null; cp -r /mnt/synapse-config/appservices /config/ 2>/dev/null; exec python -m synapse.app.homeserver -c /config/homeserver.yaml"]
- environment:
- TZ: Europe/Berlin
- SYNAPSE_DB_PASSWORD: ${SYNAPSE_DB_PASSWORD:-synapse-secure-password}
- SYNAPSE_PASSWORD_PEPPER: ${SYNAPSE_PASSWORD_PEPPER:-change-me-pepper}
- SYNAPSE_FORM_SECRET: ${SYNAPSE_FORM_SECRET:-change-me-form-secret}
- SYNAPSE_MACAROON_SECRET: ${SYNAPSE_MACAROON_SECRET:-change-me-macaroon-secret}
- SYNAPSE_REGISTRATION_SECRET: ${SYNAPSE_REGISTRATION_SECRET:-change-me-registration-secret}
- SYNAPSE_OIDC_CLIENT_SECRET: ${SYNAPSE_OIDC_CLIENT_SECRET}
- volumes:
- - ./docker/matrix/config:/mnt/synapse-config:ro
- - ./docker/matrix/data:/data
- ports:
- - "4000:8008"
- - "9002:9002" # Metrics
- healthcheck:
- test: ["CMD", "curl", "-fSs", "http://localhost:8008/health"]
- interval: 120s
- timeout: 10s
- retries: 3
- start_period: 60s
-
- element-web:
- image: vectorim/element-web:latest
- container_name: mana-matrix-element
- restart: always
- mem_limit: 48m
- depends_on:
- synapse:
- condition: service_healthy
- volumes:
- - ./docker/matrix/element-config.json:/app/config.json:ro
- ports:
- - "4010:80"
- healthcheck:
- test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/"]
- interval: 180s
- timeout: 10s
- retries: 3
- start_period: 45s
-
- matrix-web:
- build:
- context: .
- dockerfile: apps/matrix/apps/web/Dockerfile
- image: matrix-web:latest
- container_name: mana-matrix-web
- restart: always
- mem_limit: 96m
- depends_on:
- synapse:
- condition: service_healthy
- environment:
- NODE_ENV: production
- PORT: 4011
- PUBLIC_MANA_AUTH_URL: https://auth.mana.how
- ports:
- - "4011:4011"
- healthcheck:
- test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4011/health"]
- interval: 180s
- timeout: 10s
- retries: 3
- start_period: 45s
-
- # ============================================
- # Matrix Bots — Consolidated Go Service
- # Replaces 21 separate NestJS bot containers
- # ============================================
-
- mana-matrix-bot:
- build:
- context: .
- dockerfile: services/mana-matrix-bot/Dockerfile
- image: mana-matrix-bot:local
- container_name: mana-matrix-bot
- restart: always
- mem_limit: 128m
- depends_on:
- synapse:
- condition: service_healthy
- redis:
- condition: service_healthy
- environment:
- TZ: Europe/Berlin
- PORT: 4001
- # Matrix
- MATRIX_HOMESERVER_URL: http://synapse:8008
- MATRIX_STORAGE_PATH: /app/data
- # Auth & Redis
- MANA_AUTH_URL: http://mana-auth:3001
- MANA_SERVICE_KEY: ${MANA_SERVICE_KEY}
- REDIS_HOST: redis
- REDIS_PORT: 6379
- REDIS_PASSWORD: ${REDIS_PASSWORD:-redis123}
- # Voice services (GPU server via LAN)
- STT_URL: ${STT_SERVICE_URL:-http://192.168.178.11:3020}
- TTS_URL: ${TTS_SERVICE_URL:-http://192.168.178.11:3022}
- # AI (GPU server via LAN)
- OLLAMA_URL: ${OLLAMA_URL:-http://192.168.178.11:11434}
- OLLAMA_MODEL: ${OLLAMA_MODEL:-gemma3:12b}
- # Plugin tokens (all 21 bot identities)
- MATRIX_MANA_BOT_TOKEN: ${MATRIX_MANA_BOT_TOKEN}
- MATRIX_MANA_BOT_ROOMS: ${MATRIX_MANA_BOT_ROOMS:-}
- MATRIX_TODO_BOT_TOKEN: ${MATRIX_TODO_BOT_TOKEN}
- MATRIX_TODO_BOT_ROOMS: ${MATRIX_TODO_BOT_ROOMS:-}
- MATRIX_CALENDAR_BOT_TOKEN: ${MATRIX_CALENDAR_BOT_TOKEN}
- MATRIX_CALENDAR_BOT_ROOMS: ${MATRIX_CALENDAR_BOT_ROOMS:-}
- MATRIX_CLOCK_BOT_TOKEN: ${MATRIX_CLOCK_BOT_TOKEN}
- MATRIX_CLOCK_BOT_ROOMS: ${MATRIX_CLOCK_BOT_ROOMS:-}
- MATRIX_OLLAMA_BOT_TOKEN: ${MATRIX_OLLAMA_BOT_TOKEN}
- MATRIX_OLLAMA_BOT_ROOMS: ${MATRIX_OLLAMA_BOT_ROOMS:-}
- MATRIX_STATS_BOT_TOKEN: ${MATRIX_STATS_BOT_TOKEN}
- MATRIX_STATS_BOT_ROOMS: ${MATRIX_STATS_BOT_ROOMS:-}
- MATRIX_CONTACTS_BOT_TOKEN: ${MATRIX_CONTACTS_BOT_TOKEN:-}
- MATRIX_CONTACTS_BOT_ROOMS: ${MATRIX_CONTACTS_BOT_ROOMS:-}
- MATRIX_CHAT_BOT_TOKEN: ${MATRIX_CHAT_BOT_TOKEN:-}
- MATRIX_CARDS_BOT_TOKEN: ${MATRIX_CARDS_BOT_TOKEN:-}
- MATRIX_NUTRIPHI_BOT_TOKEN: ${MATRIX_NUTRIPHI_BOT_TOKEN}
- MATRIX_NUTRIPHI_BOT_ROOMS: ${MATRIX_NUTRIPHI_BOT_ROOMS:-}
- MATRIX_PICTURE_BOT_TOKEN: ${MATRIX_PICTURE_BOT_TOKEN:-}
- MATRIX_PLANTA_BOT_TOKEN: ${MATRIX_PLANTA_BOT_TOKEN}
- MATRIX_PLANTA_BOT_ROOMS: ${MATRIX_PLANTA_BOT_ROOMS:-}
- MATRIX_PRESI_BOT_TOKEN: ${MATRIX_PRESI_BOT_TOKEN:-}
- MATRIX_QUESTIONS_BOT_TOKEN: ${MATRIX_QUESTIONS_BOT_TOKEN:-}
- MATRIX_SKILLTREE_BOT_TOKEN: ${MATRIX_SKILLTREE_BOT_TOKEN:-}
- MATRIX_STORAGE_BOT_TOKEN: ${MATRIX_STORAGE_BOT_TOKEN:-}
- MATRIX_PROJECT_DOC_BOT_TOKEN: ${MATRIX_PROJECT_DOC_BOT_TOKEN}
- MATRIX_STT_BOT_TOKEN: ${MATRIX_STT_BOT_TOKEN}
- MATRIX_STT_BOT_ROOMS: ${MATRIX_STT_BOT_ROOMS:-}
- MATRIX_TTS_BOT_TOKEN: ${MATRIX_TTS_BOT_TOKEN}
- MATRIX_TTS_BOT_ROOMS: ${MATRIX_TTS_BOT_ROOMS:-}
- MATRIX_ZITARE_BOT_TOKEN: ${MATRIX_ZITARE_BOT_TOKEN}
- MATRIX_ZITARE_BOT_ROOMS: ${MATRIX_ZITARE_BOT_ROOMS:-}
- MATRIX_ONBOARDING_BOT_TOKEN: ${MATRIX_ONBOARDING_BOT_TOKEN}
- MATRIX_ONBOARDING_BOT_ROOMS: ${MATRIX_ONBOARDING_BOT_ROOMS:-}
- # Backend URLs
- TODO_BACKEND_URL: http://todo-backend:3031
- CALENDAR_BACKEND_URL: http://calendar-backend:3032
- # CLOCK_BACKEND_URL: removed — migrated to local-first
- CONTACTS_BACKEND_URL: http://contacts-backend:3033
- # ZITARE_BACKEND_URL: removed — migrated to local-first
- PLANTA_BACKEND_URL: http://planta-backend:3039
- NUTRIPHI_BACKEND_URL: http://nutriphi-backend:3038
- STORAGE_BACKEND_URL: http://storage-backend:3034
- volumes:
- - matrix_bots_data:/app/data
- # No host port mapping needed — only communicates with synapse internally
- healthcheck:
- test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:4001/health"]
- interval: 60s
- timeout: 5s
- retries: 3
- start_period: 10s
-
# ============================================
# Tier 5: Web Frontends (Ports 5000-5099)
# ============================================
@@ -1617,7 +1447,5 @@ volumes:
name: mana-analytics-data
loki_data:
name: mana-loki-data
- matrix_bots_data:
- name: mana-matrix-bots-data
stalwart_data:
name: mana-stalwart-data
diff --git a/docker/matrix/appservices/generate-as.sh b/docker/matrix/appservices/generate-as.sh
deleted file mode 100644
index 425f8ca09..000000000
--- a/docker/matrix/appservices/generate-as.sh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/bash
-
-# Generate random token
-gen_token() {
- openssl rand -hex 32
-}
-
-# Bot configurations: name, sender_localpart
-declare -a BOTS=(
- "mana:mana-bot"
- "ollama:ollama-bot"
- "stats:stats-bot"
- "projectdoc:projectdoc-bot"
- "todo:todo-bot"
- "calendar:calendar-bot"
- "nutriphi:nutriphi-bot"
- "zitare:zitare-bot"
- "clock:clock-bot"
- "tts:tts-bot"
-)
-
-echo "# Generated AS tokens for .env file:" > as-tokens.env
-echo "" >> as-tokens.env
-
-for bot_config in "${BOTS[@]}"; do
- IFS=":" read -r name sender <<< "$bot_config"
-
- as_token=$(gen_token)
- hs_token=$(gen_token)
-
- cat > "${name}-bot.yaml" << EOF
-id: ${name}-bot
-hs_token: ${hs_token}
-as_token: ${as_token}
-url: null
-sender_localpart: ${sender}
-namespaces:
- users:
- - exclusive: true
- regex: '@${sender}:mana\.how'
- rooms: []
- aliases: []
-rate_limited: false
-EOF
-
- # Convert name to uppercase for env var
- env_name=$(echo "${name}" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
- echo "MATRIX_${env_name}_BOT_AS_TOKEN=${as_token}" >> as-tokens.env
-
- echo "Created ${name}-bot.yaml with AS token"
-done
-
-echo ""
-echo "Done! Add the tokens from as-tokens.env to your .env file"
diff --git a/docker/matrix/config/appservices/generate-as.sh b/docker/matrix/config/appservices/generate-as.sh
deleted file mode 100644
index 425f8ca09..000000000
--- a/docker/matrix/config/appservices/generate-as.sh
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/bin/bash
-
-# Generate random token
-gen_token() {
- openssl rand -hex 32
-}
-
-# Bot configurations: name, sender_localpart
-declare -a BOTS=(
- "mana:mana-bot"
- "ollama:ollama-bot"
- "stats:stats-bot"
- "projectdoc:projectdoc-bot"
- "todo:todo-bot"
- "calendar:calendar-bot"
- "nutriphi:nutriphi-bot"
- "zitare:zitare-bot"
- "clock:clock-bot"
- "tts:tts-bot"
-)
-
-echo "# Generated AS tokens for .env file:" > as-tokens.env
-echo "" >> as-tokens.env
-
-for bot_config in "${BOTS[@]}"; do
- IFS=":" read -r name sender <<< "$bot_config"
-
- as_token=$(gen_token)
- hs_token=$(gen_token)
-
- cat > "${name}-bot.yaml" << EOF
-id: ${name}-bot
-hs_token: ${hs_token}
-as_token: ${as_token}
-url: null
-sender_localpart: ${sender}
-namespaces:
- users:
- - exclusive: true
- regex: '@${sender}:mana\.how'
- rooms: []
- aliases: []
-rate_limited: false
-EOF
-
- # Convert name to uppercase for env var
- env_name=$(echo "${name}" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
- echo "MATRIX_${env_name}_BOT_AS_TOKEN=${as_token}" >> as-tokens.env
-
- echo "Created ${name}-bot.yaml with AS token"
-done
-
-echo ""
-echo "Done! Add the tokens from as-tokens.env to your .env file"
diff --git a/docker/matrix/config/homeserver.yaml b/docker/matrix/config/homeserver.yaml
deleted file mode 100644
index 0e454cce2..000000000
--- a/docker/matrix/config/homeserver.yaml
+++ /dev/null
@@ -1,211 +0,0 @@
-# Mana Matrix Synapse Configuration
-# Documentation: https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html
-
-server_name: "mana.how"
-pid_file: /data/homeserver.pid
-public_baseurl: https://matrix.mana.how/
-
-# ============================================
-# Listeners
-# ============================================
-
-listeners:
- - port: 8008
- tls: false
- type: http
- x_forwarded: true
- resources:
- - names: [client, federation]
- compress: false
-
-# ============================================
-# Database (PostgreSQL)
-# ============================================
-
-database:
- name: psycopg2
- txn_limit: 10000
- args:
- user: synapse
- password: "synapse-secure-password"
- database: matrix
- host: postgres
- port: 5432
- cp_min: 5
- cp_max: 10
-
-# ============================================
-# Logging
-# ============================================
-
-log_config: "/config/log.config.yaml"
-
-# ============================================
-# Media Storage
-# ============================================
-
-media_store_path: /data/media_store
-max_upload_size: 50M
-url_preview_enabled: true
-url_preview_ip_range_blacklist:
- - '127.0.0.0/8'
- - '10.0.0.0/8'
- - '172.16.0.0/12'
- - '192.168.0.0/16'
- - '100.64.0.0/10'
- - '192.0.0.0/24'
- - '169.254.0.0/16'
- - '198.18.0.0/15'
- - '192.0.2.0/24'
- - '198.51.100.0/24'
- - '203.0.113.0/24'
- - '224.0.0.0/4'
- - '::1/128'
- - 'fe80::/10'
- - 'fc00::/7'
- - '2001:db8::/32'
- - 'ff00::/8'
- - 'fec0::/10'
-
-# ============================================
-# Registration & Authentication
-# ============================================
-
-enable_registration: false
-enable_registration_without_verification: false
-
-# Password config (enabled - OIDC not yet available from mana-auth)
-password_config:
- enabled: true
- localdb_enabled: true
- pepper: "${SYNAPSE_PASSWORD_PEPPER:-change-me-pepper}"
-
-# Session lifetime (must be >= refresh_token_lifetime)
-# Set to 10 years for bot tokens to avoid frequent expiration
-session_lifetime: 87600h
-refresh_token_lifetime: 87600h
-
-# ============================================
-# Rate Limiting
-# ============================================
-
-rc_message:
- per_second: 5
- burst_count: 20
-
-rc_registration:
- per_second: 0.5
- burst_count: 5
-
-rc_login:
- address:
- per_second: 0.5
- burst_count: 5
- account:
- per_second: 0.5
- burst_count: 5
- failed_attempts:
- per_second: 0.5
- burst_count: 5
-
-# ============================================
-# Federation
-# ============================================
-
-# Allow federation with other Matrix servers
-federation_domain_whitelist: []
-
-trusted_key_servers:
- - server_name: "matrix.org"
-
-# ============================================
-# DSGVO / Data Retention
-# ============================================
-
-retention:
- enabled: true
- default_policy:
- min_lifetime: 1d
- max_lifetime: 365d
- allowed_lifetime_min: 1d
- allowed_lifetime_max: 365d
- purge_jobs:
- - longest_max_lifetime: 3d
- interval: 12h
- - shortest_max_lifetime: 365d
- interval: 1d
-
-# Forgotten room retention
-forgotten_room_retention_period: 7d
-
-# ============================================
-# Security
-# ============================================
-
-signing_key_path: "/data/signing.key"
-
-form_secret: "${SYNAPSE_FORM_SECRET:-change-me-form-secret}"
-macaroon_secret_key: "${SYNAPSE_MACAROON_SECRET:-change-me-macaroon-secret}"
-registration_shared_secret: "${SYNAPSE_REGISTRATION_SECRET:-change-me-registration-secret}"
-
-# ============================================
-# Application Services (for Bots)
-# Currently disabled - using long-lived user tokens instead
-# TODO: Migrate bots to AS for truly permanent tokens
-# ============================================
-
-app_service_config_files: []
-
-# ============================================
-# Metrics & Telemetry
-# ============================================
-
-report_stats: false
-enable_metrics: true
-metrics_port: 9002
-
-# ============================================
-# Caching
-# ============================================
-
-caches:
- global_factor: 0.5
- per_cache_factors: {}
- expire_caches: true
- cache_entry_ttl: 30m
-
-# ============================================
-# Background Tasks
-# ============================================
-
-run_background_tasks_on: synapse
-
-# ============================================
-# Email (optional, for password reset)
-# ============================================
-
-# email:
-# smtp_host: smtp-relay.brevo.com
-# smtp_port: 587
-# smtp_user: "${SMTP_USER}"
-# smtp_pass: "${SMTP_PASSWORD}"
-# require_transport_security: true
-# notif_from: "Mana Matrix "
-
-# ============================================
-# OIDC / SSO Configuration (Mana Core Auth)
-# ============================================
-
-# OIDC disabled: mana-auth (Better Auth) does not expose OIDC discovery endpoints
-# TODO: add OIDC provider support to mana-auth, then re-enable this
-# oidc_providers:
-# - idp_id: mana
-# issuer: "https://auth.mana.how"
-# client_id: "matrix-synapse"
-# ...
-
-# SSO UI Settings
-sso:
- client_whitelist:
- - "https://element.mana.how"
- - "https://matrix.mana.how"
diff --git a/docker/matrix/config/log.config.yaml b/docker/matrix/config/log.config.yaml
deleted file mode 100644
index 39a2480ba..000000000
--- a/docker/matrix/config/log.config.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-# Synapse Logging Configuration
-
-version: 1
-
-formatters:
- precise:
- format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
-
-handlers:
- console:
- class: logging.StreamHandler
- formatter: precise
- stream: 'ext://sys.stdout'
-
- file:
- class: logging.handlers.TimedRotatingFileHandler
- formatter: precise
- filename: /data/logs/homeserver.log
- when: midnight
- backupCount: 7
- encoding: utf8
-
-loggers:
- synapse.storage.SQL:
- level: WARNING
-
- synapse.access.http.8008:
- level: WARNING
-
-root:
- level: INFO
- handlers: [console, file]
-
-disable_existing_loggers: false
diff --git a/docker/matrix/element-config.json b/docker/matrix/element-config.json
deleted file mode 100644
index 31f8e6df5..000000000
--- a/docker/matrix/element-config.json
+++ /dev/null
@@ -1,59 +0,0 @@
-{
- "default_server_config": {
- "m.homeserver": {
- "base_url": "https://matrix.mana.how",
- "server_name": "mana.how"
- },
- "m.identity_server": {
- "base_url": ""
- }
- },
- "brand": "Mana Chat",
- "integrations_ui_url": "",
- "integrations_rest_url": "",
- "integrations_widgets_urls": [],
- "disable_guests": true,
- "disable_3pid_login": true,
- "default_country_code": "DE",
- "show_labs_settings": false,
- "features": {
- "feature_video_rooms": true,
- "feature_group_calls": true,
- "feature_thread": true
- },
- "room_directory": {
- "servers": ["mana.how"]
- },
- "setting_defaults": {
- "breadcrumbs": true,
- "custom_themes": [],
- "UIFeature.e2eeDefault": false,
- "FTUE.userOnboardingButton": false,
- "analyticsOptIn": false,
- "pseudonymousAnalyticsOptIn": false
- },
- "ui_features": {
- "UIFeature.RoomEncryptionSettings": false
- },
- "force_verification": false,
- "enable_presence_by_hs_url": {},
- "default_theme": "dark",
- "permalink_prefix": "https://element.mana.how",
- "terms_and_conditions_links": [],
- "sso_redirect_options": {
- "immediate": false,
- "on_welcome_page": true
- },
- "posthog": {
- "disabled": true
- },
- "sentry": {
- "disabled": true
- },
- "analytics_owner": "",
- "privacy_policy_url": "",
- "show_analytics_setting": false,
- "bug_report_endpoint_url": "",
- "help_url": "https://mana.how/help",
- "help_encryption_url": "https://element.io/help#encryption"
-}
diff --git a/docker/matrix/element/config.json b/docker/matrix/element/config.json
deleted file mode 100644
index 31f8e6df5..000000000
--- a/docker/matrix/element/config.json
+++ /dev/null
@@ -1,59 +0,0 @@
-{
- "default_server_config": {
- "m.homeserver": {
- "base_url": "https://matrix.mana.how",
- "server_name": "mana.how"
- },
- "m.identity_server": {
- "base_url": ""
- }
- },
- "brand": "Mana Chat",
- "integrations_ui_url": "",
- "integrations_rest_url": "",
- "integrations_widgets_urls": [],
- "disable_guests": true,
- "disable_3pid_login": true,
- "default_country_code": "DE",
- "show_labs_settings": false,
- "features": {
- "feature_video_rooms": true,
- "feature_group_calls": true,
- "feature_thread": true
- },
- "room_directory": {
- "servers": ["mana.how"]
- },
- "setting_defaults": {
- "breadcrumbs": true,
- "custom_themes": [],
- "UIFeature.e2eeDefault": false,
- "FTUE.userOnboardingButton": false,
- "analyticsOptIn": false,
- "pseudonymousAnalyticsOptIn": false
- },
- "ui_features": {
- "UIFeature.RoomEncryptionSettings": false
- },
- "force_verification": false,
- "enable_presence_by_hs_url": {},
- "default_theme": "dark",
- "permalink_prefix": "https://element.mana.how",
- "terms_and_conditions_links": [],
- "sso_redirect_options": {
- "immediate": false,
- "on_welcome_page": true
- },
- "posthog": {
- "disabled": true
- },
- "sentry": {
- "disabled": true
- },
- "analytics_owner": "",
- "privacy_policy_url": "",
- "show_analytics_setting": false,
- "bug_report_endpoint_url": "",
- "help_url": "https://mana.how/help",
- "help_encryption_url": "https://element.io/help#encryption"
-}
diff --git a/docker/matrix/homeserver.yaml b/docker/matrix/homeserver.yaml
deleted file mode 100644
index 94c6c2aa5..000000000
--- a/docker/matrix/homeserver.yaml
+++ /dev/null
@@ -1,222 +0,0 @@
-# Mana Matrix Synapse Configuration
-# Documentation: https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html
-
-server_name: "mana.how"
-pid_file: /data/homeserver.pid
-public_baseurl: https://matrix.mana.how/
-
-# ============================================
-# Listeners
-# ============================================
-
-listeners:
- - port: 8008
- tls: false
- type: http
- x_forwarded: true
- resources:
- - names: [client, federation]
- compress: false
-
-# ============================================
-# Database (PostgreSQL)
-# ============================================
-
-database:
- name: psycopg2
- txn_limit: 10000
- args:
- user: synapse
- password: "synapse-secure-password"
- database: matrix
- host: postgres
- port: 5432
- cp_min: 5
- cp_max: 10
-
-# ============================================
-# Logging
-# ============================================
-
-log_config: "/config/log.config.yaml"
-
-# ============================================
-# Media Storage
-# ============================================
-
-media_store_path: /data/media_store
-max_upload_size: 50M
-url_preview_enabled: true
-url_preview_ip_range_blacklist:
- - '127.0.0.0/8'
- - '10.0.0.0/8'
- - '172.16.0.0/12'
- - '192.168.0.0/16'
- - '100.64.0.0/10'
- - '192.0.0.0/24'
- - '169.254.0.0/16'
- - '198.18.0.0/15'
- - '192.0.2.0/24'
- - '198.51.100.0/24'
- - '203.0.113.0/24'
- - '224.0.0.0/4'
- - '::1/128'
- - 'fe80::/10'
- - 'fc00::/7'
- - '2001:db8::/32'
- - 'ff00::/8'
- - 'fec0::/10'
-
-# ============================================
-# Registration & Authentication
-# ============================================
-
-enable_registration: false
-enable_registration_without_verification: false
-
-# Password config (disabled - all users authenticate via OIDC/SSO)
-password_config:
- enabled: false
- localdb_enabled: false
- pepper: "${SYNAPSE_PASSWORD_PEPPER:-change-me-pepper}"
-
-# Session lifetime (must be >= refresh_token_lifetime)
-# Set to 10 years for bot tokens to avoid frequent expiration
-session_lifetime: 87600h
-refresh_token_lifetime: 87600h
-
-# ============================================
-# Rate Limiting
-# ============================================
-
-rc_message:
- per_second: 5
- burst_count: 20
-
-rc_registration:
- per_second: 0.5
- burst_count: 5
-
-rc_login:
- address:
- per_second: 0.5
- burst_count: 5
- account:
- per_second: 0.5
- burst_count: 5
- failed_attempts:
- per_second: 0.5
- burst_count: 5
-
-# ============================================
-# Federation
-# ============================================
-
-# Allow federation with other Matrix servers
-federation_domain_whitelist: []
-
-trusted_key_servers:
- - server_name: "matrix.org"
-
-# ============================================
-# DSGVO / Data Retention
-# ============================================
-
-retention:
- enabled: true
- default_policy:
- min_lifetime: 1d
- max_lifetime: 365d
- allowed_lifetime_min: 1d
- allowed_lifetime_max: 365d
- purge_jobs:
- - longest_max_lifetime: 3d
- interval: 12h
- - shortest_max_lifetime: 365d
- interval: 1d
-
-# Forgotten room retention
-forgotten_room_retention_period: 7d
-
-# ============================================
-# Security
-# ============================================
-
-signing_key_path: "/data/signing.key"
-
-form_secret: "${SYNAPSE_FORM_SECRET:-change-me-form-secret}"
-macaroon_secret_key: "${SYNAPSE_MACAROON_SECRET:-change-me-macaroon-secret}"
-registration_shared_secret: "${SYNAPSE_REGISTRATION_SECRET:-change-me-registration-secret}"
-
-# ============================================
-# Application Services (for Bots)
-# Currently disabled - using long-lived user tokens instead
-# TODO: Migrate bots to AS for truly permanent tokens
-# ============================================
-
-app_service_config_files: []
-
-# ============================================
-# Metrics & Telemetry
-# ============================================
-
-report_stats: false
-enable_metrics: true
-metrics_port: 9002
-
-# ============================================
-# Caching
-# ============================================
-
-caches:
- global_factor: 0.5
- per_cache_factors: {}
- expire_caches: true
- cache_entry_ttl: 30m
-
-# ============================================
-# Background Tasks
-# ============================================
-
-run_background_tasks_on: synapse
-
-# ============================================
-# Email (optional, for password reset)
-# ============================================
-
-# email:
-# smtp_host: smtp-relay.brevo.com
-# smtp_port: 587
-# smtp_user: "${SMTP_USER}"
-# smtp_pass: "${SMTP_PASSWORD}"
-# require_transport_security: true
-# notif_from: "Mana Matrix "
-
-# ============================================
-# OIDC / SSO Configuration (Mana Core Auth)
-# ============================================
-
-# Enable SSO via Mana Core Auth OIDC Provider
-oidc_providers:
- - idp_id: mana
- idp_name: "Mana Core"
- idp_brand: "org.matrix.custom"
- discover: true
- issuer: "https://auth.mana.how"
- client_id: "matrix-synapse"
- client_secret: "6dc67d2dbea5c19409d21cbaec5ba77265b0296796d4ebb015d70209c68f3fd5"
- scopes: ["openid", "profile", "email"]
- user_mapping_provider:
- config:
- subject_claim: "sub"
- localpart_template: "{{ user.email.split('@')[0] }}"
- display_name_template: "{{ user.name }}"
- email_template: "{{ user.email }}"
- allow_existing_users: true
- enable_registration: true
-
-# SSO UI Settings
-sso:
- client_whitelist:
- - "https://element.mana.how"
- - "https://matrix.mana.how"
diff --git a/docker/matrix/log.config.yaml b/docker/matrix/log.config.yaml
deleted file mode 100644
index 39a2480ba..000000000
--- a/docker/matrix/log.config.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-# Synapse Logging Configuration
-
-version: 1
-
-formatters:
- precise:
- format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
-
-handlers:
- console:
- class: logging.StreamHandler
- formatter: precise
- stream: 'ext://sys.stdout'
-
- file:
- class: logging.handlers.TimedRotatingFileHandler
- formatter: precise
- filename: /data/logs/homeserver.log
- when: midnight
- backupCount: 7
- encoding: utf8
-
-loggers:
- synapse.storage.SQL:
- level: WARNING
-
- synapse.access.http.8008:
- level: WARNING
-
-root:
- level: INFO
- handlers: [console, file]
-
-disable_existing_loggers: false
diff --git a/docker/prometheus/alerts.yml b/docker/prometheus/alerts.yml
index 9594db3b6..e47506c58 100644
--- a/docker/prometheus/alerts.yml
+++ b/docker/prometheus/alerts.yml
@@ -3,7 +3,7 @@ groups:
rules:
# Service Down Alert
- alert: ServiceDown
- expr: up{job=~"mana-auth|.*-backend|mana-search|mana-media|mana-llm|synapse"} == 0
+ expr: up{job=~"mana-auth|.*-backend|mana-search|mana-media|mana-llm"} == 0
for: 1m
labels:
severity: critical
diff --git a/docker/prometheus/prometheus.yml b/docker/prometheus/prometheus.yml
index 20e05bedf..90fde6e3e 100644
--- a/docker/prometheus/prometheus.yml
+++ b/docker/prometheus/prometheus.yml
@@ -123,13 +123,6 @@ scrape_configs:
metrics_path: '/metrics'
scrape_interval: 30s
- # Matrix Synapse
- - job_name: 'synapse'
- static_configs:
- - targets: ['synapse:9002']
- metrics_path: '/_synapse/metrics'
- scrape_interval: 30s
-
# ============================================
# GPU Server (Windows PC, LAN: 192.168.178.11)
# ============================================
@@ -190,13 +183,6 @@ scrape_configs:
metrics_path: '/metrics'
scrape_interval: 15s
- # Matrix Bot (Go) — consolidated 21 bots
- - job_name: 'mana-matrix-bot'
- static_configs:
- - targets: ['mana-matrix-bot:4000']
- metrics_path: '/metrics'
- scrape_interval: 30s
-
# Sync Server (Go) — local-first data sync
- job_name: 'mana-sync'
static_configs:
@@ -204,7 +190,7 @@ scrape_configs:
metrics_path: '/metrics'
scrape_interval: 30s
- # Notification Service (Go) — email, push, matrix, webhook
+ # Notification Service (Go) — email, push, webhook
- job_name: 'mana-notify'
static_configs:
- targets: ['mana-core-notify:3013']
@@ -297,8 +283,6 @@ scrape_configs:
- https://grafana.mana.how
- https://stats.mana.how
- https://glitchtip.mana.how
- - https://matrix.mana.how
- - https://element.mana.how
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
diff --git a/docker/promtail/config.yaml b/docker/promtail/config.yaml
index cb355487c..46a0d5f6c 100644
--- a/docker/promtail/config.yaml
+++ b/docker/promtail/config.yaml
@@ -60,11 +60,6 @@ scrape_configs:
regex: "mana-mon-.*"
target_label: "tier"
replacement: "monitoring"
- # mana-matrix-* → tier=matrix
- - source_labels: ["container"]
- regex: "mana-matrix-.*"
- target_label: "tier"
- replacement: "matrix"
# mana-game-* → tier=games
- source_labels: ["container"]
regex: "mana-game-.*"
diff --git a/docs/CLOUDFLARE_DOMAINS.md b/docs/CLOUDFLARE_DOMAINS.md
index f0d67ea56..5b3c986a5 100644
--- a/docs/CLOUDFLARE_DOMAINS.md
+++ b/docs/CLOUDFLARE_DOMAINS.md
@@ -61,8 +61,7 @@ Cloudflare Tunnel (bb0ea86d...)
├── Apps (Web + API): chat.mana.how → localhost:5010
├── Services: auth.mana.how → localhost:3001
├── Landing Pages: it.mana.how → localhost:4400 (Nginx)
- ├── Monitoring: grafana.mana.how → localhost:8000
- └── Matrix: matrix.mana.how → localhost:4000
+ └── Monitoring: grafana.mana.how → localhost:8000
```
**Nginx Landing Container** (`mana-infra-landings`, Port 4400):
@@ -119,9 +118,6 @@ Cloudflare Tunnel (bb0ea86d...)
| Domain | Service | Port |
|--------|---------|------|
-| `matrix.mana.how` | Matrix Synapse | 4000 |
-| `element.mana.how` | Element Web | 4080 |
-| `link.mana.how` | Matrix Link | 4090 |
| `grafana.mana.how` | Grafana | 8000 |
| `stats.mana.how` | Umami Analytics | 8010 |
| `glitchtip.mana.how` | GlitchTip Errors | 8020 |
diff --git a/docs/CLOUDFLARE_FALLBACK.md b/docs/CLOUDFLARE_FALLBACK.md
index 493ba2ba2..f2bfe3044 100644
--- a/docs/CLOUDFLARE_FALLBACK.md
+++ b/docs/CLOUDFLARE_FALLBACK.md
@@ -211,14 +211,6 @@ playground.mana.how {
reverse_proxy 10.0.0.2:5090
}
-matrix.mana.how {
- reverse_proxy 10.0.0.2:4000
-}
-
-element.mana.how {
- reverse_proxy 10.0.0.2:4080
-}
-
grafana.mana.how {
reverse_proxy 10.0.0.2:8000
}
diff --git a/docs/MAC_MINI_SERVER.md b/docs/MAC_MINI_SERVER.md
index 2efa2296d..4ea310bc9 100644
--- a/docs/MAC_MINI_SERVER.md
+++ b/docs/MAC_MINI_SERVER.md
@@ -93,8 +93,6 @@ Cloudflare Tunnel (cloudflared)
| Todo | https://todo.mana.how |
| Calendar | https://calendar.mana.how |
| Clock | https://clock.mana.how |
-| Matrix (Synapse) | https://matrix.mana.how |
-| Element Web | https://element.mana.how |
## SSH-Zugang
@@ -411,8 +409,6 @@ curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage"
| mana-calendar-web | Calendar Frontend |
| mana-clock-backend | Clock API |
| mana-clock-web | Clock Frontend |
-| mana-synapse | Matrix Homeserver |
-| mana-element | Element Web Client |
### Nützliche Docker-Befehle
@@ -530,7 +526,6 @@ curl -s http://localhost:3000/
The health check monitors:
- All backend APIs and web frontends
- Infrastructure (PostgreSQL, Redis)
-- Matrix services (Synapse, Element, all bots)
- Monitoring stack (Grafana, Umami, GlitchTip, VictoriaMetrics)
- Alerting stack (vmalert, Alertmanager, Alert Notifier)
- Disk space for `/` and `/Volumes/ManaData` (warning at 80%, critical at 90%)
@@ -553,11 +548,11 @@ ssh mana-server "PATH=/Applications/Docker.app/Contents/Resources/bin:\$PATH &&
Wenn ein Service im Health-Check als `HTTP 000` erscheint und `docker ps -a` den Container nicht zeigt, wurde er vermutlich beim letzten Deploy übersprungen:
```bash
-# Container erstellen und starten (Beispiel: Project Doc Bot)
-docker compose -f docker-compose.macmini.yml up -d matrix-project-doc-bot
+# Container erstellen und starten
+docker compose -f docker-compose.macmini.yml up -d
# Nach Restart prüfen
-docker ps --filter name=mana-matrix-bot-projectdoc --format '{{.Names}} {{.Status}}'
+docker ps --filter name=mana- --format '{{.Names}} {{.Status}}'
```
## Wartung
@@ -642,7 +637,6 @@ Alle 63 Container haben explizite `mem_limit` in `docker-compose.macmini.yml`:
| Core (Hono/Bun) | 5 | 704 MB |
| Go Services | 5 | 384 MB |
| Other Backend | 3 | 576 MB |
-| Matrix | 4 | 784 MB |
| Web Apps | 20 | 2.560 MB |
| LLM | 2 | 384 MB |
| Monitoring | 14 | 1.792 MB |
@@ -742,8 +736,7 @@ Die externe SSD wird für persistente Daten verwendet - sowohl für große Datei
├── backups/ # PostgreSQL Backups (täglich 3:00)
├── ollama/ # LLM Modelle (~58 GB)
├── flux2/ # FLUX.2 Bildgenerierung (~15 GB)
-├── stt-models/ # Speech-to-Text Modelle (~19 GB)
-└── matrix/ # Matrix Synapse Daten
+└── stt-models/ # Speech-to-Text Modelle (~19 GB)
```
### Docker auf externer SSD
@@ -823,81 +816,6 @@ Docker Desktop benötigt "Full Disk Access" für SSD-Mounts:
Systemeinstellungen → Datenschutz & Sicherheit → Voller Festplattenzugriff → Docker.app ✅
```
-## Matrix (DSGVO-konformes Messaging)
-
-Matrix ist eine DSGVO-konforme Alternative zu Telegram für Bot-Kommunikation.
-
-### Komponenten
-
-| Service | Port | Beschreibung |
-|---------|------|--------------|
-| Synapse | 8008 | Matrix Homeserver |
-| Element Web | 8087 | Web-Client |
-
-### Matrix Bots
-
-Alle Matrix Bots laufen als Docker Container und werden via GHCR (GitHub Container Registry) deployed. Watchtower aktualisiert sie automatisch bei neuen Images.
-
-| Bot | Port | Beschreibung |
-|-----|------|--------------|
-| matrix-mana-bot | 4010 | Gateway - alle Features in einem Bot |
-| matrix-ollama-bot | 4011 | KI-Chat via GPU-Server Ollama |
-| matrix-stats-bot | 4012 | Server-Statistiken & Monitoring |
-| matrix-project-doc-bot | 4013 | Projekt-Dokumentation aus Fotos/Voice/Text |
-| matrix-todo-bot | 4014 | Aufgabenverwaltung |
-| matrix-calendar-bot | 4015 | Termine & Events |
-| matrix-nutriphi-bot | 4016 | Ernährungstracking |
-| matrix-zitare-bot | 4017 | Tägliche Zitate |
-| matrix-clock-bot | 4018 | Timer & Wecker |
-| matrix-tts-bot | 4019 | Text-to-Speech |
-
-**Health Checks:**
-```bash
-# Alle Bots prüfen
-for port in 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019; do
- echo -n "Port $port: "
- curl -s http://localhost:$port/health | jq -r '.status // "error"'
-done
-```
-
-**Logs:**
-```bash
-# Logs eines Bots
-docker logs matrix-mana-bot -f
-
-# Alle Matrix Bots
-docker ps | grep matrix-.*-bot
-```
-
-**Bot neu starten:**
-```bash
-docker compose -f docker-compose.macmini.yml restart matrix-mana-bot
-```
-
-**Images manuell aktualisieren:**
-```bash
-docker compose -f docker-compose.macmini.yml pull matrix-mana-bot
-docker compose -f docker-compose.macmini.yml up -d matrix-mana-bot
-```
-
-### Setup
-
-```bash
-# Matrix initialisieren
-./scripts/mac-mini/setup-matrix.sh
-
-# Services starten
-docker compose -f docker-compose.macmini.yml up -d synapse element-web
-
-# Admin-User erstellen
-docker exec -it mana-synapse register_new_matrix_user \
- -c /data/homeserver.yaml http://localhost:8008 -a
-```
-
-### Dokumentation
-
-Siehe [MATRIX_SELF_HOSTING.md](./MATRIX_SELF_HOSTING.md) für detaillierte Anleitung.
-
## Chronologie der Einrichtung
1. **Docker Setup** - PostgreSQL, Redis, App-Container
@@ -909,5 +827,4 @@ Siehe [MATRIX_SELF_HOSTING.md](./MATRIX_SELF_HOSTING.md) für detaillierte Anlei
7. **Email Notifications** - Redundante Benachrichtigung
8. ~~**Ollama** - Lokale LLM-Inferenz~~ → Migriert auf GPU-Server (2026-03-28)
9. ~~**Telegram Ollama Bot**~~ → Deaktiviert (2026-03-28)
-10. **Matrix Synapse** - DSGVO-konformes Messaging
-11. **GPU-Server Offload** - Alle AI-Workloads auf RTX 3090 (2026-03-28)
+10. **GPU-Server Offload** - Alle AI-Workloads auf RTX 3090 (2026-03-28)
diff --git a/docs/MATRIX_BOT_ARCHITECTURE.md b/docs/MATRIX_BOT_ARCHITECTURE.md
deleted file mode 100644
index f70e1d929..000000000
--- a/docs/MATRIX_BOT_ARCHITECTURE.md
+++ /dev/null
@@ -1,990 +0,0 @@
-# Mana Matrix Bot Architecture
-
-**Status:** Production
-**Datum:** 1. Februar 2026
-**Autor:** Till Schneider
-**Letzte Aktualisierung:** 1. Februar 2026
-
----
-
-## Executive Summary
-
-Mana setzt auf **Matrix** als primäre Messaging-Plattform für Bot-Interaktionen. Mit 19 spezialisierten Matrix-Bots und einem Gateway-Bot bieten wir eine vollständig dezentrale, DSGVO-konforme Alternative zu Cloud-basierten Chat-Diensten.
-
-**Kernprinzipien:**
-- **Volle Kontrolle** - Eigene Infrastruktur, eigene Daten
-- **DSGVO-Konformität** - Alle Daten auf eigenen Servern
-- **Unabhängigkeit** - Keine Abhängigkeit von Drittanbieter-Plattformen
-- **Einheitliche UX** - Konsistente Erfahrung über alle Bots
-
----
-
-## 1. Warum Matrix?
-
-### 1.1 Die Entscheidung gegen Telegram/Discord/Slack
-
-Bei der Wahl der Messaging-Plattform für Mana hatten wir mehrere Optionen:
-
-| Plattform | Vorteile | Nachteile |
-|-----------|----------|-----------|
-| **Telegram** | Große Reichweite, einfache API | Zentral, Daten bei Telegram, keine Kontrolle über UX |
-| **Discord** | Gaming-Community, Webhooks | US-basiert, DSGVO-Bedenken, Werbung |
-| **Slack** | Business-Standard | Teuer, Vendor Lock-in, keine Self-Hosting Option |
-| **Matrix** | Dezentral, Self-Hosted, E2E-Verschlüsselung | Kleinere Community, mehr Setup-Aufwand |
-
-**Unsere Entscheidung:** Matrix bietet die einzige Möglichkeit, eine **vollständig unabhängige** Plattform zu betreiben mit:
-- Voller Kontrolle über Nutzerdaten
-- Eigener UI/UX (Element, eigene Clients)
-- End-to-End-Verschlüsselung
-- Federation für Inter-Server-Kommunikation
-
-### 1.2 Matrix Grundkonzepte
-
-```
-┌─────────────────────────────────────────────────────────────────┐
-│ Matrix Ökosystem │
-├─────────────────────────────────────────────────────────────────┤
-│ │
-│ ┌─────────────────┐ ┌─────────────────┐ │
-│ │ Homeserver │<───>│ Homeserver │ Federation │
-│ │ (mana.how) │ │ (matrix.org) │ │
-│ └────────┬────────┘ └─────────────────┘ │
-│ │ │
-│ ▼ │
-│ ┌─────────────────────────────────────────────────────────────┐│
-│ │ Räume ││
-│ ├─────────────────────────────────────────────────────────────┤│
-│ │ !abc:mana.how │ Bot-Interaktion (1:1) ││
-│ │ !xyz:mana.how │ Gruppen-Chat (Multi-User) ││
-│ │ #public:mana.how │ Öffentlicher Raum ││
-│ └─────────────────────────────────────────────────────────────┘│
-│ │
-│ ┌─────────────────────────────────────────────────────────────┐│
-│ │ Clients ││
-│ ├─────────────────────────────────────────────────────────────┤│
-│ │ Element (Web/Desktop/Mobile) ││
-│ │ FluffyChat, Nheko, SchildiChat, ... ││
-│ │ Mana Bots (matrix-bot-sdk) ││
-│ └─────────────────────────────────────────────────────────────┘│
-│ │
-└─────────────────────────────────────────────────────────────────┘
-```
-
-**Kernkonzepte:**
-- **Homeserver:** Der Server, der Nutzerkonten und Räume hostet (wir nutzen Synapse)
-- **Räume:** Container für Nachrichten, Events und State
-- **Federation:** Server können miteinander kommunizieren
-- **E2E-Verschlüsselung:** Megolm/Olm für sichere Kommunikation
-
----
-
-## 2. Bot-Architektur Übersicht
-
-### 2.1 Gesamtarchitektur
-
-```
-┌─────────────────────────────────────────────────────────────────────────┐
-│ Mana Bot Ecosystem │
-├─────────────────────────────────────────────────────────────────────────┤
-│ │
-│ ┌──────────────────────────────────────────────────────────────────┐ │
-│ │ @mana/bot-services (Shared Business Logic) │ │
-│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │
-│ │ │ TodoSvc │ │ CalSvc │ │ AiSvc │ │ ClockSvc │ │ ... │ │ │
-│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │ │
-│ └──────────────────────────────────────────────────────────────────┘ │
-│ │ │
-│ ▼ │
-│ ┌──────────────────────────────────────────────────────────────────┐ │
-│ │ Matrix Transport Layer │ │
-│ │ (matrix-bot-sdk) │ │
-│ └──────────────────────────────────────────────────────────────────┘ │
-│ │ │
-│ ┌──────────────────────────┼──────────────────────────┐ │
-│ ▼ ▼ ▼ │
-│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
-│ │ 19 Matrix Bots │ │ Gateway Bot │ │ Shared Services │ │
-│ │ (Specialized) │ │ (All-in-One) │ │ (mana-llm, etc) │ │
-│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
-│ │ │
-│ ▼ │
-│ ┌──────────────────────────────────────────────────────────────────┐ │
-│ │ Backend APIs │ │
-│ │ chat │ todo │ contacts │ calendar │ clock │ picture │ ... │ │
-│ └──────────────────────────────────────────────────────────────────┘ │
-│ │ │
-│ ▼ │
-│ ┌──────────────────────────────────────────────────────────────────┐ │
-│ │ Data Layer │ │
-│ │ PostgreSQL │ S3/MinIO │ JSON Files │ Redis │ Ollama │ │
-│ └──────────────────────────────────────────────────────────────────┘ │
-│ │
-└─────────────────────────────────────────────────────────────────────────┘
-```
-
-### 2.2 Bot-Typen
-
-Wir unterscheiden drei Hauptkategorien von Bots:
-
-#### Typ 1: Backend-integrierte Bots
-Diese Bots fungieren als Interface zu bestehenden NestJS-Backend-APIs:
-
-```
-User → Matrix Bot → REST API → PostgreSQL
-```
-
-**Beispiele:**
-- `matrix-contacts-bot` → Contacts Backend (Port 3015)
-- `matrix-chat-bot` → Chat Backend (Port 3002)
-- `matrix-picture-bot` → Picture Backend (Port 3006)
-
-**Vorteile:**
-- Konsistente Geschäftslogik (Web + Bot identisch)
-- Zentralisierte Datenhaltung
-- Einheitliche Auth via JWT
-
-#### Typ 2: DSGVO-konforme Standalone-Bots
-Diese Bots speichern Daten lokal ohne externe Services:
-
-```
-User → Matrix Bot → JSON File (lokal)
-```
-
-**Beispiele:**
-- `matrix-todo-bot` → Lokale JSON-Datei
-- `matrix-calendar-bot` → Lokale JSON-Datei
-- `matrix-ollama-bot` → In-Memory + lokales Ollama
-
-**Vorteile:**
-- Keine Daten verlassen den Server
-- Volle DSGVO-Konformität
-- Offline-fähig
-
-#### Typ 3: Gateway-Bot
-Kombiniert alle Features in einem Bot:
-
-```
-User → matrix-mana-bot → @mana/bot-services → Multiple Backends
-```
-
-**Features:**
-- Einheitlicher Einstiegspunkt (`!mana`)
-- Intelligentes Command-Routing
-- Cross-Feature-Integration (z.B. "Termin mit Kontakt erstellen")
-
----
-
-## 3. Shared Package: @mana/bot-services
-
-### 3.1 Architektur
-
-Das Package `@mana/bot-services` stellt transport-agnostische Geschäftslogik bereit:
-
-```typescript
-// Business Logic Services
-export { TodoModule, TodoService } from './todo';
-export { CalendarModule, CalendarService } from './calendar';
-export { AiModule, AiService } from './ai';
-export { ClockModule, ClockService } from './clock';
-
-// Infrastructure Services (NEU: Konsolidiert aus 11+ Bots)
-export { SessionModule, SessionService } from './session'; // Auth via mana-auth
-export { TranscriptionModule, TranscriptionService } from './transcription'; // STT via mana-stt
-
-// Storage Provider (pluggable)
-export { FileStorageProvider } from './shared/storage/file-storage.provider';
-export { MemoryStorageProvider } from './shared/storage/memory-storage.provider';
-
-// Utilities
-export { generateId, getTodayISO, formatDateDE } from './shared/utils';
-export { parseGermanDateKeyword } from './shared/date-parser';
-```
-
-### 3.1.1 Konsolidierte Services
-
-Die folgenden Services wurden aus den einzelnen Bots konsolidiert:
-
-| Service | Vorher | Nachher | Migrierte Bots |
-|---------|--------|---------|----------------|
-| `SessionService` | 11x dupliziert | 1x in bot-services | picture, contacts, chat, zitare, skilltree, presi, questions, storage, planta, cards, nutriphi |
-| `TranscriptionService` | 6x dupliziert | 1x in bot-services | todo, clock, zitare, nutriphi, project-doc |
-
-**Status: Vollständig migriert** - Alle 11 Bots mit SessionService und alle 5 Bots mit TranscriptionService nutzen jetzt die gemeinsamen Services aus `@mana/bot-services`.
-
-### 3.2 TodoService
-
-Vollständige Aufgabenverwaltung mit deutscher Sprachunterstützung:
-
-```typescript
-interface TodoService {
- // CRUD
- addTask(userId: string, text: string): Promise;
- listTasks(userId: string, filter?: TaskFilter): Promise;
- completeTask(userId: string, taskId: string): Promise;
- deleteTask(userId: string, taskId: string): Promise;
-
- // Projekte
- createProject(userId: string, name: string): Promise;
- listProjects(userId: string): Promise;
-
- // Filter
- getTasksDueToday(userId: string): Promise;
- getTasksByPriority(userId: string, priority: Priority): Promise;
-}
-
-// Deutsche Eingabeverarbeitung
-"Morgen Arzt anrufen #gesundheit !hoch"
-→ { text: "Arzt anrufen", dueDate: tomorrow, project: "gesundheit", priority: "high" }
-```
-
-### 3.3 CalendarService
-
-Terminverwaltung mit natürlicher Spracheingabe:
-
-```typescript
-interface CalendarService {
- // Events
- createEvent(userId: string, input: string): Promise;
- getEventsForDate(userId: string, date: Date): Promise;
- getEventsInRange(userId: string, start: Date, end: Date): Promise;
-
- // Kalender
- createCalendar(userId: string, name: string): Promise;
- listCalendars(userId: string): Promise;
-}
-
-// Natürliche Eingabe
-"Meeting morgen um 14 Uhr im Büro"
-→ { title: "Meeting", date: tomorrow, time: "14:00", location: "Büro" }
-```
-
-### 3.4 AiService
-
-Integration mit lokalem LLM (Ollama) und mana-llm:
-
-```typescript
-interface AiService {
- chat(userId: string, message: string): Promise;
- setModel(userId: string, model: string): Promise;
- setSystemPrompt(userId: string, mode: SystemMode): Promise;
- clearHistory(userId: string): Promise;
-
- // Vision (für Bildanalyse)
- analyzeImage(userId: string, imageUrl: string, prompt: string): Promise;
-}
-
-type SystemMode = 'default' | 'classify' | 'summarize' | 'translate' | 'code';
-```
-
-### 3.5 Storage Provider Pattern
-
-Pluggable Storage für flexible Datenhaltung:
-
-```typescript
-interface StorageProvider {
- get(key: string): Promise;
- set(key: string, value: T): Promise;
- delete(key: string): Promise;
- list(prefix?: string): Promise;
-}
-
-// Implementierungen
-class FileStorageProvider implements StorageProvider {
- constructor(private basePath: string) {}
- // Speichert als JSON-Dateien
-}
-
-class MemoryStorageProvider implements StorageProvider {
- private store = new Map();
- // In-Memory für Tests
-}
-
-// Zukünftig möglich:
-class PostgresStorageProvider implements StorageProvider { }
-class RedisStorageProvider implements StorageProvider { }
-```
-
----
-
-## 4. Matrix Bot Implementation
-
-### 4.1 Technologie-Stack
-
-Alle Matrix-Bots nutzen einen einheitlichen Stack:
-
-| Komponente | Technologie | Version |
-|------------|-------------|---------|
-| **Framework** | NestJS | 10.x |
-| **Matrix SDK** | matrix-bot-sdk | 0.7.1 |
-| **Language** | TypeScript | 5.x |
-| **Runtime** | Node.js | 20.x |
-| **Build** | tsc + Docker | - |
-
-### 4.2 Bot-Struktur
-
-```
-services/matrix-{name}-bot/
-├── src/
-│ ├── app.module.ts # NestJS Root Module
-│ ├── main.ts # Bootstrap
-│ ├── matrix/
-│ │ ├── matrix.module.ts # Matrix SDK Integration
-│ │ ├── matrix.service.ts # Bot-Logik & Command-Handling
-│ │ └── matrix.constants.ts # Konfiguration
-│ ├── services/ # Optionale lokale Services
-│ └── utils/ # Hilfsfunktionen
-├── Dockerfile
-├── package.json
-└── tsconfig.json
-```
-
-### 4.3 Matrix Service Pattern
-
-```typescript
-@Injectable()
-export class MatrixService implements OnModuleInit, OnModuleDestroy {
- private client: MatrixClient;
- private storage: SimpleFsStorageProvider;
-
- async onModuleInit() {
- // Storage für Sync-State
- this.storage = new SimpleFsStorageProvider('./data/matrix-state.json');
-
- // Client initialisieren
- this.client = new MatrixClient(
- this.configService.get('MATRIX_HOMESERVER_URL'),
- this.configService.get('MATRIX_ACCESS_TOKEN'),
- this.storage,
- );
-
- // Crypto für E2E (optional)
- const cryptoStore = new RustSdkCryptoStorageProvider('./data/crypto');
- await this.client.crypto.prepare(cryptoStore);
-
- // Event-Handler registrieren
- this.client.on('room.message', this.handleMessage.bind(this));
-
- // Sync starten
- await this.client.start();
- }
-
- private async handleMessage(roomId: string, event: any) {
- if (event.sender === this.client.getUserId()) return;
-
- const body = event.content?.body;
- if (!body?.startsWith('!')) return;
-
- const [command, ...args] = body.slice(1).split(' ');
-
- switch (command.toLowerCase()) {
- case 'help':
- case 'hilfe':
- await this.sendHelp(roomId);
- break;
- case 'add':
- case 'hinzufuegen':
- await this.handleAdd(roomId, event.sender, args.join(' '));
- break;
- // ... weitere Commands
- }
- }
-
- private async sendMessage(roomId: string, message: string) {
- await this.client.sendMessage(roomId, {
- msgtype: 'm.text',
- body: message,
- format: 'org.matrix.custom.html',
- formatted_body: this.markdownToHtml(message),
- });
- }
-}
-```
-
-### 4.4 Command-Pattern
-
-Alle Bots nutzen ein einheitliches Command-Schema:
-
-```
-!command [args] # Englisch
-!befehl [argumente] # Deutsch (Aliase)
-```
-
-**Beispiele:**
-
-| Bot | Command | Alias | Beschreibung |
-|-----|---------|-------|--------------|
-| todo | `!add Task` | `!hinzufuegen` | Aufgabe erstellen |
-| todo | `!list` | `!liste` | Aufgaben anzeigen |
-| todo | `!done 1` | `!erledigt` | Aufgabe abschließen |
-| calendar | `!today` | `!heute` | Termine heute |
-| calendar | `!add Meeting morgen 14:00` | `!termin` | Termin erstellen |
-| contacts | `!search Max` | `!suche` | Kontakt suchen |
-
-### 4.5 Nummer-basiertes Referenzsystem
-
-Für intuitive Interaktion nutzen Bots ein Listen-Referenz-System:
-
-```
-User: !kontakte
-Bot: 1. Max Mustermann (max@example.com)
- 2. Anna Schmidt (anna@example.com)
- 3. Peter Meyer (peter@example.com)
-
-User: !anrufen 2
-Bot: Anruf an Anna Schmidt wird vorbereitet...
- Telefon: +49 123 456789
-```
-
-**Implementierung:**
-```typescript
-// Pro User wird die letzte Liste gespeichert
-private listCache = new Map();
-
-async handleList(roomId: string, userId: string) {
- const contacts = await this.contactsApi.list(userId);
- this.listCache.set(userId, contacts);
-
- const message = contacts
- .map((c, i) => `${i + 1}. ${c.name} (${c.email})`)
- .join('\n');
-
- await this.sendMessage(roomId, message);
-}
-
-async handleCall(roomId: string, userId: string, index: number) {
- const contacts = this.listCache.get(userId);
- if (!contacts || index < 1 || index > contacts.length) {
- return this.sendMessage(roomId, 'Ungültige Nummer');
- }
-
- const contact = contacts[index - 1];
- // ... Anruf-Logik
-}
-```
-
----
-
-## 5. Bot-Katalog
-
-### 5.1 Produktivitäts-Bots
-
-| Bot | Port | Storage | Beschreibung |
-|-----|------|---------|--------------|
-| **matrix-mana-bot** | 3310 | JSON | Gateway - alle Features vereint |
-| **matrix-todo-bot** | 3314 | JSON | Aufgabenverwaltung mit Projekten |
-| **matrix-calendar-bot** | 3315 | JSON | Terminverwaltung mit Erinnerungen |
-| **matrix-clock-bot** | 3318 | API | Timer, Alarme, Weltuhren |
-
-### 5.2 KI & Medien-Bots
-
-| Bot | Port | Backend | Beschreibung |
-|-----|------|---------|--------------|
-| **matrix-chat-bot** | 3327 | chat:3002 | KI-Konversationen |
-| **matrix-ollama-bot** | 3311 | mana-llm:3025 | Lokales LLM (DSGVO) |
-| **matrix-picture-bot** | 3319 | picture:3006 | AI-Bildgenerierung |
-| **matrix-tts-bot** | 3023 | mana-tts:3022 | Text-to-Speech |
-| **matrix-project-doc-bot** | 3313 | PostgreSQL+S3 | Projektdoku → Blog |
-
-### 5.3 App-Integrations-Bots
-
-| Bot | Port | Backend | Beschreibung |
-|-----|------|---------|--------------|
-| **matrix-contacts-bot** | 3320 | contacts:3015 | Kontaktverwaltung |
-| **matrix-storage-bot** | 3323 | storage:3016 | Cloud-Speicher |
-| **matrix-nutriphi-bot** | 3316 | nutriphi:3023 | Ernährungstracking |
-| **matrix-zitare-bot** | 3321 | zitare:3019 | Tägliche Zitate |
-| **matrix-questions-bot** | 3324 | questions:3011 | Q&A mit Web-Recherche |
-| **matrix-cards-bot** | 3321 | cards:3009 | Kartendecks & Lernen |
-| **matrix-planta-bot** | 3322 | planta:3022 | Pflanzenpflege |
-| **matrix-skilltree-bot** | 3324 | skilltree:3024 | Skill Tree & XP |
-| **matrix-presi-bot** | 3308 | presi:3008 | Präsentationen |
-| **matrix-stats-bot** | 3312 | Umami | Analytics-Reports |
-
----
-
-## 6. Authentifizierung
-
-### 6.1 Zwei Auth-Modelle
-
-Wir unterstützen zwei Authentifizierungsmodelle:
-
-#### Modell A: Matrix User ID (DSGVO-optimiert)
-Für Standalone-Bots ohne Backend-Anbindung:
-
-```
-Matrix User ID → Isolierte Daten pro User
-@till:mana.how → /data/till-mana-how/todos.json
-```
-
-**Vorteile:**
-- Kein Login erforderlich
-- Daten strikt isoliert
-- Funktioniert offline
-
-**Verwendung:** matrix-todo-bot, matrix-calendar-bot, matrix-ollama-bot
-
-#### Modell B: Mana Auth (JWT)
-Für Backend-integrierte Bots:
-
-```
-┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
-│ Matrix User │────>│ Matrix Bot │────>│ mana-auth │
-│ !login x y │ │ │ │ (Port 3001) │
-└─────────────────┘ └─────────────────┘ └─────────────────┘
- │ │
- │ JWT Token │
- ▼ │
- ┌─────────────────┐ │
- │ In-Memory Map │ │
- │ @user → token │ │
- └─────────────────┘ │
- │ │
- ▼ │
- ┌─────────────────┐ │
- │ Backend API │◀──────────┘
- │ (JWT Validate) │
- └─────────────────┘
-```
-
-**Login-Flow:**
-```
-User: !login till@mana.how geheimespasswort
-Bot: Login erfolgreich! Token gültig für 7 Tage.
- Nutze !logout zum Abmelden.
-
-User: !kontakte
-Bot: [Zeigt Kontakte aus Backend]
-```
-
-**Verwendung:** matrix-contacts-bot, matrix-chat-bot, matrix-picture-bot, etc.
-
-### 6.2 Token-Management
-
-```typescript
-@Injectable()
-export class AuthService {
- private tokens = new Map();
-
- async login(matrixUserId: string, email: string, password: string): Promise {
- const response = await fetch(`${this.authUrl}/api/v1/auth/login`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email, password }),
- });
-
- if (!response.ok) return false;
-
- const { accessToken, expiresIn } = await response.json();
- this.tokens.set(matrixUserId, {
- token: accessToken,
- expiresAt: Date.now() + expiresIn * 1000,
- });
-
- return true;
- }
-
- getToken(matrixUserId: string): string | null {
- const data = this.tokens.get(matrixUserId);
- if (!data || Date.now() > data.expiresAt) return null;
- return data.token;
- }
-
- logout(matrixUserId: string): void {
- this.tokens.delete(matrixUserId);
- }
-}
-```
-
----
-
-## 7. Datenbank-Anbindung
-
-### 7.1 Vier Speichermodelle
-
-| Modell | Technologie | Bots | Use Case |
-|--------|-------------|------|----------|
-| **Stateless** | Keine eigene | contacts, chat, picture | Backend delegiert |
-| **JSON Files** | Lokale Dateien | todo, calendar, mana-bot | DSGVO, einfach |
-| **PostgreSQL** | Drizzle ORM | project-doc-bot | Komplexe Relationen |
-| **S3/MinIO** | AWS SDK | project-doc-bot | Medien-Speicherung |
-
-### 7.2 JSON File Storage (DSGVO)
-
-```typescript
-// Struktur
-/data/
-├── {sanitized-matrix-user-id}/
-│ ├── todos.json
-│ ├── calendar.json
-│ └── settings.json
-```
-
-```typescript
-// FileStorageProvider
-class FileStorageProvider {
- constructor(private basePath: string) {}
-
- private getPath(key: string): string {
- return path.join(this.basePath, `${key}.json`);
- }
-
- async get(key: string): Promise {
- const filePath = this.getPath(key);
- if (!fs.existsSync(filePath)) return null;
- const data = await fs.promises.readFile(filePath, 'utf-8');
- return JSON.parse(data);
- }
-
- async set(key: string, value: T): Promise {
- const filePath = this.getPath(key);
- await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
- await fs.promises.writeFile(filePath, JSON.stringify(value, null, 2));
- }
-}
-```
-
-### 7.3 PostgreSQL + Drizzle (Komplexe Bots)
-
-```typescript
-// schema.ts (project-doc-bot)
-export const projects = pgTable('projects', {
- id: uuid('id').primaryKey().defaultRandom(),
- userId: varchar('user_id', { length: 255 }).notNull(),
- name: varchar('name', { length: 255 }).notNull(),
- description: text('description'),
- createdAt: timestamp('created_at').defaultNow(),
-});
-
-export const mediaItems = pgTable('media_items', {
- id: uuid('id').primaryKey().defaultRandom(),
- projectId: uuid('project_id').references(() => projects.id),
- type: varchar('type', { length: 50 }).notNull(), // photo, voice, text
- s3Key: varchar('s3_key', { length: 500 }),
- transcription: text('transcription'),
- createdAt: timestamp('created_at').defaultNow(),
-});
-```
-
----
-
-## 8. Matrix-spezifische Features
-
-### 8.1 Rich Media Support
-
-Matrix-Bots können verschiedene Nachrichtentypen senden:
-
-```typescript
-// Text mit Markdown/HTML
-await client.sendMessage(roomId, {
- msgtype: 'm.text',
- body: 'Plain text fallback',
- format: 'org.matrix.custom.html',
- formatted_body: 'Bold and code',
-});
-
-// Bilder
-await client.sendMessage(roomId, {
- msgtype: 'm.image',
- body: 'Generated image',
- url: await client.uploadContent(imageBuffer, 'image/png'),
- info: { w: 512, h: 512, mimetype: 'image/png' },
-});
-
-// Dateien
-await client.sendMessage(roomId, {
- msgtype: 'm.file',
- body: 'report.pdf',
- url: await client.uploadContent(pdfBuffer, 'application/pdf'),
- info: { mimetype: 'application/pdf', size: pdfBuffer.length },
-});
-
-// Audio (für TTS)
-await client.sendMessage(roomId, {
- msgtype: 'm.audio',
- body: 'Voice message',
- url: await client.uploadContent(audioBuffer, 'audio/mp3'),
- info: { mimetype: 'audio/mp3', duration: 5000 },
-});
-```
-
-### 8.2 Reactions
-
-Bots können auf Nachrichten reagieren:
-
-```typescript
-// Bestätigung
-await client.sendEvent(roomId, 'm.reaction', {
- 'm.relates_to': {
- rel_type: 'm.annotation',
- event_id: originalEventId,
- key: '✅',
- },
-});
-
-// Fehler
-await client.sendEvent(roomId, 'm.reaction', {
- 'm.relates_to': {
- rel_type: 'm.annotation',
- event_id: originalEventId,
- key: '❌',
- },
-});
-```
-
-### 8.3 Reply Threading
-
-```typescript
-await client.sendMessage(roomId, {
- msgtype: 'm.text',
- body: '> Original message\n\nMy reply',
- format: 'org.matrix.custom.html',
- formatted_body: '... My reply',
- 'm.relates_to': {
- 'm.in_reply_to': {
- event_id: originalEventId,
- },
- },
-});
-```
-
-### 8.4 End-to-End Encryption
-
-```typescript
-// Crypto Storage initialisieren
-const cryptoStore = new RustSdkCryptoStorageProvider('./data/crypto');
-
-// Client mit E2E
-const client = new MatrixClient(homeserverUrl, accessToken, storage);
-await client.crypto.prepare(cryptoStore);
-
-// Verschlüsselten Raum beitreten
-await client.joinRoom(encryptedRoomId);
-
-// Nachrichten werden automatisch ver-/entschlüsselt
-await client.sendMessage(encryptedRoomId, {
- msgtype: 'm.text',
- body: 'This will be encrypted',
-});
-```
-
----
-
-## 9. Deployment
-
-### 9.1 Docker Configuration
-
-```dockerfile
-# Dockerfile
-FROM node:20-alpine AS builder
-
-WORKDIR /app
-
-# Workspace files
-COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
-
-# Shared packages
-COPY packages/bot-services ./packages/bot-services
-
-# Bot
-COPY services/matrix-todo-bot ./services/matrix-todo-bot
-
-RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
-RUN pnpm install --frozen-lockfile
-RUN pnpm --filter @mana/bot-services build
-RUN pnpm --filter matrix-todo-bot build
-
-# Production
-FROM node:20-alpine AS production
-
-WORKDIR /app/services/matrix-todo-bot
-
-COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
-COPY --from=builder /app/services/matrix-todo-bot/node_modules ./node_modules
-COPY --from=builder /app/services/matrix-todo-bot/dist ./dist
-COPY --from=builder /app/services/matrix-todo-bot/package.json ./
-
-# Data volume für persistente Speicherung
-VOLUME /app/data
-
-ENV NODE_ENV=production
-EXPOSE 3314
-
-CMD ["node", "dist/main.js"]
-```
-
-### 9.2 Environment Variables
-
-```env
-# Matrix Connection
-MATRIX_HOMESERVER_URL=https://matrix.mana.how
-MATRIX_ACCESS_TOKEN=syt_xxx...
-MATRIX_USER_ID=@todo-bot:mana.how
-
-# Auth (für Backend-Integration)
-MANA_AUTH_URL=http://mana-auth:3001
-
-# Storage
-DATA_PATH=/app/data
-
-# Optional: Backend URLs
-TODO_BACKEND_URL=http://todo-backend:3018
-CONTACTS_BACKEND_URL=http://contacts-backend:3015
-
-# Optional: AI Services
-MANA_LLM_URL=http://mana-llm:3025
-```
-
-### 9.3 docker-compose.yml
-
-```yaml
-version: '3.8'
-
-services:
- matrix-todo-bot:
- build:
- context: .
- dockerfile: services/matrix-todo-bot/Dockerfile
- environment:
- - MATRIX_HOMESERVER_URL=${MATRIX_HOMESERVER_URL}
- - MATRIX_ACCESS_TOKEN=${MATRIX_TODO_BOT_TOKEN}
- - MATRIX_USER_ID=@todo-bot:mana.how
- volumes:
- - todo-bot-data:/app/data
- networks:
- - mana
- restart: unless-stopped
- healthcheck:
- test: ["CMD", "wget", "-q", "--spider", "http://localhost:3314/health"]
- interval: 30s
- timeout: 10s
- retries: 3
-
- matrix-calendar-bot:
- # ... analog
-
- matrix-mana-bot:
- # Gateway mit allen Services
- depends_on:
- - mana-llm
- - todo-backend
- - contacts-backend
-
-volumes:
- todo-bot-data:
- calendar-bot-data:
- mana-bot-data:
-
-networks:
- mana:
- external: true
-```
-
----
-
-## 10. Port-Allokation
-
-### Matrix Bots (3308-3327)
-
-| Port | Service | Beschreibung |
-|------|---------|--------------|
-| 3308 | matrix-presi-bot | Präsentationen |
-| 3310 | matrix-mana-bot | Gateway (All-in-One) |
-| 3311 | matrix-ollama-bot | Lokales LLM |
-| 3312 | matrix-stats-bot | Analytics |
-| 3313 | matrix-project-doc-bot | Projektdoku |
-| 3314 | matrix-todo-bot | Aufgaben |
-| 3315 | matrix-calendar-bot | Termine |
-| 3316 | matrix-nutriphi-bot | Ernährung |
-| 3318 | matrix-clock-bot | Timer/Alarme |
-| 3319 | matrix-picture-bot | Bildgenerierung |
-| 3320 | matrix-contacts-bot | Kontakte |
-| 3321 | matrix-zitare-bot | Zitate |
-| 3322 | matrix-planta-bot | Pflanzen |
-| 3323 | matrix-storage-bot | Cloud-Speicher |
-| 3324 | matrix-questions-bot | Q&A |
-| 3327 | matrix-chat-bot | KI-Chat |
-
-### Supporting Services
-
-| Port | Service | Beschreibung |
-|------|---------|--------------|
-| 3001 | mana-auth | Authentifizierung |
-| 3020 | mana-stt | Speech-to-Text |
-| 3021 | mana-search | Web-Recherche |
-| 3022 | mana-tts | Text-to-Speech |
-| 3025 | mana-llm | LLM-Abstraction |
-
----
-
-## 11. Vorteile gegenüber Drittanbieter-Plattformen
-
-### 11.1 Vollständige Kontrolle
-
-| Aspekt | Telegram/Discord | Mana Matrix |
-|--------|------------------|-----------------|
-| **Datenhoheit** | Bei Anbieter | Bei uns |
-| **Verfügbarkeit** | Abhängig von Anbieter | Eigene Infrastruktur |
-| **API-Änderungen** | Anbieter entscheidet | Wir entscheiden |
-| **Preisänderungen** | Anbieter entscheidet | Keine |
-| **Zensur/Sperrung** | Möglich | Nicht möglich |
-
-### 11.2 DSGVO-Konformität
-
-```
-┌────────────────────────────────────────────────────────────────┐
-│ DSGVO-Compliance │
-├────────────────────────────────────────────────────────────────┤
-│ │
-│ ✅ Datenverarbeitung nur auf eigenen Servern │
-│ ✅ Keine Weitergabe an Dritte │
-│ ✅ Löschung auf Anfrage (Art. 17) │
-│ ✅ Auskunft über gespeicherte Daten (Art. 15) │
-│ ✅ Datenportabilität (Art. 20) │
-│ ✅ Auftragsverarbeitungsvertrag nicht nötig │
-│ │
-└────────────────────────────────────────────────────────────────┘
-```
-
-### 11.3 Einheitliche UX
-
-Da wir beide Seiten kontrollieren (Bot + Client), können wir:
-- Konsistente Command-Syntax über alle Bots
-- Deutsche Sprachunterstützung überall
-- Einheitliches Fehler-Handling
-- Nahtlose Cross-Bot-Integration
-
----
-
-## 12. Zukünftige Entwicklung
-
-### 12.1 Geplante Erweiterungen
-
-- **Widget-Integration:** Interaktive UIs direkt in Element
-- **Voice-Bot:** Sprachsteuerung via Matrix Calls
-- **Bot-Discovery:** Automatische Bot-Erkennung in Räumen
-- **Mehr @mana/bot-services:** Nutrition, Stats, Docs Services
-
-### 12.2 Konsolidierung
-
-Der Fokus liegt auf der Konsolidierung der Bot-Services in `@mana/bot-services`:
-- Alle wiederkehrende Logik zentral
-- Einheitliche Storage-Abstraction
-- Transport-agnostische Services
-
----
-
-## 13. Fazit
-
-Mana's Matrix-Bot-Architektur bietet eine **vollständig unabhängige, DSGVO-konforme** Alternative zu Cloud-basierten Chat-Diensten. Mit 19 spezialisierten Bots und einem Gateway-Bot decken wir alle Produktivitäts- und App-Integrationsszenarien ab.
-
-**Kernvorteile:**
-1. **Volle Kontrolle** über Daten und Infrastruktur
-2. **DSGVO-Konformität** durch lokale Datenhaltung
-3. **Einheitliche UX** durch konsistente Command-Patterns
-4. **Skalierbarkeit** durch Microservices-Architektur
-5. **Erweiterbarkeit** durch @mana/bot-services
-
----
-
-*Dokument erstellt am 1. Februar 2026*
-*Letzte Aktualisierung: 1. Februar 2026*
diff --git a/docs/MATRIX_SELF_HOSTING.md b/docs/MATRIX_SELF_HOSTING.md
deleted file mode 100644
index 75dd755db..000000000
--- a/docs/MATRIX_SELF_HOSTING.md
+++ /dev/null
@@ -1,674 +0,0 @@
-# Matrix Self-Hosting auf Mac Mini
-
-Plan für DSGVO-konformes Messaging mit Matrix/Synapse auf dem Mana Server.
-
-## Übersicht
-
-```
-┌─────────────────────────────────────────────────────────────────────┐
-│ Internet │
-│ │ │
-│ ▼ │
-│ Cloudflare Tunnel │
-│ │ │
-│ ├─── matrix.mana.how ──────► Synapse (Port 8008) │
-│ ├─── element.mana.how ─────► Element Web (Port 8087) │
-│ └─── (bestehende Services) │
-│ │
-│ ┌─────────────────────────────────────────────────────────────┐ │
-│ │ Docker Container │ │
-│ │ │ │
-│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
-│ │ │ Synapse │ │ Element Web │ │ Matrix Bots │ │ │
-│ │ │ (8008) │ │ (8087) │ │ (NestJS) │ │ │
-│ │ └──────┬───────┘ └──────────────┘ └────────┬─────────┘ │ │
-│ │ │ │ │ │
-│ │ ▼ ▼ │ │
-│ │ ┌──────────────┐ ┌──────────────┐ │ │
-│ │ │ PostgreSQL │ │ Ollama │ │ │
-│ │ │ (matrix db) │ │ (11434) │ │ │
-│ │ └──────────────┘ └──────────────┘ │ │
-│ └─────────────────────────────────────────────────────────────┘ │
-└─────────────────────────────────────────────────────────────────────┘
-```
-
-## DSGVO-Vorteile
-
-| Aspekt | Telegram | Matrix (Self-Hosted) |
-|--------|----------|----------------------|
-| Datenstandort | Dubai/Singapur | Mac Mini (Deutschland) |
-| AV-Vertrag | Nicht möglich | Nicht nötig (eigene Daten) |
-| E2E-Verschlüsselung | Nur Secret Chats | Standard für alle Räume |
-| Metadaten | Bei Telegram | Lokal gespeichert |
-| Löschung | Abhängig von Telegram | Volle Kontrolle |
-
----
-
-## Phase 1: Synapse Homeserver
-
-### 1.1 Datenbank erstellen
-
-```bash
-ssh mana-server
-
-# Neue Datenbank für Matrix
-docker exec mana-postgres psql -U postgres -c "CREATE DATABASE matrix;"
-docker exec mana-postgres psql -U postgres -c "CREATE USER synapse WITH PASSWORD 'synapse-secure-password';"
-docker exec mana-postgres psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE matrix TO synapse;"
-```
-
-### 1.2 Synapse Konfiguration erstellen
-
-```bash
-# Verzeichnis erstellen
-mkdir -p ~/projects/mana-monorepo/docker/matrix
-
-# Synapse Config generieren (einmalig)
-docker run -it --rm \
- -v ~/projects/mana-monorepo/docker/matrix:/data \
- -e SYNAPSE_SERVER_NAME=mana.how \
- -e SYNAPSE_REPORT_STATS=no \
- matrixdotorg/synapse:latest generate
-```
-
-### 1.3 homeserver.yaml anpassen
-
-**Datei:** `docker/matrix/homeserver.yaml`
-
-```yaml
-server_name: "mana.how"
-pid_file: /data/homeserver.pid
-
-listeners:
- - port: 8008
- tls: false
- type: http
- x_forwarded: true
- resources:
- - names: [client, federation]
- compress: false
-
-database:
- name: psycopg2
- args:
- user: synapse
- password: "synapse-secure-password"
- database: matrix
- host: postgres
- port: 5432
- cp_min: 5
- cp_max: 10
-
-# Logging
-log_config: "/data/mana.how.log.config"
-
-# Media Store (lokaler Speicher für Medien)
-media_store_path: /data/media_store
-max_upload_size: 50M
-
-# Registrierung
-enable_registration: false
-enable_registration_without_verification: false
-
-# Admin-Account beim ersten Start erstellen
-# Nach dem Start: docker exec -it synapse register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008 -a
-
-# Rate Limiting (für Bots erhöhen)
-rc_message:
- per_second: 5
- burst_count: 20
-
-rc_registration:
- per_second: 0.5
- burst_count: 5
-
-# Für Bot-Integration: Application Services erlauben
-app_service_config_files: []
-
-# DSGVO: Datenaufbewahrung begrenzen
-retention:
- enabled: true
- default_policy:
- min_lifetime: 1d
- max_lifetime: 365d
- allowed_lifetime_min: 1d
- allowed_lifetime_max: 365d
- purge_jobs:
- - longest_max_lifetime: 3d
- interval: 12h
- - shortest_max_lifetime: 365d
- interval: 1d
-
-# Telemetrie deaktivieren
-report_stats: false
-
-# Trusted Key Server (Matrix.org)
-trusted_key_servers:
- - server_name: "matrix.org"
-
-# Signing Key
-signing_key_path: "/data/mana.how.signing.key"
-```
-
-### 1.4 Docker Compose Ergänzung
-
-Füge zu `docker-compose.macmini.yml` hinzu:
-
-```yaml
- # ============================================
- # Matrix Synapse (Homeserver)
- # ============================================
-
- synapse:
- image: matrixdotorg/synapse:latest
- container_name: mana-synapse
- restart: always
- depends_on:
- postgres:
- condition: service_healthy
- environment:
- SYNAPSE_CONFIG_PATH: /data/homeserver.yaml
- volumes:
- - ./docker/matrix:/data
- - synapse_media:/data/media_store
- ports:
- - "8008:8008"
- healthcheck:
- test: ["CMD", "curl", "-fSs", "http://localhost:8008/health"]
- interval: 30s
- timeout: 10s
- retries: 3
- start_period: 40s
-
- # ============================================
- # Element Web (Matrix Client)
- # ============================================
-
- element-web:
- image: vectorim/element-web:latest
- container_name: mana-element
- restart: always
- depends_on:
- synapse:
- condition: service_healthy
- volumes:
- - ./docker/matrix/element-config.json:/app/config.json:ro
- ports:
- - "8087:80"
- healthcheck:
- test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:80/"]
- interval: 30s
- timeout: 10s
- retries: 3
-
-# Volumes ergänzen:
-volumes:
- synapse_media:
- name: mana-synapse-media
-```
-
-### 1.5 Element Web Konfiguration
-
-**Datei:** `docker/matrix/element-config.json`
-
-```json
-{
- "default_server_config": {
- "m.homeserver": {
- "base_url": "https://matrix.mana.how",
- "server_name": "mana.how"
- },
- "m.identity_server": {
- "base_url": ""
- }
- },
- "brand": "Mana Chat",
- "integrations_ui_url": "",
- "integrations_rest_url": "",
- "integrations_widgets_urls": [],
- "disable_guests": true,
- "disable_3pid_login": true,
- "default_country_code": "DE",
- "show_labs_settings": false,
- "features": {
- "feature_video_rooms": true,
- "feature_group_calls": true
- },
- "room_directory": {
- "servers": ["mana.how"]
- },
- "setting_defaults": {
- "breadcrumbs": true
- },
- "default_theme": "dark"
-}
-```
-
-### 1.6 Cloudflare Tunnel erweitern
-
-**Datei:** `~/.cloudflared/config.yml`
-
-```yaml
-# Bestehende Einträge...
-
- - hostname: matrix.mana.how
- service: http://localhost:8008
-
- - hostname: element.mana.how
- service: http://localhost:8087
-```
-
-Nach Änderung:
-```bash
-launchctl stop com.cloudflare.cloudflared
-launchctl start com.cloudflare.cloudflared
-```
-
----
-
-## Phase 2: Synapse starten & Admin erstellen
-
-### 2.1 Container starten
-
-```bash
-cd ~/projects/mana-monorepo
-
-# Nur Synapse + Element starten
-docker compose -f docker-compose.macmini.yml up -d synapse element-web
-
-# Logs prüfen
-docker logs -f mana-synapse
-```
-
-### 2.2 Admin-User erstellen
-
-```bash
-# Interaktiv einen Admin erstellen
-docker exec -it mana-synapse register_new_matrix_user \
- -c /data/homeserver.yaml \
- http://localhost:8008 \
- -a
-
-# Eingeben:
-# Username: admin
-# Password: (sicheres Passwort)
-# Admin: yes
-```
-
-### 2.3 Testen
-
-```bash
-# Health Check
-curl https://matrix.mana.how/health
-# Erwartete Antwort: OK
-
-# Federation Check
-curl https://matrix.mana.how/_matrix/federation/v1/version
-# Erwartete Antwort: {"server":{"name":"Synapse","version":"..."}}
-
-# Element Web aufrufen
-open https://element.mana.how
-```
-
----
-
-## Phase 3: Bot-Räume einrichten
-
-### 3.1 Räume erstellen (via Element)
-
-1. **Anmelden** bei https://element.mana.how mit Admin-Account
-2. **Räume erstellen:**
- - `#ollama-bot:mana.how` - AI Chat Bot
- - `#stats-bot:mana.how` - Analytics Reports
- - `#project-doc-bot:mana.how` - Projektdokumentation
-
-### 3.2 Bot-User erstellen
-
-```bash
-# Bot-User für jeden Bot erstellen (nicht-Admin)
-docker exec -it mana-synapse register_new_matrix_user \
- -c /data/homeserver.yaml \
- http://localhost:8008
-
-# Erstelle:
-# - ollama-bot (Password notieren)
-# - stats-bot (Password notieren)
-# - projectdoc-bot (Password notieren)
-```
-
-### 3.3 Access Tokens generieren
-
-```bash
-# Für jeden Bot ein Access Token holen
-curl -X POST "https://matrix.mana.how/_matrix/client/v3/login" \
- -H "Content-Type: application/json" \
- -d '{
- "type": "m.login.password",
- "user": "ollama-bot",
- "password": "bot-password"
- }'
-
-# Response: {"access_token": "syt_xxx", ...}
-# Token für .env speichern
-```
-
----
-
-## Phase 4: Bot-Migration (NestJS)
-
-### 4.1 Neue Package-Struktur
-
-```
-services/
-├── telegram-ollama-bot/ # Alt (Telegram)
-├── telegram-stats-bot/ # Alt (Telegram)
-├── telegram-project-doc-bot/# Alt (Telegram)
-│
-├── matrix-ollama-bot/ # NEU (Matrix)
-├── matrix-stats-bot/ # NEU (Matrix)
-└── matrix-project-doc-bot/ # NEU (Matrix)
-```
-
-### 4.2 Dependencies
-
-```bash
-cd services/matrix-ollama-bot
-pnpm add matrix-bot-sdk
-```
-
-### 4.3 Bot-Grundstruktur (Beispiel: Ollama Bot)
-
-**Datei:** `services/matrix-ollama-bot/src/bot/matrix.service.ts`
-
-```typescript
-import {
- MatrixClient,
- SimpleFsStorageProvider,
- AutojoinRoomsMixin,
- RichConsoleLogger,
- LogService,
-} from 'matrix-bot-sdk';
-import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
-import { ConfigService } from '@nestjs/config';
-
-@Injectable()
-export class MatrixService implements OnModuleInit, OnModuleDestroy {
- private client: MatrixClient;
-
- constructor(private config: ConfigService) {}
-
- async onModuleInit() {
- LogService.setLogger(new RichConsoleLogger());
-
- const homeserverUrl = this.config.get('MATRIX_HOMESERVER_URL');
- const accessToken = this.config.get('MATRIX_ACCESS_TOKEN');
-
- const storage = new SimpleFsStorageProvider('bot-storage.json');
-
- this.client = new MatrixClient(homeserverUrl, accessToken, storage);
-
- // Auto-join bei Einladungen
- AutojoinRoomsMixin.setupOnClient(this.client);
-
- // Message Handler
- this.client.on('room.message', this.handleMessage.bind(this));
-
- await this.client.start();
- console.log('Matrix bot started!');
- }
-
- async onModuleDestroy() {
- await this.client.stop();
- }
-
- private async handleMessage(roomId: string, event: any) {
- // Eigene Nachrichten ignorieren
- if (event.sender === await this.client.getUserId()) return;
-
- // Nur Text-Nachrichten
- if (event.content?.msgtype !== 'm.text') return;
-
- const body = event.content.body;
-
- // Command-Handler
- if (body.startsWith('!')) {
- await this.handleCommand(roomId, event, body);
- } else {
- // Normaler Chat → Ollama
- await this.handleChat(roomId, event, body);
- }
- }
-
- private async handleCommand(roomId: string, event: any, body: string) {
- const [command, ...args] = body.slice(1).split(' ');
-
- switch (command.toLowerCase()) {
- case 'help':
- await this.sendMessage(roomId, this.getHelpText());
- break;
- case 'models':
- // Liste verfügbare Modelle
- break;
- case 'clear':
- // Chat-History löschen
- break;
- // ... weitere Commands
- }
- }
-
- private async handleChat(roomId: string, event: any, message: string) {
- // Typing-Indikator senden
- await this.client.setTyping(roomId, true);
-
- // Ollama-Anfrage (wie bisher)
- const response = await this.ollamaService.chat(message);
-
- await this.client.setTyping(roomId, false);
- await this.sendMessage(roomId, response);
- }
-
- async sendMessage(roomId: string, message: string) {
- await this.client.sendMessage(roomId, {
- msgtype: 'm.text',
- body: message,
- format: 'org.matrix.custom.html',
- formatted_body: this.markdownToHtml(message),
- });
- }
-
- private getHelpText(): string {
- return `**Mana Ollama Bot**
-
-Befehle:
-- \`!help\` - Diese Hilfe
-- \`!models\` - Verfügbare Modelle
-- \`!model \` - Modell wechseln
-- \`!clear\` - Chat-Verlauf löschen
-
-Einfach eine Nachricht schreiben für AI-Chat.`;
- }
-}
-```
-
-### 4.4 Environment Variables
-
-**Datei:** `services/matrix-ollama-bot/.env`
-
-```env
-# Server
-PORT=3311
-
-# Matrix
-MATRIX_HOMESERVER_URL=https://matrix.mana.how
-MATRIX_ACCESS_TOKEN=syt_xxx
-
-# Optional: Nur bestimmte Räume erlauben
-MATRIX_ALLOWED_ROOMS=#ollama-bot:mana.how
-
-# Ollama
-OLLAMA_URL=http://host.docker.internal:11434
-OLLAMA_MODEL=gemma3:4b
-OLLAMA_TIMEOUT=120000
-```
-
-### 4.5 Docker Compose für Matrix Bots
-
-```yaml
- # ============================================
- # Matrix Ollama Bot
- # ============================================
-
- matrix-ollama-bot:
- image: ghcr.io/memo-2023/matrix-ollama-bot:latest
- container_name: mana-matrix-ollama-bot
- restart: always
- depends_on:
- synapse:
- condition: service_healthy
- environment:
- NODE_ENV: production
- PORT: 3311
- MATRIX_HOMESERVER_URL: http://synapse:8008
- MATRIX_ACCESS_TOKEN: ${MATRIX_OLLAMA_BOT_TOKEN}
- OLLAMA_URL: http://host.docker.internal:11434
- OLLAMA_MODEL: gemma3:4b
- volumes:
- - matrix_ollama_bot_data:/app/data
- ports:
- - "3311:3311"
- healthcheck:
- test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3311/health"]
- interval: 30s
- timeout: 10s
- retries: 3
-
-# Volume ergänzen:
-volumes:
- matrix_ollama_bot_data:
- name: mana-matrix-ollama-bot
-```
-
----
-
-## Phase 5: Feature-Mapping Telegram → Matrix
-
-### Commands
-
-| Telegram | Matrix | Beschreibung |
-|----------|--------|--------------|
-| `/start` | `!help` | Hilfe anzeigen |
-| `/help` | `!help` | Hilfe anzeigen |
-| `/models` | `!models` | Modelle auflisten |
-| `/model x` | `!model x` | Modell wechseln |
-| `/clear` | `!clear` | Chat löschen |
-| `/status` | `!status` | Bot-Status |
-
-### Media-Handling
-
-| Feature | Telegram | Matrix |
-|---------|----------|--------|
-| Foto senden | `ctx.message.photo` | `m.image` msgtype |
-| Voice senden | `ctx.message.voice` | `m.audio` msgtype |
-| Datei senden | `ctx.message.document` | `m.file` msgtype |
-| Foto antworten | `ctx.replyWithPhoto()` | `sendMessage()` mit `m.image` |
-
-### Beispiel: Media-Download in Matrix
-
-```typescript
-async downloadMedia(event: any): Promise {
- const mxcUrl = event.content.url; // mxc://mana.how/abc123
- const httpUrl = this.client.mxcToHttp(mxcUrl);
-
- const response = await fetch(httpUrl);
- return Buffer.from(await response.arrayBuffer());
-}
-```
-
----
-
-## Phase 6: Health Check & Monitoring
-
-### Health Checks ergänzen
-
-**Datei:** `scripts/mac-mini/health-check.sh`
-
-```bash
-# Matrix Synapse
-if curl -sf http://localhost:8008/health > /dev/null; then
- echo "✅ Synapse: OK"
-else
- echo "❌ Synapse: FAILED"
- FAILED_SERVICES="$FAILED_SERVICES synapse"
-fi
-
-# Element Web
-if curl -sf http://localhost:8087/ > /dev/null; then
- echo "✅ Element Web: OK"
-else
- echo "❌ Element Web: FAILED"
- FAILED_SERVICES="$FAILED_SERVICES element-web"
-fi
-
-# Matrix Ollama Bot
-if curl -sf http://localhost:3311/health > /dev/null; then
- echo "✅ Matrix Ollama Bot: OK"
-else
- echo "❌ Matrix Ollama Bot: FAILED"
- FAILED_SERVICES="$FAILED_SERVICES matrix-ollama-bot"
-fi
-```
-
-### Prometheus Metrics (optional)
-
-Synapse exportiert Metrics auf Port 9000 (kann aktiviert werden):
-
-```yaml
-# In homeserver.yaml ergänzen
-enable_metrics: true
-metrics_port: 9000
-
-# prometheus.yml ergänzen
-- job_name: 'synapse'
- static_configs:
- - targets: ['synapse:9000']
-```
-
----
-
-## Zeitplan
-
-| Phase | Aufgabe | Aufwand |
-|-------|---------|---------|
-| **1** | Synapse + Element aufsetzen | 1-2h |
-| **2** | Admin & Bot-User erstellen | 30min |
-| **3** | Bot-Räume einrichten | 30min |
-| **4** | Ersten Bot migrieren (Ollama) | 2-4h |
-| **5** | Weitere Bots migrieren | je 1-2h |
-| **6** | Monitoring & Alerts | 1h |
-
-**Gesamt:** ~1 Tag für Grundsetup + Bot-Migration
-
----
-
-## Nächste Schritte
-
-1. [ ] `docker/matrix/` Verzeichnis erstellen
-2. [ ] Synapse Config generieren
-3. [ ] Docker Compose erweitern
-4. [ ] Cloudflare Tunnel konfigurieren
-5. [ ] Synapse starten & testen
-6. [ ] Admin-Account erstellen
-7. [ ] Bot-User erstellen
-8. [ ] `matrix-ollama-bot` Service erstellen
-9. [ ] Bot testen
-10. [ ] Weitere Bots migrieren
-11. [ ] Telegram Bots deaktivieren
-
----
-
-## Ressourcen
-
-- [Matrix Spec](https://spec.matrix.org/)
-- [Synapse Docs](https://element-hq.github.io/synapse/latest/)
-- [matrix-bot-sdk](https://github.com/turt2live/matrix-bot-sdk)
-- [Element Web Config](https://github.com/element-hq/element-web/blob/develop/docs/config.md)
diff --git a/docs/PORT_SCHEMA.md b/docs/PORT_SCHEMA.md
index bad18d39d..e43c2e676 100644
--- a/docs/PORT_SCHEMA.md
+++ b/docs/PORT_SCHEMA.md
@@ -70,7 +70,7 @@
| 3010 | mana-sync | Go | Local-first data sync (WebSocket + HTTP) |
| 3011 | mana-media | NestJS | Content-addressable storage, thumbnails |
| 3012 | mana-search | Go | Web search via SearXNG |
-| 3013 | mana-notify | Go | Notifications (email, push, Matrix) |
+| 3013 | mana-notify | Go | Notifications (email, push, webhook) |
| 3014 | mana-crawler | Go | Web crawler, content extraction |
| 3015 | mana-landing-builder | NestJS | Org landing page builder |
| 3016 | mana-api-gateway | Go | API keys, rate limiting, usage tracking |
@@ -107,14 +107,10 @@ Pure CRUD apps use mana-sync directly.
| 3040 | presi-server | Hono/Bun | Share links |
| 3041-3059 | *(reserved)* | | |
-## 4000-4099: Matrix/Chat Stack
+## 4000-4099: Misc
| Port | Service | Description |
|------|---------|-------------|
-| 4000 | synapse | Matrix homeserver |
-| 4001 | mana-matrix-bot | Go bot (health/metrics) |
-| 4010 | element-web | Element web client |
-| 4011 | matrix-web | SvelteKit Matrix client |
| 4400 | landings | Nginx static landing pages |
## 5000-5059: Web Frontends (SvelteKit)
@@ -175,7 +171,6 @@ Pure CRUD apps use mana-sync directly.
| 9090 | victoriametrics | Metrics storage |
| 9091 | pushgateway | Deploy metrics |
| 9093 | alertmanager | Alert routing |
-| 9095 | alert-notifier | Matrix alert bridge |
| 9100 | node-exporter | Host metrics |
| 9110 | cadvisor | Container metrics |
| 9121 | redis-exporter | Redis metrics |
diff --git a/docs/URL_SCHEMA.md b/docs/URL_SCHEMA.md
index 53414dcd1..2d20657c8 100644
--- a/docs/URL_SCHEMA.md
+++ b/docs/URL_SCHEMA.md
@@ -39,15 +39,12 @@ This document defines the URL schema for all mana.how subdomains.
| **Media Service** | media.mana.how | Image/video processing |
| **LLM Service** | llm.mana.how | LLM abstraction layer |
| **LLM Playground** | playground.mana.how | LLM testing interface |
-| **Link Shortener** | link.mana.how | URL shortener (uload) |
| **File Storage** | files.mana.how | MinIO/S3 file access |
-### Matrix/Communication
+### Automation
| Service | URL | Description |
|---------|-----|-------------|
-| **Matrix Server** | matrix.mana.how | Synapse homeserver |
-| **Element Web** | element.mana.how | Matrix web client |
| **N8N** | n.mana.how | Workflow automation |
### Monitoring & Admin
diff --git a/package.json b/package.json
index 269453e15..62985bbbd 100644
--- a/package.json
+++ b/package.json
@@ -76,9 +76,6 @@
"dev:calendar:landing": "pnpm --filter @calendar/landing dev",
"dev:calendar:app": "concurrently -n api,web -c yellow,cyan \"pnpm dev:api\" \"pnpm dev:calendar:web\"",
"dev:calendar:full": "concurrently -n auth,sync,api -c blue,magenta,yellow \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:api\"",
- "matrix:dev": "turbo run dev --filter=matrix...",
- "dev:matrix:web": "pnpm --filter @matrix/web dev",
- "dev:matrix:mobile": "pnpm --filter @matrix/mobile dev",
"mail:dev": "turbo run dev --filter=mail...",
"dev:mail:mobile": "pnpm --filter @mail/mobile dev",
"dev:mail:web": "pnpm --filter @mail/web dev",
@@ -230,9 +227,6 @@
"dev:skilltree:web": "pnpm --filter @skilltree/web dev",
"dev:skilltree:app": "pnpm dev:skilltree:web",
"dev:skilltree:full": "concurrently -n auth,sync,web -c blue,magenta,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:skilltree:web\"",
- "dev:matrix": "cd services/mana-matrix-bot && go run ./cmd/server",
- "build:matrix": "cd services/mana-matrix-bot && go build -ldflags=\"-s -w\" -o dist/mana-matrix-bot ./cmd/server",
- "test:matrix": "cd services/mana-matrix-bot && go test ./...",
"dev:llm-playground": "pnpm --filter @mana-llm/playground dev",
"build:llm-playground": "pnpm --filter @mana-llm/playground build",
"prepare": "husky",
@@ -300,8 +294,7 @@
"patchedDependencies": {},
"overrides": {
"cpu-features": "npm:empty-npm-package@1.0.0",
- "ssh2": "npm:empty-npm-package@1.0.0",
- "@matrix-org/matrix-sdk-crypto-nodejs": "npm:empty-npm-package@1.0.0"
+ "ssh2": "npm:empty-npm-package@1.0.0"
}
}
}
diff --git a/packages/notify-client/src/client.ts b/packages/notify-client/src/client.ts
index 674bf2592..c4b741aad 100644
--- a/packages/notify-client/src/client.ts
+++ b/packages/notify-client/src/client.ts
@@ -1,7 +1,6 @@
import type {
SendEmailOptions,
SendPushOptions,
- SendMatrixOptions,
SendWebhookOptions,
ScheduleOptions,
NotificationResponse,
@@ -82,24 +81,6 @@ export class NotifyClient {
});
}
- /**
- * Send a Matrix message
- */
- async sendMatrix(options: SendMatrixOptions): Promise {
- return this.send({
- channel: 'matrix',
- appId: this.appId,
- recipient: options.roomId,
- body: options.body,
- matrixOptions: {
- formattedBody: options.formattedBody,
- msgtype: options.msgtype,
- },
- priority: options.priority,
- externalId: options.externalId,
- });
- }
-
/**
* Send a webhook notification
*/
@@ -184,7 +165,6 @@ export class NotifyClient {
notifications: Array<
| ({ type: 'email' } & SendEmailOptions)
| ({ type: 'push' } & SendPushOptions)
- | ({ type: 'matrix' } & SendMatrixOptions)
| ({ type: 'webhook' } & SendWebhookOptions)
>
): Promise {
@@ -214,15 +194,6 @@ export class NotifyClient {
priority: n.priority,
externalId: n.externalId,
};
- } else if (n.type === 'matrix') {
- return {
- channel: 'matrix' as const,
- appId: this.appId,
- recipient: n.roomId,
- body: n.body,
- priority: n.priority,
- externalId: n.externalId,
- };
} else {
return {
channel: 'webhook' as const,
diff --git a/packages/notify-client/src/types.ts b/packages/notify-client/src/types.ts
index 4547cc22e..f854adb75 100644
--- a/packages/notify-client/src/types.ts
+++ b/packages/notify-client/src/types.ts
@@ -1,4 +1,4 @@
-export type NotificationChannel = 'email' | 'push' | 'matrix' | 'webhook';
+export type NotificationChannel = 'email' | 'push' | 'webhook';
export type NotificationPriority = 'low' | 'normal' | 'high' | 'critical';
export type NotificationStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'cancelled';
@@ -28,15 +28,6 @@ export interface SendPushOptions {
externalId?: string;
}
-export interface SendMatrixOptions {
- roomId: string;
- body: string;
- formattedBody?: string;
- msgtype?: 'text' | 'notice';
- priority?: NotificationPriority;
- externalId?: string;
-}
-
export interface SendWebhookOptions {
url: string;
method?: 'POST' | 'PUT';
diff --git a/packages/shared-branding/src/app-icons.ts b/packages/shared-branding/src/app-icons.ts
index 8dd319b01..8ed76a715 100644
--- a/packages/shared-branding/src/app-icons.ts
+++ b/packages/shared-branding/src/app-icons.ts
@@ -63,9 +63,6 @@ const inventorySvg = ` `;
-// Matrix icon (network/federated chat with purple gradient)
-const matrixSvg = ` `;
-
// CityCorners icon (map pin with blue gradient)
const citycornersSvg = ` `;
@@ -101,7 +98,6 @@ export const APP_ICONS = {
mail: svgToDataUrl(mailSvg),
inventory: svgToDataUrl(inventorySvg),
questions: svgToDataUrl(questionsSvg),
- matrix: svgToDataUrl(matrixSvg),
context: svgToDataUrl(contextSvg),
citycorners: svgToDataUrl(citycornersSvg),
times: svgToDataUrl(timesSvg),
diff --git a/packages/shared-branding/src/mana-apps.ts b/packages/shared-branding/src/mana-apps.ts
index 3d23755b9..27718ed11 100644
--- a/packages/shared-branding/src/mana-apps.ts
+++ b/packages/shared-branding/src/mana-apps.ts
@@ -377,23 +377,6 @@ export const MANA_APPS: ManaApp[] = [
status: 'beta',
requiredTier: 'alpha',
},
- {
- id: 'matrix',
- name: 'Mana Matrix',
- description: {
- de: 'Matrix Chat Client',
- en: 'Matrix Chat Client',
- },
- longDescription: {
- de: 'Verbinde dich mit dem dezentralen Matrix-Netzwerk für sichere, föderierte Kommunikation.',
- en: 'Connect to the decentralized Matrix network for secure, federated communication.',
- },
- icon: APP_ICONS.matrix,
- color: '#8b5cf6',
- comingSoon: false,
- status: 'beta',
- requiredTier: 'alpha',
- },
{
id: 'context',
name: 'Context',
@@ -796,7 +779,7 @@ export const APP_SLIDER_LABELS = {
* App URLs — unified app uses internal paths, separate apps use subdomains.
*
* All productivity apps are now served under mana.how/{appId}.
- * Games and Matrix remain on separate subdomains.
+ * Games remain on separate subdomains.
*/
export const APP_URLS: Record = {
// ─── Unified App (internal paths) ─────────────────────────
@@ -837,7 +820,6 @@ export const APP_URLS: Record = {
news: { dev: 'http://localhost:5173/news', prod: 'https://mana.how/news' },
mail: { dev: 'http://localhost:5173/mail', prod: 'https://mana.how/mail' },
// ─── Separate Apps (own subdomains) ───────────────────────
- matrix: { dev: 'http://localhost:5180', prod: 'https://matrix.mana.how' },
arcade: { dev: 'http://localhost:5201', prod: 'https://arcade.mana.how' },
};
diff --git a/scripts/audit-workspace-deps.mjs b/scripts/audit-workspace-deps.mjs
index 5bfe47100..5c166c2a1 100755
--- a/scripts/audit-workspace-deps.mjs
+++ b/scripts/audit-workspace-deps.mjs
@@ -50,7 +50,6 @@ const WORKSPACE_SCOPES = new Set([
'@clock',
'@contacts',
'@context',
- '@matrix',
'@music',
'@nutriphi',
'@photos',
diff --git a/scripts/mac-mini/build-app.sh b/scripts/mac-mini/build-app.sh
index 6b9f8c972..120da1474 100755
--- a/scripts/mac-mini/build-app.sh
+++ b/scripts/mac-mini/build-app.sh
@@ -146,7 +146,6 @@ if [ $# -eq 0 ]; then
echo " $0 todo-web todo-backend # Build & restart both"
echo " $0 --base # Rebuild base images"
echo " $0 --all-web # Rebuild all web apps"
- echo " $0 mana-matrix-bot # Build & restart consolidated Matrix bot (Go)"
echo " $0 --force-free todo-web # Force stop monitoring before build"
exit 1
fi
diff --git a/scripts/mac-mini/deploy-v2.sh b/scripts/mac-mini/deploy-v2.sh
index cf8c380b4..5e493978c 100755
--- a/scripts/mac-mini/deploy-v2.sh
+++ b/scripts/mac-mini/deploy-v2.sh
@@ -5,7 +5,7 @@
# Deploys the complete Mana stack:
# - Infrastructure: PostgreSQL, Redis, MinIO, SearXNG
# - Core Services: mana-auth, mana-credits, mana-user, mana-subscriptions, mana-analytics
-# - Go Services: mana-sync, mana-search, mana-crawler, mana-api-gateway, mana-notify, mana-matrix-bot
+# - Go Services: mana-sync, mana-search, mana-crawler, mana-api-gateway, mana-notify
# - Python AI: mana-llm, mana-stt, mana-tts, mana-image-gen
# - App Frontends: 19 SvelteKit web apps
#
diff --git a/scripts/mac-mini/ensure-containers-running.sh b/scripts/mac-mini/ensure-containers-running.sh
index e3a0bd8d0..a02249c83 100755
--- a/scripts/mac-mini/ensure-containers-running.sh
+++ b/scripts/mac-mini/ensure-containers-running.sh
@@ -174,10 +174,6 @@ for container in $ALL_PROBLEM_CONTAINERS; do
mana-infra-postgres) SERVICE_NAME="postgres" ;;
mana-infra-redis) SERVICE_NAME="redis" ;;
mana-infra-minio) SERVICE_NAME="minio" ;;
- mana-matrix-synapse) SERVICE_NAME="synapse" ;;
- mana-matrix-element) SERVICE_NAME="element-web" ;;
- mana-matrix-web) SERVICE_NAME="matrix-web" ;;
- mana-matrix-bot-*) SERVICE_NAME="${container#mana-matrix-bot-}"; SERVICE_NAME="matrix-${SERVICE_NAME}-bot" ;;
mana-mon-*) SERVICE_NAME="${container#mana-mon-}" ;;
mana-auto-*) SERVICE_NAME="${container#mana-auto-}" ;;
mana-service-*) SERVICE_NAME="${container#mana-service-}" ;;
diff --git a/scripts/mac-mini/memory-baseline.sh b/scripts/mac-mini/memory-baseline.sh
index fafc2e441..f4546ff08 100755
--- a/scripts/mac-mini/memory-baseline.sh
+++ b/scripts/mac-mini/memory-baseline.sh
@@ -79,17 +79,15 @@ get_category_mem() {
infra=$(get_category_mem "mana-infra")
core=$(get_category_mem "mana-core\|mana-auth\|mana-credits\|mana-user\|mana-subscriptions\|mana-analytics\|mana-api-gateway\|mana-crawler\|mana-service")
-matrix=$(get_category_mem "mana-matrix")
apps=$(get_category_mem "mana-app")
monitoring=$(get_category_mem "mana-mon")
games=$(get_category_mem "mana-game")
auto=$(get_category_mem "mana-auto")
-total=$((infra + core + matrix + apps + monitoring + games + auto))
+total=$((infra + core + apps + monitoring + games + auto))
printf "%-25s %8s MiB\n" "Infrastructure:" "$infra"
printf "%-25s %8s MiB\n" "Core Services:" "$core"
-printf "%-25s %8s MiB\n" "Matrix Stack:" "$matrix"
printf "%-25s %8s MiB\n" "Web Apps:" "$apps"
printf "%-25s %8s MiB\n" "Monitoring:" "$monitoring"
printf "%-25s %8s MiB\n" "Games:" "$games"
diff --git a/scripts/mac-mini/migrate-to-colima.sh b/scripts/mac-mini/migrate-to-colima.sh
index 6fcd6fc3b..2dec93e0a 100755
--- a/scripts/mac-mini/migrate-to-colima.sh
+++ b/scripts/mac-mini/migrate-to-colima.sh
@@ -118,7 +118,6 @@ NAMED_VOLUMES=(
"mana-grafana-data"
"mana-analytics-data"
"mana-loki-data"
- "mana-matrix-bots-data"
)
if [ "$DRY_RUN" = true ]; then
diff --git a/scripts/mac-mini/setup-matrix.sh b/scripts/mac-mini/setup-matrix.sh
deleted file mode 100755
index 81e73a34a..000000000
--- a/scripts/mac-mini/setup-matrix.sh
+++ /dev/null
@@ -1,123 +0,0 @@
-#!/bin/bash
-# Setup Matrix Synapse on Mac Mini
-# Run this script once to initialize Matrix
-
-set -e
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
-MATRIX_DIR="$PROJECT_DIR/docker/matrix"
-
-echo "============================================"
-echo " Mana Matrix Setup"
-echo "============================================"
-echo ""
-
-# Colors
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-NC='\033[0m'
-
-# Check if postgres is running
-echo "Checking PostgreSQL..."
-if ! docker exec mana-postgres pg_isready -U postgres > /dev/null 2>&1; then
- echo -e "${RED}Error: PostgreSQL is not running.${NC}"
- echo "Start it with: docker compose -f docker-compose.macmini.yml up -d postgres"
- exit 1
-fi
-echo -e "${GREEN}PostgreSQL is running${NC}"
-
-# Create matrix database
-echo ""
-echo "Creating Matrix database..."
-if docker exec mana-postgres psql -U postgres -lqt | cut -d \| -f 1 | grep -qw matrix; then
- echo -e "${YELLOW}Database 'matrix' already exists${NC}"
-else
- docker exec mana-postgres psql -U postgres -c "CREATE DATABASE matrix;"
- echo -e "${GREEN}Database 'matrix' created${NC}"
-fi
-
-# Create synapse user
-echo ""
-echo "Creating Synapse database user..."
-if docker exec mana-postgres psql -U postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='synapse'" | grep -q 1; then
- echo -e "${YELLOW}User 'synapse' already exists${NC}"
-else
- # Generate a random password if not set
- SYNAPSE_DB_PASSWORD=${SYNAPSE_DB_PASSWORD:-$(openssl rand -base64 24)}
- docker exec mana-postgres psql -U postgres -c "CREATE USER synapse WITH PASSWORD '$SYNAPSE_DB_PASSWORD';"
- docker exec mana-postgres psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE matrix TO synapse;"
- docker exec mana-postgres psql -U postgres -c "ALTER DATABASE matrix OWNER TO synapse;"
- echo -e "${GREEN}User 'synapse' created${NC}"
- echo ""
- echo -e "${YELLOW}IMPORTANT: Add this to your .env file:${NC}"
- echo "SYNAPSE_DB_PASSWORD=$SYNAPSE_DB_PASSWORD"
-fi
-
-# Create logs directory in volume
-echo ""
-echo "Creating logs directory..."
-mkdir -p "$MATRIX_DIR/logs" 2>/dev/null || true
-
-# Generate signing key if not exists
-echo ""
-echo "Checking signing key..."
-if docker volume ls | grep -q mana-synapse; then
- echo -e "${YELLOW}Synapse volume already exists - signing key should be present${NC}"
-else
- echo "Signing key will be generated on first Synapse start"
-fi
-
-# Generate secrets if not set
-echo ""
-echo "============================================"
-echo " Required Environment Variables"
-echo "============================================"
-echo ""
-echo "Add these to your .env file (generate secure values!):"
-echo ""
-
-# Generate random secrets for display
-echo "SYNAPSE_DB_PASSWORD=$(openssl rand -base64 24)"
-echo "SYNAPSE_PASSWORD_PEPPER=$(openssl rand -base64 32)"
-echo "SYNAPSE_FORM_SECRET=$(openssl rand -base64 32)"
-echo "SYNAPSE_MACAROON_SECRET=$(openssl rand -base64 32)"
-echo "SYNAPSE_REGISTRATION_SECRET=$(openssl rand -base64 32)"
-
-echo ""
-echo "============================================"
-echo " Cloudflare Tunnel Configuration"
-echo "============================================"
-echo ""
-echo "Add these ingress rules to ~/.cloudflared/config.yml:"
-echo ""
-echo " - hostname: matrix.mana.how"
-echo " service: http://localhost:8008"
-echo ""
-echo " - hostname: element.mana.how"
-echo " service: http://localhost:8087"
-echo ""
-
-echo ""
-echo "============================================"
-echo " Next Steps"
-echo "============================================"
-echo ""
-echo "1. Add environment variables to .env file"
-echo "2. Update Cloudflare Tunnel config"
-echo "3. Start Matrix services:"
-echo " docker compose -f docker-compose.macmini.yml up -d synapse element-web"
-echo ""
-echo "4. Wait for Synapse to start (check logs):"
-echo " docker logs -f mana-synapse"
-echo ""
-echo "5. Create admin user:"
-echo " docker exec -it mana-synapse register_new_matrix_user \\"
-echo " -c /data/homeserver.yaml http://localhost:8008 -a"
-echo ""
-echo "6. Test endpoints:"
-echo " curl https://matrix.mana.how/health"
-echo " open https://element.mana.how"
-echo ""
-echo -e "${GREEN}Setup complete!${NC}"
diff --git a/scripts/mac-mini/setup-tts-bot.sh b/scripts/mac-mini/setup-tts-bot.sh
deleted file mode 100755
index db35ef5c0..000000000
--- a/scripts/mac-mini/setup-tts-bot.sh
+++ /dev/null
@@ -1,160 +0,0 @@
-#!/bin/bash
-# Register and setup Matrix TTS Bot
-# Run this after Matrix Synapse is running
-
-set -e
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
-
-# Colors
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-CYAN='\033[0;36m'
-NC='\033[0m'
-
-echo "============================================"
-echo " Matrix TTS Bot Setup"
-echo "============================================"
-echo ""
-
-# Default values
-HOMESERVER_URL="${MATRIX_HOMESERVER_URL:-http://localhost:8008}"
-BOT_USERNAME="tts"
-BOT_DISPLAY_NAME="TTS"
-
-# Check if Synapse is running
-echo "Checking Synapse..."
-if ! curl -s "${HOMESERVER_URL}/health" > /dev/null 2>&1; then
- echo -e "${RED}Error: Synapse is not reachable at ${HOMESERVER_URL}${NC}"
- echo "Start it with: docker compose -f docker-compose.macmini.yml up -d synapse"
- exit 1
-fi
-echo -e "${GREEN}Synapse is running${NC}"
-echo ""
-
-# Check if registration secret is available
-if [ -z "$SYNAPSE_REGISTRATION_SECRET" ]; then
- echo -e "${YELLOW}SYNAPSE_REGISTRATION_SECRET not set.${NC}"
- echo "Please provide the registration secret from your .env file:"
- read -sp "Registration Secret: " SYNAPSE_REGISTRATION_SECRET
- echo ""
-fi
-
-# Generate bot password
-BOT_PASSWORD=$(openssl rand -base64 24)
-
-echo "Registering bot user @${BOT_USERNAME}..."
-
-# Generate HMAC for registration
-generate_mac() {
- local nonce=$1
- local user=$2
- local password=$3
- local user_type=$4
- local admin=$5
-
- local mac_input="${nonce}\x00${user}\x00${password}\x00${user_type}\x00${admin}"
- echo -n "$mac_input" | openssl dgst -sha1 -hmac "$SYNAPSE_REGISTRATION_SECRET" | cut -d' ' -f2
-}
-
-# Get nonce
-NONCE=$(curl -s "${HOMESERVER_URL}/_synapse/admin/v1/register" | jq -r '.nonce')
-
-if [ -z "$NONCE" ] || [ "$NONCE" = "null" ]; then
- echo -e "${RED}Failed to get registration nonce. Is admin registration enabled?${NC}"
- exit 1
-fi
-
-# Calculate MAC
-MAC=$(generate_mac "$NONCE" "$BOT_USERNAME" "$BOT_PASSWORD" "bot" "false")
-
-# Register user
-REGISTER_RESPONSE=$(curl -s -X POST "${HOMESERVER_URL}/_synapse/admin/v1/register" \
- -H "Content-Type: application/json" \
- -d "{
- \"nonce\": \"${NONCE}\",
- \"username\": \"${BOT_USERNAME}\",
- \"password\": \"${BOT_PASSWORD}\",
- \"displayname\": \"${BOT_DISPLAY_NAME}\",
- \"user_type\": \"bot\",
- \"admin\": false,
- \"mac\": \"${MAC}\"
- }")
-
-# Check if registration was successful
-if echo "$REGISTER_RESPONSE" | jq -e '.access_token' > /dev/null 2>&1; then
- ACCESS_TOKEN=$(echo "$REGISTER_RESPONSE" | jq -r '.access_token')
- USER_ID=$(echo "$REGISTER_RESPONSE" | jq -r '.user_id')
-
- echo -e "${GREEN}Bot registered successfully!${NC}"
- echo ""
- echo -e "${CYAN}User ID:${NC} ${USER_ID}"
- echo ""
-else
- ERROR=$(echo "$REGISTER_RESPONSE" | jq -r '.error // .errcode // "Unknown error"')
-
- # Check if user already exists
- if echo "$ERROR" | grep -qi "user.*exists\|already.*registered\|M_USER_IN_USE"; then
- echo -e "${YELLOW}User @${BOT_USERNAME} already exists. Getting access token via login...${NC}"
-
- echo "Please enter the existing bot password:"
- read -sp "Password: " EXISTING_PASSWORD
- echo ""
-
- LOGIN_RESPONSE=$(curl -s -X POST "${HOMESERVER_URL}/_matrix/client/r0/login" \
- -H "Content-Type: application/json" \
- -d "{
- \"type\": \"m.login.password\",
- \"user\": \"${BOT_USERNAME}\",
- \"password\": \"${EXISTING_PASSWORD}\"
- }")
-
- if echo "$LOGIN_RESPONSE" | jq -e '.access_token' > /dev/null 2>&1; then
- ACCESS_TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.access_token')
- USER_ID=$(echo "$LOGIN_RESPONSE" | jq -r '.user_id')
- echo -e "${GREEN}Login successful!${NC}"
- else
- echo -e "${RED}Login failed. Please check the password.${NC}"
- exit 1
- fi
- else
- echo -e "${RED}Registration failed: ${ERROR}${NC}"
- exit 1
- fi
-fi
-
-echo ""
-echo "============================================"
-echo " Add to .env file"
-echo "============================================"
-echo ""
-echo -e "${CYAN}# Matrix TTS Bot${NC}"
-echo "MATRIX_TTS_BOT_TOKEN=${ACCESS_TOKEN}"
-echo ""
-
-# Optional: Set display name and avatar
-echo "Setting display name..."
-curl -s -X PUT "${HOMESERVER_URL}/_matrix/client/r0/profile/${USER_ID}/displayname" \
- -H "Authorization: Bearer ${ACCESS_TOKEN}" \
- -H "Content-Type: application/json" \
- -d "{\"displayname\": \"🔊 ${BOT_DISPLAY_NAME}\"}" > /dev/null
-
-echo ""
-echo "============================================"
-echo " Next Steps"
-echo "============================================"
-echo ""
-echo "1. Add the MATRIX_TTS_BOT_TOKEN to your .env file"
-echo ""
-echo "2. Build the bot image:"
-echo " docker build -t matrix-tts-bot ./services/matrix-tts-bot"
-echo ""
-echo "3. Start the bot:"
-echo " docker compose -f docker-compose.macmini.yml up -d matrix-tts-bot"
-echo ""
-echo "4. Invite the bot to a room in Element:"
-echo " /invite @tts:mana.how"
-echo ""
-echo -e "${GREEN}Setup complete!${NC}"
diff --git a/scripts/mac-mini/startup.sh b/scripts/mac-mini/startup.sh
index a6fc468bd..46e7e0e72 100755
--- a/scripts/mac-mini/startup.sh
+++ b/scripts/mac-mini/startup.sh
@@ -110,7 +110,7 @@ log "Docker CLI connected"
# ─── Restore named volumes if missing ───
BACKUP_DIR="/Volumes/ManaData/backups/docker-migration-20260328"
-for vol in mana-redis-data mana-victoria-data mana-alertmanager-data mana-grafana-data mana-analytics-data mana-loki-data mana-matrix-bots-data; do
+for vol in mana-redis-data mana-victoria-data mana-alertmanager-data mana-grafana-data mana-analytics-data mana-loki-data; do
if ! docker volume inspect "$vol" >/dev/null 2>&1; then
BACKUP_FILE="$BACKUP_DIR/${vol}.tar.gz"
if [ -f "$BACKUP_FILE" ]; then
@@ -137,11 +137,6 @@ log "Starting Docker containers..."
cd "$PROJECT_ROOT"
docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --no-build 2>&1 | tee -a "$LOG_FILE"
-# ─── Force-recreate stateful containers that cache config ───
-# synapse copies homeserver.yaml at startup; stale container uses old cached config
-log "Force-recreating config-sensitive containers..."
-docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d --no-build --force-recreate synapse 2>&1 | tee -a "$LOG_FILE"
-
# ─── Wait and verify ───
log "Waiting 45s for services to initialize..."
sleep 45
@@ -155,12 +150,4 @@ for db in mana_auth mana_credits chat todo calendar clock contacts storage umami
docker exec mana-infra-postgres psql -U postgres -c "CREATE DATABASE $db;" 2>/dev/null || true
done
-# Matrix Synapse: needs its own user and C-locale database
-docker exec mana-infra-postgres psql -U postgres -c \
- "DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname='synapse') THEN CREATE USER synapse WITH PASSWORD 'synapse-secure-password'; END IF; END \$\$;" \
- 2>/dev/null || true
-docker exec mana-infra-postgres psql -U postgres -c \
- "CREATE DATABASE matrix OWNER synapse ENCODING UTF8 LC_COLLATE='C' LC_CTYPE='C' TEMPLATE template0;" \
- 2>/dev/null || true
-
log "=== Startup Complete ($RUNNING containers running) ==="
diff --git a/services/mana-auth/CLAUDE.md b/services/mana-auth/CLAUDE.md
index b7842b2f5..63d1a2291 100644
--- a/services/mana-auth/CLAUDE.md
+++ b/services/mana-auth/CLAUDE.md
@@ -19,9 +19,8 @@ Central authentication service for the Mana ecosystem. Hono + Bun + Better Auth.
1. **Organization** — B2B multi-tenant with RBAC
2. **JWT** — EdDSA tokens with minimal claims (sub, email, role, sid)
-3. **OIDC Provider** — Matrix/Synapse SSO
-4. **Two-Factor** — TOTP with backup codes
-5. **Magic Link** — Passwordless email login
+3. **Two-Factor** — TOTP with backup codes
+4. **Magic Link** — Passwordless email login
## Key Endpoints
@@ -37,9 +36,6 @@ Handled directly by Better Auth — includes sign-in, sign-up, session, 2FA, mag
| POST | `/validate` | Validate JWT token |
| GET | `/session` | Get current session |
-### OIDC (`/.well-known/*`, `/api/auth/oauth2/*`)
-OpenID Connect provider for Matrix/Synapse SSO.
-
### Me — GDPR Self-Service (`/api/v1/me/*`)
| Method | Path | Description |
|--------|------|-------------|
@@ -103,7 +99,6 @@ SMTP_HOST=stalwart # self-hosted on Mac Mini, see docs/MAIL_SERVER.md
SMTP_PORT=587
SMTP_USER=...
SMTP_PASS=...
-SYNAPSE_OIDC_CLIENT_SECRET=...
# Encryption Vault — REQUIRED IN PRODUCTION
# Base64-encoded 32-byte AES-256 key. Generate with `openssl rand -base64 32`.
diff --git a/services/mana-auth/src/auth/better-auth.config.ts b/services/mana-auth/src/auth/better-auth.config.ts
index dc01db350..1494de1e8 100644
--- a/services/mana-auth/src/auth/better-auth.config.ts
+++ b/services/mana-auth/src/auth/better-auth.config.ts
@@ -18,7 +18,6 @@ import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { jwt } from 'better-auth/plugins/jwt';
import { organization } from 'better-auth/plugins/organization';
-import { oidcProvider } from 'better-auth/plugins/oidc-provider';
import { twoFactor } from 'better-auth/plugins/two-factor';
import { magicLink } from 'better-auth/plugins/magic-link';
import { getDb } from '../db/connection';
@@ -29,10 +28,6 @@ import {
accounts,
verificationTokens,
jwks,
- oauthApplications,
- oauthAccessTokens,
- oauthAuthorizationCodes,
- oauthConsents,
twoFactorAuth,
} from '../db/schema/auth';
import {
@@ -103,12 +98,6 @@ export function createBetterAuth(databaseUrl: string) {
// Two-Factor Authentication table
twoFactor: twoFactorAuth,
-
- // OIDC Provider tables
- oauthApplication: oauthApplications,
- oauthAccessToken: oauthAccessTokens,
- oauthAuthorizationCode: oauthAuthorizationCodes,
- oauthConsent: oauthConsents,
},
}),
@@ -258,9 +247,6 @@ export function createBetterAuth(databaseUrl: string) {
// Separate apps (not part of unified app)
'https://arcade.mana.how', // Games
'https://whopxl.mana.how', // Games
- 'https://link.mana.how', // Matrix/Manalink
- 'https://element.mana.how', // Element (Matrix client)
- 'https://matrix.mana.how', // Matrix
// Local development
'http://localhost:3001',
'http://localhost:5173',
@@ -365,45 +351,6 @@ export function createBetterAuth(databaseUrl: string) {
},
}),
- /**
- * OIDC Provider Plugin
- *
- * Enables Mana Core Auth to act as an OpenID Connect Provider.
- * This allows Matrix/Synapse and other services to use SSO.
- *
- * Endpoints provided:
- * - GET /.well-known/openid-configuration
- * - GET /api/oidc/authorize
- * - POST /api/oidc/token
- * - GET /api/oidc/userinfo
- * - GET /api/oidc/jwks
- */
- oidcProvider({
- // Login page for OIDC authorization
- loginPage: '/login',
- // Consent page (skipped for trusted clients)
- consentPage: '/consent',
- // Use JWT plugin for token signing (EdDSA instead of HS256)
- // This is required for Synapse OIDC which verifies via JWKS
- useJWTPlugin: true,
- metadata: {
- issuer: process.env.BASE_URL || 'http://localhost:3001',
- },
- // Trusted clients that skip consent screen
- // These clients are considered first-party and don't need user consent
- trustedClients: [
- {
- clientId: 'matrix-synapse',
- clientSecret: process.env.SYNAPSE_OIDC_CLIENT_SECRET || '',
- name: 'Matrix Synapse',
- type: 'web',
- disabled: false,
- metadata: {},
- redirectUrls: ['https://matrix.mana.how/_synapse/client/oidc/callback'],
- skipConsent: true,
- },
- ],
- }),
/**
* Two-Factor Authentication Plugin (TOTP)
*
diff --git a/services/mana-auth/src/config.ts b/services/mana-auth/src/config.ts
index 75b3c9b03..5465e0c70 100644
--- a/services/mana-auth/src/config.ts
+++ b/services/mana-auth/src/config.ts
@@ -10,7 +10,6 @@ export interface Config {
manaNotifyUrl: string;
manaCreditsUrl: string;
manaSubscriptionsUrl: string;
- synapseOidcClientSecret: string;
/** Base64-encoded 32-byte AES-256 key encryption key (KEK). Wraps each
* user's master key in auth.encryption_vaults. Required in production
* — in development a deterministic dev KEK is auto-generated so the
@@ -55,7 +54,6 @@ export function loadConfig(): Config {
manaNotifyUrl: env('MANA_NOTIFY_URL', 'http://localhost:3013'),
manaCreditsUrl: env('MANA_CREDITS_URL', 'http://localhost:3061'),
manaSubscriptionsUrl: env('MANA_SUBSCRIPTIONS_URL', 'http://localhost:3063'),
- synapseOidcClientSecret: env('SYNAPSE_OIDC_CLIENT_SECRET'),
encryptionKek,
};
}
diff --git a/services/mana-auth/src/db/schema/auth.ts b/services/mana-auth/src/db/schema/auth.ts
index cdcaf5b24..8249d5f11 100644
--- a/services/mana-auth/src/db/schema/auth.ts
+++ b/services/mana-auth/src/db/schema/auth.ts
@@ -139,87 +139,6 @@ export const jwks = authSchema.table('jwks', {
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
});
-// OIDC Provider tables (Better Auth OIDC Provider plugin)
-// OAuth Applications (OIDC Clients like Matrix/Synapse)
-export const oauthApplications = authSchema.table('oauth_applications', {
- id: text('id').primaryKey(),
- name: text('name').notNull(),
- icon: text('icon'),
- metadata: text('metadata'),
- clientId: text('client_id').unique().notNull(),
- clientSecret: text('client_secret').notNull(),
- redirectUrls: text('redirect_urls').notNull(), // Comma-separated URLs (Better Auth expects 'redirectUrls' property name)
- type: text('type').notNull().default('web'), // web, native, spa
- disabled: boolean('disabled').default(false).notNull(),
- userId: text('user_id').references(() => users.id, { onDelete: 'cascade' }),
- createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
- updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
-});
-
-// OAuth Access Tokens
-export const oauthAccessTokens = authSchema.table('oauth_access_tokens', {
- id: text('id').primaryKey(),
- accessToken: text('access_token').unique().notNull(),
- refreshToken: text('refresh_token').unique(),
- accessTokenExpiresAt: timestamp('access_token_expires_at', { withTimezone: true }).notNull(),
- refreshTokenExpiresAt: timestamp('refresh_token_expires_at', { withTimezone: true }),
- clientId: text('client_id').notNull(),
- userId: text('user_id')
- .references(() => users.id, { onDelete: 'cascade' })
- .notNull(),
- scopes: text('scopes').notNull(), // JSON array as text
- createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
- updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
-});
-
-// OAuth Authorization Codes
-export const oauthAuthorizationCodes = authSchema.table('oauth_authorization_codes', {
- id: text('id').primaryKey(),
- code: text('code').unique().notNull(),
- clientId: text('client_id').notNull(),
- userId: text('user_id')
- .references(() => users.id, { onDelete: 'cascade' })
- .notNull(),
- scopes: text('scopes').notNull(), // JSON array as text
- redirectUri: text('redirect_uri').notNull(),
- codeChallenge: text('code_challenge'),
- codeChallengeMethod: text('code_challenge_method'),
- expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
- createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
-});
-
-// OAuth Consents (user consent records for OIDC scopes)
-export const oauthConsents = authSchema.table('oauth_consents', {
- id: text('id').primaryKey(),
- userId: text('user_id')
- .references(() => users.id, { onDelete: 'cascade' })
- .notNull(),
- clientId: text('client_id').notNull(),
- scopes: text('scopes').notNull(), // JSON array as text
- createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
- updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
-});
-
-// Matrix User Links table (for Bot SSO)
-// Links Matrix user IDs to Mana user accounts for automatic bot authentication
-export const matrixUserLinks = authSchema.table(
- 'matrix_user_links',
- {
- id: text('id').primaryKey(), // nanoid
- matrixUserId: text('matrix_user_id').unique().notNull(), // e.g., @user:matrix.mana.how
- userId: text('user_id')
- .references(() => users.id, { onDelete: 'cascade' })
- .notNull(),
- linkedAt: timestamp('linked_at', { withTimezone: true }).defaultNow().notNull(),
- lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
- // Optional: store email for convenience (denormalized from users table)
- email: text('email'),
- },
- (table) => ({
- userIdIdx: index('matrix_user_links_user_id_idx').on(table.userId),
- })
-);
-
// Passkeys table (WebAuthn credentials)
export const passkeys = authSchema.table(
'passkeys',
diff --git a/services/mana-auth/src/routes/auth.ts b/services/mana-auth/src/routes/auth.ts
index 3fc81f549..4971167fe 100644
--- a/services/mana-auth/src/routes/auth.ts
+++ b/services/mana-auth/src/routes/auth.ts
@@ -109,46 +109,77 @@ export function createAuthRoutes(
const ip = c.req.header('x-forwarded-for') || 'unknown';
try {
- const response = await auth.api.signInEmail({
- body: { email: body.email, password: body.password },
- headers: c.req.raw.headers,
- });
+ // Sign in via Better Auth's HTTP handler so we get back a real
+ // Response with Set-Cookie. The auth.api.signInEmail() SDK call
+ // only returns the body and we'd lose the signed cookie envelope
+ // that /api/auth/token needs to validate the session — the cookie
+ // value is `.`, not just the raw session token,
+ // so reconstructing it from the API response doesn't work.
+ const signInResponse = await auth.handler(
+ new Request(new URL('/api/auth/sign-in/email', config.baseUrl), {
+ method: 'POST',
+ headers: new Headers({
+ 'Content-Type': 'application/json',
+ // Forward original X-Forwarded-For so Better Auth's rate
+ // limiting and our security log see the right IP.
+ ...(c.req.header('x-forwarded-for')
+ ? { 'X-Forwarded-For': c.req.header('x-forwarded-for') as string }
+ : {}),
+ }),
+ body: JSON.stringify({ email: body.email, password: body.password }),
+ })
+ );
+
+ if (!signInResponse.ok) {
+ // Better Auth returns 403 with FORBIDDEN for unverified emails.
+ if (signInResponse.status === 403) {
+ return c.json({ error: 'Email not verified', code: 'EMAIL_NOT_VERIFIED' }, 403);
+ }
+ security.logEvent({
+ eventType: 'LOGIN_FAILURE',
+ ipAddress: ip,
+ metadata: { email: body.email },
+ });
+ lockout.recordAttempt(body.email, false, ip);
+ return c.json({ error: 'Invalid credentials' }, 401);
+ }
+
+ const response = (await signInResponse.json()) as {
+ user?: { id: string };
+ token?: string;
+ redirect?: boolean;
+ };
if (response?.user?.id) {
security.logEvent({ userId: response.user.id, eventType: 'LOGIN_SUCCESS', ipAddress: ip });
lockout.clearAttempts(body.email);
}
- // signInEmail returns { token (session token), user, redirect }
- // Use the session token to call Better Auth's JWT /token endpoint.
- //
- // In production Better Auth issues the session cookie with the
- // __Secure- prefix (because secure: true is set), so we have to
- // pass that exact cookie name back when forging the request to
- // /api/auth/token. Without the prefix the get-session middleware
- // can't find the session and the JWT mint silently fails — the
- // route falls through and returns a response without accessToken.
- const sessionToken = response?.token;
- if (sessionToken) {
- const cookieName =
- config.nodeEnv === 'production' ? '__Secure-mana.session_token' : 'mana.session_token';
+ // Capture the signed session cookie that Better Auth set on the
+ // sign-in response and forward it verbatim to /api/auth/token to
+ // mint a JWT. This is the only path that produces a cookie value
+ // with a valid HMAC signature.
+ const setCookie = signInResponse.headers.get('set-cookie');
+ if (setCookie) {
const tokenResponse = await auth.handler(
new Request(new URL('/api/auth/token', config.baseUrl), {
method: 'GET',
- headers: new Headers({ cookie: `${cookieName}=${sessionToken}` }),
+ headers: new Headers({ cookie: setCookie }),
})
);
if (tokenResponse.ok) {
- const tokenData = await tokenResponse.json();
+ const tokenData = (await tokenResponse.json()) as { token: string };
return c.json({
...response,
accessToken: tokenData.token,
- refreshToken: sessionToken,
+ refreshToken: response.token,
});
}
}
+ // JWT mint failed (or no Set-Cookie came back). Still return the
+ // sign-in body so the client at least sees the user object.
return c.json(response);
} catch (error) {
// Better Auth throws APIError with status="FORBIDDEN" for unverified emails.
diff --git a/services/mana-matrix-bot/.gitignore b/services/mana-matrix-bot/.gitignore
deleted file mode 100644
index 0e46a3f3d..000000000
--- a/services/mana-matrix-bot/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-dist/
-data/
-*.json.bak
diff --git a/services/mana-matrix-bot/CLAUDE.md b/services/mana-matrix-bot/CLAUDE.md
deleted file mode 100644
index de7829a46..000000000
--- a/services/mana-matrix-bot/CLAUDE.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# mana-matrix-bot
-
-Consolidated Go Matrix bot replacing 21 separate NestJS bot services.
-
-## Architecture
-
-- **Language:** Go 1.23
-- **Matrix SDK:** mautrix-go
-- **Port:** 4000 (health/metrics)
-- **Pattern:** Plugin architecture with compile-time registration
-
-## Structure
-
-```
-cmd/server/main.go # Entry point, imports all plugins
-internal/
- config/ # Env-based configuration
- runtime/ # Plugin lifecycle, Matrix sync, event routing
- matrix/ # Matrix client wrapper, markdown, media
- plugin/ # Plugin interface, registry, command routing
- session/ # In-memory + Redis session store
- services/ # Backend HTTP client, voice (STT/TTS)
- plugins/ # One directory per bot plugin
- todo/ # @todo-bot
- calendar/ # @calendar-bot
- gateway/ # @mana-bot (composite: AI + todo + calendar + clock + voice)
- ...
-```
-
-## Adding a New Plugin
-
-1. Create `internal/plugins/mybot/mybot.go`
-2. Implement `plugin.Plugin` interface
-3. Register via `func init() { plugin.Register("mybot", func() plugin.Plugin { return &MyBot{} }) }`
-4. Import in `cmd/server/main.go`: `_ "github.com/mana/mana-matrix-bot/internal/plugins/mybot"`
-5. Set env: `MATRIX_MYBOT_BOT_TOKEN=syt_xxx`
-
-## Commands
-
-```bash
-# Build
-go build -o dist/mana-matrix-bot ./cmd/server
-
-# Run
-PORT=4000 MATRIX_HOMESERVER_URL=http://localhost:8008 MATRIX_TODO_BOT_TOKEN=xxx ./dist/mana-matrix-bot
-
-# Test
-go test ./...
-
-# Docker
-docker build -t mana-matrix-bot:local -f Dockerfile .
-```
-
-## Environment Variables
-
-### Global
-- `PORT` — Health server port (default: 4000)
-- `MATRIX_HOMESERVER_URL` — Matrix homeserver (default: http://localhost:8008)
-- `MATRIX_STORAGE_PATH` — Sync state directory (default: ./data)
-- `MANA_AUTH_URL` — Auth service URL
-- `REDIS_HOST`, `REDIS_PORT`, `REDIS_PASSWORD` — Redis for sessions
-- `STT_URL`, `TTS_URL` — Voice services
-
-### Per Plugin (legacy env var names supported)
-- `MATRIX_{NAME}_BOT_TOKEN` — Matrix access token
-- `MATRIX_{NAME}_BOT_ROOMS` — Comma-separated allowed room IDs
-- `{NAME}_BACKEND_URL` — Backend service URL
diff --git a/services/mana-matrix-bot/Dockerfile b/services/mana-matrix-bot/Dockerfile
deleted file mode 100644
index 1015a39c5..000000000
--- a/services/mana-matrix-bot/Dockerfile
+++ /dev/null
@@ -1,28 +0,0 @@
-# Build stage
-FROM golang:1.25-alpine AS builder
-
-WORKDIR /app
-
-# Copy Go module files first for better caching
-COPY services/mana-matrix-bot/go.mod services/mana-matrix-bot/go.sum ./
-RUN go mod download
-
-# Copy source
-COPY services/mana-matrix-bot/ .
-RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /mana-matrix-bot ./cmd/server
-
-# Runtime stage
-FROM alpine:3.21
-
-RUN apk --no-cache add ca-certificates tzdata
-
-COPY --from=builder /mana-matrix-bot /usr/local/bin/mana-matrix-bot
-
-VOLUME /app/data
-
-EXPOSE 4000
-
-HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
- CMD wget -q --spider http://localhost:4000/health || exit 1
-
-ENTRYPOINT ["mana-matrix-bot"]
diff --git a/services/mana-matrix-bot/cmd/server/main.go b/services/mana-matrix-bot/cmd/server/main.go
deleted file mode 100644
index d2a3e665a..000000000
--- a/services/mana-matrix-bot/cmd/server/main.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package main
-
-import (
- "context"
- "log/slog"
- "os"
- "os/signal"
- "syscall"
- "time"
-
- "github.com/mana/mana-matrix-bot/internal/config"
- "github.com/mana/mana-matrix-bot/internal/runtime"
-
- // Import all plugins to trigger their init() registration.
- _ "github.com/mana/mana-matrix-bot/internal/plugins/calendar"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/chat"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/clock"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/contacts"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/gateway"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/cards"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/nutriphi"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/ollama"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/onboarding"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/picture"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/planta"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/presi"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/projectdoc"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/questions"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/skilltree"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/stats"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/storage"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/stt"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/todo"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/tts"
- _ "github.com/mana/mana-matrix-bot/internal/plugins/zitare"
-)
-
-func main() {
- // Structured JSON logging
- slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
- Level: slog.LevelInfo,
- })))
-
- cfg := config.Load()
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- // Create and start runtime
- rt := runtime.New(cfg)
-
- // Start health server
- health := runtime.NewHealthServer(rt, cfg.Port)
- httpServer := health.Start()
-
- // Start all plugins
- if err := rt.Start(ctx); err != nil {
- slog.Error("failed to start runtime", "error", err)
- os.Exit(1)
- }
-
- slog.Info("mana-matrix-bot running", "port", cfg.Port)
-
- // Wait for shutdown signal
- sigCh := make(chan os.Signal, 1)
- signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
- <-sigCh
-
- slog.Info("shutting down...")
- cancel()
- rt.Stop()
-
- shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer shutdownCancel()
- httpServer.Shutdown(shutdownCtx)
-
- slog.Info("shutdown complete")
-}
diff --git a/services/mana-matrix-bot/go.mod b/services/mana-matrix-bot/go.mod
deleted file mode 100644
index 85bdb0617..000000000
--- a/services/mana-matrix-bot/go.mod
+++ /dev/null
@@ -1,28 +0,0 @@
-module github.com/mana/mana-matrix-bot
-
-go 1.25.0
-
-require (
- github.com/redis/go-redis/v9 v9.18.0
- maunium.net/go/mautrix v0.26.4
-)
-
-require (
- filippo.io/edwards25519 v1.2.0 // indirect
- github.com/cespare/xxhash/v2 v2.3.0 // indirect
- github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
- github.com/mattn/go-colorable v0.1.14 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/rs/zerolog v1.34.0 // indirect
- github.com/tidwall/gjson v1.18.0 // indirect
- github.com/tidwall/match v1.1.1 // indirect
- github.com/tidwall/pretty v1.2.1 // indirect
- github.com/tidwall/sjson v1.2.5 // indirect
- go.mau.fi/util v0.9.7 // indirect
- go.uber.org/atomic v1.11.0 // indirect
- golang.org/x/crypto v0.49.0 // indirect
- golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
- golang.org/x/net v0.52.0 // indirect
- golang.org/x/sys v0.42.0 // indirect
- golang.org/x/text v0.35.0 // indirect
-)
diff --git a/services/mana-matrix-bot/go.sum b/services/mana-matrix-bot/go.sum
deleted file mode 100644
index f0a10f863..000000000
--- a/services/mana-matrix-bot/go.sum
+++ /dev/null
@@ -1,66 +0,0 @@
-filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
-filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
-github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
-github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
-github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
-github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
-github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
-github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
-github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
-github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
-github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
-github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
-github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
-github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
-github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
-github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
-github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
-github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
-github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
-github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
-github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
-github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
-github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
-github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
-github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
-github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
-go.mau.fi/util v0.9.7 h1:AWGNbJfz1zRcQOKeOEYhKUG2fT+/26Gy6kyqcH8tnBg=
-go.mau.fi/util v0.9.7/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE=
-go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
-go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
-golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
-golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
-golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
-golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
-golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
-golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
-golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-maunium.net/go/mautrix v0.26.4 h1:enHSnkf0L2V9+VnfJfNhKSReSW6pBKS/x3Su+v+Vovs=
-maunium.net/go/mautrix v0.26.4/go.mod h1:YWw8NWTszsbyFAznboicBObwHPgTSLcuTbVX2kY7U2M=
diff --git a/services/mana-matrix-bot/internal/config/config.go b/services/mana-matrix-bot/internal/config/config.go
deleted file mode 100644
index 8bab865ba..000000000
--- a/services/mana-matrix-bot/internal/config/config.go
+++ /dev/null
@@ -1,219 +0,0 @@
-package config
-
-import (
- "os"
- "strconv"
- "strings"
-)
-
-// Config holds all configuration for the consolidated Matrix bot.
-type Config struct {
- // Server
- Port int
-
- // Matrix
- HomeserverURL string
- StoragePath string
-
- // Auth
- AuthURL string
- ServiceKey string
-
- // Redis
- RedisHost string
- RedisPort int
- RedisPassword string
-
- // Voice services
- STTURL string
- TTSURL string
-
- // Plugins (keyed by plugin name)
- Plugins map[string]PluginConfig
-}
-
-// PluginConfig holds per-plugin configuration.
-type PluginConfig struct {
- Enabled bool
- AccessToken string
- AllowedRooms []string
- BackendURL string
- Extra map[string]string
-}
-
-// Load reads configuration from environment variables.
-func Load() *Config {
- port, _ := strconv.Atoi(getEnv("PORT", "4000"))
- redisPort, _ := strconv.Atoi(getEnv("REDIS_PORT", "6379"))
-
- cfg := &Config{
- Port: port,
- HomeserverURL: getEnv("MATRIX_HOMESERVER_URL", "http://localhost:8008"),
- StoragePath: getEnv("MATRIX_STORAGE_PATH", "./data"),
- AuthURL: getEnv("MANA_AUTH_URL", "http://localhost:3001"),
- ServiceKey: getEnv("MANA_SERVICE_KEY", ""),
- RedisHost: getEnv("REDIS_HOST", "localhost"),
- RedisPort: redisPort,
- RedisPassword: getEnv("REDIS_PASSWORD", ""),
- STTURL: getEnv("STT_URL", "http://localhost:3020"),
- TTSURL: getEnv("TTS_URL", "http://localhost:3022"),
- Plugins: make(map[string]PluginConfig),
- }
-
- // Load plugin configs from environment variables.
- // Pattern: PLUGIN_{NAME}_ENABLED, PLUGIN_{NAME}_ACCESS_TOKEN, etc.
- // Also supports legacy patterns: MATRIX_{NAME}_BOT_TOKEN
- pluginNames := []string{
- "gateway", "todo", "calendar", "clock", "ollama", "stats",
- "contacts", "chat", "cards", "nutriphi", "picture", "planta",
- "presi", "questions", "skilltree", "storage", "projectdoc",
- "stt", "tts", "zitare", "onboarding",
- }
-
- // Map of legacy token env var names
- legacyTokenMap := map[string]string{
- "gateway": "MATRIX_MANA_BOT_TOKEN",
- "todo": "MATRIX_TODO_BOT_TOKEN",
- "calendar": "MATRIX_CALENDAR_BOT_TOKEN",
- "clock": "MATRIX_CLOCK_BOT_TOKEN",
- "ollama": "MATRIX_OLLAMA_BOT_TOKEN",
- "stats": "MATRIX_STATS_BOT_TOKEN",
- "contacts": "MATRIX_CONTACTS_BOT_TOKEN",
- "chat": "MATRIX_CHAT_BOT_TOKEN",
- "cards": "MATRIX_CARDS_BOT_TOKEN",
- "nutriphi": "MATRIX_NUTRIPHI_BOT_TOKEN",
- "picture": "MATRIX_PICTURE_BOT_TOKEN",
- "planta": "MATRIX_PLANTA_BOT_TOKEN",
- "presi": "MATRIX_PRESI_BOT_TOKEN",
- "questions": "MATRIX_QUESTIONS_BOT_TOKEN",
- "skilltree": "MATRIX_SKILLTREE_BOT_TOKEN",
- "storage": "MATRIX_STORAGE_BOT_TOKEN",
- "projectdoc": "MATRIX_PROJECT_DOC_BOT_TOKEN",
- "stt": "MATRIX_STT_BOT_TOKEN",
- "tts": "MATRIX_TTS_BOT_TOKEN",
- "zitare": "MATRIX_ZITARE_BOT_TOKEN",
- "onboarding": "MATRIX_ONBOARDING_BOT_TOKEN",
- }
-
- legacyRoomsMap := map[string]string{
- "gateway": "MATRIX_MANA_BOT_ROOMS",
- "todo": "MATRIX_TODO_BOT_ROOMS",
- "calendar": "MATRIX_CALENDAR_BOT_ROOMS",
- "clock": "MATRIX_CLOCK_BOT_ROOMS",
- "ollama": "MATRIX_OLLAMA_BOT_ROOMS",
- "stats": "MATRIX_STATS_BOT_ROOMS",
- "contacts": "MATRIX_CONTACTS_BOT_ROOMS",
- "chat": "MATRIX_CHAT_BOT_ROOMS",
- "cards": "MATRIX_CARDS_BOT_ROOMS",
- "nutriphi": "MATRIX_NUTRIPHI_BOT_ROOMS",
- "picture": "MATRIX_PICTURE_BOT_ROOMS",
- "planta": "MATRIX_PLANTA_BOT_ROOMS",
- "presi": "MATRIX_PRESI_BOT_ROOMS",
- "questions": "MATRIX_QUESTIONS_BOT_ROOMS",
- "skilltree": "MATRIX_SKILLTREE_BOT_ROOMS",
- "storage": "MATRIX_STORAGE_BOT_ROOMS",
- "projectdoc": "MATRIX_PROJECT_DOC_BOT_ROOMS",
- "stt": "MATRIX_STT_BOT_ROOMS",
- "tts": "MATRIX_TTS_BOT_ROOMS",
- "zitare": "MATRIX_ZITARE_BOT_ROOMS",
- "onboarding": "MATRIX_ONBOARDING_BOT_ROOMS",
- }
-
- // Backend URL defaults per plugin
- backendURLMap := map[string]string{
- "todo": "TODO_BACKEND_URL",
- "calendar": "CALENDAR_BACKEND_URL",
- "clock": "CLOCK_BACKEND_URL",
- "contacts": "CONTACTS_BACKEND_URL",
- "chat": "CHAT_BACKEND_URL",
- "cards": "CARDS_BACKEND_URL",
- "nutriphi": "NUTRIPHI_BACKEND_URL",
- "picture": "PICTURE_BACKEND_URL",
- "planta": "PLANTA_BACKEND_URL",
- "presi": "PRESI_BACKEND_URL",
- "questions": "QUESTIONS_BACKEND_URL",
- "skilltree": "SKILLTREE_BACKEND_URL",
- "storage": "STORAGE_BACKEND_URL",
- "projectdoc": "PROJECTDOC_BACKEND_URL",
- "zitare": "ZITARE_BACKEND_URL",
- }
-
- for _, name := range pluginNames {
- upper := strings.ToUpper(name)
-
- // Access token: try PLUGIN_*_ACCESS_TOKEN first, then legacy
- token := os.Getenv("PLUGIN_" + upper + "_ACCESS_TOKEN")
- if token == "" {
- if legacyEnv, ok := legacyTokenMap[name]; ok {
- token = os.Getenv(legacyEnv)
- }
- }
-
- // Enabled: explicit env or auto-detect from token presence
- enabledStr := os.Getenv("PLUGIN_" + upper + "_ENABLED")
- enabled := token != ""
- if enabledStr != "" {
- enabled = enabledStr == "true" || enabledStr == "1"
- }
-
- // Allowed rooms
- var rooms []string
- roomsStr := os.Getenv("PLUGIN_" + upper + "_ALLOWED_ROOMS")
- if roomsStr == "" {
- if legacyEnv, ok := legacyRoomsMap[name]; ok {
- roomsStr = os.Getenv(legacyEnv)
- }
- }
- if roomsStr != "" {
- for _, r := range strings.Split(roomsStr, ",") {
- r = strings.TrimSpace(r)
- if r != "" {
- rooms = append(rooms, r)
- }
- }
- }
-
- // Backend URL
- backendURL := ""
- if envName, ok := backendURLMap[name]; ok {
- backendURL = os.Getenv(envName)
- }
-
- // Extra config (plugin-specific env vars)
- extra := make(map[string]string)
- // Ollama-specific
- if name == "ollama" || name == "gateway" {
- extra["ollama_url"] = getEnv("OLLAMA_URL", "http://localhost:11434")
- extra["ollama_model"] = getEnv("OLLAMA_MODEL", "gemma3:4b")
- }
- if name == "stt" || name == "gateway" {
- extra["stt_url"] = cfg.STTURL
- }
- if name == "tts" || name == "gateway" {
- extra["tts_url"] = cfg.TTSURL
- }
- // Gateway needs backend URLs for sub-handlers
- if name == "gateway" {
- extra["todo_url"] = getEnv("TODO_BACKEND_URL", "")
- extra["calendar_url"] = getEnv("CALENDAR_BACKEND_URL", "")
- extra["clock_url"] = getEnv("CLOCK_BACKEND_URL", "")
- }
-
- cfg.Plugins[name] = PluginConfig{
- Enabled: enabled,
- AccessToken: token,
- AllowedRooms: rooms,
- BackendURL: backendURL,
- Extra: extra,
- }
- }
-
- return cfg
-}
-
-func getEnv(key, fallback string) string {
- if v := os.Getenv(key); v != "" {
- return v
- }
- return fallback
-}
diff --git a/services/mana-matrix-bot/internal/matrix/client.go b/services/mana-matrix-bot/internal/matrix/client.go
deleted file mode 100644
index 78c157792..000000000
--- a/services/mana-matrix-bot/internal/matrix/client.go
+++ /dev/null
@@ -1,241 +0,0 @@
-package matrix
-
-import (
- "context"
- "fmt"
- "io"
- "log/slog"
- "net/http"
- "os"
- "path/filepath"
- "regexp"
- "time"
-
- "maunium.net/go/mautrix"
- "maunium.net/go/mautrix/event"
- "maunium.net/go/mautrix/id"
-)
-
-var mxcRegex = regexp.MustCompile(`^mxc://([^/]+)/(.+)$`)
-
-// Client wraps mautrix.Client and implements the plugin.MatrixClient interface.
-type Client struct {
- inner *mautrix.Client
- homeserver string
- accessToken string
- storagePath string
- logger *slog.Logger
-}
-
-// ClientConfig holds configuration for creating a Matrix client.
-type ClientConfig struct {
- HomeserverURL string
- AccessToken string
- StoragePath string // path for sync state file
- PluginName string
-}
-
-// NewClient creates a new Matrix client wrapper.
-func NewClient(cfg ClientConfig) (*Client, error) {
- userID := id.UserID("") // will be resolved via whoami
-
- client, err := mautrix.NewClient(cfg.HomeserverURL, userID, cfg.AccessToken)
- if err != nil {
- return nil, fmt.Errorf("create mautrix client: %w", err)
- }
-
- // Ensure storage directory exists
- if cfg.StoragePath != "" {
- dir := filepath.Dir(cfg.StoragePath)
- if err := os.MkdirAll(dir, 0o755); err != nil {
- return nil, fmt.Errorf("create storage dir: %w", err)
- }
- }
-
- logger := slog.With("plugin", cfg.PluginName)
-
- return &Client{
- inner: client,
- homeserver: cfg.HomeserverURL,
- accessToken: cfg.AccessToken,
- storagePath: cfg.StoragePath,
- logger: logger,
- }, nil
-}
-
-// Inner returns the underlying mautrix.Client for advanced operations.
-func (c *Client) Inner() *mautrix.Client {
- return c.inner
-}
-
-// Login resolves the bot's user ID via /whoami.
-func (c *Client) Login(ctx context.Context) (id.UserID, error) {
- resp, err := c.inner.Whoami(ctx)
- if err != nil {
- return "", fmt.Errorf("whoami: %w", err)
- }
- c.inner.UserID = resp.UserID
- c.logger.Info("authenticated", "user_id", resp.UserID)
- return resp.UserID, nil
-}
-
-// GetUserID returns the bot's Matrix user ID.
-func (c *Client) GetUserID() string {
- return c.inner.UserID.String()
-}
-
-// SendMessage sends a text message with markdown formatting to a room.
-func (c *Client) SendMessage(ctx context.Context, roomID string, text string) (string, error) {
- content := &event.MessageEventContent{
- MsgType: event.MsgText,
- Body: text,
- Format: event.FormatHTML,
- FormattedBody: MarkdownToHTML(text),
- }
- resp, err := c.inner.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content)
- if err != nil {
- return "", err
- }
- return resp.EventID.String(), nil
-}
-
-// SendReply sends a reply to a specific event.
-func (c *Client) SendReply(ctx context.Context, roomID string, eventID string, text string) (string, error) {
- content := &event.MessageEventContent{
- MsgType: event.MsgText,
- Body: text,
- Format: event.FormatHTML,
- FormattedBody: MarkdownToHTML(text),
- }
- content.SetReply(&event.Event{
- RoomID: id.RoomID(roomID),
- ID: id.EventID(eventID),
- })
- resp, err := c.inner.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content)
- if err != nil {
- return "", err
- }
- return resp.EventID.String(), nil
-}
-
-// SendNotice sends a notice (non-highlighted message).
-func (c *Client) SendNotice(ctx context.Context, roomID string, text string) (string, error) {
- content := &event.MessageEventContent{
- MsgType: event.MsgNotice,
- Body: text,
- Format: event.FormatHTML,
- FormattedBody: MarkdownToHTML(text),
- }
- resp, err := c.inner.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content)
- if err != nil {
- return "", err
- }
- return resp.EventID.String(), nil
-}
-
-// EditMessage edits an existing message.
-func (c *Client) EditMessage(ctx context.Context, roomID string, eventID string, text string) (string, error) {
- content := map[string]any{
- "msgtype": "m.text",
- "body": "* " + text,
- "format": "org.matrix.custom.html",
- "formatted_body": "* " + MarkdownToHTML(text),
- "m.relates_to": map[string]any{
- "rel_type": "m.replace",
- "event_id": eventID,
- },
- "m.new_content": map[string]any{
- "msgtype": "m.text",
- "body": text,
- "format": "org.matrix.custom.html",
- "formatted_body": MarkdownToHTML(text),
- },
- }
- resp, err := c.inner.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content)
- if err != nil {
- return "", err
- }
- return resp.EventID.String(), nil
-}
-
-// SetTyping sets the typing indicator for the bot in a room.
-func (c *Client) SetTyping(ctx context.Context, roomID string, typing bool) error {
- timeout := time.Duration(0)
- if typing {
- timeout = 30 * time.Second
- }
- _, err := c.inner.UserTyping(ctx, id.RoomID(roomID), typing, timeout)
- return err
-}
-
-// DownloadMedia downloads media from a mxc:// URL.
-func (c *Client) DownloadMedia(ctx context.Context, mxcURL string) ([]byte, error) {
- matches := mxcRegex.FindStringSubmatch(mxcURL)
- if len(matches) != 3 {
- return nil, fmt.Errorf("invalid mxc URL: %s", mxcURL)
- }
-
- serverName := matches[1]
- mediaID := matches[2]
-
- // Try authenticated media API (Matrix spec v1.11+)
- url := fmt.Sprintf("%s/_matrix/client/v1/media/download/%s/%s", c.homeserver, serverName, mediaID)
- req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
- if err != nil {
- return nil, err
- }
- req.Header.Set("Authorization", "Bearer "+c.accessToken)
-
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return nil, fmt.Errorf("download media: %w", err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- // Fallback to legacy API
- url = fmt.Sprintf("%s/_matrix/media/v3/download/%s/%s", c.homeserver, serverName, mediaID)
- req2, err := http.NewRequestWithContext(ctx, "GET", url, nil)
- if err != nil {
- return nil, err
- }
- resp2, err := http.DefaultClient.Do(req2)
- if err != nil {
- return nil, fmt.Errorf("download media (legacy): %w", err)
- }
- defer resp2.Body.Close()
- if resp2.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("download media failed: %d", resp2.StatusCode)
- }
- return io.ReadAll(resp2.Body)
- }
-
- return io.ReadAll(resp.Body)
-}
-
-// UploadMedia uploads media and returns the mxc:// URL.
-func (c *Client) UploadMedia(ctx context.Context, data []byte, contentType string, filename string) (string, error) {
- resp, err := c.inner.UploadBytes(ctx, data, contentType)
- if err != nil {
- return "", fmt.Errorf("upload media: %w", err)
- }
- return resp.ContentURI.String(), nil
-}
-
-// SendAudio sends an audio message to a room.
-func (c *Client) SendAudio(ctx context.Context, roomID string, mxcURL string, filename string, size int) (string, error) {
- content := &event.MessageEventContent{
- MsgType: event.MsgAudio,
- Body: filename,
- URL: id.ContentURIString(mxcURL),
- Info: &event.FileInfo{
- MimeType: "audio/mpeg",
- Size: size,
- },
- }
- resp, err := c.inner.SendMessageEvent(ctx, id.RoomID(roomID), event.EventMessage, content)
- if err != nil {
- return "", err
- }
- return resp.EventID.String(), nil
-}
diff --git a/services/mana-matrix-bot/internal/matrix/markdown.go b/services/mana-matrix-bot/internal/matrix/markdown.go
deleted file mode 100644
index de5b06c7c..000000000
--- a/services/mana-matrix-bot/internal/matrix/markdown.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package matrix
-
-import (
- "fmt"
- "regexp"
- "strings"
-)
-
-var (
- reBold = regexp.MustCompile(`\*\*(.+?)\*\*`)
- reItalic = regexp.MustCompile(`\*(.+?)\*`)
- reStrikethrough = regexp.MustCompile(`~~(.+?)~~`)
- reCode = regexp.MustCompile("`(.+?)`")
-)
-
-// MarkdownToHTML converts simple markdown to HTML for Matrix messages.
-// Matches the exact behavior of the TypeScript markdownToHtml function.
-func MarkdownToHTML(text string) string {
- result := text
- result = reBold.ReplaceAllString(result, "$1 ")
- result = reItalic.ReplaceAllString(result, "$1 ")
- result = reStrikethrough.ReplaceAllString(result, "$1")
- result = reCode.ReplaceAllString(result, "$1")
- result = strings.ReplaceAll(result, "\n", " ")
- return result
-}
-
-// EscapeHTML escapes HTML special characters.
-func EscapeHTML(text string) string {
- r := strings.NewReplacer(
- "&", "&",
- "<", "<",
- ">", ">",
- `"`, """,
- "'", "'",
- )
- return r.Replace(text)
-}
-
-// FormatNumberedList formats items as a numbered markdown list.
-func FormatNumberedList[T any](items []T, formatter func(T, int) string) string {
- var sb strings.Builder
- for i, item := range items {
- if i > 0 {
- sb.WriteByte('\n')
- }
- sb.WriteString(fmt.Sprintf("%d. %s", i+1, formatter(item, i)))
- }
- return sb.String()
-}
-
-// FormatBulletList formats items as a bullet markdown list.
-func FormatBulletList[T any](items []T, formatter func(T) string) string {
- var sb strings.Builder
- for i, item := range items {
- if i > 0 {
- sb.WriteByte('\n')
- }
- sb.WriteString("• ")
- sb.WriteString(formatter(item))
- }
- return sb.String()
-}
diff --git a/services/mana-matrix-bot/internal/matrix/markdown_test.go b/services/mana-matrix-bot/internal/matrix/markdown_test.go
deleted file mode 100644
index fd4ed647c..000000000
--- a/services/mana-matrix-bot/internal/matrix/markdown_test.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package matrix
-
-import "testing"
-
-func TestMarkdownToHTML(t *testing.T) {
- tests := []struct {
- input string
- want string
- }{
- {"**bold**", "bold "},
- {"*italic*", "italic "},
- {"~~strike~~", "strike"},
- {"`code`", "code"},
- {"line1\nline2", "line1 line2"},
- {"**bold** and *italic*", "bold and italic "},
- {"plain text", "plain text"},
- {"", ""},
- }
-
- for _, tt := range tests {
- got := MarkdownToHTML(tt.input)
- if got != tt.want {
- t.Errorf("MarkdownToHTML(%q) = %q, want %q", tt.input, got, tt.want)
- }
- }
-}
-
-func TestEscapeHTML(t *testing.T) {
- tests := []struct {
- input string
- want string
- }{
- {"