From 015cf38fd1450a98ca7a687ad5284fc6dd17c9ef Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Tue, 21 Nov 2023 13:59:41 -0800 Subject: [PATCH] Add group related alerts --- src/app/shared/GroupAlert.svelte | 50 +++++++++++++++++++ src/app/shared/GroupRequest.svelte | 18 ++++++- src/app/shared/NoteActions.svelte | 8 +-- src/app/views/GroupRotate.svelte | 4 +- src/app/views/Notifications.svelte | 76 ++++++++++++++++++++++------- src/engine/groups/commands.ts | 12 +---- src/engine/groups/model.ts | 5 ++ src/engine/groups/projections.ts | 8 ++- src/engine/groups/state.ts | 3 +- src/engine/index.ts | 7 +-- src/engine/notifications/derived.ts | 16 +++++- 11 files changed, 161 insertions(+), 46 deletions(-) create mode 100644 src/app/shared/GroupAlert.svelte diff --git a/src/app/shared/GroupAlert.svelte b/src/app/shared/GroupAlert.svelte new file mode 100644 index 00000000..7815bebd --- /dev/null +++ b/src/app/shared/GroupAlert.svelte @@ -0,0 +1,50 @@ + + + + +
+

+ {#if alert.type === "exit"} + Access revoked + {:else if alert.type === "invite"} + Group invitation + {/if} +

+ + {formatTimestamp(alert.created_at)} + +
+

+ The admin of + + + + + + + has + {#if alert.type === "exit"} + removed you from the group. + {:else if alert.type === "invite"} + given you access to the group. + {/if} +

+ {#if alert.content} +

+ "{alert.content}" +

+ {/if} +
+
diff --git a/src/app/shared/GroupRequest.svelte b/src/app/shared/GroupRequest.svelte index f4ca4d9d..a6a959bd 100644 --- a/src/app/shared/GroupRequest.svelte +++ b/src/app/shared/GroupRequest.svelte @@ -3,12 +3,15 @@ import Chip from "src/partials/Chip.svelte" import Anchor from "src/partials/Anchor.svelte" import Content from "src/partials/Content.svelte" + import GroupCircle from "src/app/shared/GroupCircle.svelte" + import GroupName from "src/app/shared/GroupName.svelte" import PersonBadgeSmall from "src/app/shared/PersonBadgeSmall.svelte" import {groupRequests} from "src/engine" import {router} from "src/app/router" export let address export let request + export let showGroup = false const dismiss = () => groupRequests.key(request.id).merge({resolved: true}) @@ -54,9 +57,20 @@

Resolving this request will {#if request.kind === 25} - add to the group. + add to {:else if request.kind === 26} - remove from the group. + remove from + {/if} + {#if showGroup} + + + + + + + {:else} + the group. {/if}

diff --git a/src/app/shared/NoteActions.svelte b/src/app/shared/NoteActions.svelte index d6bcf27b..974d1a0d 100644 --- a/src/app/shared/NoteActions.svelte +++ b/src/app/shared/NoteActions.svelte @@ -5,13 +5,7 @@ import {tweened} from "svelte/motion" import {identity, sum, uniqBy, prop, pluck, sortBy} from "ramda" import {formatSats} from "src/util/misc" - import { - LOCAL_RELAY_URL, - getGroupAddress, - getIdOrAddressTag, - asNostrEvent, - getIdOrAddress, - } from "src/util/nostr" + import {LOCAL_RELAY_URL, getGroupAddress, getIdOrAddressTag, asNostrEvent} from "src/util/nostr" import {quantify} from "hurdak" import {toast} from "src/partials/state" import Popover from "src/partials/Popover.svelte" diff --git a/src/app/views/GroupRotate.svelte b/src/app/views/GroupRotate.svelte index 2a321d3d..2ca8eac9 100644 --- a/src/app/views/GroupRotate.svelte +++ b/src/app/views/GroupRotate.svelte @@ -60,10 +60,10 @@ }) // Send new invites - publishGroupInvites(address, newMembers, gracePeriod) + publishGroupInvites(address, newMembers, $group.relays, gracePeriod) // Send evictions - publishGroupEvictions(address, removedMembers, gracePeriod) + publishGroupEvictions(address, removedMembers) // Re-publish group info publishGroupMeta(address, $group) diff --git a/src/app/views/Notifications.svelte b/src/app/views/Notifications.svelte index aba0b07b..0281d867 100644 --- a/src/app/views/Notifications.svelte +++ b/src/app/views/Notifications.svelte @@ -6,15 +6,32 @@ import {noteKinds, reactionKinds} from "src/util/nostr" import Tabs from "src/partials/Tabs.svelte" import Content from "src/partials/Content.svelte" + import GroupAlert from "src/app/shared/GroupAlert.svelte" + import GroupRequest from "src/app/shared/GroupRequest.svelte" import NotificationReactions from "src/app/views/NotificationReactions.svelte" import NotificationMention from "src/app/views/NotificationMention.svelte" import NotificationReplies from "src/app/views/NotificationReplies.svelte" import {router} from "src/app/router" import type {Event} from "src/engine" - import {pubkey, sessions, notifications, groupNotifications, loadNotifications} from "src/engine" + import { + env, + pubkey, + session, + sessions, + notifications, + otherNotifications, + groupNotifications, + loadNotifications, + } from "src/engine" const tabs = ["Mentions & Replies", "Reactions"] + if ($env.ENABLE_GROUPS) { + tabs.push("Other") + } + + const lastSynced = $session?.notifications_last_synced || 0 + const throttledNotifications = notifications.throttle(300) const setActiveTab = tab => router.at("notifications").at(tab).push() @@ -45,6 +62,8 @@ find((e: Event) => reactionKinds.includes(e.kind), n.interactions) ) + $: uncheckedOtherNotifications = $otherNotifications.filter(n => n.created_at > lastSynced) + document.title = "Notifications" onMount(() => { @@ -66,23 +85,44 @@ - - {#each tabNotifications as notification, i (notification.key)} - {@const lineText = getLineText(i)} - {#if lineText} -
- {lineText} -
-
- {/if} - {#if !notification.event} - - {:else if activeTab === tabs[0]} - + +
+
{tab}
+ {#if tab === tabs[2] && uncheckedOtherNotifications.length > 0} +
+ {uncheckedOtherNotifications.length} +
+ {/if} +
+
+ {#if tabs.slice(0, 2).includes(activeTab)} + {#each tabNotifications as notification, i (notification.key)} + {@const lineText = getLineText(i)} + {#if lineText} +
+ {lineText} +
+
+ {/if} + {#if !notification.event} + + {:else if activeTab === tabs[0]} + + {:else} + + {/if} {:else} - - {/if} + No notifications found - check back later! + {/each} {:else} - No notifications found - check back later! - {/each} + {#each $otherNotifications as notification, i (notification.id)} + {#if notification.t === "alert"} + + {:else if notification.t === "request"} + + {/if} + {:else} + No notifications found - check back later! + {/each} + {/if} diff --git a/src/engine/groups/commands.ts b/src/engine/groups/commands.ts index 4286a168..b1203729 100644 --- a/src/engine/groups/commands.ts +++ b/src/engine/groups/commands.ts @@ -257,16 +257,8 @@ export const publishGroupInvites = async (address, pubkeys, relays, gracePeriod return publishKeyRotations(address, pubkeys, template) } -export const publishGroupEvictions = async (address, pubkeys, gracePeriod) => { - const template = createEvent(24, { - tags: [ - ["a", address], - ["grace_period", String(gracePeriod)], - ], - }) - - publishKeyRotations(address, pubkeys, template) -} +export const publishGroupEvictions = async (address, pubkeys) => + publishKeyRotations(address, pubkeys, createEvent(24, {tags: [["a", address]]})) export const publishGroupMeta = async (address, meta) => { const template = createEvent(34550, { diff --git a/src/engine/groups/model.ts b/src/engine/groups/model.ts index 5d8cf370..bce94708 100644 --- a/src/engine/groups/model.ts +++ b/src/engine/groups/model.ts @@ -45,3 +45,8 @@ export type GroupRequest = Event & { group: string resolved: boolean } + +export type GroupAlert = Event & { + group: string + type: "exit" | "invite" +} diff --git a/src/engine/groups/projections.ts b/src/engine/groups/projections.ts index 0ef6fcdf..7febae08 100644 --- a/src/engine/groups/projections.ts +++ b/src/engine/groups/projections.ts @@ -10,7 +10,7 @@ import {_events} from "src/engine/events/state" import {sessions} from "src/engine/session/state" import {nip59} from "src/engine/session/derived" import {GroupAccess, MemberAccess} from "./model" -import {groups, groupSharedKeys, groupRequests} from "./state" +import {groups, groupSharedKeys, groupRequests, groupAlerts} from "./state" import {deriveAdminKeyForGroup, getRecipientKey} from "./utils" import {modifyGroupStatus, setGroupStatus} from "./commands" @@ -40,6 +40,12 @@ projections.addHandler(24, (e: Event) => { })) } + groupAlerts.key(e.id).set({ + ...e, + group: address, + type: privkey ? "invite" : "exit", + }) + setGroupStatus(recipient, address, e.created_at, { access: privkey ? MemberAccess.Granted : MemberAccess.Revoked, }) diff --git a/src/engine/groups/state.ts b/src/engine/groups/state.ts index 9ef9b4d1..df5c22db 100644 --- a/src/engine/groups/state.ts +++ b/src/engine/groups/state.ts @@ -1,7 +1,8 @@ import {collection} from "src/engine/core/utils" -import type {Group, GroupKey, GroupRequest} from "./model" +import type {Group, GroupKey, GroupRequest, GroupAlert} from "./model" export const groups = collection("address") export const groupAdminKeys = collection("pubkey") export const groupSharedKeys = collection("pubkey") export const groupRequests = collection("id") +export const groupAlerts = collection("id") diff --git a/src/engine/index.ts b/src/engine/index.ts index cbbd64ce..e44a3c5c 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -3,7 +3,7 @@ import {Storage, LocalStorageAdapter, IndexedDBAdapter, sortByPubkeyWhitelist} f import {_lists} from "./lists" import {people} from "./people" import {relays} from "./relays" -import {groups, groupSharedKeys, groupAdminKeys, groupRequests} from "./groups" +import {groups, groupSharedKeys, groupAdminKeys, groupRequests, groupAlerts} from "./groups" import {_labels} from "./labels" import {topics} from "./topics" import {deletes, _events, deletesLastUpdated} from "./events" @@ -27,7 +27,7 @@ export * from "./session" export * from "./topics" export * from "./zaps" -export const storage = new Storage(6, [ +export const storage = new Storage(8, [ new LocalStorageAdapter("pubkey", pubkey), new LocalStorageAdapter("sessions", sessions), new LocalStorageAdapter("deletes2", deletes, { @@ -43,7 +43,8 @@ export const storage = new Storage(6, [ new IndexedDBAdapter("relays", relays, 1000, sortBy(prop("count"))), new IndexedDBAdapter("channels", channels, 1000, sortBy(prop("last_checked"))), new IndexedDBAdapter("groups", groups, 1000, sortBy(prop("count"))), - new IndexedDBAdapter("groupRequests", groupRequests, 1000, sortBy(prop("created_at"))), + new IndexedDBAdapter("groupAlerts", groupAlerts, 30, sortBy(prop("created_at"))), + new IndexedDBAdapter("groupRequests", groupRequests, 100, sortBy(prop("created_at"))), new IndexedDBAdapter("groupSharedKeys", groupSharedKeys, 1000, sortBy(prop("created_at"))), new IndexedDBAdapter("groupAdminKeys", groupAdminKeys, 1000), ]) diff --git a/src/engine/notifications/derived.ts b/src/engine/notifications/derived.ts index 597c5927..3188dc01 100644 --- a/src/engine/notifications/derived.ts +++ b/src/engine/notifications/derived.ts @@ -5,6 +5,7 @@ import {reactionKinds} from "src/util/nostr" import {tryJson} from "src/util/misc" import {events, isEventMuted} from "src/engine/events/derived" import {derived} from "src/engine/core/utils" +import {groupRequests, groupAlerts} from "src/engine/groups/state" import {session} from "src/engine/session/derived" import {userEvents} from "src/engine/events/derived" @@ -33,11 +34,22 @@ export const notifications = derived( } ) +export const otherNotifications = derived([groupRequests, groupAlerts], ([$requests, $alerts]) => + sortBy( + n => -n.created_at, + [ + ...$requests.filter(r => !r.resolved).map(request => ({t: "request", ...request})), + ...$alerts.map(alert => ({t: "alert", ...alert})), + ] + ) +) + export const hasNewNotifications = derived( - [session, notifications], - ([$session, $notifications]) => { + [session, notifications, otherNotifications], + ([$session, $notifications, $otherNotifications]) => { const maxCreatedAt = $notifications .filter(e => !reactionKinds.includes(e.kind)) + .concat($otherNotifications) .map(prop("created_at")) .reduce(max, 0)