{profile.issuer}
+{profile.label}
+{state?.remaining ?? profile.period} сек.
+{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{user ? roomDisplayLabel(room, user.id) : room.name}
+
+ {user ? roomDisplayLabel(room, user.id) : room.name}
+ {room.isPinned ?
{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{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Мессенджер, QR-вход и TOTP-коды в одном приложении
+{error}
: null} + + {totpChallengeToken ? ( + <> +Сканируйте QR-код на сайте, чтобы мгновенно подтвердить вход.
+Messenger
+Вкладка использует web UI семьи, чтобы дизайн и поведение совпадали с сайтом.
+SSO Authenticator
+{user?.displayName}
+Завершайте чужие или потерянные сессии удалённо.
+{error}
: null} + +{session.userAgent ?? 'Устройство'}
++ {session.status} · {formatDate(session.updatedAt)} + {current ? ' · текущая сессия' : ''} +
+Активных сессий не найдено
: null} +Offline 2FA
+TOTP-секреты хранятся в Tauri Stronghold, а не в LocalStorage.
+{error}
: null} +{profile.label}
+{state?.remaining ?? profile.period} сек.
+