mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 16:31:04 +00:00
Add group related alerts
This commit is contained in:
parent
446f481738
commit
015cf38fd1
50
src/app/shared/GroupAlert.svelte
Normal file
50
src/app/shared/GroupAlert.svelte
Normal 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>
|
@ -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">
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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, {
|
||||
|
@ -45,3 +45,8 @@ export type GroupRequest = Event & {
|
||||
group: string
|
||||
resolved: boolean
|
||||
}
|
||||
|
||||
export type GroupAlert = Event & {
|
||||
group: string
|
||||
type: "exit" | "invite"
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
|
@ -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")
|
||||
|
@ -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),
|
||||
])
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user