folder management + cleanup

This commit is contained in:
Danny Morabito 2024-12-04 17:08:17 +01:00
parent e959809a0e
commit a97ed1746d
Signed by: dannym
GPG key ID: 7CC8056A5A04557E
3 changed files with 273 additions and 147 deletions

View file

@ -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<NDKEvent> {
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<Letter | undefined> {
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<NDKEvent> {
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<Letter | undefined> {
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);
}
}
};
}

View file

@ -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 @@
<SubscriptionSettings />
</SettingsLine>
<SettingsLine title="Folder Management">
<FolderManagement />
</SettingsLine>
<SettingsLine title="Sorting and Grouping">
<Checkbox bind:checked={$groupByStamps} label="Group by stamps" />
<Select

View file

@ -0,0 +1,114 @@
<script lang="ts">
import { FolderLabel } from '$lib/folderLabel';
import { ndk } from '$lib/stores.svelte';
import { createFolder, deleteFolder } from '$lib/utils.svelte';
import { onMount } from 'svelte';
import Icon from '@iconify/svelte';
import IconButton from '../../components/IconButton.svelte';
let loading = $state(true);
let folders = $state<FolderLabel[]>([]);
let newFolderName = $state('');
async function loadFolders() {
loading = true;
folders = await FolderLabel.getAll($ndk);
loading = false;
}
async function handleDelete(folderId: string) {
loading = true;
await deleteFolder(folderId);
await loadFolders();
}
async function handleAdd() {
if (!newFolderName.trim()) return;
loading = true;
await createFolder(newFolderName, '');
newFolderName = '';
await loadFolders();
}
onMount(loadFolders);
</script>
{#if loading}
<div class="loading">
<Icon icon="eos-icons:loading" width="5em" />
</div>
{:else}
<table>
<tbody>
{#each folders as folder}
<tr>
<td class="icon-cell">
<Icon icon={folder.icon} width="1.5em" />
</td>
<td class="name-cell">{folder.name}</td>
<td class="action-cell">
<button onclick={() => handleDelete(folder.id)}>
<Icon icon="icon-park-twotone:delete" width="1.5em" />
</button>
</td>
</tr>
{/each}
<tr class="add-folder">
<td class="input-container">
<input type="text" bind:value={newFolderName} placeholder="New folder name" />
</td>
<td class="button-cell">
<IconButton icon="material-symbols:add" text="Add" on:click={handleAdd} />
</td>
</tr>
</tbody>
</table>
{/if}
<style>
.loading {
display: flex;
justify-content: center;
align-items: center;
}
table {
width: 100%;
}
tr {
display: flex;
align-items: center;
gap: 1rem;
width: 100%;
}
.icon-cell {
display: flex;
width: 1.5em;
height: 1.5em;
}
.name-cell {
flex: 1;
}
.action-cell {
display: flex;
justify-content: flex-end;
}
.add-folder {
margin-top: 1rem;
}
.input-container {
flex: 1;
}
.button-cell {
display: flex;
justify-content: flex-end;
padding-left: 1rem;
}
</style>