This commit is contained in:
padreug 2025-02-14 23:12:34 +01:00
parent d694f9b645
commit d27f66e95d
5 changed files with 230 additions and 162 deletions

View file

@ -79,7 +79,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
*/
workbox.precacheAndRoute([{
"url": "index.html",
"revision": "0.dc5rn1lchj8"
"revision": "0.36o4mscev7"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {

View file

@ -86,7 +86,7 @@ import { useNostrStore } from '@/stores/nostr'
import { KeyRound, Copy, Check, Eye, EyeOff, Loader2 } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { CardDescription, CardTitle } from '@/components/ui/card'
import { isValidPrivateKey, formatPrivateKey } from '@/lib/nostr'
const { t } = useI18n()
@ -139,7 +139,7 @@ const generateKey = () => {
privkey.value = newKey
error.value = ''
showRecoveryMessage.value = true
showKey.value = true // Show the key when generated
showKey.value = false
} catch (err) {
console.error('Failed to generate key:', err)
error.value = t('login.error')

View file

@ -16,6 +16,7 @@ const input = ref('')
const isSending = ref(false)
const error = ref('')
const messagesEndRef = ref<HTMLDivElement | null>(null)
const isSubscribing = ref(false)
const SUPPORT_NPUB = import.meta.env.VITE_SUPPORT_NPUB
if (!SUPPORT_NPUB) {
@ -71,11 +72,22 @@ onMounted(async () => {
const supportPubkeyHex = npubToHex(SUPPORT_NPUB)
nostrStore.activeChat = supportPubkeyHex
await nostrStore.subscribeToMessages()
// Try to subscribe in the background
isSubscribing.value = true
nostrStore.subscribeToMessages()
.catch(err => {
console.debug('Support chat subscription error:', err)
// Continue anyway - messages will come through when connection succeeds
})
.finally(() => {
isSubscribing.value = false
})
scrollToBottom()
} catch (err) {
console.error('Failed to initialize support chat:', err)
error.value = 'Failed to connect to support. Please try again later.'
console.debug('Support chat setup error:', err)
// Continue anyway
}
})
@ -90,10 +102,13 @@ onUnmounted(() => {
watch(() => nostrStore.activeChat, async (newChat) => {
if (newChat) {
try {
isSubscribing.value = true
await nostrStore.subscribeToMessages()
} catch (err) {
console.error('Failed to subscribe to messages:', err)
error.value = 'Failed to connect to chat. Please try again later.'
console.debug('Chat subscription error:', err)
// Continue anyway
} finally {
isSubscribing.value = false
}
}
})
@ -235,6 +250,12 @@ const getMessageGroupClasses = (sent: boolean) => {
</Button>
</form>
</CardFooter>
<!-- Add loading indicator if needed -->
<div v-if="isSubscribing" class="absolute top-4 right-4 flex items-center gap-2 text-sm text-muted-foreground">
<div class="h-2 w-2 rounded-full bg-primary animate-pulse"></div>
Connecting...
</div>
</Card>
</template>

View file

@ -1,14 +1,15 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useNostrStore } from '@/stores/nostr'
import SupportChat from '@/components/SupportChat.vue'
import { useRouter } from 'vue-router'
import { Dialog, DialogContent } from '@/components/ui/dialog'
import Login from '@/components/Login.vue'
const router = useRouter()
const nostrStore = useNostrStore()
const showLoginDialog = ref(!nostrStore.isLoggedIn)
// Redirect to home if not logged in
if (!nostrStore.isLoggedIn) {
router.push('/')
const handleLoginSuccess = () => {
showLoginDialog.value = false
}
</script>
@ -20,6 +21,13 @@ if (!nostrStore.isLoggedIn) {
</div>
</div>
</div>
<!-- Login Dialog -->
<Dialog v-model:open="showLoginDialog">
<DialogContent class="sm:max-w-md">
<Login @success="handleLoginSuccess" />
</DialogContent>
</Dialog>
</template>
<style scoped>

View file

@ -38,9 +38,6 @@ const DEFAULT_RELAYS = [
'wss://nostr.atitlan.io'
]
// Get support agent's public key from environment variable
const SUPPORT_NPUB = import.meta.env.VITE_SUPPORT_NPUB
// Helper functions
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number = 10000): Promise<T> {
return Promise.race([
@ -132,54 +129,33 @@ export const useNostrStore = defineStore('nostr', () => {
profiles.value.clear()
processedMessageIds.value.clear()
// Close existing connections
relayPool.value.forEach(relay => {
try {
relay.close()
} catch (err) {
console.error('Error closing relay:', err)
}
})
relayPool.value = []
// Connect to relays with timeout
const connectionPromises = account.value.relays.map(async relay => {
try {
return await withTimeout(connectToRelay(relay.url), 5000)
} catch (err) {
console.error(`Timeout connecting to ${relay.url}:`, err)
return null
}
})
const connectedRelays = await Promise.all(connectionPromises)
relayPool.value = connectedRelays.filter((relay): relay is NonNullable<typeof relay> => relay !== null)
// Connect to relays
relayPool.value = (await Promise.all(
account.value.relays.map(async relay => {
console.log('Connecting to relay:', relay.url)
const connection = await connectToRelay(relay.url)
if (!connection) {
console.error('Failed to connect to relay:', relay.url)
}
return connection
})
)).filter((relay): relay is any => relay !== null)
if (relayPool.value.length === 0) {
throw new Error('Failed to connect to any relays')
}
// Set active chat to support agent first
activeChat.value = SUPPORT_NPUB
// Setup visibility change handler
setupVisibilityHandler()
// Subscribe to messages with shorter timeout
try {
await withTimeout(subscribeToMessages(), 10000)
} catch (err) {
console.error('Failed to subscribe to messages:', err)
// Continue even if subscription fails
}
// Subscribe to messages in the background
subscribeToMessages().catch(err => {
console.error('Background subscription failed:', err)
})
// Load profiles with shorter timeout
try {
await withTimeout(loadProfiles(), 5000)
} catch (err) {
console.error('Failed to load profiles:', err)
// Continue even if profile loading fails
}
} catch (err) {
console.error('Failed to initialize:', err)
throw new Error('Failed to connect to the network. Please try again.')
throw err
}
}
@ -198,43 +174,9 @@ export const useNostrStore = defineStore('nostr', () => {
relays: DEFAULT_RELAYS.map(url => ({ url, read: true, write: true }))
}
try {
// Initialize connection with a global timeout
await withTimeout(init(), 30000) // 30 second total timeout for the entire login process
} catch (err) {
// If initialization fails, clear the account
account.value = null
throw err
}
}
async function loadProfiles() {
if (!account.value) return
const filter = {
kinds: [0],
authors: Array.from(messages.value.keys())
}
if (filter.authors.length === 0) return
relayPool.value.forEach(relay => {
const sub = relay.sub([filter])
sub.on('event', (event: NostrEvent) => {
try {
const profile = JSON.parse(event.content)
profiles.value.set(event.pubkey, {
pubkey: event.pubkey,
name: profile.name,
picture: profile.picture,
about: profile.about,
nip05: profile.nip05
})
} catch (err) {
console.error('Failed to parse profile:', err)
}
})
// Initialize connection in the background
init().catch(err => {
console.error('Background initialization failed:', err)
})
}
@ -301,55 +243,42 @@ export const useNostrStore = defineStore('nostr', () => {
async function subscribeToMessages() {
if (!account.value) return
// Cleanup existing subscription if any
if (currentSubscription.value) {
unsubscribeFromMessages()
}
// Cleanup existing subscription
unsubscribeFromMessages()
let hasReceivedMessage = false
const subscriptionTimeout = setTimeout(() => {
if (!hasReceivedMessage) {
console.log('No messages received, considering subscription successful')
hasReceivedMessage = true
}
}, 5000)
// Get timestamp from 24 hours ago
const since = Math.floor(Date.now() / 1000) - (24 * 60 * 60)
try {
const subscribeToRelay = (relay: any) => {
return new Promise((resolve) => {
let subs: any[] = []
let resolved = false
// Set a timeout for the entire subscription
const timeout = setTimeout(() => {
if (!resolved) {
console.log('Subscription timeout, but continuing...')
resolved = true
resolve(true)
}
}, 8000)
const subs: any[] = []
let messageReceived = false
let isResolved = false
try {
// Filter for received messages with history
const receivedFilter = {
kinds: [4],
'#p': [account.value!.pubkey],
since: 0
}
// Filter for sent messages with history
const sentFilter = {
kinds: [4],
authors: [account.value!.pubkey],
since: 0
}
console.log('Setting up subscriptions for relay...')
// Subscribe to received messages
const receivedSub = relay.sub([receivedFilter])
const receivedSub = relay.sub([{
kinds: [4],
'#p': [account.value!.pubkey],
since,
}])
subs.push(receivedSub)
// Subscribe to sent messages
const sentSub = relay.sub([{
kinds: [4],
authors: [account.value!.pubkey],
since,
}])
subs.push(sentSub)
// Handle received messages
receivedSub.on('event', async (event: NostrEvent) => {
hasReceivedMessage = true
messageReceived = true
if (isResolved) return // Don't process events after resolution
try {
if (processedMessageIds.value.has(event.id)) return
@ -369,21 +298,15 @@ export const useNostrStore = defineStore('nostr', () => {
await addMessage(event.pubkey, dm)
processedMessageIds.value.add(event.id)
if (!profiles.value.has(event.pubkey)) {
await loadProfiles()
}
} catch (err) {
console.error('Failed to decrypt received message:', err)
}
})
// Subscribe to sent messages
const sentSub = relay.sub([sentFilter])
subs.push(sentSub)
// Handle sent messages
sentSub.on('event', async (event: NostrEvent) => {
hasReceivedMessage = true
messageReceived = true
if (isResolved) return // Don't process events after resolution
try {
if (processedMessageIds.value.has(event.id)) return
@ -414,50 +337,56 @@ export const useNostrStore = defineStore('nostr', () => {
// Store subscriptions for cleanup
currentSubscription.value = {
unsub: () => {
clearTimeout(timeout)
subs.forEach(sub => {
try {
if (sub && typeof sub.unsub === 'function') {
sub.unsub()
}
} catch (err) {
console.error('Failed to unsubscribe:', err)
console.debug('Failed to unsubscribe:', err)
}
})
}
}
// Consider subscription successful immediately
if (!resolved) {
resolved = true
resolve(true)
}
// Consider subscription successful after a short delay
setTimeout(() => {
if (!isResolved) {
isResolved = true
console.debug(messageReceived ?
'Subscription successful with messages' :
'Subscription successful, no messages yet'
)
resolve(true)
}
}, 3000)
} catch (err) {
console.error('Error in subscription:', err)
if (!resolved) {
resolved = true
resolve(true)
console.debug('Error in subscription setup:', err)
if (!isResolved) {
isResolved = true
resolve(false)
}
}
})
}
// Wait for relays
await Promise.all(
relayPool.value.map(relay =>
withTimeout(subscribeToRelay(relay), 10000)
.catch(() => true)
)
// Wait for all relays to set up subscriptions
const results = await Promise.all(
relayPool.value.map(relay => subscribeToRelay(relay))
)
clearTimeout(subscriptionTimeout)
return true
// Consider success if at least one relay worked
const success = results.some(result => result)
if (!success) {
console.debug('No relays successfully subscribed')
return false // Return false instead of throwing
}
} catch (err) {
clearTimeout(subscriptionTimeout)
console.error('Failed to subscribe to messages:', err)
return true
} catch (err) {
console.debug('Subscription process failed:', err)
return false // Return false instead of throwing
}
}
@ -472,6 +401,115 @@ export const useNostrStore = defineStore('nostr', () => {
}
}
async function loadProfiles() {
if (!account.value) return
const pubkeysToLoad = new Set<string>()
// Collect all unique pubkeys from messages
for (const [pubkey] of messages.value.entries()) {
if (!profiles.value.has(pubkey)) {
pubkeysToLoad.add(pubkey)
}
}
if (pubkeysToLoad.size === 0) return
try {
const filter = {
kinds: [0],
authors: Array.from(pubkeysToLoad)
}
const loadFromRelay = (relay: any) => {
return new Promise<void>((resolve) => {
const sub = relay.sub([filter])
sub.on('event', (event: NostrEvent) => {
try {
const profile = JSON.parse(event.content)
profiles.value.set(event.pubkey, {
pubkey: event.pubkey,
name: profile.name,
picture: profile.picture,
about: profile.about,
nip05: profile.nip05
})
} catch (err) {
console.error('Failed to parse profile:', err)
}
})
// Resolve after receiving EOSE (End of Stored Events)
sub.on('eose', () => {
resolve()
})
// Set a timeout in case EOSE is not received
setTimeout(() => {
resolve()
}, 5000)
})
}
// Load profiles from all relays concurrently
await Promise.all(relayPool.value.map(relay => loadFromRelay(relay)))
} catch (err) {
console.error('Failed to load profiles:', err)
}
}
// Add a reconnection function
async function reconnectToRelays() {
if (!account.value) return
console.log('Attempting to reconnect to relays...')
// Close existing connections
relayPool.value.forEach(relay => {
try {
relay.close()
} catch (err) {
console.error('Error closing relay:', err)
}
})
relayPool.value = []
// Reconnect
relayPool.value = (await Promise.all(
account.value.relays.map(async relay => {
console.log('Reconnecting to relay:', relay.url)
const connection = await connectToRelay(relay.url)
if (!connection) {
console.error('Failed to reconnect to relay:', relay.url)
}
return connection
})
)).filter((relay): relay is any => relay !== null)
if (relayPool.value.length === 0) {
throw new Error('Failed to connect to any relays')
}
// Resubscribe to messages
await subscribeToMessages()
}
// Add visibility change handler
function setupVisibilityHandler() {
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible' && account.value) {
console.log('Page became visible, reconnecting...')
try {
await reconnectToRelays()
} catch (err) {
console.error('Failed to reconnect:', err)
}
}
})
}
return {
account,
profiles,
@ -484,6 +522,7 @@ export const useNostrStore = defineStore('nostr', () => {
logout,
sendMessage,
subscribeToMessages,
unsubscribeFromMessages
unsubscribeFromMessages,
loadProfiles
}
})