// so timeline can be updated with evt like: edits, reactions etc
if (atBottomRef.current) {
if (document.hasFocus() && (!unreadInfo || mEvt.getSender() === mx.getUserId())) {
+ // Check if the document is in focus (user is actively viewing the app),
+ // and either there are no unread messages or the latest message is from the current user.
+ // If either condition is met, trigger the markAsRead function to send a read receipt.
requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!));
}
- if (document.hasFocus()) {
- scrollToBottomRef.current.count += 1;
- scrollToBottomRef.current.smooth = true;
- } else if (!unreadInfo) {
+ if (!document.hasFocus() && !unreadInfo) {
setUnreadInfo(getRoomUnreadInfo(room));
}
+
+ scrollToBottomRef.current.count += 1;
+ scrollToBottomRef.current.smooth = true;
+
setTimeline((ct) => ({
...ct,
range: {
)
);
+ const handleOpenEvent = useCallback(
+ async (
+ evtId: string,
+ highlight = true,
+ onScroll: ((scrolled: boolean) => void) | undefined = undefined
+ ) => {
+ const evtTimeline = getEventTimeline(room, evtId);
+ const absoluteIndex =
+ evtTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, evtTimeline, evtId);
+
+ if (typeof absoluteIndex === 'number') {
+ const scrolled = scrollToItem(absoluteIndex, {
+ behavior: 'smooth',
+ align: 'center',
+ stopInView: true,
+ });
+ if (onScroll) onScroll(scrolled);
+ setFocusItem({
+ index: absoluteIndex,
+ scrollTo: false,
+ highlight,
+ });
+ } else {
+ setTimeline(getEmptyTimeline());
+ loadEventTimeline(evtId);
+ }
+ },
+ [room, timeline, scrollToItem, loadEventTimeline]
+ );
+
useLiveTimelineRefresh(
room,
useCallback(() => {
);
const tryAutoMarkAsRead = useCallback(() => {
- if (!unreadInfo) {
+ const readUptoEventId = readUptoEventIdRef.current;
+ if (!readUptoEventId) {
requestAnimationFrame(() => markAsRead(mx, room.roomId));
return;
}
- const evtTimeline = getEventTimeline(room, unreadInfo.readUptoEventId);
+ const evtTimeline = getEventTimeline(room, readUptoEventId);
const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward);
if (latestTimeline === room.getLiveTimeline()) {
requestAnimationFrame(() => markAsRead(mx, room.roomId));
}
- }, [mx, room, unreadInfo]);
+ }, [mx, room]);
const debounceSetAtBottom = useDebounce(
useCallback((entry: IntersectionObserverEntry) => {
if (targetEntry) debounceSetAtBottom(targetEntry);
if (targetEntry?.isIntersecting && atLiveEndRef.current) {
setAtBottom(true);
- tryAutoMarkAsRead();
+ if (document.hasFocus()) {
+ tryAutoMarkAsRead();
+ }
}
},
[debounceSetAtBottom, tryAutoMarkAsRead]
useCallback(
(inFocus) => {
if (inFocus && atBottomRef.current) {
+ if (unreadInfo?.inLiveTimeline) {
+ handleOpenEvent(unreadInfo.readUptoEventId, false, (scrolled) => {
+ // the unread event is already in view
+ // so, try mark as read;
+ if (!scrolled) {
+ tryAutoMarkAsRead();
+ }
+ });
+ return;
+ }
tryAutoMarkAsRead();
}
},
- [tryAutoMarkAsRead]
+ [tryAutoMarkAsRead, unreadInfo, handleOpenEvent]
)
);
async (evt) => {
const targetId = evt.currentTarget.getAttribute('data-event-id');
if (!targetId) return;
- const replyTimeline = getEventTimeline(room, targetId);
- const absoluteIndex =
- replyTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, replyTimeline, targetId);
-
- if (typeof absoluteIndex === 'number') {
- scrollToItem(absoluteIndex, {
- behavior: 'smooth',
- align: 'center',
- stopInView: true,
- });
- setFocusItem({
- index: absoluteIndex,
- scrollTo: false,
- highlight: true,
- });
- } else {
- setTimeline(getEmptyTimeline());
- loadEventTimeline(targetId);
- }
+ handleOpenEvent(targetId);
},
- [room, timeline, scrollToItem, loadEventTimeline]
+ [handleOpenEvent]
);
const handleUserClick: MouseEventHandler<HTMLButtonElement> = useCallback(
stopInView?: boolean;
};
-export type ScrollToElement = (element: HTMLElement, opts?: ScrollToOptions) => void;
-export type ScrollToItem = (index: number, opts?: ScrollToOptions) => void;
+/**
+ * Scrolls the page to a specified element in the DOM.
+ *
+ * @param {HTMLElement} element - The DOM element to scroll to.
+ * @param {ScrollToOptions} [opts] - Optional configuration for the scroll behavior (e.g., smooth scrolling, alignment).
+ * @returns {boolean} - Returns `true` if the scroll was successful, otherwise returns `false`.
+ */
+export type ScrollToElement = (element: HTMLElement, opts?: ScrollToOptions) => boolean;
+
+/**
+ * Scrolls the page to an item at the specified index within a scrollable container.
+ *
+ * @param {number} index - The index of the item to scroll to.
+ * @param {ScrollToOptions} [opts] - Optional configuration for the scroll behavior (e.g., smooth scrolling, alignment).
+ * @returns {boolean} - Returns `true` if the scroll was successful, otherwise returns `false`.
+ */
+export type ScrollToItem = (index: number, opts?: ScrollToOptions) => boolean;
type HandleObserveAnchor = (element: HTMLElement | null) => void;
const scrollToElement = useCallback<ScrollToElement>(
(element, opts) => {
const scrollElement = getScrollElement();
- if (!scrollElement) return;
+ if (!scrollElement) return false;
if (opts?.stopInView && isInScrollView(scrollElement, element)) {
- return;
+ return false;
}
let scrollTo = element.offsetTop;
if (opts?.align === 'center' && canFitInScrollView(scrollElement, element)) {
top: scrollTo - (opts?.offset ?? 0),
behavior: opts?.behavior,
});
+ return true;
},
[getScrollElement]
);
(index, opts) => {
const { range: currentRange, limit: currentLimit, count: currentCount } = propRef.current;
- if (index < 0 || index >= currentCount) return;
+ if (index < 0 || index >= currentCount) return false;
// index is not in range change range
// and trigger scrollToItem in layoutEffect hook
if (index < currentRange.start || index >= currentRange.end) {
index,
opts,
};
- return;
+ return true;
}
// find target or it's previous rendered element to scroll to
top: opts?.offset ?? 0,
behavior: opts?.behavior,
});
- return;
+ return true;
}
- scrollToElement(itemElement, opts);
+ return scrollToElement(itemElement, opts);
},
[getScrollElement, scrollToElement, getItemElement, onRangeChange]
);