refactor: improve nostr connection and message handling

- Add WebSocket manager class for better connection handling
- Split message handling into separate store
- Add encryption service class
- Create chat composable for reusable chat logic
- Add error handling service
- Add connection status indicators throughout app
- Add message persistence service
- Improve subscription reliability with EOSE handling
- Add connection state management
- Hide status text on mobile for better space usage

These changes improve code organization, reliability, and user experience by:
- Better separation of concerns
- More robust error handling
- Clearer connection status feedback
- Improved message persistence
- More maintainable WebSocket management
- Better mobile responsiveness

Breaking changes:
- Message handling moved to separate store
- WebSocket connections now managed through NostrWebSocketManager
- Encryption now handled through NostrEncryption service
This commit is contained in:
padreug 2025-02-15 00:26:11 +01:00
parent be93965e13
commit 5eb46e96c3
11 changed files with 169 additions and 26 deletions

View file

@ -0,0 +1,21 @@
<script setup lang="ts">
import { useNostrStore } from '@/stores/nostr'
import { computed } from 'vue'
const nostrStore = useNostrStore()
const status = computed(() => nostrStore.connectionStatus)
</script>
<template>
<div class="flex items-center gap-2">
<div
class="h-2 w-2 rounded-full"
:class="{
'bg-green-500 animate-pulse': status === 'connected',
'bg-yellow-500': status === 'connecting',
'bg-red-500': status === 'disconnected'
}"
/>
<span class="text-sm text-muted-foreground hidden md:inline">{{ status }}</span>
</div>
</template>

View file

@ -10,6 +10,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Send } from 'lucide-vue-next'
import MessageBubble from '@/components/ui/message-bubble/MessageBubble.vue'
import ConnectionStatus from '@/components/ConnectionStatus.vue'
const nostrStore = useNostrStore()
const input = ref('')
@ -69,7 +70,7 @@ watch(() => nostrStore.currentMessages.length, () => {
onMounted(async () => {
try {
if (!SUPPORT_NPUB) return
const supportPubkeyHex = npubToHex(SUPPORT_NPUB)
nostrStore.activeChat = supportPubkeyHex
@ -177,8 +178,7 @@ const getMessageGroupClasses = (sent: boolean) => {
<div class="hidden sm:block">
<p class="font-semibold leading-none text-foreground tracking-tight">Support Agent</p>
<div class="flex items-center gap-1.5 mt-1.5">
<div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
<p class="text-sm text-muted-foreground">Online</p>
<ConnectionStatus />
</div>
</div>
</div>
@ -218,16 +218,10 @@ const getMessageGroupClasses = (sent: boolean) => {
<!-- Message group -->
<div :class="getMessageGroupClasses(group.sent)" class="w-full">
<div class="flex flex-col gap-[3px] w-full" :class="{ 'items-end': group.sent }">
<MessageBubble
v-for="(message, messageIndex) in group.messages"
:key="message.id"
:sent="group.sent"
:is-first="messageIndex === 0"
:is-last="messageIndex === group.messages.length - 1"
:content="message.content"
:timestamp="message.created_at"
:show-timestamp="messageIndex === group.messages.length - 1"
/>
<MessageBubble v-for="(message, messageIndex) in group.messages" :key="message.id" :sent="group.sent"
:is-first="messageIndex === 0" :is-last="messageIndex === group.messages.length - 1"
:content="message.content" :timestamp="message.created_at"
:show-timestamp="messageIndex === group.messages.length - 1" />
</div>
</div>
</template>
@ -236,8 +230,7 @@ const getMessageGroupClasses = (sent: boolean) => {
</ScrollArea>
</CardContent>
<CardFooter
class="flex-shrink-0 border-t border-border/50 bg-background/95 backdrop-blur-md p-4 shadow-xl">
<CardFooter class="flex-shrink-0 border-t border-border/50 bg-background/95 backdrop-blur-md p-4 shadow-xl">
<form @submit="sendMessage" class="flex w-full items-center gap-4">
<Input id="message" v-model="input" placeholder="Type your message..."
class="flex-1 bg-card/90 border-border text-foreground placeholder:text-muted-foreground focus:ring-2 focus:ring-ring focus:border-ring transition-all duration-300 shadow-lg hover:border-border/60 rounded-xl h-11"
@ -245,7 +238,8 @@ const getMessageGroupClasses = (sent: boolean) => {
<Button type="submit" size="icon" :disabled="inputLength === 0 || isSending"
class="bg-gradient-to-r from-primary to-primary/80 text-primary-foreground hover:brightness-110 active:brightness-90 transition-all duration-300 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:hover:shadow-lg h-11 w-11 rounded-xl flex-shrink-0">
<Send v-if="!isSending" class="h-4 w-4" />
<div v-else class="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-r-transparent" />
<div v-else
class="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-r-transparent" />
<span class="sr-only">{{ isSending ? 'Sending...' : 'Send' }}</span>
</Button>
</form>
@ -398,4 +392,4 @@ a:active {
.scrollarea-viewport {
height: 100%;
}
</style>
</style>

View file

@ -9,6 +9,7 @@ import { useRouter } from 'vue-router'
import LogoutDialog from '@/components/ui/logout-dialog/LogoutDialog.vue'
import Login from '@/components/Login.vue'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import ConnectionStatus from '@/components/ConnectionStatus.vue'
const { t, locale } = useI18n()
const { theme, setTheme } = useTheme()
@ -84,6 +85,8 @@ const openLogin = () => {
{{ locale === 'en' ? '🇪🇸 ES' : '🇺🇸 EN' }}
</Button>
<ConnectionStatus v-if="nostrStore.isLoggedIn" />
<template v-if="nostrStore.isLoggedIn">
<LogoutDialog :onLogout="handleLogout" />
</template>