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.
This commit is contained in:
padreug 2025-08-08 21:52:16 +02:00
parent aa3509d807
commit 5fa3fcf60f
2 changed files with 219 additions and 48 deletions

View file

@ -12,6 +12,10 @@
<Badge v-else variant="secondary" class="text-xs">
Disconnected
</Badge>
<!-- Total unread count -->
<Badge v-if="getTotalUnreadCount() > 0" variant="destructive" class="text-xs">
{{ getTotalUnreadCount() }} unread
</Badge>
</div>
<Button @click="refreshPeers" :disabled="isLoading" size="sm">
<RefreshCw v-if="isLoading" class="h-4 w-4 animate-spin" />
@ -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) }}
</p>
</div>
<!-- Unread message indicator -->
<div v-if="getUnreadCount(peer.pubkey) > 0" class="flex-shrink-0">
<Badge variant="destructive" class="h-6 w-6 rounded-full p-0 flex items-center justify-center text-xs font-bold">
{{ getUnreadCount(peer.pubkey) > 99 ? '99+' : getUnreadCount(peer.pubkey) }}
</Badge>
</div>
</div>
</div>
</ScrollArea>
@ -95,32 +105,38 @@
<div v-else-if="isMobile && showChat" class="flex flex-col h-full">
<!-- Chat Header with Back Button -->
<div class="flex items-center justify-between p-4 border-b">
<div class="flex items-center space-x-3">
<Button
variant="ghost"
size="icon"
@click="goBackToPeers"
class="mr-2"
>
<ArrowLeft class="h-5 w-5" />
</Button>
<Avatar class="h-8 w-8">
<AvatarImage v-if="selectedPeer && getPeerAvatar(selectedPeer)" :src="getPeerAvatar(selectedPeer)!" />
<AvatarFallback>{{ selectedPeer ? getPeerInitials(selectedPeer) : 'U' }}</AvatarFallback>
</Avatar>
<div>
<h3 class="font-medium">{{ selectedPeer?.username || 'Unknown User' }}</h3>
<p class="text-xs text-muted-foreground">
{{ selectedPeer ? formatPubkey(selectedPeer.pubkey) : '' }}
</p>
</div>
<div class="flex items-center space-x-3">
<Button
variant="ghost"
size="icon"
@click="goBackToPeers"
class="mr-2"
>
<ArrowLeft class="h-5 w-5" />
</Button>
<Avatar class="h-8 w-8">
<AvatarImage v-if="selectedPeer && getPeerAvatar(selectedPeer)" :src="getPeerAvatar(selectedPeer)!" />
<AvatarFallback>{{ selectedPeer ? getPeerInitials(selectedPeer) : 'U' }}</AvatarFallback>
</Avatar>
<div>
<h3 class="font-medium">{{ selectedPeer?.username || 'Unknown User' }}</h3>
<p class="text-xs text-muted-foreground">
{{ selectedPeer ? formatPubkey(selectedPeer.pubkey) : '' }}
</p>
</div>
<Badge v-if="isConnected" variant="default" class="text-xs">
Connected
</Badge>
<Badge v-else variant="secondary" class="text-xs">
Disconnected
</Badge>
</div>
<div class="flex items-center space-x-2">
<Badge v-if="isConnected" variant="default" class="text-xs">
Connected
</Badge>
<Badge v-else variant="secondary" class="text-xs">
Disconnected
</Badge>
<!-- Unread count for current peer -->
<Badge v-if="selectedPeer && getUnreadCount(selectedPeer.pubkey) > 0" variant="destructive" class="text-xs">
{{ getUnreadCount(selectedPeer.pubkey) }} unread
</Badge>
</div>
</div>
<!-- Messages -->
@ -181,6 +197,10 @@
<Badge v-else variant="secondary" class="text-xs">
Disconnected
</Badge>
<!-- Total unread count -->
<Badge v-if="getTotalUnreadCount() > 0" variant="destructive" class="text-xs">
{{ getTotalUnreadCount() }} unread
</Badge>
</div>
<Button @click="refreshPeers" :disabled="isLoading" size="sm">
<RefreshCw v-if="isLoading" class="h-4 w-4 animate-spin" />
@ -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) }}
</p>
</div>
<!-- Unread message indicator -->
<div v-if="getUnreadCount(peer.pubkey) > 0" class="flex-shrink-0">
<Badge variant="destructive" class="h-6 w-6 rounded-full p-0 flex items-center justify-center text-xs font-bold">
{{ getUnreadCount(peer.pubkey) > 99 ? '99+' : getUnreadCount(peer.pubkey) }}
</Badge>
</div>
</div>
</div>
</ScrollArea>
@ -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