From b5e92037423ef7fc0e870fe87b3f99f3ed5047c4 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 19 Oct 2023 13:28:14 +0100 Subject: [PATCH] feat: push notifications --- .../app/src/Element/Embed/LinkPreview.tsx | 2 +- packages/app/src/Element/Embed/PubkeyList.tsx | 3 +- packages/app/src/Element/Event/NoteFooter.tsx | 3 +- .../app/src/Element/Event/NoteReaction.tsx | 3 +- packages/app/src/Element/User/Avatar.tsx | 3 +- packages/app/src/Element/User/DisplayName.tsx | 25 +---- packages/app/src/{ => External}/SnortApi.ts | 35 +++++- packages/app/src/Notifications.ts | 3 +- packages/app/src/Pages/DonatePage.tsx | 2 +- packages/app/src/Pages/Layout.tsx | 24 ++++ packages/app/src/Pages/MessagesPage.tsx | 3 +- packages/app/src/Pages/Notifications.tsx | 3 +- packages/app/src/Pages/new/ImportFollows.tsx | 106 ------------------ packages/app/src/Pages/new/index.tsx | 6 - .../Pages/subscribe/ManageSubscription.tsx | 2 +- packages/app/src/Pages/subscribe/RenewSub.tsx | 2 +- .../src/Pages/subscribe/SubscriptionCard.tsx | 2 +- packages/app/src/Pages/subscribe/index.tsx | 2 +- packages/app/src/SnortUtils/index.ts | 22 ++++ packages/app/src/ZapPoolController.ts | 2 +- packages/app/src/service-worker.ts | 68 +++++++++++ 21 files changed, 159 insertions(+), 162 deletions(-) rename packages/app/src/{ => External}/SnortApi.ts (80%) delete mode 100644 packages/app/src/Pages/new/ImportFollows.tsx diff --git a/packages/app/src/Element/Embed/LinkPreview.tsx b/packages/app/src/Element/Embed/LinkPreview.tsx index 540f61ab..84fc9d08 100644 --- a/packages/app/src/Element/Embed/LinkPreview.tsx +++ b/packages/app/src/Element/Embed/LinkPreview.tsx @@ -2,7 +2,7 @@ import "./LinkPreview.css"; import { CSSProperties, useEffect, useState } from "react"; import Spinner from "Icons/Spinner"; -import SnortApi, { LinkPreviewData } from "SnortApi"; +import SnortApi, { LinkPreviewData } from "External/SnortApi"; import useImgProxy from "Hooks/useImgProxy"; import { MediaElement } from "Element/Embed/MediaElement"; diff --git a/packages/app/src/Element/Embed/PubkeyList.tsx b/packages/app/src/Element/Embed/PubkeyList.tsx index 518e708b..c6fadf97 100644 --- a/packages/app/src/Element/Embed/PubkeyList.tsx +++ b/packages/app/src/Element/Embed/PubkeyList.tsx @@ -2,12 +2,11 @@ import { NostrEvent } from "@snort/system"; import { FormattedMessage, FormattedNumber } from "react-intl"; import { LNURL } from "@snort/shared"; -import { dedupe, findTag, hexToBech32 } from "SnortUtils"; +import { dedupe, findTag, hexToBech32, getDisplayName } from "SnortUtils"; import FollowListBase from "Element/User/FollowListBase"; import AsyncButton from "Element/AsyncButton"; import { useWallet } from "Wallet"; import { Toastore } from "Toaster"; -import { getDisplayName } from "Element/User/DisplayName"; import { UserCache } from "Cache"; import useLogin from "Hooks/useLogin"; import useEventPublisher from "Hooks/useEventPublisher"; diff --git a/packages/app/src/Element/Event/NoteFooter.tsx b/packages/app/src/Element/Event/NoteFooter.tsx index 7999439a..4a218cac 100644 --- a/packages/app/src/Element/Event/NoteFooter.tsx +++ b/packages/app/src/Element/Event/NoteFooter.tsx @@ -9,7 +9,7 @@ import classNames from "classnames"; import { formatShort } from "Number"; import useEventPublisher from "Hooks/useEventPublisher"; -import { delay, findTag } from "SnortUtils"; +import { delay, findTag, getDisplayName } from "SnortUtils"; import { NoteCreator } from "Element/Event/NoteCreator"; import SendSats from "Element/SendSats"; import { ZapsSummary } from "Element/Event/Zap"; @@ -20,7 +20,6 @@ import useLogin from "Hooks/useLogin"; import { useInteractionCache } from "Hooks/useInteractionCache"; import { ZapPoolController } from "ZapPoolController"; import { Zapper, ZapTarget } from "Zapper"; -import { getDisplayName } from "Element/User/DisplayName"; import { useNoteCreator } from "State/NoteCreator"; import Icon from "Icons/Icon"; diff --git a/packages/app/src/Element/Event/NoteReaction.tsx b/packages/app/src/Element/Event/NoteReaction.tsx index af9b1085..e39194ef 100644 --- a/packages/app/src/Element/Event/NoteReaction.tsx +++ b/packages/app/src/Element/Event/NoteReaction.tsx @@ -4,8 +4,7 @@ import { useMemo } from "react"; import { EventKind, NostrEvent, TaggedNostrEvent, NostrPrefix, EventExt } from "@snort/system"; import Note from "Element/Event/Note"; -import { getDisplayName } from "Element/User/DisplayName"; -import { eventLink, hexToBech32 } from "SnortUtils"; +import { eventLink, hexToBech32, getDisplayName } from "SnortUtils"; import useModeration from "Hooks/useModeration"; import { FormattedMessage } from "react-intl"; import Icon from "Icons/Icon"; diff --git a/packages/app/src/Element/User/Avatar.tsx b/packages/app/src/Element/User/Avatar.tsx index ad44cb7d..d3d9eeb2 100644 --- a/packages/app/src/Element/User/Avatar.tsx +++ b/packages/app/src/Element/User/Avatar.tsx @@ -5,8 +5,7 @@ import type { UserMetadata } from "@snort/system"; import classNames from "classnames"; import useImgProxy from "Hooks/useImgProxy"; -import { getDisplayName } from "Element/User/DisplayName"; -import { defaultAvatar } from "SnortUtils"; +import { defaultAvatar, getDisplayName } from "SnortUtils"; interface AvatarProps { pubkey: string; diff --git a/packages/app/src/Element/User/DisplayName.tsx b/packages/app/src/Element/User/DisplayName.tsx index dfc12d86..55dcfbf3 100644 --- a/packages/app/src/Element/User/DisplayName.tsx +++ b/packages/app/src/Element/User/DisplayName.tsx @@ -1,35 +1,14 @@ import "./DisplayName.css"; import { useMemo } from "react"; -import { HexKey, UserMetadata, NostrPrefix } from "@snort/system"; -import AnimalName from "Element/User/AnimalName"; -import { hexToBech32 } from "SnortUtils"; +import { HexKey, UserMetadata } from "@snort/system"; +import { getDisplayNameOrPlaceHolder } from "SnortUtils"; interface DisplayNameProps { pubkey: HexKey; user: UserMetadata | undefined; } -export function getDisplayName(user: UserMetadata | undefined, pubkey: HexKey): string { - return getDisplayNameOrPlaceHolder(user, pubkey)[0]; -} - -export function getDisplayNameOrPlaceHolder(user: UserMetadata | undefined, pubkey: HexKey): [string, boolean] { - let name = hexToBech32(NostrPrefix.PublicKey, pubkey).substring(0, 12); - let isPlaceHolder = false; - - if (typeof user?.display_name === "string" && user.display_name.length > 0) { - name = user.display_name; - } else if (typeof user?.name === "string" && user.name.length > 0) { - name = user.name; - } else if (pubkey && CONFIG.animalNamePlaceholders) { - name = AnimalName(pubkey); - isPlaceHolder = true; - } - - return [name.trim(), isPlaceHolder]; -} - const DisplayName = ({ pubkey, user }: DisplayNameProps) => { const [name, isPlaceHolder] = useMemo(() => getDisplayNameOrPlaceHolder(user, pubkey), [user, pubkey]); diff --git a/packages/app/src/SnortApi.ts b/packages/app/src/External/SnortApi.ts similarity index 80% rename from packages/app/src/SnortApi.ts rename to packages/app/src/External/SnortApi.ts index 31a2f54b..7c130441 100644 --- a/packages/app/src/SnortApi.ts +++ b/packages/app/src/External/SnortApi.ts @@ -47,6 +47,12 @@ export interface LinkPreviewData { og_tags?: Array<[name: string, value: string]>; } +export interface PushNotifications { + endpoint: string; + p256dh: string; + auth: string; +} + export default class SnortApi { #url: string; #publisher?: EventPublisher; @@ -88,10 +94,18 @@ export default class SnortApi { return this.#getJson<{ address: string }>("p/on-chain"); } + getPushNotificationInfo() { + return this.#getJson<{ publicKey: string }>("api/v1/notifications/info"); + } + + registerPushNotifications(sub: PushNotifications) { + return this.#getJsonAuthd("api/v1/notifications/register", "POST", sub); + } + async #getJsonAuthd( path: string, method?: "GET" | string, - body?: { [key: string]: string }, + body?: object, headers?: { [key: string]: string }, ): Promise { if (!this.#publisher) { @@ -113,7 +127,7 @@ export default class SnortApi { async #getJson( path: string, method?: "GET" | string, - body?: { [key: string]: string }, + body?: object, headers?: { [key: string]: string }, ): Promise { const rsp = await fetch(`${this.#url}${path}`, { @@ -126,10 +140,19 @@ export default class SnortApi { }, }); - const obj = await rsp.json(); - if ("error" in obj) { - throw new SubscriptionError(obj.error, obj.code); + if (rsp.ok) { + const text = await rsp.text(); + if (text.length > 0) { + const obj = JSON.parse(text); + if ("error" in obj) { + throw new SubscriptionError(obj.error, obj.code); + } + return obj as T; + } else { + return {} as T; + } + } else { + throw new Error("Invalid response"); } - return obj as T; } } diff --git a/packages/app/src/Notifications.ts b/packages/app/src/Notifications.ts index 08453dce..1c42e0f3 100644 --- a/packages/app/src/Notifications.ts +++ b/packages/app/src/Notifications.ts @@ -1,7 +1,6 @@ import { TaggedNostrEvent, EventKind, MetadataCache } from "@snort/system"; -import { getDisplayName } from "Element/User/DisplayName"; import { MentionRegex } from "Const"; -import { defaultAvatar, tagFilterOfTextRepost } from "SnortUtils"; +import { defaultAvatar, tagFilterOfTextRepost, getDisplayName } from "SnortUtils"; import { UserCache } from "Cache"; import { LoginSession } from "Login"; import { removeUndefined } from "@snort/shared"; diff --git a/packages/app/src/Pages/DonatePage.tsx b/packages/app/src/Pages/DonatePage.tsx index 88cfd231..20d48e67 100644 --- a/packages/app/src/Pages/DonatePage.tsx +++ b/packages/app/src/Pages/DonatePage.tsx @@ -6,7 +6,7 @@ import { ApiHost, DeveloperAccounts, SnortPubKey } from "Const"; import ProfilePreview from "Element/User/ProfilePreview"; import ZapButton from "Element/Event/ZapButton"; import { bech32ToHex } from "SnortUtils"; -import SnortApi, { RevenueSplit, RevenueToday } from "SnortApi"; +import SnortApi, { RevenueSplit, RevenueToday } from "External/SnortApi"; import Modal from "Element/Modal"; import AsyncButton from "Element/AsyncButton"; import QrCode from "Element/QrCode"; diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx index 6009cb95..17574c52 100644 --- a/packages/app/src/Pages/Layout.tsx +++ b/packages/app/src/Pages/Layout.tsx @@ -22,6 +22,10 @@ import { LoginStore } from "Login"; import { NoteCreatorButton } from "Element/Event/NoteCreatorButton"; import { ProfileLink } from "Element/User/ProfileLink"; import SearchBox from "../Element/SearchBox"; +import SnortApi from "External/SnortApi"; +import useEventPublisher from "Hooks/useEventPublisher"; +import { base64 } from "@scure/base"; +import { unwrap } from "@snort/shared"; export default function Layout() { const location = useLocation(); @@ -104,6 +108,7 @@ const AccountHeader = () => { readonly: s.readonly, })); const profile = useUserProfile(publicKey); + const { publisher } = useEventPublisher(); const hasNotifications = useMemo( () => latestNotification > readNotifications, @@ -123,6 +128,25 @@ const AccountHeader = () => { console.error(e); } } + try { + if ("serviceWorker" in navigator) { + const reg = await navigator.serviceWorker.ready; + if (reg && publisher) { + const api = new SnortApi(undefined, publisher); + const sub = await reg.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: (await api.getPushNotificationInfo()).publicKey, + }); + await api.registerPushNotifications({ + endpoint: sub.endpoint, + p256dh: base64.encode(new Uint8Array(unwrap(sub.getKey("p256dh")))), + auth: base64.encode(new Uint8Array(unwrap(sub.getKey("auth")))), + }); + } + } + } catch (e) { + console.error(e); + } } if (!publicKey) { diff --git a/packages/app/src/Pages/MessagesPage.tsx b/packages/app/src/Pages/MessagesPage.tsx index 7f6bcdf9..b9675837 100644 --- a/packages/app/src/Pages/MessagesPage.tsx +++ b/packages/app/src/Pages/MessagesPage.tsx @@ -8,7 +8,7 @@ import { useUserProfile, useUserSearch } from "@snort/system-react"; import UnreadCount from "Element/UnreadCount"; import ProfileImage from "Element/User/ProfileImage"; -import { appendDedupe, debounce, parseId } from "SnortUtils"; +import { appendDedupe, debounce, parseId, getDisplayName } from "SnortUtils"; import NoteToSelf from "Element/User/NoteToSelf"; import useModeration from "Hooks/useModeration"; import useLogin from "Hooks/useLogin"; @@ -25,7 +25,6 @@ import { useEventFeed } from "Feed/EventFeed"; import { LoginSession, LoginStore } from "Login"; import { Nip28ChatSystem } from "chat/nip28"; import { ChatParticipantProfile } from "Element/Chat/ChatParticipant"; -import { getDisplayName } from "Element/User/DisplayName"; import classNames from "classnames"; const TwoCol = 768; diff --git a/packages/app/src/Pages/Notifications.tsx b/packages/app/src/Pages/Notifications.tsx index 43ccff78..16d1bad6 100644 --- a/packages/app/src/Pages/Notifications.tsx +++ b/packages/app/src/Pages/Notifications.tsx @@ -11,7 +11,7 @@ import { Bar, BarChart, Tooltip, XAxis, YAxis } from "recharts"; import useLogin from "Hooks/useLogin"; import { markNotificationsRead } from "Login"; import { Notifications, UserCache } from "Cache"; -import { dedupe, findTag, orderAscending, orderDescending } from "SnortUtils"; +import { dedupe, findTag, orderAscending, orderDescending, getDisplayName } from "SnortUtils"; import Icon from "Icons/Icon"; import ProfileImage from "Element/User/ProfileImage"; import useModeration from "Hooks/useModeration"; @@ -20,7 +20,6 @@ import Text from "Element/Text"; import { formatShort } from "Number"; import { LiveEvent } from "Element/LiveEvent"; import ProfilePreview from "Element/User/ProfilePreview"; -import { getDisplayName } from "Element/User/DisplayName"; import { Day } from "Const"; import Tabs, { Tab } from "Element/Tabs"; import classNames from "classnames"; diff --git a/packages/app/src/Pages/new/ImportFollows.tsx b/packages/app/src/Pages/new/ImportFollows.tsx deleted file mode 100644 index 8fb67bde..00000000 --- a/packages/app/src/Pages/new/ImportFollows.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useMemo, useState } from "react"; -import { useIntl, FormattedMessage } from "react-intl"; -import { useNavigate } from "react-router-dom"; - -import { ApiHost } from "Const"; -import Logo from "Element/Logo"; -import AsyncButton from "Element/AsyncButton"; -import FollowListBase from "Element/User/FollowListBase"; -import { bech32ToHex } from "SnortUtils"; -import SnortApi from "SnortApi"; -import useLogin from "Hooks/useLogin"; - -import messages from "./messages"; - -export default function ImportFollows() { - const navigate = useNavigate(); - const currentFollows = useLogin().follows; - const { formatMessage } = useIntl(); - const [twitterUsername, setTwitterUsername] = useState(""); - const [follows, setFollows] = useState([]); - const [error, setError] = useState(""); - const api = new SnortApi(ApiHost); - - const sortedTwitterFollows = useMemo(() => { - return follows.map(a => bech32ToHex(a)).sort(a => (currentFollows.item.includes(a) ? 1 : -1)); - }, [follows, currentFollows]); - - async function loadFollows() { - setFollows([]); - setError(""); - try { - const rsp = await api.twitterImport(twitterUsername); - if (Array.isArray(rsp) && rsp.length === 0) { - setError(formatMessage(messages.NoUsersFound, { twitterUsername })); - } else { - setFollows(rsp); - } - } catch (e) { - console.warn(e); - if (e instanceof Error) { - setError(e.message); - } else { - setError(formatMessage(messages.FailedToLoad)); - } - } - } - - return ( -
- -
-
-
-

- -

-

- - nostr.directory - - ), - }} - /> -

- -
- -
- -

- -

-
- setTwitterUsername(e.target.value)} - /> - - - -
- {error.length > 0 && {error}} -
- {sortedTwitterFollows.length > 0 && ( - - - - } - pubkeys={sortedTwitterFollows} - /> - )} -
-
- ); -} diff --git a/packages/app/src/Pages/new/index.tsx b/packages/app/src/Pages/new/index.tsx index 2bd6ea03..29fa7887 100644 --- a/packages/app/src/Pages/new/index.tsx +++ b/packages/app/src/Pages/new/index.tsx @@ -4,11 +4,9 @@ import { RouteObject } from "react-router-dom"; import GetVerified from "Pages/new/GetVerified"; import ProfileSetup from "Pages/new/ProfileSetup"; import NewUserFlow from "Pages/new/NewUserFlow"; -import ImportFollows from "Pages/new/ImportFollows"; import DiscoverFollows from "Pages/new/DiscoverFollows"; export const PROFILE = "/new/profile"; -export const IMPORT = "/new/import"; export const DISCOVER = "/new/discover"; export const VERIFY = "/new/verify"; @@ -21,10 +19,6 @@ export const NewUserRoutes: RouteObject[] = [ path: PROFILE, element: , }, - { - path: IMPORT, - element: , - }, { path: VERIFY, element: , diff --git a/packages/app/src/Pages/subscribe/ManageSubscription.tsx b/packages/app/src/Pages/subscribe/ManageSubscription.tsx index 7167094a..13e2436f 100644 --- a/packages/app/src/Pages/subscribe/ManageSubscription.tsx +++ b/packages/app/src/Pages/subscribe/ManageSubscription.tsx @@ -4,7 +4,7 @@ import { Link, useNavigate } from "react-router-dom"; import PageSpinner from "Element/PageSpinner"; import useEventPublisher from "Hooks/useEventPublisher"; -import SnortApi, { Subscription, SubscriptionError } from "SnortApi"; +import SnortApi, { Subscription, SubscriptionError } from "External/SnortApi"; import { mapSubscriptionErrorCode } from "."; import SubscriptionCard from "./SubscriptionCard"; diff --git a/packages/app/src/Pages/subscribe/RenewSub.tsx b/packages/app/src/Pages/subscribe/RenewSub.tsx index df0f6aa4..334a383b 100644 --- a/packages/app/src/Pages/subscribe/RenewSub.tsx +++ b/packages/app/src/Pages/subscribe/RenewSub.tsx @@ -5,7 +5,7 @@ import { unixNow, unwrap } from "@snort/shared"; import AsyncButton from "Element/AsyncButton"; import SendSats from "Element/SendSats"; import useEventPublisher from "Hooks/useEventPublisher"; -import SnortApi, { Subscription, SubscriptionError } from "SnortApi"; +import SnortApi, { Subscription, SubscriptionError } from "External/SnortApi"; import { mapPlanName, mapSubscriptionErrorCode } from "."; import useLogin from "Hooks/useLogin"; import { mostRecentSubscription } from "Subscription"; diff --git a/packages/app/src/Pages/subscribe/SubscriptionCard.tsx b/packages/app/src/Pages/subscribe/SubscriptionCard.tsx index 9a9aab65..fb009bed 100644 --- a/packages/app/src/Pages/subscribe/SubscriptionCard.tsx +++ b/packages/app/src/Pages/subscribe/SubscriptionCard.tsx @@ -1,6 +1,6 @@ import { FormattedMessage, FormattedDate, FormattedNumber } from "react-intl"; -import { Subscription } from "SnortApi"; +import { Subscription } from "External/SnortApi"; import { mapPlanName } from "."; import Icon from "Icons/Icon"; import Nip5Service from "Element/Nip5Service"; diff --git a/packages/app/src/Pages/subscribe/index.tsx b/packages/app/src/Pages/subscribe/index.tsx index d89ec2f1..237984eb 100644 --- a/packages/app/src/Pages/subscribe/index.tsx +++ b/packages/app/src/Pages/subscribe/index.tsx @@ -9,7 +9,7 @@ import { LockedFeatures, Plans, SubscriptionType } from "Subscription"; import ManageSubscriptionPage from "Pages/subscribe/ManageSubscription"; import AsyncButton from "Element/AsyncButton"; import useEventPublisher from "Hooks/useEventPublisher"; -import SnortApi, { SubscriptionError, SubscriptionErrorCode } from "SnortApi"; +import SnortApi, { SubscriptionError, SubscriptionErrorCode } from "External/SnortApi"; import SendSats from "Element/SendSats"; import classNames from "classnames"; diff --git a/packages/app/src/SnortUtils/index.ts b/packages/app/src/SnortUtils/index.ts index 4f21ff8b..e7d82df0 100644 --- a/packages/app/src/SnortUtils/index.ts +++ b/packages/app/src/SnortUtils/index.ts @@ -14,8 +14,10 @@ import { NostrEvent, MetadataCache, NostrLink, + UserMetadata, } from "@snort/system"; import { Day } from "Const"; +import AnimalName from "Element/User/AnimalName"; export const sha256 = (str: string | Uint8Array): u256 => { return utils.bytesToHex(hash(str)); @@ -495,3 +497,23 @@ export const isChristmas = () => { const event = new Date(ThisYear, 11, 25); return IsTheSeason(event); }; + +export function getDisplayName(user: UserMetadata | undefined, pubkey: HexKey): string { + return getDisplayNameOrPlaceHolder(user, pubkey)[0]; +} + +export function getDisplayNameOrPlaceHolder(user: UserMetadata | undefined, pubkey: HexKey): [string, boolean] { + let name = hexToBech32(NostrPrefix.PublicKey, pubkey).substring(0, 12); + let isPlaceHolder = false; + + if (typeof user?.display_name === "string" && user.display_name.length > 0) { + name = user.display_name; + } else if (typeof user?.name === "string" && user.name.length > 0) { + name = user.name; + } else if (pubkey && CONFIG.animalNamePlaceholders) { + name = AnimalName(pubkey); + isPlaceHolder = true; + } + + return [name.trim(), isPlaceHolder]; +} diff --git a/packages/app/src/ZapPoolController.ts b/packages/app/src/ZapPoolController.ts index 6c5124dd..ab274c99 100644 --- a/packages/app/src/ZapPoolController.ts +++ b/packages/app/src/ZapPoolController.ts @@ -1,8 +1,8 @@ import { UserCache } from "Cache"; -import { getDisplayName } from "Element/User/DisplayName"; import { LNURL, ExternalStore, unixNow } from "@snort/shared"; import { Toastore } from "Toaster"; import { LNWallet, WalletInvoiceState, Wallets } from "Wallet"; +import { getDisplayName } from "SnortUtils"; export enum ZapPoolRecipientType { Generic = 0, diff --git a/packages/app/src/service-worker.ts b/packages/app/src/service-worker.ts index cee0ca53..fab2ccd7 100644 --- a/packages/app/src/service-worker.ts +++ b/packages/app/src/service-worker.ts @@ -3,6 +3,8 @@ declare const self: ServiceWorkerGlobalScope & { __WB_MANIFEST: (string | PrecacheEntry)[]; }; +import { NostrEvent, NostrPrefix, mapEventToProfile, tryParseNostrLink } from "@snort/system"; +import { defaultAvatar, getDisplayName } from "SnortUtils"; import { clientsClaim } from "workbox-core"; import { PrecacheEntry, precacheAndRoute } from "workbox-precaching"; @@ -14,3 +16,69 @@ self.addEventListener("message", event => { self.skipWaiting(); } }); + +const enum PushType { + Mention = 1, +} + +interface PushNotification { + type: PushType; + data: object; +} + +interface PushNotificationMention { + profiles: Array; + events: Array; +} + +self.addEventListener("push", async e => { + console.debug(e); + const data = e.data?.json() as PushNotification | undefined; + console.debug(data); + if (data) { + switch (data.type) { + case PushType.Mention: { + const mention = data.data as PushNotificationMention; + for (const ev of mention.events) { + const userEvent = mention.profiles.find(a => a.pubkey === ev.pubkey); + const userProfile = userEvent ? mapEventToProfile(userEvent) : undefined; + const avatarUrl = userProfile?.picture ?? defaultAvatar(ev.pubkey); + + const notif = { + title: `Reply from ${getDisplayName(userProfile, ev.pubkey)}`, + body: replaceMentions(ev.content, mention.profiles).substring(0, 250), + icon: avatarUrl, + timestamp: ev.created_at * 1000, + }; + + console.debug("Sending notification", notif); + await self.registration.showNotification(notif.title, { + tag: "notification", + vibrate: [500], + ...notif, + }); + } + break; + } + } + } +}); + +const MentionNostrEntityRegex = /@n(pub|profile|event|ote|addr|)1[acdefghjklmnpqrstuvwxyz023456789]+/g; + +function replaceMentions(content: string, profiles: Array) { + return content + .split(MentionNostrEntityRegex) + .map(i => { + if (MentionNostrEntityRegex.test(i)) { + const link = tryParseNostrLink(i); + if (link?.type === NostrPrefix.PublicKey || link?.type === NostrPrefix.Profile) { + const px = profiles.find(a => a.pubkey === link.id); + const profile = px && mapEventToProfile(px); + return `@${getDisplayName(profile, link.id)}`; + } + } + return i; + }) + .join(""); +}