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