update and fix messanger
This commit is contained in:
@@ -2,19 +2,23 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeft, BarChart3, Loader2, MessageCircle, Paperclip, 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, 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';
|
||||
import { ChatSelectionToolbar } from '@/components/chat/chat-selection-toolbar';
|
||||
import { ChatToolsDialog, type ChatToolsTab } from '@/components/chat/chat-tools-dialog';
|
||||
import { EmojiPicker } from '@/components/chat/emoji-picker';
|
||||
import { scrollToChatMessage } from '@/components/chat/replied-message-preview';
|
||||
import { useFamilyOverlay } from '@/components/family/family-overlay-provider';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import { UserAvatar } from '@/components/id/user-avatar';
|
||||
import { useToast } from '@/components/id/toast-provider';
|
||||
import { useRealtime } from '@/components/notifications/realtime-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -52,6 +56,8 @@ import {
|
||||
isSystemMessage
|
||||
} from '@/lib/chat-message-utils';
|
||||
import { detectChatMessageType, parseMessageMetadata, resolveChatContentType } from '@/lib/chat-media';
|
||||
import { getSingleEmojiNative, isSingleEmojiMessage } from '@/lib/chat-emoji-display';
|
||||
import { shouldShowChatDateSeparator } from '@/lib/chat-date-utils';
|
||||
import { encryptE2EBlob, e2eKindFromMessageType } from '@/lib/e2e-chat';
|
||||
import { findMemberRoom, findOrCreateMemberChat, roomDisplayLabel } from '@/lib/family-chat';
|
||||
import { getE2EMessageText, useE2EChat } from '@/hooks/use-e2e-chat';
|
||||
@@ -112,6 +118,8 @@ export function MiniFamilyChat() {
|
||||
const [pollOpen, setPollOpen] = useState(false);
|
||||
const [pollQuestion, setPollQuestion] = useState('');
|
||||
const [pollOptions, setPollOptions] = useState(['', '']);
|
||||
const [chatToolsOpen, setChatToolsOpen] = useState(false);
|
||||
const [chatToolsTab, setChatToolsTab] = useState<ChatToolsTab>('search');
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const previousGroupIdRef = useRef<string | null>(null);
|
||||
@@ -131,7 +139,7 @@ export function MiniFamilyChat() {
|
||||
handleSelectionPointer
|
||||
} = useChatMessageSelection(visibleMessages);
|
||||
|
||||
const { startDragSelection, handleDragEnterMessage, isDragging } = useChatDragSelection({
|
||||
const { handleMessagePointerDown, handleDragEnterMessage, isDragging } = useChatDragSelection({
|
||||
selectionMode,
|
||||
enterSelectionMode,
|
||||
addSelectedMessage
|
||||
@@ -684,6 +692,49 @@ export function MiniFamilyChat() {
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
{view === 'chat' && activeRoom ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0"
|
||||
aria-label="Поиск по сообщениям"
|
||||
onClick={() => {
|
||||
setChatToolsTab('search');
|
||||
setChatToolsOpen(true);
|
||||
}}
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0"
|
||||
aria-label="Перейти к дате"
|
||||
onClick={() => {
|
||||
setChatToolsTab('calendar');
|
||||
setChatToolsOpen(true);
|
||||
}}
|
||||
>
|
||||
<CalendarDays className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0"
|
||||
aria-label="Медиа чата"
|
||||
onClick={() => {
|
||||
setChatToolsTab('media');
|
||||
setChatToolsOpen(true);
|
||||
}}
|
||||
>
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{view === 'chat' && activeRoom && activeRoom.type !== 'GENERAL' ? (
|
||||
<Button
|
||||
type="button"
|
||||
@@ -840,13 +891,19 @@ export function MiniFamilyChat() {
|
||||
Загрузка...
|
||||
</div>
|
||||
) : visibleMessages.length ? (
|
||||
visibleMessages.map((message) => {
|
||||
visibleMessages.map((message, messageIndex) => {
|
||||
const previousMessage = visibleMessages[messageIndex - 1];
|
||||
const showDateSeparator = shouldShowChatDateSeparator(message.createdAt, previousMessage?.createdAt);
|
||||
|
||||
if (isSystemMessage(message)) {
|
||||
return (
|
||||
<div key={message.id} id={`msg-${message.id}`} className="flex justify-center py-1">
|
||||
<p className="rounded-full bg-black/[0.06] px-3 py-1 text-[11px] text-[#667085]">
|
||||
{getMessagePreviewText(message)}
|
||||
</p>
|
||||
<div key={message.id}>
|
||||
{showDateSeparator ? <ChatDateSeparator date={message.createdAt} /> : null}
|
||||
<div id={`msg-${message.id}`} className="flex justify-center py-1">
|
||||
<p className="rounded-full bg-black/[0.06] px-3 py-1 text-[11px] text-[#667085]">
|
||||
{getMessagePreviewText(message)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -866,16 +923,31 @@ export function MiniFamilyChat() {
|
||||
? e2ePayload?.emojiId ?? message.content
|
||||
: null;
|
||||
const isSelected = selectedMessageIds.includes(message.id);
|
||||
const isLargeEmoji = isSingleEmojiMessage({
|
||||
type: message.type,
|
||||
content: emojiContent ?? message.content,
|
||||
visibleText
|
||||
});
|
||||
const largeEmojiNative = isLargeEmoji
|
||||
? getSingleEmojiNative({ type: message.type, content: emojiContent ?? message.content, visibleText })
|
||||
: '';
|
||||
|
||||
const bubble = (
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-[85%] rounded-[16px] px-3 py-2 text-sm shadow-sm',
|
||||
mine ? 'bg-[#effdde]' : 'bg-white',
|
||||
!selectionMode && 'max-w-[85%]'
|
||||
'relative',
|
||||
isLargeEmoji
|
||||
? 'bg-transparent px-0 py-0 shadow-none'
|
||||
: cn(
|
||||
'max-w-[85%] rounded-[16px] px-3 py-2 text-sm shadow-sm',
|
||||
mine ? 'bg-[#effdde]' : 'bg-white',
|
||||
!selectionMode && 'max-w-[85%]'
|
||||
)
|
||||
)}
|
||||
>
|
||||
{!mine ? <p className="mb-0.5 text-[11px] font-semibold text-[#3390ec]">{message.senderName}</p> : null}
|
||||
{!mine && !isLargeEmoji ? (
|
||||
<p className="mb-0.5 text-[11px] font-semibold text-[#3390ec]">{message.senderName}</p>
|
||||
) : null}
|
||||
{message.type === 'POLL' && message.poll ? (
|
||||
<div className="space-y-1.5">
|
||||
<p className="font-medium">{message.poll.question}</p>
|
||||
@@ -905,6 +977,13 @@ export function MiniFamilyChat() {
|
||||
</div>
|
||||
) : message.storageKey ? (
|
||||
<p className="text-sm">{getMessagePreviewText(message, visibleText)}</p>
|
||||
) : isLargeEmoji ? (
|
||||
<div className="relative inline-flex min-w-[64px] flex-col items-center">
|
||||
<span className="text-[48px] leading-none">{largeEmojiNative}</span>
|
||||
<div className="absolute bottom-0 right-0 rounded-full bg-black/45 px-1.5 py-0.5 text-[10px] text-white">
|
||||
{formatMessageTime(message.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
) : emojiContent ? (
|
||||
<div className="py-0.5">
|
||||
<ChatEmojiContent content={emojiContent} size={28} />
|
||||
@@ -914,69 +993,99 @@ export function MiniFamilyChat() {
|
||||
<ChatEmojiContent content={visibleText} size={18} />
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-1 text-[10px] text-[#a8adbc]">{formatMessageTime(message.createdAt)}</p>
|
||||
<ChatMessageReactions
|
||||
reactions={message.reactions}
|
||||
onToggle={(emoji) => {
|
||||
if (!token) return;
|
||||
void toggleChatMessageReaction(message.id, emoji, token).then((updated) => {
|
||||
setMessages((current) => current.map((item) => (item.id === updated.id ? updated : item)));
|
||||
});
|
||||
}}
|
||||
compact
|
||||
/>
|
||||
{!isLargeEmoji ? (
|
||||
<p className="mt-1 text-[10px] text-[#a8adbc]">{formatMessageTime(message.createdAt)}</p>
|
||||
) : null}
|
||||
{!isLargeEmoji ? (
|
||||
<ChatMessageReactions
|
||||
reactions={message.reactions}
|
||||
onToggle={(emoji) => {
|
||||
if (!token) return;
|
||||
void toggleChatMessageReaction(message.id, emoji, token).then((updated) => {
|
||||
setMessages((current) => current.map((item) => (item.id === updated.id ? updated : item)));
|
||||
});
|
||||
}}
|
||||
compact
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={message.id}
|
||||
id={`msg-${message.id}`}
|
||||
className={cn('flex', mine ? 'justify-end' : 'justify-start', isDragging && 'select-none')}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button !== 0 || isChatInteractiveTarget(event.target)) return;
|
||||
startDragSelection(message.id);
|
||||
}}
|
||||
onMouseEnter={() => handleDragEnterMessage(message.id)}
|
||||
onClick={(event) => {
|
||||
if (selectionMode) return;
|
||||
if (event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||
handleSelectionPointer(message.id, event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectionMode ? (
|
||||
<SelectableMessageWrapper
|
||||
selected={isSelected}
|
||||
selectionMode
|
||||
align={mine ? 'right' : 'left'}
|
||||
onClick={() => toggleSelectedMessage(message.id)}
|
||||
>
|
||||
{bubble}
|
||||
</SelectableMessageWrapper>
|
||||
) : (
|
||||
<ChatMessageContextMenu
|
||||
message={message}
|
||||
userId={user.id}
|
||||
isE2E={activeRoomIsE2E}
|
||||
visibleText={visibleText}
|
||||
onEdit={() => {
|
||||
setEditingMessageId(message.id);
|
||||
setDraft(message.content ?? '');
|
||||
}}
|
||||
onDelete={() => requestDeleteMessages([message.id])}
|
||||
onForward={() => showToast('Пересылка доступна в полном виде семьи')}
|
||||
onSelect={() => enterSelectionMode(message.id)}
|
||||
onToggleReaction={async (emoji) => {
|
||||
if (!token) return;
|
||||
const updated = await toggleChatMessageReaction(message.id, emoji, token);
|
||||
setMessages((current) => current.map((item) => (item.id === updated.id ? updated : item)));
|
||||
}}
|
||||
onCopy={() => showToast('Текст скопирован')}
|
||||
>
|
||||
{bubble}
|
||||
</ChatMessageContextMenu>
|
||||
)}
|
||||
<div key={message.id}>
|
||||
{showDateSeparator ? <ChatDateSeparator date={message.createdAt} /> : null}
|
||||
<div
|
||||
id={`msg-${message.id}`}
|
||||
className={cn('group flex gap-1.5', mine ? 'justify-end' : 'justify-start', isDragging && 'select-none')}
|
||||
onMouseDown={(event) => {
|
||||
if (isChatInteractiveTarget(event.target)) return;
|
||||
handleMessagePointerDown(message.id, event);
|
||||
}}
|
||||
onMouseEnter={() => handleDragEnterMessage(message.id)}
|
||||
onClick={(event) => {
|
||||
if (selectionMode) return;
|
||||
if (event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||
handleSelectionPointer(message.id, event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!mine ? (
|
||||
<UserAvatar
|
||||
userId={message.senderId}
|
||||
displayName={message.senderName}
|
||||
hasAvatar={message.senderHasAvatar}
|
||||
token={token}
|
||||
isVerified={message.senderIsVerified}
|
||||
verificationIcon={message.senderVerificationIcon}
|
||||
className="z-10 -mr-2 -mt-0.5 h-7 w-7 shrink-0 self-start ring-2 ring-[#eef1f6]"
|
||||
badgeSize="xs"
|
||||
/>
|
||||
) : null}
|
||||
{selectionMode ? (
|
||||
<SelectableMessageWrapper
|
||||
selected={isSelected}
|
||||
selectionMode
|
||||
align={mine ? 'right' : 'left'}
|
||||
onClick={() => toggleSelectedMessage(message.id)}
|
||||
>
|
||||
{bubble}
|
||||
</SelectableMessageWrapper>
|
||||
) : (
|
||||
<ChatMessageContextMenu
|
||||
message={message}
|
||||
userId={user.id}
|
||||
isE2E={activeRoomIsE2E}
|
||||
visibleText={visibleText}
|
||||
onEdit={() => {
|
||||
setEditingMessageId(message.id);
|
||||
setDraft(message.content ?? '');
|
||||
}}
|
||||
onDelete={() => requestDeleteMessages([message.id])}
|
||||
onForward={() => showToast('Пересылка доступна в полном виде семьи')}
|
||||
onSelect={() => enterSelectionMode(message.id)}
|
||||
onToggleReaction={async (emoji) => {
|
||||
if (!token) return;
|
||||
const updated = await toggleChatMessageReaction(message.id, emoji, token);
|
||||
setMessages((current) => current.map((item) => (item.id === updated.id ? updated : item)));
|
||||
}}
|
||||
onCopy={() => showToast('Текст скопирован')}
|
||||
>
|
||||
{bubble}
|
||||
</ChatMessageContextMenu>
|
||||
)}
|
||||
{mine ? (
|
||||
<UserAvatar
|
||||
userId={user.id}
|
||||
displayName={user.displayName}
|
||||
hasAvatar={Boolean(user.hasAvatar)}
|
||||
token={token}
|
||||
isVerified={user.isVerified}
|
||||
verificationIcon={user.verificationIcon}
|
||||
className="z-10 -ml-2 -mt-0.5 h-7 w-7 shrink-0 self-start ring-2 ring-[#eef1f6]"
|
||||
badgeSize="xs"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
@@ -1072,6 +1181,26 @@ export function MiniFamilyChat() {
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
<ChatToolsDialog
|
||||
open={chatToolsOpen}
|
||||
onOpenChange={setChatToolsOpen}
|
||||
messages={visibleMessages}
|
||||
defaultTab={chatToolsTab}
|
||||
onJumpToMessage={scrollToChatMessage}
|
||||
getMessagePreview={(message) =>
|
||||
getMessagePreviewText(
|
||||
message,
|
||||
getE2EMessageText({
|
||||
message,
|
||||
isE2E: activeRoomIsE2E,
|
||||
peerE2eKey,
|
||||
peerKeyLoading,
|
||||
e2ePayload: message.isEncrypted ? e2ePayloads[message.id] : null,
|
||||
e2eError: e2eErrors[message.id]
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ChatDeleteMessageDialog
|
||||
open={deleteMessagesDialogOpen}
|
||||
count={pendingDeleteMessageIds.length}
|
||||
|
||||
Reference in New Issue
Block a user