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([{ workbox.precacheAndRoute([{
"url": "index.html", "url": "index.html",
"revision": "0.dc5rn1lchj8" "revision": "0.36o4mscev7"
}], {}); }], {});
workbox.cleanupOutdatedCaches(); workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { 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 { KeyRound, Copy, Check, Eye, EyeOff, Loader2 } from 'lucide-vue-next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' 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' import { isValidPrivateKey, formatPrivateKey } from '@/lib/nostr'
const { t } = useI18n() const { t } = useI18n()
@ -139,7 +139,7 @@ const generateKey = () => {
privkey.value = newKey privkey.value = newKey
error.value = '' error.value = ''
showRecoveryMessage.value = true showRecoveryMessage.value = true
showKey.value = true // Show the key when generated showKey.value = false
} catch (err) { } catch (err) {
console.error('Failed to generate key:', err) console.error('Failed to generate key:', err)
error.value = t('login.error') error.value = t('login.error')

View file

@ -16,6 +16,7 @@ const input = ref('')
const isSending = ref(false) const isSending = ref(false)
const error = ref('') const error = ref('')
const messagesEndRef = ref<HTMLDivElement | null>(null) const messagesEndRef = ref<HTMLDivElement | null>(null)
const isSubscribing = ref(false)
const SUPPORT_NPUB = import.meta.env.VITE_SUPPORT_NPUB const SUPPORT_NPUB = import.meta.env.VITE_SUPPORT_NPUB
if (!SUPPORT_NPUB) { if (!SUPPORT_NPUB) {
@ -71,11 +72,22 @@ onMounted(async () => {
const supportPubkeyHex = npubToHex(SUPPORT_NPUB) const supportPubkeyHex = npubToHex(SUPPORT_NPUB)
nostrStore.activeChat = supportPubkeyHex 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() scrollToBottom()
} catch (err) { } catch (err) {
console.error('Failed to initialize support chat:', err) console.debug('Support chat setup error:', err)
error.value = 'Failed to connect to support. Please try again later.' // Continue anyway
} }
}) })
@ -90,10 +102,13 @@ onUnmounted(() => {
watch(() => nostrStore.activeChat, async (newChat) => { watch(() => nostrStore.activeChat, async (newChat) => {
if (newChat) { if (newChat) {
try { try {
isSubscribing.value = true
await nostrStore.subscribeToMessages() await nostrStore.subscribeToMessages()
} catch (err) { } catch (err) {
console.error('Failed to subscribe to messages:', err) console.debug('Chat subscription error:', err)
error.value = 'Failed to connect to chat. Please try again later.' // Continue anyway
} finally {
isSubscribing.value = false
} }
} }
}) })
@ -235,6 +250,12 @@ const getMessageGroupClasses = (sent: boolean) => {
</Button> </Button>
</form> </form>
</CardFooter> </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> </Card>
</template> </template>

View file

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

View file

@ -38,9 +38,6 @@ const DEFAULT_RELAYS = [
'wss://nostr.atitlan.io' 'wss://nostr.atitlan.io'
] ]
// Get support agent's public key from environment variable
const SUPPORT_NPUB = import.meta.env.VITE_SUPPORT_NPUB
// Helper functions // Helper functions
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number = 10000): Promise<T> { async function withTimeout<T>(promise: Promise<T>, timeoutMs: number = 10000): Promise<T> {
return Promise.race([ return Promise.race([
@ -132,54 +129,33 @@ export const useNostrStore = defineStore('nostr', () => {
profiles.value.clear() profiles.value.clear()
processedMessageIds.value.clear() processedMessageIds.value.clear()
// Close existing connections // Connect to relays
relayPool.value.forEach(relay => { relayPool.value = (await Promise.all(
try { account.value.relays.map(async relay => {
relay.close() console.log('Connecting to relay:', relay.url)
} catch (err) { const connection = await connectToRelay(relay.url)
console.error('Error closing relay:', err) if (!connection) {
} console.error('Failed to connect to relay:', relay.url)
}) }
relayPool.value = [] return connection
})
// Connect to relays with timeout )).filter((relay): relay is any => relay !== null)
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)
if (relayPool.value.length === 0) { if (relayPool.value.length === 0) {
throw new Error('Failed to connect to any relays') throw new Error('Failed to connect to any relays')
} }
// Set active chat to support agent first // Setup visibility change handler
activeChat.value = SUPPORT_NPUB setupVisibilityHandler()
// Subscribe to messages with shorter timeout // Subscribe to messages in the background
try { subscribeToMessages().catch(err => {
await withTimeout(subscribeToMessages(), 10000) console.error('Background subscription failed:', err)
} catch (err) { })
console.error('Failed to subscribe to messages:', err)
// Continue even if subscription fails
}
// 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) { } catch (err) {
console.error('Failed to initialize:', 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 })) relays: DEFAULT_RELAYS.map(url => ({ url, read: true, write: true }))
} }
try { // Initialize connection in the background
// Initialize connection with a global timeout init().catch(err => {
await withTimeout(init(), 30000) // 30 second total timeout for the entire login process console.error('Background initialization failed:', err)
} 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)
}
})
}) })
} }
@ -301,55 +243,42 @@ export const useNostrStore = defineStore('nostr', () => {
async function subscribeToMessages() { async function subscribeToMessages() {
if (!account.value) return if (!account.value) return
// Cleanup existing subscription if any // Cleanup existing subscription
if (currentSubscription.value) { unsubscribeFromMessages()
unsubscribeFromMessages()
}
let hasReceivedMessage = false // Get timestamp from 24 hours ago
const subscriptionTimeout = setTimeout(() => { const since = Math.floor(Date.now() / 1000) - (24 * 60 * 60)
if (!hasReceivedMessage) {
console.log('No messages received, considering subscription successful')
hasReceivedMessage = true
}
}, 5000)
try { try {
const subscribeToRelay = (relay: any) => { const subscribeToRelay = (relay: any) => {
return new Promise((resolve) => { return new Promise((resolve) => {
let subs: any[] = [] const subs: any[] = []
let resolved = false let messageReceived = false
let isResolved = false
// Set a timeout for the entire subscription
const timeout = setTimeout(() => {
if (!resolved) {
console.log('Subscription timeout, but continuing...')
resolved = true
resolve(true)
}
}, 8000)
try { try {
// Filter for received messages with history console.log('Setting up subscriptions for relay...')
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
}
// Subscribe to received messages // Subscribe to received messages
const receivedSub = relay.sub([receivedFilter]) const receivedSub = relay.sub([{
kinds: [4],
'#p': [account.value!.pubkey],
since,
}])
subs.push(receivedSub) 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) => { receivedSub.on('event', async (event: NostrEvent) => {
hasReceivedMessage = true messageReceived = true
if (isResolved) return // Don't process events after resolution
try { try {
if (processedMessageIds.value.has(event.id)) return if (processedMessageIds.value.has(event.id)) return
@ -369,21 +298,15 @@ export const useNostrStore = defineStore('nostr', () => {
await addMessage(event.pubkey, dm) await addMessage(event.pubkey, dm)
processedMessageIds.value.add(event.id) processedMessageIds.value.add(event.id)
if (!profiles.value.has(event.pubkey)) {
await loadProfiles()
}
} catch (err) { } catch (err) {
console.error('Failed to decrypt received message:', err) console.error('Failed to decrypt received message:', err)
} }
}) })
// Subscribe to sent messages // Handle sent messages
const sentSub = relay.sub([sentFilter])
subs.push(sentSub)
sentSub.on('event', async (event: NostrEvent) => { sentSub.on('event', async (event: NostrEvent) => {
hasReceivedMessage = true messageReceived = true
if (isResolved) return // Don't process events after resolution
try { try {
if (processedMessageIds.value.has(event.id)) return if (processedMessageIds.value.has(event.id)) return
@ -414,50 +337,56 @@ export const useNostrStore = defineStore('nostr', () => {
// Store subscriptions for cleanup // Store subscriptions for cleanup
currentSubscription.value = { currentSubscription.value = {
unsub: () => { unsub: () => {
clearTimeout(timeout)
subs.forEach(sub => { subs.forEach(sub => {
try { try {
if (sub && typeof sub.unsub === 'function') { if (sub && typeof sub.unsub === 'function') {
sub.unsub() sub.unsub()
} }
} catch (err) { } catch (err) {
console.error('Failed to unsubscribe:', err) console.debug('Failed to unsubscribe:', err)
} }
}) })
} }
} }
// Consider subscription successful immediately // Consider subscription successful after a short delay
if (!resolved) { setTimeout(() => {
resolved = true if (!isResolved) {
resolve(true) isResolved = true
} console.debug(messageReceived ?
'Subscription successful with messages' :
'Subscription successful, no messages yet'
)
resolve(true)
}
}, 3000)
} catch (err) { } catch (err) {
console.error('Error in subscription:', err) console.debug('Error in subscription setup:', err)
if (!resolved) { if (!isResolved) {
resolved = true isResolved = true
resolve(true) resolve(false)
} }
} }
}) })
} }
// Wait for relays // Wait for all relays to set up subscriptions
await Promise.all( const results = await Promise.all(
relayPool.value.map(relay => relayPool.value.map(relay => subscribeToRelay(relay))
withTimeout(subscribeToRelay(relay), 10000)
.catch(() => true)
)
) )
clearTimeout(subscriptionTimeout) // Consider success if at least one relay worked
return true 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 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 { return {
account, account,
profiles, profiles,
@ -484,6 +522,7 @@ export const useNostrStore = defineStore('nostr', () => {
logout, logout,
sendMessage, sendMessage,
subscribeToMessages, subscribeToMessages,
unsubscribeFromMessages unsubscribeFromMessages,
loadProfiles
} }
}) })