use localStorage for cache

This commit is contained in:
Shusui MOYATANI
2023-03-20 20:51:14 +09:00
parent 9a764ba086
commit 44a512c64b
16 changed files with 172 additions and 319 deletions

139
package-lock.json generated
View File

@@ -12,8 +12,8 @@
"@solidjs/meta": "^0.28.2",
"@solidjs/router": "^0.6.0",
"@tailwindcss/forms": "^0.5.3",
"@tanstack/query-persist-client-core": "^4.24.10",
"@tanstack/query-sync-storage-persister": "^4.24.10",
"@tanstack/react-query-persist-client": "^4.24.10",
"@tanstack/solid-query": "^4.24.10",
"@thisbeyond/solid-dnd": "^0.7.3",
"@types/lodash": "^4.14.191",
@@ -26,7 +26,6 @@
},
"devDependencies": {
"@jest/globals": "^29.5.0",
"@types/mocha": "^10.0.1",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"autoprefixer": "^10.4.13",
@@ -2227,48 +2226,6 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "4.24.10",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.24.10.tgz",
"integrity": "sha512-FY1DixytOcNNCydPQXLxuKEV7VSST32CAuJ55BjhDNqASnMLZn+6c30yQBMrODjmWMNwzfjMZnq0Vw7C62Fwow==",
"peer": true,
"dependencies": {
"@tanstack/query-core": "4.24.10",
"use-sync-external-store": "^1.2.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-native": "*"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/@tanstack/react-query-persist-client": {
"version": "4.24.10",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-4.24.10.tgz",
"integrity": "sha512-Ta8PQua5aJK5F1w1ckX1xFnA4ohNpoeLUvApxtpMb3DKfs1XmyeFaddwyhP7La/EdjTtiInBJ2TmEAjG7EqhCw==",
"dependencies": {
"@tanstack/query-persist-client-core": "4.24.10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-query": "4.24.10"
}
},
"node_modules/@tanstack/solid-query": {
"version": "4.24.10",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.10.tgz",
@@ -2420,12 +2377,6 @@
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ=="
},
"node_modules/@types/mocha": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz",
"integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==",
"dev": true
},
"node_modules/@types/node": {
"version": "17.0.34",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.34.tgz",
@@ -7872,7 +7823,8 @@
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"node_modules/js-yaml": {
"version": "4.1.0",
@@ -8359,18 +8311,6 @@
"node": ">=8"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -9794,18 +9734,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@@ -11090,15 +11018,6 @@
"punycode": "^2.1.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"peer": true,
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -13021,24 +12940,6 @@
"@tanstack/query-persist-client-core": "4.24.10"
}
},
"@tanstack/react-query": {
"version": "4.24.10",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.24.10.tgz",
"integrity": "sha512-FY1DixytOcNNCydPQXLxuKEV7VSST32CAuJ55BjhDNqASnMLZn+6c30yQBMrODjmWMNwzfjMZnq0Vw7C62Fwow==",
"peer": true,
"requires": {
"@tanstack/query-core": "4.24.10",
"use-sync-external-store": "^1.2.0"
}
},
"@tanstack/react-query-persist-client": {
"version": "4.24.10",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-4.24.10.tgz",
"integrity": "sha512-Ta8PQua5aJK5F1w1ckX1xFnA4ohNpoeLUvApxtpMb3DKfs1XmyeFaddwyhP7La/EdjTtiInBJ2TmEAjG7EqhCw==",
"requires": {
"@tanstack/query-persist-client-core": "4.24.10"
}
},
"@tanstack/solid-query": {
"version": "4.24.10",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.10.tgz",
@@ -13178,12 +13079,6 @@
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ=="
},
"@types/mocha": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz",
"integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==",
"dev": true
},
"@types/node": {
"version": "17.0.34",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.34.tgz",
@@ -17147,7 +17042,8 @@
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"js-yaml": {
"version": "4.1.0",
@@ -17523,15 +17419,6 @@
}
}
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"peer": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -18563,15 +18450,6 @@
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
},
"react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
"peer": true,
"requires": {
"loose-envify": "^1.1.0"
}
},
"react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@@ -19532,13 +19410,6 @@
"punycode": "^2.1.0"
}
},
"use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"peer": true,
"requires": {}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@@ -51,7 +51,7 @@
"@solidjs/router": "^0.6.0",
"@tailwindcss/forms": "^0.5.3",
"@tanstack/query-sync-storage-persister": "^4.24.10",
"@tanstack/react-query-persist-client": "^4.24.10",
"@tanstack/query-persist-client-core": "^4.24.10",
"@tanstack/solid-query": "^4.24.10",
"@thisbeyond/solid-dnd": "^0.7.3",
"@types/lodash": "^4.14.191",

View File

@@ -1,8 +1,8 @@
import { lazy, type Component } from 'solid-js';
import { createEffect, onCleanup, lazy, type Component } from 'solid-js';
import { Routes, Route } from '@solidjs/router';
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query';
// import { persistQueryClient } from '@tanstack/solid-query-persist-client';
// import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { persistQueryClient } from '@tanstack/query-persist-client-core';
const Home = lazy(() => import('@/pages/Home'));
const Hello = lazy(() => import('@/pages/Hello'));
@@ -10,21 +10,28 @@ const NotFound = lazy(() => import('@/pages/NotFound'));
const queryClient = new QueryClient({});
// const localStoragePersister = createSyncStoragePersister({ storage: window.localStorage });
const localStoragePersister = createSyncStoragePersister({
storage: window.localStorage,
});
// persistQueryClient({
// queryClient,
// persister: localStoragePersister,
// });
const App: Component = () => {
createEffect(() => {
const [unsubscribe] = persistQueryClient({
queryClient,
persister: localStoragePersister,
});
onCleanup(() => unsubscribe());
});
const App: Component = () => (
<QueryClientProvider client={queryClient}>
<Routes>
<Route path="/hello" element={() => <Hello />} />
<Route path="/" element={() => <Home />} />
<Route path="/*" element={() => <NotFound />} />
</Routes>
</QueryClientProvider>
);
return (
<QueryClientProvider client={queryClient}>
<Routes>
<Route path="/hello" element={() => <Hello />} />
<Route path="/" element={() => <Home />} />
<Route path="/*" element={() => <NotFound />} />
</Routes>
</QueryClientProvider>
);
};
export default App;

View File

@@ -145,7 +145,7 @@ const OtherConfig = () => {
return (
<div>
<h3 class="font-bold"></h3>
<div class="flex flex-col justify-evenly gap-2 sm:flex-row">
<div class="flex flex-col justify-evenly gap-2">
<div class="flex w-full">
<div class="flex-1">稿</div>
<ToggleButton
@@ -153,6 +153,16 @@ const OtherConfig = () => {
onClick={() => toggleKeepOpenPostForm()}
/>
</div>
{/*
<div class="flex w-full">
<div class="flex-1">リアクションのデフォルト</div>
<input
type="text"
maxlength="1"
// onBlur={handleChangeReaction}
/>
</div>
*/}
</div>
</div>
);
@@ -160,7 +170,7 @@ const OtherConfig = () => {
const ConfigUI = (props: ConfigProps) => {
return (
<Modal title="設定" onClose={props.onClose}>
<Modal onClose={props.onClose}>
<div class="max-h-[90vh] w-[640px] max-w-[100vw] overflow-y-scroll rounded bg-white p-4 shadow">
<div class="relative">
<div class="flex flex-col gap-1">

View File

@@ -61,6 +61,7 @@ const NotePostForm: Component<NotePostFormProps> = (props) => {
const close = () => {
textAreaRef?.blur();
clearText();
props.onClose();
};

View File

@@ -21,7 +21,7 @@ const Notification: Component<NotificationProps> = (props) => {
<Reaction event={event} />
</Match>
{/* TODO ちゃんとnotification用のコンポーネント使う */}
<Match when={event.kind === 6}>
<Match when={(event.kind as number) === 6}>
<DeprecatedRepost event={event} />
</Match>
</Switch>

View File

@@ -15,9 +15,7 @@ export type ProfileDisplayProps = {
};
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
const { config } = useConfig();
const { profile, query } = useProfile(() => ({
relayUrls: config().relayUrls,
pubkey: props.pubkey,
}));

View File

@@ -1,7 +1,6 @@
import { Component, Switch, Match } from 'solid-js';
import { npubEncode } from 'nostr-tools/nip19';
import useConfig from '@/nostr/useConfig';
import useProfile from '@/nostr/useProfile';
type UserNameDisplayProps = {
@@ -9,9 +8,7 @@ type UserNameDisplayProps = {
};
const UserNameDisplay: Component<UserNameDisplayProps> = (props) => {
const { config } = useConfig();
const { profile } = useProfile(() => ({
relayUrls: config().relayUrls,
pubkey: props.pubkey,
}));

View File

@@ -6,7 +6,6 @@ import ColumnItem from '@/components/ColumnItem';
import TextNoteDisplay from '@/components/textNote/TextNoteDisplay';
import UserDisplayName from '@/components/UserDisplayName';
import useConfig from '@/nostr/useConfig';
import useProfile from '@/nostr/useProfile';
import useEvent from '@/nostr/useEvent';
import { npubEncode } from 'nostr-tools/nip19';
@@ -16,15 +15,12 @@ type ReactionProps = {
};
const Reaction: Component<ReactionProps> = (props) => {
const { config } = useConfig();
const eventId = () => props.event.tags.find(([tagName]) => tagName === 'e')?.[1];
const { profile } = useProfile(() => ({
relayUrls: config().relayUrls,
pubkey: props.event.pubkey,
}));
const { event: reactedEvent, query: reactedEventQuery } = useEvent(() => ({
relayUrls: config().relayUrls,
eventId: eventId(),
}));
const isRemoved = () => reactedEventQuery.isSuccess && reactedEvent() == null;

View File

@@ -2,16 +2,13 @@ import { Show } from 'solid-js';
import { npubEncode } from 'nostr-tools/nip19';
import useProfile from '@/nostr/useProfile';
import useConfig from '@/nostr/useConfig';
export type GeneralUserMentionDisplayProps = {
pubkey: string;
};
const GeneralUserMentionDisplay = (props: GeneralUserMentionDisplayProps) => {
const { config } = useConfig();
const { profile } = useProfile(() => ({
relayUrls: config().relayUrls,
pubkey: props.pubkey,
}));

View File

@@ -51,17 +51,14 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
const actions = () => props.actions ?? true;
const { profile: author } = useProfile(() => ({
relayUrls: config().relayUrls,
pubkey: props.event.pubkey,
}));
const { reactions, isReactedBy, invalidateReactions } = useReactions(() => ({
relayUrls: config().relayUrls,
eventId: props.event.id as string, // TODO いつかなおす
}));
const { reposts, isRepostedBy, invalidateDeprecatedReposts } = useDeprecatedReposts(() => ({
relayUrls: config().relayUrls,
eventId: props.event.id as string, // TODO いつかなおす
}));

View File

@@ -14,10 +14,8 @@ type TextNoteDisplayByIdProps = Omit<TextNoteDisplayProps, 'event'> & {
};
const TextNoteDisplayById: Component<TextNoteDisplayByIdProps> = (props) => {
const { config } = useConfig();
const { event, query: eventQuery } = useEvent(() =>
ensureNonNull([props.eventId] as const)(([eventIdNonNull]) => ({
relayUrls: config().relayUrls,
eventId: eventIdNonNull,
})),
);

View File

@@ -1,13 +1,20 @@
import { createSignal, type Accessor } from 'solid-js';
import { createSignal, createEffect, onCleanup, type Accessor } from 'solid-js';
const [currentDate, setCurrentDate] = createSignal(new Date());
type DatePulserProps = {
interval: number;
};
// 7 seconds is used for the interval so that the last digit of relative time is changed.
setInterval(() => {
setCurrentDate(new Date());
}, 7000);
const useDatePulser = (propsProvider: () => DatePulserProps): Accessor<Date> => {
const [currentDate, setCurrentDate] = createSignal(new Date());
createEffect(() => {
const id = setInterval(() => {
setCurrentDate(new Date());
}, propsProvider().interval);
onCleanup(() => clearInterval(id));
});
const useDatePulser = (): Accessor<Date> => {
return currentDate;
};

View File

@@ -4,20 +4,23 @@ import useDatePulser from '@/hooks/useDatePulser';
import { formatRelative, formatAbsoluteLong, formatAbsoluteShort } from '@/utils/formatDate';
// 7 seconds is used here so that the last digit of relative time is changed.
const currentDateHigh = useDatePulser(() => ({ interval: 7000 }));
const currentDateLow = useDatePulser(() => ({ interval: 60 * 1000 }));
const useFormatDate = () => {
const { config } = useConfig();
const currentDate = useDatePulser();
return (date: Date) => {
switch (config().dateFormat) {
case 'absolute-long':
return formatAbsoluteLong(date, currentDate());
return formatAbsoluteLong(date, currentDateLow());
case 'absolute-short':
return formatAbsoluteShort(date, currentDate());
return formatAbsoluteShort(date, currentDateLow());
case 'relative':
return formatRelative(date, currentDate());
return formatRelative(date, currentDateHigh());
default:
return formatRelative(date, currentDate());
return formatRelative(date, currentDateHigh());
}
};
};

View File

@@ -1,64 +0,0 @@
import { createSignal, createMemo, type Signal, type Accessor } from 'solid-js';
import { type Event as NostrEvent, type Filter } from 'nostr-tools';
import useConfig from '@/nostr/useConfig';
import useBatch, { type Task } from '@/nostr/useBatch';
import useSubscription from '@/nostr/useSubscription';
export type UseBatchedEventProps<TaskArgs> = {
interval?: number;
generateKey: (args: TaskArgs) => string | number;
mergeFilters: (args: TaskArgs[]) => Filter[];
extractKey: (event: NostrEvent) => string | number | undefined;
};
const useBatchedEvent = <TaskArgs>(propsProvider: () => UseBatchedEventProps<TaskArgs>) => {
const props = createMemo(propsProvider);
return useBatch<TaskArgs, Accessor<NostrEvent>>(() => ({
interval: props().interval,
executor: (tasks) => {
const { generateKey, mergeFilters, extractKey } = props();
// TODO relayUrlsを考慮する
const { config } = useConfig();
const keyTaskMap = new Map<string | number, Task<TaskArgs, Accessor<NostrEvent>>>(
tasks.map((task) => [generateKey(task.args), task]),
);
const keyEventSignalMap = new Map<string | number, Signal<NostrEvent>>();
const filters = mergeFilters(tasks.map((task) => task.args));
useSubscription(() => ({
relayUrls: config().relayUrls,
filters,
continuous: false,
onEvent: (event: NostrEvent) => {
const key = extractKey(event);
if (key == null) return;
const task = keyTaskMap.get(key);
if (task == null) return;
let signal = keyEventSignalMap.get(key);
if (signal == null) {
signal = createSignal(event);
keyEventSignalMap.set(key, signal);
}
if (event.created_at > signal[0]().created_at) {
signal[1](event);
}
task.resolve(signal[0]);
},
onEOSE: () => {
tasks.forEach((task) => {
task.reject(new Error(`NotFound: ${JSON.stringify(filters)}`));
});
},
}));
},
}));
};
export default useBatchedEvent;

View File

@@ -1,9 +1,16 @@
import { createSignal, createMemo, untrack, type Accessor, type Signal } from 'solid-js';
import {
createSignal,
createEffect,
createMemo,
createRoot,
observable,
type Accessor,
type Signal,
} from 'solid-js';
import { type Event as NostrEvent, type Filter, Kind } from 'nostr-tools';
import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
import timeout from '@/utils/timeout';
import usePool from '@/nostr/usePool';
import useBatch, { type Task } from '@/nostr/useBatch';
import eventWrapper from '@/core/event';
import useSubscription from '@/nostr/useSubscription';
@@ -47,7 +54,7 @@ export type UseProfileProps = {
type UseProfile = {
profile: () => Profile | undefined;
query: CreateQueryResult<Accessor<NostrEvent> | undefined>;
query: CreateQueryResult<NostrEvent | undefined>;
};
// Textnote
@@ -57,7 +64,7 @@ export type UseTextNoteProps = {
export type UseTextNote = {
event: Accessor<NostrEvent | undefined>;
query: CreateQueryResult<Accessor<NostrEvent> | undefined>;
query: CreateQueryResult<NostrEvent | undefined>;
};
// Reactions
@@ -70,7 +77,7 @@ export type UseReactions = {
reactionsGroupedByContent: Accessor<Map<string, NostrEvent[]>>;
isReactedBy: (pubkey: string) => boolean;
invalidateReactions: () => Promise<void>;
query: CreateQueryResult<Accessor<BatchedEvents>>;
query: CreateQueryResult<NostrEvent[]>;
};
// DeprecatedReposts
@@ -82,7 +89,7 @@ export type UseDeprecatedReposts = {
reposts: Accessor<NostrEvent[]>;
isRepostedBy: (pubkey: string) => boolean;
invalidateDeprecatedReposts: () => Promise<void>;
query: CreateQueryResult<Accessor<BatchedEvents>>;
query: CreateQueryResult<NostrEvent[]>;
};
const { exec } = useBatch<TaskArg, TaskRes>(() => ({
@@ -134,7 +141,9 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
const resolveTasks = (registeredTasks: Task<TaskArg, TaskRes>[], event: NostrEvent) => {
registeredTasks.forEach((task) => {
const signal = signals.get(task.id) ?? createSignal({ events: [], completed: false });
const signal =
signals.get(task.id) ?? createRoot(() => createSignal({ events: [], completed: false }));
signals.set(task.id, signal);
const [batchedEvents, setBatchedEvents] = signal;
setBatchedEvents((current) => ({
...current,
@@ -196,6 +205,7 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
export const useProfile = (propsProvider: () => UseProfileProps | null): UseProfile => {
const props = createMemo(propsProvider);
const queryClient = useQueryClient();
const query = createQuery(
() => ['useProfile', props()] as const,
@@ -204,28 +214,34 @@ export const useProfile = (propsProvider: () => UseProfileProps | null): UseProf
if (currentProps == null) return undefined;
const { pubkey } = currentProps;
const promise = exec({ type: 'Profile', pubkey }, signal).then((batchedEvents) => {
return createMemo(() => {
const latestEvent = () => {
const { events } = batchedEvents();
if (events == null || events.length === 0)
throw new Error(`profile not found: ${pubkey}`);
if (events.length === 0) throw new Error(`profile not found: ${pubkey}`);
const latest = events.reduce((a, b) => (a.created_at > b.created_at ? a : b));
return latest;
};
observable(batchedEvents).subscribe(() => {
try {
queryClient.setQueryData(queryKey, latestEvent());
} catch (err) {
console.error(err);
}
});
return latestEvent();
});
// TODO timeoutと同時にsignalでキャンセルするようにしたい
return timeout(15000, `useProfile: ${pubkey}`)(promise);
},
{
// 5 minutes
staleTime: 5 * 60 * 1000,
cacheTime: 15 * 60 * 1000,
// profile is updated occasionally
staleTime: 5 * 60 * 1000, // 5min
cacheTime: 24 * 60 * 60 * 1000, // 1day
},
);
const profile = createMemo((): Profile | undefined => {
const event = query.data;
if (event == null) return undefined;
const { content } = event();
if (query.data == null) return undefined;
const { content } = query.data;
if (content == null || content.length === 0) return undefined;
// TODO 大きすぎたりしないかどうか、JSONかどうかのチェック
try {
@@ -239,28 +255,67 @@ export const useProfile = (propsProvider: () => UseProfileProps | null): UseProf
return { profile, query };
};
export const useReactions = (propsProvider: () => UseReactionsProps | null): UseReactions => {
const queryClient = useQueryClient();
export const useTextNote = (propsProvider: () => UseTextNoteProps | null): UseTextNote => {
const props = createMemo(propsProvider);
const queryKey = createMemo(() => ['useReactions', props()] as const);
const queryClient = useQueryClient();
const query = createQuery(
() => queryKey(),
({ queryKey: currentQueryKey, signal }) => {
const [, currentProps] = currentQueryKey;
if (currentProps == null) return () => ({ events: [], completed: false });
const { eventId: mentionedEventId } = currentProps;
const promise = exec({ type: 'Reactions', mentionedEventId }, signal);
return timeout(15000, `useReactions: ${mentionedEventId}`)(promise);
() => ['useTextNote', props()] as const,
({ queryKey, signal }) => {
const [, currentProps] = queryKey;
if (currentProps == null) return undefined;
const { eventId } = currentProps;
const promise = exec({ type: 'TextNote', eventId }, signal).then((batchedEvents) => {
const event = batchedEvents().events[0];
if (event == null) throw new Error(`event not found: ${eventId}`);
return event;
});
return timeout(15000, `useTextNote: ${eventId}`)(promise);
},
{
// 3 minutes
staleTime: 1 * 60 * 1000,
cacheTime: 3 * 60 * 1000,
// text note cannot be updated.
staleTime: 24 * 60 * 60 * 1000, // 1 day
cacheTime: 24 * 60 * 60 * 1000, // 1 day
},
);
const reactions = () => query.data?.()?.events ?? [];
const event = () => query.data;
return { event, query };
};
export const useReactions = (propsProvider: () => UseReactionsProps | null): UseReactions => {
const queryClient = useQueryClient();
const props = createMemo(propsProvider);
const genQueryKey = createMemo(() => ['useReactions', props()] as const);
const query = createQuery(
genQueryKey,
({ queryKey, signal }) => {
const [, currentProps] = queryKey;
if (currentProps == null) return [];
const { eventId: mentionedEventId } = currentProps;
const promise = exec({ type: 'Reactions', mentionedEventId }, signal).then(
(batchedEvents) => {
const events = () => batchedEvents().events;
setTimeout(() => {
observable(batchedEvents).subscribe(() => {
queryClient.setQueryData(queryKey, events());
});
});
return events();
},
);
return timeout(15000, `useReactions: ${mentionedEventId}`)(promise);
},
{
staleTime: 1 * 60 * 1000, // 1 min
cacheTime: 5 * 60 * 1000, // 5 min
},
);
const reactions = () => query.data ?? [];
const reactionsGroupedByContent = () => {
const result = new Map<string, NostrEvent[]>();
@@ -275,70 +330,50 @@ export const useReactions = (propsProvider: () => UseReactionsProps | null): Use
const isReactedBy = (pubkey: string): boolean =>
reactions().findIndex((event) => event.pubkey === pubkey) !== -1;
const invalidateReactions = (): Promise<void> => queryClient.invalidateQueries(queryKey());
const invalidateReactions = (): Promise<void> => queryClient.invalidateQueries(genQueryKey());
return { reactions, reactionsGroupedByContent, isReactedBy, invalidateReactions, query };
};
export const useTextNote = (propsProvider: () => UseTextNoteProps | null): UseTextNote => {
const props = createMemo(propsProvider);
const query = createQuery(
() => ['useEvent', props()] as const,
({ queryKey, signal }) => {
const [, currentProps] = queryKey;
if (currentProps == null) return undefined;
const { eventId } = currentProps;
const promise = exec({ type: 'TextNote', eventId }, signal).then((events) => {
return createMemo(() => {
const event = events().events[0];
if (event == null) throw new Error(`event not found: ${eventId}`);
return event;
});
});
return timeout(15000, `useEvent: ${eventId}`)(promise);
},
{
// a hour
staleTime: 60 * 60 * 1000,
cacheTime: 60 * 60 * 1000,
},
);
const event = () => query.data?.();
return { event, query };
};
export const useDeprecatedReposts = (
propsProvider: () => UseDeprecatedRepostsProps,
): UseDeprecatedReposts => {
const queryClient = useQueryClient();
const props = createMemo(propsProvider);
const queryKey = createMemo(() => ['useDeprecatedReposts', props()] as const);
const genQueryKey = createMemo(() => ['useDeprecatedReposts', props()] as const);
const query = createQuery(
() => queryKey(),
({ queryKey: currentQueryKey, signal }) => {
const [, currentProps] = currentQueryKey;
if (currentProps == null) return () => ({ events: [], completed: false });
genQueryKey,
({ queryKey, signal }) => {
const [, currentProps] = queryKey;
if (currentProps == null) return [];
const { eventId: mentionedEventId } = currentProps;
const promise = exec({ type: 'DeprecatedReposts', mentionedEventId }, signal);
const promise = exec({ type: 'DeprecatedReposts', mentionedEventId }, signal).then(
(batchedEvents) => {
const events = () => batchedEvents().events;
setTimeout(() => {
observable(batchedEvents).subscribe(() => {
queryClient.setQueryData(queryKey, events());
});
});
return events();
},
);
return timeout(15000, `useDeprecatedReposts: ${mentionedEventId}`)(promise);
},
{
// 1 minutes
staleTime: 1 * 60 * 1000,
cacheTime: 1 * 60 * 1000,
staleTime: 1 * 60 * 1000, // 1 min
cacheTime: 5 * 60 * 1000, // 5 min
},
);
const reposts = () => query.data?.()?.events ?? [];
const reposts = () => query.data ?? [];
const isRepostedBy = (pubkey: string): boolean =>
reposts().findIndex((event) => event.pubkey === pubkey) !== -1;
const invalidateDeprecatedReposts = (): Promise<void> =>
queryClient.invalidateQueries(queryKey());
queryClient.invalidateQueries(genQueryKey());
return { reposts, isRepostedBy, invalidateDeprecatedReposts, query };
};