commit a983fe669bcaed636691c34496a717c753ae2c25 Author: Danny Morabito Date: Wed Nov 27 20:15:51 2024 +0100 First beta diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f60ab06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +node_modules + +# Output +.output +.vercel +/.svelte-kit +/build +build.sh + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# idea +.idea \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ab78a95 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..a6ad296 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,17 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": [ + "prettier-plugin-svelte" + ], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..16e211c --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# npub.email + diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..c27c0af Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..08238bd --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "mail", + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check ." + }, + "devDependencies": { + "@iconify/svelte": "^4.0.2", + "@sveltejs/adapter-node": "^5.2.9", + "@sveltejs/adapter-auto": "^3.3.1", + "@sveltejs/kit": "^2.8.4", + "@sveltejs/vite-plugin-svelte": "^4.0.2", + "prettier": "^3.4.1", + "prettier-plugin-svelte": "^3.3.2", + "svelte": "^5.2.9", + "svelte-check": "^4.1.0", + "typescript": "^5.7.2", + "vite": "^5.4.11" + }, + "dependencies": { + "@arx/utils": "git+ssh://git@git.arx-ccn.com:222/Arx/ts-utils#v0.0.4", + "@nostr-dev-kit/ndk": "^2.10.7", + "@nostr-dev-kit/ndk-cache-dexie": "^2.5.8", + "@nostr-dev-kit/ndk-svelte": "^2.3.2" + } +} diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..3d7a966 --- /dev/null +++ b/src/app.html @@ -0,0 +1,15 @@ + + + + + + + + %sveltekit.head% + + +
+ %sveltekit.body% +
+ + diff --git a/src/components/Checkbox.svelte b/src/components/Checkbox.svelte new file mode 100644 index 0000000..9b5f306 --- /dev/null +++ b/src/components/Checkbox.svelte @@ -0,0 +1,203 @@ + + + + + \ No newline at end of file diff --git a/src/components/ColorPicker.svelte b/src/components/ColorPicker.svelte new file mode 100644 index 0000000..d208fd7 --- /dev/null +++ b/src/components/ColorPicker.svelte @@ -0,0 +1,131 @@ + + +
+ +
+
+
+
+
+ + diff --git a/src/components/DateFormatBuilder.svelte b/src/components/DateFormatBuilder.svelte new file mode 100644 index 0000000..4ab208b --- /dev/null +++ b/src/components/DateFormatBuilder.svelte @@ -0,0 +1,86 @@ + + +
+
+ + Available format letters: +
    +
  • y: year (2024)
  • +
  • m: month (01-12)
  • +
  • d: day (01-31)
  • +
+ +
Preview: {previewDate}
+
+ +
+ + Available format letters: +
    +
  • h: hour (00-23 or 01-12 if 'a' is used)
  • +
  • m: minute (00-59)
  • +
  • s: second (00-59)
  • +
  • a: adds AM/PM and switches to 12h format
  • +
+ +
Preview: {previewTime}
+
+
+ + diff --git a/src/components/Dialog.svelte b/src/components/Dialog.svelte new file mode 100644 index 0000000..0b4a51b --- /dev/null +++ b/src/components/Dialog.svelte @@ -0,0 +1,69 @@ + + + onClose()} + use:appendToBody +> +
+ {@render children?.()} +
+
+ + diff --git a/src/components/LoginWithNostr.svelte b/src/components/LoginWithNostr.svelte new file mode 100644 index 0000000..aacc61e --- /dev/null +++ b/src/components/LoginWithNostr.svelte @@ -0,0 +1,105 @@ + + +{#if isLoggingIn} + {#if ncryptsec || nsec} + Enter your password:
+ If this is a new account make sure to remember the password in order to login later, if this is an existing account, + put in the same password you used to create the account.
+ + + {#if ncryptsec} + + {:else} + + {/if} + {:else} + If you already have a nostr account, please enter your nsec below. If you don't have an nsec please use a nostr + client to create one, such as Primal.
+
+ + {/if} +{:else} + + +{/if} + + \ No newline at end of file diff --git a/src/components/MailboxFolderItems.svelte b/src/components/MailboxFolderItems.svelte new file mode 100644 index 0000000..7842029 --- /dev/null +++ b/src/components/MailboxFolderItems.svelte @@ -0,0 +1,320 @@ + + +{#snippet letterGroup(letters)} +
+ {#each letters as letter} + +
+
+ {letter.subject} +
+ {#if letter.stamps} +
+ + {letter.stamps} sats +
+ {/if} +
+
+ {#if folder.id === 'sent'} + {#each letter.recipients as recipient} + + {/each} + {:else} + + {/if} +
+
+ + {getReadableDate(letter.date)} +   + + {getReadableTime(letter.date)} +
+
{letter.preview}
+
+ {/each} +
+{/snippet} + + moveFolderDialogOpen = false} +> +

Move Letters to Folder

+

Select a folder to move the selected letters to:

+ + isOpen = false} + onfocus={() => isOpen = true} + > + + {#each options as option} + + {/each} + + +
+
+
+
+ +
+ {value ? options.find(opt => opt.value === value)?.label : placeholder} +
+ +
+ + + +
+
+
+
+ + diff --git a/src/components/SettingsLine.svelte b/src/components/SettingsLine.svelte new file mode 100644 index 0000000..a026f7d --- /dev/null +++ b/src/components/SettingsLine.svelte @@ -0,0 +1,42 @@ + + +
+
+

{title}

+ {@render children()} +
+
+ + \ No newline at end of file diff --git a/src/components/TimeCountdown.svelte b/src/components/TimeCountdown.svelte new file mode 100644 index 0000000..08b1ffd --- /dev/null +++ b/src/components/TimeCountdown.svelte @@ -0,0 +1,80 @@ + + +
+ {#if days > 0} +
+
{days}
+
days
+
+ {/if} + {#if days > 0 || hours > 0} +
+
{hours}
+
hours
+
+ {/if} + {#if days > 0 || hours > 0 || minutes > 0} +
+
{minutes}
+
minutes
+
+ {/if} +
+
{seconds}
+
seconds
+
+
+ + \ No newline at end of file diff --git a/src/components/Tooltip.svelte b/src/components/Tooltip.svelte new file mode 100644 index 0000000..de1ff2b --- /dev/null +++ b/src/components/Tooltip.svelte @@ -0,0 +1,177 @@ + + +
+ +
+ +
+ {#if show} + + + diff --git a/src/lib/folderLabel.ts b/src/lib/folderLabel.ts new file mode 100644 index 0000000..b79c67f --- /dev/null +++ b/src/lib/folderLabel.ts @@ -0,0 +1,28 @@ +import { NDKEvent, type NDKEventId, NDKKind } from '@nostr-dev-kit/ndk'; + +export class FolderLabel { + id: NDKEventId; + name: string; + icon: string; + + constructor() { + this.id = ''; + this.name = ''; + this.icon = ''; + } + + static async fromDecryptedMessage( + decryptedMessage: NDKEvent, + encryptedMessage: NDKEvent + ): Promise { + if (decryptedMessage.kind !== NDKKind.Label) throw new Error('Not a label'); + let labelType = decryptedMessage.tags.find((t) => t[0] === 'label-type')?.[1]; + if (!labelType) throw new Error('No label type'); + if (labelType !== 'folder') throw new Error('Not a folder'); + let label = new FolderLabel(); + label.id = encryptedMessage.id; + label.name = decryptedMessage.tags.find((t) => t[0] === 'name')?.[1] ?? 'No name'; + label.icon = decryptedMessage.tags.find((t) => t[0] === 'icon')?.[1] ?? ''; + return label; + } +} diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/letter.ts b/src/lib/letter.ts new file mode 100644 index 0000000..90a7d9c --- /dev/null +++ b/src/lib/letter.ts @@ -0,0 +1,58 @@ +import NDK, { NDKEvent, type NDKEventId, NDKKind, type NDKUser } from '@nostr-dev-kit/ndk'; +import { isValidNip05 } from '$lib/utils.svelte'; + +export class Letter { + id: NDKEventId; + subject: string; + from: NDKUser; + content: string; + date: Date; + recipients: Set; + stamps: number; + emailAddress?: string; + + private constructor() { + this.id = ''; + this.subject = ''; + this.from = {} as NDKUser; + this.content = ''; + this.date = new Date(); + this.recipients = new Set(); + this.stamps = 0; + } + + static async fromDecryptedMessage( + decryptedMessage: NDKEvent, + encryptedMessage: NDKEvent, + ndk: NDK + ): Promise { + if (decryptedMessage.kind !== NDKKind.Article) throw new Error('Not a letter'); + + const letter = new Letter(); + letter.id = encryptedMessage.id; + letter.subject = decryptedMessage.tags.find((t) => t[0] === 'subject')?.[1] ?? 'No subject'; + letter.from = decryptedMessage.author; + letter.content = decryptedMessage.content; + letter.date = new Date(decryptedMessage.created_at! * 1000); + letter.emailAddress = decryptedMessage.tags.find((t) => t[0] === 'email:from')?.[1]; + + for (const tag of decryptedMessage.tags) { + if (tag[0] === 'p') { + try { + if (isValidNip05(tag[1])) { + const parsed = await ndk.getUserFromNip05(tag[1]); + if (parsed) letter.recipients.add(parsed); + } else if (tag[1].startsWith('npub')) { + letter.recipients.add(ndk.getUser({ npub: tag[1] })); + } else { + letter.recipients.add(ndk.getUser({ pubkey: tag[1] })); + } + } catch (error) { + console.error('Error processing recipient:', error); + } + } + } + + return letter; + } +} diff --git a/src/lib/letterToFolderMapping.ts b/src/lib/letterToFolderMapping.ts new file mode 100644 index 0000000..4b82ecb --- /dev/null +++ b/src/lib/letterToFolderMapping.ts @@ -0,0 +1,25 @@ +import { NDKEvent, type NDKEventId, NDKKind } from '@nostr-dev-kit/ndk'; + +export class LetterToFolderMapping { + message: NDKEventId; + folder: NDKEventId; + + constructor() { + this.message = ''; + this.folder = ''; + } + + static async fromDecryptedMessage( + decryptedMessage: NDKEvent, + encryptedMessage: NDKEvent + ): Promise { + if (decryptedMessage.kind !== NDKKind.Label) throw new Error('Not a label'); + let labelType = decryptedMessage.tags.find((t) => t[0] === 'label-type')?.[1]; + if (!labelType) throw new Error('No label type'); + if (labelType !== 'letter-to-folder-mapping') throw new Error('Not a letter-to-folder-mapping'); + let mapping = new LetterToFolderMapping(); + mapping.message = decryptedMessage.tags.find((t) => t[0] === 'message')?.[1] ?? ''; + mapping.folder = decryptedMessage.tags.find((t) => t[0] === 'folder')?.[1] ?? ''; + return mapping; + } +} diff --git a/src/lib/stores.svelte.ts b/src/lib/stores.svelte.ts new file mode 100644 index 0000000..637825c --- /dev/null +++ b/src/lib/stores.svelte.ts @@ -0,0 +1,71 @@ +import { browser } from '$app/environment'; +import { writable } from 'svelte/store'; +import { NDKNip07Signer, NDKUser } from '@nostr-dev-kit/ndk'; +import NDKSvelte from '@nostr-dev-kit/ndk-svelte'; +import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'; + +const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'arxmail-cache' }); + +const nip07signer = new NDKNip07Signer(); + +export const _ndk = new NDKSvelte({ + explicitRelayUrls: [ + 'wss://relay.primal.net', + 'wss://relay.damus.io', + 'wss://relay.nostr.band', + 'wss://offchain.pub', + 'wss://relay.snort.social' + ], + autoConnectUserRelays: false, + relayAuthDefaultPolicy: async (r) => true, + enableOutboxModel: false, + // signer: nip07signer, + cacheAdapter: dexieAdapter +}); + +if (browser && localStorage.getItem('useNip07')) _ndk.signer = nip07signer; + +export const ndk = writable(_ndk); + +export const activeUser = writable(undefined); +if (browser && _ndk.activeUser) activeUser.set(_ndk.activeUser); + +export const validSortOptions = ['stamps', 'date', 'sender', 'subject']; + +export const baseHue = writable( + browser ? parseInt(localStorage.getItem('baseHue')) || 200 : 200 +); +export const groupByStamps = writable( + browser ? localStorage.getItem('groupByStamps') === 'true' : false +); +export const sortBy = writable(browser ? localStorage.getItem('sortBy') || 'date' : 'date'); +export const dateFormat = writable( + browser ? localStorage.getItem('dateFormat') || 'y-m-d' : 'y-m-d' +); +export const timeFormat = writable( + browser ? localStorage.getItem('timeFormat') || 'h:m:s' : 'h:m:s' +); + +if (browser) { + baseHue.subscribe((value) => { + document.documentElement.style.setProperty('--base-hue', value.toString()); + localStorage.setItem('baseHue', value.toString()); + }); + + groupByStamps.subscribe((value) => { + localStorage.setItem('groupByStamps', value.toString()); + }); + + sortBy.subscribe((value) => { + if (!validSortOptions.includes(value)) value = 'date'; + localStorage.setItem('sortBy', value); + }); + + dateFormat.subscribe((value) => { + localStorage.setItem('dateFormat', value); + }); + + timeFormat.subscribe((value) => { + localStorage.setItem('timeFormat', value); + }); +} diff --git a/src/lib/utils.svelte.ts b/src/lib/utils.svelte.ts new file mode 100644 index 0000000..197b894 --- /dev/null +++ b/src/lib/utils.svelte.ts @@ -0,0 +1,244 @@ +import NDK, { + NDKEvent, + type NDKEventId, + NDKKind, + NDKPrivateKeySigner, + type NDKUser +} from '@nostr-dev-kit/ndk'; +import { + dateFormat as dateFormatStore, + ndk as ndkStore, + timeFormat as timeFormatStore +} from './stores.svelte'; +import { generateSecretKey } from 'nostr-tools'; +import { Letter } from '$lib/letter'; +import { FolderLabel } from '$lib/folderLabel'; +import { LetterToFolderMapping } from '$lib/letterToFolderMapping'; + +let ndk: NDK; +let dateFormat: string; +let timeFormat: string; + +ndkStore.subscribe((n: NDK) => (ndk = n)); +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(); +} + +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); +} + +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; +} + +export async function decryptSealedMessageIntoReadableType( + encryptedMessage: NDKEvent +): Promise { + await waitForNDK(); + let rawDecrypted = await decryptSealedMessage(encryptedMessage); + switch (rawDecrypted.kind) { + case NDKKind.Article: + return getLetterFromDecryptedMessage(rawDecrypted, encryptedMessage); + case NDKKind.Label: + let labelType = rawDecrypted.tags.find((t) => t[0] === 'label-type')?.[1]; + if (labelType === 'folder') + return getLabelFromDecryptedMessage(rawDecrypted, encryptedMessage); + if (labelType === 'letter-to-folder-mapping') + return getLetterToFolderMappingFromDecryptedMessage(rawDecrypted, encryptedMessage); + } +} + +async function getLabelFromDecryptedMessage( + msg: NDKEvent, + encryptedMessage: NDKEvent +): Promise { + await waitForNDK(); + if (msg.kind != NDKKind.Label) return; + let labelType = msg.tags.find((t) => t[0] === 'label-type')?.[1]; + if (!labelType) return; + if (labelType !== 'folder') return; + return FolderLabel.fromDecryptedMessage(msg, encryptedMessage); +} + +async function getLetterToFolderMappingFromDecryptedMessage( + msg: NDKEvent, + encryptedMessage: NDKEvent +): Promise { + await waitForNDK(); + if (msg.kind != NDKKind.Label) return; + let labelType = msg.tags.find((t) => t[0] === 'label-type')?.[1]; + if (!labelType) return; + if (labelType !== 'letter-to-folder-mapping') return; + return LetterToFolderMapping.fromDecryptedMessage(msg, 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); +} + +export async function createFolder(name: string, icon: string) { + 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', 'folder']); + rawMessage.tags.push(['name', name]); + rawMessage.tags.push(['icon', icon]); + return encryptEventForRecipient(rawMessage, user); +} + +export async function createSealedLetter( + from: NDKUser, + to: NDKUser, + subject: string, + content: string, + replyTo?: 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']); + rawMessage.tags.push(['p', to.pubkey]); + return encryptEventForRecipient(rawMessage, to); +} + +export async function createCarbonCopyLetter( + 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); +} + +export async function encryptEventForRecipient( + 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; +} + +export function isValidNip05(nip05: string): boolean { + let parts = nip05.split('@'); + if (parts.length !== 2) return false; + let domain = parts[1]; + return domain.includes('.'); +} + +let letterCache: { + [id: string]: Letter; +} = $state({}); + +export async function getLetterFromDecryptedMessage( + 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]; +} + +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]); +} + +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'); + + const use12Hour = timeFormat.includes('a'); + let period = ''; + + 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 + }; + + return timeFormat.replace(/[hmsa]/g, (char) => map[char]); +} + +export function appendToBody(node: HTMLElement) { + document.body.appendChild(node); + + return { + destroy() { + if (node.parentNode) { + node.parentNode.removeChild(node); + } + } + }; +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..bd43252 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,43 @@ + + +
+ + +
+ {#if $activeUser} + + {:else} + + {/if} +
+
\ No newline at end of file diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 0000000..a3d1578 --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1 @@ +export const ssr = false; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..3152cd0 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,189 @@ + + + + + +

Create Folder

+

Please enter a name for the new folder:

+ +
+ + +
+ + +
+
+
+ +
+ + +
+ {#if letters.length === 0} + + {:else} + f.id === activeFolder)} {letters} /> + {/if} +
+
+ + \ No newline at end of file diff --git a/src/routes/.well-known/nostr.json/+server.ts b/src/routes/.well-known/nostr.json/+server.ts new file mode 100644 index 0000000..7bd6f22 --- /dev/null +++ b/src/routes/.well-known/nostr.json/+server.ts @@ -0,0 +1,24 @@ +import { error, json } from '@sveltejs/kit'; +import { PUBLIC_API_BASE_URL } from '$env/static/public'; +import { npubToPubKeyString } from '@arx/utils/nostr.ts'; + +export async function GET({ url }: { url: URL }) { + const name = url.searchParams.get('name'); + if (!name) throw error(400, 'Name parameter is required'); + + const aliasGetURL = `${PUBLIC_API_BASE_URL}/alias/${name}`; + const aliasResponse = await fetch(aliasGetURL); + if (aliasResponse.ok) { + const npub = await aliasResponse.text(); + const hex = npubToPubKeyString(npub); + return json({ + names: { + [name]: hex + } + }); + } + const errorText = await aliasResponse.text(); + if (errorText === '') throw error(404, 'Alias not found'); + + throw error(500, errorText); +} diff --git a/src/routes/compose/+page.svelte b/src/routes/compose/+page.svelte new file mode 100644 index 0000000..75060ba --- /dev/null +++ b/src/routes/compose/+page.svelte @@ -0,0 +1,255 @@ + + +{#if letterSent} + + Letter sent to {recipients.join(', ')}. + +{/if} + +{#if isSending} +
+
+
Sending Letter...
+
+
+{:else} +
+
+
New Message
+ +
+ +
+
+ +
+ {#each recipients as r} + recipients = recipients.filter((recipient) => recipient !== r)} /> + {/each} + +
+
+ +
+ + +
+
+ +
+ +
+ +
+ +
+
+{/if} + + \ No newline at end of file diff --git a/src/routes/letters/[id]/+page.server.ts b/src/routes/letters/[id]/+page.server.ts new file mode 100644 index 0000000..3806d00 --- /dev/null +++ b/src/routes/letters/[id]/+page.server.ts @@ -0,0 +1,5 @@ +export function load({ params }) { + return { + id: params.id + }; +} diff --git a/src/routes/letters/[id]/+page.svelte b/src/routes/letters/[id]/+page.svelte new file mode 100644 index 0000000..7783be8 --- /dev/null +++ b/src/routes/letters/[id]/+page.svelte @@ -0,0 +1,272 @@ + + +{#if loading} +
+ +
+{:else if error} +
+ + {error} +
+{:else if letter} +
+
+
+
+
+ +

{letter.subject}

+
+ +
+
+ From: + +
+
+ To: +
+ {#each letter.recipients as recipient} + + {/each} +
+
+
+
+ +
+ {#if letter.stamps} +
+ + {letter.stamps} sats +
+ {/if} + +
+
+
+ + +{/if} + + diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte new file mode 100644 index 0000000..285c3a9 --- /dev/null +++ b/src/routes/settings/+page.svelte @@ -0,0 +1,261 @@ + + +

Settings

+ + + {$activeUser?.npub} + + + +

+ npub.email offers email aliases that connect to your nostr account, converting incoming emails into Letters.
+ These aliases can also serve as your nip 05 identifier. +

+ +

Pricing:

+ +
    +
  • 210 sats per day
  • +
  • Minimum purchase: 21 sats (2.4 hours)
  • +
  • Flexible duration: Purchase any length of time you need
  • +
+ +

+ Purchase time blocks to activate your email alias service for yourself or gift them to another user. Once the time + expires, you'll need to purchase additional time to continue using the service. Note: emails received while the + service is inactive will not be processed. +

+ + + {#if hasUnlimitedSubscription} +

You are officially awesome!

+

+ The Arx team has granted you an unlimited subscription to npub.email for your valuable contributions to Arx, + nostr or bitcoin.
+ Keep up your great work and thank you! +

+ {:else if subscribed && subscriptionTill.getTime() > 1000} + Your subscription will end in:
+ + {:else} + You are not currently subscribed to npub.email + {/if} +
+ + + {#if cashuTokenForBuy !== ''} + {#if tokenInfoError} +

{tokenInfoError}

+ {:else} +

{tokenInfo.amount} sats

+ + + {/if} + {/if} + + +

Buy for:

+ + + +
+ + {#if subscribed} + + {#each aliases as alias} + {alias}@npub.email + {:else} +

No aliases yet

+ {/each} + + + + +
+ {/if} +
+ + + +