282 lines
No EOL
7 KiB
Svelte
282 lines
No EOL
7 KiB
Svelte
<script lang="ts">
|
|
import RecipientChip from '../../components/RecipientChip.svelte';
|
|
import { onMount } from 'svelte';
|
|
import { ndk, pageIcon, pageTitle } from '$lib/stores.svelte';
|
|
import Notification from '../../components/Notification.svelte';
|
|
import { createCarbonCopyLetter, createSealedLetter } from '$lib/utils.svelte';
|
|
import type { NDKUser } from '@nostr-dev-kit/ndk';
|
|
import { TokenInfoWithMailSubscriptionDuration } from '@arx/utils';
|
|
import Icon from '@iconify/svelte';
|
|
|
|
let isSending = $state(false);
|
|
let letterSent = $state(0);
|
|
let recipients = $state([]);
|
|
let replyTo = $state('');
|
|
let toField = $state('');
|
|
let subject = $state('');
|
|
let content = $state('');
|
|
let stamp = $state('');
|
|
let tokenInfo = $state<TokenInfoWithMailSubscriptionDuration>();
|
|
let tokenInfoError = $state('');
|
|
|
|
$effect(() => {
|
|
if (isSending) $pageTitle = 'Sending...';
|
|
else $pageTitle = 'New Letter';
|
|
});
|
|
|
|
$effect(() => {
|
|
try {
|
|
tokenInfo = new TokenInfoWithMailSubscriptionDuration(stamp);
|
|
tokenInfoError = '';
|
|
} catch (e) {
|
|
tokenInfoError = (e as Error).message;
|
|
}
|
|
});
|
|
|
|
function isValidNpub(npub: string): boolean {
|
|
return npub.startsWith('npub');
|
|
}
|
|
|
|
function isPossibleNip05(nip05: string): boolean {
|
|
let parts = nip05.split('@');
|
|
if (parts.length !== 2) return false;
|
|
let domain = parts[1];
|
|
return domain.includes('.');
|
|
}
|
|
|
|
function recepientInput(e: KeyboardEvent) {
|
|
if (e.key !== 'Enter') return;
|
|
return addRecipient();
|
|
}
|
|
|
|
async function addRecipient() {
|
|
if (!isValidNpub(toField) && !isPossibleNip05(toField)) return alert('Invalid recipient');
|
|
if (isPossibleNip05(toField)) {
|
|
const user = await $ndk.getUserFromNip05(toField);
|
|
if (!user)
|
|
return alert('Invalid recipient (nip05 does not match any npub)');
|
|
}
|
|
if (recipients.includes(toField)) toField = '';
|
|
recipients = [...recipients, toField];
|
|
toField = '';
|
|
return true;
|
|
}
|
|
|
|
async function send() {
|
|
if (!confirm('Are you sure you want to send this message?')) return;
|
|
if (toField.length != 0 && !await addRecipient())
|
|
return;
|
|
if (recipients.length === 0) return alert('Please add at least one recipient');
|
|
if (subject.length === 0) return alert('Please add a subject');
|
|
if (content.length === 0) return alert('Please add a message');
|
|
let newStamp;
|
|
if (stamp) {
|
|
if (tokenInfoError) return alert(tokenInfoError);
|
|
if (!tokenInfo) return alert('Please add a valid stamp');
|
|
newStamp = await tokenInfo.receive();
|
|
stamp = '';
|
|
}
|
|
isSending = true;
|
|
try {
|
|
const notesToSend = [];
|
|
const alreadyCheckedPublicKeys: string[] = [];
|
|
const realRecipients: NDKUser[] = [];
|
|
for (let r of recipients) {
|
|
let recipient;
|
|
if (isPossibleNip05(r))
|
|
recipient = await $ndk.getUserFromNip05(r);
|
|
else
|
|
recipient = $ndk.getUser({
|
|
npub: r
|
|
});
|
|
if (!recipient) return alert(`Invalid recipient: ${r}`);
|
|
if (alreadyCheckedPublicKeys.includes(recipient.pubkey)) continue;
|
|
alreadyCheckedPublicKeys.push(recipient.pubkey);
|
|
notesToSend.push(await createSealedLetter(
|
|
$ndk.activeUser!,
|
|
recipient,
|
|
subject,
|
|
content,
|
|
replyTo,
|
|
newStamp
|
|
));
|
|
realRecipients.push(recipient);
|
|
}
|
|
notesToSend.push(await createCarbonCopyLetter(
|
|
$ndk.activeUser!,
|
|
realRecipients,
|
|
subject,
|
|
content
|
|
));
|
|
await Promise.all(notesToSend.map(n => n.publish()));
|
|
letterSent = Date.now();
|
|
} finally {
|
|
isSending = false;
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
$pageTitle = 'New Letter';
|
|
$pageIcon = 'proicons:compose';
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
if (urlParams.has('replyTo'))
|
|
replyTo = urlParams.get('replyTo');
|
|
if (urlParams.has('to'))
|
|
recipients.push(urlParams.get('to'));
|
|
if (urlParams.has('subject'))
|
|
subject = urlParams.get('subject');
|
|
if (urlParams.has('content'))
|
|
content = urlParams.get('content');
|
|
});
|
|
</script>
|
|
|
|
{#if letterSent}
|
|
<Notification title="Letter Sent" duration={5000}>
|
|
Letter sent to {recipients.join(', ')}.
|
|
</Notification>
|
|
{/if}
|
|
|
|
{#if !isSending}
|
|
<div class="compose-container">
|
|
<div class="compose-fields">
|
|
<div class="field">
|
|
<label>To:</label>
|
|
<div class="to">
|
|
{#each recipients as r}
|
|
<RecipientChip recipient={r}
|
|
onRemove={() => recipients = recipients.filter((recipient) => recipient !== r)} />
|
|
{/each}
|
|
<input
|
|
bind:value={toField}
|
|
onkeyup={recepientInput}
|
|
placeholder="Enter npub or nip05..."
|
|
type="text"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label>Subject:</label>
|
|
<input bind:value={subject} class="input" placeholder="Enter subject..." type="text">
|
|
</div>
|
|
|
|
{#if recipients.length < 2}
|
|
<div class="field">
|
|
<label>Stamp:</label>
|
|
<input bind:value={stamp} class="input" placeholder="Enter stamp..." type="text" />
|
|
</div>
|
|
|
|
{#if tokenInfoError && stamp.length > 0}
|
|
<p class="error">
|
|
{tokenInfoError}
|
|
</p>
|
|
{:else if tokenInfo && stamp.length > 0}
|
|
<p class="stamp-count">
|
|
<Icon icon="fxemoji:stampedenvelope" /> {tokenInfo.amount} sats
|
|
</p>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="compose-editor">
|
|
<textarea bind:value={content} class="editor-content"></textarea>
|
|
</div>
|
|
|
|
<div class="compose-actions">
|
|
<button class="send-button" onclick={send}>Send</button>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.compose-container {
|
|
display: grid;
|
|
grid-template-rows: auto auto 1fr auto;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.compose-fields {
|
|
display: grid;
|
|
gap: var(--spacing-sm);
|
|
|
|
& .field {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: 80px 1fr;
|
|
place-items: end;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
background: var(--glass-bg);
|
|
border: 1px solid var(--glass-border);
|
|
padding: var(--spacing-md);
|
|
|
|
& .to {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-sm);
|
|
align-items: center;
|
|
width: 100%;
|
|
}
|
|
|
|
& label {
|
|
font-weight: bolder;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
& input {
|
|
width: 100%;
|
|
flex-basis: 100%;
|
|
}
|
|
}
|
|
}
|
|
|
|
.compose-editor {
|
|
position: relative;
|
|
background: var(--editor-bg);
|
|
border-radius: 12px;
|
|
border: 1px solid var(--input-border);
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
|
|
&:focus-within {
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px var(--input-focus);
|
|
}
|
|
|
|
& .editor-content {
|
|
background: transparent;
|
|
resize: none;
|
|
border: 0;
|
|
width: 100%;
|
|
padding: var(--spacing-md);
|
|
min-height: 300px;
|
|
}
|
|
}
|
|
|
|
.compose-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
@layer components {
|
|
input {
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.send-button {
|
|
background: var(--accent);
|
|
color: white;
|
|
padding: 0.75rem 2rem;
|
|
border-radius: 8px;
|
|
font-weight: 600;
|
|
transition: all 0.3s ease;
|
|
|
|
&:hover {
|
|
background: var(--accent-light);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
|
}
|
|
}
|
|
}
|
|
</style> |