global fix and update bot Api
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { CheckCircle2, Copy, Forward, Pencil, Reply, Trash2 } from 'lucide-react';
|
||||
import { CheckCircle2, Copy, Forward, Pencil, Pin, PinOff, Reply, Trash2, XCircle } from 'lucide-react';
|
||||
import { ChatMessageReactions } from '@/components/chat/chat-message-reactions';
|
||||
import {
|
||||
ContextMenu,
|
||||
@@ -24,6 +24,7 @@ interface ChatMessageContextMenuProps {
|
||||
onDelete?: () => void;
|
||||
onReply?: () => void;
|
||||
onForward?: () => void;
|
||||
onTogglePin?: () => void;
|
||||
onSelect?: () => void;
|
||||
onToggleReaction?: (emoji: string) => void;
|
||||
onCopy?: () => void;
|
||||
@@ -39,6 +40,7 @@ export function ChatMessageContextMenu({
|
||||
onDelete,
|
||||
onReply,
|
||||
onForward,
|
||||
onTogglePin,
|
||||
onSelect,
|
||||
onToggleReaction,
|
||||
onCopy
|
||||
@@ -125,6 +127,17 @@ export function ChatMessageContextMenu({
|
||||
Переслать
|
||||
</ContextMenuItem>
|
||||
) : null}
|
||||
{onTogglePin && !disabled ? (
|
||||
<ContextMenuItem
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
runMenuAction(onTogglePin);
|
||||
}}
|
||||
>
|
||||
{message.isPinned ? <PinOff className="h-4 w-4" /> : <Pin className="h-4 w-4" />}
|
||||
{message.isPinned ? 'Открепить' : 'Закрепить'}
|
||||
</ContextMenuItem>
|
||||
) : null}
|
||||
{deletable ? (
|
||||
<ContextMenuItem
|
||||
className="text-red-600 data-[highlighted]:bg-red-50 data-[highlighted]:text-red-700"
|
||||
@@ -161,6 +174,108 @@ export function ChatMessageContextMenu({
|
||||
);
|
||||
}
|
||||
|
||||
interface ChatSelectionContextMenuProps {
|
||||
count: number;
|
||||
canPin?: boolean;
|
||||
pinned?: boolean;
|
||||
children: React.ReactNode;
|
||||
onReply?: () => void;
|
||||
onCopy?: () => void;
|
||||
onForward?: () => void;
|
||||
onDelete?: () => void;
|
||||
onTogglePin?: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export function ChatSelectionContextMenu({
|
||||
count,
|
||||
canPin,
|
||||
pinned,
|
||||
children,
|
||||
onReply,
|
||||
onCopy,
|
||||
onForward,
|
||||
onDelete,
|
||||
onTogglePin,
|
||||
onCancel
|
||||
}: ChatSelectionContextMenuProps) {
|
||||
function runMenuAction(action?: () => void) {
|
||||
suppressChatDragSelection();
|
||||
action?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
onOpenChange={(open) => {
|
||||
if (open) suppressChatDragSelection(800);
|
||||
}}
|
||||
>
|
||||
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||||
<ContextMenuContent className="min-w-[260px]">
|
||||
<ContextMenuItem
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
runMenuAction(onReply);
|
||||
}}
|
||||
>
|
||||
<Reply className="h-4 w-4" />
|
||||
Ответить
|
||||
</ContextMenuItem>
|
||||
{canPin && onTogglePin ? (
|
||||
<ContextMenuItem
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
runMenuAction(onTogglePin);
|
||||
}}
|
||||
>
|
||||
{pinned ? <PinOff className="h-4 w-4" /> : <Pin className="h-4 w-4" />}
|
||||
{pinned ? 'Открепить' : 'Закрепить'}
|
||||
</ContextMenuItem>
|
||||
) : null}
|
||||
<ContextMenuItem
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
runMenuAction(onCopy);
|
||||
}}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
Копировать выбранное как текст
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
runMenuAction(onForward);
|
||||
}}
|
||||
>
|
||||
<Forward className="h-4 w-4" />
|
||||
Переслать выбранное
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
className="text-red-600 data-[highlighted]:bg-red-50 data-[highlighted]:text-red-700"
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
runMenuAction(onDelete);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
Удалить выбранное
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
runMenuAction(onCancel);
|
||||
}}
|
||||
>
|
||||
<XCircle className="h-4 w-4" />
|
||||
Отменить выбор
|
||||
</ContextMenuItem>
|
||||
<span className="sr-only">Выбрано сообщений: {count}</span>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
interface SelectableMessageWrapperProps {
|
||||
selected?: boolean;
|
||||
selectionMode?: boolean;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { LayoutGrid, Loader2, Send, X } from 'lucide-react';
|
||||
import { LayoutGrid, Loader2, Pin, Send, X } from 'lucide-react';
|
||||
import { BotMessageKeyboard } from '@/components/chat/bot-message-keyboard';
|
||||
import { BotChatMessageContextMenu } from '@/components/chat/bot-chat-message-context-menu';
|
||||
import { useRealtime } from '@/components/notifications/realtime-provider';
|
||||
@@ -135,7 +135,9 @@ export function FamilyBotChat({
|
||||
messageType: typeof payload.messageType === 'string' ? payload.messageType : 'text',
|
||||
messageId,
|
||||
createdAt: new Date().toISOString(),
|
||||
replyMarkupJson
|
||||
replyMarkupJson,
|
||||
isPinned: Boolean(payload.isPinned),
|
||||
pinnedAt: typeof payload.pinnedAt === 'string' ? payload.pinnedAt : null
|
||||
};
|
||||
|
||||
setMessages((current) => {
|
||||
@@ -183,6 +185,27 @@ export function FamilyBotChat({
|
||||
: item
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'bot_message_pinned') {
|
||||
const messageId = Number(payload.messageId);
|
||||
if (!Number.isFinite(messageId)) {
|
||||
void loadMessages();
|
||||
return;
|
||||
}
|
||||
|
||||
setMessages((current) =>
|
||||
current.map((item) =>
|
||||
item.messageId === messageId && item.direction === 'out'
|
||||
? {
|
||||
...item,
|
||||
isPinned: Boolean(payload.isPinned),
|
||||
pinnedAt: typeof payload.pinnedAt === 'string' ? payload.pinnedAt : null
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
}, [botRef, loadMessages, subscribe]);
|
||||
@@ -227,6 +250,12 @@ export function FamilyBotChat({
|
||||
<div key={message.id} className={cn('flex', mine ? 'justify-end' : 'justify-start')}>
|
||||
<BotChatMessageContextMenu text={message.text} onCopy={() => showToast('Текст скопирован')}>
|
||||
<div className={cn('max-w-[78%] rounded-[18px] px-3 py-2 shadow-sm', mine ? 'rounded-br-md bg-[#effdde]' : 'rounded-bl-md bg-white')}>
|
||||
{message.isPinned ? (
|
||||
<div className={cn('mb-1 flex items-center gap-1 text-[11px] font-medium', mine ? 'text-[#6c8f56]' : 'text-[#3390ec]')}>
|
||||
<Pin className="h-3 w-3" />
|
||||
Закреплено
|
||||
</div>
|
||||
) : null}
|
||||
{!mine ? <p className="mb-0.5 text-[11px] font-semibold text-[#3390ec]">{botName}</p> : null}
|
||||
<p className="whitespace-pre-wrap text-sm">{message.text || '…'}</p>
|
||||
{!mine && hasKeyboard && message.messageId > 0 ? (
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Loader2,
|
||||
Mic,
|
||||
Paperclip,
|
||||
Pin,
|
||||
Plus,
|
||||
Search,
|
||||
Send,
|
||||
@@ -37,7 +38,7 @@ import { ChatDateSeparator } from '@/components/chat/chat-date-separator';
|
||||
import { ChatToolsDialog, type ChatToolsTab } from '@/components/chat/chat-tools-dialog';
|
||||
import { ChatDeleteMessageDialog } from '@/components/chat/chat-delete-message-dialog';
|
||||
import { ChatMessageEditBanner } from '@/components/chat/chat-message-edit-banner';
|
||||
import { ChatMessageContextMenu, SelectableMessageWrapper } from '@/components/chat/chat-message-context-menu';
|
||||
import { ChatMessageContextMenu, ChatSelectionContextMenu, SelectableMessageWrapper } from '@/components/chat/chat-message-context-menu';
|
||||
import { ChatMessageReactions } from '@/components/chat/chat-message-reactions';
|
||||
import { ChatSelectionToolbar } from '@/components/chat/chat-selection-toolbar';
|
||||
import { EmojiPicker } from '@/components/chat/emoji-picker';
|
||||
@@ -78,6 +79,7 @@ import {
|
||||
removeFamilyMember,
|
||||
reportChatTyping,
|
||||
sendChatMessage,
|
||||
setChatMessagePinned,
|
||||
setChatRoomMuted,
|
||||
toggleChatMessageReaction,
|
||||
updateFamilyGroup,
|
||||
@@ -922,6 +924,39 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTogglePin(message: ChatMessage) {
|
||||
if (!token) return;
|
||||
try {
|
||||
const updated = await setChatMessagePinned(message.id, !message.isPinned, token);
|
||||
setMessages((current) => current.map((item) => (item.id === updated.id ? updated : item)));
|
||||
showToast(updated.isPinned ? 'Сообщение закреплено' : 'Сообщение откреплено');
|
||||
} catch (error) {
|
||||
showToast(getApiErrorMessage(error, 'Не удалось изменить закрепление') ?? 'Ошибка');
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectionReply() {
|
||||
const selected = getSelectedMessages();
|
||||
if (selected.length !== 1) {
|
||||
showToast('Ответить можно только на одно сообщение');
|
||||
return;
|
||||
}
|
||||
exitSelectionMode();
|
||||
setReplyToMessage(selected[0]!);
|
||||
setEditingMessageId(null);
|
||||
setDraft('');
|
||||
setEmojiOpen(false);
|
||||
}
|
||||
|
||||
function handleSelectionPin() {
|
||||
const selected = getSelectedMessages();
|
||||
if (selected.length !== 1) {
|
||||
showToast('Закрепить можно только одно сообщение');
|
||||
return;
|
||||
}
|
||||
void handleTogglePin(selected[0]!);
|
||||
}
|
||||
|
||||
function getSelectedMessages() {
|
||||
const selected = new Set(selectedMessageIds);
|
||||
return visibleMessages.filter((message) => selected.has(message.id));
|
||||
@@ -1483,6 +1518,12 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
)
|
||||
)}
|
||||
>
|
||||
{message.isPinned && !isLargeEmoji ? (
|
||||
<div className={cn('mb-1 flex items-center gap-1 text-[11px] font-medium', mine ? 'text-[#6c8f56]' : 'text-[#3390ec]')}>
|
||||
<Pin className="h-3 w-3" />
|
||||
Закреплено
|
||||
</div>
|
||||
) : null}
|
||||
{!mine ? <p className="mb-1 text-xs font-semibold text-[#3390ec]">{message.senderName}</p> : null}
|
||||
{forwardedFrom ? (
|
||||
<p className="mb-1 text-xs font-medium text-[#3390ec]">↪️ Переслано от {forwardedFrom.senderName}</p>
|
||||
@@ -1648,14 +1689,26 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
/>
|
||||
) : null}
|
||||
{selectionMode ? (
|
||||
<SelectableMessageWrapper
|
||||
selected={isSelected}
|
||||
selectionMode
|
||||
align={mine ? 'right' : 'left'}
|
||||
onClick={() => toggleSelectedMessage(message.id)}
|
||||
<ChatSelectionContextMenu
|
||||
count={selectedMessageIds.length}
|
||||
canPin={selectedMessageIds.length === 1}
|
||||
pinned={getSelectedMessages()[0]?.isPinned}
|
||||
onReply={handleSelectionReply}
|
||||
onCopy={() => void handleBulkCopy()}
|
||||
onForward={() => setForwardDialogOpen(true)}
|
||||
onDelete={() => void handleBulkDelete()}
|
||||
onTogglePin={handleSelectionPin}
|
||||
onCancel={exitSelectionMode}
|
||||
>
|
||||
{bubble}
|
||||
</SelectableMessageWrapper>
|
||||
<SelectableMessageWrapper
|
||||
selected={isSelected}
|
||||
selectionMode
|
||||
align={mine ? 'right' : 'left'}
|
||||
onClick={() => toggleSelectedMessage(message.id)}
|
||||
>
|
||||
{bubble}
|
||||
</SelectableMessageWrapper>
|
||||
</ChatSelectionContextMenu>
|
||||
) : (
|
||||
<ChatMessageContextMenu
|
||||
message={message}
|
||||
@@ -1675,6 +1728,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
setSelectedMessageIds([message.id]);
|
||||
setForwardDialogOpen(true);
|
||||
}}
|
||||
onTogglePin={() => void handleTogglePin(message)}
|
||||
onSelect={() => enterSelectionMode(message.id)}
|
||||
onToggleReaction={(emoji) => void handleToggleReaction(message.id, emoji)}
|
||||
onCopy={() => showToast('Текст скопирован')}
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
FamilyGroup,
|
||||
FamilyInviteCandidate,
|
||||
ManagedBot,
|
||||
addBotFatherToFamily,
|
||||
addBotToFamily,
|
||||
fetchMyBots,
|
||||
getApiErrorMessage,
|
||||
searchFamilyInviteUsers,
|
||||
@@ -42,7 +40,6 @@ export function FamilyInviteDialog({
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [myBots, setMyBots] = useState<ManagedBot[]>([]);
|
||||
const [myBotsLoading, setMyBotsLoading] = useState(false);
|
||||
const [addingBotId, setAddingBotId] = useState<string | null>(null);
|
||||
|
||||
const familyBotUsernames = useMemo(
|
||||
() => new Set((group?.members ?? []).map((member) => member.botUsername).filter(Boolean) as string[]),
|
||||
@@ -89,35 +86,14 @@ export function FamilyInviteDialog({
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [groupId, inviteQuery, open, token]);
|
||||
|
||||
async function addBotById(botId: string, displayName: string) {
|
||||
if (!token) return;
|
||||
setAddingBotId(botId);
|
||||
try {
|
||||
await addBotToFamily(groupId, botId, token);
|
||||
showToast(`${displayName} добавлен в семью`);
|
||||
onOpenChange(false);
|
||||
onInvited?.();
|
||||
} catch (error) {
|
||||
showToast(getApiErrorMessage(error, 'Не удалось добавить бота') ?? 'Ошибка');
|
||||
} finally {
|
||||
setAddingBotId(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function submitInvite() {
|
||||
if (!token || !selectedInviteUser) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await sendFamilyInvite(groupId, { inviteeUserId: selectedInviteUser.id }, token);
|
||||
if (selectedInviteUser.isBot) {
|
||||
if (selectedInviteUser.botId) {
|
||||
await addBotToFamily(groupId, selectedInviteUser.botId, token);
|
||||
showToast(`${selectedInviteUser.displayName} добавлен в семью`);
|
||||
} else {
|
||||
await addBotFatherToFamily(groupId, token);
|
||||
showToast('BotFather добавлен в семью');
|
||||
}
|
||||
showToast(`${selectedInviteUser.displayName} добавлен в семью`);
|
||||
} else {
|
||||
await sendFamilyInvite(groupId, { inviteeUserId: selectedInviteUser.id }, token);
|
||||
showToast('Приглашение отправлено');
|
||||
}
|
||||
onOpenChange(false);
|
||||
@@ -145,13 +121,15 @@ export function FamilyInviteDialog({
|
||||
if (candidate.botUsername === 'BotFather_bot') {
|
||||
return `${handle} — создавайте ботов через чат`;
|
||||
}
|
||||
if (candidate.botUsername === 'GetMyIdBot_bot') {
|
||||
return `${handle} — получите user_id и chat_id`;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
function submitButtonLabel() {
|
||||
if (!selectedInviteUser?.isBot) return 'Отправить приглашение';
|
||||
if (selectedInviteUser.botId) return 'Добавить бота';
|
||||
return 'Добавить BotFather';
|
||||
return 'Пригласить бота';
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -192,16 +170,15 @@ export function FamilyInviteDialog({
|
||||
<button
|
||||
key={bot.id}
|
||||
type="button"
|
||||
disabled={addingBotId === bot.id}
|
||||
className="flex w-full items-center gap-3 border-b border-[#eceef4] px-4 py-3 text-left last:border-b-0 hover:bg-[#fafbfd] disabled:opacity-60"
|
||||
onClick={() => void addBotById(bot.id, bot.name)}
|
||||
className="flex w-full items-center gap-3 border-b border-[#eceef4] px-4 py-3 text-left last:border-b-0 hover:bg-[#fafbfd]"
|
||||
onClick={() => setInviteQuery(`@${bot.username}`)}
|
||||
>
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-[#eef4ff] text-[#3390ec]">
|
||||
{addingBotId === bot.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Bot className="h-4 w-4" />}
|
||||
<Bot className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium">{bot.name}</p>
|
||||
<p className="truncate text-sm text-[#667085]">@{bot.username}</p>
|
||||
<p className="truncate text-sm text-[#667085]">@{bot.username} · выбрать через приглашение</p>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
@@ -211,7 +188,7 @@ export function FamilyInviteDialog({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-[#667085]">Или найдите пользователя / бота другого владельца через поиск выше</p>
|
||||
<p className="mt-2 text-xs text-[#667085]">Выберите своего бота, затем подтвердите приглашение в результатах поиска</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-2 max-h-56 overflow-y-auto rounded-2xl border border-[#eceef4]">
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { Loader2, Plus, Search, UsersRound } from 'lucide-react';
|
||||
import { Loader2, 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';
|
||||
import { useFamilyOverlay } from '@/components/family/family-overlay-provider';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import { useSelectedFamily } from '@/hooks/use-primary-family';
|
||||
import { addBotFatherToFamily, getApiErrorMessage } from '@/lib/api';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/components/id/toast-provider';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export function FamilySidebarPanel() {
|
||||
@@ -27,9 +25,7 @@ export function FamilySidebarPanel() {
|
||||
refresh
|
||||
} = useSelectedFamily(Boolean(user && token));
|
||||
const { openChatWithMember } = useFamilyOverlay();
|
||||
const { showToast } = useToast();
|
||||
const [inviteOpen, setInviteOpen] = useState(false);
|
||||
const [addingBotFather, setAddingBotFather] = useState(false);
|
||||
|
||||
if (!user || !token) return null;
|
||||
|
||||
@@ -118,32 +114,6 @@ export function FamilySidebarPanel() {
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{group?.botFatherAvailable ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={addingBotFather}
|
||||
className="flex w-full items-center gap-2 rounded-xl border border-dashed border-[#d5dbe8] px-2 py-2 text-left transition hover:bg-[#f4f5f8] disabled:opacity-60"
|
||||
onClick={() => {
|
||||
if (!group || !token) return;
|
||||
setAddingBotFather(true);
|
||||
void addBotFatherToFamily(group.id, token)
|
||||
.then(() => {
|
||||
showToast('BotFather добавлен в семью');
|
||||
void refresh();
|
||||
})
|
||||
.catch((error) => showToast(getApiErrorMessage(error, 'Не удалось добавить BotFather') ?? 'Ошибка'))
|
||||
.finally(() => setAddingBotFather(false));
|
||||
}}
|
||||
>
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-[#eef4ff] text-[#3390ec]">
|
||||
{addingBotFather ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-[12px] font-medium leading-tight">Добавить BotFather 🤖</p>
|
||||
<p className="truncate text-[10px] text-[#667085]">Создавайте ботов через чат</p>
|
||||
</div>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-xl bg-[#f4f5f8] px-2 py-3 text-[11px] text-[#667085]">
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft, BarChart3, CalendarDays, ImageIcon, Loader2, MessageCircle, Paperclip, Search, Send, Settings, Smile, Trash2, X } from 'lucide-react';
|
||||
import { ArrowLeft, BarChart3, CalendarDays, ImageIcon, Loader2, MessageCircle, Paperclip, Pin, 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, SelectableMessageWrapper } from '@/components/chat/chat-message-context-menu';
|
||||
import { ChatMessageContextMenu, ChatSelectionContextMenu, SelectableMessageWrapper } from '@/components/chat/chat-message-context-menu';
|
||||
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';
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
markChatRoomRead,
|
||||
sendBotMessage,
|
||||
sendChatMessage,
|
||||
setChatMessagePinned,
|
||||
toggleChatMessageReaction,
|
||||
uploadMediaObject,
|
||||
voteChatPoll
|
||||
@@ -516,6 +517,35 @@ export function MiniFamilyChat() {
|
||||
requestDeleteMessages(selected.map((message) => message.id));
|
||||
}
|
||||
|
||||
async function handleTogglePin(message: ChatMessage) {
|
||||
if (!token) return;
|
||||
try {
|
||||
const updated = await setChatMessagePinned(message.id, !message.isPinned, token);
|
||||
setMessages((current) => current.map((item) => (item.id === updated.id ? updated : item)));
|
||||
showToast(updated.isPinned ? 'Сообщение закреплено' : 'Сообщение откреплено');
|
||||
} catch (error) {
|
||||
showToast(getApiErrorMessage(error, 'Не удалось изменить закрепление') ?? 'Ошибка');
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectionReply() {
|
||||
const selected = getSelectedMessages();
|
||||
if (selected.length !== 1) {
|
||||
showToast('Ответить можно только на одно сообщение');
|
||||
return;
|
||||
}
|
||||
showToast('Ответ доступен в полном виде семьи');
|
||||
}
|
||||
|
||||
function handleSelectionPin() {
|
||||
const selected = getSelectedMessages();
|
||||
if (selected.length !== 1) {
|
||||
showToast('Закрепить можно только одно сообщение');
|
||||
return;
|
||||
}
|
||||
void handleTogglePin(selected[0]!);
|
||||
}
|
||||
|
||||
async function uploadChatFile(file: File) {
|
||||
if (!token || !activeRoom || sending) return;
|
||||
setSending(true);
|
||||
@@ -948,6 +978,12 @@ export function MiniFamilyChat() {
|
||||
)
|
||||
)}
|
||||
>
|
||||
{message.isPinned && !isLargeEmoji ? (
|
||||
<div className={cn('mb-1 flex items-center gap-1 text-[10px] font-medium', mine ? 'text-[#6c8f56]' : 'text-[#3390ec]')}>
|
||||
<Pin className="h-3 w-3" />
|
||||
Закреплено
|
||||
</div>
|
||||
) : null}
|
||||
{!mine && !isLargeEmoji ? (
|
||||
<p className="mb-0.5 text-[11px] font-semibold text-[#3390ec]">{message.senderName}</p>
|
||||
) : null}
|
||||
@@ -1045,14 +1081,26 @@ export function MiniFamilyChat() {
|
||||
/>
|
||||
) : null}
|
||||
{selectionMode ? (
|
||||
<SelectableMessageWrapper
|
||||
selected={isSelected}
|
||||
selectionMode
|
||||
align={mine ? 'right' : 'left'}
|
||||
onClick={() => toggleSelectedMessage(message.id)}
|
||||
<ChatSelectionContextMenu
|
||||
count={selectedMessageIds.length}
|
||||
canPin={selectedMessageIds.length === 1}
|
||||
pinned={getSelectedMessages()[0]?.isPinned}
|
||||
onReply={handleSelectionReply}
|
||||
onCopy={() => void handleBulkCopy()}
|
||||
onForward={() => showToast('Пересылка доступна в полном виде семьи')}
|
||||
onDelete={() => void handleBulkDelete()}
|
||||
onTogglePin={handleSelectionPin}
|
||||
onCancel={exitSelectionMode}
|
||||
>
|
||||
{bubble}
|
||||
</SelectableMessageWrapper>
|
||||
<SelectableMessageWrapper
|
||||
selected={isSelected}
|
||||
selectionMode
|
||||
align={mine ? 'right' : 'left'}
|
||||
onClick={() => toggleSelectedMessage(message.id)}
|
||||
>
|
||||
{bubble}
|
||||
</SelectableMessageWrapper>
|
||||
</ChatSelectionContextMenu>
|
||||
) : (
|
||||
<ChatMessageContextMenu
|
||||
message={message}
|
||||
@@ -1065,6 +1113,7 @@ export function MiniFamilyChat() {
|
||||
}}
|
||||
onDelete={() => requestDeleteMessages([message.id])}
|
||||
onForward={() => showToast('Пересылка доступна в полном виде семьи')}
|
||||
onTogglePin={() => void handleTogglePin(message)}
|
||||
onSelect={() => enterSelectionMode(message.id)}
|
||||
onToggleReaction={async (emoji) => {
|
||||
if (!token) return;
|
||||
|
||||
Reference in New Issue
Block a user