From 3b05b7e4d44b894053d481d95801b8b91767b5b2 Mon Sep 17 00:00:00 2001 From: lendry Date: Fri, 26 Jun 2026 13:56:54 +0300 Subject: [PATCH] global fix and add tauri app --- apps/api-gateway/Dockerfile | 1 + .../src/controllers/chat.controller.ts | 3 +- apps/api-gateway/src/dto/chat.dto.ts | 4 + apps/docs/Dockerfile | 1 + apps/frontend/Dockerfile | 1 + apps/frontend/app/auth/login/page.tsx | 11 +- apps/frontend/app/download/android/route.ts | 44 + .../chat/chat-pinned-message-banner.tsx | 36 + .../components/family/family-bot-chat.tsx | 34 +- .../components/family/family-group-view.tsx | 119 +- .../family/family-sidebar-panel.tsx | 150 +- .../components/family/mini-family-chat.tsx | 57 +- apps/frontend/lib/api.ts | 11 + apps/frontend/public/downloads/.gitkeep | 1 + apps/sso-core/Dockerfile | 1 + apps/sso-core/prisma/schema.prisma | 2 + .../src/domain/auth-grpc.controller.ts | 4 +- apps/sso-core/src/domain/chat.service.ts | 19 +- docker-compose.yml | 19 + package-lock.json | 1852 ++++++++++++++++- package.json | 3 +- shared/proto/chat.proto | 5 + tauri_app/Dockerfile.apk | 64 + tauri_app/README.md | 62 + tauri_app/docker/build-android-apk.sh | 36 + tauri_app/index.html | 13 + tauri_app/package.json | 45 + tauri_app/src-tauri/Cargo.toml | 30 + tauri_app/src-tauri/build.rs | 3 + tauri_app/src-tauri/capabilities/default.json | 11 + tauri_app/src-tauri/mobile-permissions.md | 30 + tauri_app/src-tauri/src/lib.rs | 148 ++ tauri_app/src-tauri/src/main.rs | 3 + tauri_app/src-tauri/tauri.conf.json | 45 + tauri_app/src/App.tsx | 32 + tauri_app/src/components/mobile-shell.tsx | 76 + tauri_app/src/features/auth/login-screen.tsx | 159 ++ tauri_app/src/features/auth/qr-scanner.tsx | 107 + tauri_app/src/features/chat/chats-screen.tsx | 35 + .../src/features/security/security-screen.tsx | 116 ++ .../src/features/totp/secure-totp-store.ts | 85 + tauri_app/src/features/totp/totp-screen.tsx | 161 ++ tauri_app/src/features/totp/totp.ts | 81 + tauri_app/src/lib/auth.tsx | 75 + tauri_app/src/lib/errors.ts | 6 + tauri_app/src/lib/mobile-api.ts | 138 ++ tauri_app/src/main.tsx | 10 + tauri_app/src/styles.css | 21 + tauri_app/tsconfig.json | 25 + tauri_app/vite.config.ts | 32 + 50 files changed, 3947 insertions(+), 80 deletions(-) create mode 100644 apps/frontend/app/download/android/route.ts create mode 100644 apps/frontend/components/chat/chat-pinned-message-banner.tsx create mode 100644 apps/frontend/public/downloads/.gitkeep create mode 100644 tauri_app/Dockerfile.apk create mode 100644 tauri_app/README.md create mode 100644 tauri_app/docker/build-android-apk.sh create mode 100644 tauri_app/index.html create mode 100644 tauri_app/package.json create mode 100644 tauri_app/src-tauri/Cargo.toml create mode 100644 tauri_app/src-tauri/build.rs create mode 100644 tauri_app/src-tauri/capabilities/default.json create mode 100644 tauri_app/src-tauri/mobile-permissions.md create mode 100644 tauri_app/src-tauri/src/lib.rs create mode 100644 tauri_app/src-tauri/src/main.rs create mode 100644 tauri_app/src-tauri/tauri.conf.json create mode 100644 tauri_app/src/App.tsx create mode 100644 tauri_app/src/components/mobile-shell.tsx create mode 100644 tauri_app/src/features/auth/login-screen.tsx create mode 100644 tauri_app/src/features/auth/qr-scanner.tsx create mode 100644 tauri_app/src/features/chat/chats-screen.tsx create mode 100644 tauri_app/src/features/security/security-screen.tsx create mode 100644 tauri_app/src/features/totp/secure-totp-store.ts create mode 100644 tauri_app/src/features/totp/totp-screen.tsx create mode 100644 tauri_app/src/features/totp/totp.ts create mode 100644 tauri_app/src/lib/auth.tsx create mode 100644 tauri_app/src/lib/errors.ts create mode 100644 tauri_app/src/lib/mobile-api.ts create mode 100644 tauri_app/src/main.tsx create mode 100644 tauri_app/src/styles.css create mode 100644 tauri_app/tsconfig.json create mode 100644 tauri_app/vite.config.ts diff --git a/apps/api-gateway/Dockerfile b/apps/api-gateway/Dockerfile index 1882b7f..ec07c98 100644 --- a/apps/api-gateway/Dockerfile +++ b/apps/api-gateway/Dockerfile @@ -9,6 +9,7 @@ COPY apps/sso-core/package.json ./apps/sso-core/ COPY apps/api-gateway/package.json ./apps/api-gateway/ COPY apps/frontend/package.json ./apps/frontend/ COPY apps/docs/package.json ./apps/docs/ +COPY tauri_app/package.json ./tauri_app/ RUN --mount=type=cache,target=/root/.npm \ npm config set registry "${NPM_REGISTRY}" && \ diff --git a/apps/api-gateway/src/controllers/chat.controller.ts b/apps/api-gateway/src/controllers/chat.controller.ts index 1846d0d..c70cc6c 100644 --- a/apps/api-gateway/src/controllers/chat.controller.ts +++ b/apps/api-gateway/src/controllers/chat.controller.ts @@ -84,7 +84,8 @@ export class ChatController { userId, roomId, name: dto.name, - notificationsMuted: dto.notificationsMuted + notificationsMuted: dto.notificationsMuted, + pinned: dto.pinned }) ); } diff --git a/apps/api-gateway/src/dto/chat.dto.ts b/apps/api-gateway/src/dto/chat.dto.ts index e96023b..f2f6ebd 100644 --- a/apps/api-gateway/src/dto/chat.dto.ts +++ b/apps/api-gateway/src/dto/chat.dto.ts @@ -25,6 +25,10 @@ export class UpdateChatRoomDto { @IsOptional() @IsBoolean() notificationsMuted?: boolean; + + @IsOptional() + @IsBoolean() + pinned?: boolean; } export class SendChatMessageDto { diff --git a/apps/docs/Dockerfile b/apps/docs/Dockerfile index 3bbcfa8..9fc0d6c 100644 --- a/apps/docs/Dockerfile +++ b/apps/docs/Dockerfile @@ -10,6 +10,7 @@ COPY apps/sso-core/package.json ./apps/sso-core/ COPY apps/api-gateway/package.json ./apps/api-gateway/ COPY apps/frontend/package.json ./apps/frontend/ COPY apps/docs/package.json ./apps/docs/ +COPY tauri_app/package.json ./tauri_app/ RUN --mount=type=cache,target=/root/.npm \ npm config set registry "${NPM_REGISTRY}" && \ diff --git a/apps/frontend/Dockerfile b/apps/frontend/Dockerfile index 894b280..a69cc56 100644 --- a/apps/frontend/Dockerfile +++ b/apps/frontend/Dockerfile @@ -13,6 +13,7 @@ COPY apps/sso-core/package.json ./apps/sso-core/ COPY apps/api-gateway/package.json ./apps/api-gateway/ COPY apps/frontend/package.json ./apps/frontend/ COPY apps/docs/package.json ./apps/docs/ +COPY tauri_app/package.json ./tauri_app/ RUN --mount=type=cache,target=/root/.npm \ npm config set registry "${NPM_REGISTRY}" && \ diff --git a/apps/frontend/app/auth/login/page.tsx b/apps/frontend/app/auth/login/page.tsx index db7a2ef..6594228 100644 --- a/apps/frontend/app/auth/login/page.tsx +++ b/apps/frontend/app/auth/login/page.tsx @@ -8,7 +8,7 @@ import { FormEvent, useCallback, useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; -import { ChevronLeft, KeyRound, Mail, Network, Phone, QrCode, ShieldCheck, ShieldQuestion, UserRound } from 'lucide-react'; +import { ChevronLeft, Download, KeyRound, Mail, Network, Phone, QrCode, ShieldCheck, ShieldQuestion, UserRound } from 'lucide-react'; import { QRCodeSVG } from 'qrcode.react'; @@ -1270,6 +1270,15 @@ export default function LoginPage() { + + + Скачать приложение + + diff --git a/apps/frontend/app/download/android/route.ts b/apps/frontend/app/download/android/route.ts new file mode 100644 index 0000000..6fe0f85 --- /dev/null +++ b/apps/frontend/app/download/android/route.ts @@ -0,0 +1,44 @@ +import { createReadStream, existsSync, statSync } from 'node:fs'; +import path from 'node:path'; +import { Readable } from 'node:stream'; +import { NextResponse } from 'next/server'; + +export const runtime = 'nodejs'; + +const APK_CANDIDATES = [ + process.env.TAURI_ANDROID_APK_PATH, + path.resolve(process.cwd(), 'public/downloads/lendry-id.apk'), + path.resolve(process.cwd(), '../../tauri_app/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk'), + path.resolve(process.cwd(), '../../tauri_app/src-tauri/gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk'), + path.resolve(process.cwd(), '../../tauri_app/src-tauri/gen/android/app/build/outputs/apk/arm64/release/app-arm64-release.apk'), + path.resolve(process.cwd(), '../../tauri_app/src-tauri/gen/android/app/build/outputs/apk/arm64/debug/app-arm64-debug.apk') +].filter((candidate): candidate is string => Boolean(candidate)); + +function resolveApkPath() { + return APK_CANDIDATES.find((candidate) => existsSync(candidate) && statSync(candidate).isFile()); +} + +export async function GET() { + const apkPath = resolveApkPath(); + if (!apkPath) { + return NextResponse.json( + { + message: + 'APK ещё не собран. Соберите tauri_app для Android или укажите TAURI_ANDROID_APK_PATH/public/downloads/lendry-id.apk.' + }, + { status: 404 } + ); + } + + const stat = statSync(apkPath); + const stream = Readable.toWeb(createReadStream(apkPath)) as ReadableStream; + + return new Response(stream, { + headers: { + 'Content-Type': 'application/vnd.android.package-archive', + 'Content-Length': String(stat.size), + 'Content-Disposition': 'attachment; filename="lendry-id.apk"', + 'Cache-Control': 'no-store' + } + }); +} diff --git a/apps/frontend/components/chat/chat-pinned-message-banner.tsx b/apps/frontend/components/chat/chat-pinned-message-banner.tsx new file mode 100644 index 0000000..c1835e2 --- /dev/null +++ b/apps/frontend/components/chat/chat-pinned-message-banner.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { Pin } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface ChatPinnedMessageBannerProps { + senderName: string; + preview: string; + className?: string; + onClick: () => void; +} + +export function ChatPinnedMessageBanner({ senderName, preview, className, onClick }: ChatPinnedMessageBannerProps) { + return ( + + ); +} diff --git a/apps/frontend/components/family/family-bot-chat.tsx b/apps/frontend/components/family/family-bot-chat.tsx index 827e1e3..c3675da 100644 --- a/apps/frontend/components/family/family-bot-chat.tsx +++ b/apps/frontend/components/family/family-bot-chat.tsx @@ -1,9 +1,10 @@ 'use client'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { LayoutGrid, Loader2, Pin, Send, X } from 'lucide-react'; +import { LayoutGrid, Loader2, Send, X } from 'lucide-react'; import { BotMessageKeyboard } from '@/components/chat/bot-message-keyboard'; import { BotChatMessageContextMenu } from '@/components/chat/bot-chat-message-context-menu'; +import { ChatPinnedMessageBanner } from '@/components/chat/chat-pinned-message-banner'; import { useRealtime } from '@/components/notifications/realtime-provider'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -69,6 +70,18 @@ export function FamilyBotChat({ const [botMeta, setBotMeta] = useState(null); const { subscribe } = useRealtime(); const { showToast } = useToast(); + const pinnedMessage = messages + .filter((message) => message.isPinned) + .sort((left, right) => new Date(right.pinnedAt ?? right.createdAt).getTime() - new Date(left.pinnedAt ?? left.createdAt).getTime())[0] ?? null; + + function scrollToBotMessage(messageId: string) { + const element = document.getElementById(`bot-msg-${messageId}`); + if (!element) return false; + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + element.classList.add('blink-highlight'); + window.setTimeout(() => element.classList.remove('blink-highlight'), 2000); + return true; + } const openMiniApp = useCallback( (url: string) => { @@ -236,6 +249,17 @@ export function FamilyBotChat({ return (
+ {pinnedMessage ? ( + { + if (!scrollToBotMessage(pinnedMessage.id)) { + showToast('Закреплённое сообщение пока не загружено в ленте'); + } + }} + /> + ) : null}
{loading ? (
@@ -247,15 +271,9 @@ export function FamilyBotChat({ const mine = message.direction === 'in'; const hasKeyboard = Boolean(message.replyMarkupJson?.trim()); return ( -
+
showToast('Текст скопирован')}>
- {message.isPinned ? ( -
- - Закреплено -
- ) : null} {!mine ?

{botName}

: null}

{message.text || '…'}

{!mine && hasKeyboard && message.messageId > 0 ? ( diff --git a/apps/frontend/components/family/family-group-view.tsx b/apps/frontend/components/family/family-group-view.tsx index 0bf73c8..6745be0 100644 --- a/apps/frontend/components/family/family-group-view.tsx +++ b/apps/frontend/components/family/family-group-view.tsx @@ -39,6 +39,7 @@ import { ChatToolsDialog, type ChatToolsTab } from '@/components/chat/chat-tools import { ChatDeleteMessageDialog } from '@/components/chat/chat-delete-message-dialog'; import { ChatMessageEditBanner } from '@/components/chat/chat-message-edit-banner'; import { ChatMessageContextMenu, ChatSelectionContextMenu, SelectableMessageWrapper } from '@/components/chat/chat-message-context-menu'; +import { ChatPinnedMessageBanner } from '@/components/chat/chat-pinned-message-banner'; import { ChatMessageReactions } from '@/components/chat/chat-message-reactions'; import { ChatSelectionToolbar } from '@/components/chat/chat-selection-toolbar'; import { EmojiPicker } from '@/components/chat/emoji-picker'; @@ -80,6 +81,7 @@ import { reportChatTyping, sendChatMessage, setChatMessagePinned, + setChatRoomPinned, setChatRoomMuted, toggleChatMessageReaction, updateFamilyGroup, @@ -247,6 +249,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) { const [miniAppTitle, setMiniAppTitle] = useState('Mini App'); const [botChatMeta, setBotChatMeta] = useState(null); const [deletingRoomId, setDeletingRoomId] = useState(null); + const [pinningRoomId, setPinningRoomId] = useState(null); const [familyAvatarUploading, setFamilyAvatarUploading] = useState(false); const [roomAvatarUploading, setRoomAvatarUploading] = useState(false); const [forwardDialogOpen, setForwardDialogOpen] = useState(false); @@ -257,6 +260,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) { const [chatToolsTab, setChatToolsTab] = useState('search'); const messagesEndRef = useRef(null); + const autoScrollStateRef = useRef<{ id: string | null; count: number }>({ id: null, count: 0 }); const readReceiptTimerRef = useRef | null>(null); const typingEmitTimerRef = useRef | null>(null); const lastTypingSentAtRef = useRef(0); @@ -279,6 +283,13 @@ export function FamilyGroupView({ groupId }: { groupId: string }) { () => new Map(visibleMessages.map((message) => [message.id, message])), [visibleMessages] ); + const pinnedMessage = useMemo( + () => + visibleMessages + .filter((message) => message.isPinned) + .sort((left, right) => new Date(right.pinnedAt ?? right.createdAt).getTime() - new Date(left.pinnedAt ?? left.createdAt).getTime())[0] ?? null, + [visibleMessages] + ); const { typers, registerTyper, clearTypers } = useChatTyping(user?.id); const { @@ -311,6 +322,17 @@ export function FamilyGroupView({ groupId }: { groupId: string }) { userId: user?.id, token }); + const pinnedVisibleText = pinnedMessage + ? getE2EMessageText({ + message: pinnedMessage, + isE2E: activeRoomIsE2E, + peerE2eKey, + peerKeyLoading, + e2ePayload: pinnedMessage.isEncrypted ? e2ePayloads[pinnedMessage.id] : null, + e2eError: e2eErrors[pinnedMessage.id] + }) + : ''; + const pinnedPreview = pinnedMessage ? getMessagePreviewText(pinnedMessage, pinnedVisibleText) : ''; const activeRoomTitle = useMemo( () => (activeRoom && user ? roomDisplayLabel(activeRoom, user.id) : ''), [activeRoom, user] @@ -431,8 +453,21 @@ export function FamilyGroupView({ groupId }: { groupId: string }) { }, [activeRoomId, messages, token]); useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages]); + autoScrollStateRef.current = { id: null, count: 0 }; + }, [activeRoomId]); + + useEffect(() => { + const lastMessage = visibleMessages[visibleMessages.length - 1]; + const previous = autoScrollStateRef.current; + const next = { id: lastMessage?.id ?? null, count: visibleMessages.length }; + autoScrollStateRef.current = next; + if (!next.id) return; + const lastMessageChanged = previous.id !== next.id; + const messageWasAppended = next.count >= previous.count; + if (lastMessageChanged && messageWasAppended) { + messagesEndRef.current?.scrollIntoView({ behavior: previous.id ? 'smooth' : 'auto' }); + } + }, [visibleMessages]); useEffect(() => { if (!activeRoomId || !token || activeRoomIsBot) return; @@ -683,6 +718,36 @@ export function FamilyGroupView({ groupId }: { groupId: string }) { setRooms((current) => current.map((item) => (item.id === room.id ? room : item))); } + function sortRoomsForViewer(roomList: ChatRoom[]) { + return [...roomList].sort((a, b) => { + const pinDiff = Number(Boolean(b.isPinned)) - Number(Boolean(a.isPinned)); + if (pinDiff !== 0) return pinDiff; + if (a.isPinned && b.isPinned) { + return (b.pinnedAt ?? '').localeCompare(a.pinnedAt ?? ''); + } + const rank = (type: string) => (type === 'GENERAL' ? 0 : 1); + const rankDiff = rank(a.type) - rank(b.type); + if (rankDiff !== 0) return rankDiff; + const aTime = a.lastMessage?.createdAt ?? a.updatedAt; + const bTime = b.lastMessage?.createdAt ?? b.updatedAt; + return bTime.localeCompare(aTime); + }); + } + + async function handleToggleRoomPin(room: ChatRoom) { + if (!token) return; + setPinningRoomId(room.id); + try { + const updated = await setChatRoomPinned(room.id, !room.isPinned, token); + setRooms((current) => sortRoomsForViewer(current.map((item) => (item.id === updated.id ? updated : item)))); + showToast(updated.isPinned ? 'Чат закреплён' : 'Чат откреплён'); + } catch (error) { + showToast(getApiErrorMessage(error, 'Не удалось закрепить чат') ?? 'Ошибка'); + } finally { + setPinningRoomId(null); + } + } + async function handleAddRoomMember(memberUserId: string) { if (!token || !activeRoomId) return; try { @@ -1281,7 +1346,10 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
-

{user ? roomDisplayLabel(room, user.id) : room.name}

+

+ {user ? roomDisplayLabel(room, user.id) : room.name} + {room.isPinned ? : null} +

{room.lastMessage ? ( {formatMessageTime(room.lastMessage.createdAt)} ) : null} @@ -1291,17 +1359,23 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {

- {room.type !== 'GENERAL' ? ( - - ) : null} +
))}
@@ -1452,6 +1526,17 @@ export function FamilyGroupView({ groupId }: { groupId: string }) { /> ) : ( <> + {pinnedMessage ? ( + { + if (!scrollToChatMessage(pinnedMessage.id)) { + showToast('Закреплённое сообщение пока не загружено в ленте'); + } + }} + /> + ) : null}
{visibleMessages.map((message, messageIndex) => { const previousMessage = visibleMessages[messageIndex - 1]; @@ -1518,12 +1603,6 @@ export function FamilyGroupView({ groupId }: { groupId: string }) { ) )} > - {message.isPinned && !isLargeEmoji ? ( -
- - Закреплено -
- ) : null} {!mine ?

{message.senderName}

: null} {forwardedFrom ? (

↪️ Переслано от {forwardedFrom.senderName}

diff --git a/apps/frontend/components/family/family-sidebar-panel.tsx b/apps/frontend/components/family/family-sidebar-panel.tsx index 6ee11d5..d5e5bd3 100644 --- a/apps/frontend/components/family/family-sidebar-panel.tsx +++ b/apps/frontend/components/family/family-sidebar-panel.tsx @@ -1,8 +1,8 @@ 'use client'; import Link from 'next/link'; -import { useState } from 'react'; -import { Loader2, Search, UsersRound } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { Loader2, Pin, Search, UsersRound } from 'lucide-react'; import { AvatarWithPresence } from '@/components/family/avatar-with-presence'; import { FamilyGroupSelector } from '@/components/family/family-group-selector'; import { FamilyInviteDialog } from '@/components/family/family-invite-dialog'; @@ -11,6 +11,8 @@ import { useAuth } from '@/components/id/auth-provider'; import { useSelectedFamily } from '@/hooks/use-primary-family'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; +import { ChatRoom, fetchChatRooms, getApiErrorMessage, setChatRoomPinned } from '@/lib/api'; +import { useToast } from '@/components/id/toast-provider'; export function FamilySidebarPanel() { const { user, token } = useAuth(); @@ -25,7 +27,64 @@ export function FamilySidebarPanel() { refresh } = useSelectedFamily(Boolean(user && token)); const { openChatWithMember } = useFamilyOverlay(); + const { showToast } = useToast(); const [inviteOpen, setInviteOpen] = useState(false); + const [rooms, setRooms] = useState([]); + const [pinningRoomId, setPinningRoomId] = useState(null); + const currentUserId = user?.id ?? ''; + + useEffect(() => { + if (!group?.id || !token) { + setRooms([]); + return; + } + fetchChatRooms(group.id, token) + .then((response) => setRooms(response.rooms ?? [])) + .catch(() => setRooms([])); + }, [group?.id, token]); + + const roomByPeerUserId = useMemo(() => { + const map = new Map(); + for (const room of rooms) { + if (room.type !== 'DIRECT' && room.type !== 'BOT' && room.type !== 'E2E') continue; + const peer = room.members.find((member) => member.userId !== currentUserId); + if (peer) map.set(peer.userId, room); + } + return map; + }, [currentUserId, rooms]); + + const sortedMembers = useMemo(() => { + return [...(group?.members ?? [])].sort((left, right) => { + const leftRoom = roomByPeerUserId.get(left.userId); + const rightRoom = roomByPeerUserId.get(right.userId); + const pinDiff = Number(Boolean(rightRoom?.isPinned)) - Number(Boolean(leftRoom?.isPinned)); + if (pinDiff !== 0) return pinDiff; + if (leftRoom?.isPinned && rightRoom?.isPinned) { + return (rightRoom.pinnedAt ?? '').localeCompare(leftRoom.pinnedAt ?? ''); + } + if (left.userId === currentUserId) return -1; + if (right.userId === currentUserId) return 1; + return left.displayName.localeCompare(right.displayName, 'ru'); + }); + }, [currentUserId, group?.members, roomByPeerUserId]); + + async function toggleMemberPinned(memberUserId: string) { + const room = roomByPeerUserId.get(memberUserId); + if (!room || !token) { + showToast('Откройте чат с участником, чтобы его можно было закрепить'); + return; + } + setPinningRoomId(room.id); + try { + const updated = await setChatRoomPinned(room.id, !room.isPinned, token); + setRooms((current) => current.map((item) => (item.id === updated.id ? updated : item))); + showToast(updated.isPinned ? 'Чат закреплён' : 'Чат откреплён'); + } catch (error) { + showToast(getApiErrorMessage(error, 'Не удалось закрепить чат') ?? 'Ошибка'); + } finally { + setPinningRoomId(null); + } + } if (!user || !token) return null; @@ -69,49 +128,68 @@ export function FamilySidebarPanel() { Загрузка...
) : hasFamily ? ( -
- {(group?.members ?? []).map((member) => { +
+ {sortedMembers.map((member) => { const presence = presenceByUserId.get(member.userId); - const isSelf = member.userId === user.id; + const isSelf = member.userId === currentUserId; + const memberRoom = roomByPeerUserId.get(member.userId); + const memberPinned = Boolean(memberRoom?.isPinned); return ( - + {!isSelf ? ( +

+ {member.isBot ? 'Бот' : presence?.online ? 'В сети' : 'Не в сети'} +

+ ) : null} +
+ + {!isSelf ? ( + + ) : null} +
); })}
diff --git a/apps/frontend/components/family/mini-family-chat.tsx b/apps/frontend/components/family/mini-family-chat.tsx index 3b300be..d5712f8 100644 --- a/apps/frontend/components/family/mini-family-chat.tsx +++ b/apps/frontend/components/family/mini-family-chat.tsx @@ -2,13 +2,14 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Link from 'next/link'; -import { ArrowLeft, BarChart3, CalendarDays, ImageIcon, Loader2, MessageCircle, Paperclip, Pin, Search, Send, Settings, Smile, Trash2, X } from 'lucide-react'; +import { ArrowLeft, BarChart3, CalendarDays, ImageIcon, Loader2, MessageCircle, Paperclip, Search, Send, Settings, Smile, Trash2, X } from 'lucide-react'; import { AvatarWithPresence } from '@/components/family/avatar-with-presence'; import { FamilyBotChat, BotMiniAppSheet } from '@/components/family/family-bot-chat'; import { ChatRoomAvatarDisplay } from '@/components/family/chat-room-avatar-display'; import { ChatDateSeparator } from '@/components/chat/chat-date-separator'; import { ChatEmojiContent } from '@/components/chat/chat-emoji-content'; import { ChatMessageContextMenu, ChatSelectionContextMenu, SelectableMessageWrapper } from '@/components/chat/chat-message-context-menu'; +import { ChatPinnedMessageBanner } from '@/components/chat/chat-pinned-message-banner'; import { ChatDeleteMessageDialog } from '@/components/chat/chat-delete-message-dialog'; import { ChatMessageEditBanner } from '@/components/chat/chat-message-edit-banner'; import { ChatMessageReactions } from '@/components/chat/chat-message-reactions'; @@ -122,6 +123,7 @@ export function MiniFamilyChat() { const [chatToolsOpen, setChatToolsOpen] = useState(false); const [chatToolsTab, setChatToolsTab] = useState('search'); const messagesEndRef = useRef(null); + const autoScrollStateRef = useRef<{ id: string | null; count: number }>({ id: null, count: 0 }); const fileInputRef = useRef(null); const previousGroupIdRef = useRef(null); @@ -129,6 +131,13 @@ export function MiniFamilyChat() { () => messages.filter((message) => !message.isDeleted), [messages] ); + const pinnedMessage = useMemo( + () => + visibleMessages + .filter((message) => message.isPinned) + .sort((left, right) => new Date(right.pinnedAt ?? right.createdAt).getTime() - new Date(left.pinnedAt ?? left.createdAt).getTime())[0] ?? null, + [visibleMessages] + ); const { selectionMode, @@ -152,6 +161,17 @@ export function MiniFamilyChat() { userId: user?.id, token }); + const pinnedVisibleText = pinnedMessage + ? getE2EMessageText({ + message: pinnedMessage, + isE2E: activeRoomIsE2E, + peerE2eKey, + peerKeyLoading, + e2ePayload: pinnedMessage.isEncrypted ? e2ePayloads[pinnedMessage.id] : null, + e2eError: e2eErrors[pinnedMessage.id] + }) + : ''; + const pinnedPreview = pinnedMessage ? getMessagePreviewText(pinnedMessage, pinnedVisibleText) : ''; const loadRooms = useCallback(async () => { if (!group || !token) return; @@ -357,8 +377,21 @@ export function MiniFamilyChat() { }, [activeRoom, subscribe]); useEffect(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [messages]); + autoScrollStateRef.current = { id: null, count: 0 }; + }, [activeRoom?.id]); + + useEffect(() => { + const lastMessage = visibleMessages[visibleMessages.length - 1]; + const previous = autoScrollStateRef.current; + const next = { id: lastMessage?.id ?? null, count: visibleMessages.length }; + autoScrollStateRef.current = next; + if (!next.id) return; + const lastMessageChanged = previous.id !== next.id; + const messageWasAppended = next.count >= previous.count; + if (lastMessageChanged && messageWasAppended) { + messagesEndRef.current?.scrollIntoView({ behavior: previous.id ? 'smooth' : 'auto' }); + } + }, [visibleMessages]); useEffect(() => { if (!activeRoom || !token || !messages.length) return; @@ -914,6 +947,18 @@ export function MiniFamilyChat() { /> ) : ( <> + {pinnedMessage ? ( + { + if (!scrollToChatMessage(pinnedMessage.id)) { + showToast('Закреплённое сообщение пока не загружено в ленте'); + } + }} + /> + ) : null}
- {message.isPinned && !isLargeEmoji ? ( -
- - Закреплено -
- ) : null} {!mine && !isLargeEmoji ? (

{message.senderName}

) : null} diff --git a/apps/frontend/lib/api.ts b/apps/frontend/lib/api.ts index 493e0b2..fd8c894 100644 --- a/apps/frontend/lib/api.ts +++ b/apps/frontend/lib/api.ts @@ -854,6 +854,8 @@ export interface ChatRoomMember { isVerified?: boolean; verificationIcon?: string | null; notificationsMuted: boolean; + isPinned?: boolean; + pinnedAt?: string; familyRole?: string; isChatCreator?: boolean; } @@ -869,6 +871,8 @@ export interface ChatRoom { hasAvatar: boolean; createdById?: string; updatedAt: string; + isPinned?: boolean; + pinnedAt?: string; members: ChatRoomMember[]; lastMessage?: ChatMessage; } @@ -1120,6 +1124,13 @@ export async function setChatRoomMuted(roomId: string, muted: boolean, token?: s return apiFetch(`/chat/rooms/${roomId}/mute`, { method: 'POST', body: JSON.stringify({ muted }) }, token); } +export async function setChatRoomPinned(roomId: string, pinned: boolean, token?: string | null) { + return apiFetch(`/chat/rooms/${roomId}`, { + method: 'PATCH', + body: JSON.stringify({ pinned }) + }, token); +} + export async function editChatMessage(messageId: string, content: string, token?: string | null) { return apiFetch(`/chat/messages/${messageId}`, { method: 'PATCH', body: JSON.stringify({ content }) }, token); } diff --git a/apps/frontend/public/downloads/.gitkeep b/apps/frontend/public/downloads/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/apps/frontend/public/downloads/.gitkeep @@ -0,0 +1 @@ + diff --git a/apps/sso-core/Dockerfile b/apps/sso-core/Dockerfile index 72857fa..26ce961 100644 --- a/apps/sso-core/Dockerfile +++ b/apps/sso-core/Dockerfile @@ -11,6 +11,7 @@ COPY apps/sso-core/package.json ./apps/sso-core/ COPY apps/api-gateway/package.json ./apps/api-gateway/ COPY apps/frontend/package.json ./apps/frontend/ COPY apps/docs/package.json ./apps/docs/ +COPY tauri_app/package.json ./tauri_app/ RUN --mount=type=cache,target=/root/.npm \ npm config set registry "${NPM_REGISTRY}" && \ diff --git a/apps/sso-core/prisma/schema.prisma b/apps/sso-core/prisma/schema.prisma index f85bf85..41ceb07 100644 --- a/apps/sso-core/prisma/schema.prisma +++ b/apps/sso-core/prisma/schema.prisma @@ -418,6 +418,7 @@ model ChatRoomMember { roomId String userId String notificationsMuted Boolean @default(false) + pinnedAt DateTime? lastReadAt DateTime? lastReadMessageId String? joinedAt DateTime @default(now()) @@ -425,6 +426,7 @@ model ChatRoomMember { user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([roomId, userId]) + @@index([userId, pinnedAt]) } model ChatMessage { diff --git a/apps/sso-core/src/domain/auth-grpc.controller.ts b/apps/sso-core/src/domain/auth-grpc.controller.ts index de423df..46f7736 100644 --- a/apps/sso-core/src/domain/auth-grpc.controller.ts +++ b/apps/sso-core/src/domain/auth-grpc.controller.ts @@ -985,8 +985,8 @@ export class AuthGrpcController { } @GrpcMethod('ChatService', 'UpdateRoomSettings') - updateChatRoomSettings(command: { userId: string; roomId: string; name?: string; notificationsMuted?: boolean }) { - return this.chat.updateRoomSettings(command.userId, command.roomId, command.name, command.notificationsMuted); + updateChatRoomSettings(command: { userId: string; roomId: string; name?: string; notificationsMuted?: boolean; pinned?: boolean }) { + return this.chat.updateRoomSettings(command.userId, command.roomId, command.name, command.notificationsMuted, command.pinned); } @GrpcMethod('ChatService', 'AddRoomMember') diff --git a/apps/sso-core/src/domain/chat.service.ts b/apps/sso-core/src/domain/chat.service.ts index 2717910..1d5dca5 100644 --- a/apps/sso-core/src/domain/chat.service.ts +++ b/apps/sso-core/src/domain/chat.service.ts @@ -104,6 +104,11 @@ export class ChatService { ); formatted.sort((a, b) => { + const pinDiff = Number(Boolean(b.isPinned)) - Number(Boolean(a.isPinned)); + if (pinDiff !== 0) return pinDiff; + if (a.isPinned && b.isPinned) { + return (b.pinnedAt ?? '').localeCompare(a.pinnedAt ?? ''); + } const rank = (type: string) => (type === 'GENERAL' ? 0 : 1); const rankDiff = rank(a.type) - rank(b.type); if (rankDiff !== 0) return rankDiff; @@ -360,7 +365,7 @@ export class ChatService { return this.getRoomResponse(roomId, userId); } - async updateRoomSettings(userId: string, roomId: string, name?: string, notificationsMuted?: boolean) { + async updateRoomSettings(userId: string, roomId: string, name?: string, notificationsMuted?: boolean, pinned?: boolean) { const room = await this.getRoomForMember(roomId, userId); if (name !== undefined) { const trimmedName = name.trim(); @@ -381,6 +386,12 @@ export class ChatService { data: { notificationsMuted } }); } + if (pinned !== undefined) { + await this.prisma.chatRoomMember.update({ + where: { roomId_userId: { roomId, userId } }, + data: { pinnedAt: pinned ? new Date() : null } + }); + } return this.getRoomResponse(roomId, userId); } @@ -903,6 +914,7 @@ export class ChatService { id: string; userId: string; notificationsMuted: boolean; + pinnedAt?: Date | null; user: { displayName: string; avatarStorageKey: string | null; isVerified: boolean; verificationIcon: string | null }; }>; }, @@ -910,6 +922,7 @@ export class ChatService { lastMessage?: Awaited>, viewerId?: string ) { + const viewerMembership = viewerId ? room.members.find((member) => member.userId === viewerId) : undefined; const peerMember = viewerId && (room.type === 'DIRECT' || room.type === 'BOT' || room.type === 'E2E') ? room.members.find((member) => member.userId !== viewerId) @@ -926,6 +939,8 @@ export class ChatService { hasAvatar: room.hasAvatar, createdById: room.createdById ?? undefined, updatedAt: room.updatedAt.toISOString(), + isPinned: Boolean(viewerMembership?.pinnedAt), + pinnedAt: viewerMembership?.pinnedAt?.toISOString(), members: room.members.map((member) => ({ id: member.id, userId: member.userId, @@ -934,6 +949,8 @@ export class ChatService { isVerified: member.user.isVerified, verificationIcon: member.user.isVerified ? resolveVerificationIcon(member.user.verificationIcon) : undefined, notificationsMuted: member.notificationsMuted, + isPinned: Boolean(member.pinnedAt), + pinnedAt: member.pinnedAt?.toISOString(), familyRole: familyRoleByUserId.get(member.userId) ?? 'member', isChatCreator: room.createdById === member.userId })), diff --git a/docker-compose.yml b/docker-compose.yml index a8e292f..184234b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -217,6 +217,8 @@ services: INTERNAL_WS_URL: ${INTERNAL_WS_URL:-http://media-ws:8085} ports: - "127.0.0.1:3002:3000" + volumes: + - ./apps/frontend/public/downloads:/app/public/downloads:ro depends_on: api-gateway: condition: service_healthy @@ -226,6 +228,23 @@ services: timeout: 5s retries: 10 + tauri-apk-builder: + pull_policy: build + build: + context: . + dockerfile: tauri_app/Dockerfile.apk + args: + NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org} + container_name: lendry-id-tauri-apk-builder + restart: "no" + environment: + VITE_API_URL: ${PUBLIC_API_URL:-http://localhost:3000} + VITE_FRONTEND_URL: ${PUBLIC_FRONTEND_URL:-http://localhost:3002} + PUBLIC_API_URL: ${PUBLIC_API_URL:-http://localhost:3000} + PUBLIC_FRONTEND_URL: ${PUBLIC_FRONTEND_URL:-http://localhost:3002} + volumes: + - ./apps/frontend/public/downloads:/out + docs: pull_policy: build build: diff --git a/package-lock.json b/package-lock.json index f409dbf..b58ef33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,8 @@ "name": "lendry-id", "version": "0.1.0", "workspaces": [ - "apps/*" + "apps/*", + "tauri_app" ], "devDependencies": { "typescript": "^5.9.3" @@ -807,6 +808,191 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", @@ -817,6 +1003,126 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@borewit/text-codec": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", @@ -904,6 +1210,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@floating-ui/core": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", @@ -2008,6 +2756,10 @@ "resolved": "apps/sso-core", "link": true }, + "node_modules/@lendry/tauri-app": { + "resolved": "tauri_app", + "link": true + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -5291,6 +6043,402 @@ "integrity": "sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==", "license": "MIT" }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -5747,6 +6895,281 @@ "tailwindcss": "4.3.1" } }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.1.tgz", + "integrity": "sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.1", + "@tailwindcss/oxide": "4.3.1", + "tailwindcss": "4.3.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.3.tgz", + "integrity": "sha512-EElQe8z8uD7Pi5++tJ/UfEwWuK08rd3oCDYdeIbJAb6pZRrxlqmoF5gh5H5YvzmUPhS4IRCaLSsQhvWkrfK+GQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.3", + "@tauri-apps/cli-darwin-x64": "2.11.3", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.3", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.3", + "@tauri-apps/cli-linux-arm64-musl": "2.11.3", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.3", + "@tauri-apps/cli-linux-x64-gnu": "2.11.3", + "@tauri-apps/cli-linux-x64-musl": "2.11.3", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.3", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.3", + "@tauri-apps/cli-win32-x64-msvc": "2.11.3" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.3.tgz", + "integrity": "sha512-BxpaM8bsCoXs3wd4WKYhas/G1gs7+r7B+e4WnyRk2GEoVOouJB1hoL6E6YLXZDXbYci6VFdrNnobQwd2uVL4ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.3.tgz", + "integrity": "sha512-DbZYuPB1ZEzcAHYeyCvo3ltzM27+aXwPloCrtexPnmgPgulYJm3TOq6aC4S+wPhSXteddg8zImtNkvx/gQzmwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.3.tgz", + "integrity": "sha512-741NduqBmz1XkdU8yz3OI/kBZtqHbvxo9F9ytIeWYU69/Ba9dcZEbqOU++Dp0G/XU8vAI0TfTywEl+p+BbLvaA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.3.tgz", + "integrity": "sha512-RWAXT8pTqIczXcoic+LXlo6uEbAXGB0cgh6Pg7Y9xVnEbzryQ1JHtRGj9SxzrKSemBIDBH6Qc24kK2G69i8ofA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.3.tgz", + "integrity": "sha512-qomqYS+yAkd0gXMRmhguWXc7RfVN+XKKXaEwbf5QmKURwydLFOTldd6F8/WoZDSsBMrV8dpNxz0YneGLmobiSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.3.tgz", + "integrity": "sha512-jOCXbDqeDj5XcclsOBAaXjtTgwZCVg8zEZ+dbPUCoADOgljFgL0rOkYTc96vUYgOrYEfuHYihWMxIDGaD6GwJw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.3.tgz", + "integrity": "sha512-+u3HO/F3gHwL48t9gWN/urqZvpaEJzBFmTaq5eSIhvy8TOvnhb+LgJr3Q3BG+5JxuBrCUjqtOEz6gMttdJFSBA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.3.tgz", + "integrity": "sha512-spr5Jpr6KF/vehkLwJ0YmdGv8QwpWU+uw7J8bgijO0sox6ZCYsSNMbcsQjTqPi4xl+p0woIYpWXgChgHYpAc8g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.3.tgz", + "integrity": "sha512-abkoRQih5xBa3vz2spWaex0kP/MzVzVPQHom2f8jnCq46R/luOD6Uy85EMU9/bfzf6ZzdorWJsgO+OMX90Fx2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.3.tgz", + "integrity": "sha512-Vy6AvzFm1G40hg3r+OYDB3jkuu7R4wnMzbQBKuun9v6Cgg8IierpLL7toMzrZKs/8NlG8Sg4x1iLFR52oknyHg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.3.tgz", + "integrity": "sha512-GlciF75GdbseajOyib2aCHwE3BXIqZ1liGKWLFRvCdN5wm8h8hFssEVKQ/6E+2jsMLg9v7LCTb983YFnn0QSww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", + "integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, + "node_modules/@tauri-apps/plugin-stronghold": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-stronghold/-/plugin-stronghold-2.3.1.tgz", + "integrity": "sha512-zFbD1Apk/VFdWaoGaoKcouRrZnzLFiNY9b1KDeBaN47sMaMHRYIa+ZDhvbzMOyH314+OHCQBXfe8I/ph59Lp9g==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", @@ -5808,6 +7231,51 @@ "@types/node": "*" } }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -5933,6 +7401,27 @@ "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", "license": "MIT" }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -6108,6 +7597,41 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@zxing/browser": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@zxing/browser/-/browser-0.1.5.tgz", + "integrity": "sha512-4Lmrn/il4+UNb87Gk8h1iWnhj39TASEHpd91CwwSJtY5u+wa0iH9qS0wNLAWbNVYXR66WmT5uiMhZ7oVTrKfxw==", + "license": "MIT", + "optionalDependencies": { + "@zxing/text-encoding": "^0.9.0" + }, + "peerDependencies": { + "@zxing/library": "^0.21.0" + } + }, + "node_modules/@zxing/library": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz", + "integrity": "sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==", + "license": "MIT", + "peer": true, + "dependencies": { + "ts-custom-error": "^3.2.1" + }, + "engines": { + "node": ">= 10.4.0" + }, + "optionalDependencies": { + "@zxing/text-encoding": "~0.9.0" + } + }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "license": "(Unlicense OR Apache-2.0)", + "optional": true + }, "node_modules/abbrev": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", @@ -7021,6 +8545,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -7444,6 +8975,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -7649,6 +9222,24 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-type": { "version": "21.3.4", "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", @@ -7773,6 +9364,21 @@ "dev": true, "license": "Unlicense" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7792,6 +9398,16 @@ "is-property": "^1.0.2" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -8289,6 +9905,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -9926,6 +11555,16 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-remove-scroll": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", @@ -10126,6 +11765,51 @@ "node": ">= 4" } }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -10855,6 +12539,23 @@ "node": ">=0.2.6" } }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -10889,6 +12590,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ts-custom-error": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz", + "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -11246,6 +12957,81 @@ "node": ">= 0.8" } }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, "node_modules/watchpack": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", @@ -11527,6 +13313,70 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "tauri_app": { + "name": "@lendry/tauri-app", + "version": "0.1.0", + "dependencies": { + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-slot": "^1.2.4", + "@tauri-apps/api": "^2.9.0", + "@tauri-apps/plugin-opener": "^2.5.0", + "@tauri-apps/plugin-stronghold": "^2.3.0", + "@zxing/browser": "^0.1.5", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.561.0", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "tailwind-merge": "^3.4.0", + "zod": "^4.2.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.18", + "@tauri-apps/cli": "^2.9.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3", + "vite": "^7.3.0" + } + }, + "tauri_app/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz", + "integrity": "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "tauri_app/node_modules/@radix-ui/react-slot": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.0.tgz", + "integrity": "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 8194bac..649e85b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "version": "0.1.0", "description": "Enterprise Identity Provider ecosystem inspired by Yandex ID.", "workspaces": [ - "apps/*" + "apps/*", + "tauri_app" ], "scripts": { "build": "npm run build --workspaces --if-present", diff --git a/shared/proto/chat.proto b/shared/proto/chat.proto index 8ca5690..95e24e0 100644 --- a/shared/proto/chat.proto +++ b/shared/proto/chat.proto @@ -46,6 +46,7 @@ message UpdateRoomSettingsRequest { string roomId = 2; optional string name = 3; optional bool notificationsMuted = 4; + optional bool pinned = 5; } message AddRoomMemberRequest { @@ -156,6 +157,8 @@ message ChatRoomMemberResponse { bool notificationsMuted = 5; string familyRole = 6; bool isChatCreator = 7; + bool isPinned = 8; + optional string pinnedAt = 9; } message ChatRoomResponse { @@ -171,6 +174,8 @@ message ChatRoomResponse { optional string peerUserId = 10; optional string botUsername = 11; optional bool isE2E = 12; + bool isPinned = 13; + optional string pinnedAt = 14; } message PollOptionResponse { diff --git a/tauri_app/Dockerfile.apk b/tauri_app/Dockerfile.apk new file mode 100644 index 0000000..ea85b4b --- /dev/null +++ b/tauri_app/Dockerfile.apk @@ -0,0 +1,64 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive +ENV ANDROID_HOME=/opt/android-sdk +ENV ANDROID_SDK_ROOT=/opt/android-sdk +ENV ANDROID_NDK_HOME=/opt/android-sdk/ndk/27.0.12077973 +ENV NDK_HOME=/opt/android-sdk/ndk/27.0.12077973 +ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 +ENV PATH=/root/.cargo/bin:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:/usr/local/bin:$PATH + +ARG NPM_REGISTRY=https://registry.npmjs.org + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + unzip \ + xz-utils \ + git \ + build-essential \ + pkg-config \ + libssl-dev \ + openjdk-17-jdk \ + nodejs \ + npm \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p ${ANDROID_HOME}/cmdline-tools \ + && curl -fsSL https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -o /tmp/android-tools.zip \ + && unzip -q /tmp/android-tools.zip -d /tmp/android-tools \ + && mv /tmp/android-tools/cmdline-tools ${ANDROID_HOME}/cmdline-tools/latest \ + && rm -rf /tmp/android-tools /tmp/android-tools.zip + +RUN yes | sdkmanager --licenses >/dev/null \ + && sdkmanager \ + "platform-tools" \ + "platforms;android-35" \ + "build-tools;35.0.0" \ + "ndk;27.0.12077973" + +RUN curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal \ + && rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android + +WORKDIR /workspace + +COPY package.json package-lock.json .npmrc ./ +COPY apps/sso-core/package.json ./apps/sso-core/ +COPY apps/api-gateway/package.json ./apps/api-gateway/ +COPY apps/frontend/package.json ./apps/frontend/ +COPY apps/docs/package.json ./apps/docs/ +COPY tauri_app/package.json ./tauri_app/ + +RUN --mount=type=cache,target=/root/.npm \ + npm config set registry "${NPM_REGISTRY}" && \ + npm ci --no-audit --no-fund + +COPY apps/frontend ./apps/frontend +COPY shared ./shared +COPY tsconfig.base.json ./ +COPY tauri_app ./tauri_app +COPY tauri_app/docker/build-android-apk.sh /usr/local/bin/build-android-apk + +RUN chmod +x /usr/local/bin/build-android-apk + +CMD ["build-android-apk"] diff --git a/tauri_app/README.md b/tauri_app/README.md new file mode 100644 index 0000000..f0d77f7 --- /dev/null +++ b/tauri_app/README.md @@ -0,0 +1,62 @@ +# Lendry ID Super App + +Tauri v2 приложение для macOS, iOS и Android. Использует тот же React/Tailwind/shadcn UI стиль, что и `apps/frontend`, через Vite alias: + +```ts +'@': '../apps/frontend' +``` + +## Запуск + +```bash +npm install +npm --workspace @lendry/tauri-app run tauri:dev +``` + +## Web build + +```bash +npm --workspace @lendry/tauri-app run build +``` + +## Mobile bootstrap + +```bash +npm --workspace @lendry/tauri-app run android:init +npm --workspace @lendry/tauri-app run ios:init +``` + +После генерации проверьте `src-tauri/mobile-permissions.md` и добавьте camera permissions, если CLI не внёс их автоматически. + +## Docker APK build + +При запуске общего `docker compose up --build` сервис `tauri-apk-builder` собирает Android APK и кладёт файл сюда: + +```text +apps/frontend/public/downloads/lendry-id.apk +``` + +После этого кнопка «Скачать приложение» на странице входа скачивает APK через: + +```text +/download/android +``` + +Собрать только APK: + +```bash +docker compose up --build tauri-apk-builder +``` + +## Переменные окружения + +```bash +VITE_API_URL=https://sso.example.ru/idp-api +VITE_FRONTEND_URL=https://sso.example.ru +``` + +## Реализованные модули + +- `Chats`: wrapper существующего web UI `/family`, чтобы не дублировать messenger. +- `Security`: QR scanner + approve `/auth/advanced/qr/session/:sessionId/approve`, список сессий и revoke. +- `Authenticator`: offline TOTP RFC 6238 с хранением seeds в `tauri-plugin-stronghold`. diff --git a/tauri_app/docker/build-android-apk.sh b/tauri_app/docker/build-android-apk.sh new file mode 100644 index 0000000..1e7c12e --- /dev/null +++ b/tauri_app/docker/build-android-apk.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd /workspace + +export VITE_API_URL="${VITE_API_URL:-${PUBLIC_API_URL:-http://localhost:3000}}" +export VITE_FRONTEND_URL="${VITE_FRONTEND_URL:-${PUBLIC_FRONTEND_URL:-http://localhost:3002}}" + +mkdir -p /out + +echo "==> Проверка web-сборки tauri_app" +npm --workspace @lendry/tauri-app run build + +cd /workspace/tauri_app + +if [ ! -d "src-tauri/gen/android" ]; then + echo "==> Инициализация Android проекта Tauri" + npm run android:init -- --ci || npm run android:init +fi + +echo "==> Сборка Android APK" +npm run tauri -- android build --apk + +APK_PATH="$(find /workspace/tauri_app/src-tauri/gen/android -type f -name '*.apk' | sort | tail -n 1 || true)" + +if [ -z "${APK_PATH}" ] || [ ! -f "${APK_PATH}" ]; then + echo "APK не найден после сборки" >&2 + find /workspace/tauri_app/src-tauri/gen/android -maxdepth 8 -type f | sort >&2 || true + exit 1 +fi + +cp "${APK_PATH}" /out/lendry-id.apk +chmod 0644 /out/lendry-id.apk + +echo "==> APK готов: /out/lendry-id.apk" +ls -lh /out/lendry-id.apk diff --git a/tauri_app/index.html b/tauri_app/index.html new file mode 100644 index 0000000..fab44d8 --- /dev/null +++ b/tauri_app/index.html @@ -0,0 +1,13 @@ + + + + + + + IDP App + + +
+ + + diff --git a/tauri_app/package.json b/tauri_app/package.json new file mode 100644 index 0000000..66a3e05 --- /dev/null +++ b/tauri_app/package.json @@ -0,0 +1,45 @@ +{ + "name": "@lendry/tauri-app", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "tauri": "tauri", + "tauri:dev": "tauri dev", + "tauri:build": "tauri build", + "android:init": "tauri android init", + "android:dev": "tauri android dev", + "ios:init": "tauri ios init", + "ios:dev": "tauri ios dev" + }, + "dependencies": { + "@radix-ui/react-avatar": "^1.1.11", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-slot": "^1.2.4", + "@tauri-apps/api": "^2.9.0", + "@tauri-apps/plugin-opener": "^2.5.0", + "@tauri-apps/plugin-stronghold": "^2.3.0", + "@zxing/browser": "^0.1.5", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.561.0", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "tailwind-merge": "^3.4.0", + "zod": "^4.2.1" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.18", + "@tauri-apps/cli": "^2.9.0", + "@types/node": "^24.10.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", + "tailwindcss": "^4.1.18", + "typescript": "^5.9.3", + "vite": "^7.3.0" + } +} diff --git a/tauri_app/src-tauri/Cargo.toml b/tauri_app/src-tauri/Cargo.toml new file mode 100644 index 0000000..11bd550 --- /dev/null +++ b/tauri_app/src-tauri/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "lendry_id_super_app" +version = "0.1.0" +description = "Lendry ID Super App" +authors = ["Lendry"] +edition = "2021" +rust-version = "1.77" + +[lib] +name = "lendry_id_super_app_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tauri = { version = "2", features = [] } +tauri-plugin-opener = "2" +tauri-plugin-stronghold = "2" +url = "2" +dirs-next = "2" + +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +opt-level = "s" +strip = true diff --git a/tauri_app/src-tauri/build.rs b/tauri_app/src-tauri/build.rs new file mode 100644 index 0000000..261851f --- /dev/null +++ b/tauri_app/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build(); +} diff --git a/tauri_app/src-tauri/capabilities/default.json b/tauri_app/src-tauri/capabilities/default.json new file mode 100644 index 0000000..ff33ca2 --- /dev/null +++ b/tauri_app/src-tauri/capabilities/default.json @@ -0,0 +1,11 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Основные разрешения Lendry ID Super App", + "windows": ["main"], + "permissions": [ + "core:default", + "opener:default", + "stronghold:default" + ] +} diff --git a/tauri_app/src-tauri/mobile-permissions.md b/tauri_app/src-tauri/mobile-permissions.md new file mode 100644 index 0000000..728d865 --- /dev/null +++ b/tauri_app/src-tauri/mobile-permissions.md @@ -0,0 +1,30 @@ +# Mobile Permissions + +После генерации native-проектов командами: + +```bash +npm run android:init +npm run ios:init +``` + +проверьте, что Tauri CLI добавил разрешения. Если нет, внесите их вручную. + +## Android + +`src-tauri/gen/android/app/src/main/AndroidManifest.xml`: + +```xml + + +``` + +## iOS + +`src-tauri/gen/apple//Info.plist`: + +```xml +NSCameraUsageDescription +Камера нужна для сканирования QR-кода входа Lendry ID. +``` + +QR-сканер использует HTML5 WebRTC внутри WebView. TOTP-секреты сохраняются через `tauri-plugin-stronghold`, а не в LocalStorage. diff --git a/tauri_app/src-tauri/src/lib.rs b/tauri_app/src-tauri/src/lib.rs new file mode 100644 index 0000000..36b201a --- /dev/null +++ b/tauri_app/src-tauri/src/lib.rs @@ -0,0 +1,148 @@ +use serde::{Deserialize, Serialize}; +use tauri::Manager; +use url::Url; + +const MAX_LABEL_LEN: usize = 80; +const MAX_ISSUER_LEN: usize = 80; +const MIN_TOTP_SECRET_LEN: usize = 16; +const MAX_TOTP_SECRET_LEN: usize = 256; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct ValidationResult { + ok: bool, + value: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TotpProfileInput { + issuer: String, + label: String, + secret: String, + digits: Option, + period: Option, +} + +#[tauri::command] +fn validate_qr_session_payload(payload: String) -> Result { + let trimmed = payload.trim(); + if trimmed.is_empty() { + return Err("QR-код пустой".into()); + } + + let session_id = match Url::parse(trimmed) { + Ok(url) => url + .query_pairs() + .find(|(key, _)| key == "sessionId" || key == "session_id") + .map(|(_, value)| value.to_string()) + .or_else(|| { + url.path_segments() + .and_then(|segments| segments.last().map(ToString::to_string)) + }) + .unwrap_or_default(), + Err(_) => trimmed.to_string(), + }; + + validate_uuid_like(&session_id)?; + Ok(ValidationResult { + ok: true, + value: session_id, + }) +} + +#[tauri::command] +fn validate_api_base_url(url: String) -> Result { + let parsed = Url::parse(url.trim()).map_err(|_| "Некорректный URL API".to_string())?; + match parsed.scheme() { + "http" | "https" => Ok(ValidationResult { + ok: true, + value: parsed.as_str().trim_end_matches('/').to_string(), + }), + _ => Err("URL API должен начинаться с http:// или https://".into()), + } +} + +#[tauri::command] +fn validate_totp_profile_input(input: TotpProfileInput) -> Result { + let issuer = input.issuer.trim(); + let label = input.label.trim(); + let secret = normalize_base32_secret(&input.secret); + let digits = input.digits.unwrap_or(6); + let period = input.period.unwrap_or(30); + + if issuer.is_empty() || issuer.len() > MAX_ISSUER_LEN { + return Err("Название сервиса должно содержать от 1 до 80 символов".into()); + } + if label.is_empty() || label.len() > MAX_LABEL_LEN { + return Err("Название аккаунта должно содержать от 1 до 80 символов".into()); + } + if secret.len() < MIN_TOTP_SECRET_LEN || secret.len() > MAX_TOTP_SECRET_LEN { + return Err("TOTP secret должен содержать от 16 до 256 символов base32".into()); + } + if !secret.chars().all(|ch| matches!(ch, 'A'..='Z' | '2'..='7' | '=')) { + return Err("TOTP secret должен быть в формате base32".into()); + } + if digits != 6 && digits != 8 { + return Err("TOTP поддерживает только 6 или 8 цифр".into()); + } + if !(15..=120).contains(&period) { + return Err("Период TOTP должен быть от 15 до 120 секунд".into()); + } + + Ok(ValidationResult { + ok: true, + value: secret, + }) +} + +fn validate_uuid_like(value: &str) -> Result<(), String> { + let normalized = value.trim(); + if normalized.len() != 36 { + return Err("QR-код не содержит корректный UUID сессии".into()); + } + for (index, ch) in normalized.chars().enumerate() { + let dash = matches!(index, 8 | 13 | 18 | 23); + if dash && ch != '-' { + return Err("QR-код не содержит корректный UUID сессии".into()); + } + if !dash && !ch.is_ascii_hexdigit() { + return Err("QR-код не содержит корректный UUID сессии".into()); + } + } + Ok(()) +} + +fn normalize_base32_secret(secret: &str) -> String { + secret + .trim() + .chars() + .filter(|ch| !ch.is_whitespace() && *ch != '-') + .map(|ch| ch.to_ascii_uppercase()) + .collect() +} + +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_stronghold::Builder::new(|password| { + use tauri_plugin_stronghold::stronghold::Client; + + let salt_path = dirs_next::data_dir() + .unwrap_or_else(std::env::temp_dir) + .join("lendry-id-stronghold.salt"); + tauri_plugin_stronghold::stronghold::derive_key_from_password(password.as_ref(), &salt_path) + }) + .build()) + .invoke_handler(tauri::generate_handler![ + validate_qr_session_payload, + validate_api_base_url, + validate_totp_profile_input + ]) + .setup(|app| { + let _ = app.path().app_data_dir().map(std::fs::create_dir_all); + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("ошибка запуска Lendry ID Super App"); +} diff --git a/tauri_app/src-tauri/src/main.rs b/tauri_app/src-tauri/src/main.rs new file mode 100644 index 0000000..942f14c --- /dev/null +++ b/tauri_app/src-tauri/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + lendry_id_super_app_lib::run(); +} diff --git a/tauri_app/src-tauri/tauri.conf.json b/tauri_app/src-tauri/tauri.conf.json new file mode 100644 index 0000000..baa8c48 --- /dev/null +++ b/tauri_app/src-tauri/tauri.conf.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Lendry ID", + "version": "0.1.0", + "identifier": "ru.lendry.id.superapp", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "withGlobalTauri": false, + "windows": [ + { + "title": "Lendry ID", + "width": 1180, + "height": 820, + "minWidth": 390, + "minHeight": 720, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": "default-src 'self'; connect-src 'self' http://localhost:* https://* ws://localhost:* wss://*; img-src 'self' data: blob: https://*; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; script-src 'self';" + } + }, + "bundle": { + "active": true, + "targets": ["dmg", "app", "msi", "deb", "rpm"], + "category": "Productivity", + "shortDescription": "Мессенджер, SSO-аутентификатор и TOTP в одном приложении", + "longDescription": "Lendry ID Super App объединяет семейный мессенджер, QR-подтверждение входа и безопасное хранилище TOTP-кодов.", + "ios": { + "developmentTeam": "" + }, + "android": { + "minSdkVersion": 24 + } + }, + "plugins": { + "opener": {} + } +} diff --git a/tauri_app/src/App.tsx b/tauri_app/src/App.tsx new file mode 100644 index 0000000..a623732 --- /dev/null +++ b/tauri_app/src/App.tsx @@ -0,0 +1,32 @@ +import { useState } from 'react'; +import { MobileShell, type MobileTab } from './components/mobile-shell'; +import { LoginScreen } from './features/auth/login-screen'; +import { ChatsScreen } from './features/chat/chats-screen'; +import { SecurityScreen } from './features/security/security-screen'; +import { TotpScreen } from './features/totp/totp-screen'; +import { MobileAuthProvider, useMobileAuth } from './lib/auth'; + +function AppContent() { + const { isAuthenticated } = useMobileAuth(); + const [activeTab, setActiveTab] = useState('security'); + + if (!isAuthenticated) { + return ; + } + + return ( + + {activeTab === 'chats' ? : null} + {activeTab === 'totp' ? : null} + {activeTab === 'security' ? : null} + + ); +} + +export function App() { + return ( + + + + ); +} diff --git a/tauri_app/src/components/mobile-shell.tsx b/tauri_app/src/components/mobile-shell.tsx new file mode 100644 index 0000000..48e9eb7 --- /dev/null +++ b/tauri_app/src/components/mobile-shell.tsx @@ -0,0 +1,76 @@ +import { KeyRound, MessageCircle, ShieldCheck } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +export type MobileTab = 'chats' | 'totp' | 'security'; + +const tabs: Array<{ id: MobileTab; label: string; icon: React.ComponentType<{ className?: string }> }> = [ + { id: 'chats', label: 'Чаты', icon: MessageCircle }, + { id: 'totp', label: 'Коды', icon: KeyRound }, + { id: 'security', label: 'Защита', icon: ShieldCheck } +]; + +export function MobileShell({ + activeTab, + onTabChange, + children +}: { + activeTab: MobileTab; + onTabChange: (tab: MobileTab) => void; + children: React.ReactNode; +}) { + return ( +
+
+ + +
{children}
+
+ + +
+ ); +} diff --git a/tauri_app/src/features/auth/login-screen.tsx b/tauri_app/src/features/auth/login-screen.tsx new file mode 100644 index 0000000..d0e39cc --- /dev/null +++ b/tauri_app/src/features/auth/login-screen.tsx @@ -0,0 +1,159 @@ +import { useState } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { KeyRound, Loader2, ShieldCheck } from 'lucide-react'; +import { BrandLogo, ProjectTagline } from '@/components/id/brand-logo'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { beginTotpLogin, getMobileApiUrl, loginWithPassword, setMobileApiUrl, verifyTotpLogin } from '~/lib/mobile-api'; +import { useMobileAuth } from '~/lib/auth'; +import { getErrorMessage } from '~/lib/errors'; + +export function LoginScreen() { + const { applyAuth } = useMobileAuth(); + const [apiUrl, setApiUrl] = useState(getMobileApiUrl()); + const [loginValue, setLoginValue] = useState(''); + const [password, setPassword] = useState(''); + const [totpCode, setTotpCode] = useState(''); + const [totpChallengeToken, setTotpChallengeToken] = useState(null); + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + async function submit() { + setSubmitting(true); + setError(null); + try { + const validated = await invoke<{ value: string }>('validate_api_base_url', { url: apiUrl }); + setMobileApiUrl(validated.value); + const auth = await loginWithPassword(loginValue.trim(), password); + if ('requiresTotp' in auth) { + if (!auth.totpChallengeToken) { + throw new Error('Сервер не вернул TOTP challenge'); + } + setTotpChallengeToken(auth.totpChallengeToken); + setError(null); + return; + } + applyAuth(auth); + } catch (err) { + setError(getErrorMessage(err, 'Не удалось войти')); + } finally { + setSubmitting(false); + } + } + + async function startTotpLogin() { + if (!loginValue.trim()) { + setError('Укажите почту, телефон или логин'); + return; + } + setSubmitting(true); + setError(null); + try { + const validated = await invoke<{ value: string }>('validate_api_base_url', { url: apiUrl }); + setMobileApiUrl(validated.value); + const response = await beginTotpLogin(loginValue.trim()); + setTotpChallengeToken(response.totpChallengeToken); + } catch (err) { + setError(getErrorMessage(err, 'Не удалось начать TOTP-вход')); + } finally { + setSubmitting(false); + } + } + + async function submitTotp() { + if (!totpChallengeToken) return; + setSubmitting(true); + setError(null); + try { + const auth = await verifyTotpLogin(totpChallengeToken, totpCode.trim()); + applyAuth(auth); + } catch (err) { + setError(getErrorMessage(err, 'Неверный код аутентификатора')); + } finally { + setSubmitting(false); + } + } + + return ( +
+
+
+ +

Войдите с ID

+

Мессенджер, QR-вход и TOTP-коды в одном приложении

+
+ +
+
+ + setApiUrl(event.target.value)} + placeholder="https://sso.example.ru/idp-api" + className="h-[54px] border-[#555762] bg-transparent text-white placeholder:text-[#8f92a0]" + /> +
+
+ + setLoginValue(event.target.value)} + autoComplete="username" + className="h-[54px] border-[#555762] bg-transparent text-white placeholder:text-[#8f92a0]" + /> +
+ {totpChallengeToken ? ( +
+ + setTotpCode(event.target.value.replace(/\D/g, '').slice(0, 8))} + inputMode="numeric" + autoComplete="one-time-code" + className="h-[58px] border-[#555762] bg-transparent text-center text-2xl tracking-[0.35em] text-white placeholder:text-[#8f92a0]" + placeholder="000000" + /> +
+ ) : ( +
+ + setPassword(event.target.value)} + type="password" + autoComplete="current-password" + className="h-[54px] border-[#555762] bg-transparent text-white placeholder:text-[#8f92a0]" + /> +
+ )} +
+ + {error ?

{error}

: null} + + {totpChallengeToken ? ( + <> + + + + ) : ( + <> + + + + )} + +
+
+ ); +} diff --git a/tauri_app/src/features/auth/qr-scanner.tsx b/tauri_app/src/features/auth/qr-scanner.tsx new file mode 100644 index 0000000..cb45940 --- /dev/null +++ b/tauri_app/src/features/auth/qr-scanner.tsx @@ -0,0 +1,107 @@ +import { useEffect, useRef, useState } from 'react'; +import { BrowserMultiFormatReader, type IScannerControls } from '@zxing/browser'; +import { invoke } from '@tauri-apps/api/core'; +import { Camera, CheckCircle2, Loader2, QrCode, XCircle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { approveQrLogin } from '~/lib/mobile-api'; +import { useMobileAuth } from '~/lib/auth'; +import { getErrorMessage } from '~/lib/errors'; + +export function QrScanner() { + const { token } = useMobileAuth(); + const videoRef = useRef(null); + const controlsRef = useRef(null); + const [scanning, setScanning] = useState(false); + const [status, setStatus] = useState(null); + const [approving, setApproving] = useState(false); + + useEffect(() => { + return () => { + controlsRef.current?.stop(); + }; + }, []); + + async function approvePayload(payload: string) { + if (!token || approving) return; + setApproving(true); + setStatus('Проверяем QR-код...'); + try { + const validated = await invoke<{ value: string }>('validate_qr_session_payload', { payload }); + await approveQrLogin(validated.value, token); + setStatus('Вход на сайте подтверждён'); + controlsRef.current?.stop(); + setScanning(false); + } catch (error) { + setStatus(getErrorMessage(error, 'Не удалось подтвердить вход')); + } finally { + setApproving(false); + } + } + + async function startScan() { + if (!videoRef.current) return; + setStatus(null); + setScanning(true); + const reader = new BrowserMultiFormatReader(); + try { + controlsRef.current = await reader.decodeFromVideoDevice(undefined, videoRef.current, (result, error) => { + if (result?.getText()) { + void approvePayload(result.getText()); + } else if (error) { + setStatus('Камера активна, наведите её на QR-код'); + } + }); + } catch (error) { + setStatus(getErrorMessage(error, 'Не удалось открыть камеру')); + setScanning(false); + } + } + + function stopScan() { + controlsRef.current?.stop(); + setScanning(false); + } + + return ( +
+
+
+

QR-вход

+

Сканируйте QR-код на сайте, чтобы мгновенно подтвердить вход.

+
+
+ +
+
+ +
+
+ + {status ? ( +
+ {status.includes('подтвержд') ? : } + {status} +
+ ) : null} + +
+ {scanning ? ( + + ) : ( + + )} + {approving ? ( + + ) : null} +
+
+ ); +} diff --git a/tauri_app/src/features/chat/chats-screen.tsx b/tauri_app/src/features/chat/chats-screen.tsx new file mode 100644 index 0000000..08cab15 --- /dev/null +++ b/tauri_app/src/features/chat/chats-screen.tsx @@ -0,0 +1,35 @@ +import { openUrl } from '@tauri-apps/plugin-opener'; +import { ExternalLink, MessageCircle } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +const FRONTEND_URL = (import.meta.env.VITE_FRONTEND_URL ?? 'http://localhost:3002').replace(/\/$/, ''); + +export function ChatsScreen() { + const familyUrl = `${FRONTEND_URL}/family`; + + return ( +
+
+
+
+

Messenger

+

Чаты

+

Вкладка использует web UI семьи, чтобы дизайн и поведение совпадали с сайтом.

+
+
+ +
+
+
+ +
+