feat(desktop): pin/unpin from message context menu + side panel

This commit is contained in:
byGalax
2026-05-16 17:51:01 +02:00
parent 6bff163f09
commit 3dbeabd268
2 changed files with 65 additions and 1 deletions
+38 -1
View File
@@ -1,4 +1,4 @@
import { parseMessagePayload } from '@chat-app/shared/chat';
import { parseMessagePayload, pinMessage, unpinMessage } from '@chat-app/shared/chat';
import { extractErrorCode } from '@chat-app/shared/i18n';
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -29,6 +29,7 @@ import { CallPreviewPanel } from '../components/CallPreviewPanel';
import { MediaFilesDrawer } from '../components/MediaFilesDrawer';
import { MentionAutocomplete } from '../components/MentionAutocomplete';
import { MessageBubble, type QuotedRef } from '../components/MessageBubble';
import { PinnedMessagesPanel } from '../components/PinnedMessagesPanel';
import { PollComposerDialog } from '../components/PollComposerDialog';
import { UserProfilePopover } from '../components/UserProfilePopover';
import { TypingIndicator } from '../components/TypingIndicator';
@@ -41,6 +42,7 @@ import { collectConversationAttachments, createPollPayload } from '../lib/conver
import { compressImages } from '../lib/imageCompress';
import { ensureInstallId } from '../lib/installId';
import { searchCachedMessages } from '../lib/messageCache';
import { supabase } from '../lib/supabase';
import type { OutboxItem } from '../lib/messageOutbox';
import { useConversationMessages } from '../lib/useConversationMessages';
import { useMessageReactions } from '../lib/useMessageReactions';
@@ -48,6 +50,7 @@ import { useGroupReceipts } from '../lib/useGroupReceipts';
import { markDelivered, useMessageDeliveries } from '../lib/useMessageDeliveries';
import { markRead as markMessagesReadRemote, useMessageReads } from '../lib/useMessageReads';
import { usePeerPresence } from '../lib/usePeerPresence';
import { usePinnedMessages } from '../lib/usePinnedMessages';
import { useTypingChannel } from '../lib/useTypingChannel';
const STICK_THRESHOLD = 80;
@@ -177,6 +180,25 @@ export function ConversationPage() {
} | null>(null);
const [mentionState, setMentionState] = useState<{ query: string; start: number } | null>(null);
const [emojiOpen, setEmojiOpen] = useState(false);
const pins = usePinnedMessages(id);
const [pinnedPanelOpen, setPinnedPanelOpen] = useState(false);
const pinnedIds = useMemo(() => new Set(pins.map((p) => p.messageId)), [pins]);
const handleTogglePin = useCallback(
async (messageId: string) => {
if (!id || !myId) return;
try {
if (pinnedIds.has(messageId)) {
await unpinMessage(supabase, id, messageId);
} else {
await pinMessage(supabase, id, messageId, myId);
}
} catch (err) {
console.warn('pin toggle failed', err);
}
},
[id, myId, pinnedIds],
);
const scrollRef = useRef<HTMLDivElement>(null);
const loadMoreSentinelRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -605,6 +627,8 @@ export function ConversationPage() {
: {})}
onSearchClick={() => setSearchOpen((v) => !v)}
{...(isGroup ? { onInfoClick: () => setInfoPanelOpen(true) } : {})}
pinnedCount={pins.length}
onOpenPinned={() => setPinnedPanelOpen(true)}
/>
)}
@@ -757,6 +781,8 @@ export function ConversationPage() {
});
}}
highlighted={highlightedId === m.id}
isPinned={pinnedIds.has(m.id)}
onTogglePin={handleTogglePin}
/>
</li>
);
@@ -1068,6 +1094,17 @@ export function ConversationPage() {
onClose={() => setProfilePopover(null)}
/>
)}
<PinnedMessagesPanel
open={pinnedPanelOpen}
pins={pins}
onClose={() => setPinnedPanelOpen(false)}
onJump={(_messageId) => {
// Future: scroll to message. For now just close the panel.
setPinnedPanelOpen(false);
}}
onUnpin={(messageId) => void handleTogglePin(messageId)}
/>
</div>
);
}