global fix and add tauri app

This commit is contained in:
lendry
2026-06-26 13:56:54 +03:00
parent aa228d84eb
commit 3b05b7e4d4
50 changed files with 3947 additions and 80 deletions

View File

@@ -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<BotChatMeta | null>(null);
const [deletingRoomId, setDeletingRoomId] = useState<string | null>(null);
const [pinningRoomId, setPinningRoomId] = useState<string | null>(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<ChatToolsTab>('search');
const messagesEndRef = useRef<HTMLDivElement>(null);
const autoScrollStateRef = useRef<{ id: string | null; count: number }>({ id: null, count: 0 });
const readReceiptTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const typingEmitTimerRef = useRef<ReturnType<typeof setTimeout> | 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 }) {
<ChatRoomAvatarDisplay room={room} viewerUserId={user?.id ?? ''} token={token} />
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<p className="truncate font-medium">{user ? roomDisplayLabel(room, user.id) : room.name}</p>
<p className="flex min-w-0 items-center gap-1 truncate font-medium">
<span className="truncate">{user ? roomDisplayLabel(room, user.id) : room.name}</span>
{room.isPinned ? <Pin className="h-3.5 w-3.5 shrink-0 fill-[#8a8f9e] text-[#8a8f9e]" /> : null}
</p>
{room.lastMessage ? (
<span className="shrink-0 text-[11px] text-[#a8adbc]">{formatMessageTime(room.lastMessage.createdAt)}</span>
) : null}
@@ -1291,17 +1359,23 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
</p>
</div>
</button>
{room.type !== 'GENERAL' ? (
<button
type="button"
aria-label="Удалить чат"
disabled={deletingRoomId === room.id}
className="mr-2 flex h-8 w-8 shrink-0 self-center items-center justify-center rounded-lg text-[#667085] opacity-0 transition hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 disabled:opacity-50"
onClick={() => void handleDeleteRoom(room.id)}
>
{deletingRoomId === room.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
</button>
) : null}
<button
type="button"
aria-label={room.isPinned ? 'Открепить чат' : 'Закрепить чат'}
disabled={pinningRoomId === room.id}
className={cn(
'flex h-8 w-8 shrink-0 self-center items-center justify-center rounded-lg text-[#8a8f9e] transition hover:bg-[#eceef4] hover:text-[#667085]',
room.isPinned ? 'opacity-100' : 'opacity-0 group-hover:opacity-100',
pinningRoomId === room.id && 'opacity-100'
)}
onClick={() => void handleToggleRoomPin(room)}
>
{pinningRoomId === room.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Pin className={cn('h-4 w-4', room.isPinned && 'fill-[#8a8f9e]')} />
)}
</button>
</div>
))}
</div>
@@ -1452,6 +1526,17 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
/>
) : (
<>
{pinnedMessage ? (
<ChatPinnedMessageBanner
senderName={pinnedMessage.senderName}
preview={pinnedPreview}
onClick={() => {
if (!scrollToChatMessage(pinnedMessage.id)) {
showToast('Закреплённое сообщение пока не загружено в ленте');
}
}}
/>
) : null}
<div className={cn('flex-1 space-y-2 overflow-y-auto px-4 py-4', isDragging && 'select-none')}>
{visibleMessages.map((message, messageIndex) => {
const previousMessage = visibleMessages[messageIndex - 1];
@@ -1518,12 +1603,6 @@ 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>