feat: UserState
This commit is contained in:
@ -22,40 +22,6 @@ import { SubscriptionEvent } from "@/Utils/Subscription";
|
||||
|
||||
import { Nip7OsSigner } from "./Nip7OsSigner";
|
||||
|
||||
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) {
|
||||
if (import.meta.env.VITE_SINGLE_RELAY) {
|
||||
state.relays.item = {
|
||||
[import.meta.env.VITE_SINGLE_RELAY]: { read: true, write: true },
|
||||
};
|
||||
state.relays.timestamp = 100;
|
||||
LoginStore.updateSession(state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.relays.timestamp >= createdAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
// filter out non-websocket urls
|
||||
const filtered = new Map<string, RelaySettings>();
|
||||
for (const [k, v] of Object.entries(relays)) {
|
||||
if (k.startsWith("wss://") || k.startsWith("ws://")) {
|
||||
const url = sanitizeRelayUrl(k);
|
||||
if (url) {
|
||||
filtered.set(url, v as RelaySettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
state.relays.item = Object.fromEntries(filtered.entries());
|
||||
state.relays.timestamp = createdAt;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function removeRelay(state: LoginSession, addr: string) {
|
||||
delete state.relays.item[addr];
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function logout(id: string) {
|
||||
LoginStore.removeSession(id);
|
||||
GiftsCache.clear();
|
||||
@ -141,33 +107,6 @@ export function generateRandomKey() {
|
||||
};
|
||||
}
|
||||
|
||||
export function setTags(state: LoginSession, tags: Array<string>, ts: number) {
|
||||
if (state.tags.timestamp >= ts) {
|
||||
return;
|
||||
}
|
||||
state.tags.item = tags;
|
||||
state.tags.timestamp = ts;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function setMuted(state: LoginSession, muted: Array<string>, ts: number) {
|
||||
if (state.muted.timestamp >= ts) {
|
||||
return;
|
||||
}
|
||||
state.muted.item = muted;
|
||||
state.muted.timestamp = ts;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function setBlocked(state: LoginSession, blocked: Array<string>, ts: number) {
|
||||
if (state.blocked.timestamp >= ts) {
|
||||
return;
|
||||
}
|
||||
state.blocked.item = blocked;
|
||||
state.blocked.timestamp = ts;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function updateSession(id: string, fn: (state: LoginSession) => void) {
|
||||
const session = LoginStore.get(id);
|
||||
if (session) {
|
||||
@ -176,37 +115,19 @@ export function updateSession(id: string, fn: (state: LoginSession) => void) {
|
||||
}
|
||||
}
|
||||
|
||||
export function setPinned(state: LoginSession, pinned: Array<string>, ts: number) {
|
||||
if (state.pinned.timestamp >= ts) {
|
||||
return;
|
||||
}
|
||||
state.pinned.item = pinned;
|
||||
state.pinned.timestamp = ts;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export function setBookmarked(state: LoginSession, bookmarked: Array<string>, ts: number) {
|
||||
if (state.bookmarked.timestamp >= ts) {
|
||||
return;
|
||||
}
|
||||
state.bookmarked.item = bookmarked;
|
||||
state.bookmarked.timestamp = ts;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export async function setAppData(state: LoginSession, data: SnortAppData, system: SystemInterface) {
|
||||
export async function setAppData(state: LoginSession, data: SnortAppData) {
|
||||
const pub = LoginStore.getPublisher(state.id);
|
||||
if (!pub) return;
|
||||
|
||||
await state.appData.updateJson(data, pub.signer, system);
|
||||
await state.state.setAppData(data);
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
|
||||
export async function updateAppData(id: string, system: SystemInterface, fn: (data: SnortAppData) => SnortAppData) {
|
||||
export async function updateAppData(id: string, fn: (data: SnortAppData) => SnortAppData) {
|
||||
const session = LoginStore.get(id);
|
||||
if (session) {
|
||||
const next = fn(session.appData.json);
|
||||
await setAppData(session, next, system);
|
||||
if (session?.state.appdata) {
|
||||
const next = fn(session.state.appdata);
|
||||
await setAppData(session, next);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { HexKey, JsonEventSync, KeyStorage, RelaySettings } from "@snort/system";
|
||||
import { HexKey, KeyStorage, UserState } from "@snort/system";
|
||||
|
||||
import { DisplayAs } from "@/Components/Feed/DisplayAsSelector";
|
||||
import { UserPreferences } from "@/Utils/Login/index";
|
||||
@ -21,9 +21,6 @@ export const enum LoginSessionType {
|
||||
}
|
||||
|
||||
export interface SnortAppData {
|
||||
id: string;
|
||||
mutedWords: Array<string>;
|
||||
showContentWarningPosts: boolean;
|
||||
preferences: UserPreferences;
|
||||
}
|
||||
|
||||
@ -64,40 +61,7 @@ export interface LoginSession {
|
||||
*/
|
||||
publicKey?: HexKey;
|
||||
|
||||
/**
|
||||
* All the logged in users relays
|
||||
*/
|
||||
relays: Newest<Record<string, RelaySettings>>;
|
||||
|
||||
/**
|
||||
* A list of pubkeys this user follows
|
||||
*/
|
||||
contacts: Array<Array<string>>;
|
||||
|
||||
/**
|
||||
* A list of tags this user follows
|
||||
*/
|
||||
tags: Newest<Array<string>>;
|
||||
|
||||
/**
|
||||
* A list of event ids this user has pinned
|
||||
*/
|
||||
pinned: Newest<Array<string>>;
|
||||
|
||||
/**
|
||||
* A list of event ids this user has bookmarked
|
||||
*/
|
||||
bookmarked: Newest<Array<string>>;
|
||||
|
||||
/**
|
||||
* A list of pubkeys this user has muted
|
||||
*/
|
||||
muted: Newest<Array<HexKey>>;
|
||||
|
||||
/**
|
||||
* A list of pubkeys this user has muted privately
|
||||
*/
|
||||
blocked: Newest<Array<HexKey>>;
|
||||
state: UserState<SnortAppData>;
|
||||
|
||||
/**
|
||||
* Timestamp of last read notification
|
||||
@ -114,11 +78,6 @@ export interface LoginSession {
|
||||
*/
|
||||
remoteSignerRelays?: Array<string>;
|
||||
|
||||
/**
|
||||
* Snort application data
|
||||
*/
|
||||
appData: JsonEventSync<SnortAppData>;
|
||||
|
||||
/**
|
||||
* A list of chats which we have joined (NIP-28/NIP-29)
|
||||
*/
|
||||
|
@ -3,16 +3,14 @@ import * as utils from "@noble/curves/abstract/utils";
|
||||
import * as secp from "@noble/curves/secp256k1";
|
||||
import { ExternalStore, unwrap } from "@snort/shared";
|
||||
import {
|
||||
EventKind,
|
||||
EventPublisher,
|
||||
HexKey,
|
||||
JsonEventSync,
|
||||
KeyStorage,
|
||||
NostrLink,
|
||||
NostrPrefix,
|
||||
NotEncrypted,
|
||||
RelaySettings,
|
||||
socialGraphInstance,
|
||||
UserState,
|
||||
UserStateObject,
|
||||
} from "@snort/system";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
@ -29,8 +27,6 @@ const LoggedOut = {
|
||||
item: [],
|
||||
timestamp: 0,
|
||||
},
|
||||
contacts: [],
|
||||
follows: [],
|
||||
muted: {
|
||||
item: [],
|
||||
timestamp: 0,
|
||||
@ -54,18 +50,15 @@ const LoggedOut = {
|
||||
latestNotification: 0,
|
||||
readNotifications: 0,
|
||||
subscriptions: [],
|
||||
appData: new JsonEventSync<SnortAppData>(
|
||||
{
|
||||
id: "",
|
||||
preferences: DefaultPreferences,
|
||||
mutedWords: [],
|
||||
showContentWarningPosts: false,
|
||||
},
|
||||
new NostrLink(NostrPrefix.Address, "snort", EventKind.AppData),
|
||||
true,
|
||||
),
|
||||
extraChats: [],
|
||||
stalker: false,
|
||||
state: new UserState<SnortAppData>("", {
|
||||
initAppdata: {
|
||||
preferences: DefaultPreferences,
|
||||
},
|
||||
encryptAppdata: true,
|
||||
appdataId: "snort",
|
||||
}),
|
||||
} as LoginSession;
|
||||
|
||||
export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
@ -75,7 +68,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (typeof ServiceWorkerGlobalScope !== "undefined" && self instanceof ServiceWorkerGlobalScope) {
|
||||
if (typeof ServiceWorkerGlobalScope !== "undefined" && globalThis instanceof ServiceWorkerGlobalScope) {
|
||||
// return if sw. we might want to use localForage (idb) to share keys between sw and app
|
||||
return;
|
||||
}
|
||||
@ -103,14 +96,23 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
if (v.privateKeyData) {
|
||||
v.privateKeyData = KeyStorage.fromPayload(v.privateKeyData as object);
|
||||
}
|
||||
v.appData = new JsonEventSync<SnortAppData>(
|
||||
v.appData as unknown as SnortAppData,
|
||||
new NostrLink(NostrPrefix.Address, "snort", EventKind.AppData, v.publicKey),
|
||||
true,
|
||||
const stateObj = v.state as unknown as UserStateObject<SnortAppData> | undefined;
|
||||
const stateClass = new UserState<SnortAppData>(
|
||||
v.publicKey!,
|
||||
{
|
||||
initAppdata: stateObj?.appdata ?? {
|
||||
preferences: {
|
||||
...DefaultPreferences,
|
||||
...CONFIG.defaultPreferences,
|
||||
},
|
||||
},
|
||||
encryptAppdata: true,
|
||||
appdataId: "snort",
|
||||
},
|
||||
stateObj,
|
||||
);
|
||||
v.appData.on("change", () => {
|
||||
this.#save();
|
||||
});
|
||||
stateClass.on("change", () => this.#save());
|
||||
v.state = stateClass;
|
||||
}
|
||||
this.#loadIrisKeyIfExists();
|
||||
}
|
||||
@ -174,24 +176,22 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
item: initRelays,
|
||||
timestamp: 1,
|
||||
},
|
||||
appData: new JsonEventSync<SnortAppData>(
|
||||
{
|
||||
id: "",
|
||||
state: new UserState<SnortAppData>(key, {
|
||||
initAppdata: {
|
||||
preferences: {
|
||||
...DefaultPreferences,
|
||||
...CONFIG.defaultPreferences,
|
||||
},
|
||||
mutedWords: [],
|
||||
showContentWarningPosts: false,
|
||||
},
|
||||
new NostrLink(NostrPrefix.Address, "snort", EventKind.AppData, key),
|
||||
true,
|
||||
),
|
||||
encryptAppdata: true,
|
||||
appdataId: "snort",
|
||||
}),
|
||||
remoteSignerRelays,
|
||||
privateKeyData: privateKey,
|
||||
stalker: stalker ?? false,
|
||||
} as LoginSession;
|
||||
|
||||
newSession.state.on("change", () => this.#save());
|
||||
const pub = createPublisher(newSession);
|
||||
if (pub) {
|
||||
this.setPublisher(newSession.id, pub);
|
||||
@ -229,20 +229,18 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
item: initRelays,
|
||||
timestamp: 1,
|
||||
},
|
||||
appData: new JsonEventSync<SnortAppData>(
|
||||
{
|
||||
id: "",
|
||||
state: new UserState<SnortAppData>(pubKey, {
|
||||
initAppdata: {
|
||||
preferences: {
|
||||
...DefaultPreferences,
|
||||
...CONFIG.defaultPreferences,
|
||||
},
|
||||
mutedWords: [],
|
||||
showContentWarningPosts: false,
|
||||
},
|
||||
new NostrLink(NostrPrefix.Address, "snort", EventKind.AppData, pubKey),
|
||||
true,
|
||||
),
|
||||
encryptAppdata: true,
|
||||
appdataId: "snort",
|
||||
}),
|
||||
} as LoginSession;
|
||||
newSession.state.on("change", () => this.#save());
|
||||
|
||||
if ("nostr_os" in window && window?.nostr_os) {
|
||||
window?.nostr_os.saveKey(key.value);
|
||||
@ -300,14 +298,43 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
#migrate() {
|
||||
let didMigrate = false;
|
||||
|
||||
// delete some old keys
|
||||
for (const [, acc] of this.#accounts) {
|
||||
if ("item" in acc.appData) {
|
||||
if ("appData" in acc) {
|
||||
delete acc["appData"];
|
||||
didMigrate = true;
|
||||
}
|
||||
if ("contacts" in acc) {
|
||||
delete acc["contacts"];
|
||||
didMigrate = true;
|
||||
}
|
||||
if ("follows" in acc) {
|
||||
delete acc["follows"];
|
||||
didMigrate = true;
|
||||
}
|
||||
if ("relays" in acc) {
|
||||
delete acc["relays"];
|
||||
didMigrate = true;
|
||||
}
|
||||
if ("blocked" in acc) {
|
||||
delete acc["blocked"];
|
||||
didMigrate = true;
|
||||
}
|
||||
if ("bookmarked" in acc) {
|
||||
delete acc["bookmarked"];
|
||||
didMigrate = true;
|
||||
}
|
||||
if ("muted" in acc) {
|
||||
delete acc["muted"];
|
||||
didMigrate = true;
|
||||
}
|
||||
if ("pinned" in acc) {
|
||||
delete acc["pinned"];
|
||||
didMigrate = true;
|
||||
}
|
||||
if ("tags" in acc) {
|
||||
delete acc["tags"];
|
||||
didMigrate = true;
|
||||
acc.appData = new JsonEventSync<SnortAppData>(
|
||||
acc.appData.item as SnortAppData,
|
||||
new NostrLink(NostrPrefix.Address, "snort", EventKind.AppData, acc.publicKey),
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -326,17 +353,18 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
if (v.privateKeyData instanceof KeyStorage) {
|
||||
toSave.push({
|
||||
...v,
|
||||
appData: v.appData.json,
|
||||
state: v.state instanceof UserState ? v.state.serialize() : v.state,
|
||||
privateKeyData: v.privateKeyData.toPayload(),
|
||||
});
|
||||
} else {
|
||||
toSave.push({
|
||||
...v,
|
||||
appData: v.appData.json,
|
||||
state: v.state instanceof UserState ? v.state.serialize() : v.state,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.debug("Trying to save", toSave);
|
||||
window.localStorage.setItem(AccountStoreKey, JSON.stringify(toSave));
|
||||
this.notifyChange();
|
||||
}
|
||||
|
@ -102,6 +102,11 @@ export interface UserPreferences {
|
||||
* Hides muted notes when selected
|
||||
*/
|
||||
hideMutedNotes: boolean;
|
||||
|
||||
/**
|
||||
* Show posts with content warning
|
||||
*/
|
||||
showContentWarningPosts: boolean;
|
||||
}
|
||||
|
||||
export const DefaultPreferences = {
|
||||
@ -123,4 +128,5 @@ export const DefaultPreferences = {
|
||||
checkSigs: true,
|
||||
autoTranslate: true,
|
||||
hideMutedNotes: false,
|
||||
showContentWarningPosts: false,
|
||||
} as UserPreferences;
|
||||
|
@ -1,18 +1,22 @@
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { EventExt, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { Nip10, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
|
||||
/**
|
||||
* Get the chain key as a reply event
|
||||
*
|
||||
* ie. Get the key for which this event is replying to
|
||||
*/
|
||||
export function replyChainKey(ev: TaggedNostrEvent) {
|
||||
const t = EventExt.extractThread(ev);
|
||||
return t?.replyTo?.value ?? t?.root?.value;
|
||||
const t = Nip10.parseThread(ev);
|
||||
const tag = t?.replyTo ?? t?.root;
|
||||
return tag?.tagKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chain key of this event
|
||||
*
|
||||
* ie. Get the key which ties replies to this event
|
||||
*/
|
||||
export function chainKey(ev: TaggedNostrEvent) {
|
||||
const link = NostrLink.fromEvent(ev);
|
||||
return unwrap(link.toEventTag())[1];
|
||||
return link.tagKey;
|
||||
}
|
||||
|
@ -12,13 +12,13 @@ export function ThreadContextWrapper({ link, children }: { link: NostrLink; chil
|
||||
const location = useLocation();
|
||||
const [currentId, setCurrentId] = useState(unwrap(link.toEventTag())[1]);
|
||||
const feed = useThreadFeed(link);
|
||||
const { isBlocked } = useModeration();
|
||||
const { isMuted } = useModeration();
|
||||
|
||||
const chains = useMemo(() => {
|
||||
const chains = new Map<u256, Array<TaggedNostrEvent>>();
|
||||
if (feed) {
|
||||
feed
|
||||
?.filter(a => !isBlocked(a.pubkey))
|
||||
?.filter(a => !isMuted(a.pubkey))
|
||||
.forEach(v => {
|
||||
const replyTo = replyChainKey(v);
|
||||
if (replyTo) {
|
||||
@ -31,7 +31,7 @@ export function ThreadContextWrapper({ link, children }: { link: NostrLink; chil
|
||||
});
|
||||
}
|
||||
return chains;
|
||||
}, [feed]);
|
||||
}, [feed, isMuted]);
|
||||
|
||||
// Root is the parent of the current note or the current note if its a root note or the root of the thread
|
||||
const root = useMemo(() => {
|
||||
@ -46,7 +46,7 @@ export function ThreadContextWrapper({ link, children }: { link: NostrLink; chil
|
||||
return currentNote;
|
||||
}
|
||||
}
|
||||
}, [feed.length, currentId, location]);
|
||||
}, [feed, location.state, currentId]);
|
||||
|
||||
const ctxValue = useMemo<ThreadContextState>(() => {
|
||||
return {
|
||||
@ -56,7 +56,7 @@ export function ThreadContextWrapper({ link, children }: { link: NostrLink; chil
|
||||
data: feed,
|
||||
setCurrent: v => setCurrentId(v),
|
||||
};
|
||||
}, [root, chains]);
|
||||
}, [currentId, root, chains, feed]);
|
||||
|
||||
return <ThreadContext.Provider value={ctxValue}>{children}</ThreadContext.Provider>;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { useState } from "react";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import usePreferences from "@/Hooks/usePreferences";
|
||||
import { bech32ToHex, unwrap } from "@/Utils";
|
||||
import { KieranPubKey } from "@/Utils/Const";
|
||||
import NostrBuild from "@/Utils/Upload/NostrBuild";
|
||||
@ -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.json.preferences.fileUploader);
|
||||
const fileUploader = usePreferences(s => s.fileUploader);
|
||||
const { publisher } = useEventPublisher();
|
||||
const [progress, setProgress] = useState<Array<UploadProgress>>([]);
|
||||
const [stage, setStage] = useState<UploadStage>();
|
||||
|
@ -437,9 +437,10 @@ export function getUrlHostname(url?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeRelayUrl(url: string) {
|
||||
export function sanitizeRelayUrl(url?: string) {
|
||||
if ((url?.length ?? 0) === 0) return;
|
||||
try {
|
||||
return new URL(url).toString();
|
||||
return new URL(url!).toString();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@ -537,7 +538,7 @@ export function trackEvent(
|
||||
if (
|
||||
!import.meta.env.DEV &&
|
||||
CONFIG.features.analytics &&
|
||||
(LoginStore.snapshot().appData.json.preferences.telemetry ?? true)
|
||||
(LoginStore.snapshot().state.appdata?.preferences.telemetry ?? true)
|
||||
) {
|
||||
fetch("https://pa.v0l.io/api/event", {
|
||||
method: "POST",
|
||||
@ -547,9 +548,9 @@ export function trackEvent(
|
||||
body: JSON.stringify({
|
||||
d: CONFIG.hostname,
|
||||
n: event,
|
||||
r: document.referrer === location.href ? null : document.referrer,
|
||||
r: document.referrer === window.location.href ? null : document.referrer,
|
||||
p: props,
|
||||
u: e?.destination?.url ?? `${location.protocol}//${location.host}${location.pathname}`,
|
||||
u: e?.destination?.url ?? `${window.location.protocol}//${window.location.host}${window.location.pathname}`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user