🚀 Turbocharged Performance 🚀

💪 Major speed boosts and overhaul of letter fetching and decrypting for users with large collections or slower devices.

🧹 Minor code style tweaks and fixes for a cleaner, more polished codebase.

📝 License Update 📝

Added AGPL3 license file to ensure transparency and freedom for our users.
This commit is contained in:
Danny Morabito 2024-12-05 19:09:34 +01:00
parent a97ed1746d
commit 9b09ddbce2
Signed by: dannym
GPG key ID: 7CC8056A5A04557E
19 changed files with 1402 additions and 611 deletions

View file

@ -25,8 +25,7 @@
});
onDestroy(() => {
if (timeout)
clearTimeout(timeout);
if (timeout) clearTimeout(timeout);
});
</script>
@ -38,4 +37,5 @@
img {
place-self: center;
}
</style>
</style>

View file

@ -7,13 +7,10 @@
<label
class="checkbox-container"
class:is-hovering={isHovering}
onpointerenter={() => isHovering = true}
onpointerleave={() => isHovering = false}
onpointerenter={() => (isHovering = true)}
onpointerleave={() => (isHovering = false)}
>
<input
bind:checked
type="checkbox"
/>
<input bind:checked type="checkbox" />
<div class="checkbox-wrapper">
<div class="checkbox">
<div class="checkbox-glass"></div>
@ -104,11 +101,7 @@
.glow-effect {
position: absolute;
inset: -20%;
background: radial-gradient(
circle at 50% 50%,
var(--glass-glow) 0%,
transparent 60%
);
background: radial-gradient(circle at 50% 50%, var(--glass-glow) 0%, transparent 60%);
opacity: 0;
transition: opacity 0.3s;
}
@ -194,4 +187,5 @@
transition: none !important;
}
}
</style>
</style>

View file

@ -13,18 +13,8 @@
</script>
<div class="controls">
<input
bind:value
class="slider"
{max}
{min}
style:--track-gradient={gradient}
type="range"
/>
<div
class="color-square"
style:background-color={backgroundColor}
>
<input bind:value class="slider" {max} {min} style:--track-gradient={gradient} type="range" />
<div class="color-square" style:background-color={backgroundColor}>
<div class="square-shine"></div>
<div class="glow"></div>
</div>
@ -103,11 +93,7 @@
.glow {
position: absolute;
inset: 0;
background: radial-gradient(
circle at 50% 50%,
rgba(255, 255, 255, 0.2),
transparent 70%
);
background: radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.2), transparent 70%);
animation: pulse 3s infinite alternate;
}
</style>

View file

@ -31,11 +31,7 @@
<li><b>m</b>: month (01-12)</li>
<li><b>d</b>: day (01-31)</li>
</ul>
<input
bind:value={$dateFormat}
placeholder="y-m-d"
type="text"
/>
<input bind:value={$dateFormat} placeholder="y-m-d" type="text" />
<div class="preview">Preview: {previewDate}</div>
</div>
@ -48,11 +44,7 @@
<li><b>s</b>: second (00-59)</li>
<li><b>a</b>: adds AM/PM and switches to 12h format</li>
</ul>
<input
bind:value={$timeFormat}
placeholder="h:m:s"
type="text"
/>
<input bind:value={$timeFormat} placeholder="h:m:s" type="text" />
<div class="preview">Preview: {previewTime}</div>
</div>
</div>

View file

@ -0,0 +1,65 @@
<script lang="ts">
import { type PartialLetter, partialLetterStatus } from '$lib/letterHandler.svelte';
import { slide } from 'svelte/transition';
import Icon from '@iconify/svelte';
import { getReadableDate, getReadableTime } from '$lib/utils.svelte';
import type { FolderLabel } from '$lib/folderLabel';
import NostrIdentifier from './NostrIdentifier.svelte';
import { goto } from '$app/navigation';
let {
partialLetter,
activeFolder,
selected = false,
onSelection = null
} = $props<{
partialLetter: PartialLetter;
activeFolder: FolderLabel;
selected?: boolean;
onSelection?: (() => void) | null;
}>();
function linkClicked() {
if (!onSelection) return goto('/letters/' + partialLetter.id);
onSelection();
return false;
}
</script>
<span class="letter-item" class:selected in:slide onclick={linkClicked} out:slide>
{#if partialLetter.status === partialLetterStatus.CONTENT_LOADED}
Loading...
{:else}
{@const letter = partialLetter.event}
<div class="subject-line">
<div class="letter-subject">
{#if !letter.emailAddress}
<img src="/nostr-lock.svg" width="32" />
{/if}
{letter.subject}
</div>
{#if letter.stamp}
<div class="stamps-count">
<Icon icon="fxemoji:stampedenvelope" />
{letter.stamp.amount} sats
</div>
{/if}
</div>
<div class="letter-from">
{#if activeFolder.id === 'sent'}
{#each letter.recipients as recipient}
<NostrIdentifier user={recipient.npub} />
{/each}
{:else if letter.from}
<NostrIdentifier user={letter.from.npub} emailAddress={letter.emailAddress} />
{/if}
</div>
<div class="letter-meta">
<Icon icon="ph:calendar-duotone" />
{getReadableDate(letter.date)}
&nbsp;
<Icon icon="ph:clock-duotone" />
{getReadableTime(letter.date)}
</div>
{/if}
</span>

View file

@ -6,6 +6,7 @@
import { decode as decodeKey } from 'nostr-tools/nip19';
import { decrypt as decryptNsec, encrypt as encryptNsec } from 'nostr-tools/nip49';
import { goto } from '$app/navigation';
import Icon from '@iconify/svelte';
let ncryptsec = $state(browser ? localStorage.getItem('ncryptsec') || '' : '');
let nsec = $state('');
@ -51,8 +52,7 @@
}
function cancelLoginWithNsec() {
if (!ncryptsec)
nsec = '';
if (!ncryptsec) nsec = '';
nsecField = '';
isLoggingIn = false;
}
@ -60,8 +60,7 @@
function nsecOk() {
try {
let decoded = decodeKey(nsecField);
if (decoded.type !== 'nsec')
return alert('Invalid nsec');
if (decoded.type !== 'nsec') return alert('Invalid nsec');
nsec = nsecField;
} catch (e) {
return alert('Invalid nsec');
@ -69,8 +68,7 @@
}
onMount(() => {
if ($ndk.activeUser)
activeUser.set($ndk.activeUser);
if ($ndk.activeUser) activeUser.set($ndk.activeUser);
$pageTitle = 'Login';
$pageIcon = 'line-md:login';
});
@ -90,27 +88,25 @@
</ul>
<p class="warning-action">Only enter your nsec on devices you completely trust!</p>
<p class="warning-final">If you're not 100% sure about this, STOP and use a signing extension instead (or use
bunkers, coming soon).</p>
<p class="warning-final">
If you're not 100% sure about this, STOP and use a signing extension instead (or use bunkers,
coming soon).
</p>
</div>
{#if ncryptsec || nsec}
<h3>Password Required</h3>
<p>
First time here? Create a strong password that is AT LEAST 32 characters long.<br>
A good approach is to use 4-5 random words with numbers and symbols between them.<br>
First time here? Create a strong password that is AT LEAST 32 characters long.<br />
A good approach is to use 4-5 random words with numbers and symbols between them.<br />
Example: correct-horse9battery!staple$running<br /><br />
<a
href="https://xkcd.com/936/"
target="_blank"
rel="noopener"
class="button"
>
<a href="https://xkcd.com/936/" target="_blank" rel="noopener" class="button">
(See this XKCD comic for why this works)
</a>
</p>
<p class="warning-action">
Your password is used to encrypt your nsec. Write it down somewhere safe, it cannot be recovered.
Your password is used to encrypt your nsec. Write it down somewhere safe, it cannot be
recovered.
</p>
<input
bind:value={password}
@ -132,14 +128,16 @@
</div>
{:else}
<p>
Already have a Nostr account? Enter your nsec below.<br>
Already have a Nostr account? Enter your nsec below.<br />
Need an account? Create one using any of these popular clients:
</p>
<ul class="client-list">
<li>
<a href="https://nosta.me" target="_blank" rel="noopener">
Nosta
<span class="client-desc">Simple nostr client focused on profile creation and viewing</span>
<span class="client-desc"
>Simple nostr client focused on profile creation and viewing</span
>
</a>
</li>
<li>
@ -167,12 +165,7 @@
</a>
</li>
</ul>
<input
bind:value={nsecField}
type="password"
placeholder="nsec1..."
class="nsec-input"
/>
<input bind:value={nsecField} type="password" placeholder="nsec1..." class="nsec-input" />
<button onclick={nsecOk}>Continue</button>
{/if}
{:else}
@ -186,6 +179,50 @@
align-self: center;
}
section {
background: var(--glass-bg);
padding: var(--spacing-md);
border: 1px solid var(--glass-border);
border-radius: 8px;
box-shadow: var(--depth-shadow);
margin-bottom: var(--spacing-md);
&.center {
text-align: center;
}
h3 {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
place-self: center;
flex-direction: row;
background: none;
-webkit-background-clip: border-box;
background-clip: border-box;
color: white;
}
ul li {
list-style: none;
}
li {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: var(--spacing-sm);
transition: all 0.3s ease;
position: relative;
isolation: isolate;
margin-bottom: var(--spacing-xs);
&:hover {
margin-left: var(--spacing-md);
}
}
}
.login-warning {
display: flex;
flex-direction: column;
@ -272,7 +309,7 @@
transition: transform 0.3s ease;
&::before {
content: "⚠️";
content: '⚠️';
font-size: 1.2em;
}
@ -317,12 +354,7 @@
transform: translateX(-50%);
width: 80%;
height: 2px;
background: linear-gradient(
90deg,
transparent,
var(--chip-error),
transparent
);
background: linear-gradient(90deg, transparent, var(--chip-error), transparent);
}
}
</style>
</style>

View file

@ -1,218 +1,26 @@
<script lang="ts">
import { groupByStamps, ndk, sortBy } from '$lib/stores.svelte';
import Icon from '@iconify/svelte';
import NostrIdentifier from './NostrIdentifier.svelte';
import { getReadableDate, getReadableTime } from '$lib/utils.svelte.js';
import Dialog from './Dialog.svelte';
import Select from './Select.svelte';
import type { Letter } from '$lib/letter';
import { slide } from 'svelte/transition';
import { FolderLabel } from '$lib/folderLabel';
import '$lib/style/mailbox-folder-items.css';
import { type PartialLetter, sortedLetters } from '$lib/letterHandler.svelte';
import { onMount } from 'svelte';
import LetterPreview from './LetterPreview.svelte';
let {
letters,
foldersList = $bindable<{ id: string; name: string; icon: string }[]>(),
folder = $bindable<{ id: string; name: string; icon: string }>(),
moveFolderDialogOpen = $bindable<boolean>()
} = $props();
let { folders, activeFolder } = $props();
function formatRange(start, end) {
if (start === 0) return '0';
if (end === Infinity) return `${start}+`;
return `${start}-${end}`;
}
let letters = $state<PartialLetter[]>([]);
let sortedInbox = $derived(
letters.toSorted((a, b) => {
switch ($sortBy) {
case 'stamps':
return b.stamps - a.stamps;
case 'sender':
return a.from.localeCompare(b.from);
case 'subject':
return a.subject.localeCompare(b.subject);
case 'date':
return b.date - a.date;
}
return 0;
})
);
let groupedInbox = $derived.by(() => {
const maxStamps = Math.max(...sortedInbox.map(letter => letter.stamps));
const ranges = [
10000,
1000,
210,
100,
42,
21,
10,
0
];
if (!ranges.includes(maxStamps)) {
ranges.push(maxStamps);
ranges.sort((a, b) => b - a);
}
const groups = {};
for (let i = 0; i < ranges.length; i++) {
const start = ranges[i];
const end = ranges[i + 1] || Infinity;
const rangeKey = formatRange(start, end);
groups[rangeKey] = [];
}
sortedInbox.forEach(letter => {
for (let i = 0; i < ranges.length; i++) {
const start = ranges[i];
const end = ranges[i + 1] || Infinity;
if (letter.stamps >= start && letter.stamps < end) {
const rangeKey = formatRange(start, end);
groups[rangeKey].push(letter);
break;
}
}
});
return Object.entries(groups)
.filter(([_, letters]) => letters.length > 0)
.reverse();
onMount(async () => {
const ignoredIDs =
activeFolder.id == 'inbox' || activeFolder.id == 'sent'
? folders.flatMap((f) => f.letters)
: [];
for await (const newLetters of sortedLetters(activeFolder, ignoredIDs)) letters = newLetters;
});
let selectedLetters = $state<string[]>([]);
let moveFolderName = $state('');
function clickedLetter(letter: Letter) {
if (selectedLetters.includes(letter.id))
selectedLetters = selectedLetters.filter(id => id !== letter.id);
else
selectedLetters = [...selectedLetters, letter.id];
}
async function doMove() {
if (!moveFolderName) return alert('Please select a folder');
if (selectedLetters.length === 0) return alert('Please select at least one letter');
const folderIndexToMoveTo = foldersList.findIndex(f => f.id === moveFolderName);
if (folderIndexToMoveTo === -1) return alert('Please select a valid folder');
foldersList[folderIndexToMoveTo].letters = [...foldersList[folderIndexToMoveTo].letters, ...selectedLetters];
await FolderLabel.save($ndk, foldersList);
selectedLetters = [];
moveFolderName = '';
moveFolderDialogOpen = false;
window.location.reload();
}
</script>
{#snippet letterGroup(letters)}
<main class="letter-list">
<div class="letter-group">
{#each letters as letter}
{#key letter.id}
<a in:slide out:slide class="letter-item" href="/letters/{letter.id}">
<div class="subject-line">
<div class="letter-subject">
{#if !letter.emailAddress}
<img src="/nostr-lock.svg" width="32" />
{/if}
{letter.subject}
</div>
{#if letter.stamps}
<div class="stamps-count">
<Icon icon="fxemoji:stampedenvelope" />
{letter.stamps} sats
</div>
{/if}
</div>
<div class="letter-from">
{#if folder.id === 'sent'}
{#each letter.recipients as recipient}
<NostrIdentifier user={recipient.npub} />
{/each}
{:else}
<NostrIdentifier user={letter.from.npub} emailAddress={letter.emailAddress} />
{/if}
</div>
<div class="letter-meta">
<Icon icon="ph:calendar-duotone" />
{getReadableDate(letter.date)}
&nbsp;
<Icon icon="ph:clock-duotone" />
{getReadableTime(letter.date)}
</div>
<div class="letter-preview">{letter.preview}</div>
</a>
{/key}
<LetterPreview partialLetter={letter} {activeFolder} />
{/each}
</div>
{/snippet}
<Dialog
bind:open={moveFolderDialogOpen}
>
<div class="section-title">
<h3>Move Letters to Folder</h3>
</div>
<p class="text-secondary">Select a folder to move the selected letters to:</p>
<Select bind:value={moveFolderName} label="Select folder" options={
foldersList.filter(cf => cf.id !== folder.id && cf.id != 'sent').map(folder => ({ value: folder.id, label: folder.name }))
} />
<h4>Letters ({selectedLetters.length} selected)</h4>
<div class="letter-list">
<div class="letter-group">
{#each letters as letter}
{#key letter.id}
<div class="letter-item"
onclick={() => clickedLetter(letter)}
class:selected={selectedLetters.includes(letter.id)}
>
<div class="subject-line">
<div class="letter-subject">
<Icon icon="mdi:letter-outline" /> {letter.subject}
</div>
{#if letter.stamps}
<div class="stamps-count">
<Icon icon="fxemoji:stampedenvelope" />
{letter.stamps} sats
</div>
{/if}
</div>
<div class="letter-from">
<NostrIdentifier user={letter.from.npub} emailAddress={letter.emailAddress} />
</div>
</div>
{/key}
{/each}
</div>
</div>
<div class="button-group">
<button onclick={() => moveFolderDialogOpen = false} type="button">Cancel</button>
<button onclick={doMove} type="submit">OK</button>
</div>
</Dialog>
<main class="letter-list">
{#if $groupByStamps}
{#each groupedInbox as [range, letters]}
<div class="stamp-group">
{#if range == 0}
<h3>No stamps</h3>
{:else}
<h3>{range} sats stamp</h3>
{/if}
<div class="letter-group">
{@render letterGroup(letters)}
</div>
</div>
{/each}
{:else}
{@render letterGroup(sortedInbox)}
{/if}
</main>
</main>

View file

@ -0,0 +1,98 @@
<script lang="ts">
import { FolderLabel } from '$lib/folderLabel';
import Select from './Select.svelte';
import type { Letter } from '$lib/letter';
import { type PartialLetter, sortedLetters } from '$lib/letterHandler.svelte';
import { onMount } from 'svelte';
import LetterPreview from './LetterPreview.svelte';
import { ndk } from '$lib/stores.svelte';
let {
activeFolder,
folders,
moveFolderDialogOpen = $bindable<boolean>()
} = $props<{
activeFolder: FolderLabel;
folders: FolderLabel[];
moveFolderDialogOpen: boolean;
}>();
let letters = $state<PartialLetter[]>([]);
let selectedLetters = $state<string[]>([]);
let moveFolderName = $state('');
let foldersUserCanMoveTo = $derived(
folders.filter((f) => f.id !== activeFolder.id && f.id != 'sent')
);
function clickedLetter(letter: Letter) {
if (selectedLetters.includes(letter.id)) {
selectedLetters = selectedLetters.filter((l) => l !== letter.id);
} else {
selectedLetters = [...selectedLetters, letter.id];
}
}
async function doMove() {
const selectedFolderIndex = folders.findIndex((f) => f.id == moveFolderName);
if (selectedFolderIndex == -1) return;
const activeFolderIndex = folders.findIndex((f) => f.id == activeFolder.id);
folders[activeFolderIndex].letters = activeFolder.letters.filter(
(l) => !selectedLetters.includes(l)
);
folders[selectedFolderIndex].letters = [
...folders[selectedFolderIndex].letters,
...selectedLetters
];
await FolderLabel.save($ndk, folders);
selectedLetters = [];
moveFolderDialogOpen = false;
}
onMount(async () => {
const ignoredIDs =
activeFolder.id == 'inbox' || activeFolder.id == 'sent'
? folders.flatMap((f) => f.letters)
: [];
for await (const newLetters of sortedLetters(activeFolder, ignoredIDs)) letters = newLetters;
});
</script>
{#if moveFolderDialogOpen}
<div class="section-title">
<h3>Move Letters to Folder</h3>
</div>
<p class="text-secondary">Select a folder to move the selected letters to:</p>
<Select
bind:value={moveFolderName}
label="Select folder"
options={foldersUserCanMoveTo.map((folder) => ({ value: folder.id, label: folder.name }))}
/>
<h4>Letters ({selectedLetters.length} selected)</h4>
<div class="letter-list">
<div class="letter-group">
{#each letters as letter}
<!-- <div-->
<!-- onclick={() => clickedLetter(letter)}-->
<!-- class:selected={selectedLetters.includes(letter.id)}-->
<!-- >-->
<LetterPreview
partialLetter={letter}
{activeFolder}
selected={selectedLetters.includes(letter.id)}
onSelection={() => clickedLetter(letter)}
/>
<!-- </div>-->
{/each}
</div>
</div>
<div class="button-group">
<button onclick={() => (moveFolderDialogOpen = false)} type="button">Cancel</button>
<button onclick={doMove} type="submit">OK</button>
</div>
{/if}