feat(desktop): pin/unpin from message context menu + side panel
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user