feat: UserState

This commit is contained in:
2024-04-22 14:38:14 +01:00
parent 5a7657a95d
commit 80a4b5d8e6
103 changed files with 4179 additions and 1165 deletions

View File

@ -20,6 +20,8 @@ import { Toastore } from "@/Components/Toaster/Toaster";
import ProfileImage from "@/Components/User/ProfileImage";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import useRelays from "@/Hooks/useRelays";
import { useNoteCreator } from "@/State/NoteCreator";
import { openFile, trackEvent } from "@/Utils";
import useFileUpload from "@/Utils/Upload";
@ -56,11 +58,12 @@ const quoteNoteOptions = {
export function NoteCreator() {
const { formatMessage } = useIntl();
const uploader = useFileUpload();
const login = useLogin(s => ({ relays: s.relays, publicKey: s.publicKey, pow: s.appData.json.preferences.pow }));
const publicKey = useLogin(s => s.publicKey);
const pow = usePreferences(s => s.pow);
const relays = useRelays();
const { system, publisher: pub } = useEventPublisher();
const publisher = login.pow ? pub?.pow(login.pow, GetPowWorker()) : pub;
const publisher = pow ? pub?.pow(pow, GetPowWorker()) : pub;
const note = useNoteCreator();
const relays = login.relays;
useEffect(() => {
const draft = localStorage.getItem("msgDraft");
@ -367,8 +370,9 @@ export function NoteCreator() {
function renderRelayCustomisation() {
return (
<div className="flex flex-col g8">
{Object.keys(relays.item || {})
.filter(el => relays.item[el].write)
{Object.entries(relays)
.filter(el => el[1].write)
.map(a => a[0])
.map((r, i, a) => (
<div className="p flex justify-between note-creator-relay" key={r}>
<div>{r}</div>
@ -523,7 +527,7 @@ export function NoteCreator() {
<div className="flex justify-between">
<div className="flex items-center g8">
<ProfileImage
pubkey={login.publicKey ?? ""}
pubkey={publicKey ?? ""}
className="note-creator-icon"
link=""
showUsername={false}

View File

@ -8,21 +8,16 @@ import IconButton from "@/Components/Button/IconButton";
import Icon from "@/Components/Icons/Icon";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import { saveRelays } from "@/Pages/settings/saveRelays";
import { getRelayName } from "@/Utils";
import { removeRelay } from "@/Utils/Login";
export function OkResponseRow({ rsp, close }: { rsp: OkResponse; close: () => void }) {
const [r, setResult] = useState(rsp);
const { formatMessage } = useIntl();
const { publisher, system } = useEventPublisher();
const { system } = useEventPublisher();
const login = useLogin();
async function removeRelayFromResult(r: OkResponse) {
if (publisher) {
removeRelay(login, unwrap(sanitizeRelayUrl(r.relay)));
await saveRelays(system, publisher, login.relays.item);
}
await login.state.removeRelay(unwrap(sanitizeRelayUrl(r.relay)), true);
close();
}

View File

@ -1,10 +1,10 @@
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
const HiddenNote = ({ children }: { children: React.ReactNode }) => {
const hideMutedNotes = useLogin(s => s.appData.json.preferences.hideMutedNotes);
const hideMutedNotes = usePreferences(s => s.hideMutedNotes);
const [show, setShow] = useState(false);
if (hideMutedNotes) return;

View File

@ -1,4 +1,4 @@
import { HexKey, NostrLink, NostrPrefix } from "@snort/system";
import { EventKind, HexKey, NostrLink, NostrPrefix } from "@snort/system";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -10,7 +10,7 @@ import SnortApi from "@/External/SnortApi";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import useModeration from "@/Hooks/useModeration";
import { setBookmarked, setPinned } from "@/Utils/Login";
import usePreferences from "@/Hooks/usePreferences";
import { getCurrentSubscription, SubscriptionType } from "@/Utils/Subscription";
import { ReBroadcaster } from "../../ReBroadcaster";
@ -18,7 +18,8 @@ import { ReBroadcaster } from "../../ReBroadcaster";
export function NoteContextMenu({ ev, ...props }: NoteContextMenuProps) {
const { formatMessage } = useIntl();
const login = useLogin();
const { mute, block } = useModeration();
const autoTranslate = usePreferences(s => s.autoTranslate);
const { mute } = useModeration();
const { publisher, system } = useEventPublisher();
const [showBroadcast, setShowBroadcast] = useState(false);
const lang = window.navigator.language;
@ -26,6 +27,7 @@ export function NoteContextMenu({ ev, ...props }: NoteContextMenuProps) {
type: "language",
});
const isMine = ev.pubkey === login.publicKey;
const link = NostrLink.fromEvent(ev);
async function deleteEvent() {
if (window.confirm(formatMessage(messages.ConfirmDeletion, { id: ev.id.substring(0, 8) })) && publisher) {
@ -78,7 +80,7 @@ export function NoteContextMenu({ ev, ...props }: NoteContextMenuProps) {
useEffect(() => {
const sub = getCurrentSubscription(login.subscriptions);
if (sub?.type === SubscriptionType.Premium && (login.appData.json.preferences.autoTranslate ?? true)) {
if (sub?.type === SubscriptionType.Premium && (autoTranslate ?? true)) {
translate();
}
}, []);
@ -90,19 +92,13 @@ export function NoteContextMenu({ ev, ...props }: NoteContextMenuProps) {
async function pin(id: HexKey) {
if (publisher) {
const es = [...login.pinned.item, id];
const ev = await publisher.pinned(es.map(a => new NostrLink(NostrPrefix.Note, a)));
system.BroadcastEvent(ev);
setPinned(login, es, ev.created_at * 1000);
//todo: PIN note
}
}
async function bookmark(id: string) {
if (publisher) {
const es = [...login.bookmarked.item, id];
const ev = await publisher.bookmarks(es.map(a => new NostrLink(NostrPrefix.Note, a)));
system.BroadcastEvent(ev);
setBookmarked(login, es, ev.created_at * 1000);
//todo: bookmark note
}
}
@ -132,13 +128,13 @@ export function NoteContextMenu({ ev, ...props }: NoteContextMenuProps) {
<Icon name="share" />
<FormattedMessage {...messages.Share} />
</MenuItem>
{!login.pinned.item.includes(ev.id) && !login.readonly && (
{!login.state.isOnList(EventKind.PinList, link) && !login.readonly && (
<MenuItem onClick={() => pin(ev.id)}>
<Icon name="pin" />
<FormattedMessage {...messages.Pin} />
</MenuItem>
)}
{!login.bookmarked.item.includes(ev.id) && !login.readonly && (
{!login.state.isOnList(EventKind.BookmarksList, link) && !login.readonly && (
<MenuItem onClick={() => bookmark(ev.id)}>
<Icon name="bookmark" />
<FormattedMessage {...messages.Bookmark} />
@ -158,12 +154,6 @@ export function NoteContextMenu({ ev, ...props }: NoteContextMenuProps) {
<Icon name="relay" />
<FormattedMessage defaultMessage="Broadcast Event" id="Gxcr08" />
</MenuItem>
{ev.pubkey !== login.publicKey && !login.readonly && (
<MenuItem onClick={() => block(ev.pubkey)}>
<Icon name="block" />
<FormattedMessage {...messages.Block} />
</MenuItem>
)}
<MenuItem onClick={() => translate()}>
<Icon name="translate" />
<FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} />

View File

@ -11,6 +11,7 @@ import { ZapsSummary } from "@/Components/Event/ZapsSummary";
import ZapModal from "@/Components/ZapModal/ZapModal";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import { getDisplayName } from "@/Utils";
import { Zapper, ZapTarget } from "@/Utils/Zapper";
import { ZapPoolController } from "@/Utils/ZapPoolController";
@ -23,15 +24,11 @@ export interface ZapIconProps {
}
export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
const {
publicKey,
readonly,
preferences: prefs,
} = useLogin(s => ({
const { publicKey, readonly } = useLogin(s => ({
publicKey: s.publicKey,
readonly: s.readonly,
preferences: s.appData.json.preferences,
}));
const preferences = usePreferences(s => ({ autoZap: s.autoZap, defaultZapAmount: s.defaultZapAmount }));
const walletState = useWallet();
const wallet = walletState.wallet;
const link = NostrLink.fromEvent(ev);
@ -75,7 +72,7 @@ export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
if (canFastZap && lnurl) {
setZapping(true);
try {
await fastZapInner(lnurl, prefs.defaultZapAmount);
await fastZapInner(lnurl, preferences.defaultZapAmount);
} catch (e) {
console.warn("Fast zap failed", e);
if (!(e instanceof Error) || e.message !== "User rejected") {
@ -110,13 +107,13 @@ export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
const targets = getZapTarget();
useEffect(() => {
if (prefs.autoZap && !didZap && !isMine && !zapping) {
if (preferences.autoZap && !didZap && !isMine && !zapping) {
const lnurl = getZapTarget();
if (wallet?.isReady() && lnurl) {
setZapping(true);
queueMicrotask(async () => {
try {
await fastZapInner(lnurl, prefs.defaultZapAmount);
await fastZapInner(lnurl, preferences.defaultZapAmount);
} catch {
// ignored
} finally {
@ -125,7 +122,7 @@ export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
});
}
}
}, [prefs.autoZap, author, zapping]);
}, [preferences.autoZap, author, zapping]);
return (
<>

View File

@ -9,6 +9,7 @@ import { ReplyButton } from "@/Components/Event/Note/NoteFooter/ReplyButton";
import { RepostButton } from "@/Components/Event/Note/NoteFooter/RepostButton";
import ReactionsModal from "@/Components/Event/Note/ReactionsModal";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
export interface NoteFooterProps {
replyCount?: number;
@ -25,17 +26,14 @@ export default function NoteFooter(props: NoteFooterProps) {
const { replies, reactions, zaps, reposts } = useEventReactions(link, related);
const { positive } = reactions;
const { preferences: prefs, readonly } = useLogin(s => ({
preferences: s.appData.json.preferences,
publicKey: s.publicKey,
readonly: s.readonly,
}));
const readonly = useLogin(s => s.readonly);
const enableReactions = usePreferences(s => s.enableReactions);
return (
<div className="flex flex-row gap-4 overflow-hidden max-w-full h-6 items-center">
<ReplyButton ev={ev} replyCount={props.replyCount ?? replies.length} readonly={readonly} />
<RepostButton ev={ev} reposts={reposts} />
{prefs.enableReactions && <LikeButton ev={ev} positiveReactions={positive} />}
{enableReactions && <LikeButton ev={ev} positiveReactions={positive} />}
{CONFIG.showPowIcon && <PowIcon ev={ev} />}
<FooterZapButton ev={ev} zaps={zaps} onClickZappers={() => setShowReactions(true)} />
{showReactions && <ReactionsModal initialTab={1} onClose={() => setShowReactions(false)} event={ev} />}

View File

@ -9,16 +9,15 @@ import Icon from "@/Components/Icons/Icon";
import messages from "@/Components/messages";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import { useNoteCreator } from "@/State/NoteCreator";
export const RepostButton = ({ ev, reposts }: { ev: TaggedNostrEvent; reposts: TaggedNostrEvent[] }) => {
const { formatMessage } = useIntl();
const navigate = useNavigate();
const { publisher, system } = useEventPublisher();
const { publicKey, preferences: prefs } = useLogin(s => ({
preferences: s.appData.json.preferences,
publicKey: s.publicKey,
}));
const publicKey = useLogin(s => s.publicKey);
const confirmReposts = usePreferences(s => s.confirmReposts);
const note = useNoteCreator(n => ({ show: n.show, replyTo: n.replyTo, update: n.update, quote: n.quote }));
const hasReposted = () => {
@ -27,7 +26,7 @@ export const RepostButton = ({ ev, reposts }: { ev: TaggedNostrEvent; reposts: T
const repost = async () => {
if (!hasReposted() && publisher) {
if (!prefs.confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) {
if (!confirmReposts || window.confirm(formatMessage(messages.ConfirmRepost, { id: ev.id }))) {
const evRepost = await publisher.repost(ev);
system.BroadcastEvent(evRepost);
}

View File

@ -1,4 +1,4 @@
import { HexKey, NostrLink, NostrPrefix, TaggedNostrEvent } from "@snort/system";
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
import React, { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -13,7 +13,6 @@ import messages from "@/Components/messages";
import ProfileImage from "@/Components/User/ProfileImage";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import { setBookmarked, setPinned } from "@/Utils/Login";
export default function NoteHeader(props: {
ev: TaggedNostrEvent;
@ -24,28 +23,21 @@ export default function NoteHeader(props: {
const [showReactions, setShowReactions] = useState(false);
const { ev, options, setTranslated } = props;
const { formatMessage } = useIntl();
const { pinned, bookmarked } = useLogin();
const { publisher, system } = useEventPublisher();
const { publisher } = useEventPublisher();
const login = useLogin();
async function unpin(id: HexKey) {
async function unpin() {
if (options.canUnpin && publisher) {
if (window.confirm(formatMessage(messages.ConfirmUnpin))) {
const es = pinned.item.filter(e => e !== id);
const ev = await publisher.pinned(es.map(a => new NostrLink(NostrPrefix.Note, a)));
system.BroadcastEvent(ev);
setPinned(login, es, ev.created_at * 1000);
await login.state.removeFromList(EventKind.PinList, NostrLink.fromEvent(ev));
}
}
}
async function unbookmark(id: HexKey) {
async function unbookmark() {
if (options.canUnbookmark && publisher) {
if (window.confirm(formatMessage(messages.ConfirmUnbookmark))) {
const es = bookmarked.item.filter(e => e !== id);
const ev = await publisher.pinned(es.map(a => new NostrLink(NostrPrefix.Note, a)));
system.BroadcastEvent(ev);
setBookmarked(login, es, ev.created_at * 1000);
await login.state.removeFromList(EventKind.BookmarksList, NostrLink.fromEvent(ev));
}
}
}
@ -66,7 +58,7 @@ export default function NoteHeader(props: {
{(options.showTime || options.showBookmarked) && (
<>
{options.showBookmarked && (
<div className={`saved ${options.canUnbookmark ? "pointer" : ""}`} onClick={() => unbookmark(ev.id)}>
<div className={`saved ${options.canUnbookmark ? "pointer" : ""}`} onClick={() => unbookmark()}>
<Icon name="bookmark" /> <FormattedMessage {...messages.Bookmarked} />
</div>
)}
@ -74,7 +66,7 @@ export default function NoteHeader(props: {
</>
)}
{options.showPinned && (
<div className={`pinned ${options.canUnpin ? "pointer" : ""}`} onClick={() => unpin(ev.id)}>
<div className={`pinned ${options.canUnpin ? "pointer" : ""}`} onClick={() => unpin()}>
<Icon name="pin" /> <FormattedMessage {...messages.Pinned} />
</div>
)}

View File

@ -6,14 +6,14 @@ import { NoteProps } from "@/Components/Event/EventComponent";
import { NoteTranslation } from "@/Components/Event/Note/types";
import Reveal from "@/Components/Event/Reveal";
import Text from "@/Components/Text/Text";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
const TEXT_TRUNCATE_LENGTH = 400;
export const NoteText = memo(function InnerContent(
props: NoteProps & { translated: NoteTranslation; showTranslation?: boolean },
) {
const { data: ev, options, translated, showTranslation } = props;
const appData = useLogin(s => s.appData);
const showContentWarningPosts = usePreferences(s => s.showContentWarningPosts);
const [showMore, setShowMore] = useState(false);
const body = translated && !translated.skipped && showTranslation ? translated.text : ev?.content ?? "";
const id = translated && !translated.skipped && showTranslation ? `${ev.id}-translated` : ev.id;
@ -53,7 +53,7 @@ export const NoteText = memo(function InnerContent(
</>
);
if (!appData.json.showContentWarningPosts) {
if (!showContentWarningPosts) {
const contentWarning = ev.tags.find(a => a[0] === "content-warning");
if (contentWarning) {
return (

View File

@ -8,6 +8,7 @@ import Spinner from "@/Components/Icons/Spinner";
import ZapModal from "@/Components/ZapModal/ZapModal";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import { unwrap } from "@/Utils";
import { formatShort } from "@/Utils/Number";
import { useWallet } from "@/Wallet";
@ -21,13 +22,10 @@ type PollTally = "zaps" | "pubkeys";
export default function Poll(props: PollProps) {
const { formatMessage } = useIntl();
const { publisher } = useEventPublisher();
const { publisher, system } = useEventPublisher();
const { wallet } = useWallet();
const {
preferences: prefs,
publicKey: myPubKey,
relays,
} = useLogin(s => ({ preferences: s.appData.json.preferences, publicKey: s.publicKey, relays: s.relays }));
const defaultZapAmount = usePreferences(s => s.defaultZapAmount);
const myPubKey = useLogin(s => s.publicKey);
const pollerProfile = useUserProfile(props.ev.pubkey);
const [tallyBy, setTallyBy] = useState<PollTally>("pubkeys");
const [error, setError] = useState("");
@ -45,7 +43,7 @@ export default function Poll(props: PollProps) {
ev.stopPropagation();
if (voting || !publisher) return;
const amount = prefs.defaultZapAmount;
const amount = defaultZapAmount;
try {
if (amount <= 0) {
throw new Error(
@ -62,9 +60,14 @@ export default function Poll(props: PollProps) {
}
setVoting(opt);
const r = Object.keys(relays.item);
const zap = await publisher.zap(amount * 1000, props.ev.pubkey, r, NostrLink.fromEvent(props.ev), undefined, eb =>
eb.tag(["poll_option", opt.toString()]),
const r = await system.requestRouter?.forReplyTo(props.ev.pubkey);
const zap = await publisher.zap(
amount * 1000,
props.ev.pubkey,
r ?? [],
NostrLink.fromEvent(props.ev),
undefined,
eb => eb.tag(["poll_option", opt.toString()]),
);
const lnurl = props.ev.tags.find(a => a[0] === "zap")?.[1] || pollerProfile?.lud16 || pollerProfile?.lud06;
@ -121,7 +124,7 @@ export default function Poll(props: PollProps) {
defaultMessage="You are voting with {amount} sats"
id="3qnJlS"
values={{
amount: formatShort(prefs.defaultZapAmount),
amount: formatShort(defaultZapAmount),
}}
/>
</small>

View File

@ -6,6 +6,7 @@ import { MediaElement } from "@/Components/Embed/MediaElement";
import Reveal from "@/Components/Event/Reveal";
import useFollowsControls from "@/Hooks/useFollowControls";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import { FileExtensionRegex } from "@/Utils/Const";
interface RevealMediaProps {
@ -17,15 +18,13 @@ interface RevealMediaProps {
}
export default function RevealMedia(props: RevealMediaProps) {
const { preferences, publicKey } = useLogin(s => ({
preferences: s.appData.json.preferences,
publicKey: s.publicKey,
}));
const publicKey = useLogin(s => s.publicKey);
const autoLoadMedia = usePreferences(s => s.autoLoadMedia);
const { isFollowing } = useFollowsControls();
const hideNonFollows = preferences.autoLoadMedia === "follows-only" && !isFollowing(props.creator);
const hideNonFollows = autoLoadMedia === "follows-only" && !isFollowing(props.creator);
const isMine = props.creator === publicKey;
const hideMedia = preferences.autoLoadMedia === "none" || (!isMine && hideNonFollows);
const hideMedia = autoLoadMedia === "none" || (!isMine && hideNonFollows);
const hostname = new URL(props.link).hostname;
const url = new URL(props.link);

View File

@ -123,8 +123,6 @@ export function Thread(props: { onBack?: () => void; disableSpotlight?: boolean
<pre>{JSON.stringify(thread.root, undefined, " ")}</pre>
<h1>Data</h1>
<pre>{JSON.stringify(thread.data, undefined, " ")}</pre>
<h1>Reactions</h1>
<pre>{JSON.stringify(thread.reactions, undefined, " ")}</pre>
</div>
)}
{parent && (