diff --git a/src/app.css b/src/app.css index 7bec8383..789fe8e5 100644 --- a/src/app.css +++ b/src/app.css @@ -96,7 +96,7 @@ /* tiptap */ -.input-editor, .chat-editor { +.input-editor, .chat-editor, .note-editor { @apply p-1 -m-1 min-h-12; } @@ -113,6 +113,10 @@ @apply input input-bordered p-[.65rem] h-auto; } +.note-editor .tiptap[contenteditable="true"] { + @apply input input-bordered p-[.65rem] h-auto min-h-32 pb-6; +} + .tiptap pre code { @apply link-content block w-full; } diff --git a/src/app/components/ThreadCard.svelte b/src/app/components/ThreadCard.svelte new file mode 100644 index 00000000..0d112f33 --- /dev/null +++ b/src/app/components/ThreadCard.svelte @@ -0,0 +1,45 @@ + + +
+
+
+
+
+ +
+
+
{$profileDisplay}
+
{displayPubkey(root.pubkey)}
+
+
+ {formatTimestamp(root.created_at)} +
+
+ +
+
+ {#if replies.length > 0} + Show {replies.length} {replies.length === 1 ? 'reply' : 'replies'} + {/if} +
diff --git a/src/app/components/ThreadCreate.svelte b/src/app/components/ThreadCreate.svelte new file mode 100644 index 00000000..7129c9ac --- /dev/null +++ b/src/app/components/ThreadCreate.svelte @@ -0,0 +1,86 @@ + + +
+
+

Create a Thread

+

Share your thoughts, or start a discussion.

+
+
+
+ +
+ +
+
+ + +
+
+ diff --git a/src/app/editor.ts b/src/app/editor.ts index 1dd98ed6..212951fb 100644 --- a/src/app/editor.ts +++ b/src/app/editor.ts @@ -206,3 +206,4 @@ export const getNoteEditorOptions = ({uploading, sendMessage}: EditorOptions) => }), ], }) + diff --git a/src/app/state.ts b/src/app/state.ts index cd6646f6..178603f3 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -1,9 +1,10 @@ import {nip19} from "nostr-tools" import {get, derived} from "svelte/store" import type {Maybe} from "@welshman/lib" -import {setContext, nth, max, pushToMapKey, nthEq} from "@welshman/lib" +import {setContext, partition, nth, max, pushToMapKey, nthEq} from "@welshman/lib" import { getIdFilters, + NOTE, RELAYS, REACTION, ZAP_RESPONSE, @@ -11,6 +12,8 @@ import { EVENT_TIME, getRelayTagValues, isShareableRelayUrl, + getAncestorTags, + getAncestorTagValues, } from "@welshman/util" import type {TrustedEvent} from "@welshman/util" import { @@ -93,7 +96,7 @@ export const readMembership = (event: TrustedEvent): PublishedMembership => { roomsByUrl.set(tag[1], []) } - for (const tag of event.tags.filter(nthEq(0, "t"))) { + for (const tag of event.tags.filter(nthEq(0, "~"))) { pushToMapKey(roomsByUrl, tag[2], tag[1]) } @@ -191,7 +194,7 @@ export const { }, }) -// Calendar vents +// Calendar events export const events = deriveEvents(repository, {filters: [{kinds: [EVENT_DATE, EVENT_TIME]}]}) @@ -207,6 +210,43 @@ export const eventsByUrl = derived([trackerStore, events], ([$tracker, $events]) return eventsByUrl }) +// Threads + +export type Thread = { + root: TrustedEvent + replies: TrustedEvent[] +} + +export const notes = deriveEvents(repository, {filters: [{kinds: [NOTE]}]}) + +export const threadsByUrl = derived([trackerStore, notes], ([$tracker, $notes]) => { + const threadsByUrl = new Map() + const [parents, children] = partition(e => getAncestorTags(e.tags).replies.length === 0, $notes) + + for (const event of parents) { + for (const url of $tracker.getRelays(event.id)) { + pushToMapKey(threadsByUrl, url, {root: event, replies: []}) + } + } + + for (const event of children) { + const [id] = getAncestorTagValues(event.tags).replies + + for (const url of $tracker.getRelays(event.id)) { + const threads = threadsByUrl.get(url) || [] + const thread = threads.find(thread => thread.root.id === id) + + if (!thread) { + continue + } + + thread.replies.push(event) + } + } + + return threadsByUrl +}) + // Rooms export const roomsByUrl = derived(chats, $chats => { diff --git a/src/assets/icons/Calendar Add.svg b/src/assets/icons/Calendar Add.svg new file mode 100644 index 00000000..c8be2a14 --- /dev/null +++ b/src/assets/icons/Calendar Add.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/lib/components/Icon.svelte b/src/lib/components/Icon.svelte index 8e9d2977..d2bf9960 100644 --- a/src/lib/components/Icon.svelte +++ b/src/lib/components/Icon.svelte @@ -22,6 +22,7 @@ import ArrowRight from "@assets/icons/Arrow Right.svg?dataurl" import Bag from "@assets/icons/Bag.svg?dataurl" import Bolt from "@assets/icons/Bolt.svg?dataurl" + import CalendarAdd from "@assets/icons/Calendar Add.svg?dataurl" import CalendarMinimalistic from "@assets/icons/Calendar Minimalistic.svg?dataurl" import ChatRound from "@assets/icons/Chat Round.svg?dataurl" import CheckCircle from "@assets/icons/Check Circle.svg?dataurl" @@ -87,6 +88,7 @@ "arrow-right": ArrowRight, bag: Bag, bolt: Bolt, + "calendar-add": CalendarAdd, "calendar-minimalistic": CalendarMinimalistic, "chat-round": ChatRound, "check-circle": CheckCircle, diff --git a/src/routes/spaces/[nrelay]/+layout.svelte b/src/routes/spaces/[nrelay]/+layout.svelte index b7e81dc6..cf86e443 100644 --- a/src/routes/spaces/[nrelay]/+layout.svelte +++ b/src/routes/spaces/[nrelay]/+layout.svelte @@ -2,7 +2,7 @@ import {onMount} from "svelte" import {page} from "$app/stores" import {sort, now} from "@welshman/lib" - import {displayRelayUrl, EVENT_DATE, EVENT_TIME, CLASSIFIED} from "@welshman/util" + import {displayRelayUrl, NOTE, EVENT_DATE, EVENT_TIME, CLASSIFIED} from "@welshman/util" import {subscribe} from "@welshman/app" import {fly, slide} from "@lib/transition" import Icon from "@lib/components/Icon.svelte" @@ -16,7 +16,7 @@ import SpaceExit from "@app/components/SpaceExit.svelte" import SpaceJoin from "@app/components/SpaceJoin.svelte" import RoomCreate from "@app/components/RoomCreate.svelte" - import {userMembership, roomsByUrl, decodeNRelay, GENERAL, MESSAGE, REPLY} from "@app/state" + import {userMembership, roomsByUrl, decodeNRelay, GENERAL, MESSAGE} from "@app/state" import {pushModal} from "@app/modal" import {makeSpacePath} from "@app/routes" @@ -52,7 +52,7 @@ $: otherRooms = ($roomsByUrl.get(url) || []).filter(room => !rooms.concat(GENERAL).includes(room)) onMount(() => { - const kinds = [MESSAGE, REPLY, EVENT_DATE, EVENT_TIME, CLASSIFIED] + const kinds = [NOTE, MESSAGE, EVENT_DATE, EVENT_TIME, CLASSIFIED] const sub = subscribe({filters: [{kinds, since: now() - 30}], relays: [url]}) return () => sub.close() diff --git a/src/routes/spaces/[nrelay]/[[room]]/+page.svelte b/src/routes/spaces/[nrelay]/[[room]]/+page.svelte index a6b728d5..4ae538b3 100644 --- a/src/routes/spaces/[nrelay]/[[room]]/+page.svelte +++ b/src/routes/spaces/[nrelay]/[[room]]/+page.svelte @@ -71,9 +71,9 @@ class="flex min-h-12 items-center justify-between gap-4 rounded-xl bg-base-100 px-4 shadow-xl">
- {room || "General"} + {room}
- {#if room} + {#if room !== GENERAL} {#if membership.includes(room)} diff --git a/src/routes/spaces/[nrelay]/threads/+page.svelte b/src/routes/spaces/[nrelay]/threads/+page.svelte new file mode 100644 index 00000000..f00f19a6 --- /dev/null +++ b/src/routes/spaces/[nrelay]/threads/+page.svelte @@ -0,0 +1,57 @@ + + +
+
+
+
+ + Threads +
+
+
+
+ {#each threads as {root, replies} (root.id)} + + {/each} +

+ + {#if loading} + Looking for threads... + {:else if threads.length === 0} + No threads found. + {/if} + +

+
+ +