nicolium: experimental timeline: hotkey navigation, queue
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -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<ITimeline> = ({ query, contextType = 'public' }) => {
|
||||
const { entries, fetchNextPage, isFetching, isPending } = query;
|
||||
const node = useRef<VirtuosoHandle | null>(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 (
|
||||
<TimelineStatus
|
||||
@ -97,8 +128,8 @@ const Timeline: React.FC<ITimeline> = ({ 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<ITimeline> = ({ query, contextType = 'public' }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollableList
|
||||
id='status-list'
|
||||
key='scrollable-list'
|
||||
isLoading={isFetching}
|
||||
showLoading={isPending}
|
||||
placeholderComponent={() => <PlaceholderStatus variant={'slim'} />}
|
||||
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)}
|
||||
</ScrollableList>
|
||||
<>
|
||||
<Portal>
|
||||
<ScrollTopButton
|
||||
onClick={dequeueEntries}
|
||||
count={queuedCount}
|
||||
message={messages.queue}
|
||||
liveRegionMessage={messages.queueLiveRegion}
|
||||
/>
|
||||
</Portal>
|
||||
<ScrollableList
|
||||
id='status-list'
|
||||
key='scrollable-list'
|
||||
isLoading={isFetching}
|
||||
showLoading={isPending}
|
||||
placeholderComponent={() => <PlaceholderStatus variant={'slim'} />}
|
||||
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)}
|
||||
</ScrollableList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -167,7 +167,7 @@ const hotkeyMatcherMap = {
|
||||
|
||||
type HotkeyName = keyof typeof hotkeyMatcherMap;
|
||||
|
||||
type HandlerMap = Partial<Record<HotkeyName, (event: KeyboardEvent) => void>>;
|
||||
type HandlerMap = Partial<Record<HotkeyName, (event: KeyboardEvent) => void | boolean>>;
|
||||
|
||||
function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
|
||||
const ref = useRef<T>(null);
|
||||
|
||||
@ -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<Array<{ id: string; avatar?: string; avatar_description?: string; acct: string }>>();
|
||||
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -31,6 +31,8 @@ type TimelineEntry =
|
||||
|
||||
interface TimelineData {
|
||||
entries: Array<TimelineEntry>;
|
||||
queuedEntries: Array<TimelineEntry>;
|
||||
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<State>()(
|
||||
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,
|
||||
});
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user