feat: push notifications
This commit is contained in:
parent
c823cd314d
commit
b5e9203742
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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<void>("api/v1/notifications/register", "POST", sub);
|
||||
}
|
||||
|
||||
async #getJsonAuthd<T>(
|
||||
path: string,
|
||||
method?: "GET" | string,
|
||||
body?: { [key: string]: string },
|
||||
body?: object,
|
||||
headers?: { [key: string]: string },
|
||||
): Promise<T> {
|
||||
if (!this.#publisher) {
|
||||
@ -113,7 +127,7 @@ export default class SnortApi {
|
||||
async #getJson<T>(
|
||||
path: string,
|
||||
method?: "GET" | string,
|
||||
body?: { [key: string]: string },
|
||||
body?: object,
|
||||
headers?: { [key: string]: string },
|
||||
): Promise<T> {
|
||||
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;
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
@ -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<string>("");
|
||||
const [follows, setFollows] = useState<string[]>([]);
|
||||
const [error, setError] = useState<string>("");
|
||||
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 (
|
||||
<div className="main-content new-user" dir="auto">
|
||||
<Logo />
|
||||
<div className="progress-bar">
|
||||
<div className="progress progress-last"></div>
|
||||
</div>
|
||||
<h1>
|
||||
<FormattedMessage {...messages.ImportTwitter} />
|
||||
</h1>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
{...messages.FindYourFollows}
|
||||
values={{
|
||||
provider: (
|
||||
<a href="https://nostr.directory" target="_blank" rel="noreferrer">
|
||||
nostr.directory
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<div className="next-actions continue-actions">
|
||||
<button type="button" onClick={() => navigate("/new/discover")}>
|
||||
<FormattedMessage {...messages.Next} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h2>
|
||||
<FormattedMessage {...messages.TwitterUsername} />
|
||||
</h2>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={formatMessage(messages.TwitterPlaceholder)}
|
||||
className="grow mr10"
|
||||
value={twitterUsername}
|
||||
onChange={e => setTwitterUsername(e.target.value)}
|
||||
/>
|
||||
<AsyncButton type="button" className="secondary tall" onClick={loadFollows}>
|
||||
<FormattedMessage {...messages.Check} />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
{error.length > 0 && <b className="error">{error}</b>}
|
||||
<div dir="ltr">
|
||||
{sortedTwitterFollows.length > 0 && (
|
||||
<FollowListBase
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage {...messages.FollowsOnNostr} values={{ username: twitterUsername }} />
|
||||
</h2>
|
||||
}
|
||||
pubkeys={sortedTwitterFollows}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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: <ProfileSetup />,
|
||||
},
|
||||
{
|
||||
path: IMPORT,
|
||||
element: <ImportFollows />,
|
||||
},
|
||||
{
|
||||
path: VERIFY,
|
||||
element: <GetVerified />,
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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<NostrEvent>;
|
||||
events: Array<NostrEvent>;
|
||||
}
|
||||
|
||||
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<NostrEvent>) {
|
||||
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("");
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user