diff --git a/src/app/components/CalendarEventActions.svelte b/src/app/components/CalendarEventActions.svelte index 1d971de5..7e6030ec 100644 --- a/src/app/components/CalendarEventActions.svelte +++ b/src/app/components/CalendarEventActions.svelte @@ -1,31 +1,33 @@ -
-
- - - {#if showActivity} - - {/if} - - {#snippet customActions()} - {#if event.pubkey === $pubkey} -
  • - -
  • - {/if} - {/snippet} -
    -
    +
    + {#if room && showRoom} + + Posted in # + + {/if} + + + {#if showActivity} + + {/if} + + {#snippet customActions()} + {#if event.pubkey === $pubkey} +
  • + +
  • + {/if} + {/snippet} +
    diff --git a/src/app/components/CalendarEventCreate.svelte b/src/app/components/CalendarEventCreate.svelte index 76d02fe6..733db40e 100644 --- a/src/app/components/CalendarEventCreate.svelte +++ b/src/app/components/CalendarEventCreate.svelte @@ -4,12 +4,13 @@ type Props = { url: string + room?: string } - const {url}: Props = $props() + const {url, room}: Props = $props() - + {#snippet header()} {#snippet title()} diff --git a/src/app/components/CalendarEventDate.svelte b/src/app/components/CalendarEventDate.svelte index f72f6ada..f97949b6 100644 --- a/src/app/components/CalendarEventDate.svelte +++ b/src/app/components/CalendarEventDate.svelte @@ -8,13 +8,16 @@ const {event}: Props = $props() const meta = $derived(fromPairs(event.tags) as Record) - const startDate = $derived(secondsToDate(parseInt(meta.start))) + const start = $derived(parseInt(meta.start)) - +{#if !isNaN(start)} + {@const startDate = secondsToDate(start)} + +{/if} diff --git a/src/app/components/CalendarEventForm.svelte b/src/app/components/CalendarEventForm.svelte index 3524f60d..71c5095b 100644 --- a/src/app/components/CalendarEventForm.svelte +++ b/src/app/components/CalendarEventForm.svelte @@ -23,6 +23,7 @@ type Props = { url: string + room?: string header: Snippet initialValues?: { d: string @@ -34,7 +35,7 @@ } } - const {url, header, initialValues}: Props = $props() + const {url, room, header, initialValues}: Props = $props() const shouldProtect = canEnforceNip70(url) @@ -84,6 +85,10 @@ tags.push(PROTECTED) } + if (room) { + tags.push(["h", room]) + } + const event = makeEvent(EVENT_TIME, {content, tags}) pushToast({message: "Your event has been saved!"}) diff --git a/src/app/components/CalendarEventHeader.svelte b/src/app/components/CalendarEventHeader.svelte index b097cd32..857b2e0e 100644 --- a/src/app/components/CalendarEventHeader.svelte +++ b/src/app/components/CalendarEventHeader.svelte @@ -17,18 +17,20 @@ const meta = $derived(fromPairs(event.tags) as Record) const start = $derived(parseInt(meta.start)) const end = $derived(parseInt(meta.end)) - const startDateDisplay = $derived(formatTimestampAsDate(start)) - const endDateDisplay = $derived(formatTimestampAsDate(end)) - const isSingleDay = $derived(startDateDisplay === endDateDisplay)

    {meta.title || meta.name}

    -
    - - {formatTimestampAsDate(start)} - {formatTimestampAsTime(start)} — {isSingleDay - ? formatTimestampAsTime(end) - : formatTimestamp(end)} -
    + {#if !isNaN(start) && !isNaN(end)} + {@const startDateDisplay = formatTimestampAsDate(start)} + {@const endDateDisplay = formatTimestampAsDate(end)} + {@const isSingleDay = startDateDisplay === endDateDisplay} +
    + + + {formatTimestampAsTime(start)} — {isSingleDay + ? formatTimestampAsTime(end) + : formatTimestamp(end)} +
    + {/if}
    diff --git a/src/app/components/CalendarEventItem.svelte b/src/app/components/CalendarEventItem.svelte index 497dd1e0..483b02f7 100644 --- a/src/app/components/CalendarEventItem.svelte +++ b/src/app/components/CalendarEventItem.svelte @@ -1,9 +1,11 @@ @@ -19,6 +23,9 @@
    Posted by + {#if room} + in + {/if}
    diff --git a/src/app/components/ChannelCompose.svelte b/src/app/components/ChannelCompose.svelte index 8762d6bc..989a45b0 100644 --- a/src/app/components/ChannelCompose.svelte +++ b/src/app/components/ChannelCompose.svelte @@ -1,23 +1,28 @@
    - +
    + + + + +
    diff --git a/src/app/components/ChannelComposeParent.svelte b/src/app/components/ChannelComposeParent.svelte index 008e8c4b..32b0c07e 100644 --- a/src/app/components/ChannelComposeParent.svelte +++ b/src/app/components/ChannelComposeParent.svelte @@ -5,7 +5,7 @@ import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import Icon from "@lib/components/Icon.svelte" import Button from "@lib/components/Button.svelte" - import NoteContent from "@app/components/NoteContent.svelte" + import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte" const { verb, @@ -19,16 +19,11 @@
    -

    {verb} @{displayProfileByPubkey(event.pubkey)}

    +

    {verb} @{displayProfileByPubkey(event.pubkey)}

    {#key event.id} - + {/key} - {#if ENABLE_ZAPS} - - - Send Zap - - {/if} - - +
    {#if event.pubkey === $pubkey} {/if} + + {#if path} + + + View Details + + {/if} + + + {#if ENABLE_ZAPS} + + + Zap + + {/if}
    diff --git a/src/app/components/ChannelMessageZapButton.svelte b/src/app/components/ChannelItemZapButton.svelte similarity index 100% rename from src/app/components/ChannelMessageZapButton.svelte rename to src/app/components/ChannelItemZapButton.svelte diff --git a/src/app/components/ChannelLink.svelte b/src/app/components/ChannelLink.svelte new file mode 100644 index 00000000..058578dc --- /dev/null +++ b/src/app/components/ChannelLink.svelte @@ -0,0 +1,21 @@ + + + + # + diff --git a/src/app/components/ChatComposeParent.svelte b/src/app/components/ChatComposeParent.svelte index 008e8c4b..32b0c07e 100644 --- a/src/app/components/ChatComposeParent.svelte +++ b/src/app/components/ChatComposeParent.svelte @@ -5,7 +5,7 @@ import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import Icon from "@lib/components/Icon.svelte" import Button from "@lib/components/Button.svelte" - import NoteContent from "@app/components/NoteContent.svelte" + import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte" const { verb, @@ -19,16 +19,11 @@
    -

    {verb} @{displayProfileByPubkey(event.pubkey)}

    +

    {verb} @{displayProfileByPubkey(event.pubkey)}

    {#key event.id} - + {/key} + +
  • + +
  • +
  • + +
  • + diff --git a/src/app/components/Content.svelte b/src/app/components/Content.svelte index f6c81578..72f78ff4 100644 --- a/src/app/components/Content.svelte +++ b/src/app/components/Content.svelte @@ -18,6 +18,7 @@ isAddress, isNewline, } from "@welshman/content" + import type {Parsed} from "@welshman/content" import {preventDefault, stopPropagation} from "@lib/html" import Link from "@lib/components/Link.svelte" import Danger from "@assets/icons/danger-triangle.svg?dataurl" @@ -39,10 +40,8 @@ minLength?: number maxLength?: number showEntire?: boolean - hideMediaAtDepth?: number expandMode?: string - minimalQuote?: boolean - depth?: number + trimParent?: boolean url?: string } @@ -51,10 +50,8 @@ minLength = 500, maxLength = 700, showEntire = $bindable(false), - hideMediaAtDepth = 1, expandMode = "block", - minimalQuote = false, - depth = 0, + trimParent = false, url, }: Props = $props() @@ -67,13 +64,13 @@ const isBlock = (i: number) => { const parsed = fullContent[i] - if (!parsed || hideMediaAtDepth <= depth) return false + if (!parsed) return false if (isLink(parsed) && $userSettingsValues.show_media && isStartAndEnd(i)) { return true } - if ((isEvent(parsed) || isAddress(parsed)) && isStartAndEnd(i)) { + if (isQuote(parsed) && isStartAndEnd(i)) { return true } @@ -95,6 +92,8 @@ const isStartAndEnd = (i: number) => isStart(i) && isEnd(i) + const isQuote = (p: Parsed) => isEvent(p) || isAddress(p) + const ignoreWarning = () => { warning = null } @@ -103,15 +102,37 @@ $userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1], ) - const shortContent = $derived( - showEntire - ? fullContent - : truncate(fullContent, { - minLength, - maxLength, - mediaLength: hideMediaAtDepth <= depth ? 20 : 200, - }), - ) + const dropWhile = (f: (x: T) => boolean, xs: Iterable) => { + const result: T[] = [] + + for (const x of xs) { + if (result.length === 0 && f(x)) { + continue + } + + result.push(x) + } + + return result + } + + const shortContent = $derived.by(() => { + let result = fullContent + + if (trimParent && result.length > 0 && isQuote(result[0])) { + result = dropWhile(p => isQuote(p) || isNewline(p), result) + } + + if (!showEntire) { + result = truncate(result, { + minLength, + maxLength, + mediaLength: 200, + }) + } + + return result + }) const hasEllipsis = $derived(shortContent.some(isEllipsis)) const expandInline = $derived(hasEllipsis && expandMode === "inline") @@ -152,15 +173,9 @@ {/if} {:else if isProfile(parsed)} - {:else if isEvent(parsed) || isAddress(parsed)} + {:else if isQuote(parsed)} {#if isBlock(i)} - + {:else} + import {fromNostrURI} from "@welshman/util" + import {nthEq} from "@welshman/lib" + import { + parse, + truncate, + renderAsHtml, + isText, + isEmoji, + isTopic, + isCode, + isCashu, + isInvoice, + isLink, + isProfile, + isEvent, + isAddress, + isNewline, + } from "@welshman/content" + import type {Parsed} from "@welshman/content" + import Link from "@lib/components/Link.svelte" + import Danger from "@assets/icons/danger-triangle.svg?dataurl" + import Icon from "@lib/components/Icon.svelte" + import Button from "@lib/components/Button.svelte" + import ContentToken from "@app/components/ContentToken.svelte" + import ContentEmoji from "@app/components/ContentEmoji.svelte" + import ContentCode from "@app/components/ContentCode.svelte" + import ContentLinkInline from "@app/components/ContentLinkInline.svelte" + import ContentNewline from "@app/components/ContentNewline.svelte" + import ContentTopic from "@app/components/ContentTopic.svelte" + import ContentMention from "@app/components/ContentMention.svelte" + import {entityLink, userSettingsValues} from "@app/core/state" + + interface Props { + event: any + trimParent?: boolean + url?: string + } + + const {event, trimParent = false, url}: Props = $props() + + const fullContent = parse(event) + + const isBoundary = (i: number) => { + const parsed = fullContent[i] + + if (!parsed || isNewline(parsed)) return true + if (isText(parsed)) return Boolean(parsed.value.match(/^\s+$/)) + + return false + } + + const isStart = (i: number) => isBoundary(i - 1) + + const isEnd = (i: number) => isBoundary(i + 1) + + const isStartAndEnd = (i: number) => isStart(i) && isEnd(i) + + const isQuote = (p: Parsed) => isEvent(p) || isAddress(p) + + const ignoreWarning = () => { + warning = null + } + + let warning = $state( + $userSettingsValues.hide_sensitive && event.tags.find(nthEq(0, "content-warning"))?.[1], + ) + + const dropWhile = (f: (x: T) => boolean, xs: Iterable) => { + const result: T[] = [] + + for (const x of xs) { + if (result.length === 0 && f(x)) { + continue + } + + result.push(x) + } + + return result + } + + const shortContent = $derived.by(() => { + let result = fullContent + + if (trimParent && result.length > 0 && isQuote(result[0])) { + result = dropWhile(p => isQuote(p) || isNewline(p), result) + } + + return truncate(result, {minLength: 200, maxLength: 300, mediaLength: 20}) + }) + + +
    + {#if warning} +
    + +

    + This note has been flagged by the author as "{warning}".
    + +

    +
    + {:else} +
    + {#each shortContent as parsed, i} + {#if isNewline(parsed)} + + {:else if isTopic(parsed)} + + {:else if isEmoji(parsed)} + + {:else if isCode(parsed)} + + {:else if isCashu(parsed) || isInvoice(parsed)} + + {:else if isLink(parsed)} + + {:else if isProfile(parsed)} + + {:else if isQuote(parsed)} + + {fromNostrURI(parsed.raw).slice(0, 16) + "…"} + + {:else} + {@html renderAsHtml(parsed)} + {/if} + {/each} +
    + {/if} +
    diff --git a/src/app/components/ContentQuote.svelte b/src/app/components/ContentQuote.svelte index bbfc93f0..2d2e8853 100644 --- a/src/app/components/ContentQuote.svelte +++ b/src/app/components/ContentQuote.svelte @@ -6,20 +6,17 @@ import Button from "@lib/components/Button.svelte" import Spinner from "@lib/components/Spinner.svelte" import NoteCard from "@app/components/NoteCard.svelte" - import NoteContent from "@app/components/NoteContent.svelte" + import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte" import {deriveEvent, entityLink} from "@app/core/state" import {goToEvent} from "@app/util/routes" type Props = { value: any - hideMediaAtDepth: number event: TrustedEvent - depth: number url?: string - minimal?: boolean } - const {value, event, depth, hideMediaAtDepth, url, minimal}: Props = $props() + const {value, event, url}: Props = $props() const {id, identifier, kind, pubkey, relays = []} = value const idOrAddress = id || new Address(kind, pubkey, identifier).toString() @@ -43,17 +40,17 @@ } -
    - +
    @@ -67,7 +67,7 @@ {formatTimestamp(latest.created_at)}
    - + {/if} diff --git a/src/app/components/EventMenu.svelte b/src/app/components/EventMenu.svelte index 7aecd8f6..3664e8f3 100644 --- a/src/app/components/EventMenu.svelte +++ b/src/app/components/EventMenu.svelte @@ -1,20 +1,24 @@ -
    -
    - - - {#if showActivity} - - {/if} - -
    +
    + {#if room && showRoom} + + Posted in # + + {/if} + + + {#if showActivity} + + {/if} +
    diff --git a/src/app/components/GoalCreate.svelte b/src/app/components/GoalCreate.svelte index 3163f430..1a6b7b7e 100644 --- a/src/app/components/GoalCreate.svelte +++ b/src/app/components/GoalCreate.svelte @@ -18,7 +18,12 @@ import {makeEditor} from "@app/editor" import {canEnforceNip70} from "@app/core/commands" - const {url} = $props() + type Props = { + url: string + room?: string + } + + const {url, room}: Props = $props() const shouldProtect = canEnforceNip70(url) @@ -59,6 +64,10 @@ tags.push(PROTECTED) } + if (room) { + tags.push(["h", room]) + } + publishThunk({ relays: [url], event: makeEvent(ZAP_GOAL, {content, tags}), diff --git a/src/app/components/GoalItem.svelte b/src/app/components/GoalItem.svelte index f86ccb6f..4aed8890 100644 --- a/src/app/components/GoalItem.svelte +++ b/src/app/components/GoalItem.svelte @@ -6,6 +6,7 @@ import ProfileLink from "@app/components/ProfileLink.svelte" import GoalActions from "@app/components/GoalActions.svelte" import GoalSummary from "@app/components/GoalSummary.svelte" + import ChannelLink from "@app/components/ChannelLink.svelte" import {makeGoalPath} from "@app/util/routes" type Props = { @@ -16,6 +17,7 @@ const {url, event}: Props = $props() const summary = getTagValue("summary", event.tags) + const room = getTagValue("h", event.tags) @@ -30,6 +32,9 @@
    Posted by + {#if room} + in + {/if}
    diff --git a/src/app/components/GoalSummary.svelte b/src/app/components/GoalSummary.svelte index 170818a9..749b3320 100644 --- a/src/app/components/GoalSummary.svelte +++ b/src/app/components/GoalSummary.svelte @@ -9,11 +9,12 @@ import ZapButton from "@app/components/ZapButton.svelte" type Props = { - url: string + url?: string event: TrustedEvent + class?: string } - const {url, event}: Props = $props() + const {url, event, ...props}: Props = $props() const zaps = deriveEventsMapped(repository, { filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}], @@ -27,7 +28,7 @@ const daysOld = Math.ceil((now() - event.created_at) / DAY) -
    +

    {zapAmount} sats

    diff --git a/src/app/components/LogIn.svelte b/src/app/components/LogIn.svelte index b8e55155..bae9c1f2 100644 --- a/src/app/components/LogIn.svelte +++ b/src/app/components/LogIn.svelte @@ -17,7 +17,6 @@ import {pushModal, clearModals} from "@app/util/modal" import {PLATFORM_NAME, BURROW_URL} from "@app/core/state" import {pushToast} from "@app/util/toast" - import {loadUserData} from "@app/core/requests" import {setChecked} from "@app/util/notifications" let signers: any[] = $state([]) @@ -27,9 +26,7 @@ const signUp = () => pushModal(SignUp) - const onSuccess = async (session: Session, relays: string[] = []) => { - await loadUserData(session.pubkey, relays) - + const onSuccess = async (session: Session) => { addSession(session) pushToast({message: "Successfully logged in!"}) setChecked("*") diff --git a/src/app/components/LogInBunker.svelte b/src/app/components/LogInBunker.svelte index 9e72d400..3a4a5467 100644 --- a/src/app/components/LogInBunker.svelte +++ b/src/app/components/LogInBunker.svelte @@ -14,7 +14,6 @@ import BunkerConnect from "@app/components/BunkerConnect.svelte" import BunkerUrl from "@app/components/BunkerUrl.svelte" import {Nip46Controller} from "@app/util/nip46" - import {loadUserData} from "@app/core/requests" import {clearModals} from "@app/util/modal" import {setChecked} from "@app/util/notifications" import {pushToast} from "@app/util/toast" @@ -33,9 +32,6 @@ const pubkey = await controller.broker.getPublicKey() loginWithNip46(pubkey, controller.clientSecret, response.event.pubkey, SIGNER_RELAYS) - - await loadUserData(pubkey) - setChecked("*") clearModals() }, @@ -75,8 +71,6 @@ broker.cleanup() controller.stop() - await loadUserData(pubkey) - loginWithNip46(pubkey, clientSecret, signerPubkey, relays) } else { return pushToast({ diff --git a/src/app/components/LogInPassword.svelte b/src/app/components/LogInPassword.svelte index 52a3e0fb..459d9b91 100644 --- a/src/app/components/LogInPassword.svelte +++ b/src/app/components/LogInPassword.svelte @@ -16,7 +16,6 @@ import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte" import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte" - import {loadUserData} from "@app/core/requests" import {clearModals, pushModal} from "@app/util/modal" import {setChecked} from "@app/util/notifications" import {pushToast} from "@app/util/toast" @@ -96,8 +95,6 @@ const pubkey = await broker.getPublicKey() const session = makeNip46Session(pubkey, clientSecret, response.event.pubkey, relays) - await loadUserData(pubkey) - addSession({...session, email}) broker.cleanup() setChecked("*") diff --git a/src/app/components/MenuSpace.svelte b/src/app/components/MenuSpace.svelte index 89f14c35..eca97a59 100644 --- a/src/app/components/MenuSpace.svelte +++ b/src/app/components/MenuSpace.svelte @@ -1,14 +1,16 @@ {#if props.event.kind === EVENT_TIME} -
    - -
    - -
    -
    -
    - -
    -
    + +{:else if props.event.kind === THREAD} + +{:else if props.event.kind === ZAP_GOAL} + {:else} {/if} diff --git a/src/app/components/NoteContentEventTime.svelte b/src/app/components/NoteContentEventTime.svelte new file mode 100644 index 00000000..b1f9e023 --- /dev/null +++ b/src/app/components/NoteContentEventTime.svelte @@ -0,0 +1,19 @@ + + +
    + +
    + +
    +
    +
    + +
    +
    diff --git a/src/app/components/NoteContentGoal.svelte b/src/app/components/NoteContentGoal.svelte new file mode 100644 index 00000000..cf0b52ca --- /dev/null +++ b/src/app/components/NoteContentGoal.svelte @@ -0,0 +1,17 @@ + + +
    +

    {props.event.content}

    + + +
    diff --git a/src/app/components/NoteContentMinimal.svelte b/src/app/components/NoteContentMinimal.svelte new file mode 100644 index 00000000..179b478d --- /dev/null +++ b/src/app/components/NoteContentMinimal.svelte @@ -0,0 +1,22 @@ + + +
    + {#if props.event.kind === EVENT_TIME} + + {:else if props.event.kind === THREAD} + + {:else if props.event.kind === ZAP_GOAL} + + {:else} + + {/if} +
    diff --git a/src/app/components/NoteContentMinimalEventTime.svelte b/src/app/components/NoteContentMinimalEventTime.svelte new file mode 100644 index 00000000..18d8fc26 --- /dev/null +++ b/src/app/components/NoteContentMinimalEventTime.svelte @@ -0,0 +1,36 @@ + + +
    +
    +

    {meta.title || meta.name}

    + {#if !isNaN(start) && !isNaN(end)} + {@const startDateDisplay = formatTimestampAsDate(start)} + {@const endDateDisplay = formatTimestampAsDate(end)} + {@const isSingleDay = startDateDisplay === endDateDisplay} +
    + + + {formatTimestampAsTime(start)} — {isSingleDay + ? formatTimestampAsTime(end) + : formatTimestamp(end)} +
    + {/if} +
    + +
    diff --git a/src/app/components/NoteContentMinimalGoal.svelte b/src/app/components/NoteContentMinimalGoal.svelte new file mode 100644 index 00000000..ae768550 --- /dev/null +++ b/src/app/components/NoteContentMinimalGoal.svelte @@ -0,0 +1,34 @@ + + +
    + {props.event.content} +
    + + {zapAmount}/{goalAmount} sats funded +
    +
    + diff --git a/src/app/components/NoteContentMinimalThread.svelte b/src/app/components/NoteContentMinimalThread.svelte new file mode 100644 index 00000000..d0aa1208 --- /dev/null +++ b/src/app/components/NoteContentMinimalThread.svelte @@ -0,0 +1,16 @@ + + +{#if title} + {title} +{/if} +{#if props.event.content} + +{/if} diff --git a/src/app/components/NoteContentThread.svelte b/src/app/components/NoteContentThread.svelte new file mode 100644 index 00000000..e0e74a5a --- /dev/null +++ b/src/app/components/NoteContentThread.svelte @@ -0,0 +1,28 @@ + + +
    + {#if title} +
    +

    {title}

    +

    + {formatTimestamp(props.event.created_at)} +

    +
    + {:else} + + {/if} + {#if props.event.content} + + {/if} +
    diff --git a/src/app/components/PrimaryNavItemSpace.svelte b/src/app/components/PrimaryNavItemSpace.svelte index 8faa6703..78ced315 100644 --- a/src/app/components/PrimaryNavItemSpace.svelte +++ b/src/app/components/PrimaryNavItemSpace.svelte @@ -1,18 +1,23 @@ diff --git a/src/app/components/ProfileInfo.svelte b/src/app/components/ProfileInfo.svelte index b94e5b32..c458b2a8 100644 --- a/src/app/components/ProfileInfo.svelte +++ b/src/app/components/ProfileInfo.svelte @@ -1,7 +1,7 @@ {#if $profile} - + {/if} diff --git a/src/app/components/ProfileLink.svelte b/src/app/components/ProfileLink.svelte index c9c77a9c..a844a4e8 100644 --- a/src/app/components/ProfileLink.svelte +++ b/src/app/components/ProfileLink.svelte @@ -18,6 +18,8 @@ const openProfile = () => pushModal(ProfileDetail, {pubkey, url}) - diff --git a/src/app/components/ReactionSummary.svelte b/src/app/components/ReactionSummary.svelte index 5dbfbbee..0bba7337 100644 --- a/src/app/components/ReactionSummary.svelte +++ b/src/app/components/ReactionSummary.svelte @@ -118,7 +118,7 @@ @@ -152,11 +151,10 @@ +
    diff --git a/src/app/components/SpaceQuickLinks.svelte b/src/app/components/SpaceQuickLinks.svelte deleted file mode 100644 index f403d8a7..00000000 --- a/src/app/components/SpaceQuickLinks.svelte +++ /dev/null @@ -1,144 +0,0 @@ - - -
    -

    - - Quick Links -

    -
    - -
    - - Goals - {#if $notifications.has(goalsPath)} -
    -
    - {/if} -
    - - -
    - - Threads - {#if $notifications.has(threadsPath)} -
    -
    - {/if} -
    - - -
    - - Calendar - {#if $notifications.has(calendarPath)} -
    -
    - {/if} -
    - - {#if hasNip29($relay)} - {#if $userRooms.length + $otherRooms.length > 10} - - {/if} - {#each filteredRooms() as room (room)} - {@const roomPath = makeRoomPath(url, room)} - {@const channel = $channelsById.get(makeChannelId(url, room))} - -
    - {#if channel?.closed || channel?.private} - - {:else} - - {/if} - -
    - {#if $notifications.has(roomPath)} -
    -
    - {/if} - - {/each} - - {:else} - -
    - - Chat - {#if $notifications.has(chatPath)} -
    -
    - {/if} -
    - - {/if} -
    -
    diff --git a/src/app/components/SpaceRecentActivity.svelte b/src/app/components/SpaceRecentActivity.svelte deleted file mode 100644 index de4a2672..00000000 --- a/src/app/components/SpaceRecentActivity.svelte +++ /dev/null @@ -1,106 +0,0 @@ - - -
    -
    -

    - - Recent Conversations -

    -
    - {#if $conversations.length === 0} - {#if $messages.length > 0} - {@const events = $messages.slice(0, 1)} - {@const event = events[0]} - {@const room = getTagValue("h", event.tags)} - - {:else} -
    -

    No recent conversations

    -
    - {/if} - {:else} - {#each $conversations.slice(0, limit) as { room, events, latest, earliest, participants } (latest.id)} - - {/each} - {#if $conversations.length > limit} - - {/if} - {/if} -
    -
    -
    diff --git a/src/app/components/ThreadActions.svelte b/src/app/components/ThreadActions.svelte index 53ecafed..cb6bf417 100644 --- a/src/app/components/ThreadActions.svelte +++ b/src/app/components/ThreadActions.svelte @@ -1,23 +1,27 @@ -
    -
    - - - {#if showActivity} - - {/if} - -
    +
    + {#if room && showRoom} + + Posted in # + + {/if} + + + {#if showActivity} + + {/if} +
    diff --git a/src/app/components/ThreadCreate.svelte b/src/app/components/ThreadCreate.svelte index 4f0a734f..51723d4f 100644 --- a/src/app/components/ThreadCreate.svelte +++ b/src/app/components/ThreadCreate.svelte @@ -16,7 +16,12 @@ import {makeEditor} from "@app/editor" import {canEnforceNip70} from "@app/core/commands" - const {url} = $props() + type Props = { + url: string + room?: string + } + + const {url, room}: Props = $props() const shouldProtect = canEnforceNip70(url) @@ -52,6 +57,10 @@ tags.push(PROTECTED) } + if (room) { + tags.push(["h", room]) + } + publishThunk({ relays: [url], event: makeEvent(THREAD, {content, tags}), diff --git a/src/app/components/ThreadItem.svelte b/src/app/components/ThreadItem.svelte index 1a064601..8ce989c1 100644 --- a/src/app/components/ThreadItem.svelte +++ b/src/app/components/ThreadItem.svelte @@ -1,10 +1,12 @@ @@ -33,7 +36,11 @@
    - Posted by + Posted by + + {#if room} + in + {/if}
    diff --git a/src/app/components/ZapButton.svelte b/src/app/components/ZapButton.svelte index 55c45918..7f2ece4f 100644 --- a/src/app/components/ZapButton.svelte +++ b/src/app/components/ZapButton.svelte @@ -9,7 +9,7 @@ import {pushModal} from "@app/util/modal" type Props = { - url: string + url?: string event: TrustedEvent children: Snippet replaceState?: boolean diff --git a/src/app/core/requests.ts b/src/app/core/requests.ts index e821e64a..05e9ce24 100644 --- a/src/app/core/requests.ts +++ b/src/app/core/requests.ts @@ -1,10 +1,6 @@ import {get, writable} from "svelte/store" import { partition, - chunk, - sample, - sleep, - shuffle, uniq, int, YEAR, @@ -18,12 +14,9 @@ import { fromPairs, } from "@welshman/lib" import { - MESSAGE, DELETE, - THREAD, EVENT_TIME, AUTH_INVITE, - COMMENT, ALERT_EMAIL, ALERT_WEB, ALERT_IOS, @@ -47,24 +40,10 @@ import { thunkQueue, makeFeedController, loadRelay, - loadMutes, - loadFollows, - loadProfile, - loadBlossomServers, - loadRelaySelections, - loadInboxRelaySelections, } from "@welshman/app" import {createScroller} from "@lib/html" import {daysBetween} from "@lib/util" -import { - NOTIFIER_RELAY, - INDEXER_RELAYS, - defaultPubkeys, - userRoomsByUrl, - getUrlsForEvent, - loadMembership, - loadSettings, -} from "@app/core/state" +import {NOTIFIER_RELAY, getUrlsForEvent} from "@app/core/state" // Utils @@ -359,80 +338,6 @@ export const loadAlertStatuses = (pubkey: string) => filters: [{kinds: [ALERT_STATUS], "#p": [pubkey]}], }) -// Application requests - -export const listenForNotifications = () => { - const controller = new AbortController() - - for (const [url, allRooms] of userRoomsByUrl.get()) { - // Limit how many rooms we load at a time, since we have to send a separate filter - // for each one due to relay29 being picky - const rooms = shuffle(Array.from(allRooms)).slice(0, 30) - - load({ - signal: controller.signal, - relays: [url], - filters: [ - {kinds: [THREAD], limit: 1}, - {kinds: [MESSAGE], limit: 1}, - {kinds: [COMMENT], "#K": [String(THREAD)], limit: 1}, - ...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], limit: 1})), - ], - }) - - request({ - signal: controller.signal, - relays: [url], - filters: [ - {kinds: [THREAD], since: now()}, - {kinds: [MESSAGE], since: now()}, - {kinds: [COMMENT], "#K": [String(THREAD)], since: now()}, - ...rooms.map(room => ({kinds: [MESSAGE], "#h": [room], since: now()})), - ], - }) - } - - return () => controller.abort() -} - -export const loadUserData = async (pubkey: string, relays: string[] = []) => { - await Promise.race([sleep(3000), loadRelaySelections(pubkey, relays)]) - - const promise = Promise.race([ - sleep(3000), - Promise.all([ - loadInboxRelaySelections(pubkey, relays), - loadBlossomServers(pubkey, relays), - loadMembership(pubkey, relays), - loadSettings(pubkey, relays), - loadProfile(pubkey, relays), - loadFollows(pubkey, relays), - loadMutes(pubkey, relays), - loadAlertStatuses(pubkey), - loadAlerts(pubkey), - ]), - ]) - - // Load followed profiles slowly in the background without clogging other stuff up. Only use a single - // indexer relay to avoid too many redundant validations, which slow things down and eat bandwidth - promise.then(async () => { - for (const pubkeys of chunk(50, get(defaultPubkeys))) { - const relays = sample(1, INDEXER_RELAYS) - - await sleep(1000) - - for (const pubkey of pubkeys) { - loadMembership(pubkey, relays) - loadProfile(pubkey, relays) - loadFollows(pubkey, relays) - loadMutes(pubkey, relays) - } - } - }) - - return promise -} - export const discoverRelays = (lists: List[]) => Promise.all( uniq(lists.flatMap($l => getRelaysFromList($l))) diff --git a/src/app/core/sync.ts b/src/app/core/sync.ts new file mode 100644 index 00000000..3ea0d3b0 --- /dev/null +++ b/src/app/core/sync.ts @@ -0,0 +1,250 @@ +import {page} from "$app/stores" +import type {Unsubscriber} from "svelte/store" +import {derived, get} from "svelte/store" +import {call, chunk, sleep, now, identity, WEEK, ago} from "@welshman/lib" +import { + getListTags, + getRelayTagValues, + WRAP, + MESSAGE, + ZAP_GOAL, + THREAD, + EVENT_TIME, + COMMENT, + isSignedEvent, +} from "@welshman/util" +import {request, pull} from "@welshman/net" +import { + pubkey, + loadRelay, + userFollows, + userRelaySelections, + userInboxRelaySelections, + loadRelaySelections, + loadInboxRelaySelections, + loadBlossomServers, + loadFollows, + loadMutes, + loadProfile, + repository, +} from "@welshman/app" +import { + INDEXER_RELAYS, + canDecrypt, + loadSettings, + userMembership, + defaultPubkeys, + decodeRelay, + loadMembership, +} from "@app/core/state" +import {loadAlerts, loadAlertStatuses} from "@app/core/requests" + +const syncRelays = () => { + for (const url of INDEXER_RELAYS) { + loadRelay(url) + } + + const unsubscribePage = page.subscribe($page => { + if ($page.params.relay) { + loadRelay(decodeRelay($page.params.relay)) + } + }) + + const unsubscribeMembership = userMembership.subscribe($l => { + for (const url of getRelayTagValues(getListTags($l))) { + loadRelay(url) + } + }) + + return () => { + unsubscribePage() + unsubscribeMembership() + } +} + +const syncUserData = () => { + const unsubscribePubkey = pubkey.subscribe($pubkey => { + if ($pubkey) { + loadRelaySelections($pubkey) + } + }) + + const unsubscribeSelections = userRelaySelections.subscribe($l => { + const $pubkey = pubkey.get() + + if ($pubkey) { + loadAlerts($pubkey) + loadAlertStatuses($pubkey) + loadBlossomServers($pubkey) + loadFollows($pubkey) + loadMembership($pubkey) + loadMutes($pubkey) + loadProfile($pubkey) + loadSettings($pubkey) + } + }) + + const unsubscribeFollows = userFollows.subscribe(async $l => { + for (const pubkeys of chunk(10, get(defaultPubkeys))) { + // This isn't urgent, avoid clogging other stuff up + await sleep(1000) + + for (const pk of pubkeys) { + loadRelaySelections(pk).then(() => { + loadMembership(pk) + loadProfile(pk) + loadFollows(pk) + loadMutes(pk) + }) + } + } + }) + + return () => { + unsubscribePubkey() + unsubscribeSelections() + unsubscribeFollows() + } +} + +const syncSpace = (url: string) => { + const controller = new AbortController() + + // Load historical data + pull({ + relays: [url], + signal: controller.signal, + filters: [{kinds: [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE, COMMENT]}], + events: repository + .query([{kinds: [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE, COMMENT]}]) + .filter(isSignedEvent), + }) + + // Load new events + request({ + relays: [url], + signal: controller.signal, + filters: [{kinds: [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE, COMMENT], since: now()}], + }) + + return () => controller.abort() +} + +const syncSpaces = () => { + const unsubscribersByUrl = new Map() + const unsubscribeMembership = userMembership.subscribe($l => { + const urls = getRelayTagValues(getListTags($l)) + + // Start syncing newly added spaces + for (const url of urls) { + if (!unsubscribersByUrl.has(url)) { + unsubscribersByUrl.set(url, syncSpace(url)) + } + } + + // stop syncing removed spaces + for (const [url, unsubscribe] of unsubscribersByUrl.entries()) { + if (!urls.includes(url)) { + unsubscribersByUrl.delete(url) + unsubscribe() + } + } + }) + + return () => { + Array.from(unsubscribersByUrl.values()).forEach(call) + unsubscribeMembership() + } +} + +const syncDMRelay = (url: string, pubkey: string) => { + const controller = new AbortController() + + // Load historical data + pull({ + relays: [url], + signal: controller.signal, + filters: [{kinds: [WRAP], "#p": [pubkey], until: ago(WEEK, 2)}], + events: repository + .query([{kinds: [ZAP_GOAL, EVENT_TIME, THREAD, MESSAGE, COMMENT]}]) + .filter(isSignedEvent), + }) + + // Load new events + request({ + relays: [url], + signal: controller.signal, + filters: [{kinds: [WRAP], "#p": [pubkey], since: ago(WEEK, 2)}], + }) + + return () => controller.abort() +} + +const syncDMs = () => { + const unsubscribersByUrl = new Map() + + let currentPubkey: string | undefined + + const unsubscribeAll = () => { + for (const [url, unsubscribe] of unsubscribersByUrl.entries()) { + unsubscribersByUrl.delete(url) + unsubscribe() + } + } + + const subscribeAll = (pubkey: string, urls: string[]) => { + // Start syncing newly added relays + for (const url of urls) { + if (!unsubscribersByUrl.has(url)) { + unsubscribersByUrl.set(url, syncDMRelay(url, pubkey)) + } + } + + // Stop syncing removed spaces + for (const [url, unsubscribe] of unsubscribersByUrl.entries()) { + if (!urls.includes(url)) { + unsubscribersByUrl.delete(url) + unsubscribe() + } + } + } + + // When pubkey changes, re-sync + const unsubscribePubkey = derived([pubkey, canDecrypt], identity).subscribe( + ([$pubkey, $canDecrypt]) => { + if ($pubkey !== currentPubkey) { + unsubscribeAll() + } + + // If we have a pubkey, refresh our user's relay selections then sync our subscriptions + if ($pubkey && $canDecrypt) { + loadRelaySelections($pubkey) + .then(() => loadInboxRelaySelections($pubkey)) + .then($l => subscribeAll($pubkey, getRelayTagValues(getListTags($l)))) + } + + currentPubkey = $pubkey + }, + ) + + // When user inbox relays change, update synchronization + const unsubscribeSelections = userInboxRelaySelections.subscribe($l => { + const $pubkey = pubkey.get() + + if ($pubkey && $l) { + subscribeAll($pubkey, getRelayTagValues(getListTags($l))) + } + }) + + return () => { + unsubscribeAll() + unsubscribePubkey() + unsubscribeSelections() + } +} + +export const syncApplicationData = () => { + const unsubscribers = [syncRelays(), syncUserData(), syncSpaces(), syncDMs()] + + return () => unsubscribers.forEach(call) +} diff --git a/src/app/util/history.ts b/src/app/util/history.ts new file mode 100644 index 00000000..b08d34a3 --- /dev/null +++ b/src/app/util/history.ts @@ -0,0 +1,11 @@ +import {page} from "$app/stores" + +export const lastPageBySpaceUrl = new Map() + +export const setupHistory = () => { + page.subscribe($page => { + if ($page.params.relay) { + lastPageBySpaceUrl.set($page.params.relay, $page.url.pathname) + } + }) +} diff --git a/src/app/util/notifications.ts b/src/app/util/notifications.ts index 0f3cb967..272a6b8f 100644 --- a/src/app/util/notifications.ts +++ b/src/app/util/notifications.ts @@ -3,10 +3,11 @@ import {synced, throttled} from "@welshman/store" import {pubkey, relaysByUrl} from "@welshman/app" import {prop, spec, identity, now, groupBy} from "@welshman/lib" import type {TrustedEvent} from "@welshman/util" -import {EVENT_TIME, MESSAGE, THREAD, COMMENT, getTagValue} from "@welshman/util" +import {ZAP_GOAL, EVENT_TIME, MESSAGE, THREAD, COMMENT, getTagValue} from "@welshman/util" import { makeSpacePath, makeChatPath, + makeGoalPath, makeThreadPath, makeCalendarPath, makeSpaceChatPath, @@ -76,45 +77,52 @@ export const notifications = derived( } } - const allThreadEvents = $repository.query([ - {kinds: [THREAD]}, - {kinds: [COMMENT], "#K": [String(THREAD)]}, - ]) + const allGoalComments = $repository.query([{kinds: [COMMENT], "#K": [String(ZAP_GOAL)]}]) - const allCalendarEvents = $repository.query([ - {kinds: [EVENT_TIME]}, - {kinds: [COMMENT], "#K": [String(EVENT_TIME)]}, - ]) + const allThreadComments = $repository.query([{kinds: [COMMENT], "#K": [String(THREAD)]}]) - const allMessageEvents = $repository.query([{kinds: [MESSAGE]}]) + const allCalendarComments = $repository.query([{kinds: [COMMENT], "#K": [String(EVENT_TIME)]}]) + + const allMessages = $repository.query([{kinds: [MESSAGE, THREAD, ZAP_GOAL, EVENT_TIME]}]) for (const [url, rooms] of $userRoomsByUrl.entries()) { const spacePath = makeSpacePath(url) + const goalPath = makeGoalPath(url) const threadPath = makeThreadPath(url) const calendarPath = makeCalendarPath(url) const messagesPath = makeSpaceChatPath(url) - const threadEvents = allThreadEvents.filter(e => $getUrlsForEvent(e.id).includes(url)) - const calendarEvents = allCalendarEvents.filter(e => $getUrlsForEvent(e.id).includes(url)) - const messagesEvents = allMessageEvents.filter(e => $getUrlsForEvent(e.id).includes(url)) + const goalComments = allGoalComments.filter(e => $getUrlsForEvent(e.id).includes(url)) + const threadComments = allThreadComments.filter(e => $getUrlsForEvent(e.id).includes(url)) + const calendarComments = allCalendarComments.filter(e => $getUrlsForEvent(e.id).includes(url)) + const messages = allMessages.filter(e => $getUrlsForEvent(e.id).includes(url)) - if (hasNotification(threadPath, threadEvents[0])) { - paths.add(spacePath) - paths.add(threadPath) - } + const commentsByGoalId = groupBy( + e => getTagValue("E", e.tags), + goalComments.filter(spec({kind: COMMENT})), + ) - if (hasNotification(calendarPath, calendarEvents[0])) { - paths.add(spacePath) - paths.add(calendarPath) + for (const [goalId, [comment]] of commentsByGoalId.entries()) { + const goalItemPath = makeGoalPath(url, goalId) + + if (hasNotification(goalPath, comment)) { + paths.add(goalPath) + } + if (hasNotification(goalItemPath, comment)) { + paths.add(goalItemPath) + } } const commentsByThreadId = groupBy( e => getTagValue("E", e.tags), - threadEvents.filter(spec({kind: COMMENT})), + threadComments.filter(spec({kind: COMMENT})), ) for (const [threadId, [comment]] of commentsByThreadId.entries()) { const threadItemPath = makeThreadPath(url, threadId) + if (hasNotification(threadPath, comment)) { + paths.add(threadPath) + } if (hasNotification(threadItemPath, comment)) { paths.add(threadItemPath) } @@ -122,24 +130,26 @@ export const notifications = derived( const commentsByEventId = groupBy( e => getTagValue("E", e.tags), - calendarEvents.filter(spec({kind: COMMENT})), + calendarComments.filter(spec({kind: COMMENT})), ) for (const [eventId, [comment]] of commentsByEventId.entries()) { - const calendarEventPath = makeCalendarPath(url, eventId) + const calendarItemPath = makeCalendarPath(url, eventId) - if (hasNotification(calendarEventPath, comment)) { - paths.add(calendarEventPath) + if (hasNotification(calendarPath, comment)) { + paths.add(calendarPath) + } + + if (hasNotification(calendarItemPath, comment)) { + paths.add(calendarItemPath) } } if (hasNip29($relaysByUrl.get(url))) { for (const room of rooms) { const roomPath = makeRoomPath(url, room) - const latestEvent = allMessageEvents.find( - e => - $getUrlsForEvent(e.id).includes(url) && - e.tags.find(t => t[0] === "h" && t[1] === room), + const latestEvent = allMessages.find( + e => $getUrlsForEvent(e.id).includes(url) && e.tags.find(spec(["h", room])), ) if (hasNotification(roomPath, latestEvent)) { @@ -148,7 +158,7 @@ export const notifications = derived( } } } else { - if (hasNotification(messagesPath, messagesEvents[0])) { + if (hasNotification(messagesPath, messages[0])) { paths.add(spacePath) paths.add(messagesPath) } diff --git a/src/app/util/routes.ts b/src/app/util/routes.ts index acc7818e..5ad0ac8a 100644 --- a/src/app/util/routes.ts +++ b/src/app/util/routes.ts @@ -3,7 +3,7 @@ import * as nip19 from "nostr-tools/nip19" import {goto} from "$app/navigation" import {nthEq, sleep} from "@welshman/lib" import type {TrustedEvent} from "@welshman/util" -import {tracker} from "@welshman/app" +import {tracker, relaysByUrl} from "@welshman/app" import {scrollToEvent} from "@lib/html" import {identity} from "@welshman/lib" import { @@ -23,6 +23,7 @@ import { decodeRelay, encodeRelay, userRoomsByUrl, + hasNip29, ROOM, } from "@app/core/state" @@ -36,6 +37,14 @@ export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) => .filter(identity) .map(s => encodeURIComponent(s as string)) .join("/") + } else { + const relay = relaysByUrl.get().get(url) + + if (hasNip29(relay)) { + path += "/recent" + } else { + path += "/chat" + } } return path @@ -143,3 +152,14 @@ export const getEventPath = async (event: TrustedEvent, urls: string[]) => { return entityLink(nip19.neventEncode({id: event.id, relays: urls})) } + +export const getChannelItemPath = (url: string, event: TrustedEvent) => { + switch (event.kind) { + case THREAD: + return makeThreadPath(url, event.id) + case ZAP_GOAL: + return makeGoalPath(url, event.id) + case EVENT_TIME: + return makeCalendarPath(url, event.id) + } +} diff --git a/src/app/util/storage.ts b/src/app/util/storage.ts index 19376b48..49cc8c01 100644 --- a/src/app/util/storage.ts +++ b/src/app/util/storage.ts @@ -167,20 +167,14 @@ const syncTracker = async () => { tracker.load(relaysById) - let p = Promise.resolve() - const updateOne = batch(3000, (ids: string[]) => { - p = p.then(() => { - collection.add(ids.map(id => [id, Array.from(tracker.getRelays(id))])) - }) + collection.add(ids.map(id => [id, Array.from(tracker.getRelays(id))])) }) const updateAll = throttle(3000, () => { - p = p.then(() => { - collection.set( - Array.from(tracker.relaysById.entries()).map(([id, relays]) => [id, Array.from(relays)]), - ) - }) + collection.set( + Array.from(tracker.relaysById.entries()).map(([id, relays]) => [id, Array.from(relays)]), + ) }) tracker.on("add", updateOne) diff --git a/src/assets/icons/index.html b/src/assets/icons/index.html new file mode 100644 index 00000000..c4a2df4e --- /dev/null +++ b/src/assets/icons/index.html @@ -0,0 +1,1353 @@ + + + + + + Icon Gallery + + + + + +
    +

    Icon Gallery

    +
    + +
    +
    +
    + + + + diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 8494a5b5..38530683 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -4,31 +4,17 @@ import {throttle} from "throttle-debounce" import {onMount} from "svelte" import * as nip19 from "nostr-tools/nip19" - import {get, derived} from "svelte/store" + import {get} from "svelte/store" import {App, type URLOpenListenerEvent} from "@capacitor/app" import {dev} from "$app/environment" import {goto} from "$app/navigation" import {sync, localStorageProvider} from "@welshman/store" - import { - ago, - assoc, - call, - defer, - dissoc, - identity, - memoize, - on, - sleep, - spec, - TaskQueue, - WEEK, - } from "@welshman/lib" + import {assoc, call, defer, dissoc, on, sleep, spec, TaskQueue} from "@welshman/lib" import type {TrustedEvent, StampedEvent} from "@welshman/util" import {WRAP} from "@welshman/util" import {Nip46Broker, makeSecret} from "@welshman/signer" import type {Socket, RelayMessage, ClientMessage} from "@welshman/net" import { - request, defaultSocketPolicies, makeSocketPolicyAuth, SocketEvent, @@ -40,7 +26,6 @@ isClientClose, } from "@welshman/net" import { - loadRelay, repository, pubkey, session, @@ -64,20 +49,18 @@ import {preferencesStorageProvider} from "@lib/storage" import AppContainer from "@app/components/AppContainer.svelte" import ModalContainer from "@app/components/ModalContainer.svelte" + import {setupHistory} from "@app/util/history" import {setupTracking} from "@app/util/tracking" import {setupAnalytics} from "@app/util/analytics" import { - INDEXER_RELAYS, - userMembership, userSettingsValues, relaysPendingTrust, ensureUnwrapped, canDecrypt, getSetting, relaysMostlyRestricted, - userInboxRelays, } from "@app/core/state" - import {loadUserData, listenForNotifications} from "@app/core/requests" + import {syncApplicationData} from "@app/core/sync" import {theme} from "@app/util/theme" import {toast, pushToast} from "@app/util/toast" import {initializePushNotifications} from "@app/util/push" @@ -192,8 +175,6 @@ // TODO: remove ack result if (pubkey && ["ack", connectSecret].includes(result)) { - await loadUserData(pubkey) - loginWithNip46(pubkey, clientSecret, signerPubkey, relays) broker.cleanup() success = true @@ -224,6 +205,7 @@ if (!initialized) { initialized = true + setupHistory() setupTracking() setupAnalytics() @@ -374,46 +356,8 @@ }, ) - // Load relay info - for (const url of INDEXER_RELAYS) { - loadRelay(url) - } - - // Load user data - if ($pubkey) { - await loadUserData($pubkey) - } - - // Listen for space data, populate space-based notifications - let unsubSpaces: any - - userMembership.subscribe( - memoize($membership => { - unsubSpaces?.() - unsubSpaces = listenForNotifications() - }), - ) - - // Listen for chats, populate chat-based notifications - let controller: AbortController - - derived([pubkey, canDecrypt, userInboxRelays], identity).subscribe( - ([$pubkey, $canDecrypt, $userInboxRelays]) => { - controller?.abort() - controller = new AbortController() - - if ($pubkey && $canDecrypt) { - request({ - signal: controller.signal, - relays: $userInboxRelays, - filters: [ - {kinds: [WRAP], "#p": [$pubkey], since: ago(WEEK, 2)}, - {kinds: [WRAP], "#p": [$pubkey], limit: 100}, - ], - }) - } - }, - ) + // Load user data, listen for messages, etc + syncApplicationData() // subscribe to badge count for changes notifications.badgeCount.subscribe(notifications.handleBadgeCountChanges) diff --git a/src/routes/settings/profile/+page.svelte b/src/routes/settings/profile/+page.svelte index b1ce4837..cca0b6a3 100644 --- a/src/routes/settings/profile/+page.svelte +++ b/src/routes/settings/profile/+page.svelte @@ -17,7 +17,7 @@ import FieldInline from "@lib/components/FieldInline.svelte" import Button from "@lib/components/Button.svelte" import Avatar from "@lib/components/Avatar.svelte" - import Content from "@app/components/Content.svelte" + import ContentMinimal from "@app/components/ContentMinimal.svelte" import ProfileEdit from "@app/components/ProfileEdit.svelte" import ProfileDelete from "@app/components/ProfileDelete.svelte" import SignerStatus from "@app/components/SignerStatus.svelte" @@ -66,7 +66,7 @@
    {#key $profile?.about} - + {/key}
    {#if $session?.email} diff --git a/src/routes/spaces/[relay]/+page.svelte b/src/routes/spaces/[relay]/+page.svelte index 98615536..92ecec2d 100644 --- a/src/routes/spaces/[relay]/+page.svelte +++ b/src/routes/spaces/[relay]/+page.svelte @@ -1,125 +1,10 @@ - - - {#snippet icon()} -
    - -
    - {/snippet} - {#snippet title()} - Home - {/snippet} - {#snippet action()} -
    - {#if !$userRoomsByUrl.has(url)} - - {:else if owner} - - - Contact Owner - - {/if} - -
    - {/snippet} -
    - - -
    -
    -
    -
    -
    - {#if $relay?.profile?.icon} - - {:else} - - {/if} -
    -
    -
    -
    -

    - -

    -

    {displayRelayUrl(url)}

    -
    -
    - - {#if $relay?.profile?.terms_of_service || $relay?.profile?.privacy_policy} -
    - {#if $relay.profile.terms_of_service} - - - Terms of Service - - {/if} - {#if $relay.profile.privacy_policy} - - - Privacy Policy - - {/if} -
    - {/if} -
    - -
    -
    - -
    -
    - - {#if owner} -
    -

    - - Latest Updates -

    - - {#snippet fallback()} -

    No recent posts from the relay admin

    - {/snippet} -
    -
    - {/if} -
    -
    -
    diff --git a/src/routes/spaces/[relay]/[room]/+page.svelte b/src/routes/spaces/[relay]/[room]/+page.svelte index 4ab42d7e..7b5fd658 100644 --- a/src/routes/spaces/[relay]/[room]/+page.svelte +++ b/src/routes/spaces/[relay]/[room]/+page.svelte @@ -13,6 +13,9 @@ makeRoomMeta, MESSAGE, DELETE, + THREAD, + EVENT_TIME, + ZAP_GOAL, ROOM_ADD_USER, ROOM_REMOVE_USER, } from "@welshman/util" @@ -33,7 +36,7 @@ import ThunkToast from "@app/components/ThunkToast.svelte" import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte" import ChannelName from "@app/components/ChannelName.svelte" - import ChannelMessage from "@app/components/ChannelMessage.svelte" + import ChannelItem from "@app/components/ChannelItem.svelte" import ChannelCompose from "@app/components/ChannelCompose.svelte" import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte" import { @@ -65,7 +68,7 @@ const lastChecked = $checked[$page.url.pathname] const url = decodeRelay(relay) const channel = deriveChannel(url, room) - const filter = {kinds: [MESSAGE], "#h": [room]} + const filter = {kinds: [MESSAGE, THREAD, EVENT_TIME, ZAP_GOAL], "#h": [room]} const isFavorite = $derived($userRoomsByUrl.get(url)?.has(room)) const shouldProtect = canEnforceNip70(url) const membershipStatus = deriveUserMembershipStatus(url, room) @@ -429,7 +432,7 @@ {value} {:else}
    - {#key eventToEdit} + {room} + {onSubmit} + {onEditPrevious} + content={eventToEdit?.content} + bind:this={compose} /> {/key} {/if}
    diff --git a/src/routes/spaces/[relay]/calendar/+page.svelte b/src/routes/spaces/[relay]/calendar/+page.svelte index 8cdf3fb7..540acfb5 100644 --- a/src/routes/spaces/[relay]/calendar/+page.svelte +++ b/src/routes/spaces/[relay]/calendar/+page.svelte @@ -46,17 +46,19 @@ let haveISeenTheFuture = false let prevDateDisplay: string - return $events.map(event => { - const newDateDisplay = formatTimestampAsDate(getStart(event)) - const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay - const isFuture = todayDateDisplay === newDateDisplay || event.created_at > now() - const isFirstFutureEvent = !haveISeenTheFuture && isFuture + return $events + .filter(event => !isNaN(getStart(event))) + .map(event => { + const newDateDisplay = formatTimestampAsDate(getStart(event)) + const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay + const isFuture = todayDateDisplay === newDateDisplay || event.created_at > now() + const isFirstFutureEvent = !haveISeenTheFuture && isFuture - prevDateDisplay = newDateDisplay - haveISeenTheFuture = isFuture + prevDateDisplay = newDateDisplay + haveISeenTheFuture = isFuture - return {event, dateDisplay, isFirstFutureEvent} - }) + return {event, dateDisplay, isFirstFutureEvent} + }) }) let previousScrollHeight = 0 diff --git a/src/routes/spaces/[relay]/calendar/[id]/+page.svelte b/src/routes/spaces/[relay]/calendar/[id]/+page.svelte index f104e5f3..45ca68bf 100644 --- a/src/routes/spaces/[relay]/calendar/[id]/+page.svelte +++ b/src/routes/spaces/[relay]/calendar/[id]/+page.svelte @@ -93,7 +93,7 @@
    - +
    {#if !showAll && $replies.length > 4} diff --git a/src/routes/spaces/[relay]/chat/+page.svelte b/src/routes/spaces/[relay]/chat/+page.svelte index 06105873..7c5d8419 100644 --- a/src/routes/spaces/[relay]/chat/+page.svelte +++ b/src/routes/spaces/[relay]/chat/+page.svelte @@ -5,7 +5,7 @@ import {readable} from "svelte/store" import {now, formatTimestampAsDate, MINUTE, ago} from "@welshman/lib" import type {TrustedEvent, EventContent} from "@welshman/util" - import {makeEvent, MESSAGE, DELETE} from "@welshman/util" + import {makeEvent, MESSAGE, DELETE, THREAD, EVENT_TIME, ZAP_GOAL} from "@welshman/util" import {pubkey, publishThunk} from "@welshman/app" import {slide, fade, fly} from "@lib/transition" import ChatRound from "@assets/icons/chat-round.svg?dataurl" @@ -18,7 +18,7 @@ import Divider from "@lib/components/Divider.svelte" import ThunkToast from "@app/components/ThunkToast.svelte" import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte" - import ChannelMessage from "@app/components/ChannelMessage.svelte" + import ChannelItem from "@app/components/ChannelItem.svelte" import ChannelCompose from "@app/components/ChannelCompose.svelte" import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte" import { @@ -38,7 +38,7 @@ const mounted = now() const lastChecked = $checked[$page.url.pathname] const url = decodeRelay($page.params.relay!) - const filter = {kinds: [MESSAGE]} + const filter = {kinds: [MESSAGE, THREAD, EVENT_TIME, ZAP_GOAL]} const shouldProtect = canEnforceNip70(url) const replyTo = (event: TrustedEvent) => { @@ -282,11 +282,12 @@ {:else if type === "date"} {value} {:else} + {@const event = $state.snapshot(value as TrustedEvent)}
    - @@ -316,11 +317,11 @@
    {#key eventToEdit} + {onSubmit} + {onEditPrevious} + content={eventToEdit?.content} + bind:this={compose} /> {/key} diff --git a/src/routes/spaces/[relay]/goals/[id]/+page.svelte b/src/routes/spaces/[relay]/goals/[id]/+page.svelte index b7a5bd4c..6f69cdc3 100644 --- a/src/routes/spaces/[relay]/goals/[id]/+page.svelte +++ b/src/routes/spaces/[relay]/goals/[id]/+page.svelte @@ -87,7 +87,7 @@
    - +
    {#if !showAll && $replies.length > 4} diff --git a/src/routes/spaces/[relay]/recent/+page.svelte b/src/routes/spaces/[relay]/recent/+page.svelte new file mode 100644 index 00000000..bd96c466 --- /dev/null +++ b/src/routes/spaces/[relay]/recent/+page.svelte @@ -0,0 +1,118 @@ + + + + {#snippet icon()} +
    + +
    + {/snippet} + {#snippet title()} + Recent Activity + {/snippet} + {#snippet action()} +
    + +
    + {/snippet} +
    + +
    + + {#if $conversations.length === 0} + {#if $messages.length > 0} + {@const events = $messages.slice(0, 1)} + {@const event = events[0]} + {@const room = getTagValue("h", event.tags)} + + {:else} +
    +

    No recent conversations

    +
    + {/if} + {:else} + {#each $conversations.slice(0, limit) as { room, events, latest, earliest, participants } (latest.id)} + + {/each} + {/if} +
    +
    diff --git a/src/routes/spaces/[relay]/threads/[id]/+page.svelte b/src/routes/spaces/[relay]/threads/[id]/+page.svelte index bbd19d86..3fb65c4c 100644 --- a/src/routes/spaces/[relay]/threads/[id]/+page.svelte +++ b/src/routes/spaces/[relay]/threads/[id]/+page.svelte @@ -84,7 +84,7 @@
    - +
    {#if !showAll && $replies.length > 4}