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/meta": "^0.28.2",
"@solidjs/router": "^0.6.0", "@solidjs/router": "^0.6.0",
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
"@tanstack/query-persist-client-core": "^4.24.10",
"@tanstack/query-sync-storage-persister": "^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", "@tanstack/solid-query": "^4.24.10",
"@thisbeyond/solid-dnd": "^0.7.3", "@thisbeyond/solid-dnd": "^0.7.3",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
@@ -26,7 +26,6 @@
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.5.0", "@jest/globals": "^29.5.0",
"@types/mocha": "^10.0.1",
"@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1", "@typescript-eslint/parser": "^5.48.1",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
@@ -2227,48 +2226,6 @@
"url": "https://github.com/sponsors/tannerlinsley" "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": { "node_modules/@tanstack/solid-query": {
"version": "4.24.10", "version": "4.24.10",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.10.tgz", "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", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==" "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": { "node_modules/@types/node": {
"version": "17.0.34", "version": "17.0.34",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.34.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.34.tgz",
@@ -7872,7 +7823,8 @@
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "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": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
@@ -8359,18 +8311,6 @@
"node": ">=8" "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": { "node_modules/lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -9794,18 +9734,6 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/react-is": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@@ -11090,15 +11018,6 @@
"punycode": "^2.1.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "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/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": { "@tanstack/solid-query": {
"version": "4.24.10", "version": "4.24.10",
"resolved": "https://registry.npmjs.org/@tanstack/solid-query/-/solid-query-4.24.10.tgz", "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", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==" "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": { "@types/node": {
"version": "17.0.34", "version": "17.0.34",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.34.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.34.tgz",
@@ -17147,7 +17042,8 @@
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "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": { "js-yaml": {
"version": "4.1.0", "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": { "lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" "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": { "react-is": {
"version": "18.2.0", "version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@@ -19532,13 +19410,6 @@
"punycode": "^2.1.0" "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": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@@ -51,7 +51,7 @@
"@solidjs/router": "^0.6.0", "@solidjs/router": "^0.6.0",
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
"@tanstack/query-sync-storage-persister": "^4.24.10", "@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", "@tanstack/solid-query": "^4.24.10",
"@thisbeyond/solid-dnd": "^0.7.3", "@thisbeyond/solid-dnd": "^0.7.3",
"@types/lodash": "^4.14.191", "@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 { Routes, Route } from '@solidjs/router';
import { QueryClient, QueryClientProvider } from '@tanstack/solid-query'; 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 Home = lazy(() => import('@/pages/Home'));
const Hello = lazy(() => import('@/pages/Hello')); const Hello = lazy(() => import('@/pages/Hello'));
@@ -10,14 +10,20 @@ const NotFound = lazy(() => import('@/pages/NotFound'));
const queryClient = new QueryClient({}); const queryClient = new QueryClient({});
// const localStoragePersister = createSyncStoragePersister({ storage: window.localStorage }); const localStoragePersister = createSyncStoragePersister({
storage: window.localStorage,
});
// persistQueryClient({ const App: Component = () => {
// queryClient, createEffect(() => {
// persister: localStoragePersister, const [unsubscribe] = persistQueryClient({
// }); queryClient,
persister: localStoragePersister,
});
onCleanup(() => unsubscribe());
});
const App: Component = () => ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<Routes> <Routes>
<Route path="/hello" element={() => <Hello />} /> <Route path="/hello" element={() => <Hello />} />
@@ -25,6 +31,7 @@ const App: Component = () => (
<Route path="/*" element={() => <NotFound />} /> <Route path="/*" element={() => <NotFound />} />
</Routes> </Routes>
</QueryClientProvider> </QueryClientProvider>
); );
};
export default App; export default App;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,10 +14,8 @@ type TextNoteDisplayByIdProps = Omit<TextNoteDisplayProps, 'event'> & {
}; };
const TextNoteDisplayById: Component<TextNoteDisplayByIdProps> = (props) => { const TextNoteDisplayById: Component<TextNoteDisplayByIdProps> = (props) => {
const { config } = useConfig();
const { event, query: eventQuery } = useEvent(() => const { event, query: eventQuery } = useEvent(() =>
ensureNonNull([props.eventId] as const)(([eventIdNonNull]) => ({ ensureNonNull([props.eventId] as const)(([eventIdNonNull]) => ({
relayUrls: config().relayUrls,
eventId: eventIdNonNull, 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. const useDatePulser = (propsProvider: () => DatePulserProps): Accessor<Date> => {
setInterval(() => { const [currentDate, setCurrentDate] = createSignal(new Date());
createEffect(() => {
const id = setInterval(() => {
setCurrentDate(new Date()); setCurrentDate(new Date());
}, 7000); }, propsProvider().interval);
onCleanup(() => clearInterval(id));
});
const useDatePulser = (): Accessor<Date> => {
return currentDate; return currentDate;
}; };

View File

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