feat: safe sync appdata
This commit is contained in:
parent
a089ae2ec6
commit
edf64e4125
@ -4,14 +4,12 @@ import { MixCloudRegex } from "@/Utils/Const";
|
|||||||
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 => s.appData.item.preferences.theme);
|
const theme = useLogin(s => s.appData.json.preferences.theme);
|
||||||
const lightParams = theme === "light" ? "light=1" : "light=0";
|
const lightParams = theme === "light" ? "light=1" : "light=0";
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<br />
|
<br />
|
||||||
<iframe
|
<iframe
|
||||||
// eslint-disable-next-line react/no-unknown-property
|
|
||||||
credentialless=""
|
|
||||||
title="SoundCloud player"
|
title="SoundCloud player"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="120"
|
height="120"
|
||||||
|
@ -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.appData.item.preferences.defaultZapAmount;
|
const amtSend = login.appData.json.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);
|
||||||
@ -74,7 +74,7 @@ export default function PubkeyList({ ev, className }: { ev: NostrEvent; classNam
|
|||||||
defaultMessage="Zap all {n} sats"
|
defaultMessage="Zap all {n} sats"
|
||||||
id="IVbtTS"
|
id="IVbtTS"
|
||||||
values={{
|
values={{
|
||||||
n: <FormattedNumber value={login.appData.item.preferences.defaultZapAmount * ids.length} />,
|
n: <FormattedNumber value={login.appData.json.preferences.defaultZapAmount * ids.length} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
|
@ -56,7 +56,7 @@ const quoteNoteOptions = {
|
|||||||
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.appData.item.preferences.pow }));
|
const login = useLogin(s => ({ relays: s.relays, publicKey: s.publicKey, pow: s.appData.json.preferences.pow }));
|
||||||
const { system, publisher: pub } = useEventPublisher();
|
const { system, 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();
|
||||||
|
@ -4,7 +4,7 @@ import { FormattedMessage } from "react-intl";
|
|||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
|
||||||
const HiddenNote = ({ children }: { children: React.ReactNode }) => {
|
const HiddenNote = ({ children }: { children: React.ReactNode }) => {
|
||||||
const hideMutedNotes = useLogin(s => s.appData.item.preferences.hideMutedNotes);
|
const hideMutedNotes = useLogin(s => s.appData.json.preferences.hideMutedNotes);
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
if (hideMutedNotes) return;
|
if (hideMutedNotes) return;
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ export function NoteContextMenu({ ev, ...props }: NoteContextMenuProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sub = getCurrentSubscription(login.subscriptions);
|
const sub = getCurrentSubscription(login.subscriptions);
|
||||||
if (sub?.type === SubscriptionType.Premium && (login.appData.item.preferences.autoTranslate ?? true)) {
|
if (sub?.type === SubscriptionType.Premium && (login.appData.json.preferences.autoTranslate ?? true)) {
|
||||||
translate();
|
translate();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -30,7 +30,7 @@ export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
|
|||||||
} = useLogin(s => ({
|
} = useLogin(s => ({
|
||||||
publicKey: s.publicKey,
|
publicKey: s.publicKey,
|
||||||
readonly: s.readonly,
|
readonly: s.readonly,
|
||||||
preferences: s.appData.item.preferences,
|
preferences: s.appData.json.preferences,
|
||||||
}));
|
}));
|
||||||
const walletState = useWallet();
|
const walletState = useWallet();
|
||||||
const wallet = walletState.wallet;
|
const wallet = walletState.wallet;
|
||||||
|
@ -26,7 +26,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
|||||||
const { positive } = reactions;
|
const { positive } = reactions;
|
||||||
|
|
||||||
const { preferences: prefs, readonly } = useLogin(s => ({
|
const { preferences: prefs, readonly } = useLogin(s => ({
|
||||||
preferences: s.appData.item.preferences,
|
preferences: s.appData.json.preferences,
|
||||||
publicKey: s.publicKey,
|
publicKey: s.publicKey,
|
||||||
readonly: s.readonly,
|
readonly: s.readonly,
|
||||||
}));
|
}));
|
||||||
|
@ -16,7 +16,7 @@ export const RepostButton = ({ ev, reposts }: { ev: TaggedNostrEvent; reposts: T
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { publisher, system } = useEventPublisher();
|
const { publisher, system } = useEventPublisher();
|
||||||
const { publicKey, preferences: prefs } = useLogin(s => ({
|
const { publicKey, preferences: prefs } = useLogin(s => ({
|
||||||
preferences: s.appData.item.preferences,
|
preferences: s.appData.json.preferences,
|
||||||
publicKey: s.publicKey,
|
publicKey: s.publicKey,
|
||||||
}));
|
}));
|
||||||
const note = useNoteCreator(n => ({ show: n.show, replyTo: n.replyTo, update: n.update, quote: n.quote }));
|
const note = useNoteCreator(n => ({ show: n.show, replyTo: n.replyTo, update: n.update, quote: n.quote }));
|
||||||
|
@ -53,7 +53,7 @@ export const NoteText = memo(function InnerContent(
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!appData.item.showContentWarningPosts) {
|
if (!appData.json.showContentWarningPosts) {
|
||||||
const contentWarning = ev.tags.find(a => a[0] === "content-warning");
|
const contentWarning = ev.tags.find(a => a[0] === "content-warning");
|
||||||
if (contentWarning) {
|
if (contentWarning) {
|
||||||
return (
|
return (
|
||||||
|
@ -27,7 +27,7 @@ export default function Poll(props: PollProps) {
|
|||||||
preferences: prefs,
|
preferences: prefs,
|
||||||
publicKey: myPubKey,
|
publicKey: myPubKey,
|
||||||
relays,
|
relays,
|
||||||
} = useLogin(s => ({ preferences: s.appData.item.preferences, publicKey: s.publicKey, relays: s.relays }));
|
} = useLogin(s => ({ preferences: s.appData.json.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("");
|
||||||
|
@ -17,7 +17,7 @@ interface RevealMediaProps {
|
|||||||
|
|
||||||
export default function RevealMedia(props: RevealMediaProps) {
|
export default function RevealMedia(props: RevealMediaProps) {
|
||||||
const { preferences, follows, publicKey } = useLogin(s => ({
|
const { preferences, follows, publicKey } = useLogin(s => ({
|
||||||
preferences: s.appData.item.preferences,
|
preferences: s.appData.json.preferences,
|
||||||
follows: s.follows.item,
|
follows: s.follows.item,
|
||||||
publicKey: s.publicKey,
|
publicKey: s.publicKey,
|
||||||
}));
|
}));
|
||||||
|
@ -19,7 +19,7 @@ export function RootTabs({ base = "/" }: { base: string }) {
|
|||||||
} = useLogin(s => ({
|
} = useLogin(s => ({
|
||||||
publicKey: s.publicKey,
|
publicKey: s.publicKey,
|
||||||
tags: s.tags,
|
tags: s.tags,
|
||||||
preferences: s.appData.item.preferences,
|
preferences: s.appData.json.preferences,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const menuItems = useMemo(() => rootTabItems(base, pubKey, tags), [base, pubKey, tags]);
|
const menuItems = useMemo(() => rootTabItems(base, pubKey, tags), [base, pubKey, tags]);
|
||||||
|
@ -5,7 +5,7 @@ import { LangOverride } from "@/Components/IntlProvider/langStore";
|
|||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
|
||||||
export function useLocale() {
|
export function useLocale() {
|
||||||
const { language } = useLogin(s => ({ language: s.appData.item.preferences.language }));
|
const { language } = useLogin(s => ({ language: s.appData.json.preferences.language }));
|
||||||
const loggedOutLang = useSyncExternalStore(
|
const loggedOutLang = useSyncExternalStore(
|
||||||
c => LangOverride.hook(c),
|
c => LangOverride.hook(c),
|
||||||
() => LangOverride.snapshot(),
|
() => LangOverride.snapshot(),
|
||||||
|
@ -104,8 +104,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.appData.item.preferences.pow) {
|
if (login.appData.json.preferences.pow) {
|
||||||
pub.pow(login.appData.item.preferences.pow, GetPowWorker());
|
pub.pow(login.appData.json.preferences.pow, GetPowWorker());
|
||||||
}
|
}
|
||||||
LoginStore.setPublisher(login.id, pub);
|
LoginStore.setPublisher(login.id, pub);
|
||||||
LoginStore.updateSession({
|
LoginStore.updateSession({
|
||||||
@ -121,8 +121,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.appData.item.preferences.pow) {
|
if (login.appData.json.preferences.pow) {
|
||||||
pub.pow(login.appData.item.preferences.pow, GetPowWorker());
|
pub.pow(login.appData.json.preferences.pow, GetPowWorker());
|
||||||
}
|
}
|
||||||
LoginStore.setPublisher(login.id, pub);
|
LoginStore.setPublisher(login.id, pub);
|
||||||
LoginStore.updateSession({
|
LoginStore.updateSession({
|
||||||
|
@ -22,7 +22,7 @@ export function ZapModalInput(props: {
|
|||||||
onNextStage: (v: SendSatsInputSelection) => Promise<void>;
|
onNextStage: (v: SendSatsInputSelection) => Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const { defaultZapAmount, readonly } = useLogin(s => ({
|
const { defaultZapAmount, readonly } = useLogin(s => ({
|
||||||
defaultZapAmount: s.appData.item.preferences.defaultZapAmount,
|
defaultZapAmount: s.appData.json.preferences.defaultZapAmount,
|
||||||
readonly: s.readonly,
|
readonly: s.readonly,
|
||||||
}));
|
}));
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import { EventKind, NostrLink, parseRelayTags, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
import { EventKind, NostrLink, NostrPrefix, parseRelayTags, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import { usePrevious } from "@uidotdev/usehooks";
|
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
import { Nip28ChatSystem } from "@/chat/nip28";
|
import { Nip28ChatSystem } from "@/chat/nip28";
|
||||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import { bech32ToHex, debounce, getNewest, getNewestEventTagsByKey, unwrap } from "@/Utils";
|
import { bech32ToHex, getNewest, getNewestEventTagsByKey, unwrap } from "@/Utils";
|
||||||
import { SnortPubKey } from "@/Utils/Const";
|
import { SnortPubKey } from "@/Utils/Const";
|
||||||
import {
|
import {
|
||||||
addSubscription,
|
addSubscription,
|
||||||
LoginStore,
|
LoginStore,
|
||||||
setAppData,
|
|
||||||
setBlocked,
|
setBlocked,
|
||||||
setBookmarked,
|
setBookmarked,
|
||||||
setFollows,
|
setFollows,
|
||||||
@ -19,7 +17,6 @@ import {
|
|||||||
setPinned,
|
setPinned,
|
||||||
setRelays,
|
setRelays,
|
||||||
setTags,
|
setTags,
|
||||||
SnortAppData,
|
|
||||||
} from "@/Utils/Login";
|
} from "@/Utils/Login";
|
||||||
import { SubscriptionEvent } from "@/Utils/Subscription";
|
import { SubscriptionEvent } from "@/Utils/Subscription";
|
||||||
/**
|
/**
|
||||||
@ -31,22 +28,15 @@ export default function useLoginFeed() {
|
|||||||
const { publisher, system } = useEventPublisher();
|
const { publisher, system } = useEventPublisher();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
system.checkSigs = login.appData.item.preferences.checkSigs;
|
if (login.appData.json) {
|
||||||
}, [login]);
|
system.checkSigs = login.appData.json.preferences.checkSigs;
|
||||||
|
|
||||||
const previous = usePrevious(login.appData.item);
|
if (publisher) {
|
||||||
// write appdata after 10s of no changes
|
const link = new NostrLink(NostrPrefix.Address, "snort", EventKind.AppData, pubKey);
|
||||||
useEffect(() => {
|
login.appData.sync(link, publisher.signer, system);
|
||||||
if (!previous || JSON.stringify(previous) === JSON.stringify(login.appData.item)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return debounce(10_000, async () => {
|
|
||||||
if (publisher && login.appData.item) {
|
|
||||||
const ev = await publisher.appData(login.appData.item, "snort");
|
|
||||||
await system.BroadcastEvent(ev);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}, [previous]);
|
}, [login, publisher]);
|
||||||
|
|
||||||
const subLogin = useMemo(() => {
|
const subLogin = useMemo(() => {
|
||||||
if (!login || !pubKey) return null;
|
if (!login || !pubKey) return null;
|
||||||
@ -68,7 +58,6 @@ export default function useLoginFeed() {
|
|||||||
EventKind.DirectMessage,
|
EventKind.DirectMessage,
|
||||||
]);
|
]);
|
||||||
if (CONFIG.features.subscriptions && !login.readonly) {
|
if (CONFIG.features.subscriptions && !login.readonly) {
|
||||||
b.withFilter().authors([pubKey]).kinds([EventKind.AppData]).tag("d", ["snort"]);
|
|
||||||
b.withFilter()
|
b.withFilter()
|
||||||
.relay("wss://relay.snort.social/")
|
.relay("wss://relay.snort.social/")
|
||||||
.kinds([EventKind.SnortSubscriptions])
|
.kinds([EventKind.SnortSubscriptions])
|
||||||
@ -113,13 +102,6 @@ export default function useLoginFeed() {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).then(a => addSubscription(login, ...a.filter(a => a !== undefined).map(unwrap)));
|
).then(a => addSubscription(login, ...a.filter(a => a !== undefined).map(unwrap)));
|
||||||
|
|
||||||
const appData = getNewest(loginFeed.filter(a => a.kind === EventKind.AppData));
|
|
||||||
if (appData) {
|
|
||||||
publisher.decryptGeneric(appData.content, appData.pubkey).then(d => {
|
|
||||||
setAppData(login, JSON.parse(d) as SnortAppData, appData.created_at * 1000);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [loginFeed, publisher]);
|
}, [loginFeed, publisher]);
|
||||||
|
@ -30,7 +30,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(s => s.appData.item.preferences);
|
const pref = useLogin(s => s.appData.json.preferences);
|
||||||
const { isEventMuted } = useModeration();
|
const { isEventMuted } = useModeration();
|
||||||
|
|
||||||
const createBuilder = useCallback(() => {
|
const createBuilder = useCallback(() => {
|
||||||
|
@ -11,7 +11,7 @@ export interface ImgProxySettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function useImgProxy() {
|
export default function useImgProxy() {
|
||||||
const settings = useLogin(s => s.appData.item.preferences.imgProxyConfig);
|
const settings = useLogin(s => s.appData.json.preferences.imgProxyConfig);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
proxy: (url: string, resize?: number, sha256?: string) => proxyImg(url, settings, resize, sha256),
|
proxy: (url: string, resize?: number, sha256?: string) => proxyImg(url, settings, resize, sha256),
|
||||||
|
@ -58,11 +58,11 @@ export default function useModeration() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isMutedWord(word: string) {
|
function isMutedWord(word: string) {
|
||||||
return appData.item.mutedWords.includes(word.toLowerCase());
|
return appData.json.mutedWords.includes(word.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
function isEventMuted(ev: TaggedNostrEvent | NostrEvent) {
|
function isEventMuted(ev: TaggedNostrEvent | NostrEvent) {
|
||||||
return isMuted(ev.pubkey) || appData.item.mutedWords.some(w => ev.content.toLowerCase().includes(w));
|
return isMuted(ev.pubkey) || appData.json.mutedWords.some(w => ev.content.toLowerCase().includes(w));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
16
packages/app/src/Hooks/usePreferences.ts
Normal file
16
packages/app/src/Hooks/usePreferences.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { updatePreferences, UserPreferences } from "@/Utils/Login";
|
||||||
|
|
||||||
|
import useEventPublisher from "./useEventPublisher";
|
||||||
|
import useLogin from "./useLogin";
|
||||||
|
|
||||||
|
export default function usePreferences() {
|
||||||
|
const { id, pref } = useLogin(s => ({ id: s.id, pref: s.appData.json.preferences }));
|
||||||
|
const { system } = useEventPublisher();
|
||||||
|
|
||||||
|
return {
|
||||||
|
preferences: pref,
|
||||||
|
update: async (data: UserPreferences) => {
|
||||||
|
await updatePreferences(id, data, system);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -3,7 +3,7 @@ import { useEffect } from "react";
|
|||||||
import useLogin from "./useLogin";
|
import useLogin from "./useLogin";
|
||||||
|
|
||||||
export function useTheme() {
|
export function useTheme() {
|
||||||
const { preferences } = useLogin(s => ({ preferences: s.appData.item.preferences }));
|
const { preferences } = useLogin(s => ({ preferences: s.appData.json.preferences }));
|
||||||
|
|
||||||
function setTheme(theme: "light" | "dark") {
|
function setTheme(theme: "light" | "dark") {
|
||||||
const elm = document.documentElement;
|
const elm = document.documentElement;
|
||||||
|
@ -40,7 +40,7 @@ export function SnortDeckLayout() {
|
|||||||
const login = useLogin(s => ({
|
const login = useLogin(s => ({
|
||||||
publicKey: s.publicKey,
|
publicKey: s.publicKey,
|
||||||
subscriptions: s.subscriptions,
|
subscriptions: s.subscriptions,
|
||||||
telemetry: s.appData.item.preferences.telemetry,
|
telemetry: s.appData.json.preferences.telemetry,
|
||||||
}));
|
}));
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [deckState, setDeckState] = useState<DeckState>({
|
const [deckState, setDeckState] = useState<DeckState>({
|
||||||
|
@ -27,7 +27,7 @@ export default function Index() {
|
|||||||
const { id, stalker, telemetry } = useLogin(s => ({
|
const { id, stalker, telemetry } = useLogin(s => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
stalker: s.stalker ?? false,
|
stalker: s.stalker ?? false,
|
||||||
telemetry: s.appData.item.preferences.telemetry,
|
telemetry: s.appData.json.preferences.telemetry,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useTheme();
|
useTheme();
|
||||||
|
@ -8,6 +8,6 @@ import { LoginStore } from "@/Utils/Login";
|
|||||||
export const avatar = (node: NodeObject<NodeObject<GraphNode>>) => {
|
export const avatar = (node: NodeObject<NodeObject<GraphNode>>) => {
|
||||||
const login = LoginStore.snapshot();
|
const login = LoginStore.snapshot();
|
||||||
return node.profile?.picture
|
return node.profile?.picture
|
||||||
? proxyImg(node.profile?.picture, login.appData.item.preferences.imgProxyConfig)
|
? proxyImg(node.profile?.picture, login.appData.json.preferences.imgProxyConfig)
|
||||||
: defaultAvatar(node.address);
|
: defaultAvatar(node.address);
|
||||||
};
|
};
|
||||||
|
@ -32,8 +32,8 @@ const ProfileDetails = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const follows = useFollowsFeed(id);
|
const follows = useFollowsFeed(id);
|
||||||
const { showStatus, showBadges } = useLogin(s => ({
|
const { showStatus, showBadges } = useLogin(s => ({
|
||||||
showStatus: s.appData.item.preferences.showStatus ?? false,
|
showStatus: s.appData.json.preferences.showStatus ?? false,
|
||||||
showBadges: s.appData.item.preferences.showBadges ?? false,
|
showBadges: s.appData.json.preferences.showBadges ?? false,
|
||||||
}));
|
}));
|
||||||
const [showLnQr, setShowLnQr] = useState<boolean>(false);
|
const [showLnQr, setShowLnQr] = useState<boolean>(false);
|
||||||
const badges = useProfileBadges(showBadges ? id : undefined);
|
const badges = useProfileBadges(showBadges ? id : undefined);
|
||||||
|
@ -3,7 +3,7 @@ import { RootTabRoutes } from "@/Pages/Root/RootTabRoutes";
|
|||||||
|
|
||||||
export const DefaultTab = () => {
|
export const DefaultTab = () => {
|
||||||
const { preferences, publicKey } = useLogin(s => ({
|
const { preferences, publicKey } = useLogin(s => ({
|
||||||
preferences: s.appData.item.preferences,
|
preferences: s.appData.json.preferences,
|
||||||
publicKey: s.publicKey,
|
publicKey: s.publicKey,
|
||||||
}));
|
}));
|
||||||
const tab = publicKey ? preferences.defaultRootTab : `trending/notes`;
|
const tab = publicKey ? preferences.defaultRootTab : `trending/notes`;
|
||||||
|
@ -66,7 +66,7 @@ export function ZapPoolPageInner() {
|
|||||||
values={{
|
values={{
|
||||||
number: (
|
number: (
|
||||||
<b>
|
<b>
|
||||||
<FormattedNumber value={login.appData.item.preferences.defaultZapAmount} />
|
<FormattedNumber value={login.appData.json.preferences.defaultZapAmount} />
|
||||||
</b>
|
</b>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
@ -79,13 +79,13 @@ export function ZapPoolPageInner() {
|
|||||||
values={{
|
values={{
|
||||||
nIn: (
|
nIn: (
|
||||||
<b>
|
<b>
|
||||||
<FormattedNumber value={login.appData.item.preferences.defaultZapAmount} />
|
<FormattedNumber value={login.appData.json.preferences.defaultZapAmount} />
|
||||||
</b>
|
</b>
|
||||||
),
|
),
|
||||||
nOut: (
|
nOut: (
|
||||||
<b>
|
<b>
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={ZapPoolController?.calcAllocation(login.appData.item.preferences.defaultZapAmount) ?? 0}
|
value={ZapPoolController?.calcAllocation(login.appData.json.preferences.defaultZapAmount) ?? 0}
|
||||||
/>
|
/>
|
||||||
</b>
|
</b>
|
||||||
),
|
),
|
||||||
|
@ -9,7 +9,7 @@ function ZapPoolTargetInner({ 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.appData.item.preferences.defaultZapAmount * (target.split / 100));
|
const defaultZapMount = Math.ceil(login.appData.json.preferences.defaultZapAmount * (target.split / 100));
|
||||||
return (
|
return (
|
||||||
<ProfilePreview
|
<ProfilePreview
|
||||||
pubkey={target.pubkey}
|
pubkey={target.pubkey}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { unixNowMs } from "@snort/shared";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||||
import { ToggleSwitch } from "@/Components/Icons/Toggle";
|
import { ToggleSwitch } from "@/Components/Icons/Toggle";
|
||||||
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import { FixedModeration } from "@/Pages/onboarding/fixedModeration";
|
import { FixedModeration } from "@/Pages/onboarding/fixedModeration";
|
||||||
import { appendDedupe } from "@/Utils";
|
import { appendDedupe } from "@/Utils";
|
||||||
@ -15,6 +15,7 @@ export function Moderation() {
|
|||||||
const [topics, setTopics] = useState<Array<string>>(Object.keys(FixedModeration));
|
const [topics, setTopics] = useState<Array<string>>(Object.keys(FixedModeration));
|
||||||
const [extraTerms, setExtraTerms] = useState("");
|
const [extraTerms, setExtraTerms] = useState("");
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { system } = useEventPublisher();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col g24">
|
<div className="flex flex-col g24">
|
||||||
@ -81,15 +82,10 @@ export function Moderation() {
|
|||||||
.filter(a => a.length > 1),
|
.filter(a => a.length > 1),
|
||||||
);
|
);
|
||||||
if (words.length > 0) {
|
if (words.length > 0) {
|
||||||
updateAppData(id, ad => {
|
updateAppData(id, system, ad => ({
|
||||||
return {
|
...ad,
|
||||||
item: {
|
mutedWords: appendDedupe(ad.mutedWords, words),
|
||||||
...ad,
|
}));
|
||||||
mutedWords: appendDedupe(ad.mutedWords, words),
|
|
||||||
},
|
|
||||||
timestamp: unixNowMs(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}}>
|
}}>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { unixNowMs } from "@snort/shared";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import { appendDedupe } from "@/Utils";
|
import { appendDedupe } from "@/Utils";
|
||||||
import { SnortAppData, updateAppData } from "@/Utils/Login";
|
import { SnortAppData, updateAppData } from "@/Utils/Login";
|
||||||
@ -9,36 +9,28 @@ import { SnortAppData, updateAppData } from "@/Utils/Login";
|
|||||||
export default function ModerationSettingsPage() {
|
export default function ModerationSettingsPage() {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const [muteWord, setMuteWord] = useState("");
|
const [muteWord, setMuteWord] = useState("");
|
||||||
const appData = login.appData.item;
|
const appData = login.appData.json;
|
||||||
|
const { system } = useEventPublisher();
|
||||||
|
|
||||||
function addMutedWord() {
|
function addMutedWord() {
|
||||||
updateAppData(login.id, ad => ({
|
updateAppData(login.id, system, ad => ({
|
||||||
item: {
|
...ad,
|
||||||
...ad,
|
mutedWords: appendDedupe(appData.mutedWords, [muteWord]),
|
||||||
mutedWords: appendDedupe(appData.mutedWords, [muteWord]),
|
|
||||||
},
|
|
||||||
timestamp: unixNowMs(),
|
|
||||||
}));
|
}));
|
||||||
setMuteWord("");
|
setMuteWord("");
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleToggle = (setting: keyof SnortAppData) => {
|
const handleToggle = (setting: keyof SnortAppData) => {
|
||||||
updateAppData(login.id, ad => ({
|
updateAppData(login.id, system, ad => ({
|
||||||
item: {
|
...ad,
|
||||||
...ad,
|
[setting]: !appData[setting],
|
||||||
[setting]: !appData[setting],
|
|
||||||
},
|
|
||||||
timestamp: unixNowMs(),
|
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
function removeMutedWord(word: string) {
|
function removeMutedWord(word: string) {
|
||||||
updateAppData(login.id, ad => ({
|
updateAppData(login.id, system, ad => ({
|
||||||
item: {
|
...ad,
|
||||||
...ad,
|
mutedWords: appData.mutedWords.filter(a => a !== word),
|
||||||
mutedWords: appData.mutedWords.filter(a => a !== word),
|
|
||||||
},
|
|
||||||
timestamp: unixNowMs(),
|
|
||||||
}));
|
}));
|
||||||
setMuteWord("");
|
setMuteWord("");
|
||||||
}
|
}
|
||||||
|
@ -1,36 +1,52 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
import "./Preferences.css";
|
import "./Preferences.css";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||||
import { AllLanguageCodes } from "@/Components/IntlProvider/IntlProviderUtils";
|
import { AllLanguageCodes } from "@/Components/IntlProvider/IntlProviderUtils";
|
||||||
import { useLocale } from "@/Components/IntlProvider/useLocale";
|
import usePreferences from "@/Hooks/usePreferences";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
|
||||||
import { unwrap } from "@/Utils";
|
import { unwrap } from "@/Utils";
|
||||||
import { DefaultImgProxy } from "@/Utils/Const";
|
import { DefaultImgProxy } from "@/Utils/Const";
|
||||||
import { updatePreferences, UserPreferences } from "@/Utils/Login";
|
import { UserPreferences } from "@/Utils/Login";
|
||||||
|
|
||||||
import messages from "./messages";
|
import messages from "./messages";
|
||||||
|
|
||||||
const PreferencesPage = () => {
|
const PreferencesPage = () => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { id, pref } = useLogin(s => ({ id: s.id, pref: s.appData.item.preferences }));
|
const { preferences, update: updatePerf } = usePreferences();
|
||||||
const { lang } = useLocale();
|
const [pref, setPref] = useState<UserPreferences>(preferences);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
async function update(obj: UserPreferences) {
|
||||||
|
try {
|
||||||
|
setError("");
|
||||||
|
await updatePerf(obj);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setError(formatMessage({ defaultMessage: "Failed to update, please try again", id: "OoZgbB" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="preferences flex flex-col g24">
|
<div className="preferences flex flex-col g24">
|
||||||
<h3>
|
<h3>
|
||||||
<FormattedMessage {...messages.Preferences} />
|
<FormattedMessage defaultMessage="Preferences" id="PCSt5T" />
|
||||||
</h3>
|
</h3>
|
||||||
|
<AsyncButton onClick={() => update(pref)}>
|
||||||
|
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
|
||||||
|
</AsyncButton>
|
||||||
|
{error && <b className="warning">{error}</b>}
|
||||||
<div className="flex justify-between w-max">
|
<div className="flex justify-between w-max">
|
||||||
<h4>
|
<h4>
|
||||||
<FormattedMessage defaultMessage="Language" id="y1Z3or" />
|
<FormattedMessage defaultMessage="Language" id="y1Z3or" />
|
||||||
</h4>
|
</h4>
|
||||||
<div>
|
<div>
|
||||||
<select
|
<select
|
||||||
value={lang}
|
value={pref.language}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
updatePreferences(id, {
|
setPref({
|
||||||
...pref,
|
...pref,
|
||||||
language: e.target.value,
|
language: e.target.value,
|
||||||
})
|
})
|
||||||
@ -54,7 +70,7 @@ const PreferencesPage = () => {
|
|||||||
<select
|
<select
|
||||||
value={pref.theme}
|
value={pref.theme}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
updatePreferences(id, {
|
setPref({
|
||||||
...pref,
|
...pref,
|
||||||
theme: e.target.value,
|
theme: e.target.value,
|
||||||
} as UserPreferences)
|
} as UserPreferences)
|
||||||
@ -79,7 +95,7 @@ const PreferencesPage = () => {
|
|||||||
<select
|
<select
|
||||||
value={pref.defaultRootTab}
|
value={pref.defaultRootTab}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
updatePreferences(id, {
|
setPref({
|
||||||
...pref,
|
...pref,
|
||||||
defaultRootTab: e.target.value,
|
defaultRootTab: e.target.value,
|
||||||
} as UserPreferences)
|
} as UserPreferences)
|
||||||
@ -112,7 +128,7 @@ const PreferencesPage = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pref.telemetry ?? true}
|
checked={pref.telemetry ?? true}
|
||||||
onChange={e => updatePreferences(id, { ...pref, telemetry: e.target.checked })}
|
onChange={e => setPref({ ...pref, telemetry: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -129,7 +145,7 @@ const PreferencesPage = () => {
|
|||||||
className="w-max"
|
className="w-max"
|
||||||
value={pref.autoLoadMedia}
|
value={pref.autoLoadMedia}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
updatePreferences(id, {
|
setPref({
|
||||||
...pref,
|
...pref,
|
||||||
autoLoadMedia: e.target.value,
|
autoLoadMedia: e.target.value,
|
||||||
} as UserPreferences)
|
} as UserPreferences)
|
||||||
@ -160,7 +176,7 @@ const PreferencesPage = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pref.checkSigs}
|
checked={pref.checkSigs}
|
||||||
onChange={e => updatePreferences(id, { ...pref, checkSigs: e.target.checked })}
|
onChange={e => setPref({ ...pref, checkSigs: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -177,7 +193,7 @@ const PreferencesPage = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pref.autoTranslate}
|
checked={pref.autoTranslate}
|
||||||
onChange={e => updatePreferences(id, { ...pref, autoTranslate: e.target.checked })}
|
onChange={e => setPref({ ...pref, autoTranslate: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -195,7 +211,7 @@ const PreferencesPage = () => {
|
|||||||
type="number"
|
type="number"
|
||||||
defaultValue={pref.pow}
|
defaultValue={pref.pow}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={e => updatePreferences(id, { ...pref, pow: parseInt(e.target.value || "0") })}
|
onChange={e => setPref({ ...pref, pow: parseInt(e.target.value || "0") })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -208,7 +224,7 @@ const PreferencesPage = () => {
|
|||||||
type="number"
|
type="number"
|
||||||
defaultValue={pref.defaultZapAmount}
|
defaultValue={pref.defaultZapAmount}
|
||||||
min={1}
|
min={1}
|
||||||
onChange={e => updatePreferences(id, { ...pref, defaultZapAmount: parseInt(e.target.value || "0") })}
|
onChange={e => setPref({ ...pref, defaultZapAmount: parseInt(e.target.value || "0") })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -225,7 +241,7 @@ const PreferencesPage = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pref.showBadges ?? false}
|
checked={pref.showBadges ?? false}
|
||||||
onChange={e => updatePreferences(id, { ...pref, showBadges: e.target.checked })}
|
onChange={e => setPref({ ...pref, showBadges: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -242,7 +258,7 @@ const PreferencesPage = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pref.showStatus ?? true}
|
checked={pref.showStatus ?? true}
|
||||||
onChange={e => updatePreferences(id, { ...pref, showStatus: e.target.checked })}
|
onChange={e => setPref({ ...pref, showStatus: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -259,7 +275,7 @@ const PreferencesPage = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pref.autoZap}
|
checked={pref.autoZap}
|
||||||
onChange={e => updatePreferences(id, { ...pref, autoZap: e.target.checked })}
|
onChange={e => setPref({ ...pref, autoZap: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -278,7 +294,7 @@ const PreferencesPage = () => {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pref.imgProxyConfig !== null}
|
checked={pref.imgProxyConfig !== null}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
updatePreferences(id, {
|
setPref({
|
||||||
...pref,
|
...pref,
|
||||||
imgProxyConfig: e.target.checked ? DefaultImgProxy : undefined,
|
imgProxyConfig: e.target.checked ? DefaultImgProxy : undefined,
|
||||||
})
|
})
|
||||||
@ -302,7 +318,7 @@ const PreferencesPage = () => {
|
|||||||
description: "Placeholder text for imgproxy url textbox",
|
description: "Placeholder text for imgproxy url textbox",
|
||||||
})}
|
})}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
updatePreferences(id, {
|
setPref({
|
||||||
...pref,
|
...pref,
|
||||||
imgProxyConfig: {
|
imgProxyConfig: {
|
||||||
...unwrap(pref.imgProxyConfig),
|
...unwrap(pref.imgProxyConfig),
|
||||||
@ -327,7 +343,7 @@ const PreferencesPage = () => {
|
|||||||
description: "Hexidecimal 'key' input for improxy",
|
description: "Hexidecimal 'key' input for improxy",
|
||||||
})}
|
})}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
updatePreferences(id, {
|
setPref({
|
||||||
...pref,
|
...pref,
|
||||||
imgProxyConfig: {
|
imgProxyConfig: {
|
||||||
...unwrap(pref.imgProxyConfig),
|
...unwrap(pref.imgProxyConfig),
|
||||||
@ -352,7 +368,7 @@ const PreferencesPage = () => {
|
|||||||
description: "Hexidecimal 'salt' input for imgproxy",
|
description: "Hexidecimal 'salt' input for imgproxy",
|
||||||
})}
|
})}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
updatePreferences(id, {
|
setPref({
|
||||||
...pref,
|
...pref,
|
||||||
imgProxyConfig: {
|
imgProxyConfig: {
|
||||||
...unwrap(pref.imgProxyConfig),
|
...unwrap(pref.imgProxyConfig),
|
||||||
@ -379,7 +395,7 @@ const PreferencesPage = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pref.enableReactions}
|
checked={pref.enableReactions}
|
||||||
onChange={e => updatePreferences(id, { ...pref, enableReactions: e.target.checked })}
|
onChange={e => setPref({ ...pref, enableReactions: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -395,7 +411,7 @@ const PreferencesPage = () => {
|
|||||||
value={pref.reactionEmoji}
|
value={pref.reactionEmoji}
|
||||||
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);
|
||||||
updatePreferences(id, {
|
setPref({
|
||||||
...pref,
|
...pref,
|
||||||
reactionEmoji: split?.[0] ?? "",
|
reactionEmoji: split?.[0] ?? "",
|
||||||
});
|
});
|
||||||
@ -415,7 +431,7 @@ const PreferencesPage = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pref.confirmReposts}
|
checked={pref.confirmReposts}
|
||||||
onChange={e => updatePreferences(id, { ...pref, confirmReposts: e.target.checked })}
|
onChange={e => setPref({ ...pref, confirmReposts: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -432,7 +448,7 @@ const PreferencesPage = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pref.autoShowLatest}
|
checked={pref.autoShowLatest}
|
||||||
onChange={e => updatePreferences(id, { ...pref, autoShowLatest: e.target.checked })}
|
onChange={e => setPref({ ...pref, autoShowLatest: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -446,7 +462,7 @@ const PreferencesPage = () => {
|
|||||||
<select
|
<select
|
||||||
value={pref.fileUploader}
|
value={pref.fileUploader}
|
||||||
onChange={e =>
|
onChange={e =>
|
||||||
updatePreferences(id, {
|
setPref({
|
||||||
...pref,
|
...pref,
|
||||||
fileUploader: e.target.value,
|
fileUploader: e.target.value,
|
||||||
} as UserPreferences)
|
} as UserPreferences)
|
||||||
@ -473,7 +489,7 @@ const PreferencesPage = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pref.showDebugMenus}
|
checked={pref.showDebugMenus}
|
||||||
onChange={e => updatePreferences(id, { ...pref, showDebugMenus: e.target.checked })}
|
onChange={e => setPref({ ...pref, showDebugMenus: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -490,10 +506,14 @@ const PreferencesPage = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={pref.hideMutedNotes}
|
checked={pref.hideMutedNotes}
|
||||||
onChange={e => updatePreferences(id, { ...pref, hideMutedNotes: e.target.checked })}
|
onChange={e => setPref({ ...pref, hideMutedNotes: e.target.checked })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<AsyncButton onClick={() => update(pref)}>
|
||||||
|
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
|
||||||
|
</AsyncButton>
|
||||||
|
{error && <b className="error">{error}</b>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -16,7 +16,7 @@ import { GiftsCache } from "@/Cache";
|
|||||||
import SnortApi from "@/External/SnortApi";
|
import SnortApi from "@/External/SnortApi";
|
||||||
import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils";
|
import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils";
|
||||||
import { Blasters } from "@/Utils/Const";
|
import { Blasters } from "@/Utils/Const";
|
||||||
import { LoginSession, LoginSessionType, LoginStore, Newest, SnortAppData, UserPreferences } from "@/Utils/Login/index";
|
import { LoginSession, LoginSessionType, LoginStore, SnortAppData, UserPreferences } from "@/Utils/Login/index";
|
||||||
import { entropyToPrivateKey, generateBip39Entropy } from "@/Utils/nip6";
|
import { entropyToPrivateKey, generateBip39Entropy } from "@/Utils/nip6";
|
||||||
import { SubscriptionEvent } from "@/Utils/Subscription";
|
import { SubscriptionEvent } from "@/Utils/Subscription";
|
||||||
|
|
||||||
@ -56,12 +56,9 @@ export function removeRelay(state: LoginSession, addr: string) {
|
|||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updatePreferences(id: string, p: UserPreferences) {
|
export async function updatePreferences(id: string, p: UserPreferences, system: SystemInterface) {
|
||||||
updateAppData(id, d => {
|
await updateAppData(id, system, d => {
|
||||||
return {
|
return { ...d, preferences: p };
|
||||||
item: { ...d, preferences: p },
|
|
||||||
timestamp: unixNowMs(),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,23 +203,19 @@ export function setBookmarked(state: LoginSession, bookmarked: Array<string>, ts
|
|||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setAppData(state: LoginSession, data: SnortAppData, ts: number) {
|
export async function setAppData(state: LoginSession, data: SnortAppData, system: SystemInterface) {
|
||||||
if (state.appData.timestamp >= ts) {
|
const pub = LoginStore.getPublisher(state.id);
|
||||||
return;
|
if (!pub) return;
|
||||||
}
|
|
||||||
state.appData.item = data;
|
await state.appData.updateJson(data, pub.signer, system);
|
||||||
state.appData.timestamp = ts;
|
|
||||||
LoginStore.updateSession(state);
|
LoginStore.updateSession(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateAppData(id: string, fn: (data: SnortAppData) => Newest<SnortAppData>) {
|
export async function updateAppData(id: string, system: SystemInterface, fn: (data: SnortAppData) => SnortAppData) {
|
||||||
const session = LoginStore.get(id);
|
const session = LoginStore.get(id);
|
||||||
if (session) {
|
if (session) {
|
||||||
const next = fn(session.appData.item);
|
const next = fn(session.appData.json);
|
||||||
if (next.timestamp > session.appData.timestamp) {
|
await setAppData(session, next, system);
|
||||||
session.appData = next;
|
|
||||||
LoginStore.updateSession(session);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { HexKey, KeyStorage, RelaySettings, u256 } from "@snort/system";
|
import { HexKey, JsonEventSync, KeyStorage, RelaySettings } from "@snort/system";
|
||||||
|
|
||||||
import { DisplayAs } from "@/Components/Feed/DisplayAsSelector";
|
import { DisplayAs } from "@/Components/Feed/DisplayAsSelector";
|
||||||
import { UserPreferences } from "@/Utils/Login/index";
|
import { UserPreferences } from "@/Utils/Login/index";
|
||||||
@ -21,6 +21,7 @@ export const enum LoginSessionType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SnortAppData {
|
export interface SnortAppData {
|
||||||
|
id: string;
|
||||||
mutedWords: Array<string>;
|
mutedWords: Array<string>;
|
||||||
showContentWarningPosts: boolean;
|
showContentWarningPosts: boolean;
|
||||||
preferences: UserPreferences;
|
preferences: UserPreferences;
|
||||||
@ -81,12 +82,12 @@ export interface LoginSession {
|
|||||||
/**
|
/**
|
||||||
* A list of event ids this user has pinned
|
* A list of event ids this user has pinned
|
||||||
*/
|
*/
|
||||||
pinned: Newest<Array<u256>>;
|
pinned: Newest<Array<string>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of event ids this user has bookmarked
|
* A list of event ids this user has bookmarked
|
||||||
*/
|
*/
|
||||||
bookmarked: Newest<Array<u256>>;
|
bookmarked: Newest<Array<string>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of pubkeys this user has muted
|
* A list of pubkeys this user has muted
|
||||||
@ -116,7 +117,7 @@ export interface LoginSession {
|
|||||||
/**
|
/**
|
||||||
* Snort application data
|
* Snort application data
|
||||||
*/
|
*/
|
||||||
appData: Newest<SnortAppData>;
|
appData: JsonEventSync<SnortAppData>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of chats which we have joined (NIP-28/NIP-29)
|
* A list of chats which we have joined (NIP-28/NIP-29)
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
import * as utils from "@noble/curves/abstract/utils";
|
import * as utils from "@noble/curves/abstract/utils";
|
||||||
import * as secp from "@noble/curves/secp256k1";
|
import * as secp from "@noble/curves/secp256k1";
|
||||||
import { ExternalStore, unwrap } from "@snort/shared";
|
import { ExternalStore, unwrap } from "@snort/shared";
|
||||||
import { EventPublisher, HexKey, KeyStorage, NotEncrypted, RelaySettings, socialGraphInstance } from "@snort/system";
|
import {
|
||||||
|
EventPublisher,
|
||||||
|
HexKey,
|
||||||
|
JsonEventSync,
|
||||||
|
KeyStorage,
|
||||||
|
NotEncrypted,
|
||||||
|
RelaySettings,
|
||||||
|
socialGraphInstance,
|
||||||
|
} from "@snort/system";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
import { createPublisher, LoginSession, LoginSessionType } from "@/Utils/Login/index";
|
import { createPublisher, LoginSession, LoginSessionType, SnortAppData } from "@/Utils/Login/index";
|
||||||
|
|
||||||
import { DefaultPreferences, UserPreferences } from "./Preferences";
|
import { DefaultPreferences } from "./Preferences";
|
||||||
|
|
||||||
const AccountStoreKey = "sessions";
|
const AccountStoreKey = "sessions";
|
||||||
const LoggedOut = {
|
const LoggedOut = {
|
||||||
@ -44,14 +52,15 @@ const LoggedOut = {
|
|||||||
latestNotification: 0,
|
latestNotification: 0,
|
||||||
readNotifications: 0,
|
readNotifications: 0,
|
||||||
subscriptions: [],
|
subscriptions: [],
|
||||||
appData: {
|
appData: new JsonEventSync<SnortAppData>(
|
||||||
item: {
|
{
|
||||||
mutedWords: [],
|
id: "",
|
||||||
preferences: DefaultPreferences,
|
preferences: DefaultPreferences,
|
||||||
|
mutedWords: [],
|
||||||
showContentWarningPosts: false,
|
showContentWarningPosts: false,
|
||||||
},
|
},
|
||||||
timestamp: 0,
|
true,
|
||||||
},
|
),
|
||||||
extraChats: [],
|
extraChats: [],
|
||||||
stalker: false,
|
stalker: false,
|
||||||
} as LoginSession;
|
} as LoginSession;
|
||||||
@ -87,19 +96,14 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
if (v.type === LoginSessionType.PrivateKey && v.readonly) {
|
if (v.type === LoginSessionType.PrivateKey && v.readonly) {
|
||||||
v.readonly = false;
|
v.readonly = false;
|
||||||
}
|
}
|
||||||
// fill possibly undefined (migrate up)
|
|
||||||
v.appData ??= {
|
|
||||||
item: {
|
|
||||||
mutedWords: [],
|
|
||||||
showContentWarningPosts: false,
|
|
||||||
preferences: DefaultPreferences,
|
|
||||||
},
|
|
||||||
timestamp: 0,
|
|
||||||
};
|
|
||||||
v.extraChats ??= [];
|
v.extraChats ??= [];
|
||||||
if (v.privateKeyData) {
|
if (v.privateKeyData) {
|
||||||
v.privateKeyData = KeyStorage.fromPayload(v.privateKeyData as object);
|
v.privateKeyData = KeyStorage.fromPayload(v.privateKeyData as object);
|
||||||
}
|
}
|
||||||
|
v.appData = new JsonEventSync<SnortAppData>(v.appData as unknown as SnortAppData, true);
|
||||||
|
v.appData.on("change", () => {
|
||||||
|
this.#save();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.#loadIrisKeyIfExists();
|
this.#loadIrisKeyIfExists();
|
||||||
}
|
}
|
||||||
@ -163,10 +167,18 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
item: initRelays,
|
item: initRelays,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
},
|
},
|
||||||
preferences: {
|
appData: new JsonEventSync<SnortAppData>(
|
||||||
...DefaultPreferences,
|
{
|
||||||
...CONFIG.defaultPreferences,
|
id: "",
|
||||||
},
|
preferences: {
|
||||||
|
...DefaultPreferences,
|
||||||
|
...CONFIG.defaultPreferences,
|
||||||
|
},
|
||||||
|
mutedWords: [],
|
||||||
|
showContentWarningPosts: false,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
remoteSignerRelays,
|
remoteSignerRelays,
|
||||||
privateKeyData: privateKey,
|
privateKeyData: privateKey,
|
||||||
stalker: stalker ?? false,
|
stalker: stalker ?? false,
|
||||||
@ -209,10 +221,18 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
item: initRelays,
|
item: initRelays,
|
||||||
timestamp: 1,
|
timestamp: 1,
|
||||||
},
|
},
|
||||||
preferences: {
|
appData: new JsonEventSync<SnortAppData>(
|
||||||
...DefaultPreferences,
|
{
|
||||||
...CONFIG.defaultPreferences,
|
id: "",
|
||||||
},
|
preferences: {
|
||||||
|
...DefaultPreferences,
|
||||||
|
...CONFIG.defaultPreferences,
|
||||||
|
},
|
||||||
|
mutedWords: [],
|
||||||
|
showContentWarningPosts: false,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
} as LoginSession;
|
} as LoginSession;
|
||||||
|
|
||||||
if ("nostr_os" in window && window?.nostr_os) {
|
if ("nostr_os" in window && window?.nostr_os) {
|
||||||
@ -271,41 +291,10 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
#migrate() {
|
#migrate() {
|
||||||
let didMigrate = false;
|
let didMigrate = false;
|
||||||
|
|
||||||
// update session types
|
for (const [, acc] of this.#accounts) {
|
||||||
for (const [, v] of this.#accounts) {
|
if ("item" in acc.appData) {
|
||||||
if (!v.type) {
|
|
||||||
v.type = v.privateKey ? LoginSessionType.PrivateKey : LoginSessionType.Nip7;
|
|
||||||
didMigrate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add ids
|
|
||||||
for (const [, v] of this.#accounts) {
|
|
||||||
if ((v.id?.length ?? 0) === 0) {
|
|
||||||
v.id = uuid();
|
|
||||||
didMigrate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// mark readonly
|
|
||||||
for (const [, v] of this.#accounts) {
|
|
||||||
if (v.type === LoginSessionType.PublicKey && !v.readonly) {
|
|
||||||
v.readonly = true;
|
|
||||||
didMigrate = true;
|
|
||||||
}
|
|
||||||
// reset readonly on load
|
|
||||||
if (v.type === LoginSessionType.PrivateKey && v.readonly) {
|
|
||||||
v.readonly = false;
|
|
||||||
didMigrate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
didMigrate = true;
|
||||||
|
acc.appData = new JsonEventSync<SnortAppData>(acc.appData.item as SnortAppData, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,10 +313,14 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
|||||||
if (v.privateKeyData instanceof KeyStorage) {
|
if (v.privateKeyData instanceof KeyStorage) {
|
||||||
toSave.push({
|
toSave.push({
|
||||||
...v,
|
...v,
|
||||||
|
appData: v.appData.json,
|
||||||
privateKeyData: v.privateKeyData.toPayload(),
|
privateKeyData: v.privateKeyData.toPayload(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
toSave.push({ ...v });
|
toSave.push({
|
||||||
|
...v,
|
||||||
|
appData: v.appData.json,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +65,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(s => s.appData.item.preferences.fileUploader);
|
const fileUploader = useLogin(s => s.appData.json.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>();
|
||||||
|
@ -537,7 +537,7 @@ export function trackEvent(
|
|||||||
if (
|
if (
|
||||||
!import.meta.env.DEV &&
|
!import.meta.env.DEV &&
|
||||||
CONFIG.features.analytics &&
|
CONFIG.features.analytics &&
|
||||||
(LoginStore.snapshot().appData.item.preferences.telemetry ?? true)
|
(LoginStore.snapshot().appData.json.preferences.telemetry ?? true)
|
||||||
) {
|
) {
|
||||||
fetch("https://pa.v0l.io/api/event", {
|
fetch("https://pa.v0l.io/api/event", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -38,7 +38,7 @@ export * from "./pow-util";
|
|||||||
export * from "./query-optimizer";
|
export * from "./query-optimizer";
|
||||||
export * from "./encrypted";
|
export * from "./encrypted";
|
||||||
export * from "./outbox";
|
export * from "./outbox";
|
||||||
export * from "./range-sync";
|
export * from "./sync";
|
||||||
|
|
||||||
export * from "./impl/nip4";
|
export * from "./impl/nip4";
|
||||||
export * from "./impl/nip44";
|
export * from "./impl/nip44";
|
||||||
|
7
packages/system/src/sync/index.ts
Normal file
7
packages/system/src/sync/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface HasId {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "./safe-sync";
|
||||||
|
export * from "./range-sync";
|
||||||
|
export * from "./json-in-event-sync";
|
76
packages/system/src/sync/json-in-event-sync.ts
Normal file
76
packages/system/src/sync/json-in-event-sync.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { SafeSync } from "./safe-sync";
|
||||||
|
import { HasId } from ".";
|
||||||
|
import { EventExt, EventSigner, NostrEvent, NostrLink, SystemInterface } from "..";
|
||||||
|
import debug from "debug";
|
||||||
|
import EventEmitter from "eventemitter3";
|
||||||
|
import { unixNow } from "@snort/shared";
|
||||||
|
|
||||||
|
export interface JsonSyncEvents {
|
||||||
|
change: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class JsonEventSync<T extends HasId> extends EventEmitter<JsonSyncEvents> {
|
||||||
|
#log = debug("JsonEventSync");
|
||||||
|
#sync: SafeSync;
|
||||||
|
#json: T;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
initValue: T,
|
||||||
|
readonly encrypt: boolean,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this.#sync = new SafeSync();
|
||||||
|
this.#json = initValue;
|
||||||
|
|
||||||
|
this.#sync.on("change", () => this.emit("change"));
|
||||||
|
}
|
||||||
|
|
||||||
|
get json(): Readonly<T> {
|
||||||
|
const ret = { ...this.#json };
|
||||||
|
return Object.freeze(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sync(link: NostrLink, signer: EventSigner, system: SystemInterface) {
|
||||||
|
const res = await this.#sync.sync(link, system);
|
||||||
|
this.#log("Sync result %O", res);
|
||||||
|
if (res) {
|
||||||
|
if (this.encrypt) {
|
||||||
|
this.#json = JSON.parse(await signer.nip4Decrypt(res.content, await signer.getPubKey())) as T;
|
||||||
|
} else {
|
||||||
|
this.#json = JSON.parse(res.content) as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the json content in the event
|
||||||
|
* @param val
|
||||||
|
* @param signer
|
||||||
|
*/
|
||||||
|
async updateJson(val: T, signer: EventSigner, system: SystemInterface) {
|
||||||
|
this.#log("Updating: %O", val);
|
||||||
|
const next = this.#sync.value ? ({ ...this.#sync.value } as NostrEvent) : undefined;
|
||||||
|
if (!next) {
|
||||||
|
throw new Error("Cannot update with no previous value");
|
||||||
|
}
|
||||||
|
|
||||||
|
next.content = JSON.stringify(val);
|
||||||
|
next.created_at = unixNow();
|
||||||
|
|
||||||
|
const prevTag = next.tags.find(a => a[0] === "previous");
|
||||||
|
if (prevTag) {
|
||||||
|
prevTag[1] = next.id;
|
||||||
|
} else {
|
||||||
|
next.tags.push(["previous", next.id]);
|
||||||
|
}
|
||||||
|
if (this.encrypt) {
|
||||||
|
next.content = await signer.nip4Encrypt(next.content, await signer.getPubKey());
|
||||||
|
}
|
||||||
|
next.id = EventExt.createId(next);
|
||||||
|
const signed = await signer.sign(next);
|
||||||
|
|
||||||
|
await this.#sync.update(signed, system);
|
||||||
|
this.#json = val;
|
||||||
|
this.#json.id = next.id;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { unixNow } from "@snort/shared";
|
import { unixNow } from "@snort/shared";
|
||||||
import EventEmitter from "eventemitter3";
|
import EventEmitter from "eventemitter3";
|
||||||
import { ReqFilter, RequestBuilder, SystemInterface, TaggedNostrEvent } from ".";
|
import { ReqFilter, RequestBuilder, SystemInterface, TaggedNostrEvent } from "..";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When nostr was created
|
* When nostr was created
|
101
packages/system/src/sync/safe-sync.ts
Normal file
101
packages/system/src/sync/safe-sync.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import EventEmitter from "eventemitter3";
|
||||||
|
import { EventExt, EventType, NostrEvent, NostrLink, RequestBuilder, SystemInterface } from "..";
|
||||||
|
|
||||||
|
export interface SafeSyncEvents {
|
||||||
|
change: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely sync replacable events to nostr
|
||||||
|
*
|
||||||
|
* Usefule for the following critical kinds:
|
||||||
|
* 0 (Metadata)
|
||||||
|
* 3 (Contacts)
|
||||||
|
* 10002 (Relays)
|
||||||
|
* 30078 (AppData)
|
||||||
|
*/
|
||||||
|
export class SafeSync extends EventEmitter<SafeSyncEvents> {
|
||||||
|
#base: NostrEvent | undefined;
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.#base;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the latest version
|
||||||
|
* @param link A link to the kind
|
||||||
|
*/
|
||||||
|
async sync(link: NostrLink, system: SystemInterface) {
|
||||||
|
if (link.kind === undefined) {
|
||||||
|
throw new Error("Kind must be set");
|
||||||
|
}
|
||||||
|
|
||||||
|
const rb = new RequestBuilder("sync");
|
||||||
|
const f = rb.withFilter().link(link);
|
||||||
|
if (this.#base) {
|
||||||
|
f.since(this.#base.created_at);
|
||||||
|
}
|
||||||
|
const results = await system.Fetch(rb);
|
||||||
|
const res = results.find(a => link.matchesEvent(a));
|
||||||
|
if (res && res.created_at > (this.#base?.created_at ?? 0)) {
|
||||||
|
this.#base = res;
|
||||||
|
this.emit("change");
|
||||||
|
}
|
||||||
|
return this.#base;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the base value
|
||||||
|
* @param ev
|
||||||
|
*/
|
||||||
|
setBase(ev: NostrEvent) {
|
||||||
|
this.#checkForUpdate(ev, false);
|
||||||
|
this.#base = ev;
|
||||||
|
this.emit("change");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish an update for this event
|
||||||
|
* @param ev
|
||||||
|
*/
|
||||||
|
async update(ev: NostrEvent, system: SystemInterface) {
|
||||||
|
console.debug(this.#base, ev);
|
||||||
|
this.#checkForUpdate(ev, true);
|
||||||
|
|
||||||
|
const link = NostrLink.fromEvent(ev);
|
||||||
|
// always attempt to get a newer version before broadcasting
|
||||||
|
await this.sync(link, system);
|
||||||
|
this.#checkForUpdate(ev, true);
|
||||||
|
|
||||||
|
system.BroadcastEvent(ev);
|
||||||
|
this.#base = ev;
|
||||||
|
this.emit("change");
|
||||||
|
}
|
||||||
|
|
||||||
|
#checkForUpdate(ev: NostrEvent, mustExist: boolean) {
|
||||||
|
if (!this.#base) {
|
||||||
|
if (mustExist) {
|
||||||
|
throw new Error("No previous version detected");
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const prevTag = ev.tags.find(a => a[0] === "previous");
|
||||||
|
if (prevTag && prevTag[1] !== this.#base.id) {
|
||||||
|
throw new Error("Previous tag does not match our version");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
EventExt.getType(ev.kind) !== EventType.Replaceable &&
|
||||||
|
EventExt.getType(ev.kind) !== EventType.ParameterizedReplaceable
|
||||||
|
) {
|
||||||
|
throw new Error("Not a replacable event kind");
|
||||||
|
}
|
||||||
|
if (this.#base.created_at >= ev.created_at) {
|
||||||
|
throw new Error("Same version, cannot update");
|
||||||
|
}
|
||||||
|
const link = NostrLink.fromEvent(ev);
|
||||||
|
if (!link.matchesEvent(this.#base)) {
|
||||||
|
throw new Error("Invalid event");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user