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 @@
+
+
+
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}