diff --git a/.ackrc b/.ackrc index 98e5ab2..fca27f6 100644 --- a/.ackrc +++ b/.ackrc @@ -3,4 +3,6 @@ --ignore-dir=build --ignore-dir=ios/DerivedData --ignore-dir=ios/App/App/public +--ignore-file=match:.svg +--ignore-file=match:package-lock.json diff --git a/src/app/commands.ts b/src/app/commands.ts index 344fe4f..7350952 100644 --- a/src/app/commands.ts +++ b/src/app/commands.ts @@ -1,6 +1,6 @@ import * as nip19 from "nostr-tools/nip19" import {get} from "svelte/store" -import {ctx, uniq, equals} from "@welshman/lib" +import {ctx, randomId, uniq, equals} from "@welshman/lib" import { DELETE, REPORT, @@ -28,8 +28,9 @@ import { getRelayTags, getRelayTagValues, toNostrURI, + unionFilters, } from "@welshman/util" -import type {TrustedEvent, EventContent, EventTemplate} from "@welshman/util" +import type {TrustedEvent, Filter, EventContent, EventTemplate} from "@welshman/util" import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net" import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer" import { @@ -61,6 +62,9 @@ import { userMembership, INDEXER_RELAYS, NIP46_PERMS, + ALERT, + NOTIFIER_PUBKEY, + NOTIFIER_RELAY, userRoomsByUrl, } from "@app/state" import {loadUserData} from "@app/requests" @@ -455,3 +459,35 @@ export const makeComment = ({event, content, tags = []}: CommentParams) => export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) => publishThunk({event: makeComment(params), relays}) + +export type AlertParams = { + cron: string + email: string + relay: string + handler: string + filters: Filter[] +} + +export const makeAlert = async ({cron, email, handler, relay, filters}: AlertParams) => + createEvent(ALERT, { + content: await signer + .get() + .nip44.encrypt( + NOTIFIER_PUBKEY, + JSON.stringify([ + ["cron", cron], + ["email", email], + ["relay", relay], + ["handler", handler], + ["channel", "email"], + ...unionFilters(filters).map(filter => ["filter", JSON.stringify(filter)]), + ]), + ), + tags: [ + ["d", randomId()], + ["p", NOTIFIER_PUBKEY], + ], + }) + +export const publishAlert = async (params: AlertParams) => + publishThunk({event: await makeAlert(params), relays: [NOTIFIER_RELAY]}) diff --git a/src/app/components/AlertAdd.svelte b/src/app/components/AlertAdd.svelte new file mode 100644 index 0000000..13f46e1 --- /dev/null +++ b/src/app/components/AlertAdd.svelte @@ -0,0 +1,164 @@ + + +
+ + {#snippet title()} + Add an Alert + {/snippet} + + + {#snippet label()} +

Email Address*

+ {/snippet} + {#snippet input()} + + {/snippet} +
+ + {#snippet label()} +

Frequency*

+ {/snippet} + {#snippet input()} + + {/snippet} +
+ + {#snippet label()} +

Space*

+ {/snippet} + {#snippet input()} + + {/snippet} +
+ + {#snippet label()} +

Notifications*

+ {/snippet} + {#snippet input()} +
+ + + Threads + + + + Calendar + + + + Chat + +
+ {/snippet} +
+ + + + +
diff --git a/src/app/components/AlertDelete.svelte b/src/app/components/AlertDelete.svelte new file mode 100644 index 0000000..7cd8aaa --- /dev/null +++ b/src/app/components/AlertDelete.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/app/components/AlertItem.svelte b/src/app/components/AlertItem.svelte new file mode 100644 index 0000000..30301f5 --- /dev/null +++ b/src/app/components/AlertItem.svelte @@ -0,0 +1,86 @@ + + +
+ +
+ {cron?.endsWith("1") ? "Weekly" : "Daily"} alert for + {displayList(types)} on + + {displayRelayUrl(relay)} + , sent via {channel}. +
+ {#if status} + {@const statusText = getTagValue("status", status.tags) || "error"} + {#if statusText === "ok"} + + Active + + {:else if statusText === "pending"} + + Pending + + {:else} + + {statusText.replace("-", " ").replace(/^(.)/, x => x.toUpperCase())} + + {/if} + {:else} + + Inactive + + {/if} +
diff --git a/src/app/components/Alerts.svelte b/src/app/components/Alerts.svelte new file mode 100644 index 0000000..6bc2c91 --- /dev/null +++ b/src/app/components/Alerts.svelte @@ -0,0 +1,27 @@ + + +
+
+ Alerts + +
+
+ {#each $alerts as alert (alert.event.id)} + + {:else} +

No alerts found

+ {/each} +
+
diff --git a/src/app/requests.ts b/src/app/requests.ts index 31fb7fa..c2b3074 100644 --- a/src/app/requests.ts +++ b/src/app/requests.ts @@ -47,6 +47,9 @@ import { import {createScroller} from "@lib/html" import {daysBetween} from "@lib/util" import { + ALERT, + ALERT_STATUS, + NOTIFIER_RELAY, INDEXER_RELAYS, getDefaultPubkeys, userRoomsByUrl, @@ -308,6 +311,20 @@ export const makeCalendarFeed = ({ } } +// Domain specific + +export const loadAlerts = (pubkey: string) => + load({ + relays: [NOTIFIER_RELAY], + filters: [{kinds: [ALERT], authors: [pubkey]}], + }) + +export const loadAlertStatuses = (pubkey: string) => + load({ + relays: [NOTIFIER_RELAY], + filters: [{kinds: [ALERT_STATUS], "#p": [pubkey]}], + }) + // Application requests export const listenForNotifications = () => { @@ -361,6 +378,8 @@ export const loadUserData = ( loadProfile(pubkey, request), loadFollows(pubkey, request), loadMutes(pubkey, request), + loadAlertStatuses(pubkey), + loadAlerts(pubkey), ]), ]) diff --git a/src/app/state.ts b/src/app/state.ts index a775841..1f5702a 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -41,7 +41,7 @@ import { normalizeRelayUrl, } from "@welshman/util" import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util" -import {Nip59} from "@welshman/signer" +import {Nip59, decrypt} from "@welshman/signer" import { pubkey, repository, @@ -62,6 +62,7 @@ import { ensurePlaintext, thunks, walkThunks, + signer, } from "@welshman/app" import type {Thunk, Relay} from "@welshman/app" import type {SubscribeRequestWithHandlers} from "@welshman/net" @@ -73,6 +74,15 @@ export const GENERAL = "_" export const PROTECTED = ["-"] +export const ALERT = 32830 + +export const ALERT_STATUS = 32831 + +export const NOTIFIER_PUBKEY = "27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df" + +// export const NOTIFIER_RELAY = 'wss://notifier.flotilla.social/' +export const NOTIFIER_RELAY = "ws://localhost:4738/" + export const INDEXER_RELAYS = [ "wss://purplepag.es/", "wss://relay.damus.io/", @@ -332,6 +342,40 @@ export const { load({...request, filters: [{kinds: [SETTINGS], authors: [pubkey]}]}), }) +// Alerts + +export type Alert = { + event: TrustedEvent + tags: string[][] +} + +export const alerts = deriveEventsMapped(repository, { + filters: [{kinds: [ALERT]}], + itemToEvent: item => item.event, + eventToItem: async event => { + const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content)) + + return {event, tags} + }, +}) + +// Alert Statuses + +export type AlertStatus = { + event: TrustedEvent + tags: string[][] +} + +export const alertStatuses = deriveEventsMapped(repository, { + filters: [{kinds: [ALERT_STATUS]}], + itemToEvent: item => item.event, + eventToItem: async event => { + const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content)) + + return {event, tags} + }, +}) + // Membership export const hasMembershipUrl = (list: List | undefined, url: string) => @@ -356,11 +400,7 @@ export const getMembershipRooms = (list?: List) => getGroupTags(getListTags(list)).map(([_, room, url, name = ""]) => ({url, room, name})) export const getMembershipRoomsByUrl = (url: string, list?: List) => - sort( - getGroupTags(getListTags(list)) - .filter(t => t[2] === url) - .map(nth(1)), - ) + sort(getGroupTags(getListTags(list)).filter(nthEq(2, url)).map(nth(1))) export const memberships = deriveEventsMapped(repository, { filters: [{kinds: [GROUPS]}], diff --git a/src/lib/components/Confirm.svelte b/src/lib/components/Confirm.svelte index a552e4b..30df757 100644 --- a/src/lib/components/Confirm.svelte +++ b/src/lib/components/Confirm.svelte @@ -39,7 +39,7 @@
{subtitle}
{/snippet} -

{message}

+

{message}