mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-10 19:07:06 +00:00
Listen for new threads, add reply/quote button to channels and chats, better quote handling
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user