mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-02-26 15:13:05 +01:00
scroll to bottom in unfocused window but stop sending read receipt (#2214)
* scroll to bottom in unfocused window but stop sending read receipt * send read-receipt when new message are in view after regaining focus
This commit is contained in:
parent
59e8d66255
commit
b63868bbb5
2 changed files with 82 additions and 37 deletions
|
@ -586,15 +586,19 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
// so timeline can be updated with evt like: edits, reactions etc
|
// so timeline can be updated with evt like: edits, reactions etc
|
||||||
if (atBottomRef.current) {
|
if (atBottomRef.current) {
|
||||||
if (document.hasFocus() && (!unreadInfo || mEvt.getSender() === mx.getUserId())) {
|
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()!));
|
requestAnimationFrame(() => markAsRead(mx, mEvt.getRoomId()!));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.hasFocus()) {
|
if (!document.hasFocus() && !unreadInfo) {
|
||||||
scrollToBottomRef.current.count += 1;
|
|
||||||
scrollToBottomRef.current.smooth = true;
|
|
||||||
} else if (!unreadInfo) {
|
|
||||||
setUnreadInfo(getRoomUnreadInfo(room));
|
setUnreadInfo(getRoomUnreadInfo(room));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scrollToBottomRef.current.count += 1;
|
||||||
|
scrollToBottomRef.current.smooth = true;
|
||||||
|
|
||||||
setTimeline((ct) => ({
|
setTimeline((ct) => ({
|
||||||
...ct,
|
...ct,
|
||||||
range: {
|
range: {
|
||||||
|
@ -613,6 +617,36 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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(
|
useLiveTimelineRefresh(
|
||||||
room,
|
room,
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
|
@ -646,16 +680,17 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
);
|
);
|
||||||
|
|
||||||
const tryAutoMarkAsRead = useCallback(() => {
|
const tryAutoMarkAsRead = useCallback(() => {
|
||||||
if (!unreadInfo) {
|
const readUptoEventId = readUptoEventIdRef.current;
|
||||||
|
if (!readUptoEventId) {
|
||||||
requestAnimationFrame(() => markAsRead(mx, room.roomId));
|
requestAnimationFrame(() => markAsRead(mx, room.roomId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const evtTimeline = getEventTimeline(room, unreadInfo.readUptoEventId);
|
const evtTimeline = getEventTimeline(room, readUptoEventId);
|
||||||
const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward);
|
const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward);
|
||||||
if (latestTimeline === room.getLiveTimeline()) {
|
if (latestTimeline === room.getLiveTimeline()) {
|
||||||
requestAnimationFrame(() => markAsRead(mx, room.roomId));
|
requestAnimationFrame(() => markAsRead(mx, room.roomId));
|
||||||
}
|
}
|
||||||
}, [mx, room, unreadInfo]);
|
}, [mx, room]);
|
||||||
|
|
||||||
const debounceSetAtBottom = useDebounce(
|
const debounceSetAtBottom = useDebounce(
|
||||||
useCallback((entry: IntersectionObserverEntry) => {
|
useCallback((entry: IntersectionObserverEntry) => {
|
||||||
|
@ -672,8 +707,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
if (targetEntry) debounceSetAtBottom(targetEntry);
|
if (targetEntry) debounceSetAtBottom(targetEntry);
|
||||||
if (targetEntry?.isIntersecting && atLiveEndRef.current) {
|
if (targetEntry?.isIntersecting && atLiveEndRef.current) {
|
||||||
setAtBottom(true);
|
setAtBottom(true);
|
||||||
|
if (document.hasFocus()) {
|
||||||
tryAutoMarkAsRead();
|
tryAutoMarkAsRead();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[debounceSetAtBottom, tryAutoMarkAsRead]
|
[debounceSetAtBottom, tryAutoMarkAsRead]
|
||||||
),
|
),
|
||||||
|
@ -691,10 +728,20 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
useCallback(
|
useCallback(
|
||||||
(inFocus) => {
|
(inFocus) => {
|
||||||
if (inFocus && atBottomRef.current) {
|
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]
|
[tryAutoMarkAsRead, unreadInfo, handleOpenEvent]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -832,27 +879,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
async (evt) => {
|
async (evt) => {
|
||||||
const targetId = evt.currentTarget.getAttribute('data-event-id');
|
const targetId = evt.currentTarget.getAttribute('data-event-id');
|
||||||
if (!targetId) return;
|
if (!targetId) return;
|
||||||
const replyTimeline = getEventTimeline(room, targetId);
|
handleOpenEvent(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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[room, timeline, scrollToItem, loadEventTimeline]
|
[handleOpenEvent]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUserClick: MouseEventHandler<HTMLButtonElement> = useCallback(
|
const handleUserClick: MouseEventHandler<HTMLButtonElement> = useCallback(
|
||||||
|
|
|
@ -26,8 +26,23 @@ export type ScrollToOptions = {
|
||||||
stopInView?: boolean;
|
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;
|
type HandleObserveAnchor = (element: HTMLElement | null) => void;
|
||||||
|
|
||||||
|
@ -186,10 +201,10 @@ export const useVirtualPaginator = <TScrollElement extends HTMLElement>(
|
||||||
const scrollToElement = useCallback<ScrollToElement>(
|
const scrollToElement = useCallback<ScrollToElement>(
|
||||||
(element, opts) => {
|
(element, opts) => {
|
||||||
const scrollElement = getScrollElement();
|
const scrollElement = getScrollElement();
|
||||||
if (!scrollElement) return;
|
if (!scrollElement) return false;
|
||||||
|
|
||||||
if (opts?.stopInView && isInScrollView(scrollElement, element)) {
|
if (opts?.stopInView && isInScrollView(scrollElement, element)) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
let scrollTo = element.offsetTop;
|
let scrollTo = element.offsetTop;
|
||||||
if (opts?.align === 'center' && canFitInScrollView(scrollElement, element)) {
|
if (opts?.align === 'center' && canFitInScrollView(scrollElement, element)) {
|
||||||
|
@ -207,6 +222,7 @@ export const useVirtualPaginator = <TScrollElement extends HTMLElement>(
|
||||||
top: scrollTo - (opts?.offset ?? 0),
|
top: scrollTo - (opts?.offset ?? 0),
|
||||||
behavior: opts?.behavior,
|
behavior: opts?.behavior,
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
[getScrollElement]
|
[getScrollElement]
|
||||||
);
|
);
|
||||||
|
@ -215,7 +231,7 @@ export const useVirtualPaginator = <TScrollElement extends HTMLElement>(
|
||||||
(index, opts) => {
|
(index, opts) => {
|
||||||
const { range: currentRange, limit: currentLimit, count: currentCount } = propRef.current;
|
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
|
// index is not in range change range
|
||||||
// and trigger scrollToItem in layoutEffect hook
|
// and trigger scrollToItem in layoutEffect hook
|
||||||
if (index < currentRange.start || index >= currentRange.end) {
|
if (index < currentRange.start || index >= currentRange.end) {
|
||||||
|
@ -227,7 +243,7 @@ export const useVirtualPaginator = <TScrollElement extends HTMLElement>(
|
||||||
index,
|
index,
|
||||||
opts,
|
opts,
|
||||||
};
|
};
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// find target or it's previous rendered element to scroll to
|
// find target or it's previous rendered element to scroll to
|
||||||
|
@ -241,9 +257,9 @@ export const useVirtualPaginator = <TScrollElement extends HTMLElement>(
|
||||||
top: opts?.offset ?? 0,
|
top: opts?.offset ?? 0,
|
||||||
behavior: opts?.behavior,
|
behavior: opts?.behavior,
|
||||||
});
|
});
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
scrollToElement(itemElement, opts);
|
return scrollToElement(itemElement, opts);
|
||||||
},
|
},
|
||||||
[getScrollElement, scrollToElement, getItemElement, onRangeChange]
|
[getScrollElement, scrollToElement, getItemElement, onRangeChange]
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue