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

View File

@@ -14,6 +14,8 @@ import {
AUTH_JOIN,
ROOMS,
COMMENT,
ALERT_REQUEST_PUSH,
ALERT_REQUEST_EMAIL,
isSignedEvent,
makeEvent,
displayProfile,
@@ -54,7 +56,6 @@ import {
PROTECTED,
userMembership,
INDEXER_RELAYS,
ALERT,
NOTIFIER_PUBKEY,
NOTIFIER_RELAY,
userRoomsByUrl,
@@ -368,7 +369,7 @@ export const makeComment = ({event, content, tags = []}: CommentParams) =>
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
publishThunk({event: makeComment(params), relays})
export type AlertParams = {
export type EmailAlertParams = {
feed: Feed
cron: string
email: string
@@ -376,7 +377,13 @@ export type AlertParams = {
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 = [
["feed", JSON.stringify(feed)],
["cron", cron],
@@ -384,7 +391,6 @@ export const makeAlert = async ({cron, email, feed, claims, description}: AlertP
["locale", LOCALE],
["timezone", TIMEZONE],
["description", description],
["channel", "email"],
[
"handler",
"31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1737058597050",
@@ -397,7 +403,7 @@ export const makeAlert = async ({cron, email, feed, claims, description}: AlertP
tags.push(["claim", relay, claim])
}
return makeEvent(ALERT, {
return makeEvent(ALERT_REQUEST_EMAIL, {
content: await signer.get().nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)),
tags: [
["d", randomId()],
@@ -406,5 +412,37 @@ export const makeAlert = async ({cron, email, feed, claims, description}: AlertP
})
}
export const publishAlert = async (params: AlertParams) =>
publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
export const publishEmailAlert = async (params: EmailAlertParams) =>
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]})

View File

@@ -13,22 +13,34 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {alerts, getMembershipUrls, getMembershipRoomsByUrl, userMembership} from "@app/state"
import {loadAlertStatuses, requestRelayClaims} from "@app/requests"
import {publishAlert} from "@app/commands"
import {publishEmailAlert, publishPushAlert} from "@app/commands"
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 minute = randomInt(0, 59)
const hour = (17 - timezoneOffset) % 24
const WEEKLY = `0 ${minute} ${hour} * * 1`
const DAILY = `0 ${minute} ${hour} * * *`
let loading = false
let cron = WEEKLY
let email = $alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || ""
let relay = ""
let notifyThreads = true
let notifyCalendar = true
let notifyChat = false
let loading = $state(false)
let cron = $state(WEEKLY)
let email = $state($alerts.map(a => getTagValue("email", a.tags)).filter(identity)[0] || "")
const back = () => history.back()
@@ -84,7 +96,10 @@
const cadence = cron?.endsWith("1") ? "Weekly" : "Daily"
const description = `${cadence} alert for ${displayList(display)} on ${displayRelayUrl(relay)}, sent via email.`
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 loadAlertStatuses($pubkey!)
@@ -105,25 +120,38 @@
</ModalHeader>
<FieldInline>
{#snippet label()}
<p>Email Address*</p>
<p>Alert Type*</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 bind:value={channel} class="select select-bordered">
<option value="push">Push Notification</option>
<option value="email">Email Digest</option>
</select>
{/snippet}
</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>
{#snippet label()}
<p>Space*</p>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import {onMount} from "svelte"
import {displayRelayUrl} from "@welshman/util"
import {deriveRelay} from "@welshman/app"
import {displayRelayUrl, getTagValue} from "@welshman/util"
import {pubkey, deriveRelay} from "@welshman/app"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
@@ -13,6 +13,8 @@
import SpaceExit from "@app/components/SpaceExit.svelte"
import SpaceJoin from "@app/components/SpaceJoin.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 MenuSpaceRoomItem from "@app/components/MenuSpaceRoomItem.svelte"
import InfoMissingRooms from "@app/components/InfoMissingRooms.svelte"
@@ -23,7 +25,9 @@
deriveUserRooms,
deriveOtherRooms,
hasNip29,
alerts,
} from "@app/state"
import {loadAlerts} from "@app/requests"
import {notifications} from "@app/notifications"
import {pushModal} from "@app/modal"
import {makeSpacePath} from "@app/routes"
@@ -36,6 +40,7 @@
const calendarPath = makeSpacePath(url, "calendar")
const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url)
const alert = $derived($alerts.find(a => getTagValue("feed", a.tags)?.includes(url)))
const openMenu = () => {
showMenu = true
@@ -62,6 +67,10 @@
const addRoom = () => pushModal(RoomCreate, {url}, {replaceState})
const addAlert = () => pushModal(AlertAdd, {relay: url, channel: "push"})
const deleteAlert = () => pushModal(AlertDelete, {alert})
let showMenu = $state(false)
let replaceState = $state(false)
let element: Element | undefined = $state()
@@ -72,6 +81,7 @@
onMount(() => {
replaceState = Boolean(element?.closest(".drawer"))
loadAlerts($pubkey!)
})
</script>
@@ -86,7 +96,7 @@
<Popover hideOnClick onClose={toggleMenu}>
<ul
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>
<Button onclick={showMembers}>
<Icon icon="user-rounded" />
@@ -99,6 +109,21 @@
Create Invite
</Button>
</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>
{#if $userRoomsByUrl.has(url)}
<Button onclick={leaveSpace} class="text-error">

View File

@@ -25,6 +25,9 @@ import {
EVENT_TIME,
AUTH_INVITE,
COMMENT,
ALERT_REQUEST_EMAIL,
ALERT_REQUEST_PUSH,
ALERT_STATUS,
matchFilters,
getTagValues,
getTagValue,
@@ -53,8 +56,6 @@ import {
import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util"
import {
ALERT,
ALERT_STATUS,
NOTIFIER_RELAY,
INDEXER_RELAYS,
getDefaultPubkeys,
@@ -348,7 +349,7 @@ export const makeCalendarFeed = ({
export const loadAlerts = (pubkey: string) =>
load({
relays: [NOTIFIER_RELAY],
filters: [{kinds: [ALERT], authors: [pubkey]}],
filters: [{kinds: [ALERT_REQUEST_EMAIL, ALERT_REQUEST_PUSH], authors: [pubkey]}],
})
export const loadAlertStatuses = (pubkey: string) =>

View File

@@ -41,6 +41,9 @@ import {
ROOM_JOIN,
ROOM_ADD_USER,
ROOM_REMOVE_USER,
ALERT_REQUEST_EMAIL,
ALERT_REQUEST_PUSH,
ALERT_STATUS,
getGroupTags,
getRelayTagValues,
getPubkeyTagValues,
@@ -84,10 +87,6 @@ export const ROOM = "h"
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_RELAY = import.meta.env.VITE_NOTIFIER_RELAY
@@ -344,7 +343,7 @@ export type Alert = {
}
export const alerts = deriveEventsMapped<Alert>(repository, {
filters: [{kinds: [ALERT]}],
filters: [{kinds: [ALERT_REQUEST_EMAIL, ALERT_REQUEST_PUSH]}],
itemToEvent: item => item.event,
eventToItem: async event => {
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))

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

View File

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