diff --git a/src/app/shared/Feed.svelte b/src/app/shared/Feed.svelte index f3f63adbc..670e7c29f 100644 --- a/src/app/shared/Feed.svelte +++ b/src/app/shared/Feed.svelte @@ -39,7 +39,6 @@ import Card from "src/partials/Card.svelte" import Spinner from "src/partials/Spinner.svelte" import FlexColumn from "src/partials/FlexColumn.svelte" - import Note from "src/app/shared/Note.svelte" import FeedControls from "src/app/shared/FeedControls.svelte" import {router} from "src/app/util" import type {Feed} from "src/domain" @@ -51,6 +50,7 @@ isEventMuted, unwrapRepost, } from "src/engine" + import FeedItem from "src/app/shared/FeedItem.svelte" export let feed: Feed export let anchor = null @@ -241,10 +241,10 @@ {/if} - + {#each events as note, i (note.id)}
- +
{#if i > 20 && parseInt(hash(note.id)) % 100 === 0 && $promptDismissed < ago(WEEK)} diff --git a/src/app/shared/FeedCard.svelte b/src/app/shared/FeedCard.svelte index b794b0aaa..c4447d53f 100644 --- a/src/app/shared/FeedCard.svelte +++ b/src/app/shared/FeedCard.svelte @@ -12,7 +12,6 @@ import {slide} from "src/util/transition" import {boolCtrl} from "src/partials/utils" import FlexColumn from "src/partials/FlexColumn.svelte" - import Card from "src/partials/Card.svelte" import Chip from "src/partials/Chip.svelte" import Anchor from "src/partials/Anchor.svelte" import CopyValueSimple from "src/partials/CopyValueSimple.svelte" @@ -49,7 +48,7 @@ ) - +
@@ -116,4 +115,4 @@ )} {/if} - +
diff --git a/src/app/shared/FeedItem.svelte b/src/app/shared/FeedItem.svelte new file mode 100644 index 000000000..0e0dfcdba --- /dev/null +++ b/src/app/shared/FeedItem.svelte @@ -0,0 +1,206 @@ + + +{#if ready} +
+ +
+ {#if !showParent && !topLevel} + + + + + + {#if isLastReply} + + {:else} + + {/if} + {/if} + + {#if !replyIsActive && (visibleReplies.length > 0 || collapsed) && !showEntire && depth > 0} +
+ { + collapsed = !collapsed + }}> + +
+ +
+
+ {collapsed ? "Show replies" : "Hide replies"} +
+
+
+
+ {/if} + + {#if visibleReplies.length > 0 || hiddenReplies.length > 0 || mutedReplies.length > 0} +
+ {#if hiddenReplies.length > 0} + + {#if visibleReplies.length > 0} + + {/if} + {:else if visibleReplies.length > 0} + + {/if} + {#if visibleReplies.length} + {#key showHiddenReplies} +
+ {#each visibleReplies as r, i (r.id)} + + {/each} +
+ {/key} + {/if} + {#if showHiddenReplies && mutedReplies.length > 0} + + {/if} +
+ {/if} +
+
+{:else if showLoading} + +{/if} diff --git a/src/app/shared/Note.svelte b/src/app/shared/Note.svelte index 27dd21cac..bc3fab59d 100644 --- a/src/app/shared/Note.svelte +++ b/src/app/shared/Note.svelte @@ -1,89 +1,23 @@ -{#if ready} - {@const showReply = reply && !ancestors.replies.includes(anchor) && showParent} - {@const showRoot = root && !ancestors.roots.includes(anchor) && root !== reply && showParent} -
- -
- {#if !showParent && !topLevel} - - - - - - {#if isLastReply} - - {:else} - - {/if} - {/if} -
- -
- - - -
-
-
- - - -
- - {formatTimestamp(event.created_at)} - -
-
-
-
- {#if showReply} - - - View Parent - - {/if} - {#if showRoot} - - - View Thread - - {/if} -
- {#if hidden && !showHidden} -

- You have hidden this note. - { - showHidden = true - }}>Show -

- {:else} - - {/if} -
- {#if !isDraft || event.created_at < $timestamp1 - 45} - - {:else} - - {/if} -
-
- +
+ + {#if note.kind !== 31890} + + {/if} + {#if hidden && !showHidden} +

+ You have hidden this note. + { + showHidden = true + }}>Show +

+ {:else} +
+
- - {#if !replyIsActive && (visibleReplies.length > 0 || collapsed) && !showEntire && depth > 0} -
- { - collapsed = !collapsed - }}> - -
- -
-
- {collapsed ? "Show replies" : "Hide replies"} -
-
-
-
- {/if} - - 0} - bind:this={replyCtrl} - on:start={() => { - replyIsActive = true - }} - on:reset={() => { - replyIsActive = false - }} /> - - {#if visibleReplies.length > 0 || hiddenReplies.length > 0 || mutedReplies.length > 0} -
- {#if hiddenReplies.length > 0} - - {#if visibleReplies.length > 0} - - {/if} - {:else if visibleReplies.length > 0} - - {/if} - {#if visibleReplies.length} - {#key showHiddenReplies} -
- {#each visibleReplies as r, i (r.id)} - - {/each} -
- {/key} - {/if} - {#if showHiddenReplies && mutedReplies.length > 0} - - {/if} -
- {/if} -
-
-{:else if showLoading} - -{/if} +
+ +
+ {/if} +
+ +
diff --git a/src/app/shared/NoteActions.svelte b/src/app/shared/NoteActions.svelte index a41f5cce1..a64914c2d 100644 --- a/src/app/shared/NoteActions.svelte +++ b/src/app/shared/NoteActions.svelte @@ -5,9 +5,20 @@ import {onMount} from "svelte" import {tweened} from "svelte/motion" import {derived} from "svelte/store" - import {ctx, nth, nthEq, remove, last, sortBy} from "@welshman/lib" - import {repository, signer, tagReactionTo, tagZapSplit, mute, unmute} from "@welshman/app" + import {ctx, nth, nthEq, remove, last, sortBy, uniqBy, prop, identity} from "@welshman/lib" + import { + deriveZapper, + deriveZapperForPubkey, + repository, + signer, + tagReactionTo, + tagZapSplit, + mute, + pubkey, + unmute, + } from "@welshman/app" import type {TrustedEvent, SignedEvent} from "@welshman/util" + import {deriveEvents} from "@welshman/store" import { LOCAL_RELAY_URL, toNostrURI, @@ -15,9 +26,16 @@ isSignedEvent, createEvent, getPubkeyTagValues, + getLnUrl, + zapFromEvent, + NOTE, + REACTION, + ZAP_RESPONSE, + getReplyFilters, + isChildOf, } from "@welshman/util" import {fly} from "src/util/transition" - import {formatSats} from "src/util/misc" + import {formatSats, timestamp1} from "src/util/misc" import {quantify, pluralize} from "hurdak" import {browser} from "src/partials/state" import {showInfo} from "src/partials/Toast.svelte" @@ -35,6 +53,7 @@ import PersonBadge from "src/app/shared/PersonBadge.svelte" import HandlerCard from "src/app/shared/HandlerCard.svelte" import RelayCard from "src/app/shared/RelayCard.svelte" + import NotePending from "src/app/shared/NotePending.svelte" import {router} from "src/app/util/router" import { env, @@ -47,35 +66,36 @@ getClientTags, trackerStore, sessionWithMeta, + userMutes, + sortEventsDesc, + load, } from "src/engine" import {getHandlerKey, readHandlers, displayHandler} from "src/domain" + import {openReplies} from "src/app/state" + import {isLike} from "src/util/nostr" - export let note: TrustedEvent - export let muted - export let replyCtrl - export let showHidden - export let replies, likes, zaps - export let zapper + export let event: TrustedEvent + export let showHidden = false - const signedEvent = asSignedEvent(note as any) + const signedEvent = asSignedEvent(event as any) const nevent = nip19.neventEncode({ - id: note.id, - kind: note.kind, - author: note.pubkey, - relays: ctx.app.router.Event(note).getUrls(), + id: event.id, + kind: event.kind, + author: event.pubkey, + relays: ctx.app.router.Event(event).getUrls(), }) const interpolate = (a, b) => t => a + Math.round((b - a) * t) - const mentions = getPubkeyTagValues(note.tags) + const mentions = getPubkeyTagValues(event.tags) const likesCount = tweened(0, {interpolate}) const zapsTotal = tweened(0, {interpolate}) const repliesCount = tweened(0, {interpolate}) - const kindHandlers = deriveHandlersForKind(note.kind) - const handlerId = String(note.tags.find(nthEq(0, "client"))?.[2] || "") + const kindHandlers = deriveHandlersForKind(event.kind) + const handlerId = String(event.tags.find(nthEq(0, "client"))?.[2] || "") const handlerEvent = handlerId ? repository.getEvent(handlerId) : null const noteActions = getSetting("note_actions") const seenOn = derived(trackerStore, $t => - remove(LOCAL_RELAY_URL, Array.from($t.getRelays(note.id))), + remove(LOCAL_RELAY_URL, Array.from($t.getRelays(event.id))), ) const setView = v => { @@ -92,20 +112,21 @@ const os = browser.os?.name?.toLowerCase() - const createLabel = () => router.at("notes").of(note.id).at("label").open() + const createLabel = () => router.at("notes").of(event.id).at("label").open() - const quote = () => router.at("notes/create").cx({quote: note}).open() + const quote = () => router.at("notes/create").cx({quote: event}).open() - const report = () => router.at("notes").of(note.id).at("report").open() + const report = () => router.at("notes").of(event.id).at("report").open() - const deleteNote = () => router.at("notes").of(note.id).at("delete").qp({kind: note.kind}).open() + const deleteNote = () => + router.at("notes").of(event.id).at("delete").qp({kind: event.kind}).open() const react = async content => { - if (isSignedEvent(note)) { - publish({event: note, relays: ctx.app.router.PublishEvent(note).getUrls()}) + if (isSignedEvent(event)) { + publish({event: event, relays: ctx.app.router.PublishEvent(event).getUrls()}) } - const tags = [...tagReactionTo(note), ...getClientTags()] + const tags = [...tagReactionTo(event), ...getClientTags()] const template = createEvent(7, {content, tags}) await signAndPublish(template) } @@ -115,23 +136,23 @@ } const startZap = () => { - const zapTags = note.tags.filter(nthEq(0, "zap")) - const defaultSplit = tagZapSplit(note.pubkey) + const zapTags = event.tags.filter(nthEq(0, "zap")) + const defaultSplit = tagZapSplit(event.pubkey) const splits = zapTags.length > 0 ? zapTags : [defaultSplit] router .at("zap") .qp({ splits, - id: note.id, - anonymous: Boolean(note.wrap), + id: event.id, + anonymous: Boolean(event.wrap), }) .open() } const broadcast = () => { publish({ - event: asSignedEvent(note as SignedEvent), + event: asSignedEvent(event as SignedEvent), relays: ctx.app.router.FromUser().getUrls(), }) @@ -150,32 +171,51 @@ return 0 }, handler.event.tags) - const entity = last(templateTag) === "note" ? nip19.noteEncode(note.id) : nevent + const entity = last(templateTag) === "note" ? nip19.noteEncode(event.id) : nevent window.open(templateTag[1].replace("", entity)) } + const context = deriveEvents(repository, {filters: getReplyFilters([event])}) + + $: children = $context.filter(e => isChildOf(e, event)) + let view let actions = [] let handlersShown = false + $: lnurl = getLnUrl(event.tags?.find(nthEq(0, "zap"))?.[1] || "") + $: zapper = lnurl ? deriveZapper(lnurl) : deriveZapperForPubkey(event.pubkey) + $: muted = $userMutes.has(event.id) + // Split out likes, uniqify by pubkey since a like can be duplicated across groups + $: likes = uniqBy(prop("pubkey"), children.filter(isLike)) + + // Split out zaps + $: zaps = children + .filter(e => e.kind === 9735) + .map(e => ($zapper ? zapFromEvent(e, $zapper) : null)) + .filter(identity) + $: replies = sortEventsDesc(children.filter(e => e.kind == NOTE)) + $: disableActions = !$signer || (muted && !showHidden) - $: like = likes.find(e => e.pubkey === $sessionWithMeta?.pubkey) + $: liked = likes.find(e => e.pubkey === $sessionWithMeta?.pubkey) $: $likesCount = likes.length - $: zap = zaps.find(e => e.request.pubkey === $sessionWithMeta?.pubkey) + $: zapped = zaps.find(e => e.request.pubkey === $sessionWithMeta?.pubkey) $: $zapsTotal = sum(pluck("invoiceAmount", zaps)) / 1000 - $: canZap = zapper?.allowsNostr && note.pubkey !== $sessionWithMeta?.pubkey - $: reply = replies.find(e => e.pubkey === $sessionWithMeta?.pubkey) + $: canZap = $zapper?.allowsNostr && event.pubkey !== $sessionWithMeta?.pubkey + $: replied = replies.find(e => e.pubkey === $sessionWithMeta?.pubkey) $: $repliesCount = replies.length - $: handlers = $kindHandlers.filter( - h => - h.name.toLowerCase() !== "coracle" && - h.event.tags.some( - t => - ["web", os].includes(t[0]) && - (t.length === 2 || ["note", "nevent", ""].includes(last(t))), - ), - ) + $: handlers = + event.kind != 1 && + $kindHandlers.filter( + h => + h.name.toLowerCase() !== "coracle" && + h.event.tags.some( + t => + ["web", os].includes(t[0]) && + (t.length == 2 || ["note", "nevent", ""].includes(last(t))), + ), + ) $: { actions = [] @@ -186,19 +226,23 @@ actions.push({label: "Tag", icon: "tag", onClick: createLabel}) if (muted) { - actions.push({label: "Unmute", icon: "microphone", onClick: () => unmute(note.id)}) + actions.push({label: "Unmute", icon: "microphone", onClick: () => unmute(event.id)}) } else { - actions.push({label: "Mute", icon: "microphone-slash", onClick: () => mute(["e", note.id])}) + actions.push({ + label: "Mute", + icon: "microphone-slash", + onClick: () => mute(["e", event.id]), + }) } actions.push({label: "Report", icon: "triangle-exclamation", onClick: report}) } - if (env.PLATFORM_RELAYS.length === 0 && isSignedEvent(note)) { + if (env.PLATFORM_RELAYS.length === 0 && isSignedEvent(event)) { actions.push({label: "Broadcast", icon: "rss", onClick: broadcast}) } - if (note.pubkey === $sessionWithMeta?.pubkey) { + if (event.pubkey === $sessionWithMeta?.pubkey) { actions.push({ label: "Delete", icon: "trash", @@ -214,181 +258,207 @@ } onMount(() => { - loadPubkeys(note.tags.filter(nthEq(0, "zap")).map(nth(1))) + loadPubkeys(event.tags.filter(nthEq(0, "zap")).map(nth(1))) + + const actions = getSetting("note_actions") + const kinds = [] + + if (actions.includes("replies")) { + kinds.push(NOTE) + } + + if (actions.includes("reactions")) { + kinds.push(REACTION) + } + + if (env.ENABLE_ZAPS && actions.includes("zaps")) { + kinds.push(ZAP_RESPONSE) + } + + load({ + relays: ctx.app.router.Replies(event).getUrls(), + filters: getReplyFilters([event], {kinds}), + }) }) - - {#if env.ENABLE_ZAPS && noteActions.includes("zaps")} +{#if event.created_at > $timestamp1 - 45 && event.pubkey === $pubkey} + +{:else} + - {/if} - {#if noteActions.includes("reactions")} - - {/if} - {#if handlers.length > 0 && noteActions.includes("recommended_apps")} - + {#if env.ENABLE_ZAPS && noteActions.includes("zaps")} -
- - Open with: - {#each handlers as handler} - openWithHandler(handler)}> -
- - {handler.name} -
- {#if handler.recommendations.length > 0} - - {/if} -
- {/each} -
-
-
- {/if} -
-
- {#if note.wrap} -
- - -
- {/if} - {#if $seenOn?.length > 0 && (env.PLATFORM_RELAYS.length === 0 || env.PLATFORM_RELAYS.length > 1)} - - {/if} - -
- - -{#if view} - setView(null)}> - {#if view === "info"} - {#if zaps.length > 0} -

Zapped By

-
- {#each zaps as zap} -
- - {formatSats(zap.invoiceAmount / 1000)} sats -
- {/each} -
{/if} - {#if likes.length > 0} -

Liked By

-
- {#each likes as like} - - {/each} -
+ {#if noteActions.includes("reactions")} + {/if} - {#if $seenOn?.length > 0 && (env.PLATFORM_RELAYS.length === 0 || env.PLATFORM_RELAYS.length > 1)} -

Relays

-

This note was found on {quantify($seenOn.length, "relay")} below.

-
- {#each $seenOn as url} - - {/each} + {#if handlers.length > 0 && noteActions.includes("recommended_apps")} + + +
+ + Open with: + {#each handlers as handler} + openWithHandler(handler)}> +
+ + {handler.name} +
+ {#if handler.recommendations.length > 0} + + {/if} +
+ {/each} +
+
+
+ {/if} +
+
+ {#if event.wrap} +
+ +
{/if} - {#if mentions.length > 0} -

In this conversation

-

{quantify(mentions.length, "person is", "people are")} tagged in this note.

-
- {#each mentions as pubkey} - - {/each} + {#if $seenOn?.length > 0 && (env.PLATFORM_RELAYS.length === 0 || env.PLATFORM_RELAYS.length > 1)} + {/if} - {#if handlers.length > 0 || handlerEvent} -

Apps

- {#if handlerEvent} - {@const [handler] = readHandlers(handlerEvent)} - {#if handler} -

This note was published using {displayHandler(handler)}.

- - {/if} + +
+ + + {#if view} + setView(null)}> + {#if view === "info"} + {#if zaps.length > 0} +

Zapped By

+
+ {#each zaps as zap} +
+ + {formatSats(zap.invoiceAmount / 1000)} sats +
+ {/each} +
{/if} - {#if handlers.length > 0} -
-

- This note can also be viewed using {quantify(handlers.length, "other nostr app")}. -

- {#if handlersShown} - Hide apps - {:else} - Show apps - {/if} + {#if likes.length > 0} +

Liked By

+
+ {#each likes as like} + + {/each} +
+ {/if} + {#if $seenOn?.length > 0 && (env.PLATFORM_RELAYS.length === 0 || env.PLATFORM_RELAYS.length > 1)} +

Relays

+

This note was found on {quantify($seenOn.length, "relay")} below.

+
+ {#each $seenOn as url} + + {/each} +
+ {/if} + {#if mentions.length > 0} +

In this conversation

+

{quantify(mentions.length, "person is", "people are")} tagged in this note.

+
+ {#each mentions as pubkey} + + {/each}
- {#if handlersShown} -
- - {#each handlers as handler (getHandlerKey(handler))} - - {/each} - + {/if} + {#if handlers.length > 0 || handlerEvent} +

Apps

+ {#if handlerEvent} + {@const [handler] = readHandlers(handlerEvent)} + {#if handler} +

This note was published using {displayHandler(handler)}.

+ + {/if} + {/if} + {#if handlers.length > 0} +
+

+ This note can also be viewed using {quantify(handlers.length, "other nostr app")}. +

+ {#if handlersShown} + Hide apps + {:else} + Show apps + {/if}
+ {#if handlersShown} +
+ + {#each handlers as handler (getHandlerKey(handler))} + + {/each} + +
+ {/if} {/if} {/if} +

Details

+ + + {/if} -

Details

- - - - {/if} - + + {/if} {/if} diff --git a/src/app/shared/NoteContent.svelte b/src/app/shared/NoteContent.svelte index eea2a77e6..4899aeb01 100644 --- a/src/app/shared/NoteContent.svelte +++ b/src/app/shared/NoteContent.svelte @@ -27,7 +27,6 @@ export let note export let depth = 0 export let showEntire = false - export let expandable = true export let showMedia = getSetting("show_media") let warning = getSetting("hide_sensitive") ? getContentWarning(note) : null @@ -85,7 +84,7 @@ {:else if CUSTOM_LIST_KINDS.includes(note.kind)} {:else} - +
diff --git a/src/app/shared/NoteHeader.svelte b/src/app/shared/NoteHeader.svelte new file mode 100644 index 000000000..909502d33 --- /dev/null +++ b/src/app/shared/NoteHeader.svelte @@ -0,0 +1,80 @@ + + +
+
+ + + +
+
+
+ + + +
+ + {formatTimestamp(event.created_at)} + +
+
+
+
+ {#if showReply} + + + View Parent + + {/if} + {#if showRoot} + + + View Thread + + {/if} +
+
+
+
diff --git a/src/app/shared/NotePending.svelte b/src/app/shared/NotePending.svelte index 8b74a7ee6..b154cfeb9 100644 --- a/src/app/shared/NotePending.svelte +++ b/src/app/shared/NotePending.svelte @@ -18,17 +18,19 @@ import {thunks, type Thunk} from "@welshman/app" import {PublishStatus} from "@welshman/net" import {now} from "@welshman/signer" - import {LOCAL_RELAY_URL, type SignedEvent} from "@welshman/util" + import {getAncestorTagValues, LOCAL_RELAY_URL, type TrustedEvent} from "@welshman/util" import {tweened} from "svelte/motion" import {userSettings} from "src/engine" import Anchor from "src/partials/Anchor.svelte" import {timestamp1} from "src/util/misc" + import {openReplies} from "../state" const rendered = now() - export let event: SignedEvent - export let removeDraft: () => void + export let event: TrustedEvent + $: ancestors = getAncestorTagValues(event.tags || []) + $: parent = ancestors.replies[0] $: thunk = $thunks[event.id] as Thunk $: status = thunk?.status @@ -42,7 +44,7 @@ ).length $: timeout = statuses.filter(s => s.status === PublishStatus.Timeout).length $: success = statuses.filter(s => s.status === PublishStatus.Success).length - $: total = relays.length || 0 + $: total = relays?.length || 0 const completed = tweened(0) @@ -88,6 +90,9 @@ >Sending reply in {rendered + Math.ceil($userSettings.send_delay / 1000) - $timestamp1} seconds + on:click={() => { + thunk.controller.abort() + $openReplies[parent] = true + }}>Cancel {/if}
diff --git a/src/app/shared/NoteReply.svelte b/src/app/shared/NoteReply.svelte index f28a514e6..f153ea5fd 100644 --- a/src/app/shared/NoteReply.svelte +++ b/src/app/shared/NoteReply.svelte @@ -1,45 +1,39 @@ @@ -166,11 +150,7 @@ {#if showBorder} {/if} -
+
diff --git a/src/app/shared/PersonCollections.svelte b/src/app/shared/PersonCollections.svelte index 77fd76b8f..17b0ac9b6 100644 --- a/src/app/shared/PersonCollections.svelte +++ b/src/app/shared/PersonCollections.svelte @@ -1,7 +1,7 @@ - + {#if icon === "bolt"} + {:else if icon == "openwith"} + {/if} diff --git a/src/partials/Modal.svelte b/src/partials/Modal.svelte index cf8ce3c40..aa4d586d2 100644 --- a/src/partials/Modal.svelte +++ b/src/partials/Modal.svelte @@ -100,7 +100,7 @@
+ on:click|stopPropagation={tryClose}>