aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes/(app)/messages
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2024-09-10 20:39:47 -0700
committerAdam Soltys <[email protected]>2024-09-17 09:18:36 -0700
commit1f63848ca9fb01336f04b41434de1cc06a5b3d93 (patch)
tree2affa3041863be365f33d99e1f32bda60cf93a32 /src/routes/(app)/messages
parent6e48c584605bec5623c12c470d04d07303983801 (diff)
downloadcoinos-ui-1f63848ca9fb01336f04b41434de1cc06a5b3d93.tar.gz
coinos-ui-1f63848ca9fb01336f04b41434de1cc06a5b3d93.zip
move routes
Diffstat (limited to 'src/routes/(app)/messages')
-rw-r--r--src/routes/(app)/messages/+page.server.ts42
-rw-r--r--src/routes/(app)/messages/+page.svelte92
-rw-r--r--src/routes/(app)/messages/[recipient]/+page.server.ts24
-rw-r--r--src/routes/(app)/messages/[recipient]/+page.svelte191
4 files changed, 349 insertions, 0 deletions
diff --git a/src/routes/(app)/messages/+page.server.ts b/src/routes/(app)/messages/+page.server.ts
new file mode 100644
index 00000000..94659621
--- /dev/null
+++ b/src/routes/(app)/messages/+page.server.ts
@@ -0,0 +1,42 @@
+import { get } from "$lib/utils";
+import { redirect } from "@sveltejs/kit";
+
+interface Message {
+ created_at: number;
+}
+
+interface Note {
+ created_at: number;
+ seen: number;
+}
+
+export async function load({ params, parent, }) {
+ let { user } = await parent();
+ if (user.username !== params.username) redirect(307, `/${params.username}`);
+ let { since = 0 } = params;
+
+ let messages = [],
+ notes = [];
+
+ if (user?.pubkey) {
+ let { pubkey } = user;
+ try {
+ messages = await get(`/${pubkey}/${since}/messages`);
+ messages = messages.sort((a: Message, b: Message) => b.created_at - a.created_at);
+ } catch (e) {
+ console.log(`failed to fetch nostr messages`, e);
+ }
+
+ try {
+ notes = await get(`/${pubkey}/notes`);
+ } catch (e) {
+ console.log(`failed to fetch nostr notes for ${pubkey}`, e);
+ }
+
+ notes.map((e: Note) => {
+ e.seen = e.created_at;
+ });
+ }
+
+ return { messages, notes };
+}
diff --git a/src/routes/(app)/messages/+page.svelte b/src/routes/(app)/messages/+page.svelte
new file mode 100644
index 00000000..547a1f21
--- /dev/null
+++ b/src/routes/(app)/messages/+page.svelte
@@ -0,0 +1,92 @@
+<script>
+ import { t } from "$lib/translations";
+ import Avatar from "$comp/Avatar.svelte";
+ import { event as e, password } from "$lib/store";
+ import { browser } from "$app/environment";
+ import { decrypt } from "$lib/nostr";
+
+ export let data;
+
+ let { messages, notes, invoices, sent, received, subject, user, rates } =
+ data;
+ $: refresh(data);
+ let refresh = (d) =>
+ ({ messages, notes, invoices, sent, received, subject, user, rates } = d);
+
+ let keys = new Set();
+ let latest = [];
+ let ready;
+
+ e.subscribe(async (event) => {
+ if (!(browser && event && ready)) return;
+ if (
+ event.recipient?.id === user.id &&
+ !~latest.findIndex((m) => m.id === event.id)
+ ) {
+ event.content = await decrypt({ event, user });
+
+ let i = latest.findIndex((m) => m.pubkey === event.pubkey);
+ let popped;
+ if (~i) popped = latest.splice(i, 1);
+ else popped = latest.pop();
+
+ latest.unshift(event);
+ latest = latest;
+ }
+ });
+
+ $: initialize($password);
+ let initialize = async (p) => {
+ let i = 0;
+ ready = false;
+ if (!(browser && user)) return;
+ while (i < messages.length) {
+ let event = messages[i];
+ i++;
+
+ if (!(event.author && event.recipient)) continue;
+
+ let k =
+ event.author.id === user.id
+ ? event.recipient?.pubkey
+ : event.author.pubkey;
+
+ if (!keys.has(k)) {
+ keys.add(k);
+ event.content = await decrypt({ event, user });
+ if (event.content) latest.push(event);
+ }
+ }
+
+ latest = latest;
+ ready = true;
+ };
+</script>
+
+<div class="container w-full mx-auto text-lg px-4 max-w-xl space-y-5">
+ <h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold mb-10">
+ {$t("user.messages")}
+ </h1>
+ {#if latest.length && user?.id === subject.id}
+ <div class="relative">
+ {#each latest as { content, pubkey, author, recipient }}
+ {@const user = author.id === user.id ? recipient : author}
+ <a href={`/messages/${user.username}`}>
+ <div class="flex hover:bg-gray-100 p-4 rounded-2xl">
+ <div class="my-auto">
+ <Avatar {user} size={"20"} disabled={true} />
+ </div>
+ <div class="my-auto truncate">
+ <div class="my-auto ml-1 text-lg font-bold">{user.username}</div>
+ <div class="my-auto ml-1 text-secondary text-lg truncate">
+ {content}
+ </div>
+ </div>
+ </div>
+ </a>
+ {/each}
+ </div>
+ {:else}
+ <div class="text-center">{$t("user.noMessages")}</div>
+ {/if}
+</div>
diff --git a/src/routes/(app)/messages/[recipient]/+page.server.ts b/src/routes/(app)/messages/[recipient]/+page.server.ts
new file mode 100644
index 00000000..6be9f6ec
--- /dev/null
+++ b/src/routes/(app)/messages/[recipient]/+page.server.ts
@@ -0,0 +1,24 @@
+import { get } from "$lib/utils";
+import type { Message } from "$lib/types";
+
+export async function load({ params, parent }) {
+ let { user } = await parent();
+ let { since = 0 } = params;
+
+ let messages: Message[] = [];
+ let recipient = await get(`/users/${params.recipient}`);
+
+ try {
+ messages = await get(`/${user.pubkey}/${since}/messages`);
+
+ messages = messages
+ .filter(
+ (m: Message) => m.recipient?.id === recipient.id || m.author?.id === recipient.id
+ )
+ .sort((a, b) => a.created_at - b.created_at);
+ } catch (e) {
+ console.log(`failed to fetch nostr messages`, e);
+ }
+
+ return { messages, recipient, user };
+}
diff --git a/src/routes/(app)/messages/[recipient]/+page.svelte b/src/routes/(app)/messages/[recipient]/+page.svelte
new file mode 100644
index 00000000..98638389
--- /dev/null
+++ b/src/routes/(app)/messages/[recipient]/+page.svelte
@@ -0,0 +1,191 @@
+<script>
+ import { cubicInOut } from "svelte/easing";
+ import { browser } from "$app/environment";
+ import { t } from "$lib/translations";
+ import { format } from "date-fns";
+ import { scale, fade, fly } from "svelte/transition";
+ import { enhance } from "$app/forms";
+ import Avatar from "$comp/Avatar.svelte";
+ import Icon from "$comp/Icon.svelte";
+ import { back, fail, focus } from "$lib/utils";
+ import { sign, send, encrypt, decrypt } from "$lib/nostr";
+ import { event as e, password } from "$lib/store";
+ import { tick, onMount } from "svelte";
+ import { getEventHash } from "nostr-tools";
+
+ export let data;
+
+ function fadeScale(
+ node,
+ { delay = 0, duration = 200, easing = (x) => x, baseScale = 0 }
+ ) {
+ const o = +getComputedStyle(node).opacity;
+ const m = getComputedStyle(node).transform.match(/scale\(([0-9.]+)\)/);
+ const s = m ? m[1] : 1;
+ const is = 1 - baseScale;
+
+ return {
+ delay,
+ duration,
+ css: (t) => {
+ const eased = easing(t);
+ return `opacity: ${eased * o}; transform: scale(${
+ eased * s * is + baseScale
+ })`;
+ },
+ };
+ }
+
+ let { messages, recipient, user } = data;
+ let input, pane;
+
+ $: initialize($password);
+ let initialize = async (p) => {
+ await Promise.all(
+ messages.map(
+ async (event) => (event.message = await decrypt({ event, user }))
+ )
+ );
+
+ messages = messages;
+ tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
+ };
+
+ e.subscribe(async (event) => {
+ let found = ~messages.findIndex((m) => m.id === event?.id);
+ if (event?.recipient.id === user.id && !found) {
+ event.message = await decrypt({ event, user });
+ messages.push(event);
+ messages = messages;
+ tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
+ }
+ });
+
+ 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", recipient.pubkey]],
+ };
+
+ event.message = message;
+
+ message = "";
+
+ event.author = user;
+ event.recipient = recipient;
+
+ messages.push(event);
+ messages = messages;
+ tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
+
+ try {
+ event.content = await encrypt({
+ message: event.message,
+ recipient: recipient.pubkey,
+ user,
+ });
+ event.id = getEventHash(event);
+ await sign({ event, user });
+ await send(event);
+
+ sent = true;
+ } catch (e) {
+ console.log(e);
+ fail("Failed to send message");
+ }
+
+ message = "";
+ submitting = false;
+ };
+
+ let keydown = (e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ submit();
+ }
+ };
+</script>
+
+<div class="container max-w-xl mx-auto px-4 space-y-5">
+ <button
+ type="button"
+ class="hover:opacity-80"
+ data-sveltekit-preload-data="false"
+ on:click={back}
+ >
+ <Icon icon="arrow-left" style="w-10" />
+ </button>
+
+ <div
+ class="h-[50vh] max-h-[50vh] overflow-y-scroll scrollbar-thin scrollbar-thumb-[#F2F6FC] scrollbar-track-white pr-8"
+ bind:this={pane}
+ >
+ {#each messages as { id, author, message, created_at, pubkey }}
+ {@const ours = pubkey === user.pubkey}
+ {@const theirs = !ours}
+
+ {#if message}
+ <div
+ class="flex gap-2"
+ class:flex-row-reverse={theirs}
+ class:justify-end={theirs}
+ in:fadeScale={{
+ easing: cubicInOut,
+ baseScale: 0.5,
+ }}
+ >
+ <div
+ class="rounded-2xl px-4 py-2 max-w-[300px] mb-1 text-lg"
+ class:ours
+ class:theirs
+ :key={id}
+ >
+ {message}
+ </div>
+ <div class="mt-auto">
+ <Avatar user={author} size={"12"} />
+ </div>
+ </div>
+ <div class="text-sm text-gray-400 mb-6" class:text-right={ours}>
+ {format(new Date(created_at * 1000), "MMM d")},
+ {format(new Date(created_at * 1000), "h:mm aa")}
+ </div>
+ {/if}
+ {/each}
+ </div>
+
+ <form
+ method="POST"
+ class="w-full border rounded-xl outline-none gap-4 flex p-0 pr-2"
+ on:submit|preventDefault={submit}
+ >
+ <input type="hidden" name="requester_id" value={user.id} />
+ <input type="hidden" name="recipient" value={recipient.username} />
+
+ <div
+ use:focus
+ contenteditable
+ class="grow break-all py-4 outline-none mt-0 pl-4"
+ bind:innerHTML={message}
+ on:keydown={keydown}
+ />
+ <button type="submit" class="my-auto shrink-0">
+ <Icon icon="send" style="w-8" />
+ </button>
+ </form>
+</div>
+
+<style>
+ .ours {
+ @apply bg-gradient-to-r from-[#F2F6FC] to-[#E1E3FF] text-black rounded-br-none ml-auto;
+ }
+
+ .theirs {
+ @apply bg-gray-100 text-black rounded-bl-none;
+ }
+</style>