From 3abd0dd94e28e862bc056f907d27060cf011a75a Mon Sep 17 00:00:00 2001 From: Shusui MOYATANI Date: Sun, 26 Mar 2023 11:29:38 +0900 Subject: [PATCH] feat: show thread --- src/components/Column.tsx | 52 ++++++++++----- src/components/ColumnContentDisplay.tsx | 33 ++++++++++ src/components/ColumnContext.tsx | 31 +++++++++ src/components/Config.tsx | 2 +- src/components/Modal.tsx | 2 +- src/components/ProfileDisplay.tsx | 31 +++++++-- src/components/textNote/TextNoteDisplay.tsx | 71 +++++++++++++++------ src/nostr/useBatch.ts | 2 +- src/nostr/useBatchedEvents.ts | 38 +++++++---- src/nostr/useFollowers.ts | 4 +- src/nostr/useStats.ts | 26 ++++++++ src/nostr/useSubscription.ts | 6 +- 12 files changed, 237 insertions(+), 61 deletions(-) create mode 100644 src/components/ColumnContentDisplay.tsx create mode 100644 src/components/ColumnContext.tsx create mode 100644 src/nostr/useStats.ts diff --git a/src/components/Column.tsx b/src/components/Column.tsx index 4f62d58..f743d56 100644 --- a/src/components/Column.tsx +++ b/src/components/Column.tsx @@ -1,5 +1,7 @@ -import type { Component, JSX } from 'solid-js'; +import { Show, type JSX, type Component } from 'solid-js'; import { useHandleCommand } from '@/hooks/useCommandBus'; +import { ColumnContext, useColumnState } from '@/components/ColumnContext'; +import ColumnContentDisplay from '@/components/ColumnContentDisplay'; export type ColumnProps = { name: string; @@ -12,6 +14,8 @@ export type ColumnProps = { const Column: Component = (props) => { let columnDivRef: HTMLDivElement | undefined; + const columnState = useColumnState(); + const width = () => props.width ?? 'medium'; useHandleCommand(() => ({ @@ -33,22 +37,38 @@ const Column: Component = (props) => { })); return ( -
-
- {/* 🏠 */} - {props.name} + +
+
+ {/* 🏠 */} + {props.name} +
+
    {props.children}
+ + {(columnContent) => ( +
+
+ +
+
    + +
+
+ )} +
-
    {props.children}
-
+ ); }; diff --git a/src/components/ColumnContentDisplay.tsx b/src/components/ColumnContentDisplay.tsx new file mode 100644 index 0000000..d78abe5 --- /dev/null +++ b/src/components/ColumnContentDisplay.tsx @@ -0,0 +1,33 @@ +import { Switch, Match, type Component } from 'solid-js'; + +import useConfig from '@/nostr/useConfig'; + +import { type ColumnContent } from '@/components/ColumnContext'; +import Timeline from '@/components/Timeline'; +import useSubscription from '@/nostr/useSubscription'; + +const RepliesDisplay: Component<{ eventId: string }> = (props) => { + const { config } = useConfig(); + + const { events } = useSubscription(() => ({ + relayUrls: config().relayUrls, + filters: [ + { kinds: [1], ids: [props.eventId], limit: 25 }, + { kinds: [1], '#e': [props.eventId], limit: 25 }, + ], + })); + + return ; +}; + +const ColumnContentDisplay: Component<{ columnContent: ColumnContent }> = (props) => { + return ( + + + {(replies) => } + + + ); +}; + +export default ColumnContentDisplay; diff --git a/src/components/ColumnContext.tsx b/src/components/ColumnContext.tsx new file mode 100644 index 0000000..8f52b37 --- /dev/null +++ b/src/components/ColumnContext.tsx @@ -0,0 +1,31 @@ +import { createContext, useContext, type JSX } from 'solid-js'; +import { createStore } from 'solid-js/store'; + +export type ColumnContent = { + type: 'Replies'; + eventId: string; +}; + +export type ColumnState = { + content?: ColumnContent; +}; + +export type UseColumnState = { + columnState: ColumnState; + setColumnContent: (content: ColumnContent) => void; + clearColumnContext: () => void; +}; + +export const ColumnContext = createContext(); + +export const useColumnContext = () => useContext(ColumnContext); + +export const useColumnState = (): UseColumnState => { + const [columnState, setColumnState] = createStore({}); + + return { + columnState, + setColumnContent: (content: ColumnContent) => setColumnState('content', content), + clearColumnContext: () => setColumnState('content', undefined), + }; +}; diff --git a/src/components/Config.tsx b/src/components/Config.tsx index 52f54fa..bdff71b 100644 --- a/src/components/Config.tsx +++ b/src/components/Config.tsx @@ -186,7 +186,7 @@ const ConfigUI = (props: ConfigProps) => {

設定

-
diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 430a3b3..7176d43 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -17,7 +17,7 @@ const Modal: Component = (props) => { return (
{props.children} diff --git a/src/components/ProfileDisplay.tsx b/src/components/ProfileDisplay.tsx index fe4ef0b..29ee90d 100644 --- a/src/components/ProfileDisplay.tsx +++ b/src/components/ProfileDisplay.tsx @@ -29,11 +29,11 @@ export type ProfileDisplayProps = { }; const FollowersCount: Component<{ pubkey: string }> = (props) => { - const { followersPubkeys } = useFollowers(() => ({ + const { count } = useFollowers(() => ({ pubkey: props.pubkey, })); - return {followersPubkeys().length}; + return <>{count()}; }; const ProfileDisplay: Component = (props) => { @@ -66,9 +66,11 @@ const ProfileDisplay: Component = (props) => { ); const following = () => myFollowingPubkeys().includes(props.pubkey); - const { followingPubkeys: userFollowingPubkeys } = useFollowings(() => ({ - pubkey: props.pubkey, - })); + const { followingPubkeys: userFollowingPubkeys, query: userFollowingQuery } = useFollowings( + () => ({ + pubkey: props.pubkey, + }), + ); const followed = () => { const p = pubkey(); return p != null && userFollowingPubkeys().includes(p); @@ -86,6 +88,7 @@ const ProfileDisplay: Component = (props) => { until: epoch(), }, ], + continuous: false, })); return ( @@ -210,14 +213,28 @@ const ProfileDisplay: Component = (props) => {
フォロー
-
{userFollowingPubkeys().length}
+
+ 読み込み中} + > + {userFollowingPubkeys().length} + +
フォロワー
setShowFollowers(true)}>読み込む} + fallback={ + + } keyed > diff --git a/src/components/textNote/TextNoteDisplay.tsx b/src/components/textNote/TextNoteDisplay.tsx index 3001d5a..784ec01 100644 --- a/src/components/textNote/TextNoteDisplay.tsx +++ b/src/components/textNote/TextNoteDisplay.tsx @@ -8,11 +8,6 @@ import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-squa import ChatBubbleLeft from 'heroicons/24/outline/chat-bubble-left.svg'; import EllipsisHorizontal from 'heroicons/24/outline/ellipsis-horizontal.svg'; -import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay'; -import ContentWarningDisplay from '@/components/textNote/ContentWarningDisplay'; -import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay'; -import NotePostForm from '@/components/NotePostForm'; - import eventWrapper from '@/core/event'; import useProfile from '@/nostr/useProfile'; @@ -23,12 +18,19 @@ import useReactions from '@/nostr/useReactions'; import useDeprecatedReposts from '@/nostr/useDeprecatedReposts'; import useFormatDate from '@/hooks/useFormatDate'; +import useModalState from '@/hooks/useModalState'; + +import UserNameDisplay from '@/components/UserDisplayName'; +import TextNoteDisplayById from '@/components/textNote/TextNoteDisplayById'; +import { useColumnContext } from '@/components/ColumnContext'; +import GeneralUserMentionDisplay from '@/components/textNote/GeneralUserMentionDisplay'; +import ContentWarningDisplay from '@/components/textNote/ContentWarningDisplay'; +import TextNoteContentDisplay from '@/components/textNote/TextNoteContentDisplay'; +import NotePostForm from '@/components/NotePostForm'; import ensureNonNull from '@/utils/ensureNonNull'; import npubEncodeFallback from '@/utils/npubEncodeFallback'; -import useModalState from '@/hooks/useModalState'; -import UserNameDisplay from '../UserDisplayName'; -import TextNoteDisplayById from './TextNoteDisplayById'; +import useSubscription from '@/nostr/useSubscription'; export type TextNoteDisplayProps = { event: NostrEvent; @@ -43,6 +45,7 @@ const TextNoteDisplay: Component = (props) => { const formatDate = useFormatDate(); const pubkey = usePubkey(); const { showProfile } = useModalState(); + const columnContext = useColumnContext(); const [showReplyForm, setShowReplyForm] = createSignal(false); const closeReplyForm = () => setShowReplyForm(false); @@ -60,11 +63,11 @@ const TextNoteDisplay: Component = (props) => { })); const { reactions, isReactedBy, invalidateReactions } = useReactions(() => ({ - eventId: props.event.id, // TODO いつかなおす + eventId: props.event.id, })); const { reposts, isRepostedBy, invalidateDeprecatedReposts } = useDeprecatedReposts(() => ({ - eventId: props.event.id, // TODO いつかなおす + eventId: props.event.id, })); const commands = useCommands(); @@ -118,7 +121,9 @@ const TextNoteDisplay: Component = (props) => { const createdAt = () => formatDate(event().createdAtAsDate()); - const handleRepost: JSX.EventHandler = () => { + const handleRepost: JSX.EventHandler = (ev) => { + ev.stopPropagation(); + if (isRepostedByMe()) { // TODO remove reaction return; @@ -134,7 +139,9 @@ const TextNoteDisplay: Component = (props) => { }); }; - const handleReaction: JSX.EventHandler = () => { + const handleReaction: JSX.EventHandler = (ev) => { + ev.stopPropagation(); + if (isReactedByMe()) { // TODO remove reaction return; @@ -158,11 +165,22 @@ const TextNoteDisplay: Component = (props) => { }); return ( -
+
{ + columnContext?.setColumnContent({ + type: 'Replies', + eventId: event().rootEvent()?.id ?? props.event.id, + }); + }} + >
@@ -229,7 +253,10 @@ const TextNoteDisplay: Component = (props) => { @@ -285,7 +315,10 @@ const TextNoteDisplay: Component = (props) => {
diff --git a/src/nostr/useBatch.ts b/src/nostr/useBatch.ts index ac2fc88..adf4811 100644 --- a/src/nostr/useBatch.ts +++ b/src/nostr/useBatch.ts @@ -8,7 +8,7 @@ export type Task = { }; export type UseBatchProps = { - executor: (task: Task[]) => void; + executor: (tasks: Task[]) => void; interval?: number; batchSize?: number; }; diff --git a/src/nostr/useBatchedEvents.ts b/src/nostr/useBatchedEvents.ts index da483ee..12fa0fe 100644 --- a/src/nostr/useBatchedEvents.ts +++ b/src/nostr/useBatchedEvents.ts @@ -9,11 +9,14 @@ import { 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 useBatch, { type Task } from '@/nostr/useBatch'; import eventWrapper from '@/core/event'; -import useConfig from './useConfig'; -import usePool from './usePool'; + +import useBatch, { type Task } from '@/nostr/useBatch'; +import useStats from '@/nostr/useStats'; +import useConfig from '@/nostr/useConfig'; +import usePool from '@/nostr/usePool'; + +import timeout from '@/utils/timeout'; type TaskArg = | { type: 'Profile'; pubkey: string } @@ -63,7 +66,7 @@ export type UseTextNoteProps = { }; export type UseTextNote = { - event: Accessor; + event: () => NostrEvent | null; query: CreateQueryResult; }; @@ -73,8 +76,8 @@ export type UseReactionsProps = { }; export type UseReactions = { - reactions: Accessor; - reactionsGroupedByContent: Accessor>; + reactions: () => NostrEvent[]; + reactionsGroupedByContent: () => Map; isReactedBy: (pubkey: string) => boolean; invalidateReactions: () => Promise; query: CreateQueryResult; @@ -86,7 +89,7 @@ export type UseDeprecatedRepostsProps = { }; export type UseDeprecatedReposts = { - reposts: Accessor; + reposts: () => NostrEvent[]; isRepostedBy: (pubkey: string) => boolean; invalidateDeprecatedReposts: () => Promise; query: CreateQueryResult; @@ -104,18 +107,22 @@ type Following = { }; export type UseFollowings = { - followings: Accessor; - followingPubkeys: Accessor; + followings: () => Following[]; + followingPubkeys: () => string[]; query: CreateQueryResult; }; let count = 0; -setInterval(() => console.log('batchSub', count), 1000); +const { setActiveBatchSubscriptions } = useStats(); + +setInterval(() => { + setActiveBatchSubscriptions(count); +}, 1000); const { exec } = useBatch(() => ({ interval: 2000, - batchSize: 100, + batchSize: 150, executor: (tasks) => { const profileTasks = new Map[]>(); const textNoteTasks = new Map[]>(); @@ -201,7 +208,7 @@ const { exec } = useBatch(() => ({ const { config } = useConfig(); const pool = usePool(); - const sub = pool().sub(config().relayUrls, filters); + const sub = pool().sub(config().relayUrls, filters, {}); count += 1; @@ -300,7 +307,6 @@ export const useProfile = (propsProvider: () => UseProfileProps | null): UseProf export const useTextNote = (propsProvider: () => UseTextNoteProps | null): UseTextNote => { const props = createMemo(propsProvider); - const queryClient = useQueryClient(); const query = createQuery( () => ['useTextNote', props()] as const, @@ -317,8 +323,11 @@ export const useTextNote = (propsProvider: () => UseTextNoteProps | null): UseTe }, { // Text notes never change, so they can be stored for a long time. + // However, events tend to be unreferenced as time passes. staleTime: 4 * 60 * 60 * 1000, // 4 hour cacheTime: 4 * 60 * 60 * 1000, // 4 hour + refetchOnWindowFocus: false, + refetchOnMount: false, }, ); @@ -449,6 +458,7 @@ export const useFollowings = (propsProvider: () => UseFollowingsProps | null): U staleTime: 5 * 60 * 1000, // 5 min cacheTime: 24 * 60 * 60 * 1000, // 24 hour refetchOnWindowFocus: false, + refetchInterval: 5 * 60 * 1000, // 5 min }, ); diff --git a/src/nostr/useFollowers.ts b/src/nostr/useFollowers.ts index 1b258f2..eea3191 100644 --- a/src/nostr/useFollowers.ts +++ b/src/nostr/useFollowers.ts @@ -22,5 +22,7 @@ export default function useFollowers(propsProvider: () => UseFollowersProps) { const followersPubkeys = () => uniq(events()?.map((ev) => ev.pubkey)); - return { followersPubkeys }; + const count = () => followersPubkeys().length; + + return { followersPubkeys, count }; } diff --git a/src/nostr/useStats.ts b/src/nostr/useStats.ts new file mode 100644 index 0000000..190c3d1 --- /dev/null +++ b/src/nostr/useStats.ts @@ -0,0 +1,26 @@ +import { createStore } from 'solid-js/store'; + +export type Stats = { + activeSubscriptions: number; + activeBatchSubscriptions: number; +}; + +const [stats, setStats] = createStore({ + activeSubscriptions: 0, + activeBatchSubscriptions: 0, +}); + +const useStats = () => { + const setActiveSubscriptions = (count: number) => setStats('activeSubscriptions', count); + + const setActiveBatchSubscriptions = (count: number) => + setStats('activeBatchSubscriptions', count); + + return { + stats, + setActiveSubscriptions, + setActiveBatchSubscriptions, + }; +}; + +export default useStats; diff --git a/src/nostr/useSubscription.ts b/src/nostr/useSubscription.ts index 4ed69cf..5f72c3b 100644 --- a/src/nostr/useSubscription.ts +++ b/src/nostr/useSubscription.ts @@ -2,6 +2,7 @@ import { createSignal, createEffect, onCleanup } from 'solid-js'; import type { Event as NostrEvent, Filter, SubscriptionOptions } from 'nostr-tools'; import uniqBy from 'lodash/uniqBy'; import usePool from '@/nostr/usePool'; +import useStats from './useStats'; export type UseSubscriptionProps = { relayUrls: string[]; @@ -27,7 +28,10 @@ const sortEvents = (events: NostrEvent[]) => let count = 0; -setInterval(() => console.log('sub', count), 1000); +const { setActiveSubscriptions } = useStats(); +setInterval(() => { + setActiveSubscriptions(count); +}, 1000); const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => { const pool = usePool();