lots of login dialog and nostr subsription relate updates
This commit is contained in:
parent
ed1b4cb22a
commit
d694f9b645
8 changed files with 323 additions and 189 deletions
|
|
@ -79,7 +79,7 @@ define(['./workbox-54d0af47'], (function (workbox) { 'use strict';
|
||||||
*/
|
*/
|
||||||
workbox.precacheAndRoute([{
|
workbox.precacheAndRoute([{
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.i6hkd1cnpb"
|
"revision": "0.dc5rn1lchj8"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@
|
||||||
|
|
||||||
<div class="text-center space-y-2.5">
|
<div class="text-center space-y-2.5">
|
||||||
<CardTitle class="text-2xl font-bold bg-gradient-to-r from-primary to-primary/80 inline-block text-transparent bg-clip-text">
|
<CardTitle class="text-2xl font-bold bg-gradient-to-r from-primary to-primary/80 inline-block text-transparent bg-clip-text">
|
||||||
Nostr Login
|
{{ t('login.title') }}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Login with your Nostr private key or generate a new one
|
{{ t('login.description') }}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -26,43 +26,53 @@
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Input
|
<Input
|
||||||
v-model="privkey"
|
v-model="privkey"
|
||||||
type="password"
|
:type="showKey ? 'text' : 'password'"
|
||||||
placeholder="Enter your private key"
|
:placeholder="t('login.placeholder')"
|
||||||
:class="[
|
:class="[
|
||||||
{ 'border-destructive': error },
|
{ 'border-destructive': error },
|
||||||
{ 'pr-24': privkey }, // Add padding when we have a value to prevent overlap with button
|
{ 'pr-24': privkey },
|
||||||
]"
|
]"
|
||||||
@keyup.enter="login"
|
@keyup.enter="login"
|
||||||
/>
|
/>
|
||||||
|
<div class="absolute right-1 top-1 flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
v-if="privkey"
|
v-if="privkey"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="absolute right-1 top-1 h-8"
|
class="h-8"
|
||||||
|
@click="toggleShowKey"
|
||||||
|
>
|
||||||
|
<Eye v-if="!showKey" class="h-4 w-4" />
|
||||||
|
<EyeOff v-else class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="privkey"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-8"
|
||||||
@click="copyKey"
|
@click="copyKey"
|
||||||
>
|
>
|
||||||
<Check v-if="copied" class="h-4 w-4 text-green-500" />
|
<Check v-if="copied" class="h-4 w-4 text-green-500" />
|
||||||
<Copy v-else class="h-4 w-4" />
|
<Copy v-else class="h-4 w-4" />
|
||||||
<span class="sr-only">Copy private key</span>
|
<span class="sr-only">{{ t('login.buttons.copy') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<p v-if="error" class="text-sm text-destructive">{{ error }}</p>
|
<p v-if="error" class="text-sm text-destructive">{{ error }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recovery message -->
|
<!-- Recovery message -->
|
||||||
<div v-if="showRecoveryMessage" class="text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg">
|
<div v-if="showRecoveryMessage" class="text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg">
|
||||||
<p>
|
<p>{{ t('login.recovery.message') }}</p>
|
||||||
Make sure to save your private key in a secure location. You can use it to recover your chat history on any device.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<Button @click="login" :disabled="!privkey || isLoading">
|
<Button @click="login" :disabled="!privkey || isLoading">
|
||||||
<span v-if="isLoading" class="h-4 w-4 animate-spin rounded-full border-2 border-background border-r-transparent" />
|
<Loader2 v-if="isLoading" class="h-4 w-4 animate-spin" />
|
||||||
<span v-else>Login</span>
|
<span v-else>{{ t('login.buttons.login') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" @click="generateKey">
|
<Button variant="outline" @click="generateKey" :disabled="isLoading">
|
||||||
Generate New Key
|
{{ t('login.buttons.generate') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -71,12 +81,15 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useNostrStore } from '@/stores/nostr'
|
import { useNostrStore } from '@/stores/nostr'
|
||||||
import { KeyRound, Copy, Check } 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 { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { isValidPrivateKey, formatPrivateKey } from '@/lib/nostr'
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'success'): void
|
(e: 'success'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
@ -87,25 +100,50 @@ const error = ref('')
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const copied = ref(false)
|
const copied = ref(false)
|
||||||
const showRecoveryMessage = ref(false)
|
const showRecoveryMessage = ref(false)
|
||||||
|
const showKey = ref(false)
|
||||||
|
|
||||||
|
const toggleShowKey = () => {
|
||||||
|
showKey.value = !showKey.value
|
||||||
|
}
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
if (!privkey.value) return
|
if (!privkey.value || isLoading.value) return
|
||||||
|
|
||||||
|
const formattedKey = formatPrivateKey(privkey.value)
|
||||||
|
if (!isValidPrivateKey(formattedKey)) {
|
||||||
|
error.value = t('login.error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
await nostrStore.login(privkey.value)
|
error.value = ''
|
||||||
|
await nostrStore.login(formattedKey)
|
||||||
emit('success')
|
emit('success')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Login failed:', err)
|
console.error('Login failed:', err)
|
||||||
error.value = 'Invalid private key'
|
error.value = err instanceof Error ? err.message : t('login.error')
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateKey = () => {
|
const generateKey = () => {
|
||||||
privkey.value = window.NostrTools.generatePrivateKey()
|
if (isLoading.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newKey = window.NostrTools.generatePrivateKey()
|
||||||
|
if (!isValidPrivateKey(newKey)) {
|
||||||
|
throw new Error('Generated key is invalid')
|
||||||
|
}
|
||||||
|
privkey.value = newKey
|
||||||
|
error.value = ''
|
||||||
showRecoveryMessage.value = true
|
showRecoveryMessage.value = true
|
||||||
|
showKey.value = true // Show the key when generated
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to generate key:', err)
|
||||||
|
error.value = t('login.error')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyKey = async () => {
|
const copyKey = async () => {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,20 @@ export default {
|
||||||
confirm: 'Logout'
|
confirm: 'Logout'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
login: {
|
||||||
|
title: 'Nostr Login',
|
||||||
|
description: 'Login with your Nostr private key or generate a new one',
|
||||||
|
placeholder: 'Enter your private key',
|
||||||
|
error: 'Invalid private key',
|
||||||
|
buttons: {
|
||||||
|
login: 'Login',
|
||||||
|
generate: 'Generate New Key',
|
||||||
|
copy: 'Copy private key'
|
||||||
|
},
|
||||||
|
recovery: {
|
||||||
|
message: 'Make sure to save your private key in a secure location. You can use it to recover your chat history on any device.'
|
||||||
|
}
|
||||||
|
},
|
||||||
home: {
|
home: {
|
||||||
title: 'Find Bitcoin Lightning Acceptors',
|
title: 'Find Bitcoin Lightning Acceptors',
|
||||||
subtitle: 'Discover local businesses, services, and venues that accept Bitcoin Lightning payments.',
|
subtitle: 'Discover local businesses, services, and venues that accept Bitcoin Lightning payments.',
|
||||||
|
|
|
||||||
|
|
@ -81,5 +81,19 @@ export default {
|
||||||
cancel: 'Cancelar',
|
cancel: 'Cancelar',
|
||||||
confirm: 'Cerrar Sesión'
|
confirm: 'Cerrar Sesión'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
title: 'Iniciar Sesión Nostr',
|
||||||
|
description: 'Inicia sesión con tu llave privada Nostr o genera una nueva',
|
||||||
|
placeholder: 'Ingresa tu llave privada',
|
||||||
|
error: 'Llave privada inválida',
|
||||||
|
buttons: {
|
||||||
|
login: 'Iniciar Sesión',
|
||||||
|
generate: 'Generar Nueva Llave',
|
||||||
|
copy: 'Copiar llave privada'
|
||||||
|
},
|
||||||
|
recovery: {
|
||||||
|
message: 'Asegúrate de guardar tu llave privada en un lugar seguro. Puedes usarla para recuperar tu historial de chat en cualquier dispositivo.'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,3 +106,18 @@ export function npubToHex(npub: string): string {
|
||||||
export function hexToNpub(hex: string): string {
|
export function hexToNpub(hex: string): string {
|
||||||
return window.NostrTools.nip19.npubEncode(hex)
|
return window.NostrTools.nip19.npubEncode(hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isValidPrivateKey(key: string): boolean {
|
||||||
|
try {
|
||||||
|
if (!/^[0-9a-fA-F]{64}$/.test(key)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPrivateKey(key: string): string {
|
||||||
|
return key.trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
"directory": {
|
|
||||||
"categories": {
|
|
||||||
"all": "All",
|
|
||||||
"tuktuk": "Tuk-tuk",
|
|
||||||
"restaurant": "Restaurant",
|
|
||||||
"lodging": "Lodging",
|
|
||||||
"goods": "Goods",
|
|
||||||
"services": "Services",
|
|
||||||
"taxi": "Taxi",
|
|
||||||
"lancha": "Boat",
|
|
||||||
"other": "Other"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"home": {
|
|
||||||
"title": "Find Bitcoin Lightning Acceptors",
|
|
||||||
"subtitle": "Discover local businesses, services, and venues that accept Bitcoin Lightning payments.",
|
|
||||||
"selectTown": "Select your location",
|
|
||||||
"browse": "Browse Directory",
|
|
||||||
"learnMore": "Learn More"
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
"directory": {
|
|
||||||
"categories": {
|
|
||||||
"all": "Todo",
|
|
||||||
"restaurant": "Restaurante",
|
|
||||||
"tuktuk": "Tuk-tuk",
|
|
||||||
"lodging": "Hospedaje",
|
|
||||||
"goods": "Productos",
|
|
||||||
"services": "Servicios",
|
|
||||||
"taxi": "Taxi",
|
|
||||||
"lancha": "Lancha",
|
|
||||||
"other": "Otro"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"home": {
|
|
||||||
"title": "Encuentra Aceptadores de Bitcoin Lightning",
|
|
||||||
"subtitle": "Descubre negocios locales, servicios y lugares que aceptan pagos Bitcoin Lightning.",
|
|
||||||
"selectTown": "Selecciona tu ubicación",
|
|
||||||
"browse": "Explorar Directorio",
|
|
||||||
"learnMore": "Más Información"
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import type { NostrEvent, NostrProfile, NostrAccount, DirectMessage } from '../types/nostr'
|
import type { NostrEvent, NostrProfile, NostrAccount, DirectMessage } from '../types/nostr'
|
||||||
|
import { isValidPrivateKey, formatPrivateKey } from '@/lib/nostr'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|
@ -41,12 +42,21 @@ const DEFAULT_RELAYS = [
|
||||||
const SUPPORT_NPUB = import.meta.env.VITE_SUPPORT_NPUB
|
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> {
|
||||||
|
return Promise.race([
|
||||||
|
promise,
|
||||||
|
new Promise<T>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('Operation timed out')), timeoutMs)
|
||||||
|
)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
async function connectToRelay(url: string) {
|
async function connectToRelay(url: string) {
|
||||||
console.log(`Attempting to connect to relay: ${url}`)
|
console.log(`Attempting to connect to relay: ${url}`)
|
||||||
const relay = window.NostrTools.relayInit(url)
|
const relay = window.NostrTools.relayInit(url)
|
||||||
try {
|
try {
|
||||||
console.log(`Initializing connection to ${url}...`)
|
console.log(`Initializing connection to ${url}...`)
|
||||||
await relay.connect()
|
await withTimeout(relay.connect())
|
||||||
console.log(`Successfully connected to ${url}`)
|
console.log(`Successfully connected to ${url}`)
|
||||||
return relay
|
return relay
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -114,46 +124,88 @@ export const useNostrStore = defineStore('nostr', () => {
|
||||||
|
|
||||||
// Initialize connection if account exists
|
// Initialize connection if account exists
|
||||||
async function init() {
|
async function init() {
|
||||||
if (account.value) {
|
if (!account.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
// Clear existing state
|
// Clear existing state
|
||||||
messages.value.clear()
|
messages.value.clear()
|
||||||
profiles.value.clear()
|
profiles.value.clear()
|
||||||
processedMessageIds.value.clear()
|
processedMessageIds.value.clear()
|
||||||
|
|
||||||
// Close existing connections
|
// Close existing connections
|
||||||
relayPool.value.forEach(relay => relay.close())
|
relayPool.value.forEach(relay => {
|
||||||
|
try {
|
||||||
|
relay.close()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error closing relay:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
relayPool.value = []
|
relayPool.value = []
|
||||||
|
|
||||||
// Connect to relays
|
// Connect to relays with timeout
|
||||||
const connectedRelays = await Promise.all(
|
const connectionPromises = account.value.relays.map(async relay => {
|
||||||
account.value.relays.map(relay => connectToRelay(relay.url))
|
try {
|
||||||
)
|
return await withTimeout(connectToRelay(relay.url), 5000)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Timeout connecting to ${relay.url}:`, err)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
relayPool.value = connectedRelays.filter(relay => relay !== null)
|
const connectedRelays = await Promise.all(connectionPromises)
|
||||||
|
relayPool.value = connectedRelays.filter((relay): relay is NonNullable<typeof relay> => relay !== null)
|
||||||
|
|
||||||
// Subscribe to messages and load history
|
if (relayPool.value.length === 0) {
|
||||||
await Promise.all([
|
throw new Error('Failed to connect to any relays')
|
||||||
subscribeToMessages(),
|
}
|
||||||
loadProfiles()
|
|
||||||
])
|
|
||||||
|
|
||||||
// Set active chat to support agent
|
// Set active chat to support agent first
|
||||||
activeChat.value = SUPPORT_NPUB
|
activeChat.value = SUPPORT_NPUB
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
async function login(privkey: string) {
|
async function login(privkey: string) {
|
||||||
const pubkey = window.NostrTools.getPublicKey(privkey)
|
if (!isValidPrivateKey(privkey)) {
|
||||||
|
throw new Error('Invalid private key')
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedKey = formatPrivateKey(privkey)
|
||||||
|
const pubkey = window.NostrTools.getPublicKey(formattedKey)
|
||||||
|
|
||||||
account.value = {
|
account.value = {
|
||||||
pubkey,
|
pubkey,
|
||||||
privkey,
|
privkey: formattedKey,
|
||||||
relays: DEFAULT_RELAYS.map(url => ({ url, read: true, write: true }))
|
relays: DEFAULT_RELAYS.map(url => ({ url, read: true, write: true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize connection and load messages
|
try {
|
||||||
await init()
|
// 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() {
|
async function loadProfiles() {
|
||||||
|
|
@ -254,33 +306,52 @@ export const useNostrStore = defineStore('nostr', () => {
|
||||||
unsubscribeFromMessages()
|
unsubscribeFromMessages()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hasReceivedMessage = false
|
||||||
|
const subscriptionTimeout = setTimeout(() => {
|
||||||
|
if (!hasReceivedMessage) {
|
||||||
|
console.log('No messages received, considering subscription successful')
|
||||||
|
hasReceivedMessage = true
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
try {
|
||||||
// Filter for received messages with history
|
// Filter for received messages with history
|
||||||
const receivedFilter = {
|
const receivedFilter = {
|
||||||
kinds: [4],
|
kinds: [4],
|
||||||
'#p': [account.value.pubkey],
|
'#p': [account.value!.pubkey],
|
||||||
since: 0 // Get all historical messages
|
since: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter for sent messages with history
|
// Filter for sent messages with history
|
||||||
const sentFilter = {
|
const sentFilter = {
|
||||||
kinds: [4],
|
kinds: [4],
|
||||||
authors: [account.value.pubkey],
|
authors: [account.value!.pubkey],
|
||||||
since: 0 // Get all historical messages
|
since: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscribeToRelay = (relay: any) => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
let eoseCount = 0
|
|
||||||
|
|
||||||
// Subscribe to received messages
|
// Subscribe to received messages
|
||||||
const receivedSub = relay.sub([receivedFilter])
|
const receivedSub = relay.sub([receivedFilter])
|
||||||
|
subs.push(receivedSub)
|
||||||
|
|
||||||
receivedSub.on('event', async (event: NostrEvent) => {
|
receivedSub.on('event', async (event: NostrEvent) => {
|
||||||
|
hasReceivedMessage = true
|
||||||
try {
|
try {
|
||||||
// Skip if we've already processed this message
|
if (processedMessageIds.value.has(event.id)) return
|
||||||
if (processedMessageIds.value.has(event.id)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const decrypted = await window.NostrTools.nip04.decrypt(
|
const decrypted = await window.NostrTools.nip04.decrypt(
|
||||||
account.value!.privkey,
|
account.value!.privkey,
|
||||||
|
|
@ -297,8 +368,8 @@ export const useNostrStore = defineStore('nostr', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await addMessage(event.pubkey, dm)
|
await addMessage(event.pubkey, dm)
|
||||||
|
processedMessageIds.value.add(event.id)
|
||||||
|
|
||||||
// Load profile if not already loaded
|
|
||||||
if (!profiles.value.has(event.pubkey)) {
|
if (!profiles.value.has(event.pubkey)) {
|
||||||
await loadProfiles()
|
await loadProfiles()
|
||||||
}
|
}
|
||||||
|
|
@ -309,15 +380,13 @@ export const useNostrStore = defineStore('nostr', () => {
|
||||||
|
|
||||||
// Subscribe to sent messages
|
// Subscribe to sent messages
|
||||||
const sentSub = relay.sub([sentFilter])
|
const sentSub = relay.sub([sentFilter])
|
||||||
|
subs.push(sentSub)
|
||||||
|
|
||||||
sentSub.on('event', async (event: NostrEvent) => {
|
sentSub.on('event', async (event: NostrEvent) => {
|
||||||
|
hasReceivedMessage = true
|
||||||
try {
|
try {
|
||||||
// Skip if we've already processed this message
|
if (processedMessageIds.value.has(event.id)) return
|
||||||
if (processedMessageIds.value.has(event.id)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the target pubkey from the p tag
|
|
||||||
const targetPubkey = event.tags.find(tag => tag[0] === 'p')?.[1]
|
const targetPubkey = event.tags.find(tag => tag[0] === 'p')?.[1]
|
||||||
if (!targetPubkey) return
|
if (!targetPubkey) return
|
||||||
|
|
||||||
|
|
@ -336,43 +405,69 @@ export const useNostrStore = defineStore('nostr', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await addMessage(targetPubkey, dm)
|
await addMessage(targetPubkey, dm)
|
||||||
|
processedMessageIds.value.add(event.id)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to decrypt sent message:', err)
|
console.error('Failed to decrypt sent message:', err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Listen for end of stored events
|
|
||||||
receivedSub.on('eose', () => {
|
|
||||||
eoseCount++
|
|
||||||
if (eoseCount >= 2) { // Both subscriptions have finished
|
|
||||||
resolve(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sentSub.on('eose', () => {
|
|
||||||
eoseCount++
|
|
||||||
if (eoseCount >= 2) { // Both subscriptions have finished
|
|
||||||
resolve(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Store subscriptions for cleanup
|
// Store subscriptions for cleanup
|
||||||
currentSubscription.value = {
|
currentSubscription.value = {
|
||||||
unsub: () => {
|
unsub: () => {
|
||||||
receivedSub.unsub()
|
clearTimeout(timeout)
|
||||||
sentSub.unsub()
|
subs.forEach(sub => {
|
||||||
|
try {
|
||||||
|
if (sub && typeof sub.unsub === 'function') {
|
||||||
|
sub.unsub()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to unsubscribe:', err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consider subscription successful immediately
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true
|
||||||
|
resolve(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in subscription:', err)
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true
|
||||||
|
resolve(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for all relays to load their historical messages
|
// Wait for relays
|
||||||
await Promise.all(relayPool.value.map(relay => subscribeToRelay(relay)))
|
await Promise.all(
|
||||||
|
relayPool.value.map(relay =>
|
||||||
|
withTimeout(subscribeToRelay(relay), 10000)
|
||||||
|
.catch(() => true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
clearTimeout(subscriptionTimeout)
|
||||||
|
return true
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
clearTimeout(subscriptionTimeout)
|
||||||
|
console.error('Failed to subscribe to messages:', err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsubscribeFromMessages() {
|
function unsubscribeFromMessages() {
|
||||||
if (currentSubscription.value) {
|
if (currentSubscription.value && typeof currentSubscription.value.unsub === 'function') {
|
||||||
|
try {
|
||||||
currentSubscription.value.unsub()
|
currentSubscription.value.unsub()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to unsubscribe:', err)
|
||||||
|
}
|
||||||
currentSubscription.value = null
|
currentSubscription.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue