Listen for new threads, add reply/quote button to channels and chats, better quote handling

This commit is contained in:
Jon Staab
2024-11-19 13:24:18 -08:00
parent 6a646b3240
commit f4f60a5333
12 changed files with 153 additions and 51 deletions

View File

@@ -1,19 +1,24 @@
<script lang="ts">
import {onMount} from "svelte"
import type {Readable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {createEditor, EditorContent} from "svelte-tiptap"
import {isMobile} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import {getEditorOptions, getEditorTags} from "@lib/editor"
import {getPubkeyHints} from "@app/commands"
export let onSubmit
export let onSubmit: any
export let content = ""
export let editor = createEditor(
getEditorOptions({
submit,
getPubkeyHints,
submitOnEnter: true,
autofocus: !isMobile,
}),
)
let editor: Readable<Editor>
const submit = () => {
function submit() {
if ($loading) return
onSubmit({
@@ -27,15 +32,6 @@
$: loading = $editor?.storage.fileUpload.loading
onMount(() => {
editor = createEditor(
getEditorOptions({
submit,
getPubkeyHints,
submitOnEnter: true,
autofocus: !isMobile,
}),
)
$editor.commands.setContent(content)
})
</script>

View File

@@ -13,6 +13,8 @@
import LongPress from "@lib/components/LongPress.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Content from "@app/components/Content.svelte"
import ThunkStatus from "@app/components/ThunkStatus.svelte"
import ReplySummary from "@app/components/ReplySummary.svelte"
@@ -27,6 +29,7 @@
export let url, room
export let event: TrustedEvent
export let replyTo: any = undefined
export let showPubkey = false
export let isHead = false
export let inert = false
@@ -40,6 +43,8 @@
const rootEvent = rootId ? deriveEvent(rootId, rootHints) : readable(null)
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
const reply = () => replyTo(event)
const onClick = () => {
const root = $rootEvent || event
@@ -65,6 +70,7 @@
</script>
<LongPress
data-event={event.id}
on:click={isMobile || inert ? null : onClick}
onLongPress={inert ? null : onLongPress}
class="group relative flex w-full flex-col gap-1 p-2 text-left transition-colors {inert
@@ -110,6 +116,11 @@
class:group-hover:opacity-100={!isMobile}
on:click|stopPropagation>
<ChannelMessageEmojiButton {url} {room} {event} />
{#if replyTo}
<Button class="btn join-item btn-xs" on:click={reply}>
<Icon icon="reply" size={4} />
</Button>
{/if}
<ChannelMessageMenuButton {url} {event} />
</button>
</LongPress>

View File

@@ -10,7 +10,10 @@
<script lang="ts">
import {onMount} from "svelte"
import {derived} from "svelte/store"
import {int, nthNe, MINUTE, sortBy, remove} from "@welshman/lib"
import type {Readable} from "svelte/store"
import type {Editor} from "svelte-tiptap"
import {nip19} from "nostr-tools"
import {int, nthNe, MINUTE, sortBy, remove, ctx} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {createEvent, DIRECT_MESSAGE, INBOX_RELAYS} from "@welshman/util"
import {
@@ -52,6 +55,15 @@
const showMembers = () =>
pushModal(ProfileList, {pubkeys: others, title: `People in this conversation`})
const replyTo = (event: TrustedEvent) => {
const relays = ctx.app.router.Event(event).getUrls()
const nevent = nip19.neventEncode({...event, relays})
$editor.commands.insertNEvent({nevent})
$editor.commands.insertContent("\n")
$editor.commands.focus()
}
const onSubmit = async ({content, ...params}: EventContent) => {
// Remove p tags since they result in forking the conversation
const tags = [...params.tags.filter(nthNe(0, "p")), ...remove($pubkey!, pubkeys).map(tagPubkey)]
@@ -64,6 +76,7 @@
}
let loading = true
let editor: Readable<Editor>
let elements: Element[] = []
$: {
@@ -170,7 +183,7 @@
{#if type === "date"}
<Divider>{value}</Divider>
{:else}
<ChatMessage event={assertEvent(value)} {pubkeys} {showPubkey} />
<ChatMessage event={assertEvent(value)} {pubkeys} {showPubkey} {replyTo} />
{/if}
{/each}
<p
@@ -185,5 +198,5 @@
<slot name="info" />
</p>
</div>
<ChatCompose {onSubmit} />
<ChatCompose bind:editor {onSubmit} />
</div>

View File

@@ -26,6 +26,7 @@
import {pushModal} from "@app/modal"
export let event: TrustedEvent
export let replyTo: any = undefined
export let pubkeys: string[]
export let showPubkey = false
@@ -59,6 +60,7 @@
<ThunkStatus {thunk} class="mt-1" />
{/if}
<div
data-event={event.id}
class="group chat flex items-center justify-end gap-1 px-2"
class:chat-start={event.pubkey !== $pubkey}
class:flex-row-reverse={event.pubkey !== $pubkey}
@@ -66,7 +68,7 @@
<Tippy
bind:popover
component={ChatMessageMenu}
props={{event, pubkeys, popover}}
props={{event, pubkeys, popover, replyTo}}
params={{
interactive: true,
trigger: "manual",

View File

@@ -8,6 +8,9 @@
export let event
export let pubkeys
export let popover
export let replyTo
const reply = () => replyTo(event)
const showInfo = () => {
popover.hide()
@@ -17,6 +20,11 @@
<div class="join border border-solid border-neutral text-xs">
<ChatMessageEmojiButton {event} {pubkeys} />
{#if replyTo}
<Button class="btn join-item btn-xs" on:click={reply}>
<Icon size={4} icon="reply" />
</Button>
{/if}
<Button class="btn join-item btn-xs" on:click={showInfo}>
<Icon size={4} icon="code-2" />
</Button>

View File

@@ -4,7 +4,7 @@
</script>
<code
class="w-full overflow-auto whitespace-pre rounded bg-neutral p-2 text-neutral-content"
class="w-full overflow-auto whitespace-pre rounded bg-neutral px-1 text-neutral-content"
class:block={isBlock}>
{value.trim()}
</code>

View File

@@ -1,13 +1,16 @@
<script lang="ts">
import {nip19} from "nostr-tools"
import {goto} from "$app/navigation"
import {ctx, nthEq} from "@welshman/lib"
import {Address} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import Link from "@lib/components/Link.svelte"
import {Address, DIRECT_MESSAGE} from "@welshman/util"
import {repository} from "@welshman/app"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import ChannelConversation from "@app/components/ChannelConversation.svelte"
import {deriveEvent, entityLink, MESSAGE, THREAD} from "@app/state"
import {makeThreadPath} from "@app/routes"
import {pushDrawer} from "@app/modal"
export let value
export let event
@@ -20,25 +23,63 @@
const quote = deriveEvent(idOrAddress, relays)
const entity = id ? nip19.neventEncode({id, relays}) : addr.toNaddr()
const getLocalHref = (e: TrustedEvent) => {
const url = e.tags.find(nthEq(0, "~"))?.[2]
const scrollToEvent = (id: string) => {
const element = document.querySelector(`[data-event="${id}"]`)
if (!url) return
if ([MESSAGE, THREAD].includes(e.kind)) return makeThreadPath(url, e.id)
if (element) {
element.scrollIntoView({behavior: "smooth"})
}
const kind = e.tags.find(nthEq(0, "K"))?.[1]
const id = e.tags.find(nthEq(0, "E"))?.[1]
if (!id || !kind) return
if ([MESSAGE, THREAD].includes(parseInt(kind))) return makeThreadPath(url, id)
return Boolean(element)
}
// If we found this event on a relay that the user is a member of, redirect internally
$: localHref = $quote ? getLocalHref($quote) : null
$: href = localHref || entityLink(entity)
const openMessage = (url: string, room: string, id: string) => {
const event = repository.getEvent(id)
if (event) {
return pushDrawer(ChannelConversation, {url, room, event})
}
return Boolean(event)
}
const onClick = (e: Event) => {
if ($quote) {
if ($quote.kind === DIRECT_MESSAGE) {
return scrollToEvent($quote.id)
}
const [room, url] = $quote.tags.find(nthEq(0, "~"))?.slice(1) || []
if (url && room) {
if ($quote.kind === THREAD) {
return goto(makeThreadPath(url, $quote.id))
}
if ($quote.kind === MESSAGE) {
return scrollToEvent($quote.id) || openMessage(url, room, $quote.id)
}
const kind = $quote.tags.find(nthEq(0, "K"))?.[1]
const id = $quote.tags.find(nthEq(0, "E"))?.[1]
if (id && kind) {
if (parseInt(kind) === THREAD) {
return goto(makeThreadPath(url, id))
}
if (parseInt(kind) === MESSAGE) {
return scrollToEvent(id) || openMessage(url, room, id)
}
}
}
}
window.open(entityLink(entity))
}
</script>
<Link external={!localHref} {href} class="my-2 block max-w-full text-left">
<Button class="my-2 block max-w-full text-left" on:click={onClick}>
{#if $quote}
<NoteCard event={$quote} class="bg-alt rounded-box p-4">
<slot name="note-content" event={$quote} {depth} />
@@ -48,4 +89,4 @@
<Spinner loading>Loading event...</Spinner>
</div>
{/if}
</Link>
</Button>

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import {nip19} from "nostr-tools"
import {ctx} from "@welshman/lib"
import Icon from "@lib/components/Icon.svelte"
import FieldInline from "@lib/components/FieldInline.svelte"
import Button from "@lib/components/Button.svelte"
@@ -8,10 +9,11 @@
export let event
const note1 = nip19.noteEncode(event.id)
const relays = ctx.app.router.Event(event).getUrls()
const nevent1 = nip19.neventEncode({...event, relays})
const npub1 = nip19.npubEncode(event.pubkey)
const json = JSON.stringify(event, null, 2)
const copyId = () => clip(note1)
const copyLink = () => clip(nevent1)
const copyPubkey = () => clip(npub1)
const copyJson = () => clip(json)
</script>
@@ -22,11 +24,11 @@
<div slot="info">The full details of this event are shown below.</div>
</ModalHeader>
<FieldInline>
<p slot="label">Event ID</p>
<p slot="label">Event Link</p>
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
<Icon icon="file" />
<input type="text" class="ellipsize min-w-0 grow" value={note1} />
<Button on:click={copyId} class="flex items-center">
<input type="text" class="ellipsize min-w-0 grow" value={nevent1} />
<Button on:click={copyLink} class="flex items-center">
<Icon icon="copy" />
</Button>
</label>

View File

@@ -6,6 +6,7 @@
import type {TrustedEvent} from "@welshman/util"
import {createFeedController} from "@welshman/app"
import {createScroller} from "@lib/html"
import {fly} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
@@ -54,7 +55,9 @@
<div class="col-4" bind:this={element}>
<div class="flex flex-col gap-2">
{#each events as event (event.id)}
<NoteItem {url} {event} />
<div in:fly>
<NoteItem {url} {event} />
</div>
{/each}
<p class="center my-12 flex">
<Spinner loading />

View File

@@ -11,7 +11,7 @@
export let selected: NodeViewProps["selected"]
const displayEvent = (e: TrustedEvent) => {
const content = e?.tags.find(nthEq(0, "alt"))?.[1] || e?.content
const content = e?.tags.find(nthEq(0, "alt"))?.[1] || e?.content || ""
return content.length > 1
? ellipsize(content, 30)

View File

@@ -8,9 +8,12 @@
</script>
<script lang="ts">
import {nip19} from "nostr-tools"
import {onMount, onDestroy} from "svelte"
import type {Readable} from "svelte/store"
import type {Editor} from "svelte-tiptap"
import {page} from "$app/stores"
import {sortBy, append, now} from "@welshman/lib"
import {sortBy, append, now, ctx} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {createEvent, DELETE} from "@welshman/util"
import {formatTimestampAsDate, publishThunk} from "@welshman/app"
@@ -47,6 +50,15 @@
const assertEvent = (e: any) => e as TrustedEvent
const replyTo = (event: TrustedEvent) => {
const relays = ctx.app.router.Event(event).getUrls()
const nevent = nip19.neventEncode({...event, relays})
$editor.commands.insertNEvent({nevent})
$editor.commands.insertContent("\n")
$editor.commands.focus()
}
const onSubmit = ({content, tags}: EventContent) =>
publishThunk({
relays: [url],
@@ -55,6 +67,7 @@
})
let loading = true
let editor: Readable<Editor>
let elements: Element[] = []
$: {
@@ -142,8 +155,8 @@
{#if type === "date"}
<Divider>{value}</Divider>
{:else}
<div in:slide>
<ChannelMessage {url} {room} event={assertEvent(value)} {showPubkey} />
<div in:slide class:-mt-4={!showPubkey}>
<ChannelMessage {url} {room} {replyTo} event={assertEvent(value)} {showPubkey} />
</div>
{/if}
{/each}
@@ -157,5 +170,5 @@
</Spinner>
</p>
</div>
<ChannelCompose {content} {onSubmit} />
<ChannelCompose bind:editor {content} {onSubmit} />
</div>

View File

@@ -2,7 +2,7 @@
import {onMount} from "svelte"
import {page} from "$app/stores"
import {sortBy, sleep, uniqBy, now} from "@welshman/lib"
import {getListTags, getPubkeyTagValues} from "@welshman/util"
import {getListTags, getPubkeyTagValues, LOCAL_RELAY_URL} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {feedsFromFilters, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
import {nthEq} from "@welshman/lib"
@@ -72,8 +72,21 @@
})
const unsub = subscribePersistent({
relays: [url],
filters: [{kinds: [COMMENT], "#K": [String(THREAD)], since: now()}],
relays: [url, LOCAL_RELAY_URL],
filters: [
{kinds: [THREAD], since: now()},
{kinds: [COMMENT], "#K": [String(THREAD)], since: now()},
],
onEvent: (event: TrustedEvent) => {
if (event.kind === THREAD) {
const index = Math.max(
0,
events.findIndex(e => e.created_at < event.created_at),
)
events = [...events.slice(0, index), event, ...events.slice(index)]
}
},
})
return () => {