diff --git a/src/lib/utils.svelte.ts b/src/lib/utils.svelte.ts index 04a465b..dd195a2 100644 --- a/src/lib/utils.svelte.ts +++ b/src/lib/utils.svelte.ts @@ -1,14 +1,14 @@ import NDK, { - NDKEvent, - type NDKEventId, - NDKKind, - NDKPrivateKeySigner, - type NDKUser + NDKEvent, + type NDKEventId, + NDKKind, + NDKPrivateKeySigner, + type NDKUser } from '@nostr-dev-kit/ndk'; import { - dateFormat as dateFormatStore, - ndk as ndkStore, - timeFormat as timeFormatStore + dateFormat as dateFormatStore, + ndk as ndkStore, + timeFormat as timeFormatStore } from './stores.svelte'; import { generateSecretKey } from 'nostr-tools'; import { Letter } from '$lib/letter'; @@ -23,195 +23,202 @@ dateFormatStore.subscribe((d: string) => (dateFormat = d)); timeFormatStore.subscribe((t: string) => (timeFormat = t)); async function waitForNDK() { - if (ndk) return; - await new Promise((resolve) => setTimeout(resolve, 1000)); - await waitForNDK(); + if (ndk) return; + await new Promise((resolve) => setTimeout(resolve, 1000)); + await waitForNDK(); } export function randomTimeUpTo2DaysInThePast() { - const now = Date.now(); - const twoDaysAgo = now - 2 * 24 * 60 * 60 * 1000 - 3600 * 1000; // 1 hour buffer in case of clock skew - return Math.floor((Math.floor(Math.random() * (now - twoDaysAgo)) + twoDaysAgo) / 1000); + const now = Date.now(); + const twoDaysAgo = now - 2 * 24 * 60 * 60 * 1000 - 3600 * 1000; // 1 hour buffer in case of clock skew + return Math.floor((Math.floor(Math.random() * (now - twoDaysAgo)) + twoDaysAgo) / 1000); } export async function decryptSealedMessage(message: NDKEvent): Promise { - await waitForNDK(); - const sealedMessage = JSON.parse(await ndk.signer!.nip44Decrypt(message.author, message.content)); - const author = ndk.getUser({ pubkey: sealedMessage.pubkey }); - const msg = JSON.parse(await ndk.signer!.nip44Decrypt(author, sealedMessage.content)); - const event = new NDKEvent(ndk, msg); - if (event.pubkey === '') event.pubkey = author.pubkey; - return event; + await waitForNDK(); + const sealedMessage = JSON.parse(await ndk.signer!.nip44Decrypt(message.author, message.content)); + const author = ndk.getUser({ pubkey: sealedMessage.pubkey }); + const msg = JSON.parse(await ndk.signer!.nip44Decrypt(author, sealedMessage.content)); + const event = new NDKEvent(ndk, msg); + if (event.pubkey === '') event.pubkey = author.pubkey; + return event; } export async function decryptSealedMessageIntoReadableType( - encryptedMessage: NDKEvent + encryptedMessage: NDKEvent ): Promise { - await waitForNDK(); - let rawDecrypted = await decryptSealedMessage(encryptedMessage); - switch (rawDecrypted.kind) { - case NDKKind.Article: - return getLetterFromDecryptedMessage(rawDecrypted, encryptedMessage); - } + await waitForNDK(); + let rawDecrypted = await decryptSealedMessage(encryptedMessage); + switch (rawDecrypted.kind) { + case NDKKind.Article: + return getLetterFromDecryptedMessage(rawDecrypted, encryptedMessage); + } } export async function moveMessageToFolder(id: NDKEventId, folder: NDKEventId | string) { - if (folder === 'sent') throw new Error('Cannot move message to sent folder'); - await waitForNDK(); - const user = await ndk.signer!.user(); - const rawMessage = new NDKEvent(); - rawMessage.author = user; - rawMessage.created_at = Math.ceil(Date.now() / 1000); - rawMessage.kind = NDKKind.Label; - rawMessage.content = ''; - rawMessage.tags.push(['label-type', 'letter-to-folder-mapping']); - rawMessage.tags.push(['message', id]); - rawMessage.tags.push(['folder', folder]); - return encryptEventForRecipient(rawMessage, user); + if (folder === 'sent') throw new Error('Cannot move message to sent folder'); + await waitForNDK(); + const user = await ndk.signer!.user(); + const rawMessage = new NDKEvent(); + rawMessage.author = user; + rawMessage.created_at = Math.ceil(Date.now() / 1000); + rawMessage.kind = NDKKind.Label; + rawMessage.content = ''; + rawMessage.tags.push(['label-type', 'letter-to-folder-mapping']); + rawMessage.tags.push(['message', id]); + rawMessage.tags.push(['folder', folder]); + return encryptEventForRecipient(rawMessage, user); } export async function createFolder(name: string, icon: string) { - await waitForNDK(); - const allFolders = await FolderLabel.getAll(ndk); - const newFolder = { - id: crypto.randomUUID(), - name: name, - icon: icon - }; - allFolders.push(newFolder); - await FolderLabel.save(ndk, allFolders); - return newFolder; + await waitForNDK(); + const allFolders = await FolderLabel.getAll(ndk); + const newFolder = FolderLabel.fromJSON({ + id: crypto.randomUUID(), + name: name, + icon: icon + }); + allFolders.push(newFolder); + await FolderLabel.save(ndk, allFolders); + return newFolder; +} + +export async function deleteFolder(id: string) { + await waitForNDK(); + const allFolders = await FolderLabel.getAll(ndk); + allFolders.splice(allFolders.findIndex(f => f.id === id), 1); + await FolderLabel.save(ndk, allFolders); } export async function createSealedLetter( - from: NDKUser, - to: NDKUser, - subject: string, - content: string, - replyTo?: string, - stamp?: string + from: NDKUser, + to: NDKUser, + subject: string, + content: string, + replyTo?: string, + stamp?: string ) { - await waitForNDK(); - const rawMessage = new NDKEvent(); - rawMessage.author = from; - rawMessage.created_at = Math.ceil(Date.now() / 1000); - rawMessage.kind = NDKKind.Article; - rawMessage.content = content; - rawMessage.tags.push(['subject', subject]); - if (typeof replyTo !== 'undefined' && replyTo) rawMessage.tags.push(['e', replyTo, 'reply']); - if (stamp) { - const encryptedStamp = await ndk.signer!.nip44Encrypt(to, stamp); - rawMessage.tags.push(['stamp', encryptedStamp]); - } - rawMessage.tags.push(['p', to.pubkey]); - return encryptEventForRecipient(rawMessage, to); + await waitForNDK(); + const rawMessage = new NDKEvent(); + rawMessage.author = from; + rawMessage.created_at = Math.ceil(Date.now() / 1000); + rawMessage.kind = NDKKind.Article; + rawMessage.content = content; + rawMessage.tags.push(['subject', subject]); + if (typeof replyTo !== 'undefined' && replyTo) rawMessage.tags.push(['e', replyTo, 'reply']); + if (stamp) { + const encryptedStamp = await ndk.signer!.nip44Encrypt(to, stamp); + rawMessage.tags.push(['stamp', encryptedStamp]); + } + rawMessage.tags.push(['p', to.pubkey]); + return encryptEventForRecipient(rawMessage, to); } export async function createCarbonCopyLetter( - sender: NDKUser, - recipients: NDKUser[], - subject: string, - content: string, - replyTo?: string + sender: NDKUser, + recipients: NDKUser[], + subject: string, + content: string, + replyTo?: string ) { - await waitForNDK(); - const rawMessage = new NDKEvent(); - rawMessage.author = sender; - rawMessage.created_at = Math.ceil(Date.now() / 1000); - rawMessage.kind = NDKKind.Article; - rawMessage.content = content; - rawMessage.tags.push(['subject', subject]); - if (typeof replyTo !== 'undefined' && replyTo) rawMessage.tags.push(['e', replyTo, 'reply']); - for (const recipient of recipients) rawMessage.tags.push(['p', recipient.pubkey]); - return encryptEventForRecipient(rawMessage, sender); + await waitForNDK(); + const rawMessage = new NDKEvent(); + rawMessage.author = sender; + rawMessage.created_at = Math.ceil(Date.now() / 1000); + rawMessage.kind = NDKKind.Article; + rawMessage.content = content; + rawMessage.tags.push(['subject', subject]); + if (typeof replyTo !== 'undefined' && replyTo) rawMessage.tags.push(['e', replyTo, 'reply']); + for (const recipient of recipients) rawMessage.tags.push(['p', recipient.pubkey]); + return encryptEventForRecipient(rawMessage, sender); } export async function encryptEventForRecipient( - event: NDKEvent, - recipient: NDKUser + event: NDKEvent, + recipient: NDKUser ): Promise { - await waitForNDK(); - let randomKey = generateSecretKey(); - const randomKeySinger = new NDKPrivateKeySigner(randomKey); - const seal = new NDKEvent(); - seal.pubkey = recipient.pubkey; - seal.kind = 13; - seal.content = await ndk.signer!.nip44Encrypt(recipient, JSON.stringify(event)); - seal.created_at = randomTimeUpTo2DaysInThePast(); - await seal.sign(ndk.signer); - const giftWrap = new NDKEvent(); - giftWrap.kind = 1059; - giftWrap.created_at = randomTimeUpTo2DaysInThePast(); - giftWrap.content = await randomKeySinger.nip44Encrypt(recipient, JSON.stringify(seal)); - giftWrap.tags.push(['p', recipient.pubkey]); - await giftWrap.sign(randomKeySinger); - giftWrap.ndk = ndk; - return giftWrap; + await waitForNDK(); + let randomKey = generateSecretKey(); + const randomKeySinger = new NDKPrivateKeySigner(randomKey); + const seal = new NDKEvent(); + seal.pubkey = recipient.pubkey; + seal.kind = 13; + seal.content = await ndk.signer!.nip44Encrypt(recipient, JSON.stringify(event)); + seal.created_at = randomTimeUpTo2DaysInThePast(); + await seal.sign(ndk.signer); + const giftWrap = new NDKEvent(); + giftWrap.kind = 1059; + giftWrap.created_at = randomTimeUpTo2DaysInThePast(); + giftWrap.content = await randomKeySinger.nip44Encrypt(recipient, JSON.stringify(seal)); + giftWrap.tags.push(['p', recipient.pubkey]); + await giftWrap.sign(randomKeySinger); + giftWrap.ndk = ndk; + return giftWrap; } export function isValidNip05(nip05: string): boolean { - let parts = nip05.split('@'); - if (parts.length !== 2) return false; - let domain = parts[1]; - return domain.includes('.'); + let parts = nip05.split('@'); + if (parts.length !== 2) return false; + let domain = parts[1]; + return domain.includes('.'); } let letterCache: { - [id: string]: Letter; + [id: string]: Letter; } = $state({}); export async function getLetterFromDecryptedMessage( - msg: NDKEvent, - encryptedMessage: NDKEvent + msg: NDKEvent, + encryptedMessage: NDKEvent ): Promise { - if (letterCache[encryptedMessage.id]) return letterCache[encryptedMessage.id]; - await waitForNDK(); - if (msg.kind != NDKKind.Article) return; - letterCache[encryptedMessage.id] = await Letter.fromDecryptedMessage(msg, encryptedMessage, ndk); - return letterCache[encryptedMessage.id]; + if (letterCache[encryptedMessage.id]) return letterCache[encryptedMessage.id]; + await waitForNDK(); + if (msg.kind != NDKKind.Article) return; + letterCache[encryptedMessage.id] = await Letter.fromDecryptedMessage(msg, encryptedMessage, ndk); + return letterCache[encryptedMessage.id]; } export function getReadableDate(date: Date): string { - const map = { - y: date.getFullYear(), - m: String(date.getMonth() + 1).padStart(2, '0'), - d: String(date.getDate()).padStart(2, '0') - }; - return dateFormat.replace(/[ymd]/g, (char) => map[char]); + const map = { + y: date.getFullYear(), + m: String(date.getMonth() + 1).padStart(2, '0'), + d: String(date.getDate()).padStart(2, '0') + }; + return dateFormat.replace(/[ymd]/g, (char) => map[char]); } export function getReadableTime(date: Date) { - let hours = date.getHours(); - const minutes = String(date.getMinutes()).padStart(2, '0'); - const seconds = String(date.getSeconds()).padStart(2, '0'); + let hours = date.getHours(); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); - const use12Hour = timeFormat.includes('a'); - let period = ''; + const use12Hour = timeFormat.includes('a'); + let period = ''; - if (use12Hour) { - period = hours >= 12 ? 'PM' : 'AM'; - hours = hours % 12; - hours = hours ? hours : 12; - } + if (use12Hour) { + period = hours >= 12 ? 'PM' : 'AM'; + hours = hours % 12; + hours = hours ? hours : 12; + } - const map = { - h: String(hours).padStart(2, '0'), - m: minutes, - s: seconds, - a: period - }; + const map = { + h: String(hours).padStart(2, '0'), + m: minutes, + s: seconds, + a: period + }; - return timeFormat.replace(/[hmsa]/g, (char) => map[char]); + return timeFormat.replace(/[hmsa]/g, (char) => map[char]); } export function appendToBody(node: HTMLElement) { - document.body.appendChild(node); + document.body.appendChild(node); - return { - destroy() { - if (node.parentNode) { - node.parentNode.removeChild(node); - } - } - }; + return { + destroy() { + if (node.parentNode) { + node.parentNode.removeChild(node); + } + } + }; } diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index 163abd4..4bd1815 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -15,6 +15,7 @@ import Select from '../../components/Select.svelte'; import SettingsLine from './SettingsLine.svelte'; import SubscriptionSettings from './SubscriptionSettings.svelte'; + import FolderManagement from './FolderManagement.svelte'; onMount(async () => { $pageTitle = 'Settings'; @@ -30,6 +31,10 @@ + + + + + + + + + + + +{/if} + +