diff --git a/src/app/commands.ts b/src/app/commands.ts index cb4d4f74..e0e00e91 100644 --- a/src/app/commands.ts +++ b/src/app/commands.ts @@ -22,7 +22,7 @@ import { } from "@welshman/util" import type {TrustedEvent, EventTemplate, List} from "@welshman/util" import type {SubscribeRequestWithHandlers, Subscription} from "@welshman/net" -import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net" +import {PublishStatus, AuthStatus, SocketStatus, SubscriptionEvent} from "@welshman/net" import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer" import type {Nip46Handler} from "@welshman/signer" import { @@ -58,6 +58,7 @@ import { loadMembership, loadSettings, getDefaultPubkeys, + getMembershipUrls, } from "@app/state" // Utils @@ -100,7 +101,7 @@ export const subscribePersistent = (request: SubscribeRequestWithHandlers) => { sleep(30_000), new Promise(resolve => { sub = subscribe(request) - sub.emitter.on("close", resolve) + sub.emitter.on(SubscriptionEvent.Complete, resolve) }), ]) @@ -191,16 +192,20 @@ export const addSpaceMembership = async (url: string) => { const event = await addToListPublicly(list, ["r", url]).reconcile(nip44EncryptToSelf) const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)]) - return publishThunk({event, relays}).result + return publishThunk({event, relays}) } export const removeSpaceMembership = async (url: string) => { const list = get(userMembership) || makeList({kind: MEMBERSHIPS}) const pred = (t: string[]) => t[t[0] === "r" ? 1 : 2] === url const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf) - const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)]) + const relays = uniq([ + url, + ...ctx.app.router.FromUser().getUrls(), + ...getRelayTagValues(event.tags), + ]) - return publishThunk({event, relays}).result + return publishThunk({event, relays}) } export const addRoomMembership = async (url: string, room: string) => { @@ -208,16 +213,20 @@ export const addRoomMembership = async (url: string, room: string) => { const event = await addToListPublicly(list, tagRoom(room, url)).reconcile(nip44EncryptToSelf) const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)]) - return publishThunk({event, relays}).result + return publishThunk({event, relays}) } export const removeRoomMembership = async (url: string, room: string) => { const list = get(userMembership) || makeList({kind: MEMBERSHIPS}) const pred = (t: string[]) => equals(tagRoom(room, url), t) const event = await removeFromListByPredicate(list, pred).reconcile(nip44EncryptToSelf) - const relays = uniq([...ctx.app.router.FromUser().getUrls(), ...getRelayTagValues(event.tags)]) + const relays = uniq([ + url, + ...ctx.app.router.FromUser().getUrls(), + ...getRelayTagValues(event.tags), + ]) - return publishThunk({event, relays}).result + return publishThunk({event, relays}) } export const setRelayPolicy = (url: string, read: boolean, write: boolean) => { @@ -234,8 +243,13 @@ export const setRelayPolicy = (url: string, read: boolean, write: boolean) => { return publishThunk({ event: createEvent(list.kind, {tags}), - relays: [...INDEXER_RELAYS, ...ctx.app.router.FromUser().getUrls()], - }).result + relays: [ + url, + ...INDEXER_RELAYS, + ...ctx.app.router.FromUser().getUrls(), + ...getMembershipUrls(userMembership.get()), + ], + }) } export const setInboxRelayPolicy = (url: string, enabled: boolean) => { @@ -251,8 +265,12 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => { return publishThunk({ event: createEvent(list.kind, {tags}), - relays: [...INDEXER_RELAYS, ...ctx.app.router.FromUser().getUrls()], - }).result + relays: [ + ...INDEXER_RELAYS, + ...ctx.app.router.FromUser().getUrls(), + ...getMembershipUrls(userMembership.get()), + ], + }) } } diff --git a/src/app/components/ChannelMessage.svelte b/src/app/components/ChannelMessage.svelte index 10c75c99..f9451709 100644 --- a/src/app/components/ChannelMessage.svelte +++ b/src/app/components/ChannelMessage.svelte @@ -78,7 +78,7 @@ {:else}
{/if} -
+
{#if showPubkey}
{#if !isHead} - + {/if} - +
-

+
+ +

+ + and + {#if others.length === 2} + + {:else} + {others.length - 1} + {others.length > 2 ? "others" : "other"} + {/if} +

+
+ {/if}
diff --git a/src/app/components/ChatMessage.svelte b/src/app/components/ChatMessage.svelte index eb6a955a..4f2050db 100644 --- a/src/app/components/ChatMessage.svelte +++ b/src/app/components/ChatMessage.svelte @@ -56,7 +56,7 @@ {#if thunk} - + {/if}
+ import {page} from "$app/stores" + import {derived} from "svelte/store" + import {max} from "@welshman/lib" + import {matchFilter} from "@welshman/util" + import {pubkey} from "@welshman/app" + import Icon from "@lib/components/Icon.svelte" + import Button from "@lib/components/Button.svelte" + import MenuSpace from "@app/components/MenuSpace.svelte" + import {checked, getNotification, deriveNotification, THREAD_FILTERS} from "@app/notifications" + import { + userMembership, + getMembershipRoomsByUrl, + deriveEventsForUrl, + MESSAGE, + GENERAL, + } from "@app/state" + import {makeRoomPath, makeSpacePath} from "@app/routes" + import {pushDrawer} from "@app/modal" + + export let url + + const openMenu = () => pushDrawer(MenuSpace, {url}) + + const events = deriveEventsForUrl(url, [{kinds: [MESSAGE]}]) + + const threadsPath = makeSpacePath(url, "threads") + + const threadsNotification = deriveNotification(threadsPath, THREAD_FILTERS, url) + + const notification = derived( + [page, events, checked, userMembership], + ([$page, $events, $checked, $userMembership]) => { + console.log(getMembershipRoomsByUrl(url, $userMembership).concat(GENERAL)) + return getMembershipRoomsByUrl(url, $userMembership) + .concat(GENERAL) + .some(room => { + const path = makeRoomPath(url, room) + + if ($page.url.pathname === path) return false + + const lastChecked = max([$checked["*"], $checked[path]]) + const roomEvents = $events.filter(e => matchFilter({"#~": [room]}, e)) + + return getNotification($pubkey, lastChecked, roomEvents) + }) + }, + ) + + + diff --git a/src/app/components/MenuSpacesItem.svelte b/src/app/components/MenuSpacesItem.svelte index 9e9df6b5..e5457cc8 100644 --- a/src/app/components/MenuSpacesItem.svelte +++ b/src/app/components/MenuSpacesItem.svelte @@ -5,12 +5,11 @@ import RelayName from "@app/components/RelayName.svelte" import RelayDescription from "@app/components/RelayDescription.svelte" import {makeSpacePath} from "@app/routes" - import {deriveNotification, SPACE_FILTERS} from "@app/notifications" + import {spacesNotifications} from "@app/notifications" export let url const path = makeSpacePath(url) - const notification = deriveNotification(path, SPACE_FILTERS, url) @@ -18,7 +17,7 @@
- {#if $notification} + {#if $spacesNotifications.includes(url)}
{/if}
diff --git a/src/app/components/NoteItem.svelte b/src/app/components/NoteItem.svelte index 8add8a0e..2434b01f 100644 --- a/src/app/components/NoteItem.svelte +++ b/src/app/components/NoteItem.svelte @@ -1,9 +1,7 @@
- + diff --git a/src/app/components/PrimaryNav.svelte b/src/app/components/PrimaryNav.svelte index eecfcd34..974c6793 100644 --- a/src/app/components/PrimaryNav.svelte +++ b/src/app/components/PrimaryNav.svelte @@ -9,7 +9,7 @@ import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte" import {userMembership, getMembershipUrls, PLATFORM_RELAY, PLATFORM_LOGO} from "@app/state" import {pushModal} from "@app/modal" - import {deriveNotification, spacesNotification, CHAT_FILTERS} from "@app/notifications" + import {deriveNotification, inactiveSpacesNotifications, CHAT_FILTERS} from "@app/notifications" const chatNotification = deriveNotification("/chat", CHAT_FILTERS) @@ -79,7 +79,10 @@ - + 0}>
diff --git a/src/app/components/ProfileCircles.svelte b/src/app/components/ProfileCircles.svelte index 94de3923..effc6311 100644 --- a/src/app/components/ProfileCircles.svelte +++ b/src/app/components/ProfileCircles.svelte @@ -7,7 +7,7 @@
{#each pubkeys.slice(0, 15) as pubkey (pubkey)}
- +
{/each}
diff --git a/src/app/components/ReactionSummary.svelte b/src/app/components/ReactionSummary.svelte index 1419d53d..d1fb220c 100644 --- a/src/app/components/ReactionSummary.svelte +++ b/src/app/components/ReactionSummary.svelte @@ -1,19 +1,26 @@ {#if $reactions.length > 0} @@ -27,7 +34,7 @@ class:border={isOwn} class:border-solid={isOwn} class:border-primary={isOwn} - on:click|stopPropagation={onClick}> + on:click|preventDefault|stopPropagation={onClick}> {displayReaction(content)} {#if events.length > 1} {events.length} diff --git a/src/app/components/ReplySummary.svelte b/src/app/components/ReplySummary.svelte index e6ed2443..42462c93 100644 --- a/src/app/components/ReplySummary.svelte +++ b/src/app/components/ReplySummary.svelte @@ -1,12 +1,19 @@ {#if $replies.length > 0} diff --git a/src/app/components/ThreadActions.svelte b/src/app/components/ThreadActions.svelte index bc8d5ab9..c0db2c60 100644 --- a/src/app/components/ThreadActions.svelte +++ b/src/app/components/ThreadActions.svelte @@ -1,10 +1,11 @@
- +
{#if $deleted}
Deleted
diff --git a/src/app/components/ThreadCreate.svelte b/src/app/components/ThreadCreate.svelte index ba416249..22081430 100644 --- a/src/app/components/ThreadCreate.svelte +++ b/src/app/components/ThreadCreate.svelte @@ -44,8 +44,8 @@ const tags = [["title", title], tagRoom(GENERAL, url), ...getEditorTags($editor)] publishThunk({ - event: createEvent(THREAD, {content, tags}), relays: [url], + event: createEvent(THREAD, {content, tags}), }) history.back() diff --git a/src/app/components/ThunkStatus.svelte b/src/app/components/ThunkStatus.svelte index f2c01c4d..75b6611f 100644 --- a/src/app/components/ThunkStatus.svelte +++ b/src/app/components/ThunkStatus.svelte @@ -44,6 +44,7 @@ {#if isFailure && failure} {@const [url, {message, status}] = failure} @@ -53,7 +54,7 @@ {:else if canCancel || isPending} - + Sending... {#if canCancel} diff --git a/src/app/notifications.ts b/src/app/notifications.ts index 87f49ab0..81850855 100644 --- a/src/app/notifications.ts +++ b/src/app/notifications.ts @@ -1,8 +1,9 @@ import {writable, derived} from "svelte/store" +import {page} from "$app/stores" import {deriveEvents} from "@welshman/store" import {repository, pubkey} from "@welshman/app" import {prop, max, sortBy, assoc, lt, now} from "@welshman/lib" -import type {Filter} from "@welshman/util" +import type {Filter, TrustedEvent} from "@welshman/util" import {DIRECT_MESSAGE} from "@welshman/util" import {makeSpacePath} from "@app/routes" import { @@ -43,25 +44,31 @@ export const getRoomFilters = (room: string): Filter[] => ROOM_FILTERS.map(assoc // Notification derivation +export const getNotification = ( + pubkey: string | null, + lastChecked: number, + events: TrustedEvent[], +) => { + const [latestEvent] = sortBy($e => -$e.created_at, events) + + return latestEvent?.pubkey !== pubkey && lt(lastChecked, latestEvent?.created_at) +} + export const deriveNotification = (path: string, filters: Filter[], url?: string) => { const events = url ? deriveEventsForUrl(url, filters) : deriveEvents(repository, {filters}) return derived( [pubkey, deriveChecked("*"), deriveChecked(path), events], ([$pubkey, $allChecked, $checked, $events]) => { - const [latestEvent] = sortBy($e => -$e.created_at, $events) - - return ( - latestEvent?.pubkey !== $pubkey && lt(max([$allChecked, $checked]), latestEvent?.created_at) - ) + return getNotification($pubkey, max([$allChecked, $checked]), $events) }, ) } -export const spacesNotification = derived( +export const spacesNotifications = derived( [pubkey, checked, userMembership, deriveEvents(repository, {filters: SPACE_FILTERS})], ([$pubkey, $checked, $userMembership, $events]) => { - return getMembershipUrls($userMembership).some(url => { + return getMembershipUrls($userMembership).filter(url => { const path = makeSpacePath(url) const lastChecked = max([$checked["*"], $checked[path]]) const [latestEvent] = sortBy($e => -$e.created_at, $events) @@ -70,3 +77,9 @@ export const spacesNotification = derived( }) }, ) + +export const inactiveSpacesNotifications = derived( + [page, spacesNotifications], + ([$page, $spacesNotifications]) => + $spacesNotifications.filter(url => !$page.url.pathname.startsWith(makeSpacePath(url))), +) diff --git a/src/lib/components/Tippy.svelte b/src/lib/components/Tippy.svelte index de62d016..1f3e392a 100644 --- a/src/lib/components/Tippy.svelte +++ b/src/lib/components/Tippy.svelte @@ -36,6 +36,6 @@ }) -
+
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 0019082e..2b0c019f 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -15,7 +15,6 @@ RELAYS, INBOX_RELAYS, WRAP, - DELETE, getPubkeyTagValues, getListTags, } from "@welshman/util" @@ -37,9 +36,11 @@ dropSession, getRelayUrls, userInboxRelaySelections, + load, } from "@welshman/app" import * as lib from "@welshman/lib" import * as util from "@welshman/util" + import * as welshmanSigner from "@welshman/signer" import * as net from "@welshman/net" import * as app from "@welshman/app" import AppContainer from "@app/components/AppContainer.svelte" @@ -53,13 +54,13 @@ getMembershipRooms, userMembership, ensureUnwrapped, - MEMBERSHIPS, MESSAGE, COMMENT, THREAD, GENERAL, } from "@app/state" import {loadUserData, subscribePersistent} from "@app/commands" + import * as commands from "@app/commands" import {checked} from "@app/notifications" import * as notifications from "@app/notifications" import * as state from "@app/state" @@ -72,7 +73,18 @@ let ready: Promise = Promise.resolve() onMount(async () => { - Object.assign(window, {get, nip19, ...lib, ...util, ...net, ...app, ...state, ...notifications}) + Object.assign(window, { + get, + nip19, + ...lib, + ...welshmanSigner, + ...util, + ...net, + ...app, + ...state, + ...commands, + ...notifications, + }) const getScoreEvent = () => { const ALWAYS_KEEP = Infinity @@ -183,17 +195,23 @@ const rooms = uniq(getMembershipRooms($membership).map(m => m.room)).concat(GENERAL) const relays = uniq(getMembershipUrls($membership)) + // Get one event for each of our notification categories + load({ + relays, + filters: [ + {kinds: [THREAD], limit: 1}, + {kinds: [COMMENT], "#K": [String(THREAD)], limit: 1}, + ...rooms.map(room => ({kinds: [MESSAGE], "#~": [room], limit: 1})), + ], + }) + + // Listen for new notifications/memberships subscribePersistent({ relays, filters: [ {kinds: [THREAD], since}, - {kinds: [THREAD], limit: 1}, + {kinds: [COMMENT], "#K": [String(THREAD)], since}, {kinds: [MESSAGE], "#~": rooms, since}, - {kinds: [MESSAGE], "#~": rooms, limit: 1}, - {kinds: [COMMENT], "#K": [THREAD, MESSAGE].map(String), since}, - {kinds: [COMMENT], "#K": [THREAD, MESSAGE].map(String), limit: 1}, - {kinds: [DELETE], "#k": [THREAD, COMMENT, MESSAGE].map(String), since}, - {kinds: [MEMBERSHIPS], "#r": relays, since: ago(WEEK, 2)}, ], }) }) diff --git a/src/routes/discover/+page.svelte b/src/routes/discover/+page.svelte index 315a0a82..895d9148 100644 --- a/src/routes/discover/+page.svelte +++ b/src/routes/discover/+page.svelte @@ -3,7 +3,7 @@ import {derived} from "svelte/store" import {addToMapKey, dec, gt} from "@welshman/lib" import type {Relay} from "@welshman/app" - import {relays, createSearch, relaySelections} from "@welshman/app" + import {relays, createSearch} from "@welshman/app" import {createScroller} from "@lib/html" import Icon from "@lib/components/Icon.svelte" import Page from "@lib/components/Page.svelte" @@ -41,7 +41,6 @@ let term = "" let limit = 20 let element: Element - let promise: Promise $: relaySearch = createSearch($relays, { getValue: (relay: Relay) => relay.url, @@ -59,8 +58,6 @@ }) onMount(() => { - promise = Promise.all([discoverRelays($memberships), discoverRelays($relaySelections)]) - const scroller = createScroller({ element, onScroll: () => { @@ -126,8 +123,10 @@ {/if} {/each} - {#await promise} - Loading more relays... + {#await discoverRelays($memberships)} +
+ Loading more relays... +
{/await}
diff --git a/src/routes/spaces/[relay]/+layout.svelte b/src/routes/spaces/[relay]/+layout.svelte index 7ea020e9..cc348f47 100644 --- a/src/routes/spaces/[relay]/+layout.svelte +++ b/src/routes/spaces/[relay]/+layout.svelte @@ -1,14 +1,16 @@ diff --git a/src/routes/spaces/[relay]/+page.svelte b/src/routes/spaces/[relay]/+page.svelte index b57e9a77..4074817a 100644 --- a/src/routes/spaces/[relay]/+page.svelte +++ b/src/routes/spaces/[relay]/+page.svelte @@ -3,22 +3,18 @@ import {deriveRelay} from "@welshman/app" import Icon from "@lib/components/Icon.svelte" import Link from "@lib/components/Link.svelte" - import Button from "@lib/components/Button.svelte" import Divider from "@lib/components/Divider.svelte" import PageBar from "@lib/components/PageBar.svelte" - import MenuSpace from "@app/components/MenuSpace.svelte" + import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte" import ProfileFeed from "@app/components/ProfileFeed.svelte" import RelayName from "@app/components/RelayName.svelte" import RelayDescription from "@app/components/RelayDescription.svelte" import {decodeRelay} from "@app/state" - import {pushDrawer} from "@app/modal" import {makeChatPath} from "@app/routes" const url = decodeRelay($page.params.relay) const relay = deriveRelay(url) - const openMenu = () => pushDrawer(MenuSpace, {url}) - $: pubkey = $relay?.profile?.pubkey @@ -35,9 +31,7 @@ Contact Owner {/if} - +
diff --git a/src/routes/spaces/[relay]/[room]/+page.svelte b/src/routes/spaces/[relay]/[room]/+page.svelte index 787f6c56..3a1a7869 100644 --- a/src/routes/spaces/[relay]/[room]/+page.svelte +++ b/src/routes/spaces/[relay]/[room]/+page.svelte @@ -8,11 +8,11 @@
@@ -90,9 +96,7 @@ Create a Thread - +
diff --git a/src/routes/spaces/[relay]/threads/[id]/+page.svelte b/src/routes/spaces/[relay]/threads/[id]/+page.svelte index c36676ea..44b52157 100644 --- a/src/routes/spaces/[relay]/threads/[id]/+page.svelte +++ b/src/routes/spaces/[relay]/threads/[id]/+page.svelte @@ -1,8 +1,8 @@ @@ -91,9 +88,7 @@

{title}

- +