diff --git a/src/app/components/ChatItem.svelte b/src/app/components/ChatItem.svelte
index 4c4e3324..c33d5b82 100644
--- a/src/app/components/ChatItem.svelte
+++ b/src/app/components/ChatItem.svelte
@@ -1,14 +1,16 @@
+
+
+
+ {room}
+
diff --git a/src/app/components/PrimaryNav.svelte b/src/app/components/PrimaryNav.svelte
index e5a696ae..0ea7e1fa 100644
--- a/src/app/components/PrimaryNav.svelte
+++ b/src/app/components/PrimaryNav.svelte
@@ -1,16 +1,17 @@
+
+
+
+
diff --git a/src/app/components/SignUp.svelte b/src/app/components/SignUp.svelte
index ce221d63..d2833afd 100644
--- a/src/app/components/SignUp.svelte
+++ b/src/app/components/SignUp.svelte
@@ -10,6 +10,7 @@
import LogIn from "@app/components/LogIn.svelte"
import InfoNostr from "@app/components/InfoNostr.svelte"
import {pushModal, clearModals} from "@app/modal"
+ import {setChecked} from "@app/notifications"
import {PLATFORM_NAME} from "@app/state"
import {pushToast} from "@app/toast"
@@ -42,6 +43,7 @@
if (await loginBroker.connect("", nip46Perms)) {
addSession({method: "nip46", pubkey, secret, handler: {...handler, pubkey}})
pushToast({message: "Successfully logged in!"})
+ setChecked("*")
clearModals()
} else {
pushToast({
diff --git a/src/app/components/ThreadActions.svelte b/src/app/components/ThreadActions.svelte
index 5897de9b..bc8d5ab9 100644
--- a/src/app/components/ThreadActions.svelte
+++ b/src/app/components/ThreadActions.svelte
@@ -13,6 +13,8 @@
import ThunkStatus from "@app/components/ThunkStatus.svelte"
import ThreadMenu from "@app/components/ThreadMenu.svelte"
import {publishDelete, publishReaction} from "@app/commands"
+ import {deriveNotification} from "@app/notifications"
+ import {makeSpacePath} from "@app/routes"
import {COMMENT} from "@app/state"
export let url
@@ -21,7 +23,10 @@
const thunk = $thunks[event.id]
const deleted = deriveIsDeleted(repository, event)
- const replies = deriveEvents(repository, {filters: [{kinds: [COMMENT], "#E": [event.id]}]})
+ const path = makeSpacePath(url, "threads", event.id)
+ const filters = [{kinds: [COMMENT], "#E": [event.id]}]
+ const notification = deriveNotification(path, filters, url)
+ const replies = deriveEvents(repository, {filters})
const showPopover = () => popover.show()
@@ -58,7 +63,10 @@
{$replies.length} {$replies.length === 1 ? "reply" : "replies"}
-
+
+ {#if $notification}
+
+ {/if}
Active {formatTimestampRelative(lastActive)}
{/if}
diff --git a/src/app/notifications.ts b/src/app/notifications.ts
new file mode 100644
index 00000000..e559f8eb
--- /dev/null
+++ b/src/app/notifications.ts
@@ -0,0 +1,51 @@
+import {writable, derived} from "svelte/store"
+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 {DIRECT_MESSAGE} from "@welshman/util"
+import {MESSAGE, THREAD, COMMENT, deriveEventsForUrl} from "@app/state"
+
+// Checked state
+
+export const checked = writable
>({})
+
+export const deriveChecked = (key: string) => derived(checked, prop(key))
+
+export const setChecked = (key: string, ts = now()) =>
+ checked.update(state => ({...state, [key]: ts}))
+
+// Filters for various routes
+
+export const CHAT_FILTERS: Filter[] = [{kinds: [DIRECT_MESSAGE]}]
+
+export const SPACE_FILTERS: Filter[] = [{kinds: [THREAD, MESSAGE, COMMENT]}]
+
+export const ROOM_FILTERS: Filter[] = [{kinds: [MESSAGE]}]
+
+export const THREAD_FILTERS: Filter[] = [
+ {kinds: [THREAD]},
+ {kinds: [COMMENT], "#K": [String(THREAD)]},
+]
+
+export const getNotificationFilters = (since: number): Filter[] =>
+ [...CHAT_FILTERS, ...SPACE_FILTERS, ...THREAD_FILTERS].map(assoc("since", since))
+
+export const getRoomFilters = (room: string): Filter[] => ROOM_FILTERS.map(assoc("#~", [room]))
+
+// Notification derivation
+
+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)
+ )
+ },
+ )
+}
diff --git a/src/app/state.ts b/src/app/state.ts
index 89e9c271..e18f5385 100644
--- a/src/app/state.ts
+++ b/src/app/state.ts
@@ -1,10 +1,9 @@
import twColors from "tailwindcss/colors"
-import {get, derived, writable} from "svelte/store"
+import {get, derived} from "svelte/store"
import {nip19} from "nostr-tools"
import type {Maybe} from "@welshman/lib"
import {
ctx,
- now,
setContext,
remove,
assoc,
@@ -17,7 +16,6 @@ import {
nthEq,
shuffle,
parseJson,
- prop,
} from "@welshman/lib"
import {
getIdFilters,
@@ -253,6 +251,12 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
)
}
+export const getEventsForUrl = (url: string, filters: Filter[]) =>
+ sortBy(
+ e => -e.created_at,
+ repository.query(filters).filter(e => e.tags.find(nthEq(0, "~"))?.[2] === url),
+ )
+
export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
derived(deriveEvents(repository, {filters}), $events =>
sortBy(
@@ -261,14 +265,6 @@ export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
),
)
-// Last checked timestamps, notifications
-
-export const checked = writable>({})
-
-export const deriveChecked = (key: string) => derived(checked, prop(key))
-
-export const setChecked = (key: string, ts = now()) => checked.update(assoc(key, ts))
-
// Settings
export const SETTINGS = 38489
diff --git a/src/lib/components/PrimaryNavItem.svelte b/src/lib/components/PrimaryNavItem.svelte
index 7892c992..fa791b2c 100644
--- a/src/lib/components/PrimaryNavItem.svelte
+++ b/src/lib/components/PrimaryNavItem.svelte
@@ -5,6 +5,7 @@
export let title = ""
export let href = ""
export let prefix = ""
+ export let notification = false
$: active = $page.url?.pathname?.startsWith(prefix || href || "bogus")
@@ -16,6 +17,9 @@
class:bg-base-300={active}
class:tooltip={title}
data-tip={title}>
+ {#if !active && notification}
+
+ {/if}
@@ -26,6 +30,9 @@
class:bg-base-300={active}
class:tooltip={title}
data-tip={title}>
+ {#if !active && notification}
+
+ {/if}
diff --git a/src/lib/components/SecondaryNavItem.svelte b/src/lib/components/SecondaryNavItem.svelte
index 7b91f8fd..a44c89ce 100644
--- a/src/lib/components/SecondaryNavItem.svelte
+++ b/src/lib/components/SecondaryNavItem.svelte
@@ -22,9 +22,11 @@
@@ -36,11 +38,14 @@
on:click
class={cx(
$$props.class,
- "flex items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content",
+ "relative flex items-center gap-3 text-left transition-all hover:bg-base-100 hover:text-base-content",
)}
class:text-base-content={active}
class:bg-base-100={active}>
+ {#if !active && notification}
+
+ {/if}
{:else}
{/if}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 9b2823a7..2e2b2136 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -55,8 +55,11 @@
MESSAGE,
COMMENT,
THREAD,
+ GENERAL,
} from "@app/state"
import {loadUserData, subscribePersistent} from "@app/commands"
+ import {checked} from "@app/notifications"
+ import * as notifications from "@app/notifications"
import * as state from "@app/state"
// Migration: old nostrtalk instance used different sessions
@@ -67,7 +70,7 @@
let ready: Promise = Promise.resolve()
onMount(async () => {
- Object.assign(window, {get, ...lib, ...util, ...net, ...app, ...state})
+ Object.assign(window, {get, ...lib, ...util, ...net, ...app, ...state, ...notifications})
const getScoreEvent = () => {
const ALWAYS_KEEP = Infinity
@@ -133,6 +136,7 @@
events: storageAdapters.fromRepository(repository, {throttle: 300, migrate: migrateEvents}),
relays: {keyPath: "url", store: throttled(1000, relays)},
handles: {keyPath: "nip05", store: throttled(1000, handles)},
+ checked: storageAdapters.fromObjectStore(checked, {throttle: 1000}),
freshness: storageAdapters.fromObjectStore(freshness, {
throttle: 1000,
migrate: migrateFreshness,
@@ -167,29 +171,32 @@
await loadUserData($pubkey)
}
+ // Listen for space data, populate space-based notifications
let unsubRooms: any
userMembership.subscribe($membership => {
unsubRooms?.()
const since = ago(30)
- const rooms = uniq(getMembershipRooms($membership).map(m => m.room))
+ const rooms = uniq(getMembershipRooms($membership).map(m => m.room)).concat(GENERAL)
const relays = uniq(getMembershipUrls($membership))
- if (relays.length > 0) {
- subscribePersistent({
- relays,
- filters: [
- {kinds: [THREAD], since},
- {kinds: [MESSAGE], "#~": rooms, since},
- {kinds: [COMMENT], "#K": [THREAD, MESSAGE].map(String), since},
- {kinds: [DELETE], "#k": [THREAD, COMMENT, MESSAGE].map(String), since},
- {kinds: [MEMBERSHIPS], "#r": relays, since},
- ],
- })
- }
+ subscribePersistent({
+ relays,
+ filters: [
+ {kinds: [THREAD], since},
+ {kinds: [THREAD], limit: 1},
+ {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)},
+ ],
+ })
})
+ // Listen for chats, populate chat-based notifications
let unsubChats: any
derived([pubkey, userInboxRelaySelections], identity).subscribe(
@@ -198,7 +205,10 @@
if ($pubkey) {
unsubChats = subscribePersistent({
- filters: [{kinds: [WRAP], "#p": [$pubkey], since: ago(WEEK, 2)}],
+ filters: [
+ {kinds: [WRAP], "#p": [$pubkey], since: ago(WEEK, 2)},
+ {kinds: [WRAP], "#p": [$pubkey], limit: 100},
+ ],
relays: getRelayUrls($userInboxRelaySelections),
})
}
diff --git a/src/routes/chat/[chat]/+page.svelte b/src/routes/chat/[chat]/+page.svelte
index 4542ffb9..0e275f83 100644
--- a/src/routes/chat/[chat]/+page.svelte
+++ b/src/routes/chat/[chat]/+page.svelte
@@ -1,10 +1,10 @@
diff --git a/src/routes/spaces/[relay]/+layout.svelte b/src/routes/spaces/[relay]/+layout.svelte
index 92fcd65e..8490ce7e 100644
--- a/src/routes/spaces/[relay]/+layout.svelte
+++ b/src/routes/spaces/[relay]/+layout.svelte
@@ -1,11 +1,12 @@
{#key url}
diff --git a/src/routes/spaces/[relay]/+page.svelte b/src/routes/spaces/[relay]/+page.svelte
index a6c9579e..b57e9a77 100644
--- a/src/routes/spaces/[relay]/+page.svelte
+++ b/src/routes/spaces/[relay]/+page.svelte
@@ -1,5 +1,4 @@
diff --git a/src/routes/spaces/[relay]/[room]/+page.svelte b/src/routes/spaces/[relay]/[room]/+page.svelte
index 3cf4be91..787f6c56 100644
--- a/src/routes/spaces/[relay]/[room]/+page.svelte
+++ b/src/routes/spaces/[relay]/[room]/+page.svelte
@@ -8,7 +8,7 @@
@@ -101,7 +97,9 @@
{#each events as event (event.id)}
-
+
+
+
{/each}
diff --git a/src/routes/spaces/[relay]/threads/[id]/+page.svelte b/src/routes/spaces/[relay]/threads/[id]/+page.svelte
index 2509b494..c36676ea 100644
--- a/src/routes/spaces/[relay]/threads/[id]/+page.svelte
+++ b/src/routes/spaces/[relay]/threads/[id]/+page.svelte
@@ -1,5 +1,5 @@