feat: safe sync appdata

This commit is contained in:
2024-04-15 13:23:26 +01:00
parent a089ae2ec6
commit edf64e4125
41 changed files with 386 additions and 211 deletions

View File

@ -16,7 +16,7 @@ import { GiftsCache } from "@/Cache";
import SnortApi from "@/External/SnortApi";
import { bech32ToHex, dedupeById, deleteRefCode, getCountry, sanitizeRelayUrl, unwrap } from "@/Utils";
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 { SubscriptionEvent } from "@/Utils/Subscription";
@ -56,12 +56,9 @@ export function removeRelay(state: LoginSession, addr: string) {
LoginStore.updateSession(state);
}
export function updatePreferences(id: string, p: UserPreferences) {
updateAppData(id, d => {
return {
item: { ...d, preferences: p },
timestamp: unixNowMs(),
};
export async function updatePreferences(id: string, p: UserPreferences, system: SystemInterface) {
await updateAppData(id, system, d => {
return { ...d, preferences: p };
});
}
@ -206,23 +203,19 @@ export function setBookmarked(state: LoginSession, bookmarked: Array<string>, ts
LoginStore.updateSession(state);
}
export function setAppData(state: LoginSession, data: SnortAppData, ts: number) {
if (state.appData.timestamp >= ts) {
return;
}
state.appData.item = data;
state.appData.timestamp = ts;
export async function setAppData(state: LoginSession, data: SnortAppData, system: SystemInterface) {
const pub = LoginStore.getPublisher(state.id);
if (!pub) return;
await state.appData.updateJson(data, pub.signer, system);
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);
if (session) {
const next = fn(session.appData.item);
if (next.timestamp > session.appData.timestamp) {
session.appData = next;
LoginStore.updateSession(session);
}
const next = fn(session.appData.json);
await setAppData(session, next, system);
}
}

View File

@ -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 { UserPreferences } from "@/Utils/Login/index";
@ -21,6 +21,7 @@ export const enum LoginSessionType {
}
export interface SnortAppData {
id: string;
mutedWords: Array<string>;
showContentWarningPosts: boolean;
preferences: UserPreferences;
@ -81,12 +82,12 @@ export interface LoginSession {
/**
* 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
*/
bookmarked: Newest<Array<u256>>;
bookmarked: Newest<Array<string>>;
/**
* A list of pubkeys this user has muted
@ -116,7 +117,7 @@ export interface LoginSession {
/**
* Snort application data
*/
appData: Newest<SnortAppData>;
appData: JsonEventSync<SnortAppData>;
/**
* A list of chats which we have joined (NIP-28/NIP-29)

View File

@ -1,12 +1,20 @@
import * as utils from "@noble/curves/abstract/utils";
import * as secp from "@noble/curves/secp256k1";
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 { 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 LoggedOut = {
@ -44,14 +52,15 @@ const LoggedOut = {
latestNotification: 0,
readNotifications: 0,
subscriptions: [],
appData: {
item: {
mutedWords: [],
appData: new JsonEventSync<SnortAppData>(
{
id: "",
preferences: DefaultPreferences,
mutedWords: [],
showContentWarningPosts: false,
},
timestamp: 0,
},
true,
),
extraChats: [],
stalker: false,
} as LoginSession;
@ -87,19 +96,14 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
if (v.type === LoginSessionType.PrivateKey && v.readonly) {
v.readonly = false;
}
// fill possibly undefined (migrate up)
v.appData ??= {
item: {
mutedWords: [],
showContentWarningPosts: false,
preferences: DefaultPreferences,
},
timestamp: 0,
};
v.extraChats ??= [];
if (v.privateKeyData) {
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();
}
@ -163,10 +167,18 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
item: initRelays,
timestamp: 1,
},
preferences: {
...DefaultPreferences,
...CONFIG.defaultPreferences,
},
appData: new JsonEventSync<SnortAppData>(
{
id: "",
preferences: {
...DefaultPreferences,
...CONFIG.defaultPreferences,
},
mutedWords: [],
showContentWarningPosts: false,
},
true,
),
remoteSignerRelays,
privateKeyData: privateKey,
stalker: stalker ?? false,
@ -209,10 +221,18 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
item: initRelays,
timestamp: 1,
},
preferences: {
...DefaultPreferences,
...CONFIG.defaultPreferences,
},
appData: new JsonEventSync<SnortAppData>(
{
id: "",
preferences: {
...DefaultPreferences,
...CONFIG.defaultPreferences,
},
mutedWords: [],
showContentWarningPosts: false,
},
true,
),
} as LoginSession;
if ("nostr_os" in window && window?.nostr_os) {
@ -271,41 +291,10 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
#migrate() {
let didMigrate = false;
// update session types
for (const [, v] of this.#accounts) {
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"];
for (const [, acc] of this.#accounts) {
if ("item" in acc.appData) {
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) {
toSave.push({
...v,
appData: v.appData.json,
privateKeyData: v.privateKeyData.toPayload(),
});
} else {
toSave.push({ ...v });
toSave.push({
...v,
appData: v.appData.json,
});
}
}

View File

@ -65,7 +65,7 @@ export interface UploadProgress {
export type UploadStage = "starting" | "hashing" | "uploading" | "done" | undefined;
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 [progress, setProgress] = useState<Array<UploadProgress>>([]);
const [stage, setStage] = useState<UploadStage>();

View File

@ -537,7 +537,7 @@ export function trackEvent(
if (
!import.meta.env.DEV &&
CONFIG.features.analytics &&
(LoginStore.snapshot().appData.item.preferences.telemetry ?? true)
(LoginStore.snapshot().appData.json.preferences.telemetry ?? true)
) {
fetch("https://pa.v0l.io/api/event", {
method: "POST",