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

@ -1,10 +1,10 @@
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import { MixCloudRegex } from "@/Utils/Const";
const MixCloudEmbed = ({ link }: { link: string }) => {
const feedPath = (MixCloudRegex.test(link) && RegExp.$1) + "%2F" + (MixCloudRegex.test(link) && RegExp.$2);
const theme = useLogin(s => s.appData.json.preferences.theme);
const theme = usePreferences(s => s.theme);
const lightParams = theme === "light" ? "light=1" : "light=0";
return (
<>

View File

@ -8,30 +8,31 @@ import AsyncButton from "@/Components/Button/AsyncButton";
import { Toastore } from "@/Components/Toaster/Toaster";
import FollowListBase from "@/Components/User/FollowListBase";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import { dedupe, findTag, getDisplayName, hexToBech32 } from "@/Utils";
import { useWallet } from "@/Wallet";
export default function PubkeyList({ ev, className }: { ev: NostrEvent; className?: string }) {
const wallet = useWallet();
const login = useLogin();
const { publisher } = useEventPublisher();
const defaultZapAmount = usePreferences(s => s.defaultZapAmount);
const { publisher, system } = useEventPublisher();
const ids = dedupe(ev.tags.filter(a => a[0] === "p").map(a => a[1]));
async function zapAll() {
for (const pk of ids) {
try {
const profile = await UserCache.get(pk);
const amtSend = login.appData.json.preferences.defaultZapAmount;
const amtSend = defaultZapAmount;
const lnurl = profile?.lud16 || profile?.lud06;
if (lnurl) {
const svc = new LNURL(lnurl);
await svc.load();
const relays = await system.requestRouter?.forReplyTo(pk);
const zap = await publisher?.zap(
amtSend * 1000,
pk,
Object.keys(login.relays.item),
relays ?? [],
undefined,
`Zap from ${hexToBech32("note", ev.id)}`,
);
@ -74,7 +75,7 @@ export default function PubkeyList({ ev, className }: { ev: NostrEvent; classNam
defaultMessage="Zap all {n} sats"
id="IVbtTS"
values={{
n: <FormattedNumber value={login.appData.json.preferences.defaultZapAmount * ids.length} />,
n: <FormattedNumber value={defaultZapAmount * ids.length} />,
}}
/>
</AsyncButton>

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 && (

View File

@ -3,9 +3,8 @@ import { FormattedMessage } from "react-intl";
import Icon from "@/Components/Icons/Icon";
import { RootTabRoutePath } from "@/Pages/Root/RootTabRoutes";
import { Newest } from "@/Utils/Login";
export function rootTabItems(base: string, pubKey: string | undefined, tags: Newest<Array<string>>) {
export function rootTabItems(base: string, pubKey: string | undefined, tags: Array<string>) {
const menuItems = [
{
tab: "for-you",
@ -98,7 +97,7 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: New
{
tab: "tags",
path: `${base}/topics`,
show: tags.item.length > 0,
show: tags.length > 0,
element: (
<>
<Icon name="hash" />

View File

@ -7,26 +7,26 @@ import { useLocation, useNavigate } from "react-router-dom";
import { rootTabItems } from "@/Components/Feed/RootTabItems";
import Icon from "@/Components/Icons/Icon";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import { RootTabRoutePath } from "@/Pages/Root/RootTabRoutes";
import { EventKind } from "@snort/system";
import { unwrap } from "@snort/shared";
export function RootTabs({ base = "/" }: { base: string }) {
const navigate = useNavigate();
const location = useLocation();
const {
publicKey: pubKey,
tags,
preferences,
} = useLogin(s => ({
const { publicKey: pubKey, tags } = useLogin(s => ({
publicKey: s.publicKey,
tags: s.tags,
preferences: s.appData.json.preferences,
tags: s.state.getList(EventKind.InterestSet),
}));
const defaultRootTab = usePreferences(s => s.defaultRootTab);
const menuItems = useMemo(() => rootTabItems(base, pubKey, tags), [base, pubKey, tags]);
const hashTags = tags.filter(a => a.toEventTag()?.[0] === "t").map(a => unwrap(a.toEventTag())[1]);
const menuItems = useMemo(() => rootTabItems(base, pubKey, hashTags), [base, pubKey, tags]);
let defaultTab: RootTabRoutePath;
if (pubKey) {
defaultTab = preferences.defaultRootTab;
defaultTab = defaultRootTab;
} else {
defaultTab = `trending/notes`;
}

View File

@ -26,7 +26,11 @@ export interface TimelineFollowsProps {
* A list of notes by "subject"
*/
const TimelineFollows = (props: TimelineFollowsProps) => {
const login = useLogin();
const login = useLogin(s => ({
publicKey: s.publicKey,
feedDisplayAs: s.feedDisplayAs,
tags: s.state.getList(EventKind.InterestSet),
}));
const displayAsInitial = props.displayAs ?? login.feedDisplayAs ?? "list";
const [displayAs, setDisplayAs] = useState<DisplayAs>(displayAsInitial);
const [openedAt] = useHistoryState(Math.floor(Date.now() / 1000), "openedAt");
@ -38,12 +42,12 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
items: followList,
discriminator: login.publicKey?.slice(0, 12),
extra: rb => {
if (login.tags.item.length > 0) {
rb.withFilter().kinds([EventKind.TextNote, EventKind.Repost]).tag("t", login.tags.item);
if (login.tags.length > 0) {
rb.withFilter().kinds([EventKind.TextNote, EventKind.Repost]).tags(login.tags);
}
},
}) as TimelineSubject,
[followList, login.tags.item],
[login.publicKey, followList, login.tags],
);
const feed = useTimelineFeed(subject, { method: "TIME_RANGE", now: openedAt } as TimelineFeedOptions);

View File

@ -16,12 +16,12 @@ export default function UsersFeed({ keyword, sortPopular = true }: { keyword: st
{ method: "LIMIT_UNTIL" },
);
const { muted, isEventMuted } = useModeration();
const { isEventMuted } = useModeration();
const filterPosts = useCallback(
(nts: readonly TaggedNostrEvent[]) => {
return nts.filter(a => !isEventMuted(a));
},
[muted],
[isEventMuted],
);
const usersFeed = useMemo(() => filterPosts(feed.main ?? []).map(p => p.pubkey), [feed, filterPosts]);

View File

@ -2,10 +2,10 @@ import { useSyncExternalStore } from "react";
import { getLocale } from "@/Components/IntlProvider/IntlProviderUtils";
import { LangOverride } from "@/Components/IntlProvider/langStore";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
export function useLocale() {
const { language } = useLogin(s => ({ language: s.appData.json.preferences.language }));
const language = usePreferences(s => s.language);
const loggedOutLang = useSyncExternalStore(
c => LangOverride.hook(c),
() => LangOverride.snapshot(),

View File

@ -7,6 +7,7 @@ import { FormattedMessage, useIntl } from "react-intl";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import { createPublisher, LoginStore, sessionNeedsPin } from "@/Utils/Login";
import { GetPowWorker } from "@/Utils/wasm";
@ -97,6 +98,7 @@ export function PinPrompt({
export function LoginUnlock() {
const login = useLogin();
const pow = usePreferences(s => s.pow);
const { publisher } = useEventPublisher();
async function encryptMigration(pin: string) {
@ -104,8 +106,8 @@ export function LoginUnlock() {
const newPin = await PinEncrypted.create(k, pin);
const pub = EventPublisher.privateKey(k);
if (login.appData.json.preferences.pow) {
pub.pow(login.appData.json.preferences.pow, GetPowWorker());
if (pow) {
pub.pow(pow, GetPowWorker());
}
LoginStore.setPublisher(login.id, pub);
LoginStore.updateSession({
@ -121,8 +123,8 @@ export function LoginUnlock() {
await key.unlock(pin);
const pub = createPublisher(login);
if (pub) {
if (login.appData.json.preferences.pow) {
pub.pow(login.appData.json.preferences.pow, GetPowWorker());
if (pow) {
pub.pow(pow, GetPowWorker());
}
LoginStore.setPublisher(login.id, pub);
LoginStore.updateSession({

View File

@ -4,7 +4,7 @@ import { useContext, useState } from "react";
import { FormattedMessage } from "react-intl";
import Modal from "@/Components/Modal/Modal";
import useLogin from "@/Hooks/useLogin";
import useRelays from "@/Hooks/useRelays";
import AsyncButton from "./Button/AsyncButton";
import messages from "./messages";
@ -12,7 +12,7 @@ import messages from "./messages";
export function ReBroadcaster({ onClose, ev }: { onClose: () => void; ev: TaggedNostrEvent }) {
const [selected, setSelected] = useState<Array<string>>();
const system = useContext(SnortContext);
const { relays } = useLogin(s => ({ relays: s.relays }));
const relays = useRelays();
async function sendReBroadcast() {
if (selected) {
@ -25,8 +25,8 @@ export function ReBroadcaster({ onClose, ev }: { onClose: () => void; ev: Tagged
function renderRelayCustomisation() {
return (
<div className="flex flex-col g8">
{Object.keys(relays.item || {})
.filter(el => relays.item[el].write)
{Object.keys(relays)
.filter(el => relays[el].write)
.map((r, i, a) => (
<div key={r} className="card flex justify-between">
<div>{r}</div>
@ -36,7 +36,7 @@ export function ReBroadcaster({ onClose, ev }: { onClose: () => void; ev: Tagged
checked={!selected || selected.includes(r)}
onChange={e =>
setSelected(
e.target.checked && selected && selected.length == a.length - 1
e.target.checked && selected && selected.length === a.length - 1
? undefined
: a.filter(el => (el === r ? e.target.checked : !selected || selected.includes(el))),
)

View File

@ -1,17 +1,14 @@
import "./Relay.css";
import { unixNowMs } from "@snort/shared";
import { RelaySettings } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import classNames from "classnames";
import { useContext, useMemo } from "react";
import { useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { AsyncIcon } from "@/Components/Button/AsyncIcon";
import useRelayState from "@/Feed/RelayState";
import useLogin from "@/Hooks/useLogin";
import { getRelayName, unwrap } from "@/Utils";
import { removeRelay, setRelays } from "@/Utils/Login";
import { getRelayName } from "@/Utils";
import { RelayFavicon } from "./RelaysMetadata";
@ -21,35 +18,29 @@ export interface RelayProps {
export default function Relay(props: RelayProps) {
const navigate = useNavigate();
const system = useContext(SnortContext);
const login = useLogin();
const state = useLogin(s => s.state);
const relaySettings = unwrap(login.relays.item[props.addr] ?? system.pool.getConnection(props.addr)?.Settings ?? {});
const state = useRelayState(props.addr);
const name = useMemo(() => getRelayName(props.addr), [props.addr]);
const connection = useRelayState(props.addr);
function configure(o: RelaySettings) {
setRelays(
login,
{
...login.relays.item,
[props.addr]: o,
},
unixNowMs(),
);
const relaySettings = state.relays?.find(a => a.url === props.addr)?.settings;
if (!relaySettings || !connection) return;
async function configure(o: RelaySettings) {
await state.updateRelay(props.addr, o);
}
return (
<>
<div className="relay bg-dark">
<div className={classNames("flex items-center", state?.IsClosed === false ? "bg-success" : "bg-error")}>
<div className={classNames("flex items-center", connection.isOpen ? "bg-success" : "bg-error")}>
<RelayFavicon url={props.addr} />
</div>
<div className="flex flex-col g8">
<div>
<b>{name}</b>
</div>
{!state?.Ephemeral && (
{!connection?.Ephemeral && (
<div className="flex g8">
<AsyncIcon
iconName="write"
@ -77,13 +68,13 @@ export default function Relay(props: RelayProps) {
iconName="trash"
iconSize={16}
className="button-icon-sm transparent trash-icon"
onClick={() => removeRelay(login, props.addr)}
onClick={() => state.removeRelay(props.addr)}
/>
<AsyncIcon
iconName="gear"
iconSize={16}
className="button-icon-sm transparent"
onClick={() => navigate(state?.Id ?? "")}
onClick={() => navigate(connection?.Id ?? "")}
/>
</div>
)}

View File

@ -17,15 +17,15 @@ enum Provider {
}
export default function SuggestedProfiles() {
const login = useLogin(s => ({ publicKey: s.publicKey, follows: s.contacts }));
const publicKey = useLogin(s => s.publicKey);
const [provider, setProvider] = useState(Provider.NostrBand);
const getUrlAndKey = () => {
if (!login.publicKey) return { url: null, key: null };
if (!publicKey) return { url: null, key: null };
switch (provider) {
case Provider.NostrBand: {
const api = new NostrBandApi();
const url = api.suggestedFollowsUrl(hexToBech32(NostrPrefix.PublicKey, login.publicKey));
const url = api.suggestedFollowsUrl(hexToBech32(NostrPrefix.PublicKey, publicKey));
return { url, key: `nostr-band-${url}` };
}
default:

View File

@ -33,7 +33,7 @@ export default function TrendingNotes({ count = Infinity, small = false }: { cou
const ev = a.event;
if (!System.optimizer.schnorrVerify(ev)) {
console.error(`Event with invalid sig\n\n${ev}\n\nfrom ${trendingNotesUrl}`);
return;
return undefined;
}
System.HandleEvent("*", ev as TaggedNostrEvent);
return ev;

View File

@ -1,25 +0,0 @@
import { HexKey } from "@snort/system";
import { FormattedMessage } from "react-intl";
import useModeration from "@/Hooks/useModeration";
import messages from "../messages";
interface BlockButtonProps {
pubkey: HexKey;
}
const BlockButton = ({ pubkey }: BlockButtonProps) => {
const { block, unblock, isBlocked } = useModeration();
return isBlocked(pubkey) ? (
<button className="secondary" type="button" onClick={() => unblock(pubkey)}>
<FormattedMessage {...messages.Unblock} />
</button>
) : (
<button className="secondary" type="button" onClick={() => block(pubkey)}>
<FormattedMessage {...messages.Block} />
</button>
);
};
export default BlockButton;

View File

@ -1,15 +0,0 @@
import BlockButton from "@/Components/User/BlockButton";
import ProfilePreview from "@/Components/User/ProfilePreview";
import useModeration from "@/Hooks/useModeration";
export default function BlockList() {
const { blocked } = useModeration();
return (
<div className="main-content p">
{blocked.map(a => {
return <ProfilePreview actions={<BlockButton pubkey={a} />} pubkey={a} options={{ about: false }} key={a} />;
})}
</div>
);
}

View File

@ -1,4 +1,4 @@
import { HexKey } from "@snort/system";
import { HexKey, NostrPrefix } from "@snort/system";
import { FormattedMessage } from "react-intl";
import MuteButton from "@/Components/User/MuteButton";
@ -11,26 +11,31 @@ export interface MutedListProps {
pubkeys: HexKey[];
}
export default function MutedList({ pubkeys }: MutedListProps) {
const { isMuted, muteAll } = useModeration();
const hasAllMuted = pubkeys.every(isMuted);
export default function MutedList() {
const { muteList } = useModeration();
return (
<div className="p">
<div className="flex justify-between">
<div className="bold">
<FormattedMessage {...messages.MuteCount} values={{ n: pubkeys?.length }} />
<FormattedMessage {...messages.MuteCount} values={{ n: muteList?.length }} />
</div>
<button
disabled={hasAllMuted || pubkeys.length === 0}
className="transparent"
type="button"
onClick={() => muteAll(pubkeys)}>
<FormattedMessage {...messages.MuteAll} />
</button>
</div>
{pubkeys?.map(a => {
return <ProfilePreview actions={<MuteButton pubkey={a} />} pubkey={a} options={{ about: false }} key={a} />;
{muteList?.map(a => {
switch (a.type) {
case NostrPrefix.Profile:
case NostrPrefix.PublicKey: {
return (
<ProfilePreview
actions={<MuteButton pubkey={a.id} />}
pubkey={a.id}
options={{ about: false }}
key={a.id}
/>
);
}
}
return undefined;
})}
</div>
);

View File

@ -7,6 +7,7 @@ import messages from "@/Components/messages";
import { ZapType } from "@/Components/ZapModal/ZapType";
import { ZapTypeSelector } from "@/Components/ZapModal/ZapTypeSelector";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import { formatShort } from "@/Utils/Number";
import { Zapper } from "@/Utils/Zapper";
@ -21,10 +22,8 @@ export function ZapModalInput(props: {
onChange?: (v: SendSatsInputSelection) => void;
onNextStage: (v: SendSatsInputSelection) => Promise<void>;
}) {
const { defaultZapAmount, readonly } = useLogin(s => ({
defaultZapAmount: s.appData.json.preferences.defaultZapAmount,
readonly: s.readonly,
}));
const defaultZapAmount = usePreferences(s => s.defaultZapAmount);
const readonly = useLogin(s => s.readonly);
const { formatMessage } = useIntl();
const amounts: Record<string, string> = {
[defaultZapAmount.toString()]: "",