Add group related alerts

This commit is contained in:
Jon Staab 2023-11-21 13:59:41 -08:00
parent 446f481738
commit 015cf38fd1
11 changed files with 161 additions and 46 deletions

View File

@ -0,0 +1,50 @@
<script lang="ts">
import {formatTimestamp} from 'src/util/misc'
import Card from "src/partials/Card.svelte"
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 {router} from "src/app/router"
export let address
export let alert
</script>
<Card interactive>
<Content>
<div class="flex justify-between">
<p class="text-2xl">
{#if alert.type === "exit"}
Access revoked
{:else if alert.type === "invite"}
Group invitation
{/if}
</p>
<small class="text-gray-3">
{formatTimestamp(alert.created_at)}
</small>
</div>
<p>
The admin of
<Anchor modal href={router.at('groups').of(address).at('notes').toString()}>
<Chip class="mx-1 relative top-px">
<GroupCircle {address} class="h-4 w-4" />
<GroupName {address} />
</Chip>
</Anchor>
has
{#if alert.type === "exit"}
removed you from the group.
{:else if alert.type === "invite"}
given you access to the group.
{/if}
</p>
{#if alert.content}
<p class="border-l-2 border-solid border-gray-5 pl-2">
"{alert.content}"
</p>
{/if}
</Content>
</Card>

View File

@ -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 @@
<p>
Resolving this request will
{#if request.kind === 25}
add <Chip><PersonBadgeSmall pubkey={request.pubkey} /></Chip> to the group.
add <Chip class="relative top-px mx-1"><PersonBadgeSmall pubkey={request.pubkey} /></Chip> to
{:else if request.kind === 26}
remove <Chip><PersonBadgeSmall pubkey={request.pubkey} /></Chip> from the group.
remove <Chip class="relative top-px mx-1"
><PersonBadgeSmall pubkey={request.pubkey} /></Chip> from
{/if}
{#if showGroup}
<Anchor modal href={router.at("groups").of(address).at("notes").toString()}>
<Chip class="relative top-px mx-1">
<GroupCircle {address} class="h-4 w-4" />
<GroupName {address} />
</Chip>
</Anchor>
{:else}
the group.
{/if}
</p>
<div class="flex gap-2 sm:hidden">

View File

@ -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"

View File

@ -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)

View File

@ -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 @@
</script>
<Content>
<Tabs {tabs} {activeTab} {setActiveTab} />
{#each tabNotifications as notification, i (notification.key)}
{@const lineText = getLineText(i)}
{#if lineText}
<div class="flex items-center gap-4">
<small class="whitespace-nowrap text-gray-1">{lineText}</small>
<div class="h-px w-full bg-gray-6" />
</div>
{/if}
{#if !notification.event}
<NotificationMention {notification} />
{:else if activeTab === tabs[0]}
<NotificationReplies {notification} />
<Tabs {tabs} {activeTab} {setActiveTab}>
<div slot="tab" let:tab class="flex gap-2">
<div>{tab}</div>
{#if tab === tabs[2] && uncheckedOtherNotifications.length > 0}
<div class="h-6 rounded-full bg-gray-6 px-2">
{uncheckedOtherNotifications.length}
</div>
{/if}
</div>
</Tabs>
{#if tabs.slice(0, 2).includes(activeTab)}
{#each tabNotifications as notification, i (notification.key)}
{@const lineText = getLineText(i)}
{#if lineText}
<div class="flex items-center gap-4">
<small class="whitespace-nowrap text-gray-1">{lineText}</small>
<div class="h-px w-full bg-gray-6" />
</div>
{/if}
{#if !notification.event}
<NotificationMention {notification} />
{:else if activeTab === tabs[0]}
<NotificationReplies {notification} />
{:else}
<NotificationReactions {notification} />
{/if}
{:else}
<NotificationReactions {notification} />
{/if}
<Content size="lg" class="text-center">No notifications found - check back later!</Content>
{/each}
{:else}
<Content size="lg" class="text-center">No notifications found - check back later!</Content>
{/each}
{#each $otherNotifications as notification, i (notification.id)}
{#if notification.t === "alert"}
<GroupAlert address={notification.group} alert={notification} />
{:else if notification.t === "request"}
<GroupRequest showGroup address={notification.group} request={notification} />
{/if}
{:else}
<Content size="lg" class="text-center">No notifications found - check back later!</Content>
{/each}
{/if}
</Content>

View File

@ -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, {

View File

@ -45,3 +45,8 @@ export type GroupRequest = Event & {
group: string
resolved: boolean
}
export type GroupAlert = Event & {
group: string
type: "exit" | "invite"
}

View File

@ -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,
})

View File

@ -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<Group>("address")
export const groupAdminKeys = collection<GroupKey>("pubkey")
export const groupSharedKeys = collection<GroupKey>("pubkey")
export const groupRequests = collection<GroupRequest>("id")
export const groupAlerts = collection<GroupAlert>("id")

View File

@ -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),
])

View File

@ -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)