From 94c51d76c479619074a79b7150a527a71e626d65 Mon Sep 17 00:00:00 2001 From: Shusui MOYATANI Date: Fri, 3 Mar 2023 21:27:06 +0900 Subject: [PATCH] update --- README.md | 3 +- src/clients/useBatchedEvents.ts | 90 +++++++++++++++++++++++++++++ src/clients/useDeprecatedReposts.ts | 66 +++++++++++++++++++++ src/clients/useReactions.ts | 68 +++++----------------- src/components/Notification.tsx | 8 +-- src/components/TextNote.tsx | 29 ++++++++-- 6 files changed, 203 insertions(+), 61 deletions(-) create mode 100644 src/clients/useBatchedEvents.ts create mode 100644 src/clients/useDeprecatedReposts.ts 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>; isReactedBy: (pubkey: string) => boolean; invalidateReactions: () => Promise; - query: CreateQueryResult>; + query: CreateQueryResult>; }; -const { exec } = useBatch>(() => { - return { - interval: 2500, - executor: (tasks) => { - // TODO relayUrlsを考慮する - const [config] = useConfig(); - - const eventIdTaskMap = new Map>>( - tasks.map((task) => [task.args.eventId, task]), - ); - const eventIds = Array.from(eventIdTaskMap.keys()); - const eventIdReactionsMap = new Map>(); - - useSubscription(() => ({ - relayUrls: config().relayUrls, - filters: [{ kinds: [7], '#e': eventIds }], - continuous: false, - onEvent(event: NostrEvent) { - const reactTo = event.tags.find((e) => e[0] === 'e')?.[1]; - if (reactTo == null) return; - const task = eventIdTaskMap.get(reactTo); - // possibly, the new event received - if (task == null) return; - - const reactionsSignal = - eventIdReactionsMap.get(reactTo) ?? createSignal([]); - eventIdReactionsMap.set(reactTo, reactionsSignal); - - const [reactions, setReactions] = reactionsSignal; - - setReactions((currentReactions) => [...currentReactions, event]); - - // 初回のresolveのみが有効 - task.resolve(reactions); - }, - onEOSE() { - tasks.forEach((task) => { - task.resolve(() => []); - }); - }, - })); - }, - }; -}); +const { exec } = useBatchedEvents(() => ({ + generateKey: ({ eventId }) => eventId, + mergeFilters: (args) => { + const eventIds = args.map((arg) => arg.eventId); + return [{ kinds: [7], '#e': eventIds }]; + }, + extractKey: (event: NostrEvent) => { + return event.tags.find((e) => e[0] === 'e')?.[1]; + }, +})); const useReactions = (propsProvider: () => UseReactionsProps): UseReactions => { const props = createMemo(propsProvider); @@ -71,8 +35,8 @@ const useReactions = (propsProvider: () => UseReactionsProps): UseReactions => { const query = createQuery( () => queryKey(), - ({ queryKey, signal }) => { - const [, currentProps] = queryKey; + ({ queryKey: currentQueryKey, signal }) => { + const [, currentProps] = currentQueryKey; return timeout(15000, `useReactions: ${currentProps.eventId}`)(exec(currentProps, signal)); }, { @@ -82,7 +46,7 @@ const useReactions = (propsProvider: () => UseReactionsProps): UseReactions => { }, ); - const reactions = () => query.data?.() ?? []; + const reactions = () => query.data?.()?.events ?? []; const reactionsGroupedByContent = () => { const result = new Map(); diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx index 2d0301f..7e0452b 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notification.tsx @@ -5,11 +5,11 @@ import TextNote from '@/components/TextNote'; import Reaction from '@/components/notification/Reaction'; import DeprecatedRepost from '@/components/DeprecatedRepost'; -export type TimelineProps = { +export type NotificationProps = { events: NostrEvent[]; }; -const Timeline: Component = (props) => { +const Notification: Component = (props) => { return ( {(event) => ( @@ -21,7 +21,7 @@ const Timeline: Component = (props) => { {/* TODO ちゃんとnotification用のコンポーネント使う */} - + @@ -30,4 +30,4 @@ const Timeline: Component = (props) => { ); }; -export default Timeline; +export default Notification; diff --git a/src/components/TextNote.tsx b/src/components/TextNote.tsx index 4705de8..830fc0c 100644 --- a/src/components/TextNote.tsx +++ b/src/components/TextNote.tsx @@ -12,6 +12,7 @@ import useConfig from '@/clients/useConfig'; import usePubkey from '@/clients/usePubkey'; import useCommands from '@/clients/useCommands'; import useReactions from '@/clients/useReactions'; +import useDeprecatedReposts from '@/clients/useDeprecatedReposts'; import useDatePulser from '@/hooks/useDatePulser'; import { formatRelative } from '@/utils/formatDate'; import ColumnItem from '@/components/ColumnItem'; @@ -38,7 +39,13 @@ const TextNote: Component = (props) => { eventId: props.event.id, })); + const { reposts, isRepostedBy } = useDeprecatedReposts(() => ({ + relayUrls: config().relayUrls, + eventId: props.event.id, + })); + const isReactedByMe = createMemo(() => isReactedBy(pubkey())); + const isRepostedByMe = createMemo(() => isRepostedBy(pubkey())); const replyingToPubKeys = createMemo(() => props.event.tags.filter((tag) => tag[0] === 'p').map((e) => e[1]), @@ -79,6 +86,7 @@ const TextNote: Component = (props) => {
+ {/* TODO 画像は脆弱性回避のためにimgじゃない方法で読み込みたい */} icon = (props) => { - +
+ + 0}> +
{reposts().length}
+
+
= (props) => { -
{reactions().length}
+ 0}> +
{reactions().length}
+