mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-10 02:47:06 +00:00
Update alert form to include push notifications
This commit is contained in:
@@ -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]})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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))
|
||||
|
||||
4
src/assets/icons/Bell.svg
Normal file
4
src/assets/icons/Bell.svg
Normal 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 |
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user