From d4a92abf9daf70473cd09b6e60521b81edbba579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 5 Mar 2026 10:46:29 +0100 Subject: [PATCH] nicolium: experimental timeline: hotkey navigation, queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/nicolium/src/columns/timeline.tsx | 88 ++++++++++++++----- .../src/features/ui/components/hotkeys.tsx | 2 +- packages/nicolium/src/pages/fun/circle.tsx | 5 +- .../src/queries/timelines/use-timeline.ts | 9 +- packages/nicolium/src/stores/timelines.ts | 15 ++++ packages/pl-api/lib/main.ts | 40 ++++++++- packages/pl-api/package.json | 2 +- 7 files changed, 131 insertions(+), 30 deletions(-) diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index df13c14e6..edea4b39c 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -1,10 +1,13 @@ import clsx from 'clsx'; -import React from 'react'; +import React, { useRef } from 'react'; +import { defineMessages } from 'react-intl'; import LoadMore from '@/components/load-more'; +import ScrollTopButton from '@/components/scroll-top-button'; import ScrollableList from '@/components/scrollable-list'; import Status from '@/components/statuses/status'; import Tombstone from '@/components/statuses/tombstone'; +import Portal from '@/components/ui/portal'; import PlaceholderStatus from '@/features/placeholder/components/placeholder-status'; import { useStatus } from '@/queries/statuses/use-status'; import { @@ -18,9 +21,22 @@ import { usePublicTimeline, useWrenchedTimeline, } from '@/queries/timelines/use-timelines'; +import { selectChild } from '@/utils/scroll-utils'; import type { FilterContextType } from '@/queries/settings/use-filters'; import type { TimelineEntry } from '@/stores/timelines'; +import type { VirtuosoHandle } from 'react-virtuoso'; + +const messages = defineMessages({ + queue: { + id: 'status_list.queue_label', + defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}', + }, + queueLiveRegion: { + id: 'status_list.queue_label.live_region', + defaultMessage: '{count} new {count, plural, one {post} other {posts}}.', + }, +}); interface ITimelineStatus { id: string; @@ -86,9 +102,24 @@ interface ITimeline { } const Timeline: React.FC = ({ query, contextType = 'public' }) => { - const { entries, fetchNextPage, isFetching, isPending } = query; + const node = useRef(null); - const renderEntry = (entry: TimelineEntry) => { + const { entries, queuedCount, fetchNextPage, dequeueEntries, isFetching, isPending } = query; + + const handleMoveUp = (index: number) => { + selectChild(index - 1, node, document.getElementById('status-list') ?? undefined); + }; + + const handleMoveDown = (index: number) => { + selectChild( + index + 1, + node, + document.getElementById('status-list') ?? undefined, + entries.length, + ); + }; + + const renderEntry = (entry: TimelineEntry, index: number) => { if (entry.type === 'status') { return ( = ({ query, contextType = 'public' }) => { isConnectedTop={entry.isConnectedTop} isConnectedBottom={entry.isConnectedBottom} contextType={contextType} - // onMoveUp={handleMoveUp} - // onMoveDown={handleMoveDown} + onMoveUp={() => handleMoveUp(index)} + onMoveDown={() => handleMoveDown(index)} // contextType={timelineId} // showGroup={showGroup} // variant={divideType === 'border' ? 'slim' : 'rounded'} @@ -115,24 +146,35 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { }; return ( - } - placeholderCount={20} - // className={className} - // listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', { - // 'divide-none': divideType !== 'border', - // })} - // itemClassName={clsx({ - // 'pb-3': divideType !== 'border', - // })} - // {...other} - > - {(entries || []).map(renderEntry)} - + <> + + + + } + placeholderCount={20} + ref={node} + // className={className} + // listClassName={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', { + // 'divide-none': divideType !== 'border', + // })} + // itemClassName={clsx({ + // 'pb-3': divideType !== 'border', + // })} + // {...other} + > + {(entries || []).map(renderEntry)} + + ); }; diff --git a/packages/nicolium/src/features/ui/components/hotkeys.tsx b/packages/nicolium/src/features/ui/components/hotkeys.tsx index 7be8f4720..89fb67241 100644 --- a/packages/nicolium/src/features/ui/components/hotkeys.tsx +++ b/packages/nicolium/src/features/ui/components/hotkeys.tsx @@ -167,7 +167,7 @@ const hotkeyMatcherMap = { type HotkeyName = keyof typeof hotkeyMatcherMap; -type HandlerMap = Partial void>>; +type HandlerMap = Partial void | boolean>>; function useHotkeys(handlers: HandlerMap) { const ref = useRef(null); diff --git a/packages/nicolium/src/pages/fun/circle.tsx b/packages/nicolium/src/pages/fun/circle.tsx index 42e64ae9b..fc3b2f23a 100644 --- a/packages/nicolium/src/pages/fun/circle.tsx +++ b/packages/nicolium/src/pages/fun/circle.tsx @@ -68,9 +68,8 @@ const CirclePage: React.FC = () => { progress: number; }>({ state: 'unrequested', progress: 0 }); const [expanded, setExpanded] = useState(false); - const [users, setUsers] = useState< - Array<{ id: string; avatar?: string; avatar_description?: string; acct: string }> - >(); + const [users, setUsers] = + useState>(); const intl = useIntl(); const dispatch = useAppDispatch(); diff --git a/packages/nicolium/src/queries/timelines/use-timeline.ts b/packages/nicolium/src/queries/timelines/use-timeline.ts index 8854a7544..ddae246ca 100644 --- a/packages/nicolium/src/queries/timelines/use-timeline.ts +++ b/packages/nicolium/src/queries/timelines/use-timeline.ts @@ -52,7 +52,14 @@ const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig? } }, [timelineId, timeline.entries]); - return useMemo(() => ({ ...timeline, fetchNextPage }), [timeline, fetchNextPage]); + const dequeueEntries = useCallback(() => { + timelineActions.dequeueEntries(timelineId); + }, [timelineId]); + + return useMemo( + () => ({ ...timeline, fetchNextPage, dequeueEntries }), + [timeline, fetchNextPage, dequeueEntries], + ); }; export { useTimeline }; diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index eb06bcecf..f18135bcb 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -31,6 +31,8 @@ type TimelineEntry = interface TimelineData { entries: Array; + queuedEntries: Array; + queuedCount: number; isFetching: boolean; isPending: boolean; } @@ -45,6 +47,7 @@ interface State { initialFetch: boolean, ) => void; setLoading: (timelineId: string, isFetching: boolean) => void; + dequeueEntries: (timelineId: string) => void; }; } @@ -137,12 +140,24 @@ const useTimelinesStore = create()( timeline.isFetching = isFetching; if (!isFetching) timeline.isPending = false; }), + dequeueEntries: (timelineId) => + set((state) => { + const timeline = state.timelines[timelineId]; + + if (!timeline || timeline.queuedEntries.length === 0) return; + + timeline.entries.unshift(...timeline.queuedEntries); + timeline.queuedEntries = []; + timeline.queuedCount = 0; + }), }, })), ); const createEmptyTimeline = (): TimelineData => ({ entries: [], + queuedEntries: [], + queuedCount: 0, isFetching: false, isPending: true, }); diff --git a/packages/pl-api/lib/main.ts b/packages/pl-api/lib/main.ts index 5707875c1..7d9494316 100644 --- a/packages/pl-api/lib/main.ts +++ b/packages/pl-api/lib/main.ts @@ -1,4 +1,42 @@ -export { default as PlApiClient } from '@/client'; +export { + default as PlApiClient, + accounts as accountsCategory, + admin as adminCategory, + announcements as announcementsCategory, + antennas as antennasCategory, + apps as appsCategory, + asyncRefreshes as asyncRefreshesCategory, + chats as chatsCategory, + circles as circlesCategory, + drive as driveCategory, + emails as emailsCategory, + events as eventsCategory, + experimental as experimentalCategory, + filtering as filteringCategory, + groupedNotifications as groupedNotificationsCategory, + instance as instanceCategory, + interactionRequests as interactionRequestsCategory, + lists as listsCategory, + media as mediaCategory, + myAccount as myAccountCategory, + notifications as notificationsCategory, + oauth as oauthCategory, + oembed as oembedCategory, + polls as pollsCategory, + pushNotifications as pushNotificationsCategory, + rssFeedSubscriptions as rssFeedSubscriptionsCategory, + scheduledStatuses as scheduledStatusesCategory, + search as searchCategory, + settings as settingsCategory, + shoutbox as shoutboxCategory, + statuses as statusesCategory, + stories as storiesCategory, + streaming as streamingCategory, + subscriptions as subscriptionsCategory, + timelines as timelinesCategory, + trends as trendsCategory, + utils as utilsCategory, +} from '@/client'; export { PlApiBaseClient } from '@/client-base'; export { PlApiDirectoryClient } from '@/directory-client'; export { type Response as PlApiResponse, type AsyncRefreshHeader } from '@/request'; diff --git a/packages/pl-api/package.json b/packages/pl-api/package.json index 0ab837b49..8a57bcc89 100644 --- a/packages/pl-api/package.json +++ b/packages/pl-api/package.json @@ -1,6 +1,6 @@ { "name": "pl-api", - "version": "1.0.0-rc.98", + "version": "1.0.0-rc.99", "homepage": "https://codeberg.org/mkljczk/nicolium/src/branch/develop/packages/pl-api", "bugs": { "url": "https://codeberg.org/mkljczk/nicolium/issues"