perf(P6C.T12): optimistic UI for mute / mentions-only / archive / pin / device-revoke
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.
This commit is contained in:
@@ -241,24 +241,34 @@ export function ConversationPage() {
|
||||
// Auto-clears on a successful send so the composer doesn't accidentally
|
||||
// burn the message-after-next.
|
||||
const [viewOnceNext, setViewOnceNext] = useState(false);
|
||||
const pins = usePinnedMessages(id);
|
||||
const { pins, applyOptimisticPin, applyOptimisticUnpin, restorePins } = usePinnedMessages(id);
|
||||
const [pinnedPanelOpen, setPinnedPanelOpen] = useState(false);
|
||||
const pinnedIds = useMemo(() => new Set(pins.map((p) => p.messageId)), [pins]);
|
||||
|
||||
// Optimistic pin/unpin: flip the local list synchronously so the pin badge
|
||||
// / panel updates on the same frame as the click. Realtime echo via
|
||||
// usePinnedMessages will refetch and reconcile (no-op since the optimistic
|
||||
// row matches the server). On error we restore the snapshot so the badge
|
||||
// doesn't lie about persisted state.
|
||||
const handleTogglePin = useCallback(
|
||||
async (messageId: string) => {
|
||||
if (!id || !myId) return;
|
||||
const wasPinned = pinnedIds.has(messageId);
|
||||
const snapshot = wasPinned
|
||||
? applyOptimisticUnpin(messageId)
|
||||
: applyOptimisticPin(messageId, myId);
|
||||
try {
|
||||
if (pinnedIds.has(messageId)) {
|
||||
if (wasPinned) {
|
||||
await unpinMessage(supabase, id, messageId);
|
||||
} else {
|
||||
await pinMessage(supabase, id, messageId, myId);
|
||||
}
|
||||
} catch (err) {
|
||||
restorePins(snapshot);
|
||||
console.warn('pin toggle failed', err);
|
||||
}
|
||||
},
|
||||
[id, myId, pinnedIds],
|
||||
[id, myId, pinnedIds, applyOptimisticPin, applyOptimisticUnpin, restorePins],
|
||||
);
|
||||
|
||||
const handleGifPick = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user