From 5fa3fcf60f55ad279c2d351cec55a71e9fb86633 Mon Sep 17 00:00:00 2001
From: padreug
Date: Fri, 8 Aug 2025 21:52:16 +0200
Subject: [PATCH] feat: Add unread message notifications and tracking in Nostr
chat
- Implement unread message indicators in the ChatComponent for both total unread messages and per-peer unread counts.
- Enhance the useNostrChat composable to manage unread message data, including saving and loading unread counts from localStorage.
- Introduce methods to mark messages as read and update unread counts dynamically as new messages are received.
- Refactor the message handling logic to ensure accurate tracking of unread messages based on the last read timestamp.
---
src/components/nostr/ChatComponent.vue | 92 +++++++++----
src/composables/useNostrChat.ts | 175 ++++++++++++++++++++++---
2 files changed, 219 insertions(+), 48 deletions(-)
diff --git a/src/components/nostr/ChatComponent.vue b/src/components/nostr/ChatComponent.vue
index 8d95a01..b27ede6 100644
--- a/src/components/nostr/ChatComponent.vue
+++ b/src/components/nostr/ChatComponent.vue
@@ -12,6 +12,10 @@
Disconnected
+
+
+ {{ getTotalUnreadCount() }} unread
+
@@ -67,7 +71,7 @@
:key="peer.user_id"
@click="selectPeer(peer)"
:class="[
- 'flex items-center space-x-3 p-3 rounded-lg cursor-pointer transition-colors touch-manipulation',
+ 'flex items-center space-x-3 p-3 rounded-lg cursor-pointer transition-colors touch-manipulation relative',
selectedPeer?.user_id === peer.user_id
? 'bg-primary text-primary-foreground'
: 'hover:bg-muted active:bg-muted/80'
@@ -85,6 +89,12 @@
{{ formatPubkey(peer.pubkey) }}
+
+
+
+ {{ getUnreadCount(peer.pubkey) > 99 ? '99+' : getUnreadCount(peer.pubkey) }}
+
+
@@ -95,32 +105,38 @@
-
-
-
-
-
-
- {{ selectedPeer ? getPeerInitials(selectedPeer) : 'U' }}
-
-
-
{{ selectedPeer?.username || 'Unknown User' }}
-
- {{ selectedPeer ? formatPubkey(selectedPeer.pubkey) : '' }}
-
-
+
+
+
+
+
+
+ {{ selectedPeer ? getPeerInitials(selectedPeer) : 'U' }}
+
+
+
{{ selectedPeer?.username || 'Unknown User' }}
+
+ {{ selectedPeer ? formatPubkey(selectedPeer.pubkey) : '' }}
+
-
- Connected
-
-
- Disconnected
-
+
+
+
+ Connected
+
+
+ Disconnected
+
+
+
+ {{ getUnreadCount(selectedPeer.pubkey) }} unread
+
+
@@ -181,6 +197,10 @@
Disconnected
+
+
+ {{ getTotalUnreadCount() }} unread
+
@@ -238,7 +258,7 @@
:key="peer.user_id"
@click="selectPeer(peer)"
:class="[
- 'flex items-center space-x-3 p-3 rounded-lg cursor-pointer transition-colors',
+ 'flex items-center space-x-3 p-3 rounded-lg cursor-pointer transition-colors relative',
selectedPeer?.user_id === peer.user_id
? 'bg-primary text-primary-foreground'
: 'hover:bg-muted'
@@ -256,6 +276,12 @@
{{ formatPubkey(peer.pubkey) }}
+
+
+
+ {{ getUnreadCount(peer.pubkey) > 99 ? '99+' : getUnreadCount(peer.pubkey) }}
+
+
@@ -420,11 +446,16 @@ const goBackToPeers = () => {
const {
isConnected,
messages,
- sendMessage: sendNostrMessage,
+ isLoggedIn,
connect,
disconnect,
subscribeToPeer,
- onMessageAdded
+ sendMessage: sendNostrMessage,
+ onMessageAdded,
+ markMessagesAsRead,
+ getUnreadCount,
+ getAllUnreadCounts,
+ getTotalUnreadCount
} = useNostrChat()
// Computed
@@ -491,6 +522,9 @@ const selectPeer = async (peer: Peer) => {
selectedPeer.value = peer
messageInput.value = ''
+ // Mark messages as read for this peer
+ markMessagesAsRead(peer.pubkey)
+
// On mobile, show chat view
if (isMobile.value) {
showChat.value = true
diff --git a/src/composables/useNostrChat.ts b/src/composables/useNostrChat.ts
index 57239d1..de795cb 100644
--- a/src/composables/useNostrChat.ts
+++ b/src/composables/useNostrChat.ts
@@ -20,14 +20,32 @@ export interface NostrRelayConfig {
write?: boolean
}
-// Get relays from config - requires VITE_NOSTR_RELAYS to be set
-const getRelays = (): NostrRelayConfig[] => {
- const configuredRelays = config.nostr.relays
- if (!configuredRelays || configuredRelays.length === 0) {
- throw new Error('VITE_NOSTR_RELAYS environment variable must be configured for chat functionality')
+// Add notification system for unread messages
+interface UnreadMessageData {
+ lastReadTimestamp: number
+ unreadCount: number
+}
+
+const UNREAD_MESSAGES_KEY = 'nostr-chat-unread-messages'
+
+// Get unread message data for a peer
+const getUnreadData = (peerPubkey: string): UnreadMessageData => {
+ try {
+ const stored = localStorage.getItem(`${UNREAD_MESSAGES_KEY}-${peerPubkey}`)
+ return stored ? JSON.parse(stored) : { lastReadTimestamp: 0, unreadCount: 0 }
+ } catch (error) {
+ console.warn('Failed to load unread data for peer:', peerPubkey, error)
+ return { lastReadTimestamp: 0, unreadCount: 0 }
+ }
+}
+
+// Save unread message data for a peer
+const saveUnreadData = (peerPubkey: string, data: UnreadMessageData): void => {
+ try {
+ localStorage.setItem(`${UNREAD_MESSAGES_KEY}-${peerPubkey}`, JSON.stringify(data))
+ } catch (error) {
+ console.warn('Failed to save unread data for peer:', peerPubkey, error)
}
-
- return configuredRelays.map((url: string) => ({ url, read: true, write: true }))
}
export function useNostrChat() {
@@ -36,15 +54,112 @@ export function useNostrChat() {
const isConnected = ref(false)
const messages = ref>(new Map())
const currentUser = ref<{ pubkey: string; prvkey: string } | null>(null)
- const pool = ref(null)
const processedMessageIds = ref(new Set())
-
- // Callback for when messages change
const onMessageAdded = ref<((peerPubkey: string) => void) | null>(null)
+ const pool = ref(null)
+
+ // Reactive unread counts
+ const unreadCounts = ref>(new Map())
// Computed
const isLoggedIn = computed(() => !!currentUser.value)
+ // Get unread count for a peer
+ const getUnreadCount = (peerPubkey: string): number => {
+ return unreadCounts.value.get(peerPubkey) || 0
+ }
+
+ // Get all unread counts
+ const getAllUnreadCounts = (): Map => {
+ return new Map(unreadCounts.value)
+ }
+
+ // Get total unread count across all peers
+ const getTotalUnreadCount = (): number => {
+ let total = 0
+ for (const count of unreadCounts.value.values()) {
+ total += count
+ }
+ return total
+ }
+
+ // Update unread count for a peer
+ const updateUnreadCount = (peerPubkey: string, count: number): void => {
+ if (count > 0) {
+ unreadCounts.value.set(peerPubkey, count)
+ } else {
+ unreadCounts.value.delete(peerPubkey)
+ }
+ // Force reactivity
+ unreadCounts.value = new Map(unreadCounts.value)
+ }
+
+ // Mark messages as read for a peer
+ const markMessagesAsRead = (peerPubkey: string): void => {
+ const currentTimestamp = Math.floor(Date.now() / 1000)
+ const unreadData = getUnreadData(peerPubkey)
+
+ // Update last read timestamp and reset unread count
+ const updatedData: UnreadMessageData = {
+ lastReadTimestamp: currentTimestamp,
+ unreadCount: 0
+ }
+
+ saveUnreadData(peerPubkey, updatedData)
+ updateUnreadCount(peerPubkey, 0)
+ }
+
+ // Load unread counts from localStorage
+ const loadUnreadCounts = (): void => {
+ try {
+ const keys = Object.keys(localStorage).filter(key =>
+ key.startsWith(`${UNREAD_MESSAGES_KEY}-`)
+ )
+
+ for (const key of keys) {
+ const peerPubkey = key.replace(`${UNREAD_MESSAGES_KEY}-`, '')
+ const unreadData = getUnreadData(peerPubkey)
+ if (unreadData.unreadCount > 0) {
+ unreadCounts.value.set(peerPubkey, unreadData.unreadCount)
+ }
+ }
+ } catch (error) {
+ console.warn('Failed to load unread counts from localStorage:', error)
+ }
+ }
+
+ // Initialize unread counts on startup
+ loadUnreadCounts()
+
+ // Clear all unread counts (for testing)
+ const clearAllUnreadCounts = (): void => {
+ unreadCounts.value.clear()
+ unreadCounts.value = new Map(unreadCounts.value)
+
+ // Also clear from localStorage
+ try {
+ const keys = Object.keys(localStorage).filter(key =>
+ key.startsWith(`${UNREAD_MESSAGES_KEY}-`)
+ )
+
+ for (const key of keys) {
+ localStorage.removeItem(key)
+ }
+ } catch (error) {
+ console.warn('Failed to clear unread counts from localStorage:', error)
+ }
+ }
+
+ // Get relays from config - requires VITE_NOSTR_RELAYS to be set
+ const getRelays = (): NostrRelayConfig[] => {
+ const configuredRelays = config.nostr.relays
+ if (!configuredRelays || configuredRelays.length === 0) {
+ throw new Error('VITE_NOSTR_RELAYS environment variable must be configured for chat functionality')
+ }
+
+ return configuredRelays.map((url: string) => ({ url, read: true, write: true }))
+ }
+
// Initialize Nostr pool
const initializePool = () => {
if (!pool.value) {
@@ -316,7 +431,7 @@ export function useNostrChat() {
id: event.id,
content: decryptedContent,
created_at: event.created_at,
- sent: event.pubkey === currentUser.value!.pubkey,
+ sent: isSentByMe,
pubkey: event.pubkey
}
@@ -324,12 +439,6 @@ export function useNostrChat() {
// Always use peerPubkey as the conversation key for both sent and received messages
const conversationKey = peerPubkey
- console.log('Storing message with conversation key:', conversationKey)
-
- if (!messages.value.has(conversationKey)) {
- messages.value.set(conversationKey, [])
- }
-
if (!messages.value.has(conversationKey)) {
messages.value.set(conversationKey, [])
}
@@ -342,6 +451,28 @@ export function useNostrChat() {
// Force reactivity by triggering a change
messages.value = new Map(messages.value)
+ // Track unread messages (only for received messages, not sent ones)
+ if (!isSentByMe) {
+ const unreadData = getUnreadData(peerPubkey)
+
+ // Check if this message is newer than the last read timestamp
+ if (message.created_at > unreadData.lastReadTimestamp) {
+ const updatedUnreadData: UnreadMessageData = {
+ lastReadTimestamp: unreadData.lastReadTimestamp,
+ unreadCount: unreadData.unreadCount + 1
+ }
+
+ saveUnreadData(peerPubkey, updatedUnreadData)
+ updateUnreadCount(peerPubkey, updatedUnreadData.unreadCount)
+
+ console.log(`New unread message from ${peerPubkey}. Total unread: ${updatedUnreadData.unreadCount}`)
+ } else {
+ console.log(`Message from ${peerPubkey} is older than last read timestamp. Skipping unread count.`)
+ }
+ } else {
+ console.log(`Message from ${peerPubkey} was sent by current user. Skipping unread count.`)
+ }
+
// Trigger callback if set
if (onMessageAdded.value) {
onMessageAdded.value(conversationKey)
@@ -503,7 +634,13 @@ export function useNostrChat() {
sendMessage,
getMessages,
clearMessages,
- loadCurrentUser,
- onMessageAdded
+ onMessageAdded,
+
+ // Notification methods
+ markMessagesAsRead,
+ getUnreadCount,
+ getAllUnreadCounts,
+ getTotalUnreadCount,
+ clearAllUnreadCounts
}
}
\ No newline at end of file