Work on thread detail page

This commit is contained in:
Jon Staab
2024-10-23 14:20:24 -07:00
parent 8d0c016621
commit 09028b69a4
15 changed files with 300 additions and 134 deletions

View File

@@ -181,7 +181,7 @@
} }
.note-editor .tiptap[contenteditable="true"] { .note-editor .tiptap[contenteditable="true"] {
@apply input input-bordered h-auto min-h-32 p-[.65rem] pb-6; @apply input input-bordered h-auto min-h-32 p-[.65rem] pb-6 rounded-box;
} }
.tiptap pre code { .tiptap pre code {

View File

@@ -46,6 +46,7 @@ import {
loadRelay, loadRelay,
} from "@welshman/app" } from "@welshman/app"
import { import {
REPLY,
tagRoom, tagRoom,
userMembership, userMembership,
MEMBERSHIPS, MEMBERSHIPS,
@@ -301,31 +302,52 @@ export const sendWrapped = async ({
) )
} }
export const makeReaction = ({ export type ReactionParams = {
event,
content,
tags = [],
}: {
event: TrustedEvent event: TrustedEvent
content: string content: string
tags?: string[][] tags?: string[][]
}) => }
createEvent(REACTION, {
content,
tags: [...tags, ...tagReactionTo(event)],
})
export const publishReaction = ({ export const makeReaction = ({event, content, tags = []}: ReactionParams) =>
relays, createEvent(REACTION, {content, tags: [...tags, ...tagReactionTo(event)]})
event,
content, export const publishReaction = ({relays, ...params}: ReactionParams & {relays: string[]}) =>
tags = [], publishThunk({event: makeReaction(params), relays})
}: {
relays: string[] export type ReplyParams = {
event: TrustedEvent event: TrustedEvent
content: string content: string
tags?: string[][] tags?: string[][]
}) => publishThunk({event: makeReaction({event, content, tags}), relays}) }
export const makeReply = ({event, content, tags = []}: ReplyParams) => {
const seenRoots = new Set<string>()
for (const [raw, ...tag] of event.tags.filter(t => t[0].match(/^K|E|A|I$/i))) {
const T = raw.toUpperCase()
const t = raw.toLowerCase()
if (seenRoots.has(T)) {
tags.push([t, ...tag])
} else {
tags.push([T, ...tag])
seenRoots.add(T)
}
}
if (seenRoots.size === 0) {
tags.push(["K", String(event.kind)])
tags.push(["E", event.id])
} else {
tags.push(["k", String(event.kind)])
tags.push(["e", event.id])
}
return createEvent(REPLY, {content, tags})
}
export const publishReply = ({relays, ...params}: ReplyParams & {relays: string[]}) =>
publishThunk({event: makeReply(params), relays})
export const makeDelete = ({event}: {event: TrustedEvent}) => export const makeDelete = ({event}: {event: TrustedEvent}) =>
createEvent(DELETE, {tags: [["k", String(event.kind)], ...tagEvent(event)]}) createEvent(DELETE, {tags: [["k", String(event.kind)], ...tagEvent(event)]})

View File

@@ -1,29 +1,25 @@
<script lang="ts"> <script lang="ts">
import {readable} from "svelte/store" import {readable} from "svelte/store"
import {hash, uniqBy, groupBy} from "@welshman/lib" import {hash} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {deriveProfile, deriveProfileDisplay, formatTimestampAsTime, pubkey} from "@welshman/app" import {deriveProfile, deriveProfileDisplay, formatTimestampAsTime, pubkey} from "@welshman/app"
import type {Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
import {REACTION} from "@welshman/util"
import {repository} from "@welshman/app"
import {slideAndFade, conditionalTransition} from "@lib/transition" import {slideAndFade, conditionalTransition} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Delay from "@lib/components/Delay.svelte" import Delay from "@lib/components/Delay.svelte"
import Avatar from "@lib/components/Avatar.svelte" import Avatar from "@lib/components/Avatar.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import ThunkStatus from "@app/components/ThunkStatus.svelte" import ThunkStatus from "@app/components/ThunkStatus.svelte"
import Reactions from "@app/components/Reactions.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import ChannelThread from "@app/components/ChannelThread.svelte" import ChannelThread from "@app/components/ChannelThread.svelte"
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte" import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte" import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
import {REPLY, colors, tagRoom, deriveEvent, displayReaction} from "@app/state" import {colors, tagRoom, deriveEvent} from "@app/state"
import {publishDelete, publishReaction} from "@app/commands" import {publishDelete, publishReaction} from "@app/commands"
import {pushModal, pushDrawer} from "@app/modal" import {pushDrawer} from "@app/modal"
export let url export let url, room
export let room
export let event: TrustedEvent export let event: TrustedEvent
export let thunk: Thunk export let thunk: Thunk
export let showPubkey = false export let showPubkey = false
@@ -31,8 +27,6 @@
const profile = deriveProfile(event.pubkey) const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey) const profileDisplay = deriveProfileDisplay(event.pubkey)
const reactions = deriveEvents(repository, {filters: [{kinds: [REACTION], "#e": [event.id]}]})
const replies = deriveEvents(repository, {filters: [{kinds: [REPLY], "#E": [event.id]}]})
const rootTag = event.tags.find(t => t[0].match(/^e$/i)) const rootTag = event.tags.find(t => t[0].match(/^e$/i))
const rootId = rootTag?.[1] const rootId = rootTag?.[1]
const rootHints = [rootTag?.[2]].filter(Boolean) as string[] const rootHints = [rootTag?.[2]].filter(Boolean) as string[]
@@ -46,7 +40,7 @@
const openThread = () => { const openThread = () => {
const root = $rootEvent || event const root = $rootEvent || event
pushModal(ChannelThread, {url, room, event: root}, {drawer: true}) pushDrawer(ChannelThread, {url, room, event: root})
} }
const onReactionClick = (content: string, events: TrustedEvent[]) => { const onReactionClick = (content: string, events: TrustedEvent[]) => {
@@ -99,37 +93,12 @@
</div> </div>
</div> </div>
</div> </div>
{#if $reactions.length > 0 || $replies.length > 0} <Reactions {event} {onReactionClick} showReplies={!isThread} />
<div class="ml-12 flex flex-wrap gap-2 text-xs">
{#if $replies.length > 0 && !isThread}
<div class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full">
<Icon icon="reply" />
{$replies.length}
</div>
{/if}
{#each groupBy( e => e.content, uniqBy(e => e.pubkey + e.content, $reactions), ).entries() as [content, events]}
{@const isOwn = events.some(e => e.pubkey === $pubkey)}
{@const onClick = () => onReactionClick(content, events)}
<button
type="button"
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full"
class:border={isOwn}
class:border-solid={isOwn}
class:border-primary={isOwn}
on:click|stopPropagation={onClick}>
<span>{displayReaction(content)}</span>
{#if events.length > 1}
<span>{events.length}</span>
{/if}
</button>
{/each}
</div>
{/if}
<button <button
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all group-hover:opacity-100" class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all group-hover:opacity-100"
on:click|stopPropagation> on:click|stopPropagation>
<ChannelMessageEmojiButton {url} {room} {event} /> <ChannelMessageEmojiButton {url} {room} {event} />
<ChannelMessageMenuButton {url} {room} {event} /> <ChannelMessageMenuButton {url} {event} />
</button> </button>
</button> </button>
</Delay> </Delay>

View File

@@ -6,7 +6,7 @@
import Tippy from "@lib/components/Tippy.svelte" import Tippy from "@lib/components/Tippy.svelte"
import ChannelMessageMenu from "@app/components/ChannelMessageMenu.svelte" import ChannelMessageMenu from "@app/components/ChannelMessageMenu.svelte"
export let url, room, event export let url, event
const open = () => popover.show() const open = () => popover.show()
@@ -32,6 +32,6 @@
<Tippy <Tippy
bind:popover bind:popover
component={ChannelMessageMenu} component={ChannelMessageMenu}
props={{url, room, event, onClick}} props={{url, event, onClick}}
params={{trigger: "manual", interactive: true}} /> params={{trigger: "manual", interactive: true}} />
</div> </div>

View File

@@ -9,6 +9,7 @@
import ChannelMessage from "@app/components/ChannelMessage.svelte" import ChannelMessage from "@app/components/ChannelMessage.svelte"
import ChannelCompose from "@app/components/ChannelCompose.svelte" import ChannelCompose from "@app/components/ChannelCompose.svelte"
import {tagRoom, REPLY} from "@app/state" import {tagRoom, REPLY} from "@app/state"
import {publishReply} from "@app/commands"
export let url, room, event: TrustedEvent export let url, room, event: TrustedEvent
@@ -19,30 +20,12 @@
}) })
const onSubmit = ({content, tags}: EventContent) => { const onSubmit = ({content, tags}: EventContent) => {
const seenRoots = new Set<string>() const thunk = publishReply({
event,
for (const [raw, ...tag] of event.tags.filter(t => t[0].match(/^K|E|A|I$/i))) { content,
const T = raw.toUpperCase() tags: append(tagRoom(room, url), tags),
const t = raw.toLowerCase() relays: [url],
})
if (seenRoots.has(T)) {
tags.push([t, ...tag])
} else {
tags.push([T, ...tag])
seenRoots.add(T)
}
}
if (seenRoots.size === 0) {
tags.push(["K", String(event.kind)])
tags.push(["E", event.id])
} else {
tags.push(["k", String(event.kind)])
tags.push(["e", event.id])
}
const reply = createEvent(REPLY, {content, tags: append(tagRoom(room, url), tags)})
const thunk = publishThunk({event: reply, relays: [url]})
thunks.update(assoc(thunk.event.id, thunk)) thunks.update(assoc(thunk.event.id, thunk))
} }

View File

@@ -1,21 +1,19 @@
<script lang="ts"> <script lang="ts">
import {type Instance} from "tippy.js" import {type Instance} from "tippy.js"
import {hash, uniqBy, groupBy} from "@welshman/lib" import {hash} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {deriveProfile, deriveProfileDisplay, formatTimestampAsTime, pubkey} from "@welshman/app" import {deriveProfile, deriveProfileDisplay, formatTimestampAsTime, pubkey} from "@welshman/app"
import type {MergedThunk} from "@welshman/app" import type {MergedThunk} from "@welshman/app"
import {REACTION, ZAP_RESPONSE} from "@welshman/util"
import {repository} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Tippy from "@lib/components/Tippy.svelte" import Tippy from "@lib/components/Tippy.svelte"
import Avatar from "@lib/components/Avatar.svelte" import Avatar from "@lib/components/Avatar.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import Reactions from "@app/components/Reactions.svelte"
import ThunkStatus from "@app/components/ThunkStatus.svelte" import ThunkStatus from "@app/components/ThunkStatus.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte" import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
import {colors, displayReaction} from "@app/state" import {colors} from "@app/state"
import {pushDrawer} from "@app/modal" import {pushDrawer} from "@app/modal"
import {makeDelete, makeReaction, sendWrapped} from "@app/commands" import {makeDelete, makeReaction, sendWrapped} from "@app/commands"
@@ -26,8 +24,6 @@
const profile = deriveProfile(event.pubkey) const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey) const profileDisplay = deriveProfileDisplay(event.pubkey)
const reactions = deriveEvents(repository, {filters: [{kinds: [REACTION], "#e": [event.id]}]})
const zaps = deriveEvents(repository, {filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}]})
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length] const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
const showProfile = () => pushDrawer(ProfileDetail, {pubkey: event.pubkey}) const showProfile = () => pushDrawer(ProfileDetail, {pubkey: event.pubkey})
@@ -103,25 +99,6 @@
</div> </div>
</div> </div>
</div> </div>
{#if $reactions.length > 0 || $zaps.length > 0} <Reactions {event} {onReactionClick} />
<div class="relative z-feature -mt-4 flex justify-end text-xs">
{#each groupBy( e => e.content, uniqBy(e => e.pubkey + e.content, $reactions), ).entries() as [content, events]}
{@const isOwn = events.some(e => e.pubkey === $pubkey)}
{@const onClick = () => onReactionClick(content, events)}
<button
type="button"
class="flex-inline btn btn-neutral btn-xs mr-2 gap-1 rounded-full"
class:border={isOwn}
class:border-solid={isOwn}
class:border-primary={isOwn}
on:click|stopPropagation={onClick}>
<span>{displayReaction(content)}</span>
{#if events.length > 1}
<span>{events.length}</span>
{/if}
</button>
{/each}
</div>
{/if}
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {displayPubkey, getPubkeyTagValues, getListTags} from "@welshman/util" import {displayPubkey, getPubkeyTagValues, getListTags} from "@welshman/util"
import { import {
session,
userFollows, userFollows,
deriveUserWotScore, deriveUserWotScore,
deriveProfile, deriveProfile,
@@ -23,7 +24,7 @@
const onClick = () => pushDrawer(ProfileDetail, {pubkey}) const onClick = () => pushDrawer(ProfileDetail, {pubkey})
$: following = getPubkeyTagValues(getListTags($userFollows)).includes(pubkey) $: following = pubkey === $session!.pubkey || getPubkeyTagValues(getListTags($userFollows)).includes(pubkey)
</script> </script>
<div class="flex max-w-full gap-3"> <div class="flex max-w-full gap-3">

View File

@@ -0,0 +1,42 @@
<script lang="ts">
import {groupBy, uniqBy} from "@welshman/lib"
import {REACTION} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {pubkey, repository} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import {REPLY, displayReaction} from "@app/state"
export let event
export let onReactionClick
export let showReplies = false
const reactions = deriveEvents(repository, {filters: [{kinds: [REACTION], "#e": [event.id]}]})
const replies = deriveEvents(repository, {filters: [{kinds: [REPLY], "#E": [event.id]}]})
</script>
{#if $reactions.length > 0 || $replies.length > 0}
<div class="ml-12 flex flex-wrap gap-2 text-xs">
{#if $replies.length > 0 && showReplies}
<div class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full">
<Icon icon="reply" />
{$replies.length}
</div>
{/if}
{#each groupBy( e => e.content, uniqBy(e => e.pubkey + e.content, $reactions), ).entries() as [content, events]}
{@const isOwn = events.some(e => e.pubkey === $pubkey)}
{@const onClick = () => onReactionClick(content, events)}
<button
type="button"
class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full"
class:border={isOwn}
class:border-solid={isOwn}
class:border-primary={isOwn}
on:click|stopPropagation={onClick}>
<span>{displayReaction(content)}</span>
{#if events.length > 1}
<span>{events.length}</span>
{/if}
</button>
{/each}
</div>
{/if}

View File

@@ -37,7 +37,7 @@
<form class="column gap-4" on:submit|preventDefault={startSubmit}> <form class="column gap-4" on:submit|preventDefault={startSubmit}>
<ModalHeader> <ModalHeader>
<div slot="title">Create a Thread</div> <div slot="title">Create a Thread</div>
<div slot="info">Share your thoughts, or start a discussion.</div> <div slot="info">Share a link, or start a discussion.</div>
</ModalHeader> </ModalHeader>
<div class="relative"> <div class="relative">
<div class="note-editor flex-grow overflow-hidden"> <div class="note-editor flex-grow overflow-hidden">

View File

@@ -1,14 +1,42 @@
<script lang="ts"> <script lang="ts">
import {max} from '@welshman/lib'
import {formatTimestamp, formatTimestampRelative} from "@welshman/app"
import {deriveEvents} from "@welshman/store"
import {repository} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Divider from "@lib/components/Divider.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import NoteCard from "@app/components/NoteCard.svelte" import Profile from "@app/components/Profile.svelte"
import {makeThreadPath} from "@app/routes"
import {pushDrawer} from "@app/modal"
import {REPLY} from "@app/state"
export let url
export let event export let event
const replies = deriveEvents(repository, {filters: [{kinds: [REPLY], "#E": [event.id]}]})
$: lastActive = max([...$replies, event].map(e => e.created_at))
</script> </script>
<div class="flex flex-col items-end"> <Link class="col-2 card2 bg-alt w-full cursor-pointer" href={makeThreadPath(url, event.id)}>
<NoteCard {event} class="card2 bg-alt w-full"> <div class="flex w-full justify-between gap-2">
<div class="ml-12"> <Profile pubkey={event.pubkey} />
<Content {event} /> <p class="text-sm opacity-75">
{formatTimestamp(event.created_at)}
</p>
</div>
<div class="pl-12 w-full col-4">
<Content {event} />
<div class="row-2 justify-end">
<div class="flex-inline btn btn-neutral btn-xs gap-1 rounded-full">
<Icon icon="reply" />
<span>{$replies.length} {$replies.length === 1 ? 'reply' : 'replies'}</span>
</div>
<div class="btn btn-neutral btn-xs rounded-full">
Active {formatTimestampRelative(lastActive)}
</div>
</div> </div>
</NoteCard> </div>
</div> </Link>

View File

@@ -0,0 +1,82 @@
<script lang="ts">
import {onMount} from "svelte"
import type {Readable} from "svelte/store"
import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {createEvent} from "@welshman/util"
import {publishThunk} from "@welshman/app"
import {fly} from '@lib/transition'
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {REPLY} from "@app/state"
import {getPubkeyHints, publishReply} from "@app/commands"
import {getEditorOptions, addFile, uploadFiles, getEditorTags} from "@lib/editor"
export let url
export let event
const startSubmit = () => uploadFiles($editor)
const loading = writable(false)
const submit = () => {
const thunk = publishReply({
event,
content: $editor.getText(),
tags: getEditorTags($editor),
relays: [url],
})
hide()
}
const show = () => {
visible = true
}
const hide = () => {
visible = false
}
let visible = false
let editor: Readable<Editor>
onMount(() => {
editor = createEditor(getEditorOptions({submit, loading, getPubkeyHints, autofocus: true}))
})
</script>
{#if visible}
<form on:submit|preventDefault={startSubmit} in:fly class="sticky bottom-2 card2 bg-alt mt-2 mx-2">
<div class="relative">
<div class="note-editor flex-grow overflow-hidden">
<EditorContent editor={$editor} />
</div>
<Button
data-tip="Add an image"
class="tooltip tooltip-left absolute bottom-1 right-2"
on:click={() => addFile($editor)}>
{#if $loading}
<span class="loading loading-spinner loading-xs"></span>
{:else}
<Icon icon="paperclip" size={3} />
{/if}
</Button>
</div>
<ModalFooter>
<Button class="btn btn-link" on:click={hide}>
Cancel
</Button>
<Button type="submit" class="btn btn-primary">Post Reply</Button>
</ModalFooter>
</form>
{:else}
<div class="flex justify-end p-2">
<Button class="btn btn-primary" on:click={show}>
<Icon icon="reply" />
Reply
</Button>
</div>
{/if}

View File

@@ -13,6 +13,8 @@ export const makeSpacePath = (url: string, extra = "") => {
export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}` export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}`
export const makeThreadPath = (url: string, eventId: string) => `/spaces/${encodeRelay(url)}/threads/${eventId}`
export const getPrimaryNavItem = ($page: Page) => $page.route?.id?.split("/")[1] export const getPrimaryNavItem = ($page: Page) => $page.route?.id?.split("/")[1]
export const getPrimaryNavItemIndex = ($page: Page) => { export const getPrimaryNavItemIndex = ($page: Page) => {

View File

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {NOTE} from "@welshman/util" import {NOTE, getListTags, getPubkeyTagValues} from "@welshman/util"
import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds" import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
import {nthEq} from "@welshman/lib" import {nthEq} from "@welshman/lib"
import {feedLoader} from "@welshman/app" import {feedLoader, userMutes} from "@welshman/app"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -21,6 +21,7 @@
const feed = makeIntersectionFeed(makeRelayFeed(url), feedFromFilter({kinds})) const feed = makeIntersectionFeed(makeRelayFeed(url), feedFromFilter({kinds}))
const events = deriveEventsForUrl(url, kinds) const events = deriveEventsForUrl(url, kinds)
const loader = feedLoader.getLoader(feed, {}) const loader = feedLoader.getLoader(feed, {})
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
const openMenu = () => pushDrawer(MenuSpace, {url}) const openMenu = () => pushDrawer(MenuSpace, {url})
@@ -55,16 +56,20 @@
<Icon icon="notes-minimalistic" /> <Icon icon="notes-minimalistic" />
</div> </div>
<strong slot="title">Threads</strong> <strong slot="title">Threads</strong>
<div slot="action" class="md:hidden"> <div slot="action" class="row-2">
<Button on:click={openMenu} class="btn btn-neutral btn-sm"> <Button class="btn btn-primary btn-sm" on:click={createThread}>
<Icon icon="notes-minimalistic" />
Create a Thread
</Button>
<Button on:click={openMenu} class="btn btn-neutral btn-sm md:hidden">
<Icon icon="menu-dots" /> <Icon icon="menu-dots" />
</Button> </Button>
</div> </div>
</PageBar> </PageBar>
<div class="flex flex-grow flex-col gap-2 overflow-auto p-2" bind:this={element}> <div class="flex flex-grow flex-col gap-2 overflow-auto p-2" bind:this={element}>
{#each $events.slice(0, limit) as event (event.id)} {#each $events.slice(0, limit) as event (event.id)}
{#if !event.tags.some(nthEq(0, "e"))} {#if !event.tags.some(nthEq(0, "e")) && !mutedPubkeys.includes(event.pubkey)}
<ThreadItem {event} /> <ThreadItem {url} {event} />
{/if} {/if}
{/each} {/each}
<p class="flex h-10 items-center justify-center py-20"> <p class="flex h-10 items-center justify-center py-20">
@@ -77,12 +82,4 @@
</Spinner> </Spinner>
</p> </p>
</div> </div>
<Button
class="tooltip tooltip-left fixed bottom-16 right-4 z-feature p-1 sm:right-8 md:bottom-4 md:right-4"
data-tip="Create a Thread"
on:click={createThread}>
<div class="btn btn-circle btn-primary flex h-12 w-12 items-center justify-center">
<Icon icon="notes-minimalistic" />
</div>
</Button>
</div> </div>

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import {ctx, sortBy} from "@welshman/lib"
import {page} from "$app/stores"
import {pubkey, repository} from "@welshman/app"
import {deriveEvents} from "@welshman/store"
import {getReplyFilters} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import Divider from "@lib/components/Divider.svelte"
import PageBar from "@lib/components/PageBar.svelte"
import Content from "@app/components/Content.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import MenuSpace from "@app/components/MenuSpace.svelte"
import Reactions from "@app/components/Reactions.svelte"
import ThreadReply from "@app/components/ThreadReply.svelte"
import {REPLY, deriveEvent, decodeRelay} from "@app/state"
import {publishDelete, publishReaction} from "@app/commands"
import {pushDrawer} from "@app/modal"
const {relay, id} = $page.params
const url = decodeRelay(relay)
const event = deriveEvent(id)
const replies = deriveEvents(repository, {filters: [{kinds: [REPLY], "#E": [id]}]})
const back = () => history.back()
const openMenu = () => pushDrawer(MenuSpace, {url})
const getReactionHandler = (event: TrustedEvent) => (content: string, events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey)
if (reaction) {
publishDelete({relays: [url], event: reaction})
} else {
publishReaction({event, content, relays: [url]})
}
}
</script>
<div class="flex flex-col-reverse gap-2 max-h-screen overflow-auto p-2">
{#each sortBy(e => -e.created_at, $replies) as reply (reply.id)}
<NoteCard event={reply} class="w-full border-l border-b border-r border-solid border-neutral -mt-8 pt-12 p-6 rounded-box">
<div class="ml-12">
<Content event={reply} />
<Reactions event={reply} onReactionClick={getReactionHandler(reply)} />
</div>
</NoteCard>
{/each}
<NoteCard event={$event} class="card2 bg-alt w-full py-2 z-feature relative border border-solid border-neutral">
<div class="ml-12">
<Content event={$event} />
<Reactions event={$event} onReactionClick={getReactionHandler($event)} />
</div>
</NoteCard>
<Button class="flex gap-2 mt-5 mb-3 items-center" on:click={back}>
<Icon icon="alt-arrow-left" />
Back to threads
</Button>
</div>
<ThreadReply {url} event={$event} />

View File

@@ -29,6 +29,7 @@ export default {
dark: { dark: {
...themes["dark"], ...themes["dark"],
primary: process.env.VITE_PLATFORM_ACCENT, primary: process.env.VITE_PLATFORM_ACCENT,
"primary-content": "#EAE7FF",
}, },
}, },
], ],