Space alerts dialog

This commit is contained in:
Jon Staab
2025-06-30 10:25:34 -07:00
parent b9048936ba
commit 6bdc8d4d9f
6 changed files with 68 additions and 80 deletions
+2 -9
View File
@@ -20,7 +20,6 @@ import {
ALERT_ANDROID, ALERT_ANDROID,
isSignedEvent, isSignedEvent,
makeEvent, makeEvent,
getAddress,
displayProfile, displayProfile,
normalizeRelayUrl, normalizeRelayUrl,
makeList, makeList,
@@ -62,7 +61,6 @@ import {
NOTIFIER_PUBKEY, NOTIFIER_PUBKEY,
NOTIFIER_RELAY, NOTIFIER_RELAY,
userRoomsByUrl, userRoomsByUrl,
deviceAlertAddresses,
} from "@app/state" } from "@app/state"
// Utils // Utils
@@ -445,10 +443,5 @@ export const makeAlert = async (params: AlertParams) => {
}) })
} }
export const publishAlert = async (params: AlertParams) => { export const publishAlert = async (params: AlertParams) =>
const event = await signer.get().sign(await makeAlert(params)) publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
deviceAlertAddresses.update($addresses => [...$addresses, getAddress(event)])
return publishThunk({event, relays: [NOTIFIER_RELAY]})
}
+16 -15
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {ucFirst} from "@lib/util"
import {decrypt} from "@welshman/signer" import {decrypt} from "@welshman/signer"
import {randomInt, parseJson, fromPairs, displayList, TIMEZONE, identity} from "@welshman/lib" import {randomInt, parseJson, fromPairs, displayList, TIMEZONE, identity} from "@welshman/lib"
import { import {
@@ -32,12 +31,12 @@
import {loadAlertStatuses, requestRelayClaim} from "@app/requests" import {loadAlertStatuses, requestRelayClaim} from "@app/requests"
import {publishAlert, attemptAuth} from "@app/commands" import {publishAlert, attemptAuth} from "@app/commands"
import type {AlertParams} from "@app/commands" import type {AlertParams} from "@app/commands"
import {platform, canSendPushNotifications, getPushInfo} from "@app/push" import {platform, platformName, canSendPushNotifications, getPushInfo} from "@app/push"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
type Props = { type Props = {
url?: string
channel?: string channel?: string
relay?: string
notifyChat?: boolean notifyChat?: boolean
notifyThreads?: boolean notifyThreads?: boolean
notifyCalendar?: boolean notifyCalendar?: boolean
@@ -45,7 +44,7 @@
} }
let { let {
relay = "", url = "",
channel = "email", channel = "email",
notifyChat = true, notifyChat = true,
notifyThreads = true, notifyThreads = true,
@@ -74,7 +73,7 @@
}) })
} }
if (!relay) { if (!url) {
return pushToast({ return pushToast({
theme: "error", theme: "error",
message: "Please select a space", message: "Please select a space",
@@ -111,9 +110,9 @@
loading = true loading = true
try { try {
const claims = claim ? {[relay]: claim} : {} const claims = claim ? {[url]: claim} : {}
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(relay)) const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(url))
const description = `for ${displayList(display)} on ${displayRelayUrl(relay)}` const description = `for ${displayList(display)} on ${displayRelayUrl(url)}`
const params: AlertParams = {feed, claims, description} const params: AlertParams = {feed, claims, description}
if (channel === "email") { if (channel === "email") {
@@ -133,7 +132,7 @@
try { try {
// @ts-ignore // @ts-ignore
params[platform] = await getPushInfo() params[platform] = await getPushInfo()
params.description = `${ucFirst(platform)} push notification ${description}.` params.description = `${platformName} push notification ${description}.`
} catch (e: any) { } catch (e: any) {
return pushToast({ return pushToast({
theme: "error", theme: "error",
@@ -181,11 +180,13 @@
channel = "email" channel = "email"
} }
requestRelayClaim(relay).then(code => { if (url) {
if (code) { requestRelayClaim(url).then(code => {
claim = code if (code) {
} claim = code
}) }
})
}
}) })
</script> </script>
@@ -237,7 +238,7 @@
<p>Space*</p> <p>Space*</p>
{/snippet} {/snippet}
{#snippet input()} {#snippet input()}
<select bind:value={relay} class="select select-bordered"> <select bind:value={url} class="select select-bordered">
<option value="" disabled selected>Choose a space URL</option> <option value="" disabled selected>Choose a space URL</option>
{#each getMembershipUrls($userMembership) as url (url)} {#each getMembershipUrls($userMembership) as url (url)}
<option value={url}>{displayRelayUrl(url)}</option> <option value={url}>{displayRelayUrl(url)}</option>
+15 -10
View File
@@ -1,20 +1,25 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {getTagValue} 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 AlertAdd from "@app/components/AlertAdd.svelte" import AlertAdd from "@app/components/AlertAdd.svelte"
import AlertItem from "@app/components/AlertItem.svelte" import AlertItem from "@app/components/AlertItem.svelte"
import {loadAlertStatuses, loadAlerts} from "@app/requests"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {alerts} from "@app/state" import {alerts} from "@app/state"
const startAlert = () => pushModal(AlertAdd) type Props = {
url?: string
channel?: string
hideSpaceField?: boolean
}
onMount(() => { const {url = "", channel = "push", hideSpaceField = false}: Props = $props()
loadAlertStatuses($pubkey!)
loadAlerts($pubkey!) const startAlert = () => pushModal(AlertAdd, {url, channel, hideSpaceField})
})
const filteredAlerts = $derived(
url ? $alerts.filter(a => getTagValue("feed", a.tags)?.includes(url)) : $alerts,
)
</script> </script>
<div class="card2 bg-alt flex flex-col gap-6 shadow-xl"> <div class="card2 bg-alt flex flex-col gap-6 shadow-xl">
@@ -29,10 +34,10 @@
</Button> </Button>
</div> </div>
<div class="col-4"> <div class="col-4">
{#each $alerts as alert (alert.event.id)} {#each filteredAlerts as alert (alert.event.id)}
<AlertItem {alert} /> <AlertItem {alert} />
{:else} {:else}
<p class="text-center opacity-75 py-12">No alerts found</p> <p class="text-center opacity-75 py-12">Nothing here yet!</p>
{/each} {/each}
</div> </div>
</div> </div>
+13 -22
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {displayRelayUrl, getTagValue, MESSAGE, THREAD, EVENT_TIME} from "@welshman/util" import {displayRelayUrl, getTagValue} from "@welshman/util"
import {pubkey, deriveRelay} from "@welshman/app" import {deriveRelay} from "@welshman/app"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
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"
@@ -14,6 +14,7 @@
import SpaceJoin from "@app/components/SpaceJoin.svelte" import SpaceJoin from "@app/components/SpaceJoin.svelte"
import ProfileList from "@app/components/ProfileList.svelte" import ProfileList from "@app/components/ProfileList.svelte"
import AlertAdd from "@app/components/AlertAdd.svelte" import AlertAdd from "@app/components/AlertAdd.svelte"
import Alerts from "@app/components/Alerts.svelte"
import RoomCreate from "@app/components/RoomCreate.svelte" import RoomCreate from "@app/components/RoomCreate.svelte"
import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte" import MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
import InfoMissingRooms from "@app/components/InfoMissingRooms.svelte" import InfoMissingRooms from "@app/components/InfoMissingRooms.svelte"
@@ -23,10 +24,9 @@
memberships, memberships,
deriveUserRooms, deriveUserRooms,
deriveOtherRooms, deriveOtherRooms,
deviceAlerts,
hasNip29, hasNip29,
alerts,
} from "@app/state" } from "@app/state"
import {loadAlerts} from "@app/requests"
import {notifications} from "@app/notifications" import {notifications} from "@app/notifications"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {makeSpacePath} from "@app/routes" import {makeSpacePath} from "@app/routes"
@@ -39,6 +39,7 @@
const calendarPath = makeSpacePath(url, "calendar") const calendarPath = makeSpacePath(url, "calendar")
const userRooms = deriveUserRooms(url) const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url) const otherRooms = deriveOtherRooms(url)
const hasAlerts = $derived($alerts.some(a => getTagValue("feed", a.tags)?.includes(url)))
const openMenu = () => { const openMenu = () => {
showMenu = true showMenu = true
@@ -65,21 +66,11 @@
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState}) const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
const addAlert = () => { const manageAlerts = () => {
const alert = $deviceAlerts.find(a => getTagValue("feed", a.tags)?.includes(url)) const component = hasAlerts ? Alerts : AlertAdd
const feed = getTagValue("feed", alert?.tags || []) const params = {url, channel: "push", hideSpaceField: true}
const props = { pushModal(component, params, {replaceState})
relay: url,
channel: "push",
notifyChat: feed ? feed.includes(String(MESSAGE)) : true,
notifyThreads: feed ? feed.includes(String(THREAD)) : true,
notifyCalendar: feed ? feed.includes(String(EVENT_TIME)) : true,
removeDuplicates: true,
hideSpaceField: true,
}
pushModal(AlertAdd, props, {replaceState})
} }
let showMenu = $state(false) let showMenu = $state(false)
@@ -92,11 +83,10 @@
onMount(() => { onMount(() => {
replaceState = Boolean(element?.closest(".drawer")) replaceState = Boolean(element?.closest(".drawer"))
loadAlerts($pubkey!)
}) })
</script> </script>
<div bind:this={element} class="flex h-screen flex-col justify-between"> <div bind:this={element} class="flex h-full flex-col justify-between">
<SecondaryNavSection> <SecondaryNavSection>
<div> <div>
<SecondaryNavItem class="w-full !justify-between" onclick={openMenu}> <SecondaryNavItem class="w-full !justify-between" onclick={openMenu}>
@@ -192,9 +182,10 @@
Where did my rooms go? Where did my rooms go?
</Button> </Button>
{/if} {/if}
</div></SecondaryNavSection> </div>
</SecondaryNavSection>
<div class="p-4"> <div class="p-4">
<button class="btn btn-neutral btn-sm w-full" onclick={addAlert}> <button class="btn btn-neutral btn-sm w-full" onclick={manageAlerts}>
<Icon icon="bell" /> <Icon icon="bell" />
Manage Alerts Manage Alerts
</button> </button>
+3
View File
@@ -5,10 +5,13 @@ import {PushNotifications} from "@capacitor/push-notifications"
import {parseJson, poll} from "@welshman/lib" import {parseJson, poll} from "@welshman/lib"
import {isSignedEvent} from "@welshman/util" import {isSignedEvent} from "@welshman/util"
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {ucFirst} from "@lib/util"
import {VAPID_PUBLIC_KEY} from "@app/state" import {VAPID_PUBLIC_KEY} from "@app/state"
export const platform = Capacitor.getPlatform() export const platform = Capacitor.getPlatform()
export const platformName = platform === "ios" ? "iOS" : ucFirst(platform)
export const initializePushNotifications = () => { export const initializePushNotifications = () => {
if (platform === "web") return if (platform === "web") return
+19 -24
View File
@@ -65,7 +65,6 @@ import {
getTag, getTag,
getTagValue, getTagValue,
getTagValues, getTagValues,
getAddress,
} from "@welshman/util" } from "@welshman/util"
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util" import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
import {Nip59, decrypt} from "@welshman/signer" import {Nip59, decrypt} from "@welshman/signer"
@@ -349,27 +348,21 @@ export const {
// Alerts // Alerts
export const deviceAlertAddresses = synced<string[]>("deviceAlertAddresses", [])
export type Alert = { export type Alert = {
event: TrustedEvent event: TrustedEvent
tags: string[][] tags: string[][]
} }
export const alerts = deriveEventsMapped<Alert>(repository, { export const alerts = withGetter(
filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}], deriveEventsMapped<Alert>(repository, {
itemToEvent: item => item.event, filters: [{kinds: [ALERT_EMAIL, ALERT_WEB, ALERT_IOS, ALERT_ANDROID]}],
eventToItem: async event => { itemToEvent: item => item.event,
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content)) eventToItem: async event => {
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
return {event, tags} return {event, tags}
}, },
}) }),
export const deviceAlerts = derived(
[deviceAlertAddresses, alerts],
([$deviceAlertAddresses, $alerts]) =>
$alerts.filter(a => $deviceAlertAddresses.includes(getAddress(a.event))),
) )
// Alert Statuses // Alert Statuses
@@ -379,15 +372,17 @@ export type AlertStatus = {
tags: string[][] tags: string[][]
} }
export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, { export const alertStatuses = withGetter(
filters: [{kinds: [ALERT_STATUS]}], deriveEventsMapped<AlertStatus>(repository, {
itemToEvent: item => item.event, filters: [{kinds: [ALERT_STATUS]}],
eventToItem: async event => { itemToEvent: item => item.event,
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content)) eventToItem: async event => {
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
return {event, tags} return {event, tags}
}, },
}) }),
)
export const deriveAlertStatus = (address: string) => export const deriveAlertStatus = (address: string) =>
derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address)) derived(alertStatuses, statuses => statuses.find(s => getTagValue("d", s.event.tags) === address))