🚀 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:
parent
a97ed1746d
commit
9b09ddbce2
19 changed files with 1402 additions and 611 deletions
|
@ -25,8 +25,7 @@
|
|||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (timeout)
|
||||
clearTimeout(timeout);
|
||||
if (timeout) clearTimeout(timeout);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -38,4 +37,5 @@
|
|||
img {
|
||||
place-self: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
65
src/components/LetterPreview.svelte
Normal file
65
src/components/LetterPreview.svelte
Normal 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)}
|
||||
|
||||
<Icon icon="ph:clock-duotone" />
|
||||
{getReadableTime(letter.date)}
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
|
@ -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>
|
||||
|
|
|
@ -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)}
|
||||
|
||||
<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>
|
98
src/components/MoveFolderDialog.svelte
Normal file
98
src/components/MoveFolderDialog.svelte
Normal 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}
|
Loading…
Add table
Add a link
Reference in a new issue