aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--package.json1
-rw-r--r--pnpm-lock.yaml11
-rw-r--r--src/components/About.svelte13
-rw-r--r--src/lib/nostr.js11
-rw-r--r--src/lib/socket.js27
-rw-r--r--src/lib/store.js3
-rw-r--r--src/locales/en.json13
-rw-r--r--src/routes/(app)/+layout.svelte2
-rw-r--r--src/routes/(app)/[username]/(profile)/+layout.svelte2
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.server.js1
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.svelte80
-rw-r--r--src/routes/(app)/[username]/(profile)/messages/+page.svelte123
-rw-r--r--src/routes/(app)/[username]/invoice/[hash]/+page.svelte4
-rw-r--r--src/routes/(app)/[username]/messages/+page.server.js (renamed from src/routes/(app)/[username]/(profile)/messages/+page.server.js)7
-rw-r--r--src/routes/(app)/[username]/messages/+page.svelte126
-rw-r--r--src/routes/(app)/[username]/payments/[...page]/+page.svelte2
-rw-r--r--src/routes/(app)/buy/+page.svelte33
-rw-r--r--src/routes/(app)/login/+page.svelte1
-rw-r--r--src/routes/(app)/payment/[id]/+page.svelte16
-rw-r--r--tailwind.config.cjs3
20 files changed, 247 insertions, 232 deletions
diff --git a/package.json b/package.json
index 908ec0bf..ac67d8b3 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"svelte-preprocess": "^4.10.7",
"svelte-virtual-scroll-list": "^1.1.0",
"sveltekit-i18n": "^2.2.2",
+ "tailwind-scrollbar": "^2.1.0",
"tailwindcss": "^3.2.4",
"tiny-secp256k1": "^2.2.1",
"uuid": "^9.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4f0d2ef2..3fe87f18 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,6 +29,7 @@ specifiers:
svelte-preprocess: ^4.10.7
svelte-virtual-scroll-list: ^1.1.0
sveltekit-i18n: ^2.2.2
+ tailwind-scrollbar: ^2.1.0
tailwindcss: ^3.2.4
tiny-secp256k1: ^2.2.1
uuid: ^9.0.0
@@ -68,6 +69,7 @@ devDependencies:
svelte-preprocess: 4.10.7_qs7rgzvahok4cbkjoy5bgypde4
svelte-virtual-scroll-list: [email protected]
sveltekit-i18n: [email protected]
+ tailwind-scrollbar: [email protected]
tailwindcss: [email protected]
tiny-secp256k1: 2.2.1
uuid: 9.0.0
@@ -2477,6 +2479,15 @@ packages:
svelte: 3.55.0
dev: true
+ /tailwind-scrollbar/[email protected]:
+ resolution: {integrity: sha512-zpvY5mDs0130YzYjZKBiDaw32rygxk5RyJ4KmeHjGnwkvbjm/PszON1m4Bbt2DkMRIXlXsfNevykAESgURN4KA==}
+ engines: {node: '>=12.13.0'}
+ peerDependencies:
+ tailwindcss: 3.x
+ dependencies:
+ tailwindcss: [email protected]
+ dev: true
+
/tailwindcss/[email protected]:
resolution: {integrity: sha512-AhwtHCKMtR71JgeYDaswmZXhPcW9iuI9Sp2LvZPo9upDZ7231ZJ7eA9RaURbhpXGVlrjX4cFNlB4ieTetEb7hQ==}
engines: {node: '>=12.13.0'}
diff --git a/src/components/About.svelte b/src/components/About.svelte
index d51b9268..0a0ff874 100644
--- a/src/components/About.svelte
+++ b/src/components/About.svelte
@@ -18,19 +18,6 @@
socials: []
},
{
- name: 'karnage',
- socials: [
- { type: 'twitter', url: 'https://twitter.com/KarnageBitcoin', color: 'bg-[#1D9BF0]' }
- ]
- },
- {
- name: 'secondl1ght',
- socials: [
- { type: 'twitter', url: 'https://www.twitter.com/secondl1ght', color: 'bg-[#1D9BF0]' },
- { type: 'github', url: 'https://github.com/secondl1ght', color: 'bg-gray-800' }
- ]
- },
- {
name: 'shauna',
socials: [
{ type: 'twitter', url: 'https://twitter.com/ShaunaCollister', color: 'bg-[#1D9BF0]' },
diff --git a/src/lib/nostr.js b/src/lib/nostr.js
index 13251856..d72f48ef 100644
--- a/src/lib/nostr.js
+++ b/src/lib/nostr.js
@@ -5,7 +5,7 @@ import { Buffer } from 'buffer';
import { bech32m } from 'bech32';
import { bip32, fail, wait, post, stretch } from '$lib/utils';
import { calculateId, signId } from 'nostr';
-import { password as pw, passwordPrompt, pin, loginRedirect } from '$lib/store';
+import { decrypted, password as pw, passwordPrompt, pin, loginRedirect } from '$lib/store';
import { goto, invalidate } from '$app/navigation';
const { encode, decode, fromWords, toWords } = bech32m;
@@ -58,8 +58,10 @@ export let encrypt = async ({ message, recipient, user }) => {
};
export let decrypt = async ({ event, user }) => {
+ let cache = get(decrypted);
try {
- let { content, pubkey } = event;
+ let { content, pubkey, id } = event;
+ if (cache[id]) return cache[id];
if (pubkey === user.pubkey) pubkey = event.tags[0][1];
pubkey = Uint8Array.from(Buffer.from('02' + pubkey, 'hex'));
@@ -78,7 +80,10 @@ export let decrypt = async ({ event, user }) => {
let message = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, key, cipher);
- return Buffer.from(message).toString('utf8');
+ cache[id] = Buffer.from(message).toString('utf8');
+ decrypted.set(cache);
+
+ return cache[id];
} catch (e) {
// console.log(e);
}
diff --git a/src/lib/socket.js b/src/lib/socket.js
index ce3cbe4b..aef8a513 100644
--- a/src/lib/socket.js
+++ b/src/lib/socket.js
@@ -1,23 +1,11 @@
import { get } from 'svelte/store';
-import {
- events as $events,
- invoice,
- request,
- newPayment,
- last,
- rate,
- payments,
- user
-} from '$lib/store';
+import { event, invoice, request, newPayment, last, rate, payments, user } from '$lib/store';
import { success, sat } from '$lib/utils';
import { PUBLIC_SOCKET } from '$env/static/public';
import { invalidate } from '$app/navigation';
import { browser } from '$app/environment';
let socket, token;
-let le = browser && localStorage.getItem('events');
-let events = le && le !== 'undefined' ? JSON.parse(le) : {};
-let q = Object.keys(events);
export const auth = () => token && send('login', token) && send('heartbeat');
@@ -30,23 +18,12 @@ export const messages = (data) => ({
id() {
last.set(Date.now());
},
-
invoice() {
invoice.set(data);
},
-
event() {
- data.seen = Math.round(Date.now() / 1000);
- events[data.id] = data;
- q.push(data.id);
- $events.set(events);
- while (q.length > 10000) delete events[q.shift()];
- localStorage.setItem(
- 'events',
- JSON.stringify(q.slice(-100).reduce((a, b) => (a = { ...a, [b]: events[b] }), {}))
- );
+ event.set(data);
},
-
rate() {
rate.set(data);
},
diff --git a/src/lib/store.js b/src/lib/store.js
index 93ac3d47..6853386c 100644
--- a/src/lib/store.js
+++ b/src/lib/store.js
@@ -36,11 +36,12 @@ export const animatedRate = writable(0);
export const avatar = writable();
export const banner = writable();
export const colorTheme = writable('from-primary to-gradient');
+export const event = writable();
export const events = writable({});
export const invoice = writable();
export const last = writable();
export const loginRedirect = writable();
-export const messages = persistLocal('messages', {});
+export const decrypted = persistLocal('decrypted', {});
export const newPayment = persistLocal('newPayment');
export const password = persistLocal('password');
export const passwordPrompt = writable();
diff --git a/src/locales/en.json b/src/locales/en.json
index 17f4b34e..402f0214 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -15,17 +15,6 @@
"position": "Chief Technical Officer",
"bio": "Adam is a web developer and bitcoin enthusiast who believes sound money is key to increasing global prosperity. He lives in North Vancouver with his wife Chelsea and two children."
},
- "secondl1ght": {
- "name": "secondl1ght",
- "position": "Web Developer",
- "bio": "A self-taught Web Developer, secondl1ght dove head first down the bitcoin rabbit hole and left his fiat job to focus on bitcoin development full-time. He contributes to open source projects, is a privacy advocate, and freedom lover."
- },
-
- "karnage": {
- "name": "Karnage",
- "position": "Product Designer",
- "bio": "My mission is to help startup founders succeed. I create products to achieve this goal. Pixel-perfect product design. Get it shipped."
- },
"shauna": {
"name": "Shauna",
"position": "Operations & Marketing",
@@ -297,6 +286,6 @@
"redeem": "Redeem",
"redeemed": "Redeemed",
"redeemedBy": "Redeemed by",
- "sendMessage": "Message"
+ "sendMessage": "Write a message..."
}
}
diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte
index 2a122209..45469bdb 100644
--- a/src/routes/(app)/+layout.svelte
+++ b/src/routes/(app)/+layout.svelte
@@ -58,7 +58,7 @@
onDestroy(() => browser && clearTimeout(timer));
</script>
-{#if $passwordPrompt}
+{#if browser && $passwordPrompt}
<Password {user} />
{/if}
diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte
index dfa25b78..6eb2a850 100644
--- a/src/routes/(app)/[username]/(profile)/+layout.svelte
+++ b/src/routes/(app)/[username]/(profile)/+layout.svelte
@@ -111,7 +111,7 @@
{/if}
<div class="w-full flex">
- <a href={`/${subject.pubkey}/messages`} class="mx-auto">
+ <a href={`/${subject.username}/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-2 my-auto" />
diff --git a/src/routes/(app)/[username]/(profile)/+page.server.js b/src/routes/(app)/[username]/(profile)/+page.server.js
index d1fa2f41..2777afdc 100644
--- a/src/routes/(app)/[username]/(profile)/+page.server.js
+++ b/src/routes/(app)/[username]/(profile)/+page.server.js
@@ -10,6 +10,7 @@ export async function load({ cookies, params, parent, url }) {
if (user) {
try {
messages = await get(`/${user.pubkey}/${since}/messages`);
+ messages = messages.sort((a, b) => b.created_at - a.created_at);
} catch (e) {
console.log(`failed to fetch nostr messages`, e);
}
diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte
index 0421782c..bc19a9df 100644
--- a/src/routes/(app)/[username]/(profile)/+page.svelte
+++ b/src/routes/(app)/[username]/(profile)/+page.svelte
@@ -17,8 +17,8 @@
if (!browser) return;
while (latest.length < 3 && i < messages.length) {
let event = messages[i];
- let { pubkey: k } = event.user;
- if (k !== user.pubkey && !keys.has(k)) {
+ 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);
@@ -33,36 +33,44 @@
<div class="space-y-8">
{#if user?.id === subject.id}
<div>
- {#if user.balance > 0}
- <div class="flex justify-center lg:justify-start">
- <Balance {user} />
- </div>
- {:else}
- <div class="space-y-3">
- <h1 class="text-2xl">Welcome</h1>
-
- <p class="text-secondary">
- Get started by making a deposit or getting someone to pay you in Bitcoin!
+ <div class="flex justify-center lg:justify-start">
+ <Balance {user} />
+ </div>
+ {#if !user.balance}
+ <div class="space-y-8 mt-8">
+ <p class="text-secondary text-lg">
+ Welcome! Your account balance is empty. You can start by making a deposit or by asking
+ someone to pay you.
</p>
<div class="flex gap-4 justify-center w-full">
<a href={`/${user.username}/receive`}>
- <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60">
+ <button
+ class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60 bg-black text-white"
+ >
<div class="mx-auto flex">
- <Icon icon="numpad" style="my-auto h-6 mr-2" />
- <div class="my-auto mt-1">Receive a Payment</div>
+ <Icon icon="numpad" style="my-auto h-6 mr-2 invert" />
+ <div class="my-auto mt-1">Request Payment</div>
</div>
</button>
</a>
- <!-- <a href={`/buy`}> -->
- <!-- <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60"> -->
- <!-- <div class="mx-auto flex"> -->
- <!-- <Icon icon="plus" style="my-auto h-6 mr-2" /> -->
- <!-- <div class="my-auto mt-1">Buy Bitcoin</div> -->
- <!-- </div> -->
- <!-- </button> -->
- <!-- </a> -->
+ {#if user.eligible}
+ <a href={`/buy`}>
+ <button class="rounded-full border py-3 px-6 font-bold hover:opacity-80 flex w-60">
+ <div class="mx-auto flex">
+ <!-- <Icon icon="plus" style="my-auto h-6 mr-2" /> -->
+
+ <img
+ src="/images/bitcoin.svg"
+ class="w-8 border-4 border-transparent mr-2"
+ alt="Bitcoin"
+ />
+ <div class="my-auto mt-1">Buy Bitcoin</div>
+ </div>
+ </button>
+ </a>
+ {/if}
</div>
</div>
{/if}
@@ -138,17 +146,23 @@
</div>
{/if}
- {#if latest.length}
- <div>
- <h1 class="text-2xl">Messages</h1>
- {#each latest as { content, user }}
- <div class="flex">
- <div class="my-auto">
- <Avatar {user} size={12} disabled={true} />
+ {#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={`/${user.username}/messages`}>
+ <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>
- <div class="my-auto ml-1 text-secondary">{user.username}</div>
- {content}
- </div>
+ </a>
{/each}
</div>
{/if}
diff --git a/src/routes/(app)/[username]/(profile)/messages/+page.svelte b/src/routes/(app)/[username]/(profile)/messages/+page.svelte
deleted file mode 100644
index db7b1ced..00000000
--- a/src/routes/(app)/[username]/(profile)/messages/+page.svelte
+++ /dev/null
@@ -1,123 +0,0 @@
-<script>
- import { browser } from '$app/environment';
- 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, fail, focus } from '$lib/utils';
- import { sign, send, encrypt, decrypt } from '$lib/nostr';
- import { messages, password } from '$lib/store';
- import { tick, onMount } from 'svelte';
- import { calculateId } from 'nostr';
-
- export let data;
-
- let { newMessages, subject, user } = data;
- let input, pane;
-
- $: initialize($password);
- let initialize = async (p) => {
- if (!p) return;
- $messages[user.pubkey] = $messages[user.pubkey] || {};
- $messages[user.pubkey][subject.pubkey] = $messages[user.pubkey][subject.pubkey] || {};
-
- for (let m of newMessages) {
- if (!$messages[user.pubkey][subject.pubkey][m.id]) {
- m.message = await decrypt({ event: m, user });
- $messages[user.pubkey][subject.pubkey][m.id] = m;
- tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
- }
- }
- tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
- };
-
- $: events =
- ($messages[user.pubkey] &&
- $messages[user.pubkey][subject.pubkey] &&
- Object.values($messages[user.pubkey][subject.pubkey]).sort(
- (a, b) => a.created_at - b.created
- )) ||
- [];
-
- 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;
- event.id = await calculateId(event);
- $messages[user.pubkey][subject.pubkey][event.id] = event;
- $messages = $messages;
- tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
-
- try {
- event.content = await encrypt({ message, recipient: subject.pubkey, user });
- await sign({ event, user });
- await send(event);
-
- sent = true;
- } catch (e) {
- fail('Failed to send message');
- delete $messages[user.pubkey][subject.pubkey][event.id];
- $messages = $messages;
- }
-
- message = '';
- 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('payments.sendMessage')}
- </h1>
-
- <div class="max-h-[calc(50vh)] overflow-y-scroll p-4" bind:this={pane}>
- {#each events as data}
- {@const ours = data.pubkey === user.pubkey}
- {@const theirs = !ours}
- <div class="rounded-2xl p-4 max-w-[300px] mb-1 text-md" class:ours class:theirs>
- {#if data.message}
- {data.message}
- {:else}
- -
- {/if}
- </div>
- <div class="text-sm text-gray-400 mb-6" class:text-right={ours}>
- {format(new Date(data.created_at * 1000), 'MMM d')},
- {format(new Date(data.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
- use:focus
- name="message"
- placeholder={$t('payments.sendMessage')}
- class="w-full p-4 border rounded-xl"
- bind:value={message}
- bind:this={input}
- />
- </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]/invoice/[hash]/+page.svelte b/src/routes/(app)/[username]/invoice/[hash]/+page.svelte
index b5571f23..2ff92687 100644
--- a/src/routes/(app)/[username]/invoice/[hash]/+page.svelte
+++ b/src/routes/(app)/[username]/invoice/[hash]/+page.svelte
@@ -152,9 +152,9 @@
{/if}
<div class="w-full flex justify-center">
- <button class="flex rounded-full border py-2 px-5 hover:opacity-80" on:click={() => copy(txt)}>
+ <button class="flex rounded-full border py-3 px-5 hover:opacity-80" on:click={() => copy(txt)}>
<Icon icon="copy" style="mr-1" />
- <div class="text-secondary">{txt.substr(0, 10)}..{txt.substr(-6)}</div></button
+ <div class="text-secondary">{txt.substr(0, 10)}...{txt.substr(-10)}</div></button
>
</div>
</div>
diff --git a/src/routes/(app)/[username]/(profile)/messages/+page.server.js b/src/routes/(app)/[username]/messages/+page.server.js
index 63116099..7b651389 100644
--- a/src/routes/(app)/[username]/(profile)/messages/+page.server.js
+++ b/src/routes/(app)/[username]/messages/+page.server.js
@@ -7,10 +7,13 @@ export async function load({ params, parent, url }) {
let messages = [];
try {
- messages = await get(`/nostr/${user.pubkey}/${subject.pubkey}/${since}/messages`);
+ messages = await get(`/${user.pubkey}/${since}/messages`);
+ messages = messages
+ .filter((m) => m.recipient.id === subject.id || m.author.id === subject.id)
+ .sort((a, b) => a.created_at - b.created_at);
} catch (e) {
console.log(`failed to fetch nostr messages`, e);
}
- return { newMessages: messages };
+ return { messages, user };
}
diff --git a/src/routes/(app)/[username]/messages/+page.svelte b/src/routes/(app)/[username]/messages/+page.svelte
new file mode 100644
index 00000000..f8f64aea
--- /dev/null
+++ b/src/routes/(app)/[username]/messages/+page.svelte
@@ -0,0 +1,126 @@
+<script>
+ import { browser } from '$app/environment';
+ import { t } from '$lib/translations';
+ import { format, parseISO } from 'date-fns';
+ import { scale } from 'svelte/transition';
+ import { enhance } from '$app/forms';
+ import { Avatar, Icon } from '$comp';
+ 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 { calculateId } from 'nostr';
+
+ export let data;
+
+ let { messages, subject, user } = data;
+ let input, pane, ready;
+
+ $: initialize($password);
+ let initialize = async (p) => {
+ if (!p) return;
+ ready = true;
+ tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
+ };
+
+ e.subscribe((ev) => {
+ if (ev?.recipient.id === user.id && !~messages.findIndex((m) => m.id === ev.id)) {
+ messages.push(ev);
+ 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', subject.pubkey]]
+ };
+
+ event.message = message;
+ event.id = await calculateId(event);
+ tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
+
+ try {
+ event.content = await encrypt({ message, recipient: subject.pubkey, user });
+ event.author = user;
+ event.recipient = subject;
+ await sign({ event, user });
+ await send(event);
+ messages.push(event);
+ messages = messages;
+ tick().then(() => pane && (pane.scrollTop = pane.scrollHeight));
+
+ sent = true;
+ } catch (e) {
+ fail('Failed to send message');
+ }
+
+ message = '';
+ submitting = false;
+ };
+</script>
+
+<div class="container max-w-xl mx-auto px-4">
+ {#if messages.length && ready}
+ <div
+ class="max-h-[calc(40vh)] overflow-y-scroll scrollbar-thin scrollbar-thumb-[#F2F6FC] scrollbar-track-white pr-8"
+ bind:this={pane}
+ >
+ {#each messages as event}
+ {@const ours = event.pubkey === user.pubkey}
+ {@const theirs = !ours}
+ <div class="flex gap-2" class:flex-row-reverse={theirs} class:justify-end={theirs}>
+ <div
+ class="rounded-2xl px-4 py-2 max-w-[300px] mb-1 text-lg"
+ class:ours
+ class:theirs
+ :key={event.id}
+ >
+ {#await decrypt({ event, user })}
+ -
+ {:then message}
+ {message}
+ {/await}
+ </div>
+ <div class="mt-auto">
+ <Avatar user={event.author} size={'12'} />
+ </div>
+ </div>
+ <div class="text-sm text-gray-400 mb-6" class:text-right={ours}>
+ {format(new Date(event.created_at * 1000), 'MMM d')},
+ {format(new Date(event.created_at * 1000), 'h:mm aa')}
+ </div>
+ {/each}
+ </div>
+ {/if}
+
+ <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
+ use:focus
+ name="message"
+ placeholder={$t('payments.sendMessage')}
+ class="w-full p-4 border rounded-xl outline-none"
+ bind:value={message}
+ bind:this={input}
+ />
+ </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>
diff --git a/src/routes/(app)/[username]/payments/[...page]/+page.svelte b/src/routes/(app)/[username]/payments/[...page]/+page.svelte
index 2068c306..aacba81e 100644
--- a/src/routes/(app)/[username]/payments/[...page]/+page.svelte
+++ b/src/routes/(app)/[username]/payments/[...page]/+page.svelte
@@ -182,7 +182,7 @@
{:else if p.with}
<div class="flex">
<div class="my-auto">
- <Avatar user={p.with} size={12} disabled={true} />
+ <Avatar user={p.with} size={16} disabled={true} />
</div>
<div class="my-auto ml-1 text-secondary">{p.with.username}</div>
</div>
diff --git a/src/routes/(app)/buy/+page.svelte b/src/routes/(app)/buy/+page.svelte
index 323d5ef0..2f556ada 100644
--- a/src/routes/(app)/buy/+page.svelte
+++ b/src/routes/(app)/buy/+page.svelte
@@ -1,13 +1,33 @@
<script>
import { enhance } from '$app/forms';
import { Icon, Lock, Card, Calendar, Spinner } from '$comp';
+ import { animatedRate, selectedRate } from '$lib/store';
+ import { f } from '$lib/utils';
export let data, form;
- let { username } = data.user;
+ let { username, currency } = data.user;
let loading;
let amount, n, d, cvc;
+
+ let ease = (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t);
+ let i;
+
+ let o = $selectedRate;
+ $: animate($selectedRate);
+ let animate = (n) => {
+ clearInterval(i);
+
+ let t = 0;
+ let d = o - n;
+
+ i = setInterval(() => {
+ $animatedRate = (o - d * ease(t / 100)).toFixed(2);
+ if (t > 80) (o = n) && clearInterval(i);
+ t++;
+ }, 10);
+ };
</script>
<div class="container px-4 max-w-xl mx-auto mt-10 space-y-5">
@@ -114,6 +134,17 @@
</button>
</div>
</form>
+
+ <div class="flex">
+ <div class="text-secondary flex mr-auto">
+ <div class="flex mr-1">
+ <div class="my-auto mr-1">1</div>
+ <img src="/images/bitcoin.svg" class="w-5 my-auto" alt="Bitcoin" />
+ </div>
+ <div>= {f($animatedRate, currency)}</div>
+ </div>
+ <p class="text-secondary">3% credit card fee applies</p>
+ </div>
</div>
</div>
</div>
diff --git a/src/routes/(app)/login/+page.svelte b/src/routes/(app)/login/+page.svelte
index 9a1e41da..75998cd0 100644
--- a/src/routes/(app)/login/+page.svelte
+++ b/src/routes/(app)/login/+page.svelte
@@ -1,5 +1,4 @@
<script>
- import { messages } from '$lib/store';
import { RegisterOrLogin } from '$comp';
export let form;
diff --git a/src/routes/(app)/payment/[id]/+page.svelte b/src/routes/(app)/payment/[id]/+page.svelte
index 8125c63c..2988ddb5 100644
--- a/src/routes/(app)/payment/[id]/+page.svelte
+++ b/src/routes/(app)/payment/[id]/+page.svelte
@@ -18,7 +18,7 @@
<Icon icon="arrow-left" style="w-10" />
</button>
-<div class="container mx-auto max-w-lg px-4 space-y-2 sm:space-y-5 break-all text-2xl">
+<div class="container mx-auto max-w-lg px-4 space-y-8 break-all text-2xl">
<h1 class="px-3 md:px-0 text-center text-3xl md:text-4xl font-semibold mb-10">
{$t(amount < 0 ? 'payments.sent' : 'payments.received')}
</h1>
@@ -28,7 +28,7 @@
<span class="text-lg text-secondary my-auto mr-2">{amount > 0 ? 'From' : 'To'}</span>
<div class="flex">
<div class="my-auto">
- <Avatar user={p.with} />
+ <Avatar user={p.with} size={20} />
</div>
<div class="my-auto ml-1 text-secondary">{p.with.username}</div>
</div>
@@ -69,20 +69,12 @@
</div>
</div>
- {#if type === 'lightning'}
- <div>
- <span class="text-lg text-secondary">Proof</span>
- <div class="font-bold">{$t('payments.preimage')}</div>
- <div>
- {ref}
- </div>
- </div>
- {:else if type === 'bitcoin'}
+ {#if type === 'bitcoin'}
<div>
<span class="text-lg text-secondary">Txid</span>
<div class="flex">
<div>
- <a href={`${expl}/payment/${id}`} target="_blank" rel="noreferrer">{id}</a>
+ <a href={`${expl}/tx/${id}`} target="_blank" rel="noreferrer">{id}</a>
</div>
<button class="flex font-bold hover:opacity-80 mb-auto my-auto" on:click={() => copy(id)}
><Icon icon="copy" style="ml-2 w-20 my-auto" />
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
index 2941c890..c57db020 100644
--- a/tailwind.config.cjs
+++ b/tailwind.config.cjs
@@ -12,5 +12,6 @@ module.exports = {
}
}
},
- plugins: []
+ plugins: [require('tailwind-scrollbar')],
+ safelist: ['w-16', 'h-16', 'w-20', 'h-20', 'w-24', 'h-24']
};