share main timeline logic with thread timeline
This commit is contained in:
parent
cfde8f20ee
commit
d8cb89ca57
9 changed files with 189 additions and 130 deletions
|
|
@ -49,6 +49,7 @@ import {
|
|||
useEventDivider,
|
||||
useFocusItem,
|
||||
useGetItemElement,
|
||||
useJumpHandlers,
|
||||
useMessageEditorOutOfViewFocus,
|
||||
useOnRangeChange,
|
||||
useReplyClickHandler,
|
||||
|
|
@ -214,41 +215,14 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
|
|||
);
|
||||
useMessageEditorOutOfViewFocus(scrollRef, scrollToElement, editId);
|
||||
|
||||
const handleOpenEvent = useCallback(
|
||||
async (evtId: string) => {
|
||||
const index = timeline.findIndex(evtId);
|
||||
if (index === -1) {
|
||||
setEventLoading(true);
|
||||
await timeline.jumpToEvent(evtId);
|
||||
setEventLoading(false);
|
||||
setFocusItem({ eventId: evtId, highlight: true });
|
||||
return;
|
||||
}
|
||||
|
||||
scrollToItem(index, { align: 'center', behavior: 'smooth', stopInView: true });
|
||||
setFocusItem({ eventId: evtId, highlight: true, preventScroll: true });
|
||||
},
|
||||
[timeline, scrollToItem, setFocusItem]
|
||||
const { handleOpenEvent, handleJumpToUnread } = useJumpHandlers(
|
||||
timeline,
|
||||
readReceipt,
|
||||
scrollToItem,
|
||||
setFocusItem,
|
||||
setEventLoading
|
||||
);
|
||||
|
||||
const handleJumpToUnread = useCallback(async () => {
|
||||
if (!readReceipt) return;
|
||||
const unreadEvent = timeline.findFirstUnread(ct.session.userId, readReceipt.ts);
|
||||
if (!unreadEvent) {
|
||||
setEventLoading(true);
|
||||
await timeline.jumpToEvent(readReceipt.eventId);
|
||||
setEventLoading(false);
|
||||
const unreadEvId =
|
||||
timeline.findFirstUnread(ct.session.userId, readReceipt.ts)?.event_id ??
|
||||
readReceipt.eventId;
|
||||
setFocusItem({ eventId: unreadEvId, highlight: false });
|
||||
return;
|
||||
}
|
||||
const index = timeline.findIndex(unreadEvent.event_id);
|
||||
|
||||
scrollToItem(index, { align: 'center', behavior: 'instant', stopInView: true });
|
||||
}, [ct, readReceipt, timeline, scrollToItem, setFocusItem]);
|
||||
|
||||
useEffect(() => {
|
||||
if (eventId) {
|
||||
handleOpenEvent(eventId);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ import { getThreadId } from '../../../utils/room';
|
|||
import { ItemRange, ScrollToElement, ScrollToItem } from '../../../hooks/useVirtualPaginator';
|
||||
import { randomStr } from '../../../utils/common';
|
||||
import { useObservableValue } from '../../../hooks/useObservableValue';
|
||||
import { ReadReceiptObject } from '../../../../client/storage/collections';
|
||||
import { useAlive } from '../../../hooks/useAlive';
|
||||
|
||||
const TimelineViewContext = createContext<TimelineView | null>(null);
|
||||
export const TimelineViewProvider = TimelineViewContext.Provider;
|
||||
|
|
@ -344,6 +346,60 @@ export const useFocusItem = (timeline: TimelineView, scrollToItem: ScrollToItem)
|
|||
};
|
||||
};
|
||||
|
||||
export const useJumpHandlers = (
|
||||
timeline: TimelineView,
|
||||
readReceipt: ReadReceiptObject | undefined,
|
||||
scrollToItem: ScrollToItem,
|
||||
setFocusItem: (focus: FocusItem) => void,
|
||||
setEventLoading: (loading: boolean) => void,
|
||||
threadId?: string
|
||||
) => {
|
||||
const ct = useClient();
|
||||
const alive = useAlive();
|
||||
|
||||
const handleOpenEvent = useCallback(
|
||||
async (evtId: string) => {
|
||||
const index = timeline.findIndex(evtId);
|
||||
if (index !== -1) {
|
||||
scrollToItem(index, { align: 'center', behavior: 'smooth', stopInView: true });
|
||||
setFocusItem({ eventId: evtId, highlight: true, preventScroll: true });
|
||||
return;
|
||||
}
|
||||
if (evtId !== threadId) {
|
||||
// TODO: handle it properly
|
||||
setEventLoading(true);
|
||||
await timeline.jumpToEvent(evtId);
|
||||
if (!alive()) return;
|
||||
setEventLoading(false);
|
||||
}
|
||||
setFocusItem({ eventId: evtId, highlight: true });
|
||||
},
|
||||
[timeline, scrollToItem, setFocusItem, setEventLoading, alive, threadId]
|
||||
);
|
||||
|
||||
const handleJumpToUnread = useCallback(async () => {
|
||||
if (!readReceipt) return;
|
||||
const unreadEvent = timeline.findFirstUnread(ct.session.userId, readReceipt.ts);
|
||||
if (unreadEvent) {
|
||||
const index = timeline.findIndex(unreadEvent.event_id);
|
||||
scrollToItem(index, { align: 'center', behavior: 'instant', stopInView: true });
|
||||
return;
|
||||
}
|
||||
setEventLoading(true);
|
||||
await timeline.jumpToEvent(readReceipt.eventId);
|
||||
if (!alive()) return;
|
||||
setEventLoading(false);
|
||||
const unreadEvId =
|
||||
timeline.findFirstUnread(ct.session.userId, readReceipt.ts)?.event_id ?? readReceipt.eventId;
|
||||
setFocusItem({ eventId: unreadEvId, highlight: false });
|
||||
}, [ct, readReceipt, timeline, scrollToItem, setFocusItem, setEventLoading, alive]);
|
||||
|
||||
return {
|
||||
handleOpenEvent,
|
||||
handleJumpToUnread,
|
||||
};
|
||||
};
|
||||
|
||||
export const useReplyClickHandler = (
|
||||
editor: Editor,
|
||||
threadId?: string
|
||||
|
|
|
|||
|
|
@ -64,11 +64,7 @@ export function ThreadView({ threadTimeline, threadFocusRef, requestClose }: Thr
|
|||
</Box>
|
||||
</PageHeader>
|
||||
|
||||
<TimelineView
|
||||
threadTimeline={threadTimeline}
|
||||
threadInputRef={threadInputRef}
|
||||
editor={editor}
|
||||
/>
|
||||
<TimelineView timeline={threadTimeline} threadInputRef={threadInputRef} editor={editor} />
|
||||
<Box shrink="No" direction="Column">
|
||||
<div style={{ padding: `0 ${config.space.S400}` }}>
|
||||
<RoomInput
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import React, { RefObject, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
import { Editor } from 'slate';
|
||||
import { Box, Scroll, Text, config } from 'folds';
|
||||
import { MessageEventWithoutRoomId } from 'neurone/lib/api/types';
|
||||
import { useVirtualPaginator } from '../../../hooks/useVirtualPaginator';
|
||||
import { useAlive } from '../../../hooks/useAlive';
|
||||
import { scrollToBottom } from '../../../utils/dom';
|
||||
import {
|
||||
DefaultPlaceholder,
|
||||
CompactPlaceholder,
|
||||
|
|
@ -34,7 +32,7 @@ import { useTheme } from '../../../hooks/useTheme';
|
|||
import { useRoomCreatorsTag } from '../../../hooks/useRoomCreatorsTag';
|
||||
import { usePowerLevelTags, usePowerLevelTagsState } from '../../../hooks/usePowerLevelTags';
|
||||
import { ThreadTimeline } from '../../../../client/room/ThreadTimeline';
|
||||
import { useObservableValue } from '../../../hooks/useObservableValue';
|
||||
import { useObservableValue, useObservableValueListener } from '../../../hooks/useObservableValue';
|
||||
import { useClient } from '../../../hooks/useClient';
|
||||
import { useRoomMembersAPI } from '../../../hooks/useRoomMembers';
|
||||
import { useGetRoomName } from '../../../hooks/storage/useGetRoomName';
|
||||
|
|
@ -44,16 +42,20 @@ import {
|
|||
useEditorResizeStickBottom,
|
||||
useEventCollapse,
|
||||
useEventDivider,
|
||||
useFocusItem,
|
||||
useGetItemElement,
|
||||
useJumpHandlers,
|
||||
useMessageEditorOutOfViewFocus,
|
||||
useOnRangeChange,
|
||||
useReplyClickHandler,
|
||||
useRestoreScrollToBottom,
|
||||
useTimelineBatch,
|
||||
useTimelineContainerResizeStickBottom,
|
||||
useTimelinePaginate,
|
||||
useTimelinePrefetch,
|
||||
useTimelineRelations,
|
||||
useTimelineScrollRestore,
|
||||
useUnreadMarker,
|
||||
useUserClickHandler,
|
||||
useUsernameClickHandler,
|
||||
} from '../hooks/useTimeline';
|
||||
|
|
@ -71,16 +73,18 @@ import { useMessageParserOptions } from '../../../hooks/useMessageParserOptions'
|
|||
import { MessageContent } from '../../../components/MessageContent';
|
||||
import { MessageEditor } from '../message/MessageEditor';
|
||||
import { useSendReaction } from '../hooks/useSendReaction';
|
||||
import { useWindowFocus } from '../../../hooks/useWindowFocus';
|
||||
|
||||
type TimelineViewProps = {
|
||||
threadTimeline: ThreadTimeline;
|
||||
timeline: ThreadTimeline;
|
||||
threadInputRef: RefObject<HTMLElement>;
|
||||
editor: Editor;
|
||||
eventId: string;
|
||||
};
|
||||
|
||||
const PAGINATION_LIMIT = 80;
|
||||
|
||||
export function TimelineView({ threadTimeline, threadInputRef, editor }: TimelineViewProps) {
|
||||
export function TimelineView({ timeline, threadInputRef, editor, eventId }: TimelineViewProps) {
|
||||
const ct = useClient();
|
||||
const roomId = useRoom();
|
||||
const encryptedRooms = useObservableValue(ct.encryptedRooms);
|
||||
|
|
@ -134,102 +138,91 @@ export function TimelineView({ threadTimeline, threadInputRef, editor }: Timelin
|
|||
const imagePackRooms = useImagePackRooms(roomId);
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const scrollToBottomRef = useRef({
|
||||
count: 0,
|
||||
smooth: true,
|
||||
});
|
||||
|
||||
const [focusItem, setFocusItem] = useState<
|
||||
| {
|
||||
index: number;
|
||||
scrollTo: boolean;
|
||||
highlight: boolean;
|
||||
}
|
||||
| undefined
|
||||
>();
|
||||
const alive = useAlive();
|
||||
const timelineContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { linkifyOpts, htmlReactParserOptions, htmlReactParserInlineOptions } =
|
||||
useMessageParserOptions(roomId, getRoomName, membersAPI.getName);
|
||||
|
||||
const threadMeta = useObservableValue(threadTimeline.meta);
|
||||
const { redacted, reactions, edits } = useTimelineRelations(threadTimeline);
|
||||
const threadMeta = useObservableValue(timeline.meta);
|
||||
const { redacted, reactions, edits } = useTimelineRelations(timeline);
|
||||
|
||||
const localEventSenders = useObservableValue(threadTimeline.outbox.senders);
|
||||
const localEventSenders = useObservableValue(timeline.outbox.senders);
|
||||
const { messageBatch, events, range, rangeAtStart, rangeAtEnd, atLiveEnd } =
|
||||
useTimelineBatch(threadTimeline);
|
||||
useTimelineBatch(timeline);
|
||||
|
||||
const getScrollElement = useCallback(() => scrollRef.current, []);
|
||||
|
||||
useTimelinePrefetch(threadTimeline, PAGINATION_LIMIT);
|
||||
const handlePaginate = useTimelinePaginate(threadTimeline, PAGINATION_LIMIT);
|
||||
useTimelineScrollRestore(threadTimeline, scrollRef);
|
||||
useRestoreScrollToBottom(scrollRef, messageBatch);
|
||||
const readReceipt = useUserReadReceipt(timeline.threadId);
|
||||
const lastReadEventInTimeline = useMemo(
|
||||
() => !!readReceipt && !!messageBatch.messages.get(readReceipt.eventId),
|
||||
[readReceipt, messageBatch]
|
||||
);
|
||||
|
||||
const [eventLoading, setEventLoading] = useState(false);
|
||||
useTimelinePrefetch(timeline, PAGINATION_LIMIT, !eventLoading);
|
||||
const handlePaginate = useTimelinePaginate(timeline, PAGINATION_LIMIT, !eventLoading);
|
||||
|
||||
const { getItems, scrollToItem, scrollToElement, observeBackAnchor, observeFrontAnchor } =
|
||||
useVirtualPaginator({
|
||||
count: messageBatch.size,
|
||||
limit: PAGINATION_LIMIT,
|
||||
range,
|
||||
onRangeChange: useOnRangeChange(threadTimeline),
|
||||
onRangeChange: useOnRangeChange(timeline),
|
||||
getScrollElement,
|
||||
getItemElement: useGetItemElement(scrollRef),
|
||||
onEnd: handlePaginate,
|
||||
});
|
||||
|
||||
const tryAutoMarkAsRead = useCallback(() => {
|
||||
console.log('mark thread as read.');
|
||||
}, []);
|
||||
const { focusItem, setFocusItem } = useFocusItem(timeline, scrollToItem);
|
||||
const focused = useWindowFocus();
|
||||
useTimelineScrollRestore(timeline, scrollRef);
|
||||
useRestoreScrollToBottom(scrollRef, messageBatch);
|
||||
|
||||
const { atBottomLazy, atBottomObservable, atBottomAnchorRef } = useAtBottomToggle(
|
||||
getScrollElement,
|
||||
messageBatch.next.end ?? false
|
||||
atLiveEnd
|
||||
);
|
||||
|
||||
const tryAutoMarkAsRead = useCallback(() => {
|
||||
if (!atBottomObservable.read) return;
|
||||
console.log('%cmark as read.', 'background: blue; color: white; padding: 5px 10px;');
|
||||
}, [atBottomObservable]);
|
||||
useObservableValueListener(atBottomObservable, tryAutoMarkAsRead);
|
||||
|
||||
const { markerTs } = useUnreadMarker(atBottomLazy, focused, readReceipt?.ts);
|
||||
|
||||
useTimelineContainerResizeStickBottom(
|
||||
timeline,
|
||||
getScrollElement,
|
||||
useCallback(() => timelineContainerRef.current, []),
|
||||
atBottomObservable,
|
||||
!focusItem && atLiveEnd
|
||||
);
|
||||
useEditorResizeStickBottom(
|
||||
getScrollElement,
|
||||
useCallback(() => threadInputRef?.current ?? null, [threadInputRef]),
|
||||
useCallback(() => threadInputRef.current, [threadInputRef]),
|
||||
atBottomObservable
|
||||
);
|
||||
|
||||
// scroll to focused message
|
||||
useLayoutEffect(() => {
|
||||
if (focusItem && focusItem.scrollTo) {
|
||||
scrollToItem(focusItem.index, {
|
||||
behavior: 'instant',
|
||||
align: 'center',
|
||||
stopInView: true,
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (!alive()) return;
|
||||
setFocusItem((currentItem) => {
|
||||
if (currentItem === focusItem) return undefined;
|
||||
return currentItem;
|
||||
});
|
||||
}, 2000);
|
||||
}, [alive, focusItem, scrollToItem]);
|
||||
|
||||
// scroll to bottom of timeline
|
||||
const scrollToBottomCount = scrollToBottomRef.current.count;
|
||||
useLayoutEffect(() => {
|
||||
if (scrollToBottomCount > 0) {
|
||||
const scrollEl = scrollRef.current;
|
||||
if (scrollEl)
|
||||
scrollToBottom(scrollEl, scrollToBottomRef.current.smooth ? 'smooth' : 'instant');
|
||||
}
|
||||
}, [scrollToBottomCount]);
|
||||
|
||||
useMessageEditorOutOfViewFocus(scrollRef, scrollToElement, editId);
|
||||
|
||||
const handleOpenReply = useCallback((replyId: string) => {
|
||||
console.log(replyId);
|
||||
}, []);
|
||||
const { handleOpenEvent, handleJumpToUnread } = useJumpHandlers(
|
||||
timeline,
|
||||
readReceipt,
|
||||
scrollToItem,
|
||||
setFocusItem,
|
||||
setEventLoading,
|
||||
timeline.threadId
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (eventId) {
|
||||
handleOpenEvent(eventId);
|
||||
}
|
||||
}, [eventId, handleOpenEvent]);
|
||||
|
||||
const handleUserClick = useUserClickHandler();
|
||||
const handleUsernameClick = useUsernameClickHandler(editor);
|
||||
const [replyDraft, handleReplyClick] = useReplyClickHandler(editor, threadTimeline.threadId);
|
||||
const [replyDraft, handleReplyClick] = useReplyClickHandler(editor, timeline.threadId);
|
||||
|
||||
const { reactionSender, handleAddReaction } = useSendReaction(reactions, redacted);
|
||||
|
||||
|
|
@ -254,20 +247,20 @@ export function TimelineView({ threadTimeline, threadInputRef, editor }: Timelin
|
|||
|
||||
const editEvents = edits.get(event.event_id);
|
||||
const editedEvent = getLatestEditEvent(event, editEvents, redacted);
|
||||
const highlighted = focusItem?.index === item && focusItem.highlight;
|
||||
const highlighted = focusItem?.eventId === event.event_id && focusItem.highlight;
|
||||
|
||||
const replyEventId = fallbackReply(event.content) ? undefined : getReplyId(event.content);
|
||||
const replyJSX = replyEventId ? (
|
||||
<Reply
|
||||
roomId={roomId}
|
||||
eventId={replyEventId}
|
||||
onClick={handleOpenReply}
|
||||
onClick={handleOpenEvent}
|
||||
onUserClick={handleUserClick}
|
||||
getMemberPowerTag={getMemberPowerTag}
|
||||
accessibleTagColors={accessiblePowerTagColors}
|
||||
legacyUsernameColor={legacyUsernameColor || direct}
|
||||
getMemberName={membersAPI.getName}
|
||||
timeline={threadTimeline}
|
||||
timeline={timeline}
|
||||
htmlReactParserOptions={htmlReactParserInlineOptions}
|
||||
linkifyOpts={linkifyOpts}
|
||||
/>
|
||||
|
|
@ -360,7 +353,6 @@ export function TimelineView({ threadTimeline, threadInputRef, editor }: Timelin
|
|||
);
|
||||
};
|
||||
|
||||
const readReceipt = useUserReadReceipt(threadTimeline.threadId);
|
||||
const {
|
||||
getDayDivider,
|
||||
getThreadRootDivider,
|
||||
|
|
@ -368,8 +360,8 @@ export function TimelineView({ threadTimeline, threadInputRef, editor }: Timelin
|
|||
updateDivider,
|
||||
clearDayDivider,
|
||||
clearNewDivider,
|
||||
} = useEventDivider(readReceipt?.ts);
|
||||
const { getCollapsed, updateCollapse } = useEventCollapse(threadTimeline.threadId);
|
||||
} = useEventDivider(markerTs);
|
||||
const { getCollapsed, updateCollapse } = useEventCollapse(timeline.threadId);
|
||||
const eventRenderer = (event: MessageEventWithoutRoomId, item: number) => {
|
||||
if (event.sender && ignoredUsersSet.has(event.sender)) {
|
||||
return null;
|
||||
|
|
@ -409,11 +401,8 @@ export function TimelineView({ threadTimeline, threadInputRef, editor }: Timelin
|
|||
|
||||
return (
|
||||
<Box grow="Yes" style={{ position: 'relative' }}>
|
||||
{readReceipt && (
|
||||
<UnreadControls
|
||||
onJumpToUnread={() => threadTimeline.jumpToEvent(readReceipt.eventId)}
|
||||
onMarkAsRead={() => undefined}
|
||||
/>
|
||||
{readReceipt && !lastReadEventInTimeline && (
|
||||
<UnreadControls onJumpToUnread={handleJumpToUnread} onMarkAsRead={() => undefined} />
|
||||
)}
|
||||
<Scroll ref={scrollRef} size="0" visibility="Hover" hideTrack>
|
||||
<Box direction="Column" style={{ minHeight: '100%' }}>
|
||||
|
|
@ -448,7 +437,7 @@ export function TimelineView({ threadTimeline, threadInputRef, editor }: Timelin
|
|||
padding: `${config.space.S600} ${config.space.S400} ${config.space.S600} 0`,
|
||||
}}
|
||||
>
|
||||
{!messageBatch.prev.end || !rangeAtStart || !threadMeta ? (
|
||||
{!messageBatch.prev.end || !rangeAtStart || !threadMeta || eventLoading ? (
|
||||
<PlaceholderList
|
||||
ref={observeBackAnchor}
|
||||
count={5}
|
||||
|
|
@ -459,11 +448,12 @@ export function TimelineView({ threadTimeline, threadInputRef, editor }: Timelin
|
|||
) : (
|
||||
<>{eventRenderer(threadMeta.rootEvent, -1)}</>
|
||||
)}
|
||||
{getItems().map((item) => {
|
||||
const event = events[item];
|
||||
if (!event) return null;
|
||||
return eventRenderer(event, item);
|
||||
})}
|
||||
{!eventLoading &&
|
||||
getItems().map((item) => {
|
||||
const event = events[item];
|
||||
if (!event) return null;
|
||||
return eventRenderer(event, item);
|
||||
})}
|
||||
{(!messageBatch.next.end || !rangeAtEnd) && (
|
||||
<PlaceholderList
|
||||
ref={observeFrontAnchor}
|
||||
|
|
|
|||
|
|
@ -255,6 +255,12 @@ export class ThreadTimeline implements TimelineView, Disposable {
|
|||
}
|
||||
|
||||
public async jumpToEvent(eventId: string): Promise<JumpTarget> {
|
||||
// thread root is not part of timeline.
|
||||
// return early so we do not have to wait for response
|
||||
if (eventId === this.threadId) {
|
||||
return { type: 'main', eventId };
|
||||
}
|
||||
|
||||
const resp = await this.request(
|
||||
getContext({
|
||||
pathParams: {
|
||||
|
|
@ -287,8 +293,9 @@ export class ThreadTimeline implements TimelineView, Disposable {
|
|||
if (threadId && threadId !== this.threadId) {
|
||||
return { type: 'thread', eventId, threadId };
|
||||
}
|
||||
// event is not the root of this thread
|
||||
if (!threadId && event.event_id !== this.threadId) {
|
||||
// no untheaed reply should be put in timeline
|
||||
// not event thread root
|
||||
if (!threadId) {
|
||||
return { type: 'main', eventId };
|
||||
}
|
||||
|
||||
|
|
@ -300,9 +307,18 @@ export class ThreadTimeline implements TimelineView, Disposable {
|
|||
};
|
||||
}
|
||||
|
||||
public async jumpToOldest(): Promise<void> {
|
||||
if (this.batch.read.prev.end) {
|
||||
this.range.setRange(MessageRange.atBackwardEnd(this.batch.read.size));
|
||||
return;
|
||||
}
|
||||
|
||||
this.batchManager.reload(MessageBatch.emptyAtBackwardEnd());
|
||||
}
|
||||
|
||||
public async jumpToLatest(): Promise<void> {
|
||||
if (this.batch.read.next.end) {
|
||||
this.range.setRange(MessageRange.atLiveEnd(this.batch.read.size));
|
||||
this.range.setRange(MessageRange.atForwardEnd(this.batch.read.size));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -287,9 +287,18 @@ export class Timeline implements TimelineView, Disposable {
|
|||
};
|
||||
}
|
||||
|
||||
public async jumpToOldest(): Promise<void> {
|
||||
if (this.batch.read.prev.end) {
|
||||
this.range.setRange(MessageRange.atBackwardEnd(this.batch.read.size));
|
||||
return;
|
||||
}
|
||||
|
||||
this.batchManager.reload(MessageBatch.emptyAtBackwardEnd());
|
||||
}
|
||||
|
||||
public async jumpToLatest(): Promise<void> {
|
||||
if (this.batch.read.next.end) {
|
||||
this.range.setRange(MessageRange.atLiveEnd(this.batch.read.size));
|
||||
this.range.setRange(MessageRange.atForwardEnd(this.batch.read.size));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export interface TimelineView {
|
|||
paginateBackward(limit: number): Promise<void>;
|
||||
paginateForward(limit: number): Promise<void>;
|
||||
jumpToEvent(eventId: string): Promise<JumpTarget>;
|
||||
jumpToOldest(): Promise<void>;
|
||||
jumpToLatest(): Promise<void>;
|
||||
findEvent(eventId: string): MessageEventWithoutRoomId | undefined;
|
||||
findIndex(eventId: string): number;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,16 @@ export class MessageBatch {
|
|||
|
||||
public readonly change: BatchChange;
|
||||
|
||||
static emptyAtBackwardEnd(): MessageBatch {
|
||||
return new MessageBatch(
|
||||
randomStr(),
|
||||
new Messages([]),
|
||||
new BatchInfo(undefined, true),
|
||||
new BatchInfo(),
|
||||
new BatchChange()
|
||||
);
|
||||
}
|
||||
|
||||
static emptyAtForwardEnd(): MessageBatch {
|
||||
return new MessageBatch(
|
||||
randomStr(),
|
||||
|
|
|
|||
|
|
@ -24,7 +24,14 @@ export class MessageRange implements Disposable {
|
|||
|
||||
static rangeLimit = 80;
|
||||
|
||||
static atLiveEnd(len: number): RangeInfo {
|
||||
static atBackwardEnd(len: number): RangeInfo {
|
||||
return {
|
||||
start: 0,
|
||||
end: Math.min(len, 0 + this.rangeLimit),
|
||||
};
|
||||
}
|
||||
|
||||
static atForwardEnd(len: number): RangeInfo {
|
||||
return {
|
||||
start: Math.max(0, len - this.rangeLimit),
|
||||
end: len,
|
||||
|
|
@ -34,7 +41,7 @@ export class MessageRange implements Disposable {
|
|||
constructor(batch: ObservableValue<MessageBatch>) {
|
||||
const len = batch.read.size;
|
||||
|
||||
this.info = new ObservableValue(MessageRange.atLiveEnd(len), this.publishKey);
|
||||
this.info = new ObservableValue(MessageRange.atForwardEnd(len), this.publishKey);
|
||||
this.oldLength = len;
|
||||
|
||||
this.disposer.collect(batch.subscribe(this.onBatchUpdate.bind(this)));
|
||||
|
|
@ -49,7 +56,7 @@ export class MessageRange implements Disposable {
|
|||
const lenAdded = size - oldLen;
|
||||
this.setRange(
|
||||
oldLen === 0 && batch.next.end
|
||||
? MessageRange.atLiveEnd(size)
|
||||
? MessageRange.atForwardEnd(size)
|
||||
: this.adjustBackwardGrowth(lenAdded)
|
||||
);
|
||||
return;
|
||||
|
|
@ -71,7 +78,7 @@ export class MessageRange implements Disposable {
|
|||
return;
|
||||
}
|
||||
|
||||
this.setRange(MessageRange.atLiveEnd(size));
|
||||
this.setRange(MessageRange.atForwardEnd(size));
|
||||
|
||||
// clear the top cache since the timeline batch has changed
|
||||
this.scrollTopCache = undefined;
|
||||
|
|
|
|||
Loading…
Reference in a new issue