forked from coracle/flotilla
Simplify and optimize notifications
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {remove, assoc} from "@welshman/lib"
|
import {remove} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey, loadInboxRelaySelections} from "@welshman/app"
|
import {pubkey, loadInboxRelaySelections} from "@welshman/app"
|
||||||
import {fade} from "@lib/transition"
|
import {fade} from "@lib/transition"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import {makeChatPath} from "@app/routes"
|
import {makeChatPath} from "@app/routes"
|
||||||
import {CHAT_FILTERS, deriveNotification} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
|
|
||||||
export let id: string
|
export let id: string
|
||||||
export let pubkeys: string[]
|
export let pubkeys: string[]
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
const others = remove($pubkey!, pubkeys)
|
const others = remove($pubkey!, pubkeys)
|
||||||
const active = $page.params.chat === id
|
const active = $page.params.chat === id
|
||||||
const path = makeChatPath(pubkeys)
|
const path = makeChatPath(pubkeys)
|
||||||
const notification = deriveNotification(path, CHAT_FILTERS.map(assoc("authors", pubkeys)))
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
for (const pk of others) {
|
for (const pk of others) {
|
||||||
@@ -47,7 +46,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if !active && $notification}
|
{#if !active && $notifications.has(path)}
|
||||||
<div class="h-2 w-2 rounded-full bg-primary" transition:fade />
|
<div class="h-2 w-2 rounded-full bg-primary" transition:fade />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
deriveUserRooms,
|
deriveUserRooms,
|
||||||
deriveOtherRooms,
|
deriveOtherRooms,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
import {deriveNotification, THREAD_FILTERS} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
import {pullConservatively} from "@app/requests"
|
import {pullConservatively} from "@app/requests"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
@@ -30,7 +30,6 @@
|
|||||||
export let url
|
export let url
|
||||||
|
|
||||||
const threadsPath = makeSpacePath(url, "threads")
|
const threadsPath = makeSpacePath(url, "threads")
|
||||||
const threadsNotification = deriveNotification(threadsPath, THREAD_FILTERS, url)
|
|
||||||
const userRooms = deriveUserRooms(url)
|
const userRooms = deriveUserRooms(url)
|
||||||
const otherRooms = deriveOtherRooms(url)
|
const otherRooms = deriveOtherRooms(url)
|
||||||
|
|
||||||
@@ -114,7 +113,7 @@
|
|||||||
<SecondaryNavItem href={makeSpacePath(url)}>
|
<SecondaryNavItem href={makeSpacePath(url)}>
|
||||||
<Icon icon="home-smile" /> Home
|
<Icon icon="home-smile" /> Home
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
<SecondaryNavItem href={threadsPath} notification={$threadsNotification}>
|
<SecondaryNavItem href={threadsPath} notification={$notifications.has(threadsPath)}>
|
||||||
<Icon icon="notes-minimalistic" /> Threads
|
<Icon icon="notes-minimalistic" /> Threads
|
||||||
</SecondaryNavItem>
|
</SecondaryNavItem>
|
||||||
<div class="h-2" />
|
<div class="h-2" />
|
||||||
|
|||||||
@@ -1,48 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {page} from "$app/stores"
|
|
||||||
import {derived} from "svelte/store"
|
|
||||||
import {max} from "@welshman/lib"
|
|
||||||
import {matchFilter, MESSAGE} from "@welshman/util"
|
|
||||||
import {pubkey} from "@welshman/app"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import MenuSpace from "@app/components/MenuSpace.svelte"
|
import MenuSpace from "@app/components/MenuSpace.svelte"
|
||||||
import {checked, getNotification, deriveNotification, THREAD_FILTERS} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
import {userMembership, getMembershipRoomsByUrl, deriveEventsForUrl, GENERAL} from "@app/state"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {makeRoomPath, makeSpacePath} from "@app/routes"
|
|
||||||
import {pushDrawer} from "@app/modal"
|
import {pushDrawer} from "@app/modal"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
|
const path = makeSpacePath(url)
|
||||||
|
|
||||||
const openMenu = () => pushDrawer(MenuSpace, {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]) =>
|
|
||||||
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({"#h": [room]}, e))
|
|
||||||
|
|
||||||
return getNotification($pubkey, lastChecked, roomEvents)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button on:click={openMenu} class="btn btn-neutral btn-sm relative md:hidden">
|
<Button on:click={openMenu} class="btn btn-neutral btn-sm relative md:hidden">
|
||||||
<Icon icon="menu-dots" />
|
<Icon icon="menu-dots" />
|
||||||
{#if $threadsNotification || $notification}
|
{#if $notifications.has(path)}
|
||||||
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-primary" />
|
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-primary" />
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {readable} from "svelte/store"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
import ChannelName from "@app/components/ChannelName.svelte"
|
import ChannelName from "@app/components/ChannelName.svelte"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {deriveChannel, channelIsLocked} from "@app/state"
|
import {deriveChannel, channelIsLocked} from "@app/state"
|
||||||
import {deriveNotification, getRoomFilters} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
export let room
|
export let room
|
||||||
@@ -13,12 +12,9 @@
|
|||||||
|
|
||||||
const path = makeSpacePath(url, room)
|
const path = makeSpacePath(url, room)
|
||||||
const channel = deriveChannel(url, room)
|
const channel = deriveChannel(url, room)
|
||||||
const notification = notify
|
|
||||||
? deriveNotification(path, getRoomFilters(room), url)
|
|
||||||
: readable(false)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SecondaryNavItem href={path} notification={$notification}>
|
<SecondaryNavItem href={path} notification={notify ? $notifications.has(path) : false}>
|
||||||
{#if channelIsLocked($channel)}
|
{#if channelIsLocked($channel)}
|
||||||
<Icon icon="lock" size={4} />
|
<Icon icon="lock" size={4} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {inactiveSpacesNotifications} from "@app/notifications"
|
import {inactiveNotifications} from "@app/notifications"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<div slot="icon"><SpaceAvatar {url} /></div>
|
<div slot="icon"><SpaceAvatar {url} /></div>
|
||||||
<div slot="title" class="flex gap-1">
|
<div slot="title" class="flex gap-1">
|
||||||
<RelayName {url} />
|
<RelayName {url} />
|
||||||
{#if $inactiveSpacesNotifications.includes(path)}
|
{#if $inactiveNotifications.has(path)}
|
||||||
<div class="relative top-1 h-2 w-2 rounded-full bg-primary" />
|
<div class="relative top-1 h-2 w-2 rounded-full bg-primary" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,20 +17,22 @@
|
|||||||
PLATFORM_LOGO,
|
PLATFORM_LOGO,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {deriveNotification, inactiveSpacesNotifications, CHAT_FILTERS} from "@app/notifications"
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
import {notifications, inactiveNotifications} from "@app/notifications"
|
||||||
const chatNotification = deriveNotification("/chat", CHAT_FILTERS)
|
|
||||||
|
|
||||||
const addSpace = () => pushModal(SpaceAdd)
|
const addSpace = () => pushModal(SpaceAdd)
|
||||||
|
|
||||||
const showSpacesMenu = () =>
|
const showSpacesMenu = () => (spacePaths.length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd))
|
||||||
getMembershipUrls($userMembership).length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd)
|
|
||||||
|
|
||||||
const showSettingsMenu = () => pushModal(MenuSettings)
|
const showSettingsMenu = () => pushModal(MenuSettings)
|
||||||
|
|
||||||
const openNotes = () => ($canDecrypt ? goto("/notes") : pushModal(ChatEnable, {next: "/notes"}))
|
const openNotes = () => ($canDecrypt ? goto("/notes") : pushModal(ChatEnable, {next: "/notes"}))
|
||||||
|
|
||||||
const openChat = () => ($canDecrypt ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
|
const openChat = () => ($canDecrypt ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
|
||||||
|
|
||||||
|
$: spaceUrls = getMembershipUrls($userMembership)
|
||||||
|
$: spacePaths = spaceUrls.map(url => makeSpacePath(url))
|
||||||
|
$: anySpaceNotifications = spacePaths.some(path => $inactiveNotifications.has(path))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative z-nav hidden w-14 flex-shrink-0 bg-base-200 pt-4 md:block">
|
<div class="relative z-nav hidden w-14 flex-shrink-0 bg-base-200 pt-4 md:block">
|
||||||
@@ -43,7 +45,7 @@
|
|||||||
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" />
|
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#each getMembershipUrls($userMembership) as url (url)}
|
{#each spaceUrls as url (url)}
|
||||||
<PrimaryNavItemSpace {url} />
|
<PrimaryNavItemSpace {url} />
|
||||||
{/each}
|
{/each}
|
||||||
<PrimaryNavItem title="Add Space" on:click={addSpace} class="tooltip-right">
|
<PrimaryNavItem title="Add Space" on:click={addSpace} class="tooltip-right">
|
||||||
@@ -66,7 +68,7 @@
|
|||||||
title="Messages"
|
title="Messages"
|
||||||
on:click={openChat}
|
on:click={openChat}
|
||||||
class="tooltip-right"
|
class="tooltip-right"
|
||||||
notification={$chatNotification}>
|
notification={$notifications.has("/chat")}>
|
||||||
<Avatar icon="letter" class="!h-10 !w-10" />
|
<Avatar icon="letter" class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
|
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
|
||||||
@@ -88,13 +90,13 @@
|
|||||||
<PrimaryNavItem title="Notes" href="/notes">
|
<PrimaryNavItem title="Notes" href="/notes">
|
||||||
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
|
<Avatar icon="notes-minimalistic" class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem title="Messages" on:click={openChat} notification={$chatNotification}>
|
<PrimaryNavItem
|
||||||
|
title="Messages"
|
||||||
|
on:click={openChat}
|
||||||
|
notification={$notifications.has("/chat")}>
|
||||||
<Avatar icon="letter" class="!h-10 !w-10" />
|
<Avatar icon="letter" class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem title="Spaces" on:click={showSpacesMenu} notification={anySpaceNotifications}>
|
||||||
title="Spaces"
|
|
||||||
on:click={showSpacesMenu}
|
|
||||||
notification={$inactiveSpacesNotifications.length > 0}>
|
|
||||||
<Avatar icon="settings-minimalistic" class="!h-10 !w-10" />
|
<Avatar icon="settings-minimalistic" class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||||
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {spacesNotifications} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
|
|
||||||
export let url
|
export let url
|
||||||
|
|
||||||
@@ -14,6 +14,6 @@
|
|||||||
title={displayRelayUrl(url)}
|
title={displayRelayUrl(url)}
|
||||||
href={path}
|
href={path}
|
||||||
class="tooltip-right"
|
class="tooltip-right"
|
||||||
notification={$spacesNotifications.includes(path)}>
|
notification={$notifications.has(path)}>
|
||||||
<SpaceAvatar {url} />
|
<SpaceAvatar {url} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
||||||
import ThreadMenu from "@app/components/ThreadMenu.svelte"
|
import ThreadMenu from "@app/components/ThreadMenu.svelte"
|
||||||
import {publishDelete, publishReaction} from "@app/commands"
|
import {publishDelete, publishReaction} from "@app/commands"
|
||||||
import {deriveNotification} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {PROTECTED} from "@app/state"
|
import {PROTECTED} from "@app/state"
|
||||||
|
|
||||||
@@ -28,7 +28,6 @@
|
|||||||
const path = makeSpacePath(url, "threads", event.id)
|
const path = makeSpacePath(url, "threads", event.id)
|
||||||
const filters = [{kinds: [COMMENT], "#E": [event.id]}]
|
const filters = [{kinds: [COMMENT], "#E": [event.id]}]
|
||||||
const replies = deriveEvents(repository, {filters})
|
const replies = deriveEvents(repository, {filters})
|
||||||
const notification = deriveNotification(path, filters, url)
|
|
||||||
|
|
||||||
const showPopover = () => popover.show()
|
const showPopover = () => popover.show()
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@
|
|||||||
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
|
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn btn-neutral btn-xs relative hidden rounded-full sm:flex">
|
<div class="btn btn-neutral btn-xs relative hidden rounded-full sm:flex">
|
||||||
{#if $notification}
|
{#if $notifications.has(path)}
|
||||||
<div class="h-2 w-2 rounded-full bg-primary" />
|
<div class="h-2 w-2 rounded-full bg-primary" />
|
||||||
{/if}
|
{/if}
|
||||||
Active {formatTimestampRelative(lastActive)}
|
Active {formatTimestampRelative(lastActive)}
|
||||||
|
|||||||
+63
-75
@@ -1,16 +1,15 @@
|
|||||||
import {writable, derived} from "svelte/store"
|
import {writable, derived} from "svelte/store"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {deriveEvents} from "@welshman/store"
|
import {pubkey} from "@welshman/app"
|
||||||
import {repository, pubkey} from "@welshman/app"
|
import {prop, max, sortBy, now} from "@welshman/lib"
|
||||||
import {prop, max, sortBy, assoc, lt, now} from "@welshman/lib"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import type {Filter, TrustedEvent} from "@welshman/util"
|
import {MESSAGE} from "@welshman/util"
|
||||||
import {DIRECT_MESSAGE, MESSAGE, THREAD, COMMENT} from "@welshman/util"
|
import {makeSpacePath, makeChatPath, makeThreadPath, makeRoomPath} from "@app/routes"
|
||||||
import {makeSpacePath, makeThreadPath, makeRoomPath} from "@app/routes"
|
|
||||||
import {
|
import {
|
||||||
LEGACY_THREAD,
|
THREAD_FILTER,
|
||||||
|
COMMENT_FILTER,
|
||||||
|
chats,
|
||||||
getEventsForUrl,
|
getEventsForUrl,
|
||||||
deriveEventsForUrl,
|
|
||||||
getMembershipUrls,
|
|
||||||
userRoomsByUrl,
|
userRoomsByUrl,
|
||||||
repositoryStore,
|
repositoryStore,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
@@ -19,85 +18,74 @@ import {
|
|||||||
|
|
||||||
export const checked = writable<Record<string, number>>({})
|
export const checked = writable<Record<string, number>>({})
|
||||||
|
|
||||||
checked.subscribe(v => console.log("====== checked", v))
|
|
||||||
|
|
||||||
export const deriveChecked = (key: string) => derived(checked, prop(key))
|
export const deriveChecked = (key: string) => derived(checked, prop(key))
|
||||||
|
|
||||||
export const setChecked = (key: string, ts = now()) =>
|
export const setChecked = (key: string) =>
|
||||||
Boolean(console.trace("====== setChecked", key)) ||
|
checked.update(state => ({...state, [key]: now()}))
|
||||||
checked.update(state => ({...state, [key]: ts}))
|
|
||||||
|
|
||||||
// Filters for various routes
|
// Derived notifications state
|
||||||
|
|
||||||
export const CHAT_FILTERS: Filter[] = [{kinds: [DIRECT_MESSAGE]}]
|
export const notifications = derived(
|
||||||
|
[pubkey, checked, chats, userRoomsByUrl, repositoryStore],
|
||||||
|
([$pubkey, $checked, $chats, $userRoomsByUrl, $repository]) => {
|
||||||
|
const hasNotification = (path: string, events: TrustedEvent[]) => {
|
||||||
|
const [latestEvent] = sortBy($e => -$e.created_at, events)
|
||||||
|
|
||||||
export const SPACE_FILTERS: Filter[] = [{kinds: [THREAD, MESSAGE, COMMENT]}]
|
if (!latestEvent || latestEvent.pubkey === $pubkey) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export const ROOM_FILTERS: Filter[] = [{kinds: [MESSAGE]}]
|
let checkPath = ""
|
||||||
|
let lastChecked = $checked["*"]
|
||||||
|
|
||||||
export const THREAD_FILTERS: Filter[] = [
|
for (const segment of path.slice(1).split("/")) {
|
||||||
{kinds: [THREAD, LEGACY_THREAD]},
|
checkPath += "/" + segment
|
||||||
{kinds: [COMMENT], "#K": [String(THREAD), String(LEGACY_THREAD)]},
|
lastChecked = max([lastChecked, $checked[checkPath]])
|
||||||
]
|
}
|
||||||
|
|
||||||
export const getNotificationFilters = (since: number): Filter[] =>
|
return lastChecked < latestEvent.created_at
|
||||||
[...CHAT_FILTERS, ...SPACE_FILTERS, ...THREAD_FILTERS].map(assoc("since", since))
|
|
||||||
|
|
||||||
export const getRoomFilters = (room: string): Filter[] => ROOM_FILTERS.map(assoc("#h", [room]))
|
|
||||||
|
|
||||||
// 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]) => {
|
|
||||||
return getNotification($pubkey, max([$allChecked, $checked]), $events)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const spacesNotifications = derived(
|
|
||||||
[pubkey, checked, userRoomsByUrl, repositoryStore],
|
|
||||||
([$pubkey, $checked, $userRoomsByUrl, $repository]) => {
|
|
||||||
const hasNotification = (url: string, path: string, filters: Filter[]) => {
|
|
||||||
const lastChecked = max([$checked["*"], $checked[path]])
|
|
||||||
const events = getEventsForUrl($repository, url, filters)
|
|
||||||
|
|
||||||
return getNotification($pubkey, lastChecked, events)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from($userRoomsByUrl.entries())
|
const paths = new Set<string>()
|
||||||
.filter(([url, rooms]) => {
|
|
||||||
if (hasNotification(url, makeThreadPath(url), THREAD_FILTERS)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const room of rooms) {
|
for (const {pubkeys, messages} of $chats) {
|
||||||
if (hasNotification(url, makeRoomPath(url, room), [{kinds: [MESSAGE], "#h": [room]}])) {
|
const chatPath = makeChatPath(pubkeys)
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
if (hasNotification(chatPath, messages)) {
|
||||||
})
|
paths.add("/chat")
|
||||||
.map(([url]) => makeSpacePath(url))
|
paths.add(chatPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [url, rooms] of $userRoomsByUrl.entries()) {
|
||||||
|
const spacePath = makeSpacePath(url)
|
||||||
|
const threadPath = makeThreadPath(url)
|
||||||
|
const threadFilters = [THREAD_FILTER, COMMENT_FILTER]
|
||||||
|
const threadEvents = getEventsForUrl($repository, url, threadFilters)
|
||||||
|
|
||||||
|
if (hasNotification(threadPath, threadEvents)) {
|
||||||
|
paths.add(spacePath)
|
||||||
|
paths.add(threadPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const room of rooms) {
|
||||||
|
const roomPath = makeRoomPath(url, room)
|
||||||
|
const roomFilters = [{kinds: [MESSAGE], "#h": [room]}]
|
||||||
|
const roomEvents = getEventsForUrl($repository, url, roomFilters)
|
||||||
|
|
||||||
|
if (hasNotification(roomPath, roomEvents)) {
|
||||||
|
paths.add(spacePath)
|
||||||
|
paths.add(roomPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paths
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export const inactiveSpacesNotifications = derived(
|
export const inactiveNotifications = derived(
|
||||||
[page, spacesNotifications],
|
[page, notifications],
|
||||||
([$page, $spacesNotifications]) =>
|
([$page, $notifications]) =>
|
||||||
$spacesNotifications.filter(path => !$page.url.pathname.startsWith(path)),
|
new Set(Array.from($notifications).filter(path => !$page.url.pathname.startsWith(path))),
|
||||||
)
|
)
|
||||||
|
|||||||
+16
-2
@@ -40,7 +40,14 @@ import {
|
|||||||
asDecryptedEvent,
|
asDecryptedEvent,
|
||||||
normalizeRelayUrl,
|
normalizeRelayUrl,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
|
import type {
|
||||||
|
TrustedEvent,
|
||||||
|
Repository,
|
||||||
|
SignedEvent,
|
||||||
|
PublishedList,
|
||||||
|
List,
|
||||||
|
Filter,
|
||||||
|
} from "@welshman/util"
|
||||||
import {Nip59} from "@welshman/signer"
|
import {Nip59} from "@welshman/signer"
|
||||||
import {
|
import {
|
||||||
pubkey,
|
pubkey,
|
||||||
@@ -63,7 +70,7 @@ import {
|
|||||||
thunks,
|
thunks,
|
||||||
walkThunks,
|
walkThunks,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import type {Thunk} from "@welshman/app"
|
import type {Thunk, Relay} from "@welshman/app"
|
||||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
||||||
import {deriveEvents, deriveEventsMapped, withGetter, synced} from "@welshman/store"
|
import {deriveEvents, deriveEventsMapped, withGetter, synced} from "@welshman/store"
|
||||||
|
|
||||||
@@ -107,6 +114,13 @@ export const IMGPROXY_URL = "https://imgproxy.coracle.social"
|
|||||||
|
|
||||||
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
|
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
|
||||||
|
|
||||||
|
export const THREAD_FILTER: Filter = {kinds: [THREAD, LEGACY_THREAD]}
|
||||||
|
|
||||||
|
export const COMMENT_FILTER: Filter = {
|
||||||
|
kinds: [COMMENT],
|
||||||
|
"#K": [String(THREAD), String(LEGACY_THREAD)],
|
||||||
|
}
|
||||||
|
|
||||||
export const NIP46_PERMS =
|
export const NIP46_PERMS =
|
||||||
"nip04_encrypt,nip04_decrypt,nip44_encrypt,nip44_decrypt," +
|
"nip04_encrypt,nip04_decrypt,nip44_encrypt,nip44_decrypt," +
|
||||||
[CLIENT_AUTH, AUTH_JOIN, MESSAGE, THREAD, COMMENT, GROUPS, WRAP, REACTION]
|
[CLIENT_AUTH, AUTH_JOIN, MESSAGE, THREAD, COMMENT, GROUPS, WRAP, REACTION]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
import {checkRelayConnection, checkRelayAuth, checkRelayAccess} from "@app/commands"
|
import {checkRelayConnection, checkRelayAuth, checkRelayAccess} from "@app/commands"
|
||||||
import {decodeRelay} from "@app/state"
|
import {decodeRelay} from "@app/state"
|
||||||
import {spacesNotifications} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
|
|
||||||
const url = decodeRelay($page.params.relay)
|
const url = decodeRelay($page.params.relay)
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
// We have to watch this one, since on mobile the badge will be visible when active
|
// We have to watch this one, since on mobile the badge will be visible when active
|
||||||
$: {
|
$: {
|
||||||
if ($spacesNotifications.includes($page.url.pathname)) {
|
if ($notifications.has($page.url.pathname)) {
|
||||||
setChecked($page.url.pathname)
|
setChecked($page.url.pathname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {sortBy, min, nthEq, sleep} from "@welshman/lib"
|
import {sortBy, min, nthEq, sleep} from "@welshman/lib"
|
||||||
import {getListTags, getPubkeyTagValues, THREAD, COMMENT} from "@welshman/util"
|
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||||
import {throttled} from "@welshman/store"
|
import {throttled} from "@welshman/store"
|
||||||
import {feedsFromFilters, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
import {feedsFromFilters, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
||||||
import {createFeedController, userMutes} from "@welshman/app"
|
import {createFeedController, userMutes} from "@welshman/app"
|
||||||
@@ -16,15 +16,14 @@
|
|||||||
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
||||||
import ThreadItem from "@app/components/ThreadItem.svelte"
|
import ThreadItem from "@app/components/ThreadItem.svelte"
|
||||||
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
||||||
import {LEGACY_THREAD, decodeRelay, deriveEventsForUrl} from "@app/state"
|
import {THREAD_FILTER, COMMENT_FILTER, decodeRelay, deriveEventsForUrl} from "@app/state"
|
||||||
import {THREAD_FILTERS, setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
const url = decodeRelay($page.params.relay)
|
const url = decodeRelay($page.params.relay)
|
||||||
const threads = deriveEventsForUrl(url, [{kinds: [THREAD, LEGACY_THREAD]}])
|
const feeds = feedsFromFilters([THREAD_FILTER, COMMENT_FILTER])
|
||||||
const comments = deriveEventsForUrl(url, [
|
const threads = deriveEventsForUrl(url, [THREAD_FILTER])
|
||||||
{kinds: [COMMENT], "#K": [String(THREAD), String(LEGACY_THREAD)]},
|
const comments = deriveEventsForUrl(url, [COMMENT_FILTER])
|
||||||
])
|
|
||||||
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
|
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
|
||||||
|
|
||||||
const events = throttled(
|
const events = throttled(
|
||||||
@@ -51,7 +50,7 @@
|
|||||||
|
|
||||||
const ctrl = createFeedController({
|
const ctrl = createFeedController({
|
||||||
useWindowing: true,
|
useWindowing: true,
|
||||||
feed: makeIntersectionFeed(makeRelayFeed(url), feedsFromFilters(THREAD_FILTERS)),
|
feed: makeIntersectionFeed(makeRelayFeed(url), feeds),
|
||||||
onExhausted: () => {
|
onExhausted: () => {
|
||||||
loading = false
|
loading = false
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user