Update alert form to include push notifications

This commit is contained in:
Jon Staab
2025-06-19 10:01:16 -07:00
parent 43da7d628e
commit 18a383edab
7 changed files with 138 additions and 41 deletions
+45 -7
View File
@@ -14,6 +14,8 @@ import {
AUTH_JOIN, AUTH_JOIN,
ROOMS, ROOMS,
COMMENT, COMMENT,
ALERT_REQUEST_PUSH,
ALERT_REQUEST_EMAIL,
isSignedEvent, isSignedEvent,
makeEvent, makeEvent,
displayProfile, displayProfile,
@@ -54,7 +56,6 @@ import {
PROTECTED, PROTECTED,
userMembership, userMembership,
INDEXER_RELAYS, INDEXER_RELAYS,
ALERT,
NOTIFIER_PUBKEY, NOTIFIER_PUBKEY,
NOTIFIER_RELAY, NOTIFIER_RELAY,
userRoomsByUrl, userRoomsByUrl,
@@ -368,7 +369,7 @@ export const makeComment = ({event, content, tags = []}: CommentParams) =>
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) => export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
publishThunk({event: makeComment(params), relays}) publishThunk({event: makeComment(params), relays})
export type AlertParams = { export type EmailAlertParams = {
feed: Feed feed: Feed
cron: string cron: string
email: string email: string
@@ -376,7 +377,13 @@ export type AlertParams = {
claims: Record<string, string> claims: Record<string, string>
} }
export const makeAlert = async ({cron, email, feed, claims, description}: AlertParams) => { export const makeEmailAlert = async ({
cron,
email,
feed,
claims,
description,
}: EmailAlertParams) => {
const tags = [ const tags = [
["feed", JSON.stringify(feed)], ["feed", JSON.stringify(feed)],
["cron", cron], ["cron", cron],
@@ -384,7 +391,6 @@ export const makeAlert = async ({cron, email, feed, claims, description}: AlertP
["locale", LOCALE], ["locale", LOCALE],
["timezone", TIMEZONE], ["timezone", TIMEZONE],
["description", description], ["description", description],
["channel", "email"],
[ [
"handler", "handler",
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050", "31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
@@ -397,7 +403,7 @@ export const makeAlert = async ({cron, email, feed, claims, description}: AlertP
tags.push(["claim", relay, claim]) tags.push(["claim", relay, claim])
} }
return makeEvent(ALERT, { return makeEvent(ALERT_REQUEST_EMAIL, {
content: await signer.get().nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)), content: await signer.get().nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)),
tags: [ tags: [
["d", randomId()], ["d", randomId()],
@@ -406,5 +412,37 @@ export const makeAlert = async ({cron, email, feed, claims, description}: AlertP
}) })
} }
export const publishAlert = async (params: AlertParams) => export const publishEmailAlert = async (params: EmailAlertParams) =>
publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]}) publishThunk({event: await makeEmailAlert(params), relays: [NOTIFIER_RELAY]})
export type PushAlertParams = {
feed: Feed
description: string
claims: Record<string, string>
}
export const makePushAlert = async ({feed, claims, description}: PushAlertParams) => {
const tags = [
["feed", JSON.stringify(feed)],
["locale", LOCALE],
["timezone", TIMEZONE],
["description", description],
["token", ""],
["platform", ""],
]
for (const [relay, claim] of Object.entries(claims)) {
tags.push(["claim", relay, claim])
}
return makeEvent(ALERT_REQUEST_PUSH, {
content: await signer.get().nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)),
tags: [
["d", randomId()],
["p", NOTIFIER_PUBKEY],
],
})
}
export const publishPushAlert = async (params: PushAlertParams) =>
publishThunk({event: await makePushAlert(params), relays: [NOTIFIER_RELAY]})
+51 -23
View File
@@ -13,22 +13,34 @@
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import {alerts, getMembershipUrls, getMembershipRoomsByUrl, userMembership} from "@app/state" import {alerts, getMembershipUrls, getMembershipRoomsByUrl, userMembership} from "@app/state"
import {loadAlertStatuses, requestRelayClaims} from "@app/requests" import {loadAlertStatuses, requestRelayClaims} from "@app/requests"
import {publishAlert} from "@app/commands" import {publishEmailAlert, publishPushAlert} from "@app/commands"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
type Props = {
channel?: string
relay?: string
notifyChat?: boolean
notifyThreads?: boolean
notifyCalendar?: boolean
}
let {
relay = "",
channel = "email",
notifyChat = true,
notifyThreads = true,
notifyCalendar = true,
}: Props = $props()
const timezoneOffset = parseInt(TIMEZONE.slice(3)) / 100 const timezoneOffset = parseInt(TIMEZONE.slice(3)) / 100
const minute = randomInt(0, 59) const minute = randomInt(0, 59)
const hour = (17 - timezoneOffset) % 24 const hour = (17 - timezoneOffset) % 24
const WEEKLY = `0 ${minute} ${hour} * * 1` const WEEKLY = `0 ${minute} ${hour} * * 1`
const DAILY = `0 ${minute} ${hour} * * *` const DAILY = `0 ${minute} ${hour} * * *`
let loading = false let loading = $state(false)
let cron = WEEKLY let cron = $state(WEEKLY)
let email = $alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || "" let email = $state($alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || "")
let relay = ""
let notifyThreads = true
let notifyCalendar = true
let notifyChat = false
const back = () => history.back() const back = () => history.back()
@@ -84,7 +96,10 @@
const cadence = cron?.endsWith("1") ? "Weekly" : "Daily" const cadence = cron?.endsWith("1") ? "Weekly" : "Daily"
const description = `${cadence} alert for ${displayList(display)} on ${displayRelayUrl(relay)}, sent via email.` const description = `${cadence} alert for ${displayList(display)} on ${displayRelayUrl(relay)}, sent via email.`
const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(relay)) const feed = makeIntersectionFeed(feedFromFilters(filters), makeRelayFeed(relay))
const thunk = await publishAlert({cron, email, feed, claims, description}) const thunk =
channel === "email"
? await publishEmailAlert({cron, email, feed, claims, description})
: await publishPushAlert({feed, claims, description})
await thunk.result await thunk.result
await loadAlertStatuses($pubkey!) await loadAlertStatuses($pubkey!)
@@ -105,25 +120,38 @@
</ModalHeader> </ModalHeader>
<FieldInline> <FieldInline>
{#snippet label()} {#snippet label()}
<p>Email Address*</p> <p>Alert Type*</p>
{/snippet} {/snippet}
{#snippet input()} {#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2"> <select bind:value={channel} class="select select-bordered">
<input placeholder="email@example.com" bind:value={email} /> <option value="push">Push Notification</option>
</label> <option value="email">Email Digest</option>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Frequency*</p>
{/snippet}
{#snippet input()}
<select bind:value={cron} class="select select-bordered">
<option value={WEEKLY}>Weekly</option>
<option value={DAILY}>Daily</option>
</select> </select>
{/snippet} {/snippet}
</FieldInline> </FieldInline>
{#if channel === "email"}
<FieldInline>
{#snippet label()}
<p>Email Address*</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<input placeholder="email@example.com" bind:value={email} />
</label>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Frequency*</p>
{/snippet}
{#snippet input()}
<select bind:value={cron} class="select select-bordered">
<option value={WEEKLY}>Weekly</option>
<option value={DAILY}>Daily</option>
</select>
{/snippet}
</FieldInline>
{/if}
<FieldInline> <FieldInline>
{#snippet label()} {#snippet label()}
<p>Space*</p> <p>Space*</p>
+28 -3
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl, getTagValue} from "@welshman/util"
import {deriveRelay} from "@welshman/app" import {pubkey, 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"
@@ -13,6 +13,8 @@
import SpaceExit from "@app/components/SpaceExit.svelte" import SpaceExit from "@app/components/SpaceExit.svelte"
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 AlertDelete from "@app/components/AlertDelete.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,7 +25,9 @@
deriveUserRooms, deriveUserRooms,
deriveOtherRooms, deriveOtherRooms,
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"
@@ -36,6 +40,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 alert = $derived($alerts.find(a => getTagValue("feed", a.tags)?.includes(url)))
const openMenu = () => { const openMenu = () => {
showMenu = true showMenu = true
@@ -62,6 +67,10 @@
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState}) const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
const addAlert = () => pushModal(AlertAdd, {relay: url, channel: "push"})
const deleteAlert = () => pushModal(AlertDelete, {alert})
let showMenu = $state(false) let showMenu = $state(false)
let replaceState = $state(false) let replaceState = $state(false)
let element: Element | undefined = $state() let element: Element | undefined = $state()
@@ -72,6 +81,7 @@
onMount(() => { onMount(() => {
replaceState = Boolean(element?.closest(".drawer")) replaceState = Boolean(element?.closest(".drawer"))
loadAlerts($pubkey!)
}) })
</script> </script>
@@ -86,7 +96,7 @@
<Popover hideOnClick onClose={toggleMenu}> <Popover hideOnClick onClose={toggleMenu}>
<ul <ul
transition:fly transition:fly
class="menu absolute z-popover mt-2 w-full rounded-box bg-base-100 p-2 shadow-xl"> class="menu absolute z-popover mt-2 w-full gap-1 rounded-box bg-base-100 p-2 shadow-xl">
<li> <li>
<Button onclick={showMembers}> <Button onclick={showMembers}>
<Icon icon="user-rounded" /> <Icon icon="user-rounded" />
@@ -99,6 +109,21 @@
Create Invite Create Invite
</Button> </Button>
</li> </li>
{#if alert}
<li>
<Button onclick={deleteAlert}>
<Icon icon="bell" />
Disable alerts
</Button>
</li>
{:else}
<li>
<Button onclick={addAlert}>
<Icon icon="bell" />
Enable alerts
</Button>
</li>
{/if}
<li> <li>
{#if $userRoomsByUrl.has(url)} {#if $userRoomsByUrl.has(url)}
<Button onclick={leaveSpace} class="text-error"> <Button onclick={leaveSpace} class="text-error">
+4 -3
View File
@@ -25,6 +25,9 @@ import {
EVENT_TIME, EVENT_TIME,
AUTH_INVITE, AUTH_INVITE,
COMMENT, COMMENT,
ALERT_REQUEST_EMAIL,
ALERT_REQUEST_PUSH,
ALERT_STATUS,
matchFilters, matchFilters,
getTagValues, getTagValues,
getTagValue, getTagValue,
@@ -53,8 +56,6 @@ import {
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
import { import {
ALERT,
ALERT_STATUS,
NOTIFIER_RELAY, NOTIFIER_RELAY,
INDEXER_RELAYS, INDEXER_RELAYS,
getDefaultPubkeys, getDefaultPubkeys,
@@ -348,7 +349,7 @@ export const makeCalendarFeed = ({
export const loadAlerts = (pubkey: string) => export const loadAlerts = (pubkey: string) =>
load({ load({
relays: [NOTIFIER_RELAY], relays: [NOTIFIER_RELAY],
filters: [{kinds: [ALERT], authors: [pubkey]}], filters: [{kinds: [ALERT_REQUEST_EMAIL, ALERT_REQUEST_PUSH], authors: [pubkey]}],
}) })
export const loadAlertStatuses = (pubkey: string) => export const loadAlertStatuses = (pubkey: string) =>
+4 -5
View File
@@ -41,6 +41,9 @@ import {
ROOM_JOIN, ROOM_JOIN,
ROOM_ADD_USER, ROOM_ADD_USER,
ROOM_REMOVE_USER, ROOM_REMOVE_USER,
ALERT_REQUEST_EMAIL,
ALERT_REQUEST_PUSH,
ALERT_STATUS,
getGroupTags, getGroupTags,
getRelayTagValues, getRelayTagValues,
getPubkeyTagValues, getPubkeyTagValues,
@@ -84,10 +87,6 @@ export const ROOM = "h"
export const PROTECTED = ["-"] export const PROTECTED = ["-"]
export const ALERT = 32830
export const ALERT_STATUS = 32831
export const NOTIFIER_PUBKEY = import.meta.env.VITE_NOTIFIER_PUBKEY export const NOTIFIER_PUBKEY = import.meta.env.VITE_NOTIFIER_PUBKEY
export const NOTIFIER_RELAY = import.meta.env.VITE_NOTIFIER_RELAY export const NOTIFIER_RELAY = import.meta.env.VITE_NOTIFIER_RELAY
@@ -344,7 +343,7 @@ export type Alert = {
} }
export const alerts = deriveEventsMapped<Alert>(repository, { export const alerts = deriveEventsMapped<Alert>(repository, {
filters: [{kinds: [ALERT]}], filters: [{kinds: [ALERT_REQUEST_EMAIL, ALERT_REQUEST_PUSH]}],
itemToEvent: item => item.event, itemToEvent: item => item.event,
eventToItem: async event => { eventToItem: async event => {
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content)) const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.7491 9.70957V9.00497C18.7491 5.13623 15.7274 2 12 2C8.27256 2 5.25087 5.13623 5.25087 9.00497V9.70957C5.25087 10.5552 5.00972 11.3818 4.5578 12.0854L3.45036 13.8095C2.43882 15.3843 3.21105 17.5249 4.97036 18.0229C9.57274 19.3257 14.4273 19.3257 19.0296 18.0229C20.789 17.5249 21.5612 15.3843 20.5496 13.8095L19.4422 12.0854C18.9903 11.3818 18.7491 10.5552 18.7491 9.70957Z" stroke="#1C274C" stroke-width="1.5"/>
<path d="M7.5 19C8.15503 20.7478 9.92246 22 12 22C14.0775 22 15.845 20.7478 16.5 19" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 674 B

+2
View File
@@ -9,6 +9,7 @@
import {switcher} from "@welshman/lib" import {switcher} from "@welshman/lib"
import AddSquare from "@assets/icons/Add Square.svg?dataurl" import AddSquare from "@assets/icons/Add Square.svg?dataurl"
import ArrowsALogout2 from "@assets/icons/Arrows ALogout 2.svg?dataurl" import ArrowsALogout2 from "@assets/icons/Arrows ALogout 2.svg?dataurl"
import Bell from "@assets/icons/Bell.svg?dataurl"
import Bookmark from "@assets/icons/Bookmark.svg?dataurl" import Bookmark from "@assets/icons/Bookmark.svg?dataurl"
import BillList from "@assets/icons/Bill List.svg?dataurl" import BillList from "@assets/icons/Bill List.svg?dataurl"
import Code2 from "@assets/icons/Code 2.svg?dataurl" import Code2 from "@assets/icons/Code 2.svg?dataurl"
@@ -108,6 +109,7 @@
const data = switcher(icon, { const data = switcher(icon, {
"add-square": AddSquare, "add-square": AddSquare,
"arrows-a-logout-2": ArrowsALogout2, "arrows-a-logout-2": ArrowsALogout2,
bell: Bell,
bookmark: Bookmark, bookmark: Bookmark,
"bill-list": BillList, "bill-list": BillList,
"code-2": Code2, "code-2": Code2,