mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 14:04:21 +01:00
feat: show thread
This commit is contained in:
@@ -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 { useHandleCommand } from '@/hooks/useCommandBus';
|
||||||
|
import { ColumnContext, useColumnState } from '@/components/ColumnContext';
|
||||||
|
import ColumnContentDisplay from '@/components/ColumnContentDisplay';
|
||||||
|
|
||||||
export type ColumnProps = {
|
export type ColumnProps = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -12,6 +14,8 @@ export type ColumnProps = {
|
|||||||
const Column: Component<ColumnProps> = (props) => {
|
const Column: Component<ColumnProps> = (props) => {
|
||||||
let columnDivRef: HTMLDivElement | undefined;
|
let columnDivRef: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
const columnState = useColumnState();
|
||||||
|
|
||||||
const width = () => props.width ?? 'medium';
|
const width = () => props.width ?? 'medium';
|
||||||
|
|
||||||
useHandleCommand(() => ({
|
useHandleCommand(() => ({
|
||||||
@@ -33,22 +37,38 @@ const Column: Component<ColumnProps> = (props) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<ColumnContext.Provider value={columnState}>
|
||||||
ref={columnDivRef}
|
<div
|
||||||
class="flex w-[80vw] shrink-0 snap-center snap-always flex-col border-r sm:snap-align-none"
|
ref={columnDivRef}
|
||||||
classList={{
|
class="relative flex w-[80vw] shrink-0 snap-center snap-always flex-col border-r sm:snap-align-none"
|
||||||
'sm:w-[500px]': width() === 'widest',
|
classList={{
|
||||||
'sm:w-[350px]': width() === 'wide',
|
'sm:w-[500px]': width() === 'widest',
|
||||||
'sm:w-[310px]': width() === 'medium',
|
'sm:w-[350px]': width() === 'wide',
|
||||||
'sm:w-[270px]': width() === 'narrow',
|
'sm:w-[310px]': width() === 'medium',
|
||||||
}}
|
'sm:w-[270px]': width() === 'narrow',
|
||||||
>
|
}}
|
||||||
<div class="flex h-8 shrink-0 items-center border-b bg-white px-2">
|
>
|
||||||
{/* <span class="column-icon">🏠</span> */}
|
<div class="flex h-8 shrink-0 items-center border-b bg-white px-2">
|
||||||
<span class="column-name">{props.name}</span>
|
{/* <span class="column-icon">🏠</span> */}
|
||||||
|
<span class="column-name">{props.name}</span>
|
||||||
|
</div>
|
||||||
|
<ul class="flex flex-col overflow-y-scroll scroll-smooth">{props.children}</ul>
|
||||||
|
<Show when={columnState.columnState.content} keyed>
|
||||||
|
{(columnContent) => (
|
||||||
|
<div class="absolute h-full w-full bg-white">
|
||||||
|
<div class="flex h-8 shrink-0 items-center border-b bg-white px-2">
|
||||||
|
<button class="w-full text-left" onClick={() => columnState?.clearColumnContext()}>
|
||||||
|
<ホームに戻る
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ul class="flex h-full flex-col overflow-y-scroll scroll-smooth">
|
||||||
|
<ColumnContentDisplay columnContent={columnContent} />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<ul class="flex flex-col overflow-y-scroll scroll-smooth">{props.children}</ul>
|
</ColumnContext.Provider>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
33
src/components/ColumnContentDisplay.tsx
Normal file
33
src/components/ColumnContentDisplay.tsx
Normal file
@@ -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 <Timeline events={[...events()].reverse()} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ColumnContentDisplay: Component<{ columnContent: ColumnContent }> = (props) => {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Match when={props.columnContent.type === 'Replies' && props.columnContent} keyed>
|
||||||
|
{(replies) => <RepliesDisplay eventId={replies.eventId} />}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ColumnContentDisplay;
|
||||||
31
src/components/ColumnContext.tsx
Normal file
31
src/components/ColumnContext.tsx
Normal file
@@ -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<UseColumnState>();
|
||||||
|
|
||||||
|
export const useColumnContext = () => useContext(ColumnContext);
|
||||||
|
|
||||||
|
export const useColumnState = (): UseColumnState => {
|
||||||
|
const [columnState, setColumnState] = createStore<ColumnState>({});
|
||||||
|
|
||||||
|
return {
|
||||||
|
columnState,
|
||||||
|
setColumnContent: (content: ColumnContent) => setColumnState('content', content),
|
||||||
|
clearColumnContext: () => setColumnState('content', undefined),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -186,7 +186,7 @@ const ConfigUI = (props: ConfigProps) => {
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<h2 class="flex-1 text-center font-bold">設定</h2>
|
<h2 class="flex-1 text-center font-bold">設定</h2>
|
||||||
<button class="absolute top-1 right-0 h-4 w-4" onClick={() => props.onClose?.()}>
|
<button class="absolute top-1 right-0 z-0 h-4 w-4" onClick={() => props.onClose?.()}>
|
||||||
<XMark />
|
<XMark />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const Modal: Component<ModalProps> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
class="absolute top-0 left-0 flex h-screen w-screen cursor-default place-content-center place-items-center bg-black/30"
|
class="absolute top-0 left-0 z-10 flex h-screen w-screen cursor-default place-content-center place-items-center bg-black/30"
|
||||||
onClick={handleClickContainer}
|
onClick={handleClickContainer}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ export type ProfileDisplayProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FollowersCount: Component<{ pubkey: string }> = (props) => {
|
const FollowersCount: Component<{ pubkey: string }> = (props) => {
|
||||||
const { followersPubkeys } = useFollowers(() => ({
|
const { count } = useFollowers(() => ({
|
||||||
pubkey: props.pubkey,
|
pubkey: props.pubkey,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return <span>{followersPubkeys().length}</span>;
|
return <>{count()}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
||||||
@@ -66,9 +66,11 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
);
|
);
|
||||||
const following = () => myFollowingPubkeys().includes(props.pubkey);
|
const following = () => myFollowingPubkeys().includes(props.pubkey);
|
||||||
|
|
||||||
const { followingPubkeys: userFollowingPubkeys } = useFollowings(() => ({
|
const { followingPubkeys: userFollowingPubkeys, query: userFollowingQuery } = useFollowings(
|
||||||
pubkey: props.pubkey,
|
() => ({
|
||||||
}));
|
pubkey: props.pubkey,
|
||||||
|
}),
|
||||||
|
);
|
||||||
const followed = () => {
|
const followed = () => {
|
||||||
const p = pubkey();
|
const p = pubkey();
|
||||||
return p != null && userFollowingPubkeys().includes(p);
|
return p != null && userFollowingPubkeys().includes(p);
|
||||||
@@ -86,6 +88,7 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
until: epoch(),
|
until: epoch(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
continuous: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -210,14 +213,28 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
|
|||||||
<div class="flex border-t px-4 py-2">
|
<div class="flex border-t px-4 py-2">
|
||||||
<div class="flex flex-1 flex-col items-start">
|
<div class="flex flex-1 flex-col items-start">
|
||||||
<div class="text-sm">フォロー</div>
|
<div class="text-sm">フォロー</div>
|
||||||
<div class="text-xl">{userFollowingPubkeys().length}</div>
|
<div class="text-xl">
|
||||||
|
<Show
|
||||||
|
when={userFollowingQuery.isFetched}
|
||||||
|
fallback={<span class="text-sm">読み込み中</span>}
|
||||||
|
>
|
||||||
|
{userFollowingPubkeys().length}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-col items-start">
|
<div class="flex flex-1 flex-col items-start">
|
||||||
<div class="text-sm">フォロワー</div>
|
<div class="text-sm">フォロワー</div>
|
||||||
<div class="text-xl">
|
<div class="text-xl">
|
||||||
<Show
|
<Show
|
||||||
when={showFollowers()}
|
when={showFollowers()}
|
||||||
fallback={<button onClick={() => setShowFollowers(true)}>読み込む</button>}
|
fallback={
|
||||||
|
<button
|
||||||
|
class="text-sm hover:text-stone-800 hover:underline"
|
||||||
|
onClick={() => setShowFollowers(true)}
|
||||||
|
>
|
||||||
|
読み込む
|
||||||
|
</button>
|
||||||
|
}
|
||||||
keyed
|
keyed
|
||||||
>
|
>
|
||||||
<FollowersCount pubkey={props.pubkey} />
|
<FollowersCount pubkey={props.pubkey} />
|
||||||
|
|||||||
@@ -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 ChatBubbleLeft from 'heroicons/24/outline/chat-bubble-left.svg';
|
||||||
import EllipsisHorizontal from 'heroicons/24/outline/ellipsis-horizontal.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 eventWrapper from '@/core/event';
|
||||||
|
|
||||||
import useProfile from '@/nostr/useProfile';
|
import useProfile from '@/nostr/useProfile';
|
||||||
@@ -23,12 +18,19 @@ import useReactions from '@/nostr/useReactions';
|
|||||||
import useDeprecatedReposts from '@/nostr/useDeprecatedReposts';
|
import useDeprecatedReposts from '@/nostr/useDeprecatedReposts';
|
||||||
|
|
||||||
import useFormatDate from '@/hooks/useFormatDate';
|
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 ensureNonNull from '@/utils/ensureNonNull';
|
||||||
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
import npubEncodeFallback from '@/utils/npubEncodeFallback';
|
||||||
import useModalState from '@/hooks/useModalState';
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
import UserNameDisplay from '../UserDisplayName';
|
|
||||||
import TextNoteDisplayById from './TextNoteDisplayById';
|
|
||||||
|
|
||||||
export type TextNoteDisplayProps = {
|
export type TextNoteDisplayProps = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
@@ -43,6 +45,7 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
const formatDate = useFormatDate();
|
const formatDate = useFormatDate();
|
||||||
const pubkey = usePubkey();
|
const pubkey = usePubkey();
|
||||||
const { showProfile } = useModalState();
|
const { showProfile } = useModalState();
|
||||||
|
const columnContext = useColumnContext();
|
||||||
|
|
||||||
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
const [showReplyForm, setShowReplyForm] = createSignal(false);
|
||||||
const closeReplyForm = () => setShowReplyForm(false);
|
const closeReplyForm = () => setShowReplyForm(false);
|
||||||
@@ -60,11 +63,11 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const { reactions, isReactedBy, invalidateReactions } = useReactions(() => ({
|
const { reactions, isReactedBy, invalidateReactions } = useReactions(() => ({
|
||||||
eventId: props.event.id, // TODO いつかなおす
|
eventId: props.event.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { reposts, isRepostedBy, invalidateDeprecatedReposts } = useDeprecatedReposts(() => ({
|
const { reposts, isRepostedBy, invalidateDeprecatedReposts } = useDeprecatedReposts(() => ({
|
||||||
eventId: props.event.id, // TODO いつかなおす
|
eventId: props.event.id,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const commands = useCommands();
|
const commands = useCommands();
|
||||||
@@ -118,7 +121,9 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
|
|
||||||
const createdAt = () => formatDate(event().createdAtAsDate());
|
const createdAt = () => formatDate(event().createdAtAsDate());
|
||||||
|
|
||||||
const handleRepost: JSX.EventHandler<HTMLButtonElement, MouseEvent> = () => {
|
const handleRepost: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
if (isRepostedByMe()) {
|
if (isRepostedByMe()) {
|
||||||
// TODO remove reaction
|
// TODO remove reaction
|
||||||
return;
|
return;
|
||||||
@@ -134,7 +139,9 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReaction: JSX.EventHandler<HTMLButtonElement, MouseEvent> = () => {
|
const handleReaction: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
if (isReactedByMe()) {
|
if (isReactedByMe()) {
|
||||||
// TODO remove reaction
|
// TODO remove reaction
|
||||||
return;
|
return;
|
||||||
@@ -158,11 +165,22 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="nostr-textnote flex flex-col">
|
<div
|
||||||
|
class="nostr-textnote flex flex-col"
|
||||||
|
onClick={() => {
|
||||||
|
columnContext?.setColumnContent({
|
||||||
|
type: 'Replies',
|
||||||
|
eventId: event().rootEvent()?.id ?? props.event.id,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div class="flex w-full gap-1">
|
<div class="flex w-full gap-1">
|
||||||
<button
|
<button
|
||||||
class="author-icon h-10 w-10 shrink-0 overflow-hidden"
|
class="author-icon h-10 w-10 shrink-0 overflow-hidden"
|
||||||
onClick={() => showProfile(event().pubkey)}
|
onClick={(ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
showProfile(event().pubkey);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Show when={author()?.picture}>
|
<Show when={author()?.picture}>
|
||||||
{/* TODO 画像は脆弱性回避のためにimgじゃない方法で読み込みたい */}
|
{/* TODO 画像は脆弱性回避のためにimgじゃない方法で読み込みたい */}
|
||||||
@@ -173,7 +191,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
<div class="flex justify-between gap-1 text-xs">
|
<div class="flex justify-between gap-1 text-xs">
|
||||||
<button
|
<button
|
||||||
class="author flex min-w-0 truncate hover:text-blue-500"
|
class="author flex min-w-0 truncate hover:text-blue-500"
|
||||||
onClick={() => showProfile(event().pubkey)}
|
onClick={() => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
showProfile(event().pubkey);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* TODO link to author */}
|
{/* TODO link to author */}
|
||||||
<Show when={(author()?.display_name?.length ?? 0) > 0}>
|
<Show when={(author()?.display_name?.length ?? 0) > 0}>
|
||||||
@@ -211,7 +232,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
{(replyToPubkey: string) => (
|
{(replyToPubkey: string) => (
|
||||||
<button
|
<button
|
||||||
class="pr-1 text-blue-500 hover:underline"
|
class="pr-1 text-blue-500 hover:underline"
|
||||||
onClick={() => showProfile(replyToPubkey)}
|
onClick={(ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
showProfile(replyToPubkey);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<GeneralUserMentionDisplay pubkey={replyToPubkey} />
|
<GeneralUserMentionDisplay pubkey={replyToPubkey} />
|
||||||
</button>
|
</button>
|
||||||
@@ -229,7 +253,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
<Show when={overflow()}>
|
<Show when={overflow()}>
|
||||||
<button
|
<button
|
||||||
class="text-xs text-stone-600 hover:text-stone-800"
|
class="text-xs text-stone-600 hover:text-stone-800"
|
||||||
onClick={() => setShowOverflow((current) => !current)}
|
onClick={(ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
setShowOverflow((current) => !current);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Show when={!showOverflow()} fallback="隠す">
|
<Show when={!showOverflow()} fallback="隠す">
|
||||||
続きを読む
|
続きを読む
|
||||||
@@ -240,7 +267,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
<div class="actions flex w-48 items-center justify-between gap-8 pt-1">
|
<div class="actions flex w-48 items-center justify-between gap-8 pt-1">
|
||||||
<button
|
<button
|
||||||
class="h-4 w-4 shrink-0 text-zinc-400"
|
class="h-4 w-4 shrink-0 text-zinc-400"
|
||||||
onClick={() => setShowReplyForm((current) => !current)}
|
onClick={() => {
|
||||||
|
stopPropagation();
|
||||||
|
setShowReplyForm((current) => !current);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ChatBubbleLeft />
|
<ChatBubbleLeft />
|
||||||
</button>
|
</button>
|
||||||
@@ -285,7 +315,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
|
|||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="h-4 w-4 text-zinc-400"
|
class="h-4 w-4 text-zinc-400"
|
||||||
onClick={() => setShowMenu((current) => !current)}
|
onClick={(ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
setShowMenu((current) => !current);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<EllipsisHorizontal />
|
<EllipsisHorizontal />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export type Task<TaskArgs, TaskResult> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UseBatchProps<TaskArgs, TaskResult> = {
|
export type UseBatchProps<TaskArgs, TaskResult> = {
|
||||||
executor: (task: Task<TaskArgs, TaskResult>[]) => void;
|
executor: (tasks: Task<TaskArgs, TaskResult>[]) => void;
|
||||||
interval?: number;
|
interval?: number;
|
||||||
batchSize?: number;
|
batchSize?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ import {
|
|||||||
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 useBatch, { type Task } from '@/nostr/useBatch';
|
|
||||||
import eventWrapper from '@/core/event';
|
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 TaskArg =
|
||||||
| { type: 'Profile'; pubkey: string }
|
| { type: 'Profile'; pubkey: string }
|
||||||
@@ -63,7 +66,7 @@ export type UseTextNoteProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UseTextNote = {
|
export type UseTextNote = {
|
||||||
event: Accessor<NostrEvent | null>;
|
event: () => NostrEvent | null;
|
||||||
query: CreateQueryResult<NostrEvent | null>;
|
query: CreateQueryResult<NostrEvent | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,8 +76,8 @@ export type UseReactionsProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UseReactions = {
|
export type UseReactions = {
|
||||||
reactions: Accessor<NostrEvent[]>;
|
reactions: () => NostrEvent[];
|
||||||
reactionsGroupedByContent: Accessor<Map<string, NostrEvent[]>>;
|
reactionsGroupedByContent: () => Map<string, NostrEvent[]>;
|
||||||
isReactedBy: (pubkey: string) => boolean;
|
isReactedBy: (pubkey: string) => boolean;
|
||||||
invalidateReactions: () => Promise<void>;
|
invalidateReactions: () => Promise<void>;
|
||||||
query: CreateQueryResult<NostrEvent[]>;
|
query: CreateQueryResult<NostrEvent[]>;
|
||||||
@@ -86,7 +89,7 @@ export type UseDeprecatedRepostsProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UseDeprecatedReposts = {
|
export type UseDeprecatedReposts = {
|
||||||
reposts: Accessor<NostrEvent[]>;
|
reposts: () => NostrEvent[];
|
||||||
isRepostedBy: (pubkey: string) => boolean;
|
isRepostedBy: (pubkey: string) => boolean;
|
||||||
invalidateDeprecatedReposts: () => Promise<void>;
|
invalidateDeprecatedReposts: () => Promise<void>;
|
||||||
query: CreateQueryResult<NostrEvent[]>;
|
query: CreateQueryResult<NostrEvent[]>;
|
||||||
@@ -104,18 +107,22 @@ type Following = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UseFollowings = {
|
export type UseFollowings = {
|
||||||
followings: Accessor<Following[]>;
|
followings: () => Following[];
|
||||||
followingPubkeys: Accessor<string[]>;
|
followingPubkeys: () => string[];
|
||||||
query: CreateQueryResult<NostrEvent | null>;
|
query: CreateQueryResult<NostrEvent | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
setInterval(() => console.log('batchSub', count), 1000);
|
const { setActiveBatchSubscriptions } = useStats();
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
setActiveBatchSubscriptions(count);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
||||||
interval: 2000,
|
interval: 2000,
|
||||||
batchSize: 100,
|
batchSize: 150,
|
||||||
executor: (tasks) => {
|
executor: (tasks) => {
|
||||||
const profileTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
const profileTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
||||||
const textNoteTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
const textNoteTasks = new Map<string, Task<TaskArg, TaskRes>[]>();
|
||||||
@@ -201,7 +208,7 @@ const { exec } = useBatch<TaskArg, TaskRes>(() => ({
|
|||||||
const { config } = useConfig();
|
const { config } = useConfig();
|
||||||
const pool = usePool();
|
const pool = usePool();
|
||||||
|
|
||||||
const sub = pool().sub(config().relayUrls, filters);
|
const sub = pool().sub(config().relayUrls, filters, {});
|
||||||
|
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
@@ -300,7 +307,6 @@ export const useProfile = (propsProvider: () => UseProfileProps | null): UseProf
|
|||||||
|
|
||||||
export const useTextNote = (propsProvider: () => UseTextNoteProps | null): UseTextNote => {
|
export const useTextNote = (propsProvider: () => UseTextNoteProps | null): UseTextNote => {
|
||||||
const props = createMemo(propsProvider);
|
const props = createMemo(propsProvider);
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const query = createQuery(
|
const query = createQuery(
|
||||||
() => ['useTextNote', props()] as const,
|
() => ['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.
|
// 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
|
staleTime: 4 * 60 * 60 * 1000, // 4 hour
|
||||||
cacheTime: 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
|
staleTime: 5 * 60 * 1000, // 5 min
|
||||||
cacheTime: 24 * 60 * 60 * 1000, // 24 hour
|
cacheTime: 24 * 60 * 60 * 1000, // 24 hour
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
|
refetchInterval: 5 * 60 * 1000, // 5 min
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -22,5 +22,7 @@ export default function useFollowers(propsProvider: () => UseFollowersProps) {
|
|||||||
|
|
||||||
const followersPubkeys = () => uniq(events()?.map((ev) => ev.pubkey));
|
const followersPubkeys = () => uniq(events()?.map((ev) => ev.pubkey));
|
||||||
|
|
||||||
return { followersPubkeys };
|
const count = () => followersPubkeys().length;
|
||||||
|
|
||||||
|
return { followersPubkeys, count };
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/nostr/useStats.ts
Normal file
26
src/nostr/useStats.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
|
||||||
|
export type Stats = {
|
||||||
|
activeSubscriptions: number;
|
||||||
|
activeBatchSubscriptions: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [stats, setStats] = createStore<Stats>({
|
||||||
|
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;
|
||||||
@@ -2,6 +2,7 @@ import { createSignal, createEffect, onCleanup } from 'solid-js';
|
|||||||
import type { Event as NostrEvent, Filter, SubscriptionOptions } from 'nostr-tools';
|
import type { Event as NostrEvent, Filter, SubscriptionOptions } from 'nostr-tools';
|
||||||
import uniqBy from 'lodash/uniqBy';
|
import uniqBy from 'lodash/uniqBy';
|
||||||
import usePool from '@/nostr/usePool';
|
import usePool from '@/nostr/usePool';
|
||||||
|
import useStats from './useStats';
|
||||||
|
|
||||||
export type UseSubscriptionProps = {
|
export type UseSubscriptionProps = {
|
||||||
relayUrls: string[];
|
relayUrls: string[];
|
||||||
@@ -27,7 +28,10 @@ const sortEvents = (events: NostrEvent[]) =>
|
|||||||
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
setInterval(() => console.log('sub', count), 1000);
|
const { setActiveSubscriptions } = useStats();
|
||||||
|
setInterval(() => {
|
||||||
|
setActiveSubscriptions(count);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
||||||
const pool = usePool();
|
const pool = usePool();
|
||||||
|
|||||||
Reference in New Issue
Block a user