feat: multi-account system

This commit is contained in:
2023-04-14 12:33:19 +01:00
parent 723589fec7
commit fe788853c9
58 changed files with 966 additions and 1080 deletions

View File

@ -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]);

View File

@ -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());
}
}

View File

@ -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) {

View File

@ -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(() => {
navigate("/");
})
);
logout(publicKey);
navigate("/");
}}>
<FormattedMessage {...messages.Logout} />
</button>

View File

@ -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 />

View File

@ -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>;

View File

@ -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]);

View File

@ -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);
publisher.broadcast(ev);
dispatch(setPinned({ keys: es, createdAt: new Date().getTime() }));
if (ev) {
publisher.broadcast(ev);
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);
publisher.broadcast(ev);
dispatch(setBookmarked({ keys: es, createdAt: new Date().getTime() }));
if (ev) {
publisher.broadcast(ev);
setBookmarked(login, es, ev.created_at * 1000);
}
}
}
}

View File

@ -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);
publisher.broadcast(ev);
dispatch(setPinned({ keys: es, createdAt: new Date().getTime() }));
if (ev) {
publisher.broadcast(ev);
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);
publisher.broadcast(ev);
dispatch(setBookmarked({ keys: es, createdAt: new Date().getTime() }));
if (ev) {
publisher.broadcast(ev);
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} />

View File

@ -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("");

View File

@ -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,
[props.addr]: o,
},
createdAt: Math.floor(new Date().getTime() / 1000),
})
setRelays(
login,
{
...login.relays.item,
[props.addr]: o,
},
unixNowMs()
);
}

View File

@ -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;

View File

@ -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: "👍",

View File

@ -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">