feat: multi-account system
This commit is contained in:
parent
723589fec7
commit
fe788853c9
@ -74,6 +74,15 @@ export const RecommendedFollows = [
|
||||
"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", // verbiricha
|
||||
];
|
||||
|
||||
/**
|
||||
* Snort imgproxy details
|
||||
*/
|
||||
export const DefaultImgProxy = {
|
||||
url: "https://imgproxy.snort.social",
|
||||
key: "a82fcf26aa0ccb55dfc6b4bd6a1c90744d3be0f38429f21a8828b43449ce7cebe6bdc2b09a827311bef37b18ce35cb1e6b1c60387a254541afa9e5b4264ae942",
|
||||
salt: "a897770d9abf163de055e9617891214e75a9016d748f8ef865e6ffbcb9ed932295659549773a22a019a5f06d0b440c320be411e3fddfe784e199e4f03d74bd9b",
|
||||
};
|
||||
|
||||
/**
|
||||
* NIP06-defined derivation path for private keys
|
||||
*/
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { useState, useMemo, ChangeEvent } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { HexKey, TaggedRawEvent } from "@snort/nostr";
|
||||
|
||||
import Note from "Element/Note";
|
||||
import { RootState } from "State/Store";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { UserCache } from "Cache/UserCache";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
interface BookmarksProps {
|
||||
pubkey: HexKey;
|
||||
bookmarks: readonly TaggedRawEvent[];
|
||||
@ -16,7 +16,7 @@ interface BookmarksProps {
|
||||
|
||||
const Bookmarks = ({ pubkey, bookmarks, related }: BookmarksProps) => {
|
||||
const [onlyPubkey, setOnlyPubkey] = useState<HexKey | "all">("all");
|
||||
const loginPubKey = useSelector((s: RootState) => s.login.publicKey);
|
||||
const loginPubKey = useLogin().publicKey;
|
||||
const ps = useMemo(() => {
|
||||
return [...new Set(bookmarks.map(ev => ev.pubkey))];
|
||||
}, [bookmarks]);
|
||||
|
@ -1,17 +1,15 @@
|
||||
import "./DM.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { HexKey, TaggedRawEvent } from "@snort/nostr";
|
||||
import { TaggedRawEvent } from "@snort/nostr";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import NoteTime from "Element/NoteTime";
|
||||
import Text from "Element/Text";
|
||||
import { setLastReadDm } from "Pages/MessagesPage";
|
||||
import { RootState } from "State/Store";
|
||||
import { incDmInteraction } from "State/Login";
|
||||
import { unwrap } from "Util";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -20,8 +18,7 @@ export type DMProps = {
|
||||
};
|
||||
|
||||
export default function DM(props: DMProps) {
|
||||
const dispatch = useDispatch();
|
||||
const pubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const pubKey = useLogin().publicKey;
|
||||
const publisher = useEventPublisher();
|
||||
const [content, setContent] = useState("Loading...");
|
||||
const [decrypted, setDecrypted] = useState(false);
|
||||
@ -35,7 +32,6 @@ export default function DM(props: DMProps) {
|
||||
setContent(decrypted || "<ERROR>");
|
||||
if (!isMe) {
|
||||
setLastReadDm(props.data.pubkey);
|
||||
dispatch(incDmInteraction());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
import "./FollowButton.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import { RootState } from "State/Store";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { parseId } from "Util";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -15,7 +15,7 @@ export interface FollowButtonProps {
|
||||
export default function FollowButton(props: FollowButtonProps) {
|
||||
const pubkey = parseId(props.pubkey);
|
||||
const publiser = useEventPublisher();
|
||||
const isFollowing = useSelector<RootState, boolean>(s => s.login.follows?.includes(pubkey) ?? false);
|
||||
const isFollowing = useLogin().follows.item.includes(pubkey);
|
||||
const baseClassname = `${props.className} follow-button`;
|
||||
|
||||
async function follow(pubkey: HexKey) {
|
||||
|
@ -1,24 +1,22 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { logout } from "State/Login";
|
||||
|
||||
import { logout } from "Login";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import messages from "./messages";
|
||||
|
||||
export default function LogoutButton() {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const publicKey = useLogin().publicKey;
|
||||
|
||||
if (!publicKey) return;
|
||||
return (
|
||||
<button
|
||||
className="secondary"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
logout(() => {
|
||||
logout(publicKey);
|
||||
navigate("/");
|
||||
})
|
||||
);
|
||||
}}>
|
||||
<FormattedMessage {...messages.Logout} />
|
||||
</button>
|
||||
|
@ -1,14 +1,11 @@
|
||||
import { MixCloudRegex } from "Const";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "State/Store";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
const MixCloudEmbed = ({ link }: { link: string }) => {
|
||||
const feedPath = (MixCloudRegex.test(link) && RegExp.$1) + "%2F" + (MixCloudRegex.test(link) && RegExp.$2);
|
||||
|
||||
const lightTheme = useSelector<RootState, boolean>(s => s.login.preferences.theme === "light");
|
||||
|
||||
const lightTheme = useLogin().preferences.theme === "light";
|
||||
const lightParams = lightTheme ? "light=1" : "light=0";
|
||||
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
|
@ -1,7 +1,7 @@
|
||||
import "./Nip05.css";
|
||||
import { useQuery } from "react-query";
|
||||
import Icon from "Icons/Icon";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import Icon from "Icons/Icon";
|
||||
|
||||
interface NostrJson {
|
||||
names: Record<string, string>;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState, ChangeEvent } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { UserMetadata } from "@snort/nostr";
|
||||
|
||||
import { unwrap } from "Util";
|
||||
import { formatShort } from "Number";
|
||||
@ -20,10 +20,9 @@ import Copy from "Element/Copy";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { debounce } from "Util";
|
||||
import { UserMetadata } from "@snort/nostr";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
import { RootState } from "State/Store";
|
||||
|
||||
type Nip05ServiceProps = {
|
||||
name: string;
|
||||
@ -40,7 +39,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
||||
const navigate = useNavigate();
|
||||
const { helpText = true } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const pubkey = useSelector((s: RootState) => s.login.publicKey);
|
||||
const pubkey = useLogin().publicKey;
|
||||
const user = useUserProfile(pubkey);
|
||||
const publisher = useEventPublisher();
|
||||
const svc = useMemo(() => new ServiceProvider(props.service), [props.service]);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import "./Note.css";
|
||||
import React, { useMemo, useState, useLayoutEffect, ReactNode } from "react";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { TaggedRawEvent, HexKey, EventKind, NostrPrefix } from "@snort/nostr";
|
||||
@ -25,13 +24,13 @@ import NoteFooter, { Translation } from "Element/NoteFooter";
|
||||
import NoteTime from "Element/NoteTime";
|
||||
import Reveal from "Element/Reveal";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { setPinned, setBookmarked } from "State/Login";
|
||||
import type { RootState } from "State/Store";
|
||||
import { UserCache } from "Cache/UserCache";
|
||||
import Poll from "Element/Poll";
|
||||
import { EventExt } from "System/EventExt";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setBookmarked, setPinned } from "Login";
|
||||
|
||||
import messages from "./messages";
|
||||
import { EventExt } from "System/EventExt";
|
||||
|
||||
export interface NoteProps {
|
||||
data: TaggedRawEvent;
|
||||
@ -72,7 +71,6 @@ const HiddenNote = ({ children }: { children: React.ReactNode }) => {
|
||||
|
||||
export default function Note(props: NoteProps) {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { data: ev, related, highlight, options: opt, ignoreModeration = false } = props;
|
||||
const [showReactions, setShowReactions] = useState(false);
|
||||
const deletions = useMemo(() => getReactions(related, ev.id, EventKind.Deletion), [related]);
|
||||
@ -82,7 +80,8 @@ export default function Note(props: NoteProps) {
|
||||
const [extendable, setExtendable] = useState<boolean>(false);
|
||||
const [showMore, setShowMore] = useState<boolean>(false);
|
||||
const baseClassName = `note card ${props.className ? props.className : ""}`;
|
||||
const { pinned, bookmarked } = useSelector((s: RootState) => s.login);
|
||||
const login = useLogin();
|
||||
const { pinned, bookmarked } = login;
|
||||
const publisher = useEventPublisher();
|
||||
const [translated, setTranslated] = useState<Translation>();
|
||||
const { formatMessage } = useIntl();
|
||||
@ -135,10 +134,12 @@ export default function Note(props: NoteProps) {
|
||||
async function unpin(id: HexKey) {
|
||||
if (options.canUnpin) {
|
||||
if (window.confirm(formatMessage(messages.ConfirmUnpin))) {
|
||||
const es = pinned.filter(e => e !== id);
|
||||
const es = pinned.item.filter(e => e !== id);
|
||||
const ev = await publisher.pinned(es);
|
||||
if (ev) {
|
||||
publisher.broadcast(ev);
|
||||
dispatch(setPinned({ keys: es, createdAt: new Date().getTime() }));
|
||||
setPinned(login, es, ev.created_at * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -146,10 +147,12 @@ export default function Note(props: NoteProps) {
|
||||
async function unbookmark(id: HexKey) {
|
||||
if (options.canUnbookmark) {
|
||||
if (window.confirm(formatMessage(messages.ConfirmUnbookmark))) {
|
||||
const es = bookmarked.filter(e => e !== id);
|
||||
const es = bookmarked.item.filter(e => e !== id);
|
||||
const ev = await publisher.bookmarked(es);
|
||||
if (ev) {
|
||||
publisher.broadcast(ev);
|
||||
dispatch(setBookmarked({ keys: es, createdAt: new Date().getTime() }));
|
||||
setBookmarked(login, es, ev.created_at * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,13 +17,14 @@ import SendSats from "Element/SendSats";
|
||||
import { ParsedZap, ZapsSummary } from "Element/Zap";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { RootState } from "State/Store";
|
||||
import { UserPreferences, setPinned, setBookmarked } from "State/Login";
|
||||
import { setReplyTo, setShow, reset } from "State/NoteCreator";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { SnortPubKey, TranslateHost } from "Const";
|
||||
import { LNURL } from "LNURL";
|
||||
import { DonateLNURL } from "Pages/DonatePage";
|
||||
import { useWallet } from "Wallet";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setBookmarked, setPinned } from "Login";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -94,10 +95,9 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const { ev, showReactions, setShowReactions, positive, negative, reposts, zaps } = props;
|
||||
const dispatch = useDispatch();
|
||||
const { formatMessage } = useIntl();
|
||||
const { pinned, bookmarked } = useSelector((s: RootState) => s.login);
|
||||
const login = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const login = useLogin();
|
||||
const { pinned, bookmarked, publicKey, preferences: prefs } = login;
|
||||
const { mute, block } = useModeration();
|
||||
const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
const author = useUserProfile(ev.pubkey);
|
||||
const publisher = useEventPublisher();
|
||||
const showNoteCreatorModal = useSelector((s: RootState) => s.noteCreator.show);
|
||||
@ -108,13 +108,13 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const walletState = useWallet();
|
||||
const wallet = walletState.wallet;
|
||||
|
||||
const isMine = ev.pubkey === login;
|
||||
const isMine = ev.pubkey === publicKey;
|
||||
const lang = window.navigator.language;
|
||||
const langNames = new Intl.DisplayNames([...window.navigator.languages], {
|
||||
type: "language",
|
||||
});
|
||||
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||
const didZap = ZapCache.has(ev.id) || zaps.some(a => a.sender === login);
|
||||
const didZap = ZapCache.has(ev.id) || zaps.some(a => a.sender === publicKey);
|
||||
const longPress = useLongPress(
|
||||
e => {
|
||||
e.stopPropagation();
|
||||
@ -126,11 +126,11 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
);
|
||||
|
||||
function hasReacted(emoji: string) {
|
||||
return positive?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === login);
|
||||
return positive?.some(({ pubkey, content }) => normalizeReaction(content) === emoji && pubkey === publicKey);
|
||||
}
|
||||
|
||||
function hasReposted() {
|
||||
return reposts.some(a => a.pubkey === login);
|
||||
return reposts.some(a => a.pubkey === publicKey);
|
||||
}
|
||||
|
||||
async function react(content: string) {
|
||||
@ -320,17 +320,21 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
}
|
||||
|
||||
async function pin(id: HexKey) {
|
||||
const es = [...pinned, id];
|
||||
const es = [...pinned.item, id];
|
||||
const ev = await publisher.pinned(es);
|
||||
if (ev) {
|
||||
publisher.broadcast(ev);
|
||||
dispatch(setPinned({ keys: es, createdAt: new Date().getTime() }));
|
||||
setPinned(login, es, ev.created_at * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async function bookmark(id: HexKey) {
|
||||
const es = [...bookmarked, id];
|
||||
const es = [...bookmarked.item, id];
|
||||
const ev = await publisher.bookmarked(es);
|
||||
if (ev) {
|
||||
publisher.broadcast(ev);
|
||||
dispatch(setBookmarked({ keys: es, createdAt: new Date().getTime() }));
|
||||
setBookmarked(login, es, ev.created_at * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyEvent() {
|
||||
@ -355,13 +359,13 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
<Icon name="share" />
|
||||
<FormattedMessage {...messages.Share} />
|
||||
</MenuItem>
|
||||
{!pinned.includes(ev.id) && (
|
||||
{!pinned.item.includes(ev.id) && (
|
||||
<MenuItem onClick={() => pin(ev.id)}>
|
||||
<Icon name="pin" />
|
||||
<FormattedMessage {...messages.Pin} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{!bookmarked.includes(ev.id) && (
|
||||
{!bookmarked.item.includes(ev.id) && (
|
||||
<MenuItem onClick={() => bookmark(ev.id)}>
|
||||
<Icon name="bookmark" />
|
||||
<FormattedMessage {...messages.Bookmark} />
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { TaggedRawEvent } from "@snort/nostr";
|
||||
import { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||
|
||||
import { ParsedZap } from "Element/Zap";
|
||||
import Text from "Element/Text";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { RootState } from "State/Store";
|
||||
import { useWallet } from "Wallet";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { LNURL } from "LNURL";
|
||||
@ -14,6 +12,7 @@ import { unwrap } from "Util";
|
||||
import { formatShort } from "Number";
|
||||
import Spinner from "Icons/Spinner";
|
||||
import SendSats from "Element/SendSats";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
interface PollProps {
|
||||
ev: TaggedRawEvent;
|
||||
@ -24,8 +23,7 @@ export default function Poll(props: PollProps) {
|
||||
const { formatMessage } = useIntl();
|
||||
const publisher = useEventPublisher();
|
||||
const { wallet } = useWallet();
|
||||
const prefs = useSelector((s: RootState) => s.login.preferences);
|
||||
const myPubKey = useSelector((s: RootState) => s.login.publicKey);
|
||||
const { preferences: prefs, publicKey: myPubKey } = useLogin();
|
||||
const pollerProfile = useUserProfile(props.ev.pubkey);
|
||||
const [error, setError] = useState("");
|
||||
const [invoice, setInvoice] = useState("");
|
||||
|
@ -2,7 +2,6 @@ import "./Relay.css";
|
||||
import { useMemo } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faPlug,
|
||||
@ -16,35 +15,33 @@ import {
|
||||
import { RelaySettings } from "@snort/nostr";
|
||||
|
||||
import useRelayState from "Feed/RelayState";
|
||||
import { setRelays } from "State/Login";
|
||||
import { RootState } from "State/Store";
|
||||
import { System } from "System";
|
||||
import { getRelayName, unwrap } from "Util";
|
||||
import { getRelayName, unixNowMs, unwrap } from "Util";
|
||||
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setRelays } from "Login";
|
||||
|
||||
export interface RelayProps {
|
||||
addr: string;
|
||||
}
|
||||
|
||||
export default function Relay(props: RelayProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { formatMessage } = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const allRelaySettings = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
|
||||
const relaySettings = unwrap(allRelaySettings[props.addr] ?? System.Sockets.get(props.addr)?.Settings ?? {});
|
||||
const login = useLogin();
|
||||
const relaySettings = unwrap(login.relays.item[props.addr] ?? System.Sockets.get(props.addr)?.Settings ?? {});
|
||||
const state = useRelayState(props.addr);
|
||||
const name = useMemo(() => getRelayName(props.addr), [props.addr]);
|
||||
|
||||
function configure(o: RelaySettings) {
|
||||
dispatch(
|
||||
setRelays({
|
||||
relays: {
|
||||
...allRelaySettings,
|
||||
setRelays(
|
||||
login,
|
||||
{
|
||||
...login.relays.item,
|
||||
[props.addr]: o,
|
||||
},
|
||||
createdAt: Math.floor(new Date().getTime() / 1000),
|
||||
})
|
||||
unixNowMs()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import MediaLink from "Element/MediaLink";
|
||||
import Reveal from "Element/Reveal";
|
||||
import { RootState } from "State/Store";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
interface RevealMediaProps {
|
||||
creator: string;
|
||||
@ -11,11 +10,10 @@ interface RevealMediaProps {
|
||||
}
|
||||
|
||||
export default function RevealMedia(props: RevealMediaProps) {
|
||||
const pref = useSelector((s: RootState) => s.login.preferences);
|
||||
const follows = useSelector((s: RootState) => s.login.follows);
|
||||
const publicKey = useSelector((s: RootState) => s.login.publicKey);
|
||||
const login = useLogin();
|
||||
const { preferences: pref, follows, publicKey } = login;
|
||||
|
||||
const hideNonFollows = pref.autoLoadMedia === "follows-only" && !follows.includes(props.creator);
|
||||
const hideNonFollows = pref.autoLoadMedia === "follows-only" && !follows.item.includes(props.creator);
|
||||
const isMine = props.creator === publicKey;
|
||||
const hideMedia = pref.autoLoadMedia === "none" || (!isMine && hideNonFollows);
|
||||
const hostname = new URL(props.link).hostname;
|
||||
|
@ -1,11 +1,9 @@
|
||||
import "./SendSats.css";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { useSelector } from "react-redux";
|
||||
import { HexKey, RawEvent } from "@snort/nostr";
|
||||
|
||||
import { formatShort } from "Number";
|
||||
import { RootState } from "State/Store";
|
||||
import Icon from "Icons/Icon";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
@ -18,6 +16,7 @@ import { useWallet } from "Wallet";
|
||||
import { EventExt } from "System/EventExt";
|
||||
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
enum ZapType {
|
||||
PublicZap = 1,
|
||||
@ -41,7 +40,7 @@ export interface SendSatsProps {
|
||||
export default function SendSats(props: SendSatsProps) {
|
||||
const onClose = props.onClose || (() => undefined);
|
||||
const { note, author, target } = props;
|
||||
const defaultZapAmount = useSelector((s: RootState) => s.login.preferences.defaultZapAmount);
|
||||
const defaultZapAmount = useLogin().preferences.defaultZapAmount;
|
||||
const amounts = [defaultZapAmount, 1_000, 5_000, 10_000, 20_000, 50_000, 100_000, 1_000_000];
|
||||
const emojis: Record<number, string> = {
|
||||
1_000: "👍",
|
||||
|
@ -13,6 +13,7 @@ import { findTag } from "Util";
|
||||
import { UserCache } from "Cache/UserCache";
|
||||
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
function getInvoice(zap: TaggedRawEvent): InvoiceDetails | undefined {
|
||||
const bolt11 = findTag(zap, "bolt11");
|
||||
@ -103,7 +104,7 @@ export interface ParsedZap {
|
||||
|
||||
const Zap = ({ zap, showZapped = true }: { zap: ParsedZap; showZapped?: boolean }) => {
|
||||
const { amount, content, sender, valid, receiver } = zap;
|
||||
const pubKey = useSelector((s: RootState) => s.login.publicKey);
|
||||
const pubKey = useLogin().publicKey;
|
||||
|
||||
return valid && sender ? (
|
||||
<div className="zap note card">
|
||||
|
41
packages/app/src/ExternalStore.ts
Normal file
41
packages/app/src/ExternalStore.ts
Normal file
@ -0,0 +1,41 @@
|
||||
type HookFn = () => void;
|
||||
|
||||
interface HookFilter {
|
||||
fn: HookFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple React hookable store with manual change notifications
|
||||
*/
|
||||
export default abstract class ExternalStore<TSnapshot> {
|
||||
#hooks: Array<HookFilter> = [];
|
||||
#snapshot: Readonly<TSnapshot> = {} as Readonly<TSnapshot>;
|
||||
#changed = true;
|
||||
|
||||
hook(fn: HookFn) {
|
||||
this.#hooks.push({
|
||||
fn,
|
||||
});
|
||||
return () => {
|
||||
const idx = this.#hooks.findIndex(a => a.fn === fn);
|
||||
if (idx >= 0) {
|
||||
this.#hooks.splice(idx, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
if (this.#changed) {
|
||||
this.#snapshot = this.takeSnapshot();
|
||||
this.#changed = false;
|
||||
}
|
||||
return this.#snapshot;
|
||||
}
|
||||
|
||||
protected notifyChange() {
|
||||
this.#changed = true;
|
||||
this.#hooks.forEach(h => h.fn());
|
||||
}
|
||||
|
||||
abstract takeSnapshot(): TSnapshot;
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { HexKey, Lists } from "@snort/nostr";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import useNotelistSubscription from "Hooks/useNotelistSubscription";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export default function useBookmarkFeed(pubkey?: HexKey) {
|
||||
const { bookmarked } = useSelector((s: RootState) => s.login);
|
||||
return useNotelistSubscription(pubkey, Lists.Bookmarked, bookmarked);
|
||||
const { bookmarked } = useLogin();
|
||||
return useNotelistSubscription(pubkey, Lists.Bookmarked, bookmarked.item);
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import { EventKind, RelaySettings, TaggedRawEvent, HexKey, RawEvent, u256, UserMetadata, Lists } from "@snort/nostr";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import { bech32ToHex, delay, unwrap } from "Util";
|
||||
import { DefaultRelays, HashtagRegex } from "Const";
|
||||
import { System } from "System";
|
||||
import { EventExt } from "System/EventExt";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@ -26,10 +25,7 @@ declare global {
|
||||
export type EventPublisher = ReturnType<typeof useEventPublisher>;
|
||||
|
||||
export default function useEventPublisher() {
|
||||
const pubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
|
||||
const follows = useSelector<RootState, HexKey[]>(s => s.login.follows);
|
||||
const relays = useSelector((s: RootState) => s.login.relays);
|
||||
const { publicKey: pubKey, privateKey: privKey, follows, relays } = useLogin();
|
||||
const hasNip07 = "nostr" in window;
|
||||
|
||||
async function signEvent(ev: RawEvent): Promise<RawEvent> {
|
||||
@ -270,7 +266,7 @@ export default function useEventPublisher() {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.ContactList);
|
||||
ev.content = JSON.stringify(relays);
|
||||
for (const pk of follows) {
|
||||
for (const pk of follows.item) {
|
||||
ev.tags.push(["p", pk]);
|
||||
}
|
||||
|
||||
@ -297,7 +293,7 @@ export default function useEventPublisher() {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.ContactList);
|
||||
ev.content = JSON.stringify(newRelays ?? relays);
|
||||
const temp = new Set(follows);
|
||||
const temp = new Set(follows.item);
|
||||
if (Array.isArray(pkAdd)) {
|
||||
pkAdd.forEach(a => temp.add(a));
|
||||
} else {
|
||||
@ -317,7 +313,7 @@ export default function useEventPublisher() {
|
||||
if (pubKey) {
|
||||
const ev = EventExt.forPubKey(pubKey, EventKind.ContactList);
|
||||
ev.content = JSON.stringify(relays);
|
||||
for (const pk of follows) {
|
||||
for (const pk of follows.item) {
|
||||
if (pk === pkRemove || pk.length !== 64) {
|
||||
continue;
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { HexKey, TaggedRawEvent, EventKind } from "@snort/nostr";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import { PubkeyReplaceableNoteStore, RequestBuilder } from "System";
|
||||
import useRequestBuilder from "Hooks/useRequestBuilder";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export default function useFollowsFeed(pubkey?: HexKey) {
|
||||
const { publicKey, follows } = useSelector((s: RootState) => s.login);
|
||||
const { publicKey, follows } = useLogin();
|
||||
const isMe = publicKey === pubkey;
|
||||
|
||||
const sub = useMemo(() => {
|
||||
@ -20,7 +19,7 @@ export default function useFollowsFeed(pubkey?: HexKey) {
|
||||
const contactFeed = useRequestBuilder<PubkeyReplaceableNoteStore>(PubkeyReplaceableNoteStore, sub);
|
||||
return useMemo(() => {
|
||||
if (isMe) {
|
||||
return follows;
|
||||
return follows.item;
|
||||
}
|
||||
|
||||
return getFollowing(contactFeed.data ?? [], pubkey);
|
||||
|
@ -1,23 +1,8 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AnyAction, ThunkDispatch } from "@reduxjs/toolkit";
|
||||
import { TaggedRawEvent, HexKey, Lists, EventKind } from "@snort/nostr";
|
||||
|
||||
import { bech32ToHex, getNewest, getNewestEventTagsByKey, unwrap } from "Util";
|
||||
import { makeNotification } from "Notifications";
|
||||
import {
|
||||
setFollows,
|
||||
setRelays,
|
||||
setMuted,
|
||||
setTags,
|
||||
setPinned,
|
||||
setBookmarked,
|
||||
setBlocked,
|
||||
sendNotification,
|
||||
setLatestNotifications,
|
||||
addSubscription,
|
||||
} from "State/Login";
|
||||
import { RootState } from "State/Store";
|
||||
import { makeNotification, sendNotification } from "Notifications";
|
||||
import useEventPublisher, { barrierNip07 } from "Feed/EventPublisher";
|
||||
import { getMutedKeys } from "Feed/MuteList";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
@ -25,6 +10,8 @@ import { FlatNoteStore, RequestBuilder } from "System";
|
||||
import useRequestBuilder from "Hooks/useRequestBuilder";
|
||||
import { EventExt } from "System/EventExt";
|
||||
import { DmCache } from "Cache";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { addSubscription, setBlocked, setBookmarked, setFollows, setMuted, setPinned, setRelays, setTags } from "Login";
|
||||
import { SnortPubKey } from "Const";
|
||||
import { SubscriptionEvent } from "Subscription";
|
||||
|
||||
@ -32,13 +19,8 @@ import { SubscriptionEvent } from "Subscription";
|
||||
* Managed loading data for the current logged in user
|
||||
*/
|
||||
export default function useLoginFeed() {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
publicKey: pubKey,
|
||||
privateKey: privKey,
|
||||
latestMuted,
|
||||
readNotifications,
|
||||
} = useSelector((s: RootState) => s.login);
|
||||
const login = useLogin();
|
||||
const { publicKey: pubKey, privateKey: privKey, readNotifications, muted: stateMuted } = login;
|
||||
const { isMuted } = useModeration();
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
@ -86,10 +68,10 @@ export default function useLoginFeed() {
|
||||
if (contactList) {
|
||||
if (contactList.content !== "" && contactList.content !== "{}") {
|
||||
const relays = JSON.parse(contactList.content);
|
||||
dispatch(setRelays({ relays, createdAt: contactList.created_at }));
|
||||
setRelays(login, relays, contactList.created_at * 1000);
|
||||
}
|
||||
const pTags = contactList.tags.filter(a => a[0] === "p").map(a => a[1]);
|
||||
dispatch(setFollows({ keys: pTags, createdAt: contactList.created_at }));
|
||||
setFollows(login, pTags, contactList.created_at * 1000);
|
||||
}
|
||||
|
||||
const dms = loginFeed.data.filter(a => a.kind === EventKind.DirectMessage);
|
||||
@ -109,9 +91,9 @@ export default function useLoginFeed() {
|
||||
} as SubscriptionEvent;
|
||||
}
|
||||
})
|
||||
).then(a => dispatch(addSubscription(a.filter(a => a !== undefined).map(unwrap))));
|
||||
).then(a => addSubscription(login, ...a.filter(a => a !== undefined).map(unwrap)));
|
||||
}
|
||||
}, [dispatch, loginFeed]);
|
||||
}, [loginFeed]);
|
||||
|
||||
// send out notifications
|
||||
useEffect(() => {
|
||||
@ -119,34 +101,26 @@ export default function useLoginFeed() {
|
||||
const replies = loginFeed.data.filter(
|
||||
a => a.kind === EventKind.TextNote && !isMuted(a.pubkey) && a.created_at > readNotifications
|
||||
);
|
||||
replies.forEach(nx => {
|
||||
dispatch(setLatestNotifications(nx.created_at));
|
||||
makeNotification(nx).then(notification => {
|
||||
if (notification) {
|
||||
(dispatch as ThunkDispatch<RootState, undefined, AnyAction>)(sendNotification(notification));
|
||||
replies.forEach(async nx => {
|
||||
const n = await makeNotification(nx);
|
||||
if (n) {
|
||||
sendNotification(login, n);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [dispatch, loginFeed, readNotifications]);
|
||||
}, [loginFeed, readNotifications]);
|
||||
|
||||
function handleMutedFeed(mutedFeed: TaggedRawEvent[]) {
|
||||
const muted = getMutedKeys(mutedFeed);
|
||||
dispatch(setMuted(muted));
|
||||
setMuted(login, muted.keys, muted.createdAt * 1000);
|
||||
|
||||
const newest = getNewest(mutedFeed);
|
||||
if (newest && newest.content.length > 0 && pubKey && newest.created_at > latestMuted) {
|
||||
decryptBlocked(newest, pubKey, privKey)
|
||||
if (muted.raw && (muted.raw?.content?.length ?? 0) > 0 && pubKey) {
|
||||
decryptBlocked(muted.raw, pubKey, privKey)
|
||||
.then(plaintext => {
|
||||
try {
|
||||
const blocked = JSON.parse(plaintext);
|
||||
const keys = blocked.filter((p: string) => p && p.length === 2 && p[0] === "p").map((p: string) => p[1]);
|
||||
dispatch(
|
||||
setBlocked({
|
||||
keys,
|
||||
createdAt: newest.created_at,
|
||||
})
|
||||
);
|
||||
setBlocked(login, keys, unwrap(muted.raw).created_at * 1000);
|
||||
} catch (error) {
|
||||
console.debug("Couldn't parse JSON");
|
||||
}
|
||||
@ -158,26 +132,21 @@ export default function useLoginFeed() {
|
||||
function handlePinnedFeed(pinnedFeed: TaggedRawEvent[]) {
|
||||
const newest = getNewestEventTagsByKey(pinnedFeed, "e");
|
||||
if (newest) {
|
||||
dispatch(setPinned(newest));
|
||||
setPinned(login, newest.keys, newest.createdAt * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function handleTagFeed(tagFeed: TaggedRawEvent[]) {
|
||||
const newest = getNewestEventTagsByKey(tagFeed, "t");
|
||||
if (newest) {
|
||||
dispatch(
|
||||
setTags({
|
||||
tags: newest.keys,
|
||||
createdAt: newest.createdAt,
|
||||
})
|
||||
);
|
||||
setTags(login, newest.keys, newest.createdAt * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function handleBookmarkFeed(bookmarkFeed: TaggedRawEvent[]) {
|
||||
const newest = getNewestEventTagsByKey(bookmarkFeed, "e");
|
||||
if (newest) {
|
||||
dispatch(setBookmarked(newest));
|
||||
setBookmarked(login, newest.keys, newest.createdAt * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@ -200,7 +169,7 @@ export default function useLoginFeed() {
|
||||
const bookmarkFeed = getList(listsFeed.data, Lists.Bookmarked);
|
||||
handleBookmarkFeed(bookmarkFeed);
|
||||
}
|
||||
}, [dispatch, listsFeed]);
|
||||
}, [listsFeed]);
|
||||
|
||||
/*const fRelays = useRelaysFeedFollows(follows);
|
||||
useEffect(() => {
|
||||
|
@ -1,15 +1,13 @@
|
||||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { getNewest } from "Util";
|
||||
import { HexKey, TaggedRawEvent, Lists, EventKind } from "@snort/nostr";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import { getNewest } from "Util";
|
||||
import { ParameterizedReplaceableNoteStore, RequestBuilder } from "System";
|
||||
import useRequestBuilder from "Hooks/useRequestBuilder";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export default function useMutedFeed(pubkey?: HexKey) {
|
||||
const { publicKey, muted } = useSelector((s: RootState) => s.login);
|
||||
const { publicKey, muted } = useLogin();
|
||||
const isMe = publicKey === pubkey;
|
||||
|
||||
const sub = useMemo(() => {
|
||||
@ -28,18 +26,20 @@ export default function useMutedFeed(pubkey?: HexKey) {
|
||||
return [];
|
||||
}, [mutedFeed, pubkey]);
|
||||
|
||||
return isMe ? muted : mutedList;
|
||||
return isMe ? muted.item : mutedList;
|
||||
}
|
||||
|
||||
export function getMutedKeys(rawNotes: TaggedRawEvent[]): {
|
||||
createdAt: number;
|
||||
keys: HexKey[];
|
||||
raw?: TaggedRawEvent;
|
||||
} {
|
||||
const newest = getNewest(rawNotes);
|
||||
if (newest) {
|
||||
const { created_at, tags } = newest;
|
||||
const keys = tags.filter(t => t[0] === "p").map(t => t[1]);
|
||||
return {
|
||||
raw: newest,
|
||||
keys,
|
||||
createdAt: created_at,
|
||||
};
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import { HexKey, Lists } from "@snort/nostr";
|
||||
import useNotelistSubscription from "Hooks/useNotelistSubscription";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export default function usePinnedFeed(pubkey?: HexKey) {
|
||||
const { pinned } = useSelector((s: RootState) => s.login);
|
||||
const pinned = useLogin().pinned.item;
|
||||
return useNotelistSubscription(pubkey, Lists.Pinned, pinned);
|
||||
}
|
||||
|
@ -1,17 +1,15 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { u256, EventKind } from "@snort/nostr";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import { UserPreferences } from "State/Login";
|
||||
import { appendDedupe, NostrLink } from "Util";
|
||||
import { FlatNoteStore, RequestBuilder } from "System";
|
||||
import useRequestBuilder from "Hooks/useRequestBuilder";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export default function useThreadFeed(link: NostrLink) {
|
||||
const [trackingEvents, setTrackingEvent] = useState<u256[]>([link.id]);
|
||||
const [allEvents, setAllEvents] = useState<u256[]>([link.id]);
|
||||
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
const pref = useLogin().preferences;
|
||||
|
||||
const sub = useMemo(() => {
|
||||
const sub = new RequestBuilder(`thread:${link.id.substring(0, 8)}`);
|
||||
|
@ -1,13 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { EventKind, u256 } from "@snort/nostr";
|
||||
|
||||
import { unixNow, unwrap, tagFilterOfTextRepost } from "Util";
|
||||
import { RootState } from "State/Store";
|
||||
import { UserPreferences } from "State/Login";
|
||||
import { FlatNoteStore, RequestBuilder } from "System";
|
||||
import useRequestBuilder from "Hooks/useRequestBuilder";
|
||||
import useTimelineWindow from "Hooks/useTimelineWindow";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export interface TimelineFeedOptions {
|
||||
method: "TIME_RANGE" | "LIMIT_UNTIL";
|
||||
@ -31,7 +29,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
||||
});
|
||||
const [trackingEvents, setTrackingEvent] = useState<u256[]>([]);
|
||||
const [trackingParentEvents, setTrackingParentEvents] = useState<u256[]>([]);
|
||||
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
const pref = useLogin().preferences;
|
||||
|
||||
const createBuilder = useCallback(() => {
|
||||
if (subject.type !== "global" && subject.items.length === 0) {
|
||||
|
@ -1,8 +1,7 @@
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import * as base64 from "@protobufjs/base64";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "State/Store";
|
||||
import { hmacSha256, unwrap } from "Util";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export interface ImgProxySettings {
|
||||
url: string;
|
||||
@ -11,7 +10,7 @@ export interface ImgProxySettings {
|
||||
}
|
||||
|
||||
export default function useImgProxy() {
|
||||
const settings = useSelector((s: RootState) => s.login.preferences.imgProxyConfig);
|
||||
const settings = useLogin().preferences.imgProxyConfig;
|
||||
const te = new TextEncoder();
|
||||
|
||||
function urlSafe(s: string) {
|
||||
|
9
packages/app/src/Hooks/useLogin.tsx
Normal file
9
packages/app/src/Hooks/useLogin.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { LoginStore } from "Login";
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
export default function useLogin() {
|
||||
return useSyncExternalStore(
|
||||
s => LoginStore.hook(s),
|
||||
() => LoginStore.snapshot()
|
||||
);
|
||||
}
|
@ -1,95 +1,72 @@
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
|
||||
import type { RootState } from "State/Store";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { setMuted, setBlocked } from "State/Login";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setBlocked, setMuted } from "Login";
|
||||
import { appendDedupe } from "Util";
|
||||
|
||||
export default function useModeration() {
|
||||
const dispatch = useDispatch();
|
||||
const { blocked, muted } = useSelector((s: RootState) => s.login);
|
||||
const login = useLogin();
|
||||
const { muted, blocked } = login;
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
async function setMutedList(pub: HexKey[], priv: HexKey[]) {
|
||||
try {
|
||||
const ev = await publisher.muted(pub, priv);
|
||||
console.debug(ev);
|
||||
if (ev) {
|
||||
publisher.broadcast(ev);
|
||||
return ev.created_at * 1000;
|
||||
}
|
||||
} catch (error) {
|
||||
console.debug("Couldn't change mute list");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function isMuted(id: HexKey) {
|
||||
return muted.includes(id) || blocked.includes(id);
|
||||
return muted.item.includes(id) || blocked.item.includes(id);
|
||||
}
|
||||
|
||||
function isBlocked(id: HexKey) {
|
||||
return blocked.includes(id);
|
||||
return blocked.item.includes(id);
|
||||
}
|
||||
|
||||
function unmute(id: HexKey) {
|
||||
const newMuted = muted.filter(p => p !== id);
|
||||
dispatch(
|
||||
setMuted({
|
||||
createdAt: new Date().getTime(),
|
||||
keys: newMuted,
|
||||
})
|
||||
);
|
||||
setMutedList(newMuted, blocked);
|
||||
async function unmute(id: HexKey) {
|
||||
const newMuted = muted.item.filter(p => p !== id);
|
||||
const ts = await setMutedList(newMuted, blocked.item);
|
||||
setMuted(login, newMuted, ts);
|
||||
}
|
||||
|
||||
function unblock(id: HexKey) {
|
||||
const newBlocked = blocked.filter(p => p !== id);
|
||||
dispatch(
|
||||
setBlocked({
|
||||
createdAt: new Date().getTime(),
|
||||
keys: newBlocked,
|
||||
})
|
||||
);
|
||||
setMutedList(muted, newBlocked);
|
||||
async function unblock(id: HexKey) {
|
||||
const newBlocked = blocked.item.filter(p => p !== id);
|
||||
const ts = await setMutedList(muted.item, newBlocked);
|
||||
setBlocked(login, newBlocked, ts);
|
||||
}
|
||||
|
||||
function mute(id: HexKey) {
|
||||
const newMuted = muted.includes(id) ? muted : muted.concat([id]);
|
||||
setMutedList(newMuted, blocked);
|
||||
dispatch(
|
||||
setMuted({
|
||||
createdAt: new Date().getTime(),
|
||||
keys: newMuted,
|
||||
})
|
||||
);
|
||||
async function mute(id: HexKey) {
|
||||
const newMuted = muted.item.includes(id) ? muted.item : muted.item.concat([id]);
|
||||
const ts = await setMutedList(newMuted, blocked.item);
|
||||
setMuted(login, newMuted, ts);
|
||||
}
|
||||
|
||||
function block(id: HexKey) {
|
||||
const newBlocked = blocked.includes(id) ? blocked : blocked.concat([id]);
|
||||
setMutedList(muted, newBlocked);
|
||||
dispatch(
|
||||
setBlocked({
|
||||
createdAt: new Date().getTime(),
|
||||
keys: newBlocked,
|
||||
})
|
||||
);
|
||||
async function block(id: HexKey) {
|
||||
const newBlocked = blocked.item.includes(id) ? blocked.item : blocked.item.concat([id]);
|
||||
const ts = await setMutedList(muted.item, newBlocked);
|
||||
setBlocked(login, newBlocked, ts);
|
||||
}
|
||||
|
||||
function muteAll(ids: HexKey[]) {
|
||||
const newMuted = Array.from(new Set(muted.concat(ids)));
|
||||
setMutedList(newMuted, blocked);
|
||||
dispatch(
|
||||
setMuted({
|
||||
createdAt: new Date().getTime(),
|
||||
keys: newMuted,
|
||||
})
|
||||
);
|
||||
async function muteAll(ids: HexKey[]) {
|
||||
const newMuted = appendDedupe(muted.item, ids);
|
||||
const ts = await setMutedList(newMuted, blocked.item);
|
||||
setMuted(login, newMuted, ts);
|
||||
}
|
||||
|
||||
return {
|
||||
muted,
|
||||
muted: muted.item,
|
||||
mute,
|
||||
muteAll,
|
||||
unmute,
|
||||
isMuted,
|
||||
blocked,
|
||||
blocked: blocked.item,
|
||||
block,
|
||||
unblock,
|
||||
isBlocked,
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { useMemo } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { HexKey, Lists, EventKind } from "@snort/nostr";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import { FlatNoteStore, ParameterizedReplaceableNoteStore, RequestBuilder } from "System";
|
||||
import useRequestBuilder from "Hooks/useRequestBuilder";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export default function useNotelistSubscription(pubkey: HexKey | undefined, l: Lists, defaultIds: HexKey[]) {
|
||||
const { preferences, publicKey } = useSelector((s: RootState) => s.login);
|
||||
const { preferences, publicKey } = useLogin();
|
||||
const isMe = publicKey === pubkey;
|
||||
|
||||
const sub = useMemo(() => {
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { type ReactNode } from "react";
|
||||
import { IntlProvider as ReactIntlProvider } from "react-intl";
|
||||
|
||||
import { ReadPreferences } from "State/Login";
|
||||
import enMessages from "translations/en.json";
|
||||
import esMessages from "translations/es_ES.json";
|
||||
import zhMessages from "translations/zh_CN.json";
|
||||
@ -16,6 +15,7 @@ import deMessages from "translations/de_DE.json";
|
||||
import ruMessages from "translations/ru_RU.json";
|
||||
import svMessages from "translations/sv_SE.json";
|
||||
import hrMessages from "translations/hr_HR.json";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
const DefaultLocale = "en-US";
|
||||
|
||||
@ -73,7 +73,7 @@ const getMessages = (locale: string) => {
|
||||
};
|
||||
|
||||
export const IntlProvider = ({ children }: { children: ReactNode }) => {
|
||||
const { language } = ReadPreferences();
|
||||
const { language } = useLogin().preferences;
|
||||
const locale = language ?? getLocale();
|
||||
|
||||
return (
|
||||
|
143
packages/app/src/Login/Functions.ts
Normal file
143
packages/app/src/Login/Functions.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { HexKey, RelaySettings } from "@snort/nostr";
|
||||
import * as secp from "@noble/secp256k1";
|
||||
|
||||
import { DefaultRelays, SnortPubKey } from "Const";
|
||||
import { EventPublisher } from "Feed/EventPublisher";
|
||||
import { LoginStore, UserPreferences, LoginSession } from "Login";
|
||||
import { generateBip39Entropy, entropyToDerivedKey } from "nip6";
|
||||
import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unixNowMs } from "Util";
|
||||
import { getCurrentSubscription, SubscriptionEvent } from "Subscription";
|
||||
|
||||
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) {
|
||||
if (state.relays.timestamp > createdAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
// filter out non-websocket urls
|
||||
const filtered = new Map<string, RelaySettings>();
|
||||
for (const [k, v] of Object.entries(relays)) {
|
||||
if (k.startsWith("wss://") || k.startsWith("ws://")) {
|
||||
const url = sanitizeRelayUrl(k);
|
||||
if (url) {
|
||||
filtered.set(url, v as RelaySettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
state.relays.item = Object.fromEntries(filtered.entries());
|
||||
state.relays.timestamp = createdAt;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function removeRelay(state: LoginSession, addr: string) {
|
||||
delete state.relays.item[addr];
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function updatePreferences(state: LoginSession, p: UserPreferences) {
|
||||
state.preferences = p;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function logout(k: HexKey) {
|
||||
LoginStore.removeSession(k);
|
||||
}
|
||||
|
||||
export function markNotificationsRead(state: LoginSession) {
|
||||
state.readNotifications = unixNowMs();
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function clearEntropy(state: LoginSession) {
|
||||
state.generatedEntropy = undefined;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new key and login with this generated key
|
||||
*/
|
||||
export async function generateNewLogin(publisher: EventPublisher) {
|
||||
const ent = generateBip39Entropy();
|
||||
const entHex = secp.utils.bytesToHex(ent);
|
||||
const newKeyHex = entropyToDerivedKey(ent);
|
||||
let newRelays: Record<string, RelaySettings> = {};
|
||||
|
||||
try {
|
||||
const rsp = await fetch("https://api.nostr.watch/v1/online");
|
||||
if (rsp.ok) {
|
||||
const online: string[] = await rsp.json();
|
||||
const pickRandom = randomSample(online, 4);
|
||||
const relayObjects = pickRandom.map(a => [a, { read: true, write: true }]);
|
||||
newRelays = {
|
||||
...Object.fromEntries(relayObjects),
|
||||
...Object.fromEntries(DefaultRelays.entries()),
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
const ev = await publisher.addFollow([bech32ToHex(SnortPubKey), newKeyHex], newRelays);
|
||||
publisher.broadcast(ev);
|
||||
|
||||
LoginStore.loginWithPrivateKey(newKeyHex, entHex);
|
||||
}
|
||||
|
||||
export function setTags(state: LoginSession, tags: Array<string>, ts: number) {
|
||||
if (state.tags.timestamp > ts) {
|
||||
return;
|
||||
}
|
||||
state.tags.item = tags;
|
||||
state.tags.timestamp = ts;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function setMuted(state: LoginSession, muted: Array<string>, ts: number) {
|
||||
if (state.muted.timestamp > ts) {
|
||||
return;
|
||||
}
|
||||
state.muted.item = muted;
|
||||
state.muted.timestamp = ts;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function setBlocked(state: LoginSession, blocked: Array<string>, ts: number) {
|
||||
if (state.blocked.timestamp > ts) {
|
||||
return;
|
||||
}
|
||||
state.blocked.item = blocked;
|
||||
state.blocked.timestamp = ts;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function setFollows(state: LoginSession, follows: Array<string>, ts: number) {
|
||||
if (state.follows.timestamp > ts) {
|
||||
return;
|
||||
}
|
||||
state.follows.item = follows;
|
||||
state.follows.timestamp = ts;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function setPinned(state: LoginSession, pinned: Array<string>, ts: number) {
|
||||
if (state.pinned.timestamp > ts) {
|
||||
return;
|
||||
}
|
||||
state.pinned.item = pinned;
|
||||
state.pinned.timestamp = ts;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function setBookmarked(state: LoginSession, bookmarked: Array<string>, ts: number) {
|
||||
if (state.bookmarked.timestamp > ts) {
|
||||
return;
|
||||
}
|
||||
state.bookmarked.item = bookmarked;
|
||||
state.bookmarked.timestamp = ts;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[]) {
|
||||
state.subscriptions = dedupeById([...state.subscriptions, ...subs]);
|
||||
state.currentSubscription = getCurrentSubscription(state.subscriptions);
|
||||
LoginStore.updateSession(state);
|
||||
}
|
88
packages/app/src/Login/LoginSession.ts
Normal file
88
packages/app/src/Login/LoginSession.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { HexKey, RelaySettings, u256 } from "@snort/nostr";
|
||||
import { UserPreferences } from "Login";
|
||||
import { SubscriptionEvent } from "Subscription";
|
||||
|
||||
/**
|
||||
* Stores latest copy of an item
|
||||
*/
|
||||
interface Newest<T> {
|
||||
item: T;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface LoginSession {
|
||||
/**
|
||||
* Current user private key
|
||||
*/
|
||||
privateKey?: HexKey;
|
||||
|
||||
/**
|
||||
* BIP39-generated, hex-encoded entropy
|
||||
*/
|
||||
generatedEntropy?: string;
|
||||
|
||||
/**
|
||||
* Current users public key
|
||||
*/
|
||||
publicKey?: HexKey;
|
||||
|
||||
/**
|
||||
* All the logged in users relays
|
||||
*/
|
||||
relays: Newest<Record<string, RelaySettings>>;
|
||||
|
||||
/**
|
||||
* A list of pubkeys this user follows
|
||||
*/
|
||||
follows: Newest<Array<HexKey>>;
|
||||
|
||||
/**
|
||||
* A list of tags this user follows
|
||||
*/
|
||||
tags: Newest<Array<string>>;
|
||||
|
||||
/**
|
||||
* A list of event ids this user has pinned
|
||||
*/
|
||||
pinned: Newest<Array<u256>>;
|
||||
|
||||
/**
|
||||
* A list of event ids this user has bookmarked
|
||||
*/
|
||||
bookmarked: Newest<Array<u256>>;
|
||||
|
||||
/**
|
||||
* A list of pubkeys this user has muted
|
||||
*/
|
||||
muted: Newest<Array<HexKey>>;
|
||||
|
||||
/**
|
||||
* A list of pubkeys this user has muted privately
|
||||
*/
|
||||
blocked: Newest<Array<HexKey>>;
|
||||
|
||||
/**
|
||||
* Latest notification
|
||||
*/
|
||||
latestNotification: number;
|
||||
|
||||
/**
|
||||
* Timestamp of last read notification
|
||||
*/
|
||||
readNotifications: number;
|
||||
|
||||
/**
|
||||
* Users cusom preferences
|
||||
*/
|
||||
preferences: UserPreferences;
|
||||
|
||||
/**
|
||||
* Snort subscriptions licences
|
||||
*/
|
||||
subscriptions: Array<SubscriptionEvent>;
|
||||
|
||||
/**
|
||||
* Current active subscription
|
||||
*/
|
||||
currentSubscription?: SubscriptionEvent;
|
||||
}
|
181
packages/app/src/Login/MultiAccountStore.ts
Normal file
181
packages/app/src/Login/MultiAccountStore.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import { HexKey, RelaySettings } from "@snort/nostr";
|
||||
|
||||
import { DefaultRelays } from "Const";
|
||||
import ExternalStore from "ExternalStore";
|
||||
import { LoginSession } from "Login";
|
||||
import { deepClone, sanitizeRelayUrl, unwrap } from "Util";
|
||||
import { DefaultPreferences, UserPreferences } from "./Preferences";
|
||||
|
||||
const AccountStoreKey = "sessions";
|
||||
const LoggedOut = {
|
||||
preferences: DefaultPreferences,
|
||||
tags: {
|
||||
item: [],
|
||||
timestamp: 0,
|
||||
},
|
||||
follows: {
|
||||
item: [],
|
||||
timestamp: 0,
|
||||
},
|
||||
muted: {
|
||||
item: [],
|
||||
timestamp: 0,
|
||||
},
|
||||
blocked: {
|
||||
item: [],
|
||||
timestamp: 0,
|
||||
},
|
||||
bookmarked: {
|
||||
item: [],
|
||||
timestamp: 0,
|
||||
},
|
||||
pinned: {
|
||||
item: [],
|
||||
timestamp: 0,
|
||||
},
|
||||
relays: {
|
||||
item: Object.fromEntries([...DefaultRelays.entries()].map(a => [unwrap(sanitizeRelayUrl(a[0])), a[1]])),
|
||||
timestamp: 0,
|
||||
},
|
||||
latestNotification: 0,
|
||||
readNotifications: 0,
|
||||
subscriptions: []
|
||||
} as LoginSession;
|
||||
const LegacyKeys = {
|
||||
PrivateKeyItem: "secret",
|
||||
PublicKeyItem: "pubkey",
|
||||
NotificationsReadItem: "notifications-read",
|
||||
UserPreferencesKey: "preferences",
|
||||
RelayListKey: "last-relays",
|
||||
FollowList: "last-follows",
|
||||
};
|
||||
|
||||
export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
#activeAccount?: HexKey;
|
||||
#accounts: Map<string, LoginSession>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const existing = window.localStorage.getItem(AccountStoreKey);
|
||||
if (existing) {
|
||||
this.#accounts = new Map((JSON.parse(existing) as Array<LoginSession>).map(a => [unwrap(a.publicKey), a]));
|
||||
} else {
|
||||
this.#accounts = new Map();
|
||||
}
|
||||
this.#migrate();
|
||||
if (!this.#activeAccount) {
|
||||
this.#activeAccount = this.#accounts.keys().next().value;
|
||||
}
|
||||
}
|
||||
|
||||
getSessions() {
|
||||
return [...this.#accounts.keys()];
|
||||
}
|
||||
|
||||
loginWithPubkey(key: HexKey, relays?: Record<string, RelaySettings>) {
|
||||
if (this.#accounts.has(key)) {
|
||||
throw new Error("Already logged in with this pubkey");
|
||||
}
|
||||
const initRelays = relays ?? Object.fromEntries(DefaultRelays.entries());
|
||||
const newSession = {
|
||||
...LoggedOut,
|
||||
publicKey: key,
|
||||
relays: {
|
||||
item: initRelays,
|
||||
timestamp: 1,
|
||||
},
|
||||
preferences: deepClone(DefaultPreferences),
|
||||
} as LoginSession;
|
||||
|
||||
this.#accounts.set(key, newSession);
|
||||
this.#activeAccount = key;
|
||||
this.#save();
|
||||
return newSession;
|
||||
}
|
||||
|
||||
loginWithPrivateKey(key: HexKey, entropy?: string) {
|
||||
const pubKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(key));
|
||||
if (this.#accounts.has(pubKey)) {
|
||||
throw new Error("Already logged in with this pubkey");
|
||||
}
|
||||
this.#accounts.set(pubKey, {
|
||||
...LoggedOut,
|
||||
privateKey: key,
|
||||
publicKey: pubKey,
|
||||
generatedEntropy: entropy,
|
||||
preferences: deepClone(DefaultPreferences),
|
||||
} as LoginSession);
|
||||
this.#activeAccount = pubKey;
|
||||
this.#save();
|
||||
}
|
||||
|
||||
updateSession(s: LoginSession) {
|
||||
const pk = unwrap(s.publicKey);
|
||||
if (this.#accounts.has(pk)) {
|
||||
this.#accounts.set(pk, s);
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
||||
removeSession(k: string) {
|
||||
if (this.#accounts.delete(k)) {
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
||||
takeSnapshot(): LoginSession {
|
||||
const s = this.#activeAccount ? this.#accounts.get(this.#activeAccount) : undefined;
|
||||
if (!s) return LoggedOut;
|
||||
|
||||
return deepClone(s);
|
||||
}
|
||||
|
||||
#migrate() {
|
||||
let didMigrate = false;
|
||||
const oldPreferences = window.localStorage.getItem(LegacyKeys.UserPreferencesKey);
|
||||
const pref: UserPreferences = oldPreferences ? JSON.parse(oldPreferences) : deepClone(DefaultPreferences);
|
||||
window.localStorage.removeItem(LegacyKeys.UserPreferencesKey);
|
||||
|
||||
const privKey = window.localStorage.getItem(LegacyKeys.PrivateKeyItem);
|
||||
if (privKey) {
|
||||
const pubKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(privKey));
|
||||
this.#accounts.set(pubKey, {
|
||||
...LoggedOut,
|
||||
privateKey: privKey,
|
||||
publicKey: pubKey,
|
||||
preferences: pref,
|
||||
} as LoginSession);
|
||||
window.localStorage.removeItem(LegacyKeys.PrivateKeyItem);
|
||||
window.localStorage.removeItem(LegacyKeys.PublicKeyItem);
|
||||
didMigrate = true;
|
||||
}
|
||||
|
||||
const pubKey = window.localStorage.getItem(LegacyKeys.PublicKeyItem);
|
||||
if (pubKey) {
|
||||
this.#accounts.set(pubKey, {
|
||||
...LoggedOut,
|
||||
publicKey: pubKey,
|
||||
preferences: pref,
|
||||
} as LoginSession);
|
||||
window.localStorage.removeItem(LegacyKeys.PublicKeyItem);
|
||||
didMigrate = true;
|
||||
}
|
||||
|
||||
window.localStorage.removeItem(LegacyKeys.RelayListKey);
|
||||
window.localStorage.removeItem(LegacyKeys.FollowList);
|
||||
window.localStorage.removeItem(LegacyKeys.NotificationsReadItem);
|
||||
if (didMigrate) {
|
||||
console.debug("Finished migration to MultiAccountStore");
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
||||
#save() {
|
||||
if (!this.#activeAccount && this.#accounts.size > 0) {
|
||||
this.#activeAccount = [...this.#accounts.keys()][0];
|
||||
}
|
||||
window.localStorage.setItem(AccountStoreKey, JSON.stringify([...this.#accounts.values()]));
|
||||
this.notifyChange();
|
||||
}
|
||||
}
|
90
packages/app/src/Login/Preferences.ts
Normal file
90
packages/app/src/Login/Preferences.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { DefaultImgProxy } from "Const";
|
||||
import { ImgProxySettings } from "Hooks/useImgProxy";
|
||||
|
||||
export interface UserPreferences {
|
||||
/**
|
||||
* User selected language
|
||||
*/
|
||||
language?: string;
|
||||
|
||||
/**
|
||||
* Enable reactions / reposts / zaps
|
||||
*/
|
||||
enableReactions: boolean;
|
||||
|
||||
/**
|
||||
* Reaction emoji
|
||||
*/
|
||||
reactionEmoji: string;
|
||||
|
||||
/**
|
||||
* Automatically load media (show link only) (bandwidth/privacy)
|
||||
*/
|
||||
autoLoadMedia: "none" | "follows-only" | "all";
|
||||
|
||||
/**
|
||||
* Select between light/dark theme
|
||||
*/
|
||||
theme: "system" | "light" | "dark";
|
||||
|
||||
/**
|
||||
* Ask for confirmation when reposting notes
|
||||
*/
|
||||
confirmReposts: boolean;
|
||||
|
||||
/**
|
||||
* Automatically show the latests notes
|
||||
*/
|
||||
autoShowLatest: boolean;
|
||||
|
||||
/**
|
||||
* Show debugging menus to help diagnose issues
|
||||
*/
|
||||
showDebugMenus: boolean;
|
||||
|
||||
/**
|
||||
* File uploading service to upload attachments to
|
||||
*/
|
||||
fileUploader: "void.cat" | "nostr.build" | "nostrimg.com";
|
||||
|
||||
/**
|
||||
* Use imgproxy to optimize images
|
||||
*/
|
||||
imgProxyConfig: ImgProxySettings | null;
|
||||
|
||||
/**
|
||||
* Default page to select on load
|
||||
*/
|
||||
defaultRootTab: "posts" | "conversations" | "global";
|
||||
|
||||
/**
|
||||
* Default zap amount
|
||||
*/
|
||||
defaultZapAmount: number;
|
||||
|
||||
/**
|
||||
* For each fast zap an additional X% will be sent to Snort donate address
|
||||
*/
|
||||
fastZapDonate: number;
|
||||
|
||||
/**
|
||||
* Auto-zap every post
|
||||
*/
|
||||
autoZap: boolean;
|
||||
}
|
||||
|
||||
export const DefaultPreferences = {
|
||||
enableReactions: true,
|
||||
reactionEmoji: "+",
|
||||
autoLoadMedia: "follows-only",
|
||||
theme: "system",
|
||||
confirmReposts: false,
|
||||
showDebugMenus: false,
|
||||
autoShowLatest: false,
|
||||
fileUploader: "void.cat",
|
||||
imgProxyConfig: DefaultImgProxy,
|
||||
defaultRootTab: "posts",
|
||||
defaultZapAmount: 50,
|
||||
fastZapDonate: 0.0,
|
||||
autoZap: false,
|
||||
} as UserPreferences;
|
6
packages/app/src/Login/index.ts
Normal file
6
packages/app/src/Login/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { MultiAccountStore } from "./MultiAccountStore";
|
||||
export const LoginStore = new MultiAccountStore();
|
||||
|
||||
export * from "./Preferences";
|
||||
export * from "./LoginSession";
|
||||
export * from "./Functions";
|
@ -2,12 +2,19 @@ import Nostrich from "nostrich.webp";
|
||||
|
||||
import { TaggedRawEvent } from "@snort/nostr";
|
||||
import { EventKind } from "@snort/nostr";
|
||||
import type { NotificationRequest } from "State/Login";
|
||||
import { MetadataCache } from "Cache";
|
||||
import { getDisplayName } from "Element/ProfileImage";
|
||||
import { MentionRegex } from "Const";
|
||||
import { tagFilterOfTextRepost, unwrap } from "Util";
|
||||
import { UserCache } from "Cache/UserCache";
|
||||
import { LoginSession } from "Login";
|
||||
|
||||
export interface NotificationRequest {
|
||||
title: string;
|
||||
body: string;
|
||||
icon: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export async function makeNotification(ev: TaggedRawEvent): Promise<NotificationRequest | null> {
|
||||
switch (ev.kind) {
|
||||
@ -52,3 +59,20 @@ function replaceTagsWithUser(ev: TaggedRawEvent, users: MetadataCache[]) {
|
||||
})
|
||||
.join();
|
||||
}
|
||||
|
||||
export async function sendNotification(state: LoginSession, req: NotificationRequest) {
|
||||
const hasPermission = "Notification" in window && Notification.permission === "granted";
|
||||
const shouldShowNotification = hasPermission && req.timestamp > state.readNotifications;
|
||||
if (shouldShowNotification) {
|
||||
try {
|
||||
const worker = await navigator.serviceWorker.ready;
|
||||
worker.showNotification(req.title, {
|
||||
tag: "notification",
|
||||
vibrate: [500],
|
||||
...req,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,17 @@
|
||||
import "./ChatPage.css";
|
||||
import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
|
||||
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import { bech32ToHex } from "Util";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
|
||||
import DM from "Element/DM";
|
||||
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
|
||||
import { dmsInChat, isToSelf } from "Pages/MessagesPage";
|
||||
import NoteToSelf from "Element/NoteToSelf";
|
||||
import { RootState } from "State/Store";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useDmCache } from "Hooks/useDmsCache";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
type RouterParams = {
|
||||
id: string;
|
||||
@ -23,7 +21,7 @@ export default function ChatPage() {
|
||||
const params = useParams<RouterParams>();
|
||||
const publisher = useEventPublisher();
|
||||
const id = bech32ToHex(params.id ?? "");
|
||||
const pubKey = useSelector((s: RootState) => s.login.publicKey);
|
||||
const pubKey = useLogin().publicKey;
|
||||
const [content, setContent] = useState<string>();
|
||||
const dmListRef = useRef<HTMLDivElement>(null);
|
||||
const dms = filterDms(useDmCache());
|
||||
|
@ -1,30 +1,27 @@
|
||||
import { useMemo } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
|
||||
import Timeline from "Element/Timeline";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { setTags } from "State/Login";
|
||||
import type { RootState } from "State/Store";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setTags } from "Login";
|
||||
|
||||
const HashTagsPage = () => {
|
||||
const params = useParams();
|
||||
const tag = (params.tag ?? "").toLowerCase();
|
||||
const dispatch = useDispatch();
|
||||
const { tags } = useSelector((s: RootState) => s.login);
|
||||
const login = useLogin();
|
||||
const isFollowing = useMemo(() => {
|
||||
return tags.includes(tag);
|
||||
}, [tags, tag]);
|
||||
return login.tags.item.includes(tag);
|
||||
}, [login, tag]);
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
function followTags(ts: string[]) {
|
||||
dispatch(
|
||||
setTags({
|
||||
tags: ts,
|
||||
createdAt: new Date().getTime(),
|
||||
})
|
||||
);
|
||||
publisher.tags(ts).then(ev => publisher.broadcast(ev));
|
||||
async function followTags(ts: string[]) {
|
||||
const ev = await publisher.tags(ts);
|
||||
if (ev) {
|
||||
publisher.broadcast(ev);
|
||||
setTags(login, ts, ev.created_at * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@ -33,11 +30,14 @@ const HashTagsPage = () => {
|
||||
<div className="action-heading">
|
||||
<h2>#{tag}</h2>
|
||||
{isFollowing ? (
|
||||
<button type="button" className="secondary" onClick={() => followTags(tags.filter(t => t !== tag))}>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary"
|
||||
onClick={() => followTags(login.tags.item.filter(t => t !== tag))}>
|
||||
<FormattedMessage defaultMessage="Unfollow" />
|
||||
</button>
|
||||
) : (
|
||||
<button type="button" onClick={() => followTags(tags.concat([tag]))}>
|
||||
<button type="button" onClick={() => followTags(login.tags.item.concat([tag]))}>
|
||||
<FormattedMessage defaultMessage="Follow" />
|
||||
</button>
|
||||
)}
|
||||
|
@ -4,13 +4,10 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { RelaySettings } from "@snort/nostr";
|
||||
import messages from "./messages";
|
||||
|
||||
import { bech32ToHex, randomSample, unixNowMs, unwrap } from "Util";
|
||||
import Icon from "Icons/Icon";
|
||||
import { RootState } from "State/Store";
|
||||
import { init, setRelays } from "State/Login";
|
||||
import { setShow, reset } from "State/NoteCreator";
|
||||
import { System } from "System";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
@ -20,11 +17,11 @@ import useModeration from "Hooks/useModeration";
|
||||
import { NoteCreator } from "Element/NoteCreator";
|
||||
import { db } from "Db";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { DefaultRelays, SnortPubKey } from "Const";
|
||||
import SubDebug from "Element/SubDebug";
|
||||
import { preload } from "Cache";
|
||||
import { useDmCache } from "Hooks/useDmsCache";
|
||||
import { mapPlanName } from "./subscribe";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
@ -33,9 +30,7 @@ export default function Layout() {
|
||||
const isReplyNoteCreatorShowing = replyTo && isNoteCreatorShowing;
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { loggedOut, publicKey, relays, preferences, newUserKey, subscription } = useSelector(
|
||||
(s: RootState) => s.login
|
||||
);
|
||||
const { publicKey, relays, preferences, currentSubscription } = useLogin();
|
||||
const [pageClass, setPageClass] = useState("page");
|
||||
const pub = useEventPublisher();
|
||||
useLoginFeed();
|
||||
@ -72,11 +67,11 @@ export default function Layout() {
|
||||
useEffect(() => {
|
||||
if (relays) {
|
||||
(async () => {
|
||||
for (const [k, v] of Object.entries(relays)) {
|
||||
for (const [k, v] of Object.entries(relays.item)) {
|
||||
await System.ConnectToRelay(k, v);
|
||||
}
|
||||
for (const [k, c] of System.Sockets) {
|
||||
if (!relays[k] && !c.Ephemeral) {
|
||||
if (!relays.item[k] && !c.Ephemeral) {
|
||||
System.DisconnectRelay(k);
|
||||
}
|
||||
}
|
||||
@ -117,7 +112,6 @@ export default function Layout() {
|
||||
await preload();
|
||||
}
|
||||
console.debug(`Using db: ${a ? "IndexedDB" : "In-Memory"}`);
|
||||
dispatch(init());
|
||||
|
||||
try {
|
||||
if ("registerProtocolHandler" in window.navigator) {
|
||||
@ -133,53 +127,16 @@ export default function Layout() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
async function handleNewUser() {
|
||||
let newRelays: Record<string, RelaySettings> = {};
|
||||
|
||||
try {
|
||||
const rsp = await fetch("https://api.nostr.watch/v1/online");
|
||||
if (rsp.ok) {
|
||||
const online: string[] = await rsp.json();
|
||||
const pickRandom = randomSample(online, 4);
|
||||
const relayObjects = pickRandom.map(a => [a, { read: true, write: true }]);
|
||||
newRelays = {
|
||||
...Object.fromEntries(relayObjects),
|
||||
...Object.fromEntries(DefaultRelays.entries()),
|
||||
};
|
||||
dispatch(
|
||||
setRelays({
|
||||
relays: newRelays,
|
||||
createdAt: unixNowMs(),
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
const ev = await pub.addFollow([bech32ToHex(SnortPubKey), unwrap(publicKey)], newRelays);
|
||||
pub.broadcast(ev);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (newUserKey === true) {
|
||||
handleNewUser().catch(console.warn);
|
||||
}
|
||||
}, [newUserKey]);
|
||||
|
||||
if (typeof loggedOut !== "boolean") {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={pageClass}>
|
||||
{!shouldHideHeader && (
|
||||
<header>
|
||||
<div className="logo" onClick={() => navigate("/")}>
|
||||
<h1>Snort</h1>
|
||||
{subscription && (
|
||||
{currentSubscription && (
|
||||
<small className="flex">
|
||||
<Icon name="diamond" size={10} className="mr5" />
|
||||
{mapPlanName(subscription.type)}
|
||||
{mapPlanName(currentSubscription.type)}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
@ -214,7 +171,7 @@ const AccountHeader = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { isMuted } = useModeration();
|
||||
const { publicKey, latestNotification, readNotifications } = useSelector((s: RootState) => s.login);
|
||||
const { publicKey, latestNotification, readNotifications } = useLogin();
|
||||
const dms = useDmCache();
|
||||
|
||||
const hasNotifications = useMemo(
|
||||
|
@ -1,22 +1,23 @@
|
||||
import "./Login.css";
|
||||
|
||||
import { CSSProperties, useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import { setPrivateKey, setPublicKey, setRelays, setGeneratedPrivateKey } from "State/Login";
|
||||
import { DefaultRelays, EmailRegex, MnemonicRegex } from "Const";
|
||||
import { EmailRegex, MnemonicRegex } from "Const";
|
||||
import { bech32ToHex, unwrap } from "Util";
|
||||
import { generateBip39Entropy, entropyToDerivedKey } from "nip6";
|
||||
import ZapButton from "Element/ZapButton";
|
||||
import useImgProxy from "Hooks/useImgProxy";
|
||||
import Icon from "Icons/Icon";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { generateNewLogin, LoginStore } from "Login";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
|
||||
import messages from "./messages";
|
||||
import Icon from "Icons/Icon";
|
||||
|
||||
interface ArtworkEntry {
|
||||
name: string;
|
||||
@ -24,26 +25,28 @@ interface ArtworkEntry {
|
||||
link: string;
|
||||
}
|
||||
|
||||
const KarnageKey = bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac");
|
||||
|
||||
// todo: fill more
|
||||
const Artwork: Array<ArtworkEntry> = [
|
||||
{
|
||||
name: "",
|
||||
pubkey: bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"),
|
||||
pubkey: KarnageKey,
|
||||
link: "https://void.cat/d/VKhPayp9ekeXYZGzAL9CxP",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
pubkey: bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"),
|
||||
pubkey: KarnageKey,
|
||||
link: "https://void.cat/d/3H2h8xxc3aEN6EVeobd8tw",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
pubkey: bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"),
|
||||
pubkey: KarnageKey,
|
||||
link: "https://void.cat/d/7i9W9PXn3TV86C4RUefNC9",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
pubkey: bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"),
|
||||
pubkey: KarnageKey,
|
||||
link: "https://void.cat/d/KtoX4ei6RYHY7HESg3Ve3k",
|
||||
},
|
||||
];
|
||||
@ -64,9 +67,9 @@ export async function getNip05PubKey(addr: string): Promise<string> {
|
||||
}
|
||||
|
||||
export default function LoginPage() {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const publicKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const publisher = useEventPublisher();
|
||||
const login = useLogin();
|
||||
const [key, setKey] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [art, setArt] = useState<ArtworkEntry>();
|
||||
@ -77,10 +80,10 @@ export default function LoginPage() {
|
||||
const hasSubtleCrypto = window.crypto.subtle !== undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (publicKey) {
|
||||
if (login.publicKey) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [publicKey, navigate]);
|
||||
}, [login, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
const ret = unwrap(Artwork.at(Artwork.length * Math.random()));
|
||||
@ -99,28 +102,28 @@ export default function LoginPage() {
|
||||
}
|
||||
const hexKey = bech32ToHex(key);
|
||||
if (secp.utils.isValidPrivateKey(hexKey)) {
|
||||
dispatch(setPrivateKey(hexKey));
|
||||
LoginStore.loginWithPrivateKey(hexKey);
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
} else if (key.startsWith("npub")) {
|
||||
const hexKey = bech32ToHex(key);
|
||||
dispatch(setPublicKey(hexKey));
|
||||
LoginStore.loginWithPubkey(hexKey);
|
||||
} else if (key.match(EmailRegex)) {
|
||||
const hexKey = await getNip05PubKey(key);
|
||||
dispatch(setPublicKey(hexKey));
|
||||
LoginStore.loginWithPubkey(hexKey);
|
||||
} else if (key.match(MnemonicRegex)) {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
const ent = generateBip39Entropy(key);
|
||||
const keyHex = entropyToDerivedKey(ent);
|
||||
dispatch(setPrivateKey(keyHex));
|
||||
LoginStore.loginWithPrivateKey(keyHex);
|
||||
} else if (secp.utils.isValidPrivateKey(key)) {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
dispatch(setPrivateKey(key));
|
||||
LoginStore.loginWithPrivateKey(key);
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
@ -139,29 +142,14 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
async function makeRandomKey() {
|
||||
const ent = generateBip39Entropy();
|
||||
const entHex = secp.utils.bytesToHex(ent);
|
||||
const newKeyHex = entropyToDerivedKey(ent);
|
||||
dispatch(setGeneratedPrivateKey({ key: newKeyHex, entropy: entHex }));
|
||||
await generateNewLogin(publisher);
|
||||
navigate("/new");
|
||||
}
|
||||
|
||||
async function doNip07Login() {
|
||||
const relays = "getRelays" in window.nostr ? await window.nostr.getRelays() : undefined;
|
||||
const pubKey = await window.nostr.getPublicKey();
|
||||
dispatch(setPublicKey(pubKey));
|
||||
|
||||
if ("getRelays" in window.nostr) {
|
||||
const relays = await window.nostr.getRelays();
|
||||
dispatch(
|
||||
setRelays({
|
||||
relays: {
|
||||
...relays,
|
||||
...Object.fromEntries(DefaultRelays.entries()),
|
||||
},
|
||||
createdAt: 1,
|
||||
})
|
||||
);
|
||||
}
|
||||
LoginStore.loginWithPubkey(pubKey, relays);
|
||||
}
|
||||
|
||||
function altLogins() {
|
||||
@ -198,9 +186,9 @@ export default function LoginPage() {
|
||||
/>
|
||||
</p>
|
||||
<div className="login-actions">
|
||||
<button type="button" onClick={() => makeRandomKey()}>
|
||||
<AsyncButton onClick={() => makeRandomKey()}>
|
||||
<FormattedMessage defaultMessage="Generate Key" description="Button: Generate a new key" />
|
||||
</button>
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -1,18 +1,16 @@
|
||||
import { useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { HexKey, RawEvent } from "@snort/nostr";
|
||||
|
||||
import UnreadCount from "Element/UnreadCount";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import { hexToBech32 } from "../Util";
|
||||
import { incDmInteraction } from "State/Login";
|
||||
import { RootState } from "State/Store";
|
||||
import { hexToBech32 } from "Util";
|
||||
import NoteToSelf from "Element/NoteToSelf";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { useDmCache } from "Hooks/useDmsCache";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
import { useDmCache } from "Hooks/useDmsCache";
|
||||
|
||||
type DmChat = {
|
||||
pubkey: HexKey;
|
||||
@ -21,18 +19,19 @@ type DmChat = {
|
||||
};
|
||||
|
||||
export default function MessagesPage() {
|
||||
const dispatch = useDispatch();
|
||||
const myPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const dmInteraction = useSelector<RootState, number>(s => s.login.dmInteraction);
|
||||
const login = useLogin();
|
||||
const { isMuted } = useModeration();
|
||||
const dms = useDmCache();
|
||||
|
||||
const chats = useMemo(() => {
|
||||
if (login.publicKey) {
|
||||
return extractChats(
|
||||
dms.filter(a => !isMuted(a.pubkey)),
|
||||
myPubKey ?? ""
|
||||
login.publicKey
|
||||
);
|
||||
}, [dms, myPubKey, dmInteraction]);
|
||||
}
|
||||
return [];
|
||||
}, [dms, login]);
|
||||
|
||||
const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unreadMessages, 0), [chats]);
|
||||
|
||||
@ -50,7 +49,7 @@ export default function MessagesPage() {
|
||||
}
|
||||
|
||||
function person(chat: DmChat) {
|
||||
if (chat.pubkey === myPubKey) return noteToSelf(chat);
|
||||
if (chat.pubkey === login.publicKey) return noteToSelf(chat);
|
||||
return (
|
||||
<div className="flex mb10" key={chat.pubkey}>
|
||||
<ProfileImage pubkey={chat.pubkey} className="f-grow" link={`/messages/${hexToBech32("npub", chat.pubkey)}`} />
|
||||
@ -63,7 +62,6 @@ export default function MessagesPage() {
|
||||
for (const c of chats) {
|
||||
setLastReadDm(c.pubkey);
|
||||
}
|
||||
dispatch(incDmInteraction());
|
||||
}
|
||||
|
||||
return (
|
||||
@ -78,7 +76,11 @@ export default function MessagesPage() {
|
||||
</div>
|
||||
{chats
|
||||
.sort((a, b) => {
|
||||
return a.pubkey === myPubKey ? -1 : b.pubkey === myPubKey ? 1 : b.newestMessage - a.newestMessage;
|
||||
return a.pubkey === login.publicKey
|
||||
? -1
|
||||
: b.pubkey === login.publicKey
|
||||
? 1
|
||||
: b.newestMessage - a.newestMessage;
|
||||
})
|
||||
.map(person)}
|
||||
</div>
|
||||
|
@ -1,32 +1,25 @@
|
||||
import { NostrPrefix } from "@snort/nostr";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import Spinner from "Icons/Spinner";
|
||||
import { setRelays } from "State/Login";
|
||||
import { parseNostrLink, profileLink, unixNowMs, unwrap } from "Util";
|
||||
import { parseNostrLink, profileLink } from "Util";
|
||||
import { getNip05PubKey } from "Pages/Login";
|
||||
import { System } from "System";
|
||||
|
||||
export default function NostrLinkHandler() {
|
||||
const params = useParams();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const link = decodeURIComponent(params["*"] ?? "").toLowerCase();
|
||||
|
||||
async function handleLink(link: string) {
|
||||
const nav = parseNostrLink(link);
|
||||
if (nav) {
|
||||
if ((nav.relays?.length ?? 0) > 0) {
|
||||
// todo: add as ephemerial connection
|
||||
dispatch(
|
||||
setRelays({
|
||||
relays: Object.fromEntries(unwrap(nav.relays).map(a => [a, { read: true, write: false }])),
|
||||
createdAt: unixNowMs(),
|
||||
})
|
||||
);
|
||||
nav.relays?.map(a => System.ConnectEphemeralRelay(a));
|
||||
}
|
||||
if (nav.type === NostrPrefix.Event || nav.type === NostrPrefix.Note || nav.type === NostrPrefix.Address) {
|
||||
navigate(`/e/${nav.encode()}`);
|
||||
|
@ -1,17 +1,15 @@
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
import { markNotificationsRead } from "State/Login";
|
||||
import { RootState } from "State/Store";
|
||||
|
||||
import Timeline from "Element/Timeline";
|
||||
import { TaskList } from "Tasks/TaskList";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { markNotificationsRead } from "Login";
|
||||
|
||||
export default function NotificationsPage() {
|
||||
const dispatch = useDispatch();
|
||||
const pubkey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const login = useLogin();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(markNotificationsRead());
|
||||
markNotificationsRead(login);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@ -19,12 +17,12 @@ export default function NotificationsPage() {
|
||||
<div className="main-content">
|
||||
<TaskList />
|
||||
</div>
|
||||
{pubkey && (
|
||||
{login.publicKey && (
|
||||
<Timeline
|
||||
subject={{
|
||||
type: "ptag",
|
||||
items: [pubkey],
|
||||
discriminator: pubkey.slice(0, 12),
|
||||
items: [login.publicKey],
|
||||
discriminator: login.publicKey.slice(0, 12),
|
||||
}}
|
||||
postsOnly={false}
|
||||
method={"TIME_RANGE"}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import "./ProfilePage.css";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { encodeTLV, EventKind, HexKey, NostrPrefix } from "@snort/nostr";
|
||||
|
||||
@ -36,7 +35,6 @@ import BlockList from "Element/BlockList";
|
||||
import MutedList from "Element/MutedList";
|
||||
import FollowsList from "Element/FollowListBase";
|
||||
import IconButton from "Element/IconButton";
|
||||
import { RootState } from "State/Store";
|
||||
import FollowsYou from "Element/FollowsYou";
|
||||
import QrCode from "Element/QrCode";
|
||||
import Modal from "Element/Modal";
|
||||
@ -46,6 +44,7 @@ import useHorizontalScroll from "Hooks/useHorizontalScroll";
|
||||
import { EmailRegex } from "Const";
|
||||
import { getNip05PubKey } from "Pages/Login";
|
||||
import { LNURL } from "LNURL";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -106,7 +105,7 @@ export default function ProfilePage() {
|
||||
const navigate = useNavigate();
|
||||
const [id, setId] = useState<string>();
|
||||
const user = useUserProfile(id);
|
||||
const loginPubKey = useSelector((s: RootState) => s.login.publicKey);
|
||||
const loginPubKey = useLogin().publicKey;
|
||||
const isMe = loginPubKey === id;
|
||||
const [showLnQr, setShowLnQr] = useState<boolean>(false);
|
||||
const [showProfileQr, setShowProfileQr] = useState<boolean>(false);
|
||||
|
@ -12,6 +12,7 @@ import { TimelineSubject } from "Feed/TimelineFeed";
|
||||
import { debounce, getRelayName, sha256, unixNow, unwrap } from "Util";
|
||||
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
interface RelayOption {
|
||||
url: string;
|
||||
@ -22,7 +23,7 @@ export default function RootPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { publicKey: pubKey, tags, preferences } = useSelector((s: RootState) => s.login);
|
||||
const { publicKey: pubKey, tags, preferences } = useLogin();
|
||||
|
||||
const RootTab: Record<string, Tab> = {
|
||||
Posts: {
|
||||
@ -65,7 +66,7 @@ export default function RootPage() {
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
const tagTabs = tags.map((t, idx) => {
|
||||
const tagTabs = tags.item.map((t, idx) => {
|
||||
return { text: `#${t}`, value: idx + 3, data: `/tag/${t}` };
|
||||
});
|
||||
const tabs = [RootTab.Posts, RootTab.PostsAndReplies, RootTab.Global, ...tagTabs];
|
||||
@ -81,8 +82,8 @@ export default function RootPage() {
|
||||
}
|
||||
|
||||
const FollowsHint = () => {
|
||||
const { publicKey: pubKey, follows } = useSelector((s: RootState) => s.login);
|
||||
if (follows?.length === 0 && pubKey) {
|
||||
const { publicKey: pubKey, follows } = useLogin();
|
||||
if (follows.item?.length === 0 && pubKey) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
{...messages.NoFollows}
|
||||
@ -100,7 +101,7 @@ const FollowsHint = () => {
|
||||
};
|
||||
|
||||
const GlobalTab = () => {
|
||||
const { relays } = useSelector((s: RootState) => s.login);
|
||||
const { relays } = useLogin();
|
||||
const [relay, setRelay] = useState<RelayOption>();
|
||||
const [allRelays, setAllRelays] = useState<RelayOption[]>();
|
||||
const [now] = useState(unixNow());
|
||||
@ -177,8 +178,8 @@ const GlobalTab = () => {
|
||||
};
|
||||
|
||||
const PostsTab = () => {
|
||||
const follows = useSelector((s: RootState) => s.login.follows);
|
||||
const subject: TimelineSubject = { type: "pubkey", items: follows, discriminator: "follows" };
|
||||
const { follows } = useLogin();
|
||||
const subject: TimelineSubject = { type: "pubkey", items: follows.item, discriminator: "follows" };
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -189,8 +190,8 @@ const PostsTab = () => {
|
||||
};
|
||||
|
||||
const ConversationsTab = () => {
|
||||
const { follows } = useSelector((s: RootState) => s.login);
|
||||
const subject: TimelineSubject = { type: "pubkey", items: follows, discriminator: "follows" };
|
||||
const { follows } = useLogin();
|
||||
const subject: TimelineSubject = { type: "pubkey", items: follows.item, discriminator: "follows" };
|
||||
|
||||
return <Timeline subject={subject} postsOnly={false} method={"TIME_RANGE"} window={undefined} relay={undefined} />;
|
||||
};
|
||||
|
@ -1,24 +1,25 @@
|
||||
import { useMemo } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
|
||||
import { RecommendedFollows } from "Const";
|
||||
import Logo from "Element/Logo";
|
||||
import FollowListBase from "Element/FollowListBase";
|
||||
import { useMemo } from "react";
|
||||
import { clearEntropy } from "State/Login";
|
||||
import { clearEntropy } from "Login";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export default function DiscoverFollows() {
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const login = useLogin();
|
||||
const navigate = useNavigate();
|
||||
const sortedReccomends = useMemo(() => {
|
||||
return RecommendedFollows.sort(() => (Math.random() >= 0.5 ? -1 : 1)).map(a => a.toLowerCase());
|
||||
}, []);
|
||||
|
||||
async function clearEntropyAndGo() {
|
||||
dispatch(clearEntropy());
|
||||
clearEntropy(login);
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
|
@ -1,20 +1,19 @@
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import Logo from "Element/Logo";
|
||||
import { services } from "Pages/Verification";
|
||||
import Nip5Service from "Element/Nip5Service";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import type { RootState } from "State/Store";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export default function GetVerified() {
|
||||
const navigate = useNavigate();
|
||||
const { publicKey } = useSelector((s: RootState) => s.login);
|
||||
const { publicKey } = useLogin();
|
||||
const user = useUserProfile(publicKey);
|
||||
const [isVerified, setIsVerified] = useState(false);
|
||||
const name = user?.name || "nostrich";
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
@ -7,15 +6,15 @@ import { ApiHost } from "Const";
|
||||
import Logo from "Element/Logo";
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
import FollowListBase from "Element/FollowListBase";
|
||||
import { RootState } from "State/Store";
|
||||
import { bech32ToHex } from "Util";
|
||||
import SnortApi from "SnortApi";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export default function ImportFollows() {
|
||||
const navigate = useNavigate();
|
||||
const currentFollows = useSelector((s: RootState) => s.login.follows);
|
||||
const currentFollows = useLogin().follows;
|
||||
const { formatMessage } = useIntl();
|
||||
const [twitterUsername, setTwitterUsername] = useState<string>("");
|
||||
const [follows, setFollows] = useState<string[]>([]);
|
||||
@ -23,7 +22,7 @@ export default function ImportFollows() {
|
||||
const api = new SnortApi(ApiHost);
|
||||
|
||||
const sortedTwitterFollows = useMemo(() => {
|
||||
return follows.map(a => bech32ToHex(a)).sort(a => (currentFollows.includes(a) ? 1 : -1));
|
||||
return follows.map(a => bech32ToHex(a)).sort(a => (currentFollows.item.includes(a) ? 1 : -1));
|
||||
}, [follows, currentFollows]);
|
||||
|
||||
async function loadFollows() {
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import Logo from "Element/Logo";
|
||||
import { CollapsedSection } from "Element/Collapsed";
|
||||
import Copy from "Element/Copy";
|
||||
import { RootState } from "State/Store";
|
||||
import { hexToBech32 } from "Util";
|
||||
import { hexToMnemonic } from "nip6";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
@ -69,7 +68,7 @@ const Extensions = () => {
|
||||
};
|
||||
|
||||
export default function NewUserFlow() {
|
||||
const { publicKey, privateKey, generatedEntropy } = useSelector((s: RootState) => s.login);
|
||||
const { publicKey, privateKey, generatedEntropy } = useLogin();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
|
@ -1,22 +1,20 @@
|
||||
import "./Index.css";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Icon from "Icons/Icon";
|
||||
import { logout } from "State/Login";
|
||||
import { logout } from "Login";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { unwrap } from "Util";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
const SettingsIndex = () => {
|
||||
const dispatch = useDispatch();
|
||||
const login = useLogin();
|
||||
const navigate = useNavigate();
|
||||
|
||||
function handleLogout() {
|
||||
dispatch(
|
||||
logout(() => {
|
||||
logout(unwrap(login.publicKey));
|
||||
navigate("/");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -1,20 +1,20 @@
|
||||
import "./Preferences.css";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Link } from "react-router-dom";
|
||||
import emoji from "@jukben/emoji-search";
|
||||
|
||||
import { DefaultImgProxy, setPreferences, UserPreferences } from "State/Login";
|
||||
import { RootState } from "State/Store";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { updatePreferences, UserPreferences } from "Login";
|
||||
import { DefaultImgProxy } from "Const";
|
||||
import { unwrap } from "Util";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
const PreferencesPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { formatMessage } = useIntl();
|
||||
const perf = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
const login = useLogin();
|
||||
const perf = login.preferences;
|
||||
|
||||
return (
|
||||
<div className="preferences">
|
||||
@ -32,12 +32,10 @@ const PreferencesPage = () => {
|
||||
<select
|
||||
value={perf.language}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
updatePreferences(login, {
|
||||
...perf,
|
||||
language: e.target.value,
|
||||
} as UserPreferences)
|
||||
)
|
||||
})
|
||||
}
|
||||
style={{ textTransform: "capitalize" }}>
|
||||
{["en", "ja", "es", "hu", "zh-CN", "zh-TW", "fr", "ar", "it", "id", "de", "ru", "sv", "hr"]
|
||||
@ -62,12 +60,10 @@ const PreferencesPage = () => {
|
||||
<select
|
||||
value={perf.theme}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
updatePreferences(login, {
|
||||
...perf,
|
||||
theme: e.target.value,
|
||||
} as UserPreferences)
|
||||
)
|
||||
}>
|
||||
<option value="system">
|
||||
<FormattedMessage {...messages.System} />
|
||||
@ -91,12 +87,10 @@ const PreferencesPage = () => {
|
||||
<select
|
||||
value={perf.defaultRootTab}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
updatePreferences(login, {
|
||||
...perf,
|
||||
defaultRootTab: e.target.value,
|
||||
} as UserPreferences)
|
||||
)
|
||||
}>
|
||||
<option value="posts">
|
||||
<FormattedMessage {...messages.Posts} />
|
||||
@ -122,12 +116,10 @@ const PreferencesPage = () => {
|
||||
<select
|
||||
value={perf.autoLoadMedia}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
updatePreferences(login, {
|
||||
...perf,
|
||||
autoLoadMedia: e.target.value,
|
||||
} as UserPreferences)
|
||||
)
|
||||
}>
|
||||
<option value="none">
|
||||
<FormattedMessage {...messages.None} />
|
||||
@ -153,7 +145,7 @@ const PreferencesPage = () => {
|
||||
type="number"
|
||||
defaultValue={perf.defaultZapAmount}
|
||||
min={1}
|
||||
onChange={e => dispatch(setPreferences({ ...perf, defaultZapAmount: parseInt(e.target.value || "0") }))}
|
||||
onChange={e => updatePreferences(login, { ...perf, defaultZapAmount: parseInt(e.target.value || "0") })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -189,7 +181,7 @@ const PreferencesPage = () => {
|
||||
defaultValue={perf.fastZapDonate * 100}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={e => dispatch(setPreferences({ ...perf, fastZapDonate: parseInt(e.target.value || "0") / 100 }))}
|
||||
onChange={e => updatePreferences(login, { ...perf, fastZapDonate: parseInt(e.target.value || "0") / 100 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -206,7 +198,7 @@ const PreferencesPage = () => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perf.autoZap}
|
||||
onChange={e => dispatch(setPreferences({ ...perf, autoZap: e.target.checked }))}
|
||||
onChange={e => updatePreferences(login, { ...perf, autoZap: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -225,12 +217,10 @@ const PreferencesPage = () => {
|
||||
type="checkbox"
|
||||
checked={perf.imgProxyConfig !== null}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
updatePreferences(login, {
|
||||
...perf,
|
||||
imgProxyConfig: e.target.checked ? DefaultImgProxy : null,
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -250,15 +240,13 @@ const PreferencesPage = () => {
|
||||
description: "Placeholder text for imgproxy url textbox",
|
||||
})}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
updatePreferences(login, {
|
||||
...perf,
|
||||
imgProxyConfig: {
|
||||
...unwrap(perf.imgProxyConfig),
|
||||
url: e.target.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -276,15 +264,13 @@ const PreferencesPage = () => {
|
||||
description: "Hexidecimal 'key' input for improxy",
|
||||
})}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
updatePreferences(login, {
|
||||
...perf,
|
||||
imgProxyConfig: {
|
||||
...unwrap(perf.imgProxyConfig),
|
||||
key: e.target.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -302,15 +288,13 @@ const PreferencesPage = () => {
|
||||
description: "Hexidecimal 'salt' input for imgproxy",
|
||||
})}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
updatePreferences(login, {
|
||||
...perf,
|
||||
imgProxyConfig: {
|
||||
...unwrap(perf.imgProxyConfig),
|
||||
salt: e.target.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
@ -331,7 +315,7 @@ const PreferencesPage = () => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perf.enableReactions}
|
||||
onChange={e => dispatch(setPreferences({ ...perf, enableReactions: e.target.checked }))}
|
||||
onChange={e => updatePreferences(login, { ...perf, enableReactions: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -348,12 +332,10 @@ const PreferencesPage = () => {
|
||||
className="emoji-selector"
|
||||
value={perf.reactionEmoji}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
updatePreferences(login, {
|
||||
...perf,
|
||||
reactionEmoji: e.target.value,
|
||||
} as UserPreferences)
|
||||
)
|
||||
})
|
||||
}>
|
||||
<option value="+">
|
||||
+ <FormattedMessage {...messages.Default} />
|
||||
@ -382,7 +364,7 @@ const PreferencesPage = () => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perf.confirmReposts}
|
||||
onChange={e => dispatch(setPreferences({ ...perf, confirmReposts: e.target.checked }))}
|
||||
onChange={e => updatePreferences(login, { ...perf, confirmReposts: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -399,7 +381,7 @@ const PreferencesPage = () => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perf.autoShowLatest}
|
||||
onChange={e => dispatch(setPreferences({ ...perf, autoShowLatest: e.target.checked }))}
|
||||
onChange={e => updatePreferences(login, { ...perf, autoShowLatest: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -415,12 +397,10 @@ const PreferencesPage = () => {
|
||||
<select
|
||||
value={perf.fileUploader}
|
||||
onChange={e =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
updatePreferences(login, {
|
||||
...perf,
|
||||
fileUploader: e.target.value,
|
||||
} as UserPreferences)
|
||||
)
|
||||
}>
|
||||
<option value="void.cat">
|
||||
void.cat <FormattedMessage {...messages.Default} />
|
||||
@ -444,7 +424,7 @@ const PreferencesPage = () => {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perf.showDebugMenus}
|
||||
onChange={e => dispatch(setPreferences({ ...perf, showDebugMenus: e.target.checked }))}
|
||||
onChange={e => updatePreferences(login, { ...perf, showDebugMenus: e.target.checked })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -2,22 +2,21 @@ import "./Profile.css";
|
||||
import Nostrich from "nostrich.webp";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faShop } from "@fortawesome/free-solid-svg-icons";
|
||||
import { HexKey, TaggedRawEvent } from "@snort/nostr";
|
||||
import { TaggedRawEvent } from "@snort/nostr";
|
||||
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { hexToBech32, openFile } from "Util";
|
||||
import Copy from "Element/Copy";
|
||||
import { RootState } from "State/Store";
|
||||
import useFileUpload from "Upload";
|
||||
|
||||
import messages from "./messages";
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
import { mapEventToProfile, UserCache } from "Cache";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
export interface ProfileSettingsProps {
|
||||
avatar?: boolean;
|
||||
@ -27,8 +26,7 @@ export interface ProfileSettingsProps {
|
||||
|
||||
export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
const navigate = useNavigate();
|
||||
const id = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
|
||||
const { publicKey: id, privateKey: privKey } = useLogin();
|
||||
const user = useUserProfile(id ?? "");
|
||||
const publisher = useEventPublisher();
|
||||
const uploader = useFileUpload();
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
import useRelayState from "Feed/RelayState";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { removeRelay } from "State/Login";
|
||||
import { parseId, unwrap } from "Util";
|
||||
import { System } from "System";
|
||||
import { removeRelay } from "Login";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
const RelayInfo = () => {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const login = useLogin();
|
||||
|
||||
const conn = Array.from(System.Sockets.values()).find(a => a.Id === params.id);
|
||||
const stats = useRelayState(conn?.Address ?? "");
|
||||
@ -105,7 +105,7 @@ const RelayInfo = () => {
|
||||
<div
|
||||
className="btn error"
|
||||
onClick={() => {
|
||||
dispatch(removeRelay(unwrap(conn).Address));
|
||||
removeRelay(login, unwrap(conn).Address);
|
||||
navigate("/settings/relays");
|
||||
}}>
|
||||
<FormattedMessage {...messages.Remove} />
|
||||
|
@ -1,24 +1,23 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { randomSample } from "Util";
|
||||
import { randomSample, unixNowMs } from "Util";
|
||||
import Relay from "Element/Relay";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import { RootState } from "State/Store";
|
||||
import { setRelays } from "State/Login";
|
||||
import { System } from "System";
|
||||
|
||||
import messages from "./messages";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { setRelays } from "Login";
|
||||
|
||||
const RelaySettingsPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const publisher = useEventPublisher();
|
||||
const relays = useSelector((s: RootState) => s.login.relays);
|
||||
const login = useLogin();
|
||||
const relays = login.relays;
|
||||
const [newRelay, setNewRelay] = useState<string>();
|
||||
|
||||
const otherConnections = useMemo(() => {
|
||||
return [...System.Sockets.keys()].filter(a => relays[a] === undefined);
|
||||
return [...System.Sockets.keys()].filter(a => relays.item[a] === undefined);
|
||||
}, [relays]);
|
||||
|
||||
async function saveRelays() {
|
||||
@ -69,13 +68,10 @@ const RelaySettingsPage = () => {
|
||||
if ((newRelay?.length ?? 0) > 0) {
|
||||
const parsed = new URL(newRelay ?? "");
|
||||
const payload = {
|
||||
relays: {
|
||||
...relays,
|
||||
[parsed.toString()]: { read: false, write: false },
|
||||
},
|
||||
createdAt: Math.floor(new Date().getTime() / 1000),
|
||||
...relays.item,
|
||||
[parsed.toString()]: { read: true, write: true },
|
||||
};
|
||||
dispatch(setRelays(payload));
|
||||
setRelays(login, payload, unixNowMs());
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,7 +81,7 @@ const RelaySettingsPage = () => {
|
||||
<FormattedMessage {...messages.Relays} />
|
||||
</h3>
|
||||
<div className="flex f-col mb10">
|
||||
{Object.keys(relays || {}).map(a => (
|
||||
{Object.keys(relays.item || {}).map(a => (
|
||||
<Relay addr={a} key={a} />
|
||||
))}
|
||||
</div>
|
||||
|
@ -1,537 +0,0 @@
|
||||
import { AnyAction, createSlice, PayloadAction, ThunkAction } from "@reduxjs/toolkit";
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
|
||||
import { DefaultRelays } from "Const";
|
||||
import { RelaySettings } from "@snort/nostr";
|
||||
import type { AppDispatch, RootState } from "State/Store";
|
||||
import { ImgProxySettings } from "Hooks/useImgProxy";
|
||||
import { dedupeById, sanitizeRelayUrl, unwrap } from "Util";
|
||||
import { DmCache } from "Cache";
|
||||
import { getCurrentSubscription, SubscriptionEvent } from "Subscription";
|
||||
|
||||
const PrivateKeyItem = "secret";
|
||||
const PublicKeyItem = "pubkey";
|
||||
const NotificationsReadItem = "notifications-read";
|
||||
const UserPreferencesKey = "preferences";
|
||||
const RelayListKey = "last-relays";
|
||||
const FollowList = "last-follows";
|
||||
|
||||
export interface NotificationRequest {
|
||||
title: string;
|
||||
body: string;
|
||||
icon: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
/**
|
||||
* User selected language
|
||||
*/
|
||||
language?: string;
|
||||
|
||||
/**
|
||||
* Enable reactions / reposts / zaps
|
||||
*/
|
||||
enableReactions: boolean;
|
||||
|
||||
/**
|
||||
* Reaction emoji
|
||||
*/
|
||||
reactionEmoji: string;
|
||||
|
||||
/**
|
||||
* Automatically load media (show link only) (bandwidth/privacy)
|
||||
*/
|
||||
autoLoadMedia: "none" | "follows-only" | "all";
|
||||
|
||||
/**
|
||||
* Select between light/dark theme
|
||||
*/
|
||||
theme: "system" | "light" | "dark";
|
||||
|
||||
/**
|
||||
* Ask for confirmation when reposting notes
|
||||
*/
|
||||
confirmReposts: boolean;
|
||||
|
||||
/**
|
||||
* Automatically show the latests notes
|
||||
*/
|
||||
autoShowLatest: boolean;
|
||||
|
||||
/**
|
||||
* Show debugging menus to help diagnose issues
|
||||
*/
|
||||
showDebugMenus: boolean;
|
||||
|
||||
/**
|
||||
* File uploading service to upload attachments to
|
||||
*/
|
||||
fileUploader: "void.cat" | "nostr.build" | "nostrimg.com";
|
||||
|
||||
/**
|
||||
* Use imgproxy to optimize images
|
||||
*/
|
||||
imgProxyConfig: ImgProxySettings | null;
|
||||
|
||||
/**
|
||||
* Default page to select on load
|
||||
*/
|
||||
defaultRootTab: "posts" | "conversations" | "global";
|
||||
|
||||
/**
|
||||
* Default zap amount
|
||||
*/
|
||||
defaultZapAmount: number;
|
||||
|
||||
/**
|
||||
* For each fast zap an additional X% will be sent to Snort donate address
|
||||
*/
|
||||
fastZapDonate: number;
|
||||
|
||||
/**
|
||||
* Auto-zap every post
|
||||
*/
|
||||
autoZap: boolean;
|
||||
}
|
||||
|
||||
export interface LoginStore {
|
||||
/**
|
||||
* If there is no login
|
||||
*/
|
||||
loggedOut?: boolean;
|
||||
|
||||
/**
|
||||
* Current user private key
|
||||
*/
|
||||
privateKey?: HexKey;
|
||||
|
||||
/**
|
||||
* BIP39-generated, hex-encoded entropy
|
||||
*/
|
||||
generatedEntropy?: string;
|
||||
|
||||
/**
|
||||
* Current users public key
|
||||
*/
|
||||
publicKey?: HexKey;
|
||||
|
||||
/**
|
||||
* If user generated key on snort
|
||||
*/
|
||||
newUserKey: boolean;
|
||||
|
||||
/**
|
||||
* All the logged in users relays
|
||||
*/
|
||||
relays: Record<string, RelaySettings>;
|
||||
|
||||
/**
|
||||
* Newest relay list timestamp
|
||||
*/
|
||||
latestRelays: number;
|
||||
|
||||
/**
|
||||
* A list of pubkeys this user follows
|
||||
*/
|
||||
follows: HexKey[];
|
||||
|
||||
/**
|
||||
* Newest relay list timestamp
|
||||
*/
|
||||
latestFollows: number;
|
||||
|
||||
/**
|
||||
* A list of tags this user follows
|
||||
*/
|
||||
tags: string[];
|
||||
|
||||
/**
|
||||
* Newest tag list timestamp
|
||||
*/
|
||||
latestTags: number;
|
||||
|
||||
/**
|
||||
* A list of event ids this user has pinned
|
||||
*/
|
||||
pinned: HexKey[];
|
||||
|
||||
/**
|
||||
* Last seen pinned list event timestamp
|
||||
*/
|
||||
latestPinned: number;
|
||||
|
||||
/**
|
||||
* A list of event ids this user has bookmarked
|
||||
*/
|
||||
bookmarked: HexKey[];
|
||||
|
||||
/**
|
||||
* Last seen bookmark list event timestamp
|
||||
*/
|
||||
latestBookmarked: number;
|
||||
|
||||
/**
|
||||
* A list of pubkeys this user has muted
|
||||
*/
|
||||
muted: HexKey[];
|
||||
|
||||
/**
|
||||
* Last seen mute list event timestamp
|
||||
*/
|
||||
latestMuted: number;
|
||||
|
||||
/**
|
||||
* A list of pubkeys this user has muted privately
|
||||
*/
|
||||
blocked: HexKey[];
|
||||
|
||||
/**
|
||||
* Latest notification
|
||||
*/
|
||||
latestNotification: number;
|
||||
|
||||
/**
|
||||
* Timestamp of last read notification
|
||||
*/
|
||||
readNotifications: number;
|
||||
|
||||
/**
|
||||
* Counter to trigger refresh of unread dms
|
||||
*/
|
||||
dmInteraction: 0;
|
||||
|
||||
/**
|
||||
* Users cusom preferences
|
||||
*/
|
||||
preferences: UserPreferences;
|
||||
|
||||
/**
|
||||
* Subscription events for Snort subscriptions
|
||||
*/
|
||||
subscriptions: Array<SubscriptionEvent>;
|
||||
|
||||
/**
|
||||
* Current Snort subscription
|
||||
*/
|
||||
subscription?: SubscriptionEvent;
|
||||
}
|
||||
|
||||
export const DefaultImgProxy = {
|
||||
url: "https://imgproxy.snort.social",
|
||||
key: "a82fcf26aa0ccb55dfc6b4bd6a1c90744d3be0f38429f21a8828b43449ce7cebe6bdc2b09a827311bef37b18ce35cb1e6b1c60387a254541afa9e5b4264ae942",
|
||||
salt: "a897770d9abf163de055e9617891214e75a9016d748f8ef865e6ffbcb9ed932295659549773a22a019a5f06d0b440c320be411e3fddfe784e199e4f03d74bd9b",
|
||||
};
|
||||
|
||||
export const InitState = {
|
||||
loggedOut: undefined,
|
||||
publicKey: undefined,
|
||||
privateKey: undefined,
|
||||
newUserKey: false,
|
||||
relays: {},
|
||||
latestRelays: 0,
|
||||
follows: [],
|
||||
latestFollows: 0,
|
||||
tags: [],
|
||||
latestTags: 0,
|
||||
pinned: [],
|
||||
latestPinned: 0,
|
||||
bookmarked: [],
|
||||
latestBookmarked: 0,
|
||||
muted: [],
|
||||
blocked: [],
|
||||
latestMuted: 0,
|
||||
latestNotification: 0,
|
||||
readNotifications: new Date().getTime(),
|
||||
dms: [],
|
||||
dmInteraction: 0,
|
||||
subscriptions: [],
|
||||
preferences: {
|
||||
enableReactions: true,
|
||||
reactionEmoji: "+",
|
||||
autoLoadMedia: "follows-only",
|
||||
theme: "system",
|
||||
confirmReposts: false,
|
||||
showDebugMenus: false,
|
||||
autoShowLatest: false,
|
||||
fileUploader: "void.cat",
|
||||
imgProxyConfig: DefaultImgProxy,
|
||||
defaultRootTab: "posts",
|
||||
defaultZapAmount: 50,
|
||||
fastZapDonate: 0.0,
|
||||
autoZap: false,
|
||||
},
|
||||
} as LoginStore;
|
||||
|
||||
export interface SetRelaysPayload {
|
||||
relays: Record<string, RelaySettings>;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface SetFollowsPayload {
|
||||
keys: HexKey[];
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export interface SetGeneratedKeyPayload {
|
||||
key: HexKey;
|
||||
entropy: HexKey;
|
||||
}
|
||||
|
||||
export const ReadPreferences = () => {
|
||||
const pref = window.localStorage.getItem(UserPreferencesKey);
|
||||
if (pref) {
|
||||
return JSON.parse(pref) as UserPreferences;
|
||||
}
|
||||
return InitState.preferences;
|
||||
};
|
||||
|
||||
const LoginSlice = createSlice({
|
||||
name: "Login",
|
||||
initialState: InitState,
|
||||
reducers: {
|
||||
init: state => {
|
||||
state.privateKey = window.localStorage.getItem(PrivateKeyItem) ?? undefined;
|
||||
if (state.privateKey) {
|
||||
window.localStorage.removeItem(PublicKeyItem); // reset nip07 if using private key
|
||||
state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(state.privateKey));
|
||||
state.loggedOut = false;
|
||||
} else {
|
||||
state.loggedOut = true;
|
||||
}
|
||||
|
||||
// check pub key only
|
||||
const pubKey = window.localStorage.getItem(PublicKeyItem);
|
||||
if (pubKey && !state.privateKey) {
|
||||
state.publicKey = pubKey;
|
||||
state.loggedOut = false;
|
||||
}
|
||||
|
||||
const lastRelayList = window.localStorage.getItem(RelayListKey);
|
||||
if (lastRelayList) {
|
||||
state.relays = JSON.parse(lastRelayList);
|
||||
} else {
|
||||
state.relays = Object.fromEntries(
|
||||
[...DefaultRelays.entries()].map(a => [unwrap(sanitizeRelayUrl(a[0])), a[1]])
|
||||
);
|
||||
}
|
||||
|
||||
const lastFollows = window.localStorage.getItem(FollowList);
|
||||
if (lastFollows) {
|
||||
state.follows = JSON.parse(lastFollows);
|
||||
}
|
||||
|
||||
// notifications
|
||||
const readNotif = parseInt(window.localStorage.getItem(NotificationsReadItem) ?? "0");
|
||||
if (!isNaN(readNotif)) {
|
||||
state.readNotifications = readNotif;
|
||||
}
|
||||
|
||||
// preferences
|
||||
const pref = ReadPreferences();
|
||||
state.preferences = pref;
|
||||
},
|
||||
setPrivateKey: (state, action: PayloadAction<HexKey>) => {
|
||||
state.loggedOut = false;
|
||||
state.privateKey = action.payload;
|
||||
window.localStorage.setItem(PrivateKeyItem, action.payload);
|
||||
state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(action.payload));
|
||||
},
|
||||
setGeneratedPrivateKey: (state, action: PayloadAction<SetGeneratedKeyPayload>) => {
|
||||
state.loggedOut = false;
|
||||
state.newUserKey = true;
|
||||
state.privateKey = action.payload.key;
|
||||
state.generatedEntropy = action.payload.entropy;
|
||||
window.localStorage.setItem(PrivateKeyItem, action.payload.key);
|
||||
state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(action.payload.key));
|
||||
},
|
||||
clearEntropy: state => {
|
||||
state.generatedEntropy = undefined;
|
||||
},
|
||||
setPublicKey: (state, action: PayloadAction<HexKey>) => {
|
||||
window.localStorage.setItem(PublicKeyItem, action.payload);
|
||||
state.loggedOut = false;
|
||||
state.publicKey = action.payload;
|
||||
},
|
||||
setRelays: (state, action: PayloadAction<SetRelaysPayload>) => {
|
||||
const relays = action.payload.relays;
|
||||
const createdAt = action.payload.createdAt;
|
||||
if (state.latestRelays > createdAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
// filter out non-websocket urls
|
||||
const filtered = new Map<string, RelaySettings>();
|
||||
for (const [k, v] of Object.entries(relays)) {
|
||||
if (k.startsWith("wss://") || k.startsWith("ws://")) {
|
||||
const url = sanitizeRelayUrl(k);
|
||||
if (url) {
|
||||
filtered.set(url, v as RelaySettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.relays = Object.fromEntries(filtered.entries());
|
||||
state.latestRelays = createdAt;
|
||||
window.localStorage.setItem(RelayListKey, JSON.stringify(state.relays));
|
||||
},
|
||||
removeRelay: (state, action: PayloadAction<string>) => {
|
||||
delete state.relays[action.payload];
|
||||
state.relays = { ...state.relays };
|
||||
window.localStorage.setItem(RelayListKey, JSON.stringify(state.relays));
|
||||
},
|
||||
setFollows: (state, action: PayloadAction<SetFollowsPayload>) => {
|
||||
const { keys, createdAt } = action.payload;
|
||||
if (state.latestFollows > createdAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = new Set(state.follows);
|
||||
const update = Array.isArray(keys) ? keys : [keys];
|
||||
|
||||
let changes = false;
|
||||
for (const pk of update.filter(a => a.length === 64)) {
|
||||
if (!existing.has(pk)) {
|
||||
existing.add(pk);
|
||||
changes = true;
|
||||
}
|
||||
}
|
||||
for (const pk of existing) {
|
||||
if (!update.includes(pk)) {
|
||||
existing.delete(pk);
|
||||
changes = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (changes) {
|
||||
state.follows = Array.from(existing);
|
||||
state.latestFollows = createdAt;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(FollowList, JSON.stringify(state.follows));
|
||||
},
|
||||
setTags(state, action: PayloadAction<{ createdAt: number; tags: string[] }>) {
|
||||
const { createdAt, tags } = action.payload;
|
||||
if (createdAt >= state.latestTags) {
|
||||
const newTags = new Set([...tags]);
|
||||
state.tags = Array.from(newTags);
|
||||
state.latestTags = createdAt;
|
||||
}
|
||||
},
|
||||
setMuted(state, action: PayloadAction<{ createdAt: number; keys: HexKey[] }>) {
|
||||
const { createdAt, keys } = action.payload;
|
||||
if (createdAt >= state.latestMuted) {
|
||||
const muted = new Set([...keys]);
|
||||
state.muted = Array.from(muted);
|
||||
state.latestMuted = createdAt;
|
||||
}
|
||||
},
|
||||
setPinned(state, action: PayloadAction<{ createdAt: number; keys: HexKey[] }>) {
|
||||
const { createdAt, keys } = action.payload;
|
||||
if (createdAt >= state.latestPinned) {
|
||||
const pinned = new Set([...keys]);
|
||||
state.pinned = Array.from(pinned);
|
||||
state.latestPinned = createdAt;
|
||||
}
|
||||
},
|
||||
setBookmarked(state, action: PayloadAction<{ createdAt: number; keys: HexKey[] }>) {
|
||||
const { createdAt, keys } = action.payload;
|
||||
if (createdAt >= state.latestBookmarked) {
|
||||
const bookmarked = new Set([...keys]);
|
||||
state.bookmarked = Array.from(bookmarked);
|
||||
state.latestBookmarked = createdAt;
|
||||
}
|
||||
},
|
||||
setBlocked(state, action: PayloadAction<{ createdAt: number; keys: HexKey[] }>) {
|
||||
const { createdAt, keys } = action.payload;
|
||||
if (createdAt >= state.latestMuted) {
|
||||
const blocked = new Set([...keys]);
|
||||
state.blocked = Array.from(blocked);
|
||||
state.latestMuted = createdAt;
|
||||
}
|
||||
},
|
||||
incDmInteraction: state => {
|
||||
state.dmInteraction += 1;
|
||||
},
|
||||
logout: (state, payload: PayloadAction<() => void>) => {
|
||||
const relays = { ...state.relays };
|
||||
state = Object.assign(state, InitState);
|
||||
state.loggedOut = true;
|
||||
window.localStorage.clear();
|
||||
state.relays = relays;
|
||||
window.localStorage.setItem(RelayListKey, JSON.stringify(relays));
|
||||
queueMicrotask(async () => {
|
||||
await DmCache.clear();
|
||||
payload.payload();
|
||||
});
|
||||
},
|
||||
markNotificationsRead: state => {
|
||||
state.readNotifications = Math.ceil(new Date().getTime() / 1000);
|
||||
window.localStorage.setItem(NotificationsReadItem, state.readNotifications.toString());
|
||||
},
|
||||
setLatestNotifications: (state, action: PayloadAction<number>) => {
|
||||
state.latestNotification = action.payload;
|
||||
},
|
||||
setPreferences: (state, action: PayloadAction<UserPreferences>) => {
|
||||
state.preferences = action.payload;
|
||||
window.localStorage.setItem(UserPreferencesKey, JSON.stringify(state.preferences));
|
||||
},
|
||||
addSubscription: (state, action: PayloadAction<Array<SubscriptionEvent>>) => {
|
||||
state.subscriptions = dedupeById([...state.subscriptions, ...action.payload]);
|
||||
state.subscription = getCurrentSubscription(state.subscriptions);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const {
|
||||
init,
|
||||
setPrivateKey,
|
||||
setGeneratedPrivateKey,
|
||||
clearEntropy,
|
||||
setPublicKey,
|
||||
setRelays,
|
||||
removeRelay,
|
||||
setFollows,
|
||||
setTags,
|
||||
setMuted,
|
||||
setPinned,
|
||||
setBookmarked,
|
||||
setBlocked,
|
||||
incDmInteraction,
|
||||
logout,
|
||||
markNotificationsRead,
|
||||
setLatestNotifications,
|
||||
setPreferences,
|
||||
addSubscription,
|
||||
} = LoginSlice.actions;
|
||||
|
||||
export function sendNotification({
|
||||
title,
|
||||
body,
|
||||
icon,
|
||||
timestamp,
|
||||
}: NotificationRequest): ThunkAction<void, RootState, undefined, AnyAction> {
|
||||
return async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const { readNotifications } = state.login;
|
||||
const hasPermission = "Notification" in window && Notification.permission === "granted";
|
||||
const shouldShowNotification = hasPermission && timestamp > readNotifications;
|
||||
if (shouldShowNotification) {
|
||||
try {
|
||||
const worker = await navigator.serviceWorker.ready;
|
||||
worker.showNotification(title, {
|
||||
tag: "notification",
|
||||
vibrate: [500],
|
||||
body,
|
||||
icon,
|
||||
timestamp,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const reducer = LoginSlice.reducer;
|
@ -1,10 +1,8 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { reducer as LoginReducer } from "State/Login";
|
||||
import { reducer as NoteCreatorReducer } from "State/NoteCreator";
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
login: LoginReducer,
|
||||
noteCreator: NoteCreatorReducer,
|
||||
},
|
||||
});
|
||||
|
@ -1,8 +1,7 @@
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import Icon from "Icons/Icon";
|
||||
import { useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "State/Store";
|
||||
import { UITask } from "Tasks";
|
||||
import { Nip5Task } from "./Nip5Task";
|
||||
|
||||
@ -10,7 +9,7 @@ const AllTasks: Array<UITask> = [new Nip5Task()];
|
||||
AllTasks.forEach(a => a.load());
|
||||
|
||||
export const TaskList = () => {
|
||||
const publicKey = useSelector((s: RootState) => s.login.publicKey);
|
||||
const publicKey = useLogin().publicKey;
|
||||
const user = useUserProfile(publicKey);
|
||||
const [, setTick] = useState<number>(0);
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "State/Store";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import NostrBuild from "Upload/NostrBuild";
|
||||
import VoidCat from "Upload/VoidCat";
|
||||
import NostrImg from "./NostrImg";
|
||||
@ -14,7 +13,7 @@ export interface Uploader {
|
||||
}
|
||||
|
||||
export default function useFileUpload(): Uploader {
|
||||
const fileUploader = useSelector((s: RootState) => s.login.preferences.fileUploader);
|
||||
const fileUploader = useLogin().preferences.fileUploader;
|
||||
|
||||
switch (fileUploader) {
|
||||
case "nostr.build": {
|
||||
|
@ -154,6 +154,14 @@ export function unixNowMs() {
|
||||
return new Date().getTime();
|
||||
}
|
||||
|
||||
export function deepClone<T>(obj: T) {
|
||||
if ("structuredClone" in window) {
|
||||
return structuredClone(obj);
|
||||
} else {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple debounce
|
||||
*/
|
||||
|
Loading…
x
Reference in New Issue
Block a user