aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/AppHeader.svelte2
-rw-r--r--src/lib/nostr.js62
-rw-r--r--src/routes/(app)/[username]/(profile)/+layout.svelte10
-rw-r--r--src/routes/(app)/[username]/(profile)/messages/+page.server.js16
-rw-r--r--src/routes/(app)/[username]/(profile)/messages/+page.svelte106
-rw-r--r--src/routes/(app)/[username]/message/+page.svelte41
6 files changed, 190 insertions, 47 deletions
diff --git a/src/components/AppHeader.svelte b/src/components/AppHeader.svelte
index dd1beb68..e5747542 100644
--- a/src/components/AppHeader.svelte
+++ b/src/components/AppHeader.svelte
@@ -100,7 +100,7 @@
<ul class="space-y-5 w-48">
{#each menuButtons as { href, icon, key }}
<li>
- <a {href}>
+ <a {href} data-sveltekit-prefetch="off">
<button class="flex justify-center items-center font-semibold hover:opacity-80"
><Icon {icon} style="mr-2 w-6 md:w-8" /> {$t(key)}
</button>
diff --git a/src/lib/nostr.js b/src/lib/nostr.js
index 048738bc..aff75c1b 100644
--- a/src/lib/nostr.js
+++ b/src/lib/nostr.js
@@ -16,7 +16,6 @@ export let generate = async (user) => {
if (!get(pin)?.length === 6) return;
-
mnemonic = generateMnemonic();
seed = mnemonicToSeedSync(mnemonic);
entropy = mnemonicToEntropy(mnemonic);
@@ -37,9 +36,60 @@ export let generate = async (user) => {
user.salt = Buffer.from(salt).toString('hex');
};
-export let sign = async ({ event, user }) => {
- let { cipher, username, salt } = user;
+export let encrypt = async ({ message, recipient, user }) => {
+ let sharedPoint = Buffer.from(
+ ecc.pointMultiply(
+ Uint8Array.from(Buffer.from('02' + recipient, 'hex')),
+ Buffer.from(await getPrivateKey(user), 'hex')
+ )
+ );
+
+ let iv = crypto.getRandomValues(new Uint8Array(16));
+ let cipher = await crypto.subtle.encrypt(
+ { name: 'AES-CBC', iv },
+ await crypto.subtle.importKey('raw', sharedPoint.slice(1), 'AES-CBC', true, [
+ 'encrypt',
+ 'decrypt'
+ ]),
+ new TextEncoder().encode(message)
+ );
+
+ return Buffer.from(cipher).toString('base64') + '?iv=' + Buffer.from(iv).toString('base64');
+};
+
+export let decrypt = async ({ event, user }) => {
+ try {
+ let { content, pubkey } = event;
+ if (pubkey === user.pubkey) pubkey = event.tags[0][1];
+ pubkey = Uint8Array.from(Buffer.from('02' + pubkey, 'hex'));
+
+ let sharedPoint = Buffer.from(
+ ecc.pointMultiply(pubkey, Buffer.from(await getPrivateKey(user), 'hex'))
+ );
+
+ let [cipher, iv] = content.split('?iv=');
+ cipher = Uint8Array.from(Buffer.from(cipher, 'base64'));
+ iv = Uint8Array.from(Buffer.from(iv, 'base64'));
+ let key = await crypto.subtle.importKey('raw', sharedPoint.slice(1), 'AES-CBC', true, [
+ 'encrypt',
+ 'decrypt'
+ ]);
+
+ let message = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, key, cipher);
+
+ return Buffer.from(message).toString('utf8');
+ } catch (e) {
+ console.log(e);
+ }
+};
+
+function typedArrayToBuffer(array) {
+ return array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset);
+}
+
+let getPrivateKey = async (user) => {
+ let { cipher, username, salt } = user;
let password = get(pw);
if (!password) {
@@ -49,7 +99,6 @@ export let sign = async ({ event, user }) => {
try {
await post('/password', { password });
} catch (e) {
- console.log(e);
passwordPrompt.set(true);
}
@@ -70,8 +119,11 @@ export let sign = async ({ event, user }) => {
seed = mnemonicToSeedSync(mnemonic);
key = bip32.fromSeed(seed);
child = key.derivePath("m/44'/1237'/0'/0/0");
- privkey = child.privateKey;
+ return child.privateKey;
+};
+export let sign = async ({ event, user }) => {
+ let privkey = await getPrivateKey(user);
event.id = await calculateId(event);
event.sig = await signId(privkey, event.id);
};
diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte
index 2d259256..98842016 100644
--- a/src/routes/(app)/[username]/(profile)/+layout.svelte
+++ b/src/routes/(app)/[username]/(profile)/+layout.svelte
@@ -83,6 +83,16 @@
</div>
{/if}
{/if}
+ <div class="w-full flex">
+ <a href={`/${subject.pubkey}/messages`} class="mx-auto">
+ <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60">
+ <div class="mx-auto flex">
+ <Icon icon="support" style="mr-1" />
+ <div>Message</div>
+ </div>
+ </button>
+ </a>
+ </div>
{#if !subject.anon}
<div class="w-full flex">
<a href={`/${subject.username}/receive`} class="mx-auto">
diff --git a/src/routes/(app)/[username]/(profile)/messages/+page.server.js b/src/routes/(app)/[username]/(profile)/messages/+page.server.js
new file mode 100644
index 00000000..2efddf77
--- /dev/null
+++ b/src/routes/(app)/[username]/(profile)/messages/+page.server.js
@@ -0,0 +1,16 @@
+import { get } from '$lib/utils';
+
+export async function load({ params, parent, url }) {
+ let { user, subject } = await parent();
+
+ let messages = [];
+
+ try {
+ messages = await get(`/nostr/${user.pubkey}/${subject.pubkey}/messages`);
+ console.log('MESSAGES', messages);
+ } catch (e) {
+ console.log(`failed to fetch nostr messages`, e);
+ }
+
+ return { messages };
+}
diff --git a/src/routes/(app)/[username]/(profile)/messages/+page.svelte b/src/routes/(app)/[username]/(profile)/messages/+page.svelte
new file mode 100644
index 00000000..3f8f5a72
--- /dev/null
+++ b/src/routes/(app)/[username]/(profile)/messages/+page.svelte
@@ -0,0 +1,106 @@
+<script>
+ import { t } from '$lib/translations';
+ import { format, parseISO } from 'date-fns';
+ import { scale } from 'svelte/transition';
+ import { enhance } from '$app/forms';
+ import { Icon } from '$comp';
+ import { back, failure } from '$lib/utils';
+ import { sign, send, encrypt, decrypt } from '$lib/nostr';
+
+ export let data;
+ export let form;
+
+ const scrollToBottom = (node) => {
+ const scroll = () =>
+ node.scroll({
+ top: node.scrollHeight,
+ behavior: 'smooth'
+ });
+ scroll();
+
+ return { update: scroll };
+ };
+
+ let { messages, subject, user } = data;
+
+ let sent, submitting, message;
+ let submit = async () => {
+ submitting = true;
+
+ let event = {
+ pubkey: user.pubkey,
+ created_at: Math.floor(Date.now() / 1000),
+ kind: 4,
+ tags: [['p', subject.pubkey]]
+ };
+
+ event.message = message;
+ messages.push(event);
+ messages = messages;
+
+ try {
+ event.content = await encrypt({ message, recipient: subject.pubkey, user });
+ await sign({ event, user });
+ await send(event);
+
+ sent = true;
+ } catch (e) {
+ failure('Failed to send message');
+ messages.pop();
+ messages = messages;
+ }
+
+ submitting = false;
+ };
+</script>
+
+<div class="container max-w-lg px-4 mx-auto mt-10 space-y-5">
+ <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold">
+ {$t('transactions.sendMessage')}
+ </h1>
+
+ <div class="max-h-[calc(50vh)] overflow-y-scroll p-4" use:scrollToBottom={messages}>
+ {#each messages as event}
+ {@const ours = event.pubkey === user.pubkey}
+ {@const theirs = !ours}
+ <div class="rounded-2xl p-4 max-w-[300px] mb-1 text-md" class:ours class:theirs>
+ {#if event.message}
+ {event.message}
+ {:else}
+ {#await decrypt({ event, user })}
+ -
+ {:then message}
+ {(event.message = message) && ''}
+ {event.message}
+ {/await}
+ {/if}
+ </div>
+ <div class="text-sm text-gray-400 ml-2 mb-6">
+ {format(new Date(event.created_at * 1000), 'MMM d')},
+ {format(new Date(event.created_at * 1000), 'h:mm aa')}
+ </div>
+ {/each}
+ </div>
+
+ <form method="POST" class="space-y-5" on:submit|preventDefault={submit}>
+ <input type="hidden" name="requester_id" value={user.id} />
+ <input type="hidden" name="recipient" value={subject.username} />
+
+ <input
+ name="message"
+ placeholder={$t('transactions.sendMessage')}
+ class="w-full p-4 border rounded-xl"
+ bind:value={message}
+ />
+ </form>
+</div>
+
+<style>
+ .ours {
+ @apply bg-gray-100 rounded-br-none ml-auto;
+ }
+
+ .theirs {
+ @apply bg-gray-200 rounded-bl-none;
+ }
+</style>
diff --git a/src/routes/(app)/[username]/message/+page.svelte b/src/routes/(app)/[username]/message/+page.svelte
deleted file mode 100644
index c8a57b79..00000000
--- a/src/routes/(app)/[username]/message/+page.svelte
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
- import { t } from '$lib/translations';
- import { enhance } from '$app/forms';
- import { Icon } from '$comp';
- import { back } from '$lib/utils';
-
- export let data;
- export let form;
-
- let { subject, user } = data;
-</script>
-
-<button class="ml-5 md:ml-20 mt-10 hover:opacity-80" on:click={back}>
- <Icon icon="arrow-left" style="w-10" />
-</button>
-
-<div class="container max-w-lg px-4 mx-auto mt-10 space-y-5">
- <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold">
- {$t('transactions.sendMessage')}
- </h1>
-
- <form action="?/create" method="POST" use:enhance class="space-y-5">
- <input type="hidden" name="requester_id" value={user.id} />
- <input type="hidden" name="recipient" value={subject.username} />
-
- <textarea
- name="memo"
- placeholder={$t('transactions.sendMessage')}
- class="w-full p-4 border rounded-xl h-48"
- />
-
- <div class="w-full flex">
- <button
- type="submit"
- class="bg-black text-white border rounded-full px-6 py-2 font-bold mx-auto"
- >
- {$t('transactions.submit')}
- </button>
- </div>
- </form>
-</div>