fix: build
This commit is contained in:
parent
80a4b5d8e6
commit
bfdcbca08b
37
packages/app/.eslintrc.js
Normal file
37
packages/app/.eslintrc.js
Normal file
@ -0,0 +1,37 @@
|
||||
/* eslint-disable import/no-anonymous-default-export */
|
||||
module.exports = {
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint", "formatjs", "react-refresh", "simple-import-sort"],
|
||||
rules: {
|
||||
"formatjs/enforce-id": [
|
||||
"error",
|
||||
{
|
||||
idInterpolationPattern: "[sha512:contenthash:base64:6]",
|
||||
},
|
||||
],
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"react-refresh/only-export-components": "error",
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"max-lines": ["warn", { max: 300, skipBlankLines: true, skipComments: true }],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.tsx"],
|
||||
rules: {
|
||||
"max-lines": ["warn", { max: 200, skipBlankLines: true, skipComments: true }],
|
||||
},
|
||||
},
|
||||
],
|
||||
root: true,
|
||||
ignorePatterns: ["build/", "*.test.ts", "*.js"],
|
||||
env: {
|
||||
browser: true,
|
||||
worker: true,
|
||||
commonjs: true,
|
||||
node: false,
|
||||
},
|
||||
};
|
@ -1,44 +0,0 @@
|
||||
/* eslint-disable import/no-anonymous-default-export */
|
||||
export default [
|
||||
{
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint", "formatjs", "react-refresh", "simple-import-sort"],
|
||||
rules: {
|
||||
"formatjs/enforce-id": [
|
||||
"error",
|
||||
{
|
||||
idInterpolationPattern: "[sha512:contenthash:base64:6]",
|
||||
},
|
||||
],
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"react-refresh/only-export-components": "error",
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"max-lines": ["warn", { max: 300, skipBlankLines: true, skipComments: true }],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.tsx"],
|
||||
rules: {
|
||||
"max-lines": ["warn", { max: 200, skipBlankLines: true, skipComments: true }],
|
||||
},
|
||||
},
|
||||
],
|
||||
root: true,
|
||||
ignores: ["build/", "*.test.ts", "*.js"],
|
||||
env: {
|
||||
browser: true,
|
||||
worker: true,
|
||||
commonjs: true,
|
||||
node: false,
|
||||
},
|
||||
},
|
||||
];
|
@ -1,4 +1,4 @@
|
||||
import { EventKind, HexKey, NostrLink, NostrPrefix } from "@snort/system";
|
||||
import { EventKind, NostrEvent, NostrLink } from "@snort/system";
|
||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
@ -90,16 +90,12 @@ export function NoteContextMenu({ ev, ...props }: NoteContextMenuProps) {
|
||||
await navigator.clipboard.writeText(link);
|
||||
}
|
||||
|
||||
async function pin(id: HexKey) {
|
||||
if (publisher) {
|
||||
//todo: PIN note
|
||||
}
|
||||
async function pin(ev: NostrEvent) {
|
||||
await login.state.addToList(EventKind.PinList, NostrLink.fromEvent(ev), true);
|
||||
}
|
||||
|
||||
async function bookmark(id: string) {
|
||||
if (publisher) {
|
||||
//todo: bookmark note
|
||||
}
|
||||
async function bookmark(ev: NostrEvent) {
|
||||
await login.state.addToList(EventKind.BookmarksList, NostrLink.fromEvent(ev), true);
|
||||
}
|
||||
|
||||
async function copyEvent() {
|
||||
@ -129,13 +125,13 @@ export function NoteContextMenu({ ev, ...props }: NoteContextMenuProps) {
|
||||
<FormattedMessage {...messages.Share} />
|
||||
</MenuItem>
|
||||
{!login.state.isOnList(EventKind.PinList, link) && !login.readonly && (
|
||||
<MenuItem onClick={() => pin(ev.id)}>
|
||||
<MenuItem onClick={() => pin(ev)}>
|
||||
<Icon name="pin" />
|
||||
<FormattedMessage {...messages.Pin} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{!login.state.isOnList(EventKind.BookmarksList, link) && !login.readonly && (
|
||||
<MenuItem onClick={() => bookmark(ev.id)}>
|
||||
<MenuItem onClick={() => bookmark(ev)}>
|
||||
<Icon name="bookmark" />
|
||||
<FormattedMessage {...messages.Bookmark} />
|
||||
</MenuItem>
|
||||
|
@ -1,5 +1,7 @@
|
||||
import "./RootTabs.css";
|
||||
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { EventKind } from "@snort/system";
|
||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
@ -9,8 +11,6 @@ import Icon from "@/Components/Icons/Icon";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import usePreferences from "@/Hooks/usePreferences";
|
||||
import { RootTabRoutePath } from "@/Pages/Root/RootTabRoutes";
|
||||
import { EventKind } from "@snort/system";
|
||||
import { unwrap } from "@snort/shared";
|
||||
|
||||
export function RootTabs({ base = "/" }: { base: string }) {
|
||||
const navigate = useNavigate();
|
||||
|
@ -255,7 +255,7 @@ class IrisAccount extends Component<Props> {
|
||||
}
|
||||
|
||||
async declineReserved() {
|
||||
if (!confirm(`Are you sure you want to decline iris.to/${name}?`)) {
|
||||
if (!window.confirm(`Are you sure you want to decline iris.to/${this.state.newUserName}?`)) {
|
||||
return;
|
||||
}
|
||||
const login = LoginStore.snapshot();
|
||||
|
@ -1,36 +1,36 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
function useHistoryState(initialValue, key) {
|
||||
const currentHistoryState = history.state ? history.state[key] : undefined;
|
||||
function useHistoryState<T>(initialValue: T, key: string) {
|
||||
const currentHistoryState = globalThis.history.state ? globalThis.history.state[key] : undefined;
|
||||
const myInitialValue = currentHistoryState === undefined ? initialValue : currentHistoryState;
|
||||
const [state, setState] = useState(myInitialValue);
|
||||
|
||||
const latestValue = useRef(state);
|
||||
|
||||
const setHistoryState = value => {
|
||||
const newHistoryState = { ...history.state, [key]: value };
|
||||
history.replaceState(newHistoryState, "");
|
||||
const setHistoryState = (value: T) => {
|
||||
const newHistoryState = { ...globalThis.history.state, [key]: value };
|
||||
globalThis.history.replaceState(newHistoryState, "");
|
||||
latestValue.current = value;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (state !== latestValue.current) {
|
||||
setHistoryState(state);
|
||||
const newHistoryState = { ...history.state, [key]: state };
|
||||
history.replaceState(newHistoryState, "");
|
||||
const newHistoryState = { ...globalThis.history.state, [key]: state };
|
||||
globalThis.history.replaceState(newHistoryState, "");
|
||||
latestValue.current = state;
|
||||
}
|
||||
|
||||
// Cleanup logic
|
||||
return () => {
|
||||
if (state !== latestValue.current) {
|
||||
const newHistoryState = { ...history.state, [key]: state };
|
||||
history.replaceState(newHistoryState, ""); // Save the final state
|
||||
const newHistoryState = { ...globalThis.history.state, [key]: state };
|
||||
globalThis.history.replaceState(newHistoryState, ""); // Save the final state
|
||||
}
|
||||
};
|
||||
}, [state, key]);
|
||||
|
||||
const popStateListener = event => {
|
||||
const popStateListener = (event: PopStateEvent) => {
|
||||
if (event.state && key in event.state) {
|
||||
setState(event.state[key]);
|
||||
}
|
||||
|
@ -1,7 +1,15 @@
|
||||
import { EventKind, NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { dedupe } from "@snort/shared";
|
||||
import { EventKind, NostrEvent, NostrLink, TaggedNostrEvent, ToNostrEventTag } from "@snort/system";
|
||||
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import { dedupe } from "@snort/shared";
|
||||
|
||||
export class MutedWordTag implements ToNostrEventTag {
|
||||
constructor(readonly word: string) {}
|
||||
|
||||
toEventTag(): string[] | undefined {
|
||||
return ["word", this.word.toLowerCase()];
|
||||
}
|
||||
}
|
||||
|
||||
export default function useModeration() {
|
||||
const state = useLogin(s => s.state);
|
||||
@ -30,13 +38,33 @@ export default function useModeration() {
|
||||
}
|
||||
|
||||
function isMutedWord(word: string) {
|
||||
return false;
|
||||
const words = getMutedWords();
|
||||
return words.includes(word);
|
||||
}
|
||||
|
||||
async function addMutedWord(word: string) {
|
||||
await state.addToList(EventKind.MuteList, new MutedWordTag(word.toLowerCase()));
|
||||
}
|
||||
|
||||
async function removeMutedWord(word: string) {
|
||||
await state.removeFromList(EventKind.MuteList, new MutedWordTag(word.toLowerCase()));
|
||||
}
|
||||
|
||||
function isEventMuted(ev: TaggedNostrEvent | NostrEvent) {
|
||||
return isMuted(ev.pubkey) || false;
|
||||
}
|
||||
|
||||
function getMutedWords() {
|
||||
return state
|
||||
.getList(EventKind.MuteList, o => {
|
||||
if (o[0] === "word") {
|
||||
return new MutedWordTag(o[1]);
|
||||
}
|
||||
})
|
||||
.filter(a => a instanceof MutedWordTag)
|
||||
.map(a => (a as MutedWordTag).word);
|
||||
}
|
||||
|
||||
return {
|
||||
muteList: state.muted,
|
||||
mute,
|
||||
@ -45,5 +73,8 @@ export default function useModeration() {
|
||||
isMuted,
|
||||
isMutedWord,
|
||||
isEventMuted,
|
||||
addMutedWord,
|
||||
removeMutedWord,
|
||||
getMutedWords,
|
||||
};
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ export function ZapPoolDonateSection() {
|
||||
if (!CONFIG.features.zapPool) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const zapPool = useSyncExternalStore(
|
||||
c => unwrap(ZapPoolController).hook(c),
|
||||
() => unwrap(ZapPoolController).snapshot(),
|
||||
|
@ -9,9 +9,9 @@ import { Link, useParams } from "react-router-dom";
|
||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||
import Timeline from "@/Components/Feed/Timeline";
|
||||
import ProfileImage from "@/Components/User/ProfileImage";
|
||||
import { TimelineSubject } from "@/Feed/TimelineFeed";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import { formatShort } from "@/Utils/Number";
|
||||
import { TimelineSubject } from "@/Feed/TimelineFeed";
|
||||
|
||||
const HashTagsPage = () => {
|
||||
const params = useParams();
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { EventKind, NostrLink, NostrPrefix, parseNostrLink } from "@snort/system";
|
||||
import { useEventFeed } from "@snort/system-react";
|
||||
import classNames from "classnames";
|
||||
@ -13,7 +14,6 @@ import useLogin from "@/Hooks/useLogin";
|
||||
import { LogoHeader } from "@/Pages/Layout/LogoHeader";
|
||||
import NotificationsHeader from "@/Pages/Layout/NotificationsHeader";
|
||||
import { bech32ToHex } from "@/Utils";
|
||||
import { unwrap } from "@snort/shared";
|
||||
|
||||
export function Header() {
|
||||
const navigate = useNavigate();
|
||||
|
@ -9,7 +9,7 @@ import useLogin from "@/Hooks/useLogin";
|
||||
export default function RightColumn() {
|
||||
const { pubkey } = useLogin(s => ({ pubkey: s.publicKey }));
|
||||
const hideRightColumnPaths = ["/login", "/new", "/messages"];
|
||||
const show = !hideRightColumnPaths.some(path => location.pathname.startsWith(path));
|
||||
const show = !hideRightColumnPaths.some(path => globalThis.location.pathname.startsWith(path));
|
||||
|
||||
const getTitleMessage = () => {
|
||||
return pubkey ? (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { EventKind, NostrLink, TaggedNostrEvent, Nip10 } from "@snort/system";
|
||||
import { EventKind, Nip10, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
|
||||
export function getNotificationContext(ev: TaggedNostrEvent) {
|
||||
switch (ev.kind) {
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { EventKind } from "@snort/system";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import Timeline from "@/Components/Feed/Timeline";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import { EventKind } from "@snort/system";
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { TimelineSubject } from "@/Feed/TimelineFeed";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
|
||||
export function TopicsPage() {
|
||||
const { tags, pubKey } = useLogin(s => ({
|
||||
|
@ -1,39 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import { appendDedupe } from "@/Utils";
|
||||
import { SnortAppData, updateAppData } from "@/Utils/Login";
|
||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||
import useModeration from "@/Hooks/useModeration";
|
||||
import { useAllPreferences } from "@/Hooks/usePreferences";
|
||||
|
||||
export default function ModerationSettingsPage() {
|
||||
const state = useAllPreferences();
|
||||
const { addMutedWord, removeMutedWord, getMutedWords } = useModeration();
|
||||
const preferences = useAllPreferences();
|
||||
const [muteWord, setMuteWord] = useState("");
|
||||
|
||||
function addMutedWord() {
|
||||
updateAppData(login.id, ad => ({
|
||||
...ad,
|
||||
mutedWords: appendDedupe(appData.mutedWords, [muteWord]),
|
||||
}));
|
||||
setMuteWord("");
|
||||
}
|
||||
|
||||
const handleToggle = (setting: keyof SnortAppData) => {
|
||||
updateAppData(login.id, ad => ({
|
||||
...ad,
|
||||
[setting]: !appData[setting],
|
||||
}));
|
||||
};
|
||||
|
||||
function removeMutedWord(word: string) {
|
||||
updateAppData(login.id, ad => ({
|
||||
...ad,
|
||||
mutedWords: appData.mutedWords.filter(a => a !== word),
|
||||
}));
|
||||
setMuteWord("");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>
|
||||
@ -44,8 +20,13 @@ export default function ModerationSettingsPage() {
|
||||
<div className="flex items-center mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={appData.showContentWarningPosts}
|
||||
onChange={() => handleToggle("showContentWarningPosts")}
|
||||
checked={preferences.preferences.showContentWarningPosts}
|
||||
onChange={() =>
|
||||
preferences.update({
|
||||
...preferences.preferences,
|
||||
showContentWarningPosts: !preferences.preferences.showContentWarningPosts,
|
||||
})
|
||||
}
|
||||
className="mr-2"
|
||||
id="showContentWarningPosts"
|
||||
/>
|
||||
@ -67,11 +48,11 @@ export default function ModerationSettingsPage() {
|
||||
value={muteWord}
|
||||
onChange={e => setMuteWord(e.target.value.toLowerCase())}
|
||||
/>
|
||||
<button type="button" onClick={addMutedWord}>
|
||||
<AsyncButton type="button" onClick={() => addMutedWord(muteWord)}>
|
||||
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
|
||||
</button>
|
||||
</AsyncButton>
|
||||
</div>
|
||||
{appData.mutedWords.map(v => (
|
||||
{getMutedWords().map(v => (
|
||||
<div key={v} className="p br b flex items-center justify-between">
|
||||
<div>{v}</div>
|
||||
<button type="button" onClick={() => removeMutedWord(v)}>
|
||||
|
@ -100,7 +100,7 @@ export async function subscribeToNotifications(publisher: EventPublisher) {
|
||||
endpoint: sub.endpoint,
|
||||
p256dh: base64.encode(new Uint8Array(unwrap(sub.getKey("p256dh")))),
|
||||
auth: base64.encode(new Uint8Array(unwrap(sub.getKey("auth")))),
|
||||
scope: `${location.protocol}//${location.hostname}`,
|
||||
scope: `${globalThis.location.protocol}//${globalThis.location.hostname}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -134,12 +134,12 @@ class ZapPool extends ExternalStore<Array<ZapPoolRecipient>> {
|
||||
}
|
||||
|
||||
#save() {
|
||||
self.localStorage.setItem("zap-pool", JSON.stringify(this.takeSnapshot()));
|
||||
self.localStorage.setItem("zap-pool-last-payout", this.#lastPayout.toString());
|
||||
globalThis.localStorage.setItem("zap-pool", JSON.stringify(this.takeSnapshot()));
|
||||
globalThis.localStorage.setItem("zap-pool-last-payout", this.#lastPayout.toString());
|
||||
}
|
||||
|
||||
#load() {
|
||||
const existing = self.localStorage.getItem("zap-pool");
|
||||
const existing = globalThis.localStorage.getItem("zap-pool");
|
||||
if (existing) {
|
||||
const arr = JSON.parse(existing) as Array<ZapPoolRecipient>;
|
||||
this.#store = new Map(arr.map(a => [`${a.pubkey}-${a.type}`, a]));
|
||||
@ -157,7 +157,7 @@ class ZapPool extends ExternalStore<Array<ZapPoolRecipient>> {
|
||||
]);
|
||||
}
|
||||
|
||||
const lastPayout = self.localStorage.getItem("zap-pool-last-payout");
|
||||
const lastPayout = globalThis.localStorage.getItem("zap-pool-last-payout");
|
||||
if (lastPayout) {
|
||||
this.#lastPayout = Number(lastPayout);
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ import { storeRefCode, unwrap } from "@/Utils";
|
||||
import { hasWasm, wasmInit, WasmPath } from "@/Utils/wasm";
|
||||
import { Wallets } from "@/Wallet";
|
||||
import { setupWebLNWalletConfig } from "@/Wallet";
|
||||
|
||||
import { LoginStore } from "./Utils/Login";
|
||||
|
||||
async function initSite() {
|
||||
|
@ -1,7 +1,6 @@
|
||||
/// <reference lib="webworker" />
|
||||
import { CacheableResponsePlugin } from "workbox-cacheable-response";
|
||||
|
||||
import { encodeTLVEntries, NostrLink, NostrPrefix, TLVEntryType, tryParseNostrLink } from "@snort/system";
|
||||
import { CacheableResponsePlugin } from "workbox-cacheable-response";
|
||||
import { clientsClaim } from "workbox-core";
|
||||
import { ExpirationPlugin } from "workbox-expiration";
|
||||
import { precacheAndRoute, PrecacheEntry } from "workbox-precaching";
|
||||
@ -181,7 +180,7 @@ self.addEventListener("notificationclick", event => {
|
||||
if (mention.event) {
|
||||
return `/${new NostrLink(NostrPrefix.Note, mention.event).encode()}`;
|
||||
}
|
||||
} else if (ev.type == PushType.DirectMessage) {
|
||||
} else if (ev.type === PushType.DirectMessage) {
|
||||
const reaction = ev.data as CompactReaction;
|
||||
return `/messages/${encodeTLVEntries("chat4" as NostrPrefix, {
|
||||
type: TLVEntryType.Author,
|
||||
|
@ -24,6 +24,13 @@ export class NostrHashtagLink implements ToNostrEventTag {
|
||||
}
|
||||
}
|
||||
|
||||
export class UnknownTag implements ToNostrEventTag {
|
||||
constructor(readonly value: Array<string>) {}
|
||||
toEventTag(): string[] | undefined {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
export class NostrLink implements ToNostrEventTag {
|
||||
constructor(
|
||||
readonly type: NostrPrefix,
|
||||
@ -188,7 +195,7 @@ export class NostrLink implements ToNostrEventTag {
|
||||
tag: Array<string>,
|
||||
author?: string,
|
||||
kind?: number,
|
||||
fnOther?: (tag: Array<string>) => T,
|
||||
fnOther?: (tag: Array<string>) => T | undefined,
|
||||
) {
|
||||
const relays = tag.length > 2 ? [tag[2]] : undefined;
|
||||
switch (tag[0]) {
|
||||
@ -203,15 +210,12 @@ export class NostrLink implements ToNostrEventTag {
|
||||
return new NostrLink(NostrPrefix.Address, dTag, Number(kind), author, relays, tag[3]);
|
||||
}
|
||||
default: {
|
||||
if (fnOther) {
|
||||
return fnOther(tag);
|
||||
return fnOther?.(tag) ?? new UnknownTag(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error(`Unknown tag kind ${tag[0]}`);
|
||||
}
|
||||
|
||||
static fromTags<T = NostrLink>(tags: ReadonlyArray<Array<string>>, fnOther?: (tag: Array<string>) => T) {
|
||||
static fromTags<T = NostrLink>(tags: ReadonlyArray<Array<string>>, fnOther?: (tag: Array<string>) => T | undefined) {
|
||||
return removeUndefined(
|
||||
tags.map(a => {
|
||||
try {
|
||||
|
@ -396,9 +396,12 @@ export class UserState<TAppData> extends EventEmitter<UserStateEvents> {
|
||||
return false;
|
||||
}
|
||||
|
||||
getList(kind: EventKind): Array<ToNostrEventTag> {
|
||||
getList<T extends ToNostrEventTag>(
|
||||
kind: EventKind,
|
||||
fnOther?: (tag: Array<string>) => T | undefined,
|
||||
): Array<ToNostrEventTag> {
|
||||
const list = this.#standardLists.get(kind);
|
||||
return NostrLink.fromTags(list?.tags ?? []);
|
||||
return NostrLink.fromTags<T>(list?.tags ?? [], fnOther);
|
||||
}
|
||||
|
||||
serialize(): UserStateObject<TAppData> {
|
||||
|
Loading…
x
Reference in New Issue
Block a user