feat: push notifications
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
c823cd314d
commit
b5e9203742
@ -2,7 +2,7 @@ import "./LinkPreview.css";
|
|||||||
import { CSSProperties, useEffect, useState } from "react";
|
import { CSSProperties, useEffect, useState } from "react";
|
||||||
|
|
||||||
import Spinner from "Icons/Spinner";
|
import Spinner from "Icons/Spinner";
|
||||||
import SnortApi, { LinkPreviewData } from "SnortApi";
|
import SnortApi, { LinkPreviewData } from "External/SnortApi";
|
||||||
import useImgProxy from "Hooks/useImgProxy";
|
import useImgProxy from "Hooks/useImgProxy";
|
||||||
import { MediaElement } from "Element/Embed/MediaElement";
|
import { MediaElement } from "Element/Embed/MediaElement";
|
||||||
|
|
||||||
|
@ -2,12 +2,11 @@ import { NostrEvent } from "@snort/system";
|
|||||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||||
import { LNURL } from "@snort/shared";
|
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 FollowListBase from "Element/User/FollowListBase";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import { useWallet } from "Wallet";
|
import { useWallet } from "Wallet";
|
||||||
import { Toastore } from "Toaster";
|
import { Toastore } from "Toaster";
|
||||||
import { getDisplayName } from "Element/User/DisplayName";
|
|
||||||
import { UserCache } from "Cache";
|
import { UserCache } from "Cache";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import useEventPublisher from "Hooks/useEventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
|
@ -9,7 +9,7 @@ import classNames from "classnames";
|
|||||||
|
|
||||||
import { formatShort } from "Number";
|
import { formatShort } from "Number";
|
||||||
import useEventPublisher from "Hooks/useEventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { delay, findTag } from "SnortUtils";
|
import { delay, findTag, getDisplayName } from "SnortUtils";
|
||||||
import { NoteCreator } from "Element/Event/NoteCreator";
|
import { NoteCreator } from "Element/Event/NoteCreator";
|
||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
import { ZapsSummary } from "Element/Event/Zap";
|
import { ZapsSummary } from "Element/Event/Zap";
|
||||||
@ -20,7 +20,6 @@ import useLogin from "Hooks/useLogin";
|
|||||||
import { useInteractionCache } from "Hooks/useInteractionCache";
|
import { useInteractionCache } from "Hooks/useInteractionCache";
|
||||||
import { ZapPoolController } from "ZapPoolController";
|
import { ZapPoolController } from "ZapPoolController";
|
||||||
import { Zapper, ZapTarget } from "Zapper";
|
import { Zapper, ZapTarget } from "Zapper";
|
||||||
import { getDisplayName } from "Element/User/DisplayName";
|
|
||||||
import { useNoteCreator } from "State/NoteCreator";
|
import { useNoteCreator } from "State/NoteCreator";
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
|
|
||||||
|
@ -4,8 +4,7 @@ import { useMemo } from "react";
|
|||||||
import { EventKind, NostrEvent, TaggedNostrEvent, NostrPrefix, EventExt } from "@snort/system";
|
import { EventKind, NostrEvent, TaggedNostrEvent, NostrPrefix, EventExt } from "@snort/system";
|
||||||
|
|
||||||
import Note from "Element/Event/Note";
|
import Note from "Element/Event/Note";
|
||||||
import { getDisplayName } from "Element/User/DisplayName";
|
import { eventLink, hexToBech32, getDisplayName } from "SnortUtils";
|
||||||
import { eventLink, hexToBech32 } from "SnortUtils";
|
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
|
@ -5,8 +5,7 @@ import type { UserMetadata } from "@snort/system";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import useImgProxy from "Hooks/useImgProxy";
|
import useImgProxy from "Hooks/useImgProxy";
|
||||||
import { getDisplayName } from "Element/User/DisplayName";
|
import { defaultAvatar, getDisplayName } from "SnortUtils";
|
||||||
import { defaultAvatar } from "SnortUtils";
|
|
||||||
|
|
||||||
interface AvatarProps {
|
interface AvatarProps {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
|
@ -1,35 +1,14 @@
|
|||||||
import "./DisplayName.css";
|
import "./DisplayName.css";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { HexKey, UserMetadata, NostrPrefix } from "@snort/system";
|
import { HexKey, UserMetadata } from "@snort/system";
|
||||||
import AnimalName from "Element/User/AnimalName";
|
import { getDisplayNameOrPlaceHolder } from "SnortUtils";
|
||||||
import { hexToBech32 } from "SnortUtils";
|
|
||||||
|
|
||||||
interface DisplayNameProps {
|
interface DisplayNameProps {
|
||||||
pubkey: HexKey;
|
pubkey: HexKey;
|
||||||
user: UserMetadata | undefined;
|
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 DisplayName = ({ pubkey, user }: DisplayNameProps) => {
|
||||||
const [name, isPlaceHolder] = useMemo(() => getDisplayNameOrPlaceHolder(user, pubkey), [user, pubkey]);
|
const [name, isPlaceHolder] = useMemo(() => getDisplayNameOrPlaceHolder(user, pubkey), [user, pubkey]);
|
||||||
|
|
||||||
|
@ -47,6 +47,12 @@ export interface LinkPreviewData {
|
|||||||
og_tags?: Array<[name: string, value: string]>;
|
og_tags?: Array<[name: string, value: string]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PushNotifications {
|
||||||
|
endpoint: string;
|
||||||
|
p256dh: string;
|
||||||
|
auth: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default class SnortApi {
|
export default class SnortApi {
|
||||||
#url: string;
|
#url: string;
|
||||||
#publisher?: EventPublisher;
|
#publisher?: EventPublisher;
|
||||||
@ -88,10 +94,18 @@ export default class SnortApi {
|
|||||||
return this.#getJson<{ address: string }>("p/on-chain");
|
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>(
|
async #getJsonAuthd<T>(
|
||||||
path: string,
|
path: string,
|
||||||
method?: "GET" | string,
|
method?: "GET" | string,
|
||||||
body?: { [key: string]: string },
|
body?: object,
|
||||||
headers?: { [key: string]: string },
|
headers?: { [key: string]: string },
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
if (!this.#publisher) {
|
if (!this.#publisher) {
|
||||||
@ -113,7 +127,7 @@ export default class SnortApi {
|
|||||||
async #getJson<T>(
|
async #getJson<T>(
|
||||||
path: string,
|
path: string,
|
||||||
method?: "GET" | string,
|
method?: "GET" | string,
|
||||||
body?: { [key: string]: string },
|
body?: object,
|
||||||
headers?: { [key: string]: string },
|
headers?: { [key: string]: string },
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const rsp = await fetch(`${this.#url}${path}`, {
|
const rsp = await fetch(`${this.#url}${path}`, {
|
||||||
@ -126,10 +140,19 @@ export default class SnortApi {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const obj = await rsp.json();
|
if (rsp.ok) {
|
||||||
if ("error" in obj) {
|
const text = await rsp.text();
|
||||||
throw new SubscriptionError(obj.error, obj.code);
|
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 { TaggedNostrEvent, EventKind, MetadataCache } from "@snort/system";
|
||||||
import { getDisplayName } from "Element/User/DisplayName";
|
|
||||||
import { MentionRegex } from "Const";
|
import { MentionRegex } from "Const";
|
||||||
import { defaultAvatar, tagFilterOfTextRepost } from "SnortUtils";
|
import { defaultAvatar, tagFilterOfTextRepost, getDisplayName } from "SnortUtils";
|
||||||
import { UserCache } from "Cache";
|
import { UserCache } from "Cache";
|
||||||
import { LoginSession } from "Login";
|
import { LoginSession } from "Login";
|
||||||
import { removeUndefined } from "@snort/shared";
|
import { removeUndefined } from "@snort/shared";
|
||||||
|
@ -6,7 +6,7 @@ import { ApiHost, DeveloperAccounts, SnortPubKey } from "Const";
|
|||||||
import ProfilePreview from "Element/User/ProfilePreview";
|
import ProfilePreview from "Element/User/ProfilePreview";
|
||||||
import ZapButton from "Element/Event/ZapButton";
|
import ZapButton from "Element/Event/ZapButton";
|
||||||
import { bech32ToHex } from "SnortUtils";
|
import { bech32ToHex } from "SnortUtils";
|
||||||
import SnortApi, { RevenueSplit, RevenueToday } from "SnortApi";
|
import SnortApi, { RevenueSplit, RevenueToday } from "External/SnortApi";
|
||||||
import Modal from "Element/Modal";
|
import Modal from "Element/Modal";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import QrCode from "Element/QrCode";
|
import QrCode from "Element/QrCode";
|
||||||
|
@ -22,6 +22,10 @@ import { LoginStore } from "Login";
|
|||||||
import { NoteCreatorButton } from "Element/Event/NoteCreatorButton";
|
import { NoteCreatorButton } from "Element/Event/NoteCreatorButton";
|
||||||
import { ProfileLink } from "Element/User/ProfileLink";
|
import { ProfileLink } from "Element/User/ProfileLink";
|
||||||
import SearchBox from "../Element/SearchBox";
|
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() {
|
export default function Layout() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -104,6 +108,7 @@ const AccountHeader = () => {
|
|||||||
readonly: s.readonly,
|
readonly: s.readonly,
|
||||||
}));
|
}));
|
||||||
const profile = useUserProfile(publicKey);
|
const profile = useUserProfile(publicKey);
|
||||||
|
const { publisher } = useEventPublisher();
|
||||||
|
|
||||||
const hasNotifications = useMemo(
|
const hasNotifications = useMemo(
|
||||||
() => latestNotification > readNotifications,
|
() => latestNotification > readNotifications,
|
||||||
@ -123,6 +128,25 @@ const AccountHeader = () => {
|
|||||||
console.error(e);
|
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) {
|
if (!publicKey) {
|
||||||
|
@ -8,7 +8,7 @@ import { useUserProfile, useUserSearch } from "@snort/system-react";
|
|||||||
|
|
||||||
import UnreadCount from "Element/UnreadCount";
|
import UnreadCount from "Element/UnreadCount";
|
||||||
import ProfileImage from "Element/User/ProfileImage";
|
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 NoteToSelf from "Element/User/NoteToSelf";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
@ -25,7 +25,6 @@ import { useEventFeed } from "Feed/EventFeed";
|
|||||||
import { LoginSession, LoginStore } from "Login";
|
import { LoginSession, LoginStore } from "Login";
|
||||||
import { Nip28ChatSystem } from "chat/nip28";
|
import { Nip28ChatSystem } from "chat/nip28";
|
||||||
import { ChatParticipantProfile } from "Element/Chat/ChatParticipant";
|
import { ChatParticipantProfile } from "Element/Chat/ChatParticipant";
|
||||||
import { getDisplayName } from "Element/User/DisplayName";
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
const TwoCol = 768;
|
const TwoCol = 768;
|
||||||
|
@ -11,7 +11,7 @@ import { Bar, BarChart, Tooltip, XAxis, YAxis } from "recharts";
|
|||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { markNotificationsRead } from "Login";
|
import { markNotificationsRead } from "Login";
|
||||||
import { Notifications, UserCache } from "Cache";
|
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 Icon from "Icons/Icon";
|
||||||
import ProfileImage from "Element/User/ProfileImage";
|
import ProfileImage from "Element/User/ProfileImage";
|
||||||
import useModeration from "Hooks/useModeration";
|
import useModeration from "Hooks/useModeration";
|
||||||
@ -20,7 +20,6 @@ import Text from "Element/Text";
|
|||||||
import { formatShort } from "Number";
|
import { formatShort } from "Number";
|
||||||
import { LiveEvent } from "Element/LiveEvent";
|
import { LiveEvent } from "Element/LiveEvent";
|
||||||
import ProfilePreview from "Element/User/ProfilePreview";
|
import ProfilePreview from "Element/User/ProfilePreview";
|
||||||
import { getDisplayName } from "Element/User/DisplayName";
|
|
||||||
import { Day } from "Const";
|
import { Day } from "Const";
|
||||||
import Tabs, { Tab } from "Element/Tabs";
|
import Tabs, { Tab } from "Element/Tabs";
|
||||||
import classNames from "classnames";
|
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 GetVerified from "Pages/new/GetVerified";
|
||||||
import ProfileSetup from "Pages/new/ProfileSetup";
|
import ProfileSetup from "Pages/new/ProfileSetup";
|
||||||
import NewUserFlow from "Pages/new/NewUserFlow";
|
import NewUserFlow from "Pages/new/NewUserFlow";
|
||||||
import ImportFollows from "Pages/new/ImportFollows";
|
|
||||||
import DiscoverFollows from "Pages/new/DiscoverFollows";
|
import DiscoverFollows from "Pages/new/DiscoverFollows";
|
||||||
|
|
||||||
export const PROFILE = "/new/profile";
|
export const PROFILE = "/new/profile";
|
||||||
export const IMPORT = "/new/import";
|
|
||||||
export const DISCOVER = "/new/discover";
|
export const DISCOVER = "/new/discover";
|
||||||
export const VERIFY = "/new/verify";
|
export const VERIFY = "/new/verify";
|
||||||
|
|
||||||
@ -21,10 +19,6 @@ export const NewUserRoutes: RouteObject[] = [
|
|||||||
path: PROFILE,
|
path: PROFILE,
|
||||||
element: <ProfileSetup />,
|
element: <ProfileSetup />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: IMPORT,
|
|
||||||
element: <ImportFollows />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: VERIFY,
|
path: VERIFY,
|
||||||
element: <GetVerified />,
|
element: <GetVerified />,
|
||||||
|
@ -4,7 +4,7 @@ import { Link, useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
import PageSpinner from "Element/PageSpinner";
|
import PageSpinner from "Element/PageSpinner";
|
||||||
import useEventPublisher from "Hooks/useEventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
|
import SnortApi, { Subscription, SubscriptionError } from "External/SnortApi";
|
||||||
import { mapSubscriptionErrorCode } from ".";
|
import { mapSubscriptionErrorCode } from ".";
|
||||||
import SubscriptionCard from "./SubscriptionCard";
|
import SubscriptionCard from "./SubscriptionCard";
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { unixNow, unwrap } from "@snort/shared";
|
|||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
import useEventPublisher from "Hooks/useEventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import SnortApi, { Subscription, SubscriptionError } from "SnortApi";
|
import SnortApi, { Subscription, SubscriptionError } from "External/SnortApi";
|
||||||
import { mapPlanName, mapSubscriptionErrorCode } from ".";
|
import { mapPlanName, mapSubscriptionErrorCode } from ".";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { mostRecentSubscription } from "Subscription";
|
import { mostRecentSubscription } from "Subscription";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { FormattedMessage, FormattedDate, FormattedNumber } from "react-intl";
|
import { FormattedMessage, FormattedDate, FormattedNumber } from "react-intl";
|
||||||
|
|
||||||
import { Subscription } from "SnortApi";
|
import { Subscription } from "External/SnortApi";
|
||||||
import { mapPlanName } from ".";
|
import { mapPlanName } from ".";
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import Nip5Service from "Element/Nip5Service";
|
import Nip5Service from "Element/Nip5Service";
|
||||||
|
@ -9,7 +9,7 @@ import { LockedFeatures, Plans, SubscriptionType } from "Subscription";
|
|||||||
import ManageSubscriptionPage from "Pages/subscribe/ManageSubscription";
|
import ManageSubscriptionPage from "Pages/subscribe/ManageSubscription";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
import useEventPublisher from "Hooks/useEventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import SnortApi, { SubscriptionError, SubscriptionErrorCode } from "SnortApi";
|
import SnortApi, { SubscriptionError, SubscriptionErrorCode } from "External/SnortApi";
|
||||||
import SendSats from "Element/SendSats";
|
import SendSats from "Element/SendSats";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
@ -14,8 +14,10 @@ import {
|
|||||||
NostrEvent,
|
NostrEvent,
|
||||||
MetadataCache,
|
MetadataCache,
|
||||||
NostrLink,
|
NostrLink,
|
||||||
|
UserMetadata,
|
||||||
} from "@snort/system";
|
} from "@snort/system";
|
||||||
import { Day } from "Const";
|
import { Day } from "Const";
|
||||||
|
import AnimalName from "Element/User/AnimalName";
|
||||||
|
|
||||||
export const sha256 = (str: string | Uint8Array): u256 => {
|
export const sha256 = (str: string | Uint8Array): u256 => {
|
||||||
return utils.bytesToHex(hash(str));
|
return utils.bytesToHex(hash(str));
|
||||||
@ -495,3 +497,23 @@ export const isChristmas = () => {
|
|||||||
const event = new Date(ThisYear, 11, 25);
|
const event = new Date(ThisYear, 11, 25);
|
||||||
return IsTheSeason(event);
|
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 { UserCache } from "Cache";
|
||||||
import { getDisplayName } from "Element/User/DisplayName";
|
|
||||||
import { LNURL, ExternalStore, unixNow } from "@snort/shared";
|
import { LNURL, ExternalStore, unixNow } from "@snort/shared";
|
||||||
import { Toastore } from "Toaster";
|
import { Toastore } from "Toaster";
|
||||||
import { LNWallet, WalletInvoiceState, Wallets } from "Wallet";
|
import { LNWallet, WalletInvoiceState, Wallets } from "Wallet";
|
||||||
|
import { getDisplayName } from "SnortUtils";
|
||||||
|
|
||||||
export enum ZapPoolRecipientType {
|
export enum ZapPoolRecipientType {
|
||||||
Generic = 0,
|
Generic = 0,
|
||||||
|
@ -3,6 +3,8 @@ declare const self: ServiceWorkerGlobalScope & {
|
|||||||
__WB_MANIFEST: (string | PrecacheEntry)[];
|
__WB_MANIFEST: (string | PrecacheEntry)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
import { NostrEvent, NostrPrefix, mapEventToProfile, tryParseNostrLink } from "@snort/system";
|
||||||
|
import { defaultAvatar, getDisplayName } from "SnortUtils";
|
||||||
import { clientsClaim } from "workbox-core";
|
import { clientsClaim } from "workbox-core";
|
||||||
import { PrecacheEntry, precacheAndRoute } from "workbox-precaching";
|
import { PrecacheEntry, precacheAndRoute } from "workbox-precaching";
|
||||||
|
|
||||||
@ -14,3 +16,69 @@ self.addEventListener("message", event => {
|
|||||||
self.skipWaiting();
|
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…
Reference in New Issue
Block a user