Skip to content

Commit 1eb05f7

Browse files
authored
Unread indicator (#115)
* Unread indicator and last seen for groups * Switch to store
1 parent aa049d1 commit 1eb05f7

File tree

4 files changed

+78
-24
lines changed

4 files changed

+78
-24
lines changed

src/pages/chats/group/GroupChatPage.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useState} from "react"
1+
import {useState, useEffect, useCallback} from "react"
22
import {useLocation} from "@/navigation"
33
import {useGroupsStore} from "@/stores/groups"
44
import {usePrivateMessagesStore} from "@/stores/privateMessages"
@@ -8,6 +8,7 @@ import GroupChatHeader from "./GroupChatHeader"
88
import {SortedMap} from "@/utils/SortedMap/SortedMap"
99
import {MessageType} from "../message/Message"
1010
import {comparator} from "../utils/messageGrouping"
11+
import {getMillisecondTimestamp} from "nostr-double-ratchet/src"
1112

1213
const GroupChatPage = () => {
1314
const location = useLocation()
@@ -18,13 +19,51 @@ const GroupChatPage = () => {
1819
const groups = useGroupsStore((state) => state.groups)
1920
const group = id ? groups[id] : undefined
2021
const {events} = usePrivateMessagesStore()
22+
const markOpened = usePrivateMessagesStore((state) => state.markOpened)
2123
const [replyingTo, setReplyingTo] = useState<MessageType | undefined>(undefined)
2224

2325
if (!id || !group) {
2426
return <div className="p-4">Group not found</div>
2527
}
2628

2729
const messages = events.get(id) ?? new SortedMap<string, MessageType>([], comparator)
30+
const lastMessageEntry = messages.last()
31+
const lastMessage = lastMessageEntry ? lastMessageEntry[1] : undefined
32+
const lastMessageTimestamp = lastMessage ? getMillisecondTimestamp(lastMessage) : undefined
33+
34+
const markGroupOpened = useCallback(() => {
35+
if (!id) return
36+
markOpened(id)
37+
}, [id, markOpened])
38+
39+
useEffect(() => {
40+
if (!id) return
41+
42+
markGroupOpened()
43+
44+
const handleVisibilityChange = () => {
45+
if (document.visibilityState === "visible") {
46+
markGroupOpened()
47+
}
48+
}
49+
50+
const handleFocus = () => {
51+
markGroupOpened()
52+
}
53+
54+
document.addEventListener("visibilitychange", handleVisibilityChange)
55+
window.addEventListener("focus", handleFocus)
56+
57+
return () => {
58+
document.removeEventListener("visibilitychange", handleVisibilityChange)
59+
window.removeEventListener("focus", handleFocus)
60+
}
61+
}, [id, markGroupOpened])
62+
63+
useEffect(() => {
64+
if (!id || lastMessageTimestamp === undefined) return
65+
markOpened(id)
66+
}, [id, lastMessageTimestamp, markOpened])
2867

2968
return (
3069
<>

src/pages/chats/list/ChatListItem.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const ChatListItem = ({id, isPublic = false, type}: ChatListItemProps) => {
6767
const lastSeenPrivateTime = usePrivateMessagesStore(
6868
(state) => state.lastSeen.get(id) || 0
6969
)
70-
const updateLastSeenPrivate = usePrivateMessagesStore((state) => state.updateLastSeen)
70+
const markPrivateChatOpened = usePrivateMessagesStore((state) => state.markOpened)
7171

7272
// Use ref to avoid effect recreation when store updates
7373
const updateLatestMessageRef = useRef(updateLatestMessage)
@@ -230,7 +230,15 @@ const ChatListItem = ({id, isPublic = false, type}: ChatListItemProps) => {
230230
unreadBadge = <div className="indicator-item badge badge-primary badge-xs" />
231231
}
232232
}
233-
} else if (!group) {
233+
} else if (group) {
234+
if (actualLatest?.created_at && actualLatest.pubkey !== myPubKey) {
235+
const latestTimestamp = getMillisecondTimestamp(actualLatest as MessageType)
236+
const hasUnread = latestTimestamp > lastSeenPrivateTime
237+
if (!lastSeenPrivateTime || hasUnread) {
238+
unreadBadge = <div className="indicator-item badge badge-primary badge-xs" />
239+
}
240+
}
241+
} else {
234242
if (actualLatest?.created_at && actualLatest.pubkey !== myPubKey) {
235243
const latestTimestamp = getMillisecondTimestamp(actualLatest as MessageType)
236244
const hasUnread = latestTimestamp > lastSeenPrivateTime
@@ -259,8 +267,8 @@ const ChatListItem = ({id, isPublic = false, type}: ChatListItemProps) => {
259267
onClick={() => {
260268
if (isPublic) {
261269
updateLastSeenPublic(id)
262-
} else if (!group) {
263-
updateLastSeenPrivate(id)
270+
} else {
271+
markPrivateChatOpened(id)
264272
}
265273
}}
266274
className={classNames("px-2 py-4 flex items-center border-b border-custom", {

src/pages/chats/private/PrivateChat.tsx

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,18 @@ const Chat = ({id}: {id: string}) => {
2222

2323
// Get messages reactively from events store - this will update when new messages are added
2424
const eventsMap = usePrivateMessagesStore((state) => state.events)
25-
const updateLastSeen = usePrivateMessagesStore((state) => state.updateLastSeen)
25+
const markOpened = usePrivateMessagesStore((state) => state.markOpened)
2626
const messages = eventsMap.get(id) ?? new SortedMap<string, MessageType>([], comparator)
2727
const lastMessageEntry = messages.last()
2828
const lastMessage = lastMessageEntry ? lastMessageEntry[1] : undefined
2929
const lastMessageTimestamp = lastMessage
3030
? getMillisecondTimestamp(lastMessage)
3131
: undefined
32-
const lastMessageId = lastMessage?.id
3332

3433
const markChatOpened = useCallback(() => {
3534
if (!id) return
36-
const events = usePrivateMessagesStore.getState().events
37-
const latestMessage = events.get(id)?.last()?.[1]
38-
const latestTimestamp = latestMessage
39-
? getMillisecondTimestamp(latestMessage)
40-
: undefined
41-
const targetTimestamp = Math.max(Date.now(), latestTimestamp ?? 0)
42-
const current = usePrivateMessagesStore.getState().lastSeen.get(id) || 0
43-
if (targetTimestamp > current) {
44-
updateLastSeen(id, targetTimestamp)
45-
}
46-
}, [id, updateLastSeen])
35+
markOpened(id)
36+
}, [id, markOpened])
4737

4838
useEffect(() => {
4939
if (!id) {
@@ -89,11 +79,8 @@ const Chat = ({id}: {id: string}) => {
8979

9080
useEffect(() => {
9181
if (!id || lastMessageTimestamp === undefined) return
92-
const existing = usePrivateMessagesStore.getState().lastSeen.get(id) || 0
93-
if (lastMessageTimestamp > existing) {
94-
updateLastSeen(id, lastMessageTimestamp)
95-
}
96-
}, [id, lastMessageId, lastMessageTimestamp, updateLastSeen])
82+
markOpened(id)
83+
}, [id, lastMessageTimestamp, markOpened])
9784

9885
const handleSendReaction = async (messageId: string, emoji: string) => {
9986
const myPubKey = useUserStore.getState().publicKey

src/stores/privateMessages.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as messageRepository from "@/utils/messageRepository"
44
import {KIND_REACTION} from "@/utils/constants"
55
import {SortedMap} from "@/utils/SortedMap/SortedMap"
66
import {create} from "zustand"
7+
import {getMillisecondTimestamp} from "nostr-double-ratchet/src"
78
import {useUserStore} from "./user"
89

910
const addToMap = (
@@ -32,14 +33,15 @@ interface PrivateMessagesStoreActions {
3233
updates: Partial<MessageType>
3334
) => Promise<void>
3435
updateLastSeen: (chatId: string, timestamp?: number) => void
36+
markOpened: (chatId: string) => void
3537
removeSession: (chatId: string) => Promise<void>
3638
removeMessage: (chatId: string, messageId: string) => Promise<void>
3739
clear: () => Promise<void>
3840
}
3941

4042
type PrivateMessagesStore = PrivateMessagesStoreState & PrivateMessagesStoreActions
4143

42-
export const usePrivateMessagesStore = create<PrivateMessagesStore>((set) => {
44+
export const usePrivateMessagesStore = create<PrivateMessagesStore>((set, get) => {
4345
const rehydration = Promise.all([
4446
messageRepository.loadAll(),
4547
messageRepository.loadLastSeen(),
@@ -171,5 +173,23 @@ export const usePrivateMessagesStore = create<PrivateMessagesStore>((set) => {
171173
})
172174
messageRepository.saveLastSeen(chatId, effectiveTimestamp).catch(console.error)
173175
},
176+
177+
markOpened: (chatId: string) => {
178+
if (!chatId) return
179+
const state = get()
180+
const events = state.events
181+
const messageMap = events.get(chatId)
182+
const latestEntry = messageMap?.last()
183+
const latestMessage = latestEntry ? latestEntry[1] : undefined
184+
const latestTimestamp = latestMessage
185+
? getMillisecondTimestamp(latestMessage)
186+
: undefined
187+
const targetTimestamp = Math.max(Date.now(), latestTimestamp ?? 0)
188+
const current = state.lastSeen.get(chatId) || 0
189+
if (targetTimestamp <= current) {
190+
return
191+
}
192+
state.updateLastSeen(chatId, targetTimestamp)
193+
},
174194
}
175195
})

0 commit comments

Comments
 (0)