update and fix messanger
This commit is contained in:
@@ -5,14 +5,17 @@ import { useRouter } from 'next/navigation';
|
||||
import {
|
||||
ArrowLeft,
|
||||
BarChart3,
|
||||
CalendarDays,
|
||||
Camera,
|
||||
Check,
|
||||
CheckCheck,
|
||||
FileText,
|
||||
ImageIcon,
|
||||
Loader2,
|
||||
Mic,
|
||||
Paperclip,
|
||||
Plus,
|
||||
Search,
|
||||
Send,
|
||||
Settings,
|
||||
Smile,
|
||||
@@ -30,6 +33,8 @@ import { BotMiniAppSheet, FamilyBotChat } from '@/components/family/family-bot-c
|
||||
import { ChatRoomAvatarDisplay } from '@/components/family/chat-room-avatar-display';
|
||||
import { ChatEmojiContent } from '@/components/chat/chat-emoji-content';
|
||||
import { ChatForwardDialog } from '@/components/chat/chat-forward-dialog';
|
||||
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';
|
||||
@@ -89,6 +94,9 @@ import { getE2EMessageText, useE2EChat } from '@/hooks/use-e2e-chat';
|
||||
import { useChatMessageSelection } from '@/hooks/use-chat-message-selection';
|
||||
import { useChatDragSelection } from '@/hooks/use-chat-drag-selection';
|
||||
import { useChatTyping } from '@/hooks/use-chat-typing';
|
||||
import { shouldShowChatDateSeparator } from '@/lib/chat-date-utils';
|
||||
import { AVATAR_UPDATED_EVENT, dispatchChatRoomAvatarUpdated } from '@/lib/avatar-events';
|
||||
import { getSingleEmojiNative, isSingleEmojiMessage } from '@/lib/chat-emoji-display';
|
||||
import { findMemberRoom, readStoredActiveRoomId, roomDisplayLabel, storeActiveRoomId } from '@/lib/family-chat';
|
||||
import {
|
||||
getChatListPreviewText,
|
||||
@@ -243,6 +251,8 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
const [forwarding, setForwarding] = useState(false);
|
||||
const [bulkDeleting, setBulkDeleting] = useState(false);
|
||||
const [replyToMessage, setReplyToMessage] = useState<ChatMessage | null>(null);
|
||||
const [chatToolsOpen, setChatToolsOpen] = useState(false);
|
||||
const [chatToolsTab, setChatToolsTab] = useState<ChatToolsTab>('search');
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const readReceiptTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -279,7 +289,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
addSelectedMessage,
|
||||
handleSelectionPointer
|
||||
} = useChatMessageSelection(visibleMessages);
|
||||
const { startDragSelection, handleDragEnterMessage, isDragging } = useChatDragSelection({
|
||||
const { handleMessagePointerDown, handleDragEnterMessage, isDragging } = useChatDragSelection({
|
||||
selectionMode,
|
||||
enterSelectionMode,
|
||||
addSelectedMessage
|
||||
@@ -377,6 +387,14 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
void loadPresence();
|
||||
}, [isPinLocked, isReady, loadGroup, loadPresence, showToast, token]);
|
||||
|
||||
useEffect(() => {
|
||||
function handleAvatarUpdated() {
|
||||
void loadGroup();
|
||||
}
|
||||
window.addEventListener(AVATAR_UPDATED_EVENT, handleAvatarUpdated);
|
||||
return () => window.removeEventListener(AVATAR_UPDATED_EVENT, handleAvatarUpdated);
|
||||
}, [loadGroup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token || isPinLocked) return;
|
||||
void loadPresence();
|
||||
@@ -640,6 +658,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
body: JSON.stringify({ storageKey: presigned.storageKey })
|
||||
}, token);
|
||||
await loadGroup();
|
||||
dispatchChatRoomAvatarUpdated(activeRoomId);
|
||||
showToast('Фото чата обновлено');
|
||||
} catch (error) {
|
||||
showToast(getApiErrorMessage(error, 'Не удалось загрузить фото чата') ?? 'Ошибка');
|
||||
@@ -1300,6 +1319,46 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{!activeRoomIsBot ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-xl"
|
||||
aria-label="Поиск по сообщениям"
|
||||
onClick={() => {
|
||||
setChatToolsTab('search');
|
||||
setChatToolsOpen(true);
|
||||
}}
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-xl"
|
||||
aria-label="Перейти к дате"
|
||||
onClick={() => {
|
||||
setChatToolsTab('calendar');
|
||||
setChatToolsOpen(true);
|
||||
}}
|
||||
>
|
||||
<CalendarDays className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-xl"
|
||||
aria-label="Медиа чата"
|
||||
onClick={() => {
|
||||
setChatToolsTab('media');
|
||||
setChatToolsOpen(true);
|
||||
}}
|
||||
>
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
{canManageActiveBot ? (
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -1349,13 +1408,19 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
) : (
|
||||
<>
|
||||
<div className={cn('flex-1 space-y-2 overflow-y-auto px-4 py-4', isDragging && 'select-none')}>
|
||||
{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 px-2 py-1">
|
||||
<p className="rounded-full bg-black/[0.06] px-3 py-1.5 text-xs text-[#667085]">
|
||||
{getMessagePreviewText(message)}
|
||||
</p>
|
||||
<div key={message.id}>
|
||||
{showDateSeparator ? <ChatDateSeparator date={message.createdAt} /> : null}
|
||||
<div id={`msg-${message.id}`} className="flex justify-center px-2 py-1">
|
||||
<p className="rounded-full bg-black/[0.06] px-3 py-1.5 text-xs text-[#667085]">
|
||||
{getMessagePreviewText(message)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1387,12 +1452,25 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
})
|
||||
: '';
|
||||
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(
|
||||
'relative rounded-[18px] px-3 py-2 shadow-sm',
|
||||
mine ? 'rounded-br-md bg-[#effdde] text-[#1f2430]' : 'rounded-bl-md bg-white text-[#1f2430]',
|
||||
!selectionMode && 'max-w-[78%]'
|
||||
'relative',
|
||||
isLargeEmoji
|
||||
? 'bg-transparent px-0 py-0 shadow-none'
|
||||
: cn(
|
||||
'rounded-[18px] px-3 py-2 shadow-sm',
|
||||
mine ? 'rounded-br-md bg-[#effdde] text-[#1f2430]' : 'rounded-bl-md bg-white text-[#1f2430]',
|
||||
!selectionMode && 'max-w-[78%]'
|
||||
)
|
||||
)}
|
||||
>
|
||||
{!mine ? <p className="mb-1 text-xs font-semibold text-[#3390ec]">{message.senderName}</p> : null}
|
||||
@@ -1483,6 +1561,21 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
</button>
|
||||
);
|
||||
})()
|
||||
) : isLargeEmoji ? (
|
||||
<div className="relative inline-flex min-w-[72px] flex-col items-center">
|
||||
<span className="text-[56px] leading-none">{largeEmojiNative}</span>
|
||||
<div className="absolute bottom-0 right-0 flex items-center gap-0.5 rounded-full bg-black/45 px-1.5 py-0.5 text-[10px] text-white">
|
||||
{message.editedAt ? <span>изм.</span> : null}
|
||||
<span>{formatMessageTime(message.createdAt)}</span>
|
||||
{mine ? (
|
||||
message.readByAll ? (
|
||||
<CheckCheck className="h-3 w-3" aria-label="Прочитано" />
|
||||
) : (
|
||||
<Check className="h-3 w-3" aria-label="Отправлено" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : emojiContent ? (
|
||||
<div className="py-1">
|
||||
<ChatEmojiContent content={emojiContent} size={32} />
|
||||
@@ -1492,32 +1585,37 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
<ChatEmojiContent content={visibleText} size={22} />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn('mt-1 flex items-center justify-end gap-1 text-[11px]', mine ? 'text-[#7ea06a]' : 'text-[#a8adbc]')}>
|
||||
{message.editedAt ? <span className="mr-1">изменено</span> : null}
|
||||
<span>{formatMessageTime(message.createdAt)}</span>
|
||||
{mine ? (
|
||||
message.readByAll ? (
|
||||
<CheckCheck className="h-3.5 w-3.5 text-[#3390ec]" aria-label="Прочитано" />
|
||||
) : (
|
||||
<Check className="h-3.5 w-3.5" aria-label="Отправлено" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
<ChatMessageReactions
|
||||
reactions={message.reactions}
|
||||
onToggle={(emoji) => void handleToggleReaction(message.id, emoji)}
|
||||
/>
|
||||
{!isLargeEmoji ? (
|
||||
<div className={cn('mt-1 flex items-center justify-end gap-1 text-[11px]', mine ? 'text-[#7ea06a]' : 'text-[#a8adbc]')}>
|
||||
{message.editedAt ? <span className="mr-1">изменено</span> : null}
|
||||
<span>{formatMessageTime(message.createdAt)}</span>
|
||||
{mine ? (
|
||||
message.readByAll ? (
|
||||
<CheckCheck className="h-3.5 w-3.5 text-[#3390ec]" aria-label="Прочитано" />
|
||||
) : (
|
||||
<Check className="h-3.5 w-3.5" aria-label="Отправлено" />
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{!isLargeEmoji ? (
|
||||
<ChatMessageReactions
|
||||
reactions={message.reactions}
|
||||
onToggle={(emoji) => void handleToggleReaction(message.id, emoji)}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={message.id}>
|
||||
{showDateSeparator ? <ChatDateSeparator date={message.createdAt} /> : null}
|
||||
<div
|
||||
key={message.id}
|
||||
id={`msg-${message.id}`}
|
||||
className={cn('group flex gap-2', mine ? 'justify-end' : 'justify-start', isDragging && 'select-none')}
|
||||
onMouseDown={(event) => {
|
||||
if (event.button !== 0 || isChatInteractiveTarget(event.target)) return;
|
||||
startDragSelection(message.id);
|
||||
if (isChatInteractiveTarget(event.target)) return;
|
||||
handleMessagePointerDown(message.id, event);
|
||||
}}
|
||||
onMouseEnter={() => handleDragEnterMessage(message.id)}
|
||||
onClick={(event) => {
|
||||
@@ -1535,7 +1633,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
token={token}
|
||||
isVerified={message.senderIsVerified}
|
||||
verificationIcon={message.senderVerificationIcon}
|
||||
className="h-8 w-8 self-end"
|
||||
className="z-10 -mr-3 -mt-0.5 h-8 w-8 shrink-0 self-start ring-2 ring-[#eef1f6]"
|
||||
badgeSize="xs"
|
||||
/>
|
||||
) : null}
|
||||
@@ -1556,7 +1654,13 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
visibleText={visibleText}
|
||||
onEdit={() => void startEditMessage(message)}
|
||||
onDelete={() => requestDeleteMessages([message.id])}
|
||||
onReply={() => setReplyToMessage(message)}
|
||||
onReply={() => {
|
||||
exitSelectionMode();
|
||||
setReplyToMessage(message);
|
||||
setEditingMessageId(null);
|
||||
setDraft('');
|
||||
setEmojiOpen(false);
|
||||
}}
|
||||
onForward={() => {
|
||||
setSelectedMessageIds([message.id]);
|
||||
setForwardDialogOpen(true);
|
||||
@@ -1568,6 +1672,19 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
{bubble}
|
||||
</ChatMessageContextMenu>
|
||||
)}
|
||||
{mine ? (
|
||||
<UserAvatar
|
||||
userId={user?.id ?? message.senderId}
|
||||
displayName={user?.displayName ?? message.senderName}
|
||||
hasAvatar={Boolean(user?.hasAvatar)}
|
||||
token={token}
|
||||
isVerified={user?.isVerified}
|
||||
verificationIcon={user?.verificationIcon}
|
||||
className="z-10 -ml-3 -mt-0.5 h-8 w-8 shrink-0 self-start ring-2 ring-[#eef1f6]"
|
||||
badgeSize="xs"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -1705,6 +1822,28 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
||||
onSelectRoom={(roomId) => void handleForwardToRoom(roomId)}
|
||||
/>
|
||||
|
||||
<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]
|
||||
})
|
||||
)
|
||||
}
|
||||
getMediaUrl={(message) => (message.storageKey ? mediaUrls[message.storageKey]?.blobUrl ?? null : null)}
|
||||
/>
|
||||
|
||||
<ChatDeleteMessageDialog
|
||||
open={deleteMessagesDialogOpen}
|
||||
count={pendingDeleteMessageIds.length}
|
||||
|
||||
Reference in New Issue
Block a user