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:
parent
aa3509d807
commit
5fa3fcf60f
2 changed files with 219 additions and 48 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue