diff --git a/packages/nicolium/src/columns/timeline.tsx b/packages/nicolium/src/columns/timeline.tsx index e9d031df3..ea4b747ba 100644 --- a/packages/nicolium/src/columns/timeline.tsx +++ b/packages/nicolium/src/columns/timeline.tsx @@ -220,14 +220,17 @@ const TimelineStatus: React.FC = (props): React.JSX.Element => ); }; -type IBaseTimeline = Pick; +type IBaseTimeline = Pick< + IScrollableList, + 'emptyMessageIcon' | 'emptyMessageText' | 'onTopItemChanged' +>; interface ITimeline extends IBaseTimeline { query: ReturnType; contextType?: FilterContextType; } -const Timeline: React.FC = ({ query, contextType = 'public' }) => { +const Timeline: React.FC = ({ query, contextType = 'public', ...props }) => { const node = useRef(null); const { @@ -293,6 +296,7 @@ const Timeline: React.FC = ({ query, contextType = 'public' }) => { ref={node} hasMore={hasNextPage} onLoadMore={fetchNextPage} + {...props} > {(entries || []).map(renderEntry)} diff --git a/packages/nicolium/src/components/scrollable-list.tsx b/packages/nicolium/src/components/scrollable-list.tsx index 51275a7da..74af0acef 100644 --- a/packages/nicolium/src/components/scrollable-list.tsx +++ b/packages/nicolium/src/components/scrollable-list.tsx @@ -70,6 +70,8 @@ interface IScrollableList extends VirtuosoProps { onScrollToTop?: () => void; /** Callback when the list is scrolled. */ onScroll?: () => void; + /** Callback when the topmost visible item index changes. */ + onTopItemChanged?: (index: number) => void; /** Placeholder component to render while loading. */ placeholderComponent?: React.ComponentType | React.NamedExoticComponent; /** Number of placeholders to render while loading. */ @@ -104,6 +106,7 @@ const ScrollableList = React.forwardRef( onScroll, onScrollToTop, onLoadMore, + onTopItemChanged, className, listClassName, itemClassName, @@ -220,6 +223,7 @@ const ScrollableList = React.forwardRef( // HACK: using the first index can be buggy. // Track the second item instead, unless the endIndex comes before it (eg one 1 item in view). topIndex.current = Math.min(range.startIndex + 1, range.endIndex); + onTopItemChanged?.(topIndex.current); handleScroll(); }; diff --git a/packages/nicolium/src/queries/timelines/use-timeline.ts b/packages/nicolium/src/queries/timelines/use-timeline.ts index 8667ca0b8..fd1e23148 100644 --- a/packages/nicolium/src/queries/timelines/use-timeline.ts +++ b/packages/nicolium/src/queries/timelines/use-timeline.ts @@ -14,7 +14,12 @@ interface StreamConfig { params?: StreamingParams; } -const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig?: StreamConfig) => { +const useTimeline = ( + timelineId: string, + fetcher: TimelineFetcher, + streamConfig?: StreamConfig, + restoring?: boolean, +) => { const timeline = useStoreTimeline(timelineId); const timelineActions = useTimelinesActions(); @@ -30,11 +35,11 @@ const useTimeline = (timelineId: string, fetcher: TimelineFetcher, streamConfig? try { const response = await fetcher(); importEntities({ statuses: response.items }); - timelineActions.expandTimeline(timelineId, response.items, !!response.next, true); + timelineActions.expandTimeline(timelineId, response.items, !!response.next, true, restoring); } catch (error) { timelineActions.setError(timelineId, true); } - }, [timelineId]); + }, [timelineId, restoring]); const fetchNextPage = useCallback(async () => { timelineActions.setLoading(timelineId, true); diff --git a/packages/nicolium/src/queries/timelines/use-timelines.ts b/packages/nicolium/src/queries/timelines/use-timelines.ts index f9c7a3e1a..070ae7fe7 100644 --- a/packages/nicolium/src/queries/timelines/use-timelines.ts +++ b/packages/nicolium/src/queries/timelines/use-timelines.ts @@ -17,14 +17,19 @@ import type { WrenchedTimelineParams, } from 'pl-api'; -const useHomeTimeline = (params?: Omit) => { +const useHomeTimeline = ( + params?: Omit, + maxId?: string, +) => { const client = useClient(); const stream = 'home'; return useTimeline( 'home', - (paginationParams) => client.timelines.homeTimeline({ ...params, ...paginationParams }), + (paginationParams) => + client.timelines.homeTimeline({ ...params, ...(paginationParams || { max_id: maxId }) }), { stream }, + !!maxId, ); }; diff --git a/packages/nicolium/src/stores/timelines.ts b/packages/nicolium/src/stores/timelines.ts index 4c26af603..80c0a2c00 100644 --- a/packages/nicolium/src/stores/timelines.ts +++ b/packages/nicolium/src/stores/timelines.ts @@ -23,8 +23,8 @@ type TimelineEntry = } | { type: 'gap'; - sinceId: string; - maxId: string; + sinceId?: string; + minId: string; }; interface TimelineData { @@ -46,6 +46,7 @@ interface State { statuses: Array, hasMore?: boolean, initialFetch?: boolean, + restoring?: boolean, ) => void; receiveStreamingStatus: (timelineId: string, status: Status) => void; deleteStatus: (statusId: string) => void; @@ -144,13 +145,19 @@ const useTimelinesStore = create()( mutative((set) => ({ timelines: {} as Record, actions: { - expandTimeline: (timelineId, statuses, hasMore, initialFetch = false) => + expandTimeline: (timelineId, statuses, hasMore, initialFetch = false, restoring = false) => set((state) => { const timeline = state.timelines[timelineId] ?? createEmptyTimeline(); const entries = processPage(statuses); if (initialFetch) timeline.entries = entries; else timeline.entries.push(...entries); + if (restoring) { + timeline.entries.unshift({ + type: 'gap', + minId: statuses[0].id, + }); + } timeline.isPending = false; timeline.isFetching = false; if (typeof hasMore === 'boolean') {