feat: show thread

This commit is contained in:
Shusui MOYATANI
2023-03-26 11:29:38 +09:00
parent 904c5a547c
commit 3abd0dd94e
12 changed files with 237 additions and 61 deletions

View File

@@ -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<ColumnProps> = (props) => {
let columnDivRef: HTMLDivElement | undefined;
const columnState = useColumnState();
const width = () => props.width ?? 'medium';
useHandleCommand(() => ({
@@ -33,22 +37,38 @@ const Column: Component<ColumnProps> = (props) => {
}));
return (
<div
ref={columnDivRef}
class="flex w-[80vw] shrink-0 snap-center snap-always flex-col border-r sm:snap-align-none"
classList={{
'sm:w-[500px]': width() === 'widest',
'sm:w-[350px]': width() === 'wide',
'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> */}
<span class="column-name">{props.name}</span>
<ColumnContext.Provider value={columnState}>
<div
ref={columnDivRef}
class="relative flex w-[80vw] shrink-0 snap-center snap-always flex-col border-r sm:snap-align-none"
classList={{
'sm:w-[500px]': width() === 'widest',
'sm:w-[350px]': width() === 'wide',
'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> */}
<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>
<ul class="flex flex-col overflow-y-scroll scroll-smooth">{props.children}</ul>
</div>
</ColumnContext.Provider>
);
};

View 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;

View 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),
};
};

View File

@@ -186,7 +186,7 @@ const ConfigUI = (props: ConfigProps) => {
<div class="relative">
<div class="flex flex-col gap-1">
<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 />
</button>
</div>

View File

@@ -17,7 +17,7 @@ const Modal: Component<ModalProps> = (props) => {
return (
<div
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}
>
{props.children}

View File

@@ -29,11 +29,11 @@ export type ProfileDisplayProps = {
};
const FollowersCount: Component<{ pubkey: string }> = (props) => {
const { followersPubkeys } = useFollowers(() => ({
const { count } = useFollowers(() => ({
pubkey: props.pubkey,
}));
return <span>{followersPubkeys().length}</span>;
return <>{count()}</>;
};
const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
@@ -66,9 +66,11 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (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<ProfileDisplayProps> = (props) => {
until: epoch(),
},
],
continuous: false,
}));
return (
@@ -210,14 +213,28 @@ const ProfileDisplay: Component<ProfileDisplayProps> = (props) => {
<div class="flex border-t px-4 py-2">
<div class="flex flex-1 flex-col items-start">
<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 class="flex flex-1 flex-col items-start">
<div class="text-sm"></div>
<div class="text-xl">
<Show
when={showFollowers()}
fallback={<button onClick={() => setShowFollowers(true)}></button>}
fallback={
<button
class="text-sm hover:text-stone-800 hover:underline"
onClick={() => setShowFollowers(true)}
>
</button>
}
keyed
>
<FollowersCount pubkey={props.pubkey} />

View File

@@ -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<TextNoteDisplayProps> = (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<TextNoteDisplayProps> = (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<TextNoteDisplayProps> = (props) => {
const createdAt = () => formatDate(event().createdAtAsDate());
const handleRepost: JSX.EventHandler<HTMLButtonElement, MouseEvent> = () => {
const handleRepost: JSX.EventHandler<HTMLButtonElement, MouseEvent> = (ev) => {
ev.stopPropagation();
if (isRepostedByMe()) {
// TODO remove reaction
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()) {
// TODO remove reaction
return;
@@ -158,11 +165,22 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
});
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">
<button
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}>
{/* TODO 画像は脆弱性回避のためにimgじゃない方法で読み込みたい */}
@@ -173,7 +191,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
<div class="flex justify-between gap-1 text-xs">
<button
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 */}
<Show when={(author()?.display_name?.length ?? 0) > 0}>
@@ -211,7 +232,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
{(replyToPubkey: string) => (
<button
class="pr-1 text-blue-500 hover:underline"
onClick={() => showProfile(replyToPubkey)}
onClick={(ev) => {
ev.stopPropagation();
showProfile(replyToPubkey);
}}
>
<GeneralUserMentionDisplay pubkey={replyToPubkey} />
</button>
@@ -229,7 +253,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
<Show when={overflow()}>
<button
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="隠す">
@@ -240,7 +267,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
<div class="actions flex w-48 items-center justify-between gap-8 pt-1">
<button
class="h-4 w-4 shrink-0 text-zinc-400"
onClick={() => setShowReplyForm((current) => !current)}
onClick={() => {
stopPropagation();
setShowReplyForm((current) => !current);
}}
>
<ChatBubbleLeft />
</button>
@@ -285,7 +315,10 @@ const TextNoteDisplay: Component<TextNoteDisplayProps> = (props) => {
<div>
<button
class="h-4 w-4 text-zinc-400"
onClick={() => setShowMenu((current) => !current)}
onClick={(ev) => {
ev.stopPropagation();
setShowMenu((current) => !current);
}}
>
<EllipsisHorizontal />
</button>