useConversations reported unreadCount: 0 for everything, so the inbox never showed new messages. Add a lightweight, reliable unread signal: - useDMRead: per-conversation, per-user last-read timestamps in localStorage (local-only; compares against the inner rumor's real created_at, not the fuzzed gift-wrap time, so it's accurate). - DirectMessagesPage: a conversation is unread when its latest message is from the other person and newer than last-read — rendered as a red dot, bolder name, brighter preview text, and a subtle highlighted card. - ConversationPage: opening a chat marks it read up to the newest message, so the dot clears on return (each page mounts fresh, so no stale state). Scope: kept to the Messages page. Deliberately did NOT add a global nav badge, which would require background-decrypting all gift wraps on every page — the same heavy cost avoided for the crew bell. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
43 lines
1.8 KiB
TypeScript
43 lines
1.8 KiB
TypeScript
import { useCallback } from 'react';
|
|
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
|
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
|
|
|
/**
|
|
* Per-conversation "last read" timestamps (unix seconds) for direct messages,
|
|
* stored per-pubkey in localStorage. A conversation counts as unread when its
|
|
* latest message (from the OTHER person) is newer than this timestamp.
|
|
*
|
|
* Local-only by design: this is a lightweight "have I seen it" convenience, not
|
|
* a synced read-receipt broadcast to anyone. Nothing here leaves the device.
|
|
*
|
|
* NIP-17 note: gift-wrapped DMs are decrypted client-side, so we compare against
|
|
* the message's own `created_at`. (Gift-wrap timestamps can be slightly fuzzed,
|
|
* but the inner rumor's `created_at` — what we store on DirectMessage — is the
|
|
* sender's real time, so this comparison is reliable for read state.)
|
|
*/
|
|
export function useDMRead() {
|
|
const { user } = useCurrentUser();
|
|
const key = user ? `dm-last-read:${user.pubkey}` : 'dm-last-read:anon';
|
|
const [map, setMap] = useLocalStorage<Record<string, number>>(key, {});
|
|
|
|
/** Mark a conversation (keyed by the other party's pubkey) read as of `ts`. */
|
|
const markRead = useCallback(
|
|
(conversationWith: string, ts?: number) => {
|
|
const at = ts ?? Math.floor(Date.now() / 1000);
|
|
setMap((prev) => {
|
|
const key = conversationWith.toLowerCase();
|
|
if ((prev[key] ?? 0) >= at) return prev; // never move backwards
|
|
return { ...prev, [key]: at };
|
|
});
|
|
},
|
|
[setMap],
|
|
);
|
|
|
|
/** Last-read unix seconds for a conversation, or 0 if never opened. */
|
|
const getLastRead = useCallback(
|
|
(conversationWith: string) => map[conversationWith.toLowerCase()] ?? 0,
|
|
[map],
|
|
);
|
|
|
|
return { lastReadMap: map, markRead, getLastRead };
|
|
}
|