1
0
forked from Kieran/snort

feat: appdata

This commit is contained in:
Kieran 2023-11-13 16:51:29 +00:00
parent 540f29dd69
commit 24978f4e62
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
29 changed files with 181 additions and 136 deletions

View File

@ -4,7 +4,7 @@ import useLogin from "Hooks/useLogin";
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 { theme } = useLogin(s => ({ theme: s.preferences.theme })); const theme = useLogin(s => s.appData.item.preferences.theme);
const lightParams = theme === "light" ? "light=1" : "light=0"; const lightParams = theme === "light" ? "light=1" : "light=0";
return ( return (
<> <>

View File

@ -22,7 +22,7 @@ export default function PubkeyList({ ev, className }: { ev: NostrEvent; classNam
for (const pk of ids) { for (const pk of ids) {
try { try {
const profile = await UserCache.get(pk); const profile = await UserCache.get(pk);
const amtSend = login.preferences.defaultZapAmount; const amtSend = login.appData.item.preferences.defaultZapAmount;
const lnurl = profile?.lud16 || profile?.lud06; const lnurl = profile?.lud16 || profile?.lud06;
if (lnurl) { if (lnurl) {
const svc = new LNURL(lnurl); const svc = new LNURL(lnurl);
@ -72,7 +72,7 @@ export default function PubkeyList({ ev, className }: { ev: NostrEvent; classNam
<FormattedMessage <FormattedMessage
defaultMessage="Zap all {n} sats" defaultMessage="Zap all {n} sats"
values={{ values={{
n: <FormattedNumber value={login.preferences.defaultZapAmount * ids.length} />, n: <FormattedNumber value={login.appData.item.preferences.defaultZapAmount * ids.length} />,
}} }}
/> />
</AsyncButton> </AsyncButton>

View File

@ -83,7 +83,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
useEffect(() => { useEffect(() => {
const sub = getCurrentSubscription(login.subscriptions); const sub = getCurrentSubscription(login.subscriptions);
if (sub?.type === SubscriptionType.Premium && (login.preferences.autoTranslate ?? true)) { if (sub?.type === SubscriptionType.Premium && (login.appData.item.preferences.autoTranslate ?? true)) {
translate(); translate();
} }
}, []); }, []);
@ -162,7 +162,7 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
<FormattedMessage {...messages.Mute} /> <FormattedMessage {...messages.Mute} />
</MenuItem> </MenuItem>
)} )}
{login.preferences.enableReactions && !login.readonly && ( {login.appData.item.preferences.enableReactions && !login.readonly && (
<MenuItem onClick={() => props.react("-")}> <MenuItem onClick={() => props.react("-")}>
<Icon name="dislike" /> <Icon name="dislike" />
<FormattedMessage {...messages.DislikeAction} /> <FormattedMessage {...messages.DislikeAction} />
@ -182,12 +182,10 @@ export function NoteContextMenu({ ev, ...props }: NosteContextMenuProps) {
<Icon name="translate" /> <Icon name="translate" />
<FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} /> <FormattedMessage {...messages.TranslateTo} values={{ lang: langNames.of(lang.split("-")[0]) }} />
</MenuItem> </MenuItem>
{login.preferences.showDebugMenus && ( <MenuItem onClick={() => copyEvent()}>
<MenuItem onClick={() => copyEvent()}> <Icon name="json" />
<Icon name="json" /> <FormattedMessage {...messages.CopyJSON} />
<FormattedMessage {...messages.CopyJSON} /> </MenuItem>
</MenuItem>
)}
{isMine && !login.readonly && ( {isMine && !login.readonly && (
<MenuItem onClick={() => deleteEvent()}> <MenuItem onClick={() => deleteEvent()}>
<Icon name="trash" className="red" /> <Icon name="trash" className="red" />

View File

@ -27,7 +27,7 @@ import { ToggleSwitch } from "Icons/Toggle";
export function NoteCreator() { export function NoteCreator() {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const uploader = useFileUpload(); const uploader = useFileUpload();
const login = useLogin(s => ({ relays: s.relays, publicKey: s.publicKey, pow: s.preferences.pow })); const login = useLogin(s => ({ relays: s.relays, publicKey: s.publicKey, pow: s.appData.item.preferences.pow }));
const { publisher: pub } = useEventPublisher(); const { publisher: pub } = useEventPublisher();
const publisher = login.pow ? pub?.pow(login.pow, GetPowWorker()) : pub; const publisher = login.pow ? pub?.pow(login.pow, GetPowWorker()) : pub;
const note = useNoteCreator(); const note = useNoteCreator();

View File

@ -52,7 +52,7 @@ export default function NoteFooter(props: NoteFooterProps) {
publicKey, publicKey,
preferences: prefs, preferences: prefs,
readonly, readonly,
} = useLogin(s => ({ preferences: s.preferences, publicKey: s.publicKey, readonly: s.readonly })); } = useLogin(s => ({ preferences: s.appData.item.preferences, publicKey: s.publicKey, readonly: s.readonly }));
const author = useUserProfile(ev.pubkey); const author = useUserProfile(ev.pubkey);
const interactionCache = useInteractionCache(publicKey, ev.id); const interactionCache = useInteractionCache(publicKey, ev.id);
const { publisher, system } = useEventPublisher(); const { publisher, system } = useEventPublisher();

View File

@ -23,7 +23,11 @@ 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 { preferences: prefs, publicKey: myPubKey, relays } = useLogin(); const {
preferences: prefs,
publicKey: myPubKey,
relays,
} = useLogin(s => ({ preferences: s.appData.item.preferences, publicKey: s.publicKey, relays: s.relays }));
const pollerProfile = useUserProfile(props.ev.pubkey); const pollerProfile = useUserProfile(props.ev.pubkey);
const [tallyBy, setTallyBy] = useState<PollTally>("pubkeys"); const [tallyBy, setTallyBy] = useState<PollTally>("pubkeys");
const [error, setError] = useState(""); const [error, setError] = useState("");

View File

@ -13,12 +13,15 @@ interface RevealMediaProps {
} }
export default function RevealMedia(props: RevealMediaProps) { export default function RevealMedia(props: RevealMediaProps) {
const login = useLogin(); const { preferences, follows, publicKey } = useLogin(s => ({
const { preferences: pref, follows, publicKey } = login; preferences: s.appData.item.preferences,
follows: s.follows.item,
publicKey: s.publicKey,
}));
const hideNonFollows = pref.autoLoadMedia === "follows-only" && !follows.item.includes(props.creator); const hideNonFollows = preferences.autoLoadMedia === "follows-only" && !follows.includes(props.creator);
const isMine = props.creator === publicKey; const isMine = props.creator === publicKey;
const hideMedia = pref.autoLoadMedia === "none" || (!isMine && hideNonFollows); const hideMedia = preferences.autoLoadMedia === "none" || (!isMine && hideNonFollows);
const hostname = new URL(props.link).hostname; const hostname = new URL(props.link).hostname;
const url = new URL(props.link); const url = new URL(props.link);

View File

@ -118,9 +118,11 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
noteOnClick={props.noteOnClick} noteOnClick={props.noteOnClick}
noteRenderer={props.noteRenderer} noteRenderer={props.noteRenderer}
/> />
{sortedFeed.length > 0 && <ShowMoreInView {sortedFeed.length > 0 && (
onClick={async () => await FollowsFeed.loadMore(system, login, sortedFeed[sortedFeed.length - 1].created_at)} <ShowMoreInView
/>} onClick={async () => await FollowsFeed.loadMore(system, login, sortedFeed[sortedFeed.length - 1].created_at)}
/>
)}
</> </>
); );
}; };
@ -168,4 +170,4 @@ function weaveTimeline(
refTime: main[skip].created_at, refTime: main[skip].created_at,
}, },
].sort((a, b) => (a.refTime > b.refTime ? -1 : 1)); ].sort((a, b) => (a.refTime > b.refTime ? -1 : 1));
} }

View File

@ -100,8 +100,8 @@ export function LoginUnlock() {
const newPin = await PinEncrypted.create(k, pin); const newPin = await PinEncrypted.create(k, pin);
const pub = EventPublisher.privateKey(k); const pub = EventPublisher.privateKey(k);
if (login.preferences.pow) { if (login.appData.item.preferences.pow) {
pub.pow(login.preferences.pow, GetPowWorker()); pub.pow(login.appData.item.preferences.pow, GetPowWorker());
} }
LoginStore.setPublisher(login.id, pub); LoginStore.setPublisher(login.id, pub);
LoginStore.updateSession({ LoginStore.updateSession({
@ -117,8 +117,8 @@ export function LoginUnlock() {
await key.unlock(pin); await key.unlock(pin);
const pub = createPublisher(login); const pub = createPublisher(login);
if (pub) { if (pub) {
if (login.preferences.pow) { if (login.appData.item.preferences.pow) {
pub.pow(login.preferences.pow, GetPowWorker()); pub.pow(login.appData.item.preferences.pow, GetPowWorker());
} }
LoginStore.setPublisher(login.id, pub); LoginStore.setPublisher(login.id, pub);
LoginStore.updateSession({ LoginStore.updateSession({

View File

@ -247,7 +247,7 @@ function SendSatsInput(props: {
onNextStage: (v: SendSatsInputSelection) => Promise<void>; onNextStage: (v: SendSatsInputSelection) => Promise<void>;
}) { }) {
const { defaultZapAmount, readonly } = useLogin(s => ({ const { defaultZapAmount, readonly } = useLogin(s => ({
defaultZapAmount: s.preferences.defaultZapAmount, defaultZapAmount: s.appData.item.preferences.defaultZapAmount,
readonly: s.readonly, readonly: s.readonly,
})); }));
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();

View File

@ -2,7 +2,7 @@ import { useEffect, useMemo } from "react";
import { TaggedNostrEvent, EventKind, RequestBuilder, NoteCollection, NostrLink } from "@snort/system"; import { TaggedNostrEvent, EventKind, RequestBuilder, NoteCollection, NostrLink } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { bech32ToHex, findTag, getNewest, getNewestEventTagsByKey, unwrap } from "SnortUtils"; import { bech32ToHex, debounce, findTag, getNewest, getNewestEventTagsByKey, unwrap } from "SnortUtils";
import { makeNotification, sendNotification } from "Notifications"; import { makeNotification, sendNotification } from "Notifications";
import useEventPublisher from "Hooks/useEventPublisher"; import useEventPublisher from "Hooks/useEventPublisher";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
@ -40,9 +40,19 @@ export default function useLoginFeed() {
useRefreshFeedCache(GiftsCache, true); useRefreshFeedCache(GiftsCache, true);
useEffect(() => { useEffect(() => {
system.checkSigs = login.preferences.checkSigs; system.checkSigs = login.appData.item.preferences.checkSigs;
}, [login]); }, [login]);
// write appdata after 10s of no changes
useEffect(() => {
return debounce(10_000, async () => {
if (publisher && login.appData.item) {
const ev = await publisher.appData(login.appData.item, "snort");
await system.BroadcastEvent(ev);
}
});
}, [login.appData.timestamp]);
const subLogin = useMemo(() => { const subLogin = useMemo(() => {
if (!login || !pubKey) return null; if (!login || !pubKey) return null;

View File

@ -28,7 +28,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
window: options.window, window: options.window,
now: options.now ?? unixNow(), now: options.now ?? unixNow(),
}); });
const pref = useLogin().preferences; const pref = useLogin(s => s.appData.item.preferences);
const createBuilder = useCallback(() => { const createBuilder = useCallback(() => {
if (subject.type !== "global" && subject.items.length === 0) { if (subject.type !== "global" && subject.items.length === 0) {

View File

@ -10,7 +10,7 @@ export interface ImgProxySettings {
} }
export default function useImgProxy() { export default function useImgProxy() {
const settings = useLogin().preferences.imgProxyConfig; const settings = useLogin(s => s.appData.item.preferences.imgProxyConfig);
const te = new TextEncoder(); const te = new TextEncoder();
function urlSafe(s: string) { function urlSafe(s: string) {

View File

@ -2,7 +2,7 @@ import { LoginSession, LoginStore } from "Login";
import { useSyncExternalStore } from "react"; import { useSyncExternalStore } from "react";
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector"; import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector";
export default function useLogin<T extends object = LoginSession>(selector?: (v: LoginSession) => T) { export default function useLogin<T = LoginSession>(selector?: (v: LoginSession) => T) {
if (selector) { if (selector) {
return useSyncExternalStoreWithSelector( return useSyncExternalStoreWithSelector(
s => LoginStore.hook(s), s => LoginStore.hook(s),

View File

@ -2,7 +2,7 @@ import { useEffect } from "react";
import useLogin from "./useLogin"; import useLogin from "./useLogin";
export function useTheme() { export function useTheme() {
const { preferences } = useLogin(); const { preferences } = useLogin(s => ({ preferences: s.appData.item.preferences }));
function setTheme(theme: "light" | "dark") { function setTheme(theme: "light" | "dark") {
const elm = document.documentElement; const elm = document.documentElement;

View File

@ -95,7 +95,7 @@ class LangStore extends ExternalStore<string | null> {
const LangOverride = new LangStore(); const LangOverride = new LangStore();
export function useLocale() { export function useLocale() {
const { language } = useLogin(s => ({ language: s.preferences.language })); const { language } = useLogin(s => ({ language: s.appData.item.preferences.language }));
const loggedOutLang = useSyncExternalStore( const loggedOutLang = useSyncExternalStore(
c => LangOverride.hook(c), c => LangOverride.hook(c),
() => LangOverride.snapshot(), () => LangOverride.snapshot(),

View File

@ -13,7 +13,7 @@ import * as secp from "@noble/curves/secp256k1";
import * as utils from "@noble/curves/abstract/utils"; import * as utils from "@noble/curves/abstract/utils";
import { DefaultRelays, SnortPubKey } from "Const"; import { DefaultRelays, SnortPubKey } from "Const";
import { LoginStore, UserPreferences, LoginSession, LoginSessionType, SnortAppData } from "Login"; import { LoginStore, UserPreferences, LoginSession, LoginSessionType, SnortAppData, Newest } from "Login";
import { generateBip39Entropy, entropyToPrivateKey } from "nip6"; import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
import { bech32ToHex, dedupeById, sanitizeRelayUrl, unwrap } from "SnortUtils"; import { bech32ToHex, dedupeById, sanitizeRelayUrl, unwrap } from "SnortUtils";
import { SubscriptionEvent } from "Subscription"; import { SubscriptionEvent } from "Subscription";
@ -54,9 +54,13 @@ export function removeRelay(state: LoginSession, addr: string) {
LoginStore.updateSession(state); LoginStore.updateSession(state);
} }
export function updatePreferences(state: LoginSession, p: UserPreferences) { export function updatePreferences(id: string, p: UserPreferences) {
state.preferences = p; updateAppData(id, d => {
LoginStore.updateSession(state); return {
item: { ...d, preferences: p },
timestamp: unixNowMs(),
};
});
} }
export function logout(id: string) { export function logout(id: string) {
@ -183,6 +187,17 @@ export function setAppData(state: LoginSession, data: SnortAppData, ts: number)
LoginStore.updateSession(state); LoginStore.updateSession(state);
} }
export function updateAppData(id: string, fn: (data: SnortAppData) => Newest<SnortAppData>) {
const session = LoginStore.get(id);
if (session) {
const next = fn(session.appData.item);
if (next.timestamp > session.appData.timestamp) {
session.appData = next;
LoginStore.updateSession(session);
}
}
}
export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[]) { export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[]) {
const newSubs = dedupeById([...(state.subscriptions || []), ...subs]); const newSubs = dedupeById([...(state.subscriptions || []), ...subs]);
if (newSubs.length !== state.subscriptions.length) { if (newSubs.length !== state.subscriptions.length) {

View File

@ -5,7 +5,7 @@ import { SubscriptionEvent } from "Subscription";
/** /**
* Stores latest copy of an item * Stores latest copy of an item
*/ */
interface Newest<T> { export interface Newest<T> {
item: T; item: T;
timestamp: number; timestamp: number;
} }
@ -20,6 +20,7 @@ export const enum LoginSessionType {
export interface SnortAppData { export interface SnortAppData {
mutedWords: Array<string>; mutedWords: Array<string>;
preferences: UserPreferences;
} }
export interface LoginSession { export interface LoginSession {
@ -104,11 +105,6 @@ export interface LoginSession {
*/ */
readNotifications: number; readNotifications: number;
/**
* Users cusom preferences
*/
preferences: UserPreferences;
/** /**
* Snort subscriptions licences * Snort subscriptions licences
*/ */

View File

@ -7,13 +7,12 @@ import { deepClone, sanitizeRelayUrl, unwrap, ExternalStore } from "@snort/share
import { DefaultRelays } from "Const"; import { DefaultRelays } from "Const";
import { LoginSession, LoginSessionType, createPublisher } from "Login"; import { LoginSession, LoginSessionType, createPublisher } from "Login";
import { DefaultPreferences } from "./Preferences"; import { DefaultPreferences, UserPreferences } from "./Preferences";
const AccountStoreKey = "sessions"; const AccountStoreKey = "sessions";
const LoggedOut = { const LoggedOut = {
id: "default", id: "default",
type: "public_key", type: "public_key",
preferences: DefaultPreferences,
readonly: true, readonly: true,
tags: { tags: {
item: [], item: [],
@ -49,6 +48,7 @@ const LoggedOut = {
appData: { appData: {
item: { item: {
mutedWords: [], mutedWords: [],
preferences: DefaultPreferences,
}, },
timestamp: 0, timestamp: 0,
}, },
@ -83,11 +83,11 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
v.appData ??= { v.appData ??= {
item: { item: {
mutedWords: [], mutedWords: [],
preferences: DefaultPreferences,
}, },
timestamp: 0, timestamp: 0,
}; };
v.extraChats ??= []; v.extraChats ??= [];
v.preferences.checkSigs ??= false;
if (v.privateKeyData) { if (v.privateKeyData) {
v.privateKeyData = KeyStorage.fromPayload(v.privateKeyData as object); v.privateKeyData = KeyStorage.fromPayload(v.privateKeyData as object);
} }
@ -102,6 +102,13 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
})); }));
} }
get(id: string) {
const s = this.#accounts.get(id);
if (s) {
return { ...s };
}
}
allSubscriptions() { allSubscriptions() {
return [...this.#accounts.values()].map(a => a.subscriptions).flat(); return [...this.#accounts.values()].map(a => a.subscriptions).flat();
} }
@ -246,14 +253,6 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
#migrate() { #migrate() {
let didMigrate = false; let didMigrate = false;
// replace default tab with notes
for (const [, v] of this.#accounts) {
if ((v.preferences.defaultRootTab as string) === "posts") {
v.preferences.defaultRootTab = "notes";
didMigrate = true;
}
}
// update session types // update session types
for (const [, v] of this.#accounts) { for (const [, v] of this.#accounts) {
if (!v.type) { if (!v.type) {
@ -283,6 +282,15 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
} }
} }
// move preferences to appdata
for (const [, v] of this.#accounts) {
if ("preferences" in v) {
v.appData.item.preferences = v.preferences as UserPreferences;
delete v["preferences"];
didMigrate = true;
}
}
if (didMigrate) { if (didMigrate) {
console.debug("Finished migration in MultiAccountStore"); console.debug("Finished migration in MultiAccountStore");
this.#save(); this.#save();

View File

@ -33,8 +33,6 @@ const HashTagsPage = () => {
export default HashTagsPage; export default HashTagsPage;
export function HashTagHeader({ tag }: { tag: string }) { export function HashTagHeader({ tag }: { tag: string }) {
const login = useLogin(); const login = useLogin();
const isFollowing = useMemo(() => { const isFollowing = useMemo(() => {
@ -55,35 +53,36 @@ export function HashTagHeader({ tag }: { tag: string }) {
const sub = useMemo(() => { const sub = useMemo(() => {
const rb = new RequestBuilder(`hashtag-counts:${tag}`); const rb = new RequestBuilder(`hashtag-counts:${tag}`);
rb.withFilter() rb.withFilter().kinds([EventKind.CategorizedBookmarks]).tag("d", ["follow"]).tag("t", [tag.toLowerCase()]);
.kinds([EventKind.CategorizedBookmarks])
.tag("d", ["follow"])
.tag("t", [tag.toLowerCase()]);
return rb; return rb;
}, [tag]); }, [tag]);
const followsTag = useRequestBuilder(NoteCollection, sub); const followsTag = useRequestBuilder(NoteCollection, sub);
const pubkeys = dedupe((followsTag.data ?? []).map(a => a.pubkey)); const pubkeys = dedupe((followsTag.data ?? []).map(a => a.pubkey));
return <div className="flex items-center justify-between"> return (
<div className="flex flex-col g8"> <div className="flex items-center justify-between">
<h2>#{tag}</h2> <div className="flex flex-col g8">
<div className="flex"> <h2>#{tag}</h2>
{pubkeys.slice(0, 5).map(a => <ProfileImage pubkey={a} showUsername={false} link={""} showFollowingMark={false} size={40} />)} <div className="flex">
{pubkeys.length > 5 && <span> {pubkeys.slice(0, 5).map(a => (
+<FormattedNumber value={pubkeys.length - 5} /> <ProfileImage pubkey={a} showUsername={false} link={""} showFollowingMark={false} size={40} />
</span>} ))}
{pubkeys.length > 5 && (
<span>
+<FormattedNumber value={pubkeys.length - 5} />
</span>
)}
</div>
</div> </div>
{isFollowing ? (
<AsyncButton className="secondary" onClick={() => followTags(login.tags.item.filter(t => t !== tag))}>
<FormattedMessage defaultMessage="Unfollow" />
</AsyncButton>
) : (
<AsyncButton onClick={() => followTags(login.tags.item.concat([tag]))}>
<FormattedMessage defaultMessage="Follow" />
</AsyncButton>
)}
</div> </div>
{isFollowing ? ( );
<AsyncButton }
className="secondary"
onClick={() => followTags(login.tags.item.filter(t => t !== tag))}>
<FormattedMessage defaultMessage="Unfollow" />
</AsyncButton>
) : (
<AsyncButton onClick={() => followTags(login.tags.item.concat([tag]))}>
<FormattedMessage defaultMessage="Follow" />
</AsyncButton>
)}
</div>
}

View File

@ -89,8 +89,8 @@ export default function ProfilePage({ id: propId, state }: ProfilePageProps) {
// ignored // ignored
} }
})(); })();
const showBadges = login.preferences.showBadges ?? false; const showBadges = login.appData.item.preferences.showBadges ?? false;
const showStatus = login.preferences.showStatus ?? true; const showStatus = login.appData.item.preferences.showStatus ?? true;
// feeds // feeds
const { blocked } = useModeration(); const { blocked } = useModeration();

View File

@ -181,7 +181,10 @@ export const TagsTab = (params: { tag?: string }) => {
}; };
const DefaultTab = () => { const DefaultTab = () => {
const { preferences, publicKey } = useLogin(); const { preferences, publicKey } = useLogin(s => ({
preferences: s.appData.item.preferences,
publicKey: s.publicKey,
}));
const tab = publicKey ? preferences.defaultRootTab ?? `notes` : `trending/notes`; const tab = publicKey ? preferences.defaultRootTab ?? `notes` : `trending/notes`;
const elm = RootTabRoutes.find(a => a.path === tab)?.element; const elm = RootTabRoutes.find(a => a.path === tab)?.element;
return elm; return elm;

View File

@ -34,7 +34,7 @@ function ZapTarget({ target }: { target: ZapPoolRecipient }) {
const login = useLogin(); const login = useLogin();
const profile = useUserProfile(target.pubkey); const profile = useUserProfile(target.pubkey);
const hasAddress = profile?.lud16 || profile?.lud06; const hasAddress = profile?.lud16 || profile?.lud06;
const defaultZapMount = Math.ceil(login.preferences.defaultZapAmount * (target.split / 100)); const defaultZapMount = Math.ceil(login.appData.item.preferences.defaultZapAmount * (target.split / 100));
return ( return (
<ProfilePreview <ProfilePreview
pubkey={target.pubkey} pubkey={target.pubkey}
@ -107,7 +107,7 @@ export default function ZapPoolPage() {
values={{ values={{
number: ( number: (
<b> <b>
<FormattedNumber value={login.preferences.defaultZapAmount} /> <FormattedNumber value={login.appData.item.preferences.defaultZapAmount} />
</b> </b>
), ),
}} }}
@ -119,12 +119,14 @@ export default function ZapPoolPage() {
values={{ values={{
nIn: ( nIn: (
<b> <b>
<FormattedNumber value={login.preferences.defaultZapAmount} /> <FormattedNumber value={login.appData.item.preferences.defaultZapAmount} />
</b> </b>
), ),
nOut: ( nOut: (
<b> <b>
<FormattedNumber value={ZapPoolController.calcAllocation(login.preferences.defaultZapAmount)} /> <FormattedNumber
value={ZapPoolController.calcAllocation(login.appData.item.preferences.defaultZapAmount)}
/>
</b> </b>
), ),
}} }}

View File

@ -1,6 +1,6 @@
import { unixNowMs } from "@snort/shared"; import { unixNowMs } from "@snort/shared";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { setAppData } from "Login"; import { updateAppData } from "Login";
import { appendDedupe } from "SnortUtils"; import { appendDedupe } from "SnortUtils";
import { useState } from "react"; import { useState } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
@ -10,32 +10,24 @@ export function ModerationSettings() {
const [muteWord, setMuteWord] = useState(""); const [muteWord, setMuteWord] = useState("");
function addMutedWord() { function addMutedWord() {
login.appData ??= { updateAppData(login.id, ad => ({
item: { item: {
mutedWords: [], ...ad,
},
timestamp: 0,
};
setAppData(
login,
{
...login.appData.item,
mutedWords: appendDedupe(login.appData.item.mutedWords, [muteWord]), mutedWords: appendDedupe(login.appData.item.mutedWords, [muteWord]),
}, },
unixNowMs(), timestamp: unixNowMs(),
); }));
setMuteWord(""); setMuteWord("");
} }
function removeMutedWord(word: string) { function removeMutedWord(word: string) {
setAppData( updateAppData(login.id, ad => ({
login, item: {
{ ...ad,
...login.appData.item,
mutedWords: login.appData.item.mutedWords.filter(a => a !== word), mutedWords: login.appData.item.mutedWords.filter(a => a !== word),
}, },
unixNowMs(), timestamp: unixNowMs(),
); }));
setMuteWord(""); setMuteWord("");
} }

View File

@ -35,8 +35,7 @@ export const AllLanguageCodes = [
const PreferencesPage = () => { const PreferencesPage = () => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const login = useLogin(); const { id, perf } = useLogin(s => ({ id: s.id, perf: s.appData.item.preferences }));
const perf = login.preferences;
const { lang } = useLocale(); const { lang } = useLocale();
return ( return (
@ -53,7 +52,7 @@ const PreferencesPage = () => {
<select <select
value={lang} value={lang}
onChange={e => onChange={e =>
updatePreferences(login, { updatePreferences(id, {
...perf, ...perf,
language: e.target.value, language: e.target.value,
}) })
@ -77,7 +76,7 @@ const PreferencesPage = () => {
<select <select
value={perf.theme} value={perf.theme}
onChange={e => onChange={e =>
updatePreferences(login, { updatePreferences(id, {
...perf, ...perf,
theme: e.target.value, theme: e.target.value,
} as UserPreferences) } as UserPreferences)
@ -102,7 +101,7 @@ const PreferencesPage = () => {
<select <select
value={perf.defaultRootTab} value={perf.defaultRootTab}
onChange={e => onChange={e =>
updatePreferences(login, { updatePreferences(id, {
...perf, ...perf,
defaultRootTab: e.target.value, defaultRootTab: e.target.value,
} as UserPreferences) } as UserPreferences)
@ -132,7 +131,7 @@ const PreferencesPage = () => {
<input <input
type="checkbox" type="checkbox"
checked={perf.telemetry ?? true} checked={perf.telemetry ?? true}
onChange={e => updatePreferences(login, { ...perf, telemetry: e.target.checked })} onChange={e => updatePreferences(id, { ...perf, telemetry: e.target.checked })}
/> />
</div> </div>
</div> </div>
@ -149,7 +148,7 @@ const PreferencesPage = () => {
className="w-max" className="w-max"
value={perf.autoLoadMedia} value={perf.autoLoadMedia}
onChange={e => onChange={e =>
updatePreferences(login, { updatePreferences(id, {
...perf, ...perf,
autoLoadMedia: e.target.value, autoLoadMedia: e.target.value,
} as UserPreferences) } as UserPreferences)
@ -180,7 +179,7 @@ const PreferencesPage = () => {
<input <input
type="checkbox" type="checkbox"
checked={perf.checkSigs} checked={perf.checkSigs}
onChange={e => updatePreferences(login, { ...perf, checkSigs: e.target.checked })} onChange={e => updatePreferences(id, { ...perf, checkSigs: e.target.checked })}
/> />
</div> </div>
</div> </div>
@ -197,7 +196,7 @@ const PreferencesPage = () => {
<input <input
type="checkbox" type="checkbox"
checked={perf.autoTranslate} checked={perf.autoTranslate}
onChange={e => updatePreferences(login, { ...perf, autoTranslate: e.target.checked })} onChange={e => updatePreferences(id, { ...perf, autoTranslate: e.target.checked })}
/> />
</div> </div>
</div> </div>
@ -215,7 +214,7 @@ const PreferencesPage = () => {
type="number" type="number"
defaultValue={perf.pow} defaultValue={perf.pow}
min={0} min={0}
onChange={e => updatePreferences(login, { ...perf, pow: parseInt(e.target.value || "0") })} onChange={e => updatePreferences(id, { ...perf, pow: parseInt(e.target.value || "0") })}
/> />
</div> </div>
</div> </div>
@ -228,7 +227,7 @@ const PreferencesPage = () => {
type="number" type="number"
defaultValue={perf.defaultZapAmount} defaultValue={perf.defaultZapAmount}
min={1} min={1}
onChange={e => updatePreferences(login, { ...perf, defaultZapAmount: parseInt(e.target.value || "0") })} onChange={e => updatePreferences(id, { ...perf, defaultZapAmount: parseInt(e.target.value || "0") })}
/> />
</div> </div>
</div> </div>
@ -245,7 +244,7 @@ const PreferencesPage = () => {
<input <input
type="checkbox" type="checkbox"
checked={perf.showBadges ?? false} checked={perf.showBadges ?? false}
onChange={e => updatePreferences(login, { ...perf, showBadges: e.target.checked })} onChange={e => updatePreferences(id, { ...perf, showBadges: e.target.checked })}
/> />
</div> </div>
</div> </div>
@ -262,7 +261,7 @@ const PreferencesPage = () => {
<input <input
type="checkbox" type="checkbox"
checked={perf.showStatus ?? true} checked={perf.showStatus ?? true}
onChange={e => updatePreferences(login, { ...perf, showStatus: e.target.checked })} onChange={e => updatePreferences(id, { ...perf, showStatus: e.target.checked })}
/> />
</div> </div>
</div> </div>
@ -279,7 +278,7 @@ const PreferencesPage = () => {
<input <input
type="checkbox" type="checkbox"
checked={perf.autoZap} checked={perf.autoZap}
onChange={e => updatePreferences(login, { ...perf, autoZap: e.target.checked })} onChange={e => updatePreferences(id, { ...perf, autoZap: e.target.checked })}
/> />
</div> </div>
</div> </div>
@ -298,7 +297,7 @@ const PreferencesPage = () => {
type="checkbox" type="checkbox"
checked={perf.imgProxyConfig !== null} checked={perf.imgProxyConfig !== null}
onChange={e => onChange={e =>
updatePreferences(login, { updatePreferences(id, {
...perf, ...perf,
imgProxyConfig: e.target.checked ? DefaultImgProxy : null, imgProxyConfig: e.target.checked ? DefaultImgProxy : null,
}) })
@ -321,7 +320,7 @@ const PreferencesPage = () => {
description: "Placeholder text for imgproxy url textbox", description: "Placeholder text for imgproxy url textbox",
})} })}
onChange={e => onChange={e =>
updatePreferences(login, { updatePreferences(id, {
...perf, ...perf,
imgProxyConfig: { imgProxyConfig: {
...unwrap(perf.imgProxyConfig), ...unwrap(perf.imgProxyConfig),
@ -345,7 +344,7 @@ const PreferencesPage = () => {
description: "Hexidecimal 'key' input for improxy", description: "Hexidecimal 'key' input for improxy",
})} })}
onChange={e => onChange={e =>
updatePreferences(login, { updatePreferences(id, {
...perf, ...perf,
imgProxyConfig: { imgProxyConfig: {
...unwrap(perf.imgProxyConfig), ...unwrap(perf.imgProxyConfig),
@ -369,7 +368,7 @@ const PreferencesPage = () => {
description: "Hexidecimal 'salt' input for imgproxy", description: "Hexidecimal 'salt' input for imgproxy",
})} })}
onChange={e => onChange={e =>
updatePreferences(login, { updatePreferences(id, {
...perf, ...perf,
imgProxyConfig: { imgProxyConfig: {
...unwrap(perf.imgProxyConfig), ...unwrap(perf.imgProxyConfig),
@ -396,7 +395,7 @@ const PreferencesPage = () => {
<input <input
type="checkbox" type="checkbox"
checked={perf.enableReactions} checked={perf.enableReactions}
onChange={e => updatePreferences(login, { ...perf, enableReactions: e.target.checked })} onChange={e => updatePreferences(id, { ...perf, enableReactions: e.target.checked })}
/> />
</div> </div>
</div> </div>
@ -413,7 +412,7 @@ const PreferencesPage = () => {
onChange={e => { onChange={e => {
const split = e.target.value.match(/[\p{L}\S]{1}/u); const split = e.target.value.match(/[\p{L}\S]{1}/u);
console.debug(e.target.value, split); console.debug(e.target.value, split);
updatePreferences(login, { updatePreferences(id, {
...perf, ...perf,
reactionEmoji: split?.[0] ?? "", reactionEmoji: split?.[0] ?? "",
}); });
@ -433,7 +432,7 @@ const PreferencesPage = () => {
<input <input
type="checkbox" type="checkbox"
checked={perf.confirmReposts} checked={perf.confirmReposts}
onChange={e => updatePreferences(login, { ...perf, confirmReposts: e.target.checked })} onChange={e => updatePreferences(id, { ...perf, confirmReposts: e.target.checked })}
/> />
</div> </div>
</div> </div>
@ -450,7 +449,7 @@ const PreferencesPage = () => {
<input <input
type="checkbox" type="checkbox"
checked={perf.autoShowLatest} checked={perf.autoShowLatest}
onChange={e => updatePreferences(login, { ...perf, autoShowLatest: e.target.checked })} onChange={e => updatePreferences(id, { ...perf, autoShowLatest: e.target.checked })}
/> />
</div> </div>
</div> </div>
@ -464,7 +463,7 @@ const PreferencesPage = () => {
<select <select
value={perf.fileUploader} value={perf.fileUploader}
onChange={e => onChange={e =>
updatePreferences(login, { updatePreferences(id, {
...perf, ...perf,
fileUploader: e.target.value, fileUploader: e.target.value,
} as UserPreferences) } as UserPreferences)
@ -489,7 +488,7 @@ const PreferencesPage = () => {
<input <input
type="checkbox" type="checkbox"
checked={perf.showDebugMenus} checked={perf.showDebugMenus}
onChange={e => updatePreferences(login, { ...perf, showDebugMenus: e.target.checked })} onChange={e => updatePreferences(id, { ...perf, showDebugMenus: e.target.checked })}
/> />
</div> </div>
</div> </div>

View File

@ -62,7 +62,7 @@ export interface UploadProgress {
export type UploadStage = "starting" | "hashing" | "uploading" | "done" | undefined; export type UploadStage = "starting" | "hashing" | "uploading" | "done" | undefined;
export default function useFileUpload(): Uploader { export default function useFileUpload(): Uploader {
const fileUploader = useLogin().preferences.fileUploader; const fileUploader = useLogin(s => s.appData.item.preferences.fileUploader);
const { publisher } = useEventPublisher(); const { publisher } = useEventPublisher();
const [progress, setProgress] = useState<Array<UploadProgress>>([]); const [progress, setProgress] = useState<Array<UploadProgress>>([]);
const [stage, setStage] = useState<UploadStage>(); const [stage, setStage] = useState<UploadStage>();

View File

@ -160,7 +160,7 @@ async function initSite() {
// inject analytics script // inject analytics script
// <script defer data-domain="snort.social" src="http://analytics.v0l.io/js/script.js"></script> // <script defer data-domain="snort.social" src="http://analytics.v0l.io/js/script.js"></script>
if (CONFIG.features.analytics && (login.preferences.telemetry ?? true)) { if (CONFIG.features.analytics && (login.appData.item.preferences.telemetry ?? true)) {
const sc = document.createElement("script"); const sc = document.createElement("script");
sc.src = "https://analytics.v0l.io/js/script.js"; sc.src = "https://analytics.v0l.io/js/script.js";
sc.defer = true; sc.defer = true;

View File

@ -330,6 +330,13 @@ export class EventPublisher {
return await this.#sign(eb); return await this.#sign(eb);
} }
async appData(data: object, id: string) {
const eb = this.#eb(EventKind.AppData);
eb.content(await this.nip4Encrypt(JSON.stringify(data), this.#pubKey));
eb.tag(["d", id]);
return await this.#sign(eb);
}
/** /**
* NIP-59 Gift Wrap event with ephemeral key * NIP-59 Gift Wrap event with ephemeral key
*/ */

View File

@ -139,21 +139,28 @@ export interface MessageEncryptor {
decryptData(payload: MessageEncryptorPayload, sharedSecet: Uint8Array): Promise<string> | string; decryptData(payload: MessageEncryptorPayload, sharedSecet: Uint8Array): Promise<string> | string;
} }
export function decodeEncryptionPayload(p: string) { export function decodeEncryptionPayload(p: string): MessageEncryptorPayload {
if (p.startsWith("{") && p.endsWith("}")) { if (p.startsWith("{") && p.endsWith("}")) {
const pj = JSON.parse(p) as { v: number; nonce: string; ciphertext: string }; const pj = JSON.parse(p) as { v: number; nonce: string; ciphertext: string };
return { return {
v: pj.v, v: pj.v,
nonce: base64.decode(pj.nonce), nonce: base64.decode(pj.nonce),
ciphertext: base64.decode(pj.ciphertext), ciphertext: base64.decode(pj.ciphertext),
} as MessageEncryptorPayload; };
} else if (p.includes("?iv=")) {
const [ciphertext, nonce] = p.split("?iv=");
return {
v: MessageEncryptorVersion.Nip4,
nonce: base64.decode(nonce),
ciphertext: base64.decode(ciphertext),
};
} else { } else {
const buf = base64.decode(p); const buf = base64.decode(p);
return { return {
v: buf[0], v: buf[0],
nonce: buf.subarray(1, 25), nonce: buf.subarray(1, 25),
ciphertext: buf.subarray(25), ciphertext: buf.subarray(25),
} as MessageEncryptorPayload; };
} }
} }