diff --git a/README.md b/README.md
index 799d1e4..3df8f84 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,8 @@ along with this program. If not, see .
### 日本語
このプログラムは自由ソフトウェアです。あなたはこれを、Free Software Foundationによって発行された
-GNUアフェロー一般公衆利用許諾書(バージョン3か、それ以降のいずれかのバージョン)が定める条件の下で再頒布または改変することができます。
+GNUアフェロー一般公衆利用許諾書(バージョン3か、それ以降のいずれかのバージョン)
+が定める条件の下で再頒布または改変することができます。
このプログラムは有用であることを願って頒布されますが、 *全くの無保証* です。
*商業可能性* や *特定目的への適合性* に対する保証は言外に示されたものも含め、全く存在しません。
diff --git a/src/clients/useBatchedEvents.ts b/src/clients/useBatchedEvents.ts
new file mode 100644
index 0000000..a758247
--- /dev/null
+++ b/src/clients/useBatchedEvents.ts
@@ -0,0 +1,90 @@
+import { createSignal, createMemo, type Signal, type Accessor } from 'solid-js';
+import { type Event as NostrEvent } from 'nostr-tools/event';
+import { type Filter } from 'nostr-tools/filter';
+
+import useConfig from '@/clients/useConfig';
+import useBatch, { type Task } from '@/clients/useBatch';
+import useSubscription from '@/clients/useSubscription';
+
+export type UseBatchedEventsProps = {
+ interval?: number;
+ generateKey: (args: TaskArgs) => string | number;
+ mergeFilters: (args: TaskArgs[]) => Filter[];
+ extractKey: (event: NostrEvent) => string | number | undefined;
+};
+
+export type BatchedEvents = {
+ events: NostrEvent[];
+ completed: boolean;
+};
+
+const useBatchedEvents = (propsProvider: () => UseBatchedEventsProps) => {
+ const props = createMemo(propsProvider);
+
+ return useBatch>(() => {
+ return {
+ interval: props().interval,
+ executor: (tasks) => {
+ const { generateKey, mergeFilters, extractKey } = props();
+ // TODO relayUrlsを考慮する
+ const [config] = useConfig();
+
+ const keyTaskMap = new Map>>(
+ tasks.map((task) => [generateKey(task.args), task]),
+ );
+ const filters = mergeFilters(tasks.map((task) => task.args));
+ const keyEventSignalsMap = new Map>();
+
+ const getSignalForKey = (key: string | number): Signal => {
+ const eventsSignal =
+ keyEventSignalsMap.get(key) ??
+ createSignal({
+ events: [],
+ completed: false,
+ });
+ keyEventSignalsMap.set(key, eventsSignal);
+ return eventsSignal;
+ };
+ const didReceivedEventsForKey = (key: string | number): boolean =>
+ keyEventSignalsMap.has(key);
+
+ 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;
+
+ const [events, setEvents] = getSignalForKey(key);
+
+ setEvents((currentEvents) => ({
+ ...currentEvents,
+ events: [...currentEvents.events, event],
+ }));
+
+ task.resolve(events);
+ },
+ onEOSE: () => {
+ tasks.forEach((task) => {
+ const key = generateKey(task.args);
+ if (didReceivedEventsForKey(key)) {
+ const [, setEvents] = getSignalForKey(key);
+ setEvents((currentEvents) => ({
+ ...currentEvents,
+ completed: true,
+ }));
+ } else {
+ task.reject(new Error(`NotFound: ${key}`));
+ }
+ });
+ },
+ }));
+ },
+ };
+ });
+};
+
+export default useBatchedEvents;
diff --git a/src/clients/useDeprecatedReposts.ts b/src/clients/useDeprecatedReposts.ts
new file mode 100644
index 0000000..d2ffa06
--- /dev/null
+++ b/src/clients/useDeprecatedReposts.ts
@@ -0,0 +1,66 @@
+import { createMemo, type Accessor } from 'solid-js';
+import { type Event as NostrEvent } from 'nostr-tools/event';
+import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
+
+import useBatchedEvents, { type BatchedEvents } from '@/clients/useBatchedEvents';
+import timeout from '@/utils/timeout';
+
+export type UseDeprecatedRepostsProps = {
+ relayUrls: string[];
+ eventId: string;
+};
+
+export type UseDeprecatedReposts = {
+ reposts: Accessor;
+ isRepostedBy: (pubkey: string) => boolean;
+ invalidateDeprecatedReposts: () => Promise;
+ query: CreateQueryResult>;
+};
+
+const { exec } = useBatchedEvents(() => ({
+ generateKey: ({ eventId }) => eventId,
+ mergeFilters: (args) => {
+ const eventIds = args.map((arg) => arg.eventId);
+ return [{ kinds: [6], '#e': eventIds }];
+ },
+ extractKey: (event: NostrEvent) => {
+ return event.tags.find((e) => e[0] === 'e')?.[1];
+ },
+}));
+
+const useDeprecatedReposts = (
+ propsProvider: () => UseDeprecatedRepostsProps,
+): UseDeprecatedReposts => {
+ const props = createMemo(propsProvider);
+ const queryKey = createMemo(() => ['useDeprecatedReposts', props()] as const);
+
+ const query = createQuery(
+ () => queryKey(),
+ ({ queryKey: currentQueryKey, signal }) => {
+ const [, currentProps] = currentQueryKey;
+ return timeout(
+ 15000,
+ `useDeprecatedReposts: ${currentProps.eventId}`,
+ )(exec(currentProps, signal));
+ },
+ {
+ // 1 minutes
+ staleTime: 1 * 60 * 1000,
+ cacheTime: 1 * 60 * 1000,
+ },
+ );
+
+ const reposts = () => query.data?.()?.events ?? [];
+
+ const isRepostedBy = (pubkey: string): boolean =>
+ reposts().findIndex((event) => event.pubkey === pubkey) !== -1;
+
+ const invalidateDeprecatedReposts = (): Promise => {
+ const queryClient = useQueryClient();
+ return queryClient.invalidateQueries(queryKey());
+ };
+
+ return { reposts, isRepostedBy, invalidateDeprecatedReposts, query };
+};
+
+export default useDeprecatedReposts;
diff --git a/src/clients/useReactions.ts b/src/clients/useReactions.ts
index 97e04df..f5a1965 100644
--- a/src/clients/useReactions.ts
+++ b/src/clients/useReactions.ts
@@ -1,10 +1,8 @@
-import { createSignal, createMemo, type Signal, type Accessor } from 'solid-js';
+import { createMemo, type Accessor } from 'solid-js';
import { type Event as NostrEvent } from 'nostr-tools/event';
import { createQuery, useQueryClient, type CreateQueryResult } from '@tanstack/solid-query';
-import useConfig from '@/clients/useConfig';
-import useBatch, { type Task } from '@/clients/useBatch';
-import useSubscription from '@/clients/useSubscription';
+import useBatchedEvents, { type BatchedEvents } from '@/clients/useBatchedEvents';
import timeout from '@/utils/timeout';
export type UseReactionsProps = {
@@ -17,53 +15,19 @@ export type UseReactions = {
reactionsGroupedByContent: Accessor