b1f37752d6
Audit of write-actions revealed that send (and edit via realtime UPDATE) are already optimistic via local state insertion in `useConversationMessages`, and friend nicknames are pure-local localStorage. Five user-write actions were waiting on the ~100-200 ms server roundtrip + realtime echo: * Toggle mute (`setConversationMutedUntil`) * Toggle mentions-only (`setConversationMentionsOnly`) * Toggle archive (`setConversationArchived`) * Pin / unpin message (`pinMessage` / `unpinMessage`) * Revoke device (`revokeDevice` RPC) All five now flip local state synchronously and roll back on failure. The existing realtime subscriptions reconcile canonically (no-op when the optimistic patch already matches the server row), so this is purely a UX latency improvement — no protocol or persistence changes. Reactions (`toggleReaction` / `voteExclusive`) were intentionally skipped this round: rollback semantics for the exclusive-vote path with multiple sequential awaits are messy enough to warrant a dedicated pass.
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
import { listPinnedMessages, type PinnedMessage } from '@chat-app/shared/chat';
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
import { supabase } from './supabase';
|
|
|
|
export interface UsePinnedMessagesResult {
|
|
pins: PinnedMessage[];
|
|
// Optimistic insert. Caller flips the UI immediately; server insert +
|
|
// realtime echo will reconcile (dedup'd by messageId). Returns the
|
|
// previous snapshot so the caller can roll back on error.
|
|
applyOptimisticPin: (messageId: string, pinnedBy: string) => PinnedMessage[];
|
|
applyOptimisticUnpin: (messageId: string) => PinnedMessage[];
|
|
// Hard restore for rollback after a failed server call.
|
|
restorePins: (snapshot: PinnedMessage[]) => void;
|
|
}
|
|
|
|
// Live list of pinned messages for one conversation. Subscribes to the
|
|
// `pinned_messages` realtime channel for the conv so the header pill +
|
|
// side-panel update without a refetch. The `applyOptimistic*` helpers let
|
|
// callers flip local state synchronously on user action so the pin button
|
|
// doesn't appear unresponsive while the ~100-200ms server roundtrip + the
|
|
// realtime refetch round complete.
|
|
export function usePinnedMessages(conversationId: string | undefined): UsePinnedMessagesResult {
|
|
const [pins, setPins] = useState<PinnedMessage[]>([]);
|
|
|
|
useEffect(() => {
|
|
if (!conversationId) {
|
|
setPins([]);
|
|
return;
|
|
}
|
|
let cancelled = false;
|
|
|
|
void listPinnedMessages(supabase, conversationId).then((rows) => {
|
|
if (!cancelled) setPins(rows);
|
|
});
|
|
|
|
const channel = supabase
|
|
.channel('pinned_messages:' + conversationId)
|
|
.on(
|
|
'postgres_changes',
|
|
{
|
|
event: '*',
|
|
schema: 'public',
|
|
table: 'pinned_messages',
|
|
filter: 'conversation_id=eq.' + conversationId,
|
|
},
|
|
() => {
|
|
void listPinnedMessages(supabase, conversationId).then((rows) => {
|
|
if (!cancelled) setPins(rows);
|
|
});
|
|
},
|
|
)
|
|
.subscribe();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
void supabase.removeChannel(channel);
|
|
};
|
|
}, [conversationId]);
|
|
|
|
const applyOptimisticPin = useCallback<UsePinnedMessagesResult['applyOptimisticPin']>(
|
|
(messageId, pinnedBy) => {
|
|
if (!conversationId) return pins;
|
|
let snapshot: PinnedMessage[] = pins;
|
|
setPins((prev) => {
|
|
snapshot = prev;
|
|
if (prev.some((p) => p.messageId === messageId)) return prev;
|
|
const optimistic: PinnedMessage = {
|
|
conversationId,
|
|
messageId,
|
|
pinnedBy,
|
|
pinnedAt: new Date().toISOString(),
|
|
};
|
|
// Newest first matches the listPinnedMessages order.
|
|
return [optimistic, ...prev];
|
|
});
|
|
return snapshot;
|
|
},
|
|
[conversationId, pins],
|
|
);
|
|
|
|
const applyOptimisticUnpin = useCallback<UsePinnedMessagesResult['applyOptimisticUnpin']>(
|
|
(messageId) => {
|
|
let snapshot: PinnedMessage[] = pins;
|
|
setPins((prev) => {
|
|
snapshot = prev;
|
|
return prev.filter((p) => p.messageId !== messageId);
|
|
});
|
|
return snapshot;
|
|
},
|
|
[pins],
|
|
);
|
|
|
|
const restorePins = useCallback<UsePinnedMessagesResult['restorePins']>((snapshot) => {
|
|
setPins(snapshot);
|
|
}, []);
|
|
|
|
return { pins, applyOptimisticPin, applyOptimisticUnpin, restorePins };
|
|
}
|