From 211d8f33f03fc6df11a6ee81476cf0e759f46954 Mon Sep 17 00:00:00 2001 From: Shusui MOYATANI Date: Sat, 27 Jan 2024 22:58:00 +0900 Subject: [PATCH] WIP cursor --- src/components/ColumnItem.tsx | 45 +++++- src/components/column/BookmarkColumn.tsx | 7 +- src/components/column/ChannelColumn.tsx | 1 + src/components/column/Column.tsx | 104 +++++++------ src/components/column/ColumnSettings.tsx | 23 +-- src/components/column/Columns.tsx | 146 ++++++++++--------- src/components/column/FollowingColumn.tsx | 7 +- src/components/column/LoadMore.tsx | 28 ++-- src/components/column/NotificationColumn.tsx | 11 +- src/components/column/PostsColumn.tsx | 7 +- src/components/column/ReactionsColumn.tsx | 11 +- src/components/column/RelaysColumn.tsx | 7 +- src/components/column/SearchColumn.tsx | 7 +- src/components/event/EventDisplay.tsx | 18 ++- src/components/timeline/Notification.tsx | 54 ------- src/components/timeline/Timeline.tsx | 83 +++++++++-- src/core/useConfig.ts | 9 +- src/hooks/useCommandBus.ts | 20 ++- src/hooks/useCursor.ts | 40 +++++ src/hooks/useShortcutKeys.ts | 29 ++-- 20 files changed, 366 insertions(+), 291 deletions(-) delete mode 100644 src/components/timeline/Notification.tsx create mode 100644 src/hooks/useCursor.ts diff --git a/src/components/ColumnItem.tsx b/src/components/ColumnItem.tsx index b797bd3a..96d65b6f 100644 --- a/src/components/ColumnItem.tsx +++ b/src/components/ColumnItem.tsx @@ -1,11 +1,48 @@ -import { type Component, type JSX } from 'solid-js'; +import { createEffect, type Component, type JSX } from 'solid-js'; + +import useCursor, { + useColumnCursor, + useTimelineCursor, + ItemCursorContext, +} from '@/hooks/useCursor'; type ColumnItemProps = { + itemId: string; children: JSX.Element; }; -const ColumnItem: Component = (props) => ( -
{props.children}
-); +const ColumnItem: Component = (props) => { + let containerRef: HTMLDivElement | undefined; + + const { cursor, setCursor } = useCursor(); + const { columnCursor } = useColumnCursor(); + const { timelineCursor } = useTimelineCursor(); + + const selected = () => + cursor().columnId === columnCursor.columnId && + cursor().timelineId === timelineCursor.timelineId && + cursor().itemId === props.itemId; + + createEffect(() => { + if (selected()) { + containerRef?.scrollIntoView(false); + } + }); + + return ( + +
+ {props.children} +
+
+ ); +}; export default ColumnItem; diff --git a/src/components/column/BookmarkColumn.tsx b/src/components/column/BookmarkColumn.tsx index 501a71c0..a0fde553 100644 --- a/src/components/column/BookmarkColumn.tsx +++ b/src/components/column/BookmarkColumn.tsx @@ -12,8 +12,6 @@ import { useTranslation } from '@/i18n/useTranslation'; import useParameterizedReplaceableEvent from '@/nostr/useParameterizedReplaceableEvent'; type BookmarkColumnDisplayProps = { - columnIndex: number; - lastColumn: boolean; column: BookmarkColumnType; }; @@ -31,17 +29,16 @@ const BookmarkColumn: Component = (props) => { return ( } - settings={() => } + settings={() => } onClose={() => removeColumn(props.column.id)} /> } width={props.column.width} - columnIndex={props.columnIndex} - lastColumn={props.lastColumn} > {(ev) => } diff --git a/src/components/column/ChannelColumn.tsx b/src/components/column/ChannelColumn.tsx index d6f6d9ef..9948c807 100644 --- a/src/components/column/ChannelColumn.tsx +++ b/src/components/column/ChannelColumn.tsx @@ -42,6 +42,7 @@ const ChannelColumn: Component = (props) => { return ( void; - columnIndex: number; - lastColumn: boolean; width: 'widest' | 'wide' | 'medium' | 'narrow' | null | undefined; header: JSX.Element; children: JSX.Element; @@ -20,6 +21,7 @@ const Column: Component = (props) => { let columnDivRef: HTMLDivElement | undefined; const timelineState = useTimelineState(); + const { config } = useConfig(); const i18n = useTranslation(); const width = () => props.width ?? 'medium'; @@ -27,7 +29,11 @@ const Column: Component = (props) => { useHandleCommand(() => ({ commandType: 'moveToColumn', handler: (command) => { - if (command.command === 'moveToColumn' && command.columnIndex === props.columnIndex) { + if ( + command.columnIndex >= 0 && + command.columnIndex < config().columns.length && + config().columns[command.columnIndex].id === props.columnId + ) { columnDivRef?.scrollIntoView({ behavior: 'smooth', inline: 'center' }); } }, @@ -36,57 +42,63 @@ const Column: Component = (props) => { useHandleCommand(() => ({ commandType: 'moveToLastColumn', handler: () => { - if (props.lastColumn) { + const lastColumn = config().columns[config().columns.length - 1]; + if (lastColumn.id === props.columnId) { columnDivRef?.scrollIntoView({ behavior: 'smooth' }); } }, })); return ( - -
- -
{props.header}
-
- {props.children} -
- - } + + +
- {(timeline) => ( - <> -
- -
-
- -
- - )} - -
-
+ {props.children} +
+ + } + > + {(timeline) => ( + <> +
+ +
+
+ +
+ + )} +
+ + + ); }; diff --git a/src/components/column/ColumnSettings.tsx b/src/components/column/ColumnSettings.tsx index 835da29d..b53386ef 100644 --- a/src/components/column/ColumnSettings.tsx +++ b/src/components/column/ColumnSettings.tsx @@ -6,12 +6,11 @@ import Trash from 'heroicons/24/outline/trash.svg'; import { ColumnType } from '@/core/column'; import useConfig from '@/core/useConfig'; -import { useRequestCommand } from '@/hooks/useCommandBus'; +import useCursor from '@/hooks/useCursor'; import { useTranslation } from '@/i18n/useTranslation'; type ColumnSettingsProps = { column: ColumnType; - columnIndex: number; }; type ColumnSettingsSectionProps = { @@ -29,15 +28,15 @@ const ColumnSettingsSection: Component = (props) => const ColumnSettings: Component = (props) => { const i18n = useTranslation(); const { saveColumn, removeColumn, moveColumn } = useConfig(); - const request = useRequestCommand(); + const { setCursor } = useCursor(); const setColumnWidth = (width: ColumnType['width']) => { saveColumn({ ...props.column, width }); }; - const move = (index: number) => { - moveColumn(props.column.id, index); - request({ command: 'moveToColumn', columnIndex: index }).catch((err) => console.error(err)); + const move = (diff: number) => { + moveColumn(props.column.id, diff); + setCursor({ columnId: props.column.id }); }; return ( @@ -63,20 +62,12 @@ const ColumnSettings: Component = (props) => {
- - - - - {props.children} - - + + {props.children} + ); }; diff --git a/src/components/column/NotificationColumn.tsx b/src/components/column/NotificationColumn.tsx index 3d585c6a..e5da347c 100644 --- a/src/components/column/NotificationColumn.tsx +++ b/src/components/column/NotificationColumn.tsx @@ -6,7 +6,7 @@ import BasicColumnHeader from '@/components/column/BasicColumnHeader'; import Column from '@/components/column/Column'; import ColumnSettings from '@/components/column/ColumnSettings'; import LoadMore, { useLoadMore } from '@/components/column/LoadMore'; -import Notification from '@/components/timeline/Notification'; +import Timeline from '@/components/timeline/Timeline'; import { NotificationColumnType } from '@/core/column'; import { applyContentFilter } from '@/core/contentFilter'; import useConfig from '@/core/useConfig'; @@ -14,8 +14,6 @@ import { useTranslation } from '@/i18n/useTranslation'; import useSubscription from '@/nostr/useSubscription'; type NotificationColumnDisplayProps = { - columnIndex: number; - lastColumn: boolean; column: NotificationColumnType; }; @@ -47,21 +45,20 @@ const NotificationColumn: Component = (props) => return ( } - settings={() => } + settings={() => } onClose={() => removeColumn(props.column.id)} /> } width={props.column.width} - columnIndex={props.columnIndex} - lastColumn={props.lastColumn} timelineRef={loadMore.timelineRef} > - + ); diff --git a/src/components/column/PostsColumn.tsx b/src/components/column/PostsColumn.tsx index b18d0185..17049296 100644 --- a/src/components/column/PostsColumn.tsx +++ b/src/components/column/PostsColumn.tsx @@ -14,8 +14,6 @@ import { useTranslation } from '@/i18n/useTranslation'; import useSubscription from '@/nostr/useSubscription'; type PostsColumnDisplayProps = { - columnIndex: number; - lastColumn: boolean; column: PostsColumnType; }; @@ -47,17 +45,16 @@ const PostsColumn: Component = (props) => { return ( } - settings={() => } + settings={() => } onClose={() => removeColumn(props.column.id)} /> } width={props.column.width} - columnIndex={props.columnIndex} - lastColumn={props.lastColumn} timelineRef={loadMore.timelineRef} > diff --git a/src/components/column/ReactionsColumn.tsx b/src/components/column/ReactionsColumn.tsx index a0454e5f..d89171a6 100644 --- a/src/components/column/ReactionsColumn.tsx +++ b/src/components/column/ReactionsColumn.tsx @@ -6,7 +6,7 @@ import BasicColumnHeader from '@/components/column/BasicColumnHeader'; import Column from '@/components/column/Column'; import ColumnSettings from '@/components/column/ColumnSettings'; import LoadMore, { useLoadMore } from '@/components/column/LoadMore'; -import Notification from '@/components/timeline/Notification'; +import Timeline from '@/components/timeline/Timeline'; import { ReactionsColumnType } from '@/core/column'; import { applyContentFilter } from '@/core/contentFilter'; import useConfig from '@/core/useConfig'; @@ -14,8 +14,6 @@ import { useTranslation } from '@/i18n/useTranslation'; import useSubscription from '@/nostr/useSubscription'; type ReactionsColumnDisplayProps = { - columnIndex: number; - lastColumn: boolean; column: ReactionsColumnType; }; @@ -47,21 +45,20 @@ const ReactionsColumn: Component = (props) => { return ( } - settings={() => } + settings={() => } onClose={() => removeColumn(props.column.id)} /> } width={props.column.width} - columnIndex={props.columnIndex} - lastColumn={props.lastColumn} timelineRef={loadMore.timelineRef} > - + ); diff --git a/src/components/column/RelaysColumn.tsx b/src/components/column/RelaysColumn.tsx index 365a1e36..0f4ccd82 100644 --- a/src/components/column/RelaysColumn.tsx +++ b/src/components/column/RelaysColumn.tsx @@ -14,8 +14,6 @@ import { useTranslation } from '@/i18n/useTranslation'; import useSubscription from '@/nostr/useSubscription'; type RelaysColumnDisplayProps = { - columnIndex: number; - lastColumn: boolean; column: RelaysColumnType; }; @@ -48,17 +46,16 @@ const RelaysColumn: Component = (props) => { return ( } - settings={() => } + settings={() => } onClose={() => removeColumn(props.column.id)} /> } width={props.column.width} - columnIndex={props.columnIndex} - lastColumn={props.lastColumn} timelineRef={loadMore.timelineRef} > diff --git a/src/components/column/SearchColumn.tsx b/src/components/column/SearchColumn.tsx index 3f622bf2..0928f66f 100644 --- a/src/components/column/SearchColumn.tsx +++ b/src/components/column/SearchColumn.tsx @@ -77,8 +77,6 @@ const SearchColumnHeader: Component = (props) => { }; export type SearchColumnDisplayProps = { - columnIndex: number; - lastColumn: boolean; column: SearchColumnType; }; @@ -121,16 +119,15 @@ const SearchColumn: Component = (props) => { return ( } + settings={() => } onClose={() => removeColumn(props.column.id)} /> } width={props.column.width} - columnIndex={props.columnIndex} - lastColumn={props.lastColumn} timelineRef={loadMore.timelineRef} > diff --git a/src/components/event/EventDisplay.tsx b/src/components/event/EventDisplay.tsx index 79921b5f..33a68388 100644 --- a/src/components/event/EventDisplay.tsx +++ b/src/components/event/EventDisplay.tsx @@ -1,9 +1,10 @@ -import { Switch, Match, Component } from 'solid-js'; +import { Switch, Match, lazy, Component } from 'solid-js'; import * as Kind from 'nostr-tools/kinds'; import { type Event as NostrEvent } from 'nostr-tools/pure'; -// import ChannelInfo from '@/components/event/ChannelInfo'; +// eslint-disable-next-line import/no-cycle +import Reaction from '@/components/event/Reaction'; // eslint-disable-next-line import/no-cycle import Repost from '@/components/event/Repost'; // eslint-disable-next-line import/no-cycle @@ -11,6 +12,8 @@ import TextNote from '@/components/event/TextNote'; import EventLink from '@/components/EventLink'; import { useTranslation } from '@/i18n/useTranslation'; +const ZapReceipt = lazy(() => import('@/components/event/ZapReceipt')); + export type EventDisplayProps = { event: NostrEvent; embedding?: boolean; @@ -47,13 +50,14 @@ const EventDisplay: Component = (props) => { + + + + + + ); - /* - - - - */ }; export default EventDisplay; diff --git a/src/components/timeline/Notification.tsx b/src/components/timeline/Notification.tsx deleted file mode 100644 index feccd7ed..00000000 --- a/src/components/timeline/Notification.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { For, Switch, Match, lazy, type Component, Show } from 'solid-js'; - -import * as Kind from 'nostr-tools/kinds'; -import { type Event as NostrEvent } from 'nostr-tools/pure'; - -import ColumnItem from '@/components/ColumnItem'; -import Reaction from '@/components/event/Reaction'; -import Repost from '@/components/event/Repost'; -import TextNote from '@/components/event/TextNote'; -import useConfig from '@/core/useConfig'; - -const ZapReceipt = lazy(() => import('@/components/event/ZapReceipt')); - -export type NotificationProps = { - events: NostrEvent[]; -}; - -const Notification: Component = (props) => { - const { shouldMuteEvent } = useConfig(); - - return ( - - {(event) => ( - - unknown event
}> - - - - - - - - - - - {/* TODO ちゃんとnotification用のコンポーネント使う */} - - - - - - - - - - - - - )} - - ); -}; - -export default Notification; diff --git a/src/components/timeline/Timeline.tsx b/src/components/timeline/Timeline.tsx index 20a57d3c..70213d5b 100644 --- a/src/components/timeline/Timeline.tsx +++ b/src/components/timeline/Timeline.tsx @@ -1,10 +1,13 @@ -import { For, type Component, Show } from 'solid-js'; +import { For, createEffect, createMemo, type Component, Show } from 'solid-js'; import { type Event as NostrEvent } from 'nostr-tools/pure'; import ColumnItem from '@/components/ColumnItem'; import EventDisplay from '@/components/event/EventDisplay'; import useConfig from '@/core/useConfig'; +import { useHandleCommand } from '@/hooks/useCommandBus'; +import useCursor, { TimelineCursorContext, useColumnCursor } from '@/hooks/useCursor'; +import generateId from '@/utils/generateId'; export type TimelineProps = { events: NostrEvent[]; @@ -13,16 +16,76 @@ export type TimelineProps = { const Timeline: Component = (props) => { const { shouldMuteEvent } = useConfig(); + const { columnCursor } = useColumnCursor(); + const { cursor, setCursor } = useCursor(); + + const timelineId = createMemo(() => generateId()); + + const updateCursor = (updateIndex: (index: number) => number) => { + const current = cursor(); + if (current.timelineId === timelineId()) { + const index = props.events.findIndex(({ id }) => id === current.itemId); + if (index < 0) return; + const newIndex = updateIndex(index); + const event = props.events[newIndex]; + + setCursor({ + columnId: columnCursor.columnId, + timelineId: timelineId(), + itemId: event.id, + }); + } + }; + + useHandleCommand(() => ({ + commandType: 'moveToFirstItem', + handler: () => { + updateCursor(() => 0); + }, + })); + + useHandleCommand(() => ({ + commandType: 'moveToPrevItem', + handler: () => { + updateCursor((index) => Math.max(index - 1, 0)); + }, + })); + + useHandleCommand(() => ({ + commandType: 'moveToNextItem', + handler: () => { + updateCursor((index) => Math.min(index + 1, props.events.length - 1)); + }, + })); + + createEffect(() => { + const current = cursor(); + + if (current.columnId === columnCursor.columnId && current.timelineId == null) { + setCursor({ columnId: columnCursor.columnId, timelineId: timelineId() }); + } + + if (current.timelineId === timelineId() && current.itemId == null) { + setCursor({ + columnId: columnCursor.columnId, + timelineId: timelineId(), + itemId: props.events[0]?.id, + }); + } + }); + return ( - - {(event) => ( - - - - - - )} - + + + {(event) => ( + + + + + + )} + + ); }; diff --git a/src/core/useConfig.ts b/src/core/useConfig.ts index d899e3a2..0e4fcb3f 100644 --- a/src/core/useConfig.ts +++ b/src/core/useConfig.ts @@ -62,7 +62,7 @@ type UseConfig = { removeRelay: (url: string) => void; // column saveColumn: (column: ColumnType) => void; - moveColumn: (columnId: string, index: number) => void; + moveColumn: (columnId: string, diff: number) => void; removeColumn: (columnId: string) => void; initializeColumns: (param: { pubkey: string }) => void; // emoji @@ -168,15 +168,12 @@ const useConfig = (): UseConfig => { }); }; - const moveColumn = (columnId: string, index: number) => { + const moveColumn = (columnId: string, diff: number) => { setConfig('columns', (current) => { - // index starts with 1 - const idx = index - 1; - const toIndex = Math.max(Math.min(idx, current.length), 0); const fromIndex = current.findIndex((e) => e.id === columnId); + const toIndex = Math.max(Math.min(fromIndex + diff, current.length), 0); if (fromIndex < 0 || toIndex === fromIndex) return current; - console.log(fromIndex, toIndex); const modified = [...current]; const [column] = modified.splice(fromIndex, 1); modified.splice(toIndex, 0, column); diff --git a/src/hooks/useCommandBus.ts b/src/hooks/useCommandBus.ts index 8c4e214c..e91e42da 100644 --- a/src/hooks/useCommandBus.ts +++ b/src/hooks/useCommandBus.ts @@ -1,14 +1,10 @@ import { useRequestMessage, useHandleMessage } from '@/hooks/useMessageBus'; -type UseHandleCommandProps = { - commandType: string; - handler: (command: Command) => void; -}; - type CommandBase = { command: T }; export type OpenPostForm = CommandBase<'openPostForm'> & { content?: string }; export type ClosePostForm = CommandBase<'closePostForm'>; +export type MoveToFirstItem = CommandBase<'moveToFirstItem'>; export type MoveToNextItem = CommandBase<'moveToNextItem'>; export type MoveToPrevItem = CommandBase<'moveToPrevItem'>; export type MoveToPrevColumn = CommandBase<'moveToPrevColumn'>; @@ -25,6 +21,7 @@ export type CloseItemDetail = CommandBase<'closeItemDetail'>; export type Command = | OpenPostForm | ClosePostForm + | MoveToFirstItem | MoveToNextItem | MoveToPrevItem | MoveToPrevColumn @@ -40,16 +37,23 @@ export type Command = export type CommandType = Command['command']; +type UseHandleCommandProps> = { + commandType: T; + handler: (command: C) => void; +}; + export const useRequestCommand = () => useRequestMessage(() => ({ id: 'CommandChannel' })); -export const useHandleCommand = (propsProvider: () => UseHandleCommandProps) => { +export const useHandleCommand = >( + propsProvider: () => UseHandleCommandProps, +) => { useHandleMessage(() => ({ id: 'CommandChannel', - handler: (command) => { + handler: (command: Command) => { const { commandType, handler } = propsProvider(); if (command.command === commandType) { - handler(command); + handler(command as C); } }, })); diff --git a/src/hooks/useCursor.ts b/src/hooks/useCursor.ts new file mode 100644 index 00000000..d463d42b --- /dev/null +++ b/src/hooks/useCursor.ts @@ -0,0 +1,40 @@ +import { createRoot, createSignal, createContext, useContext } from 'solid-js'; + +export type Cursor = { + columnId?: string; + timelineId?: string; + itemId?: string; +}; + +export type ColumnCursor = { columnId: string }; +export type TimelineCursor = { timelineId: string }; +export type ItemCursor = { itemId: string }; + +export const ColumnCursorContext = createContext(); +export const useColumnCursor = () => { + const columnCursor = useContext(ColumnCursorContext); + if (columnCursor == null) throw new Error('invalid use of columnCursor'); + + return { columnCursor }; +}; + +export const TimelineCursorContext = createContext(); +export const useTimelineCursor = () => { + const timelineCursor = useContext(TimelineCursorContext); + if (timelineCursor == null) throw new Error('invalid use of timelineCursor'); + + return { timelineCursor }; +}; + +export const ItemCursorContext = createContext(); +export const useItemCursor = () => { + const itemCursor = useContext(ItemCursorContext); + if (itemCursor == null) throw new Error('invalid use of timelineCursor'); + + return { itemCursor }; +}; + +const [cursor, setCursor] = createRoot(() => createSignal({})); +const useCursor = () => ({ cursor, setCursor }); + +export default useCursor; diff --git a/src/hooks/useShortcutKeys.ts b/src/hooks/useShortcutKeys.ts index 053fff9a..dcb43c24 100644 --- a/src/hooks/useShortcutKeys.ts +++ b/src/hooks/useShortcutKeys.ts @@ -8,24 +8,25 @@ type Shortcut = { key: string; command: Command }; const defaultShortcut: Shortcut[] = [ { key: 'n', command: { command: 'openPostForm' } }, - { key: 'h', command: { command: 'moveToPrevColumn' } }, - { key: 'j', command: { command: 'moveToNextItem' } }, - { key: 'k', command: { command: 'moveToPrevItem' } }, - { key: 'l', command: { command: 'moveToNextColumn' } }, - { key: '1', command: { command: 'moveToColumn', columnIndex: 1 } }, - { key: '2', command: { command: 'moveToColumn', columnIndex: 2 } }, - { key: '3', command: { command: 'moveToColumn', columnIndex: 3 } }, - { key: '4', command: { command: 'moveToColumn', columnIndex: 4 } }, - { key: '5', command: { command: 'moveToColumn', columnIndex: 5 } }, - { key: '6', command: { command: 'moveToColumn', columnIndex: 6 } }, - { key: '7', command: { command: 'moveToColumn', columnIndex: 7 } }, - { key: '8', command: { command: 'moveToColumn', columnIndex: 8 } }, - { key: '9', command: { command: 'moveToColumn', columnIndex: 9 } }, + { key: '1', command: { command: 'moveToColumn', columnIndex: 0 } }, + { key: '2', command: { command: 'moveToColumn', columnIndex: 1 } }, + { key: '3', command: { command: 'moveToColumn', columnIndex: 2 } }, + { key: '4', command: { command: 'moveToColumn', columnIndex: 3 } }, + { key: '5', command: { command: 'moveToColumn', columnIndex: 4 } }, + { key: '6', command: { command: 'moveToColumn', columnIndex: 5 } }, + { key: '7', command: { command: 'moveToColumn', columnIndex: 6 } }, + { key: '8', command: { command: 'moveToColumn', columnIndex: 7 } }, + { key: '9', command: { command: 'moveToColumn', columnIndex: 8 } }, { key: '0', command: { command: 'moveToLastColumn' } }, + { key: 'Home', command: { command: 'moveToFirstItem' } }, + { key: 'ArrowRight', command: { command: 'moveToNextColumn' } }, + { key: 'h', command: { command: 'moveToPrevColumn' } }, { key: 'ArrowLeft', command: { command: 'moveToPrevColumn' } }, + { key: 'l', command: { command: 'moveToNextColumn' } }, { key: 'ArrowDown', command: { command: 'moveToNextItem' } }, + { key: 'j', command: { command: 'moveToNextItem' } }, { key: 'ArrowUp', command: { command: 'moveToPrevItem' } }, - { key: 'ArrowRight', command: { command: 'moveToNextColumn' } }, + { key: 'k', command: { command: 'moveToPrevItem' } }, { key: 'f', command: { command: 'like' } }, { key: 't', command: { command: 'repost' } }, { key: 'r', command: { command: 'openReplyForm' } },