Message List: Update behaviour (#6969)
This commit is contained in:
parent
1162804e9d
commit
8edd3fe4ba
@ -88,7 +88,7 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
|
|||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
'--composer-text-size', `${Math.max(newSize, IS_IOS ? 16 : 15)}px`,
|
'--composer-text-size', `${Math.max(newSize, IS_IOS ? 16 : 15)}px`,
|
||||||
);
|
);
|
||||||
document.documentElement.style.setProperty('--message-meta-height', `${Math.floor(newSize * 1.3125)}px`);
|
document.documentElement.style.setProperty('--message-meta-height', `${Math.floor(newSize * 1.25)}px`);
|
||||||
document.documentElement.style.setProperty('--message-text-size', `${newSize}px`);
|
document.documentElement.style.setProperty('--message-text-size', `${newSize}px`);
|
||||||
document.documentElement.setAttribute('data-message-text-size', newSize.toString());
|
document.documentElement.setAttribute('data-message-text-size', newSize.toString());
|
||||||
|
|
||||||
|
|||||||
@ -61,7 +61,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.live-tail {
|
.live-tail {
|
||||||
min-height: max(0rem, calc(100cqh - var(--middle-header-panes-height) - 3rem));
|
overflow-anchor: none;
|
||||||
|
min-height: max(0rem, calc(100cqh - var(--middle-header-panes-height) - 2.5rem));
|
||||||
|
|
||||||
.message-list-item {
|
.message-list-item {
|
||||||
animation: live-tail-message-mount 0.2s ease-out;
|
animation: live-tail-message-mount 0.2s ease-out;
|
||||||
|
|||||||
@ -288,6 +288,7 @@ const MessageList = ({
|
|||||||
const isLiveTailAutoScrollingRef = useRef(false);
|
const isLiveTailAutoScrollingRef = useRef(false);
|
||||||
const liveTailReleaseTimerRef = useRef<number>();
|
const liveTailReleaseTimerRef = useRef<number>();
|
||||||
const liveTailStartOriginalIdRef = useRef<number>();
|
const liveTailStartOriginalIdRef = useRef<number>();
|
||||||
|
const scrollTopBeforeUpdateRef = useRef<number>();
|
||||||
const [releasedLiveTailStartOriginalId, setReleasedLiveTailStartOriginalId] = useState<number>();
|
const [releasedLiveTailStartOriginalId, setReleasedLiveTailStartOriginalId] = useState<number>();
|
||||||
|
|
||||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
|
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
|
||||||
@ -307,25 +308,58 @@ const MessageList = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const previousLiveTailStartOriginalId = liveTailStartOriginalIdRef.current;
|
const previousLiveTailStartOriginalId = liveTailStartOriginalIdRef.current;
|
||||||
|
const hasActiveLiveTail = previousLiveTailStartOriginalId !== undefined
|
||||||
|
&& previousLiveTailStartOriginalId !== releasedLiveTailStartOriginalId;
|
||||||
let renderedLiveTailStartOriginalId: number | undefined;
|
let renderedLiveTailStartOriginalId: number | undefined;
|
||||||
|
|
||||||
for (let i = messageIds.length - 1; i >= 0; i--) {
|
for (let i = messageIds.length - 1; i >= 0; i--) {
|
||||||
const message = messagesById[messageIds[i]];
|
const message = messagesById[messageIds[i]];
|
||||||
if (message?.isTypingDraft && !message.isOutgoing) {
|
if (!message) {
|
||||||
return getMessageOriginalId(message);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalId = getMessageOriginalId(message);
|
||||||
|
if (
|
||||||
|
hasActiveLiveTail
|
||||||
|
&& message.isOutgoing
|
||||||
|
&& originalId >= previousLiveTailStartOriginalId
|
||||||
|
) {
|
||||||
|
return originalId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.isTypingDraft && !message.isOutgoing) {
|
||||||
|
if (
|
||||||
|
hasActiveLiveTail
|
||||||
|
&& originalId === previousLiveTailStartOriginalId
|
||||||
|
) {
|
||||||
|
renderedLiveTailStartOriginalId = previousLiveTailStartOriginalId;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasActiveLiveTail) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new live tail from our message to keep consistency with in-tail focusing
|
||||||
|
const previousMessage = i > 0 ? messagesById[messageIds[i - 1]] : undefined;
|
||||||
|
if (previousMessage?.isOutgoing) {
|
||||||
|
return getMessageOriginalId(previousMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
previousLiveTailStartOriginalId !== undefined
|
previousLiveTailStartOriginalId !== undefined
|
||||||
&& message?.wasTypingDraft
|
&& message.wasTypingDraft
|
||||||
&& getMessageOriginalId(message) === previousLiveTailStartOriginalId
|
&& originalId === previousLiveTailStartOriginalId
|
||||||
) {
|
) {
|
||||||
renderedLiveTailStartOriginalId = previousLiveTailStartOriginalId;
|
renderedLiveTailStartOriginalId = previousLiveTailStartOriginalId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderedLiveTailStartOriginalId;
|
return renderedLiveTailStartOriginalId;
|
||||||
}, [messageIds, messagesById]);
|
}, [messageIds, messagesById, releasedLiveTailStartOriginalId]);
|
||||||
|
|
||||||
liveTailStartOriginalIdRef.current = liveTailStartOriginalId;
|
liveTailStartOriginalIdRef.current = liveTailStartOriginalId;
|
||||||
|
|
||||||
@ -463,6 +497,9 @@ const MessageList = ({
|
|||||||
const isCurrentLastMessageTypingDraft = Boolean(
|
const isCurrentLastMessageTypingDraft = Boolean(
|
||||||
currentLastMessage?.isTypingDraft || currentLastMessage?.wasTypingDraft,
|
currentLastMessage?.isTypingDraft || currentLastMessage?.wasTypingDraft,
|
||||||
);
|
);
|
||||||
|
const isCurrentLastMessageIncomingTypingDraft = Boolean(
|
||||||
|
currentLastMessage?.isTypingDraft && !currentLastMessage.isOutgoing,
|
||||||
|
);
|
||||||
|
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
if (!messageIds || !messagesById || type === 'scheduled' || isAccountFrozen || !isActive) return;
|
if (!messageIds || !messagesById || type === 'scheduled' || isAccountFrozen || !isActive) return;
|
||||||
@ -698,7 +735,10 @@ const MessageList = ({
|
|||||||
useSyncEffect(
|
useSyncEffect(
|
||||||
() => {
|
() => {
|
||||||
isReplacingHistoryRef.current = true;
|
isReplacingHistoryRef.current = true;
|
||||||
forceMeasure(() => rememberScrollPositionRef.current());
|
forceMeasure(() => {
|
||||||
|
scrollTopBeforeUpdateRef.current = containerRef.current?.scrollTop;
|
||||||
|
rememberScrollPositionRef.current();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
// This will run before modifying content and should match deps for `useLayoutEffectWithPrevDeps` below
|
// This will run before modifying content and should match deps for `useLayoutEffectWithPrevDeps` below
|
||||||
[messageIds, isViewportNewest, effectiveLiveTailStartOriginalId, rememberScrollPositionRef],
|
[messageIds, isViewportNewest, effectiveLiveTailStartOriginalId, rememberScrollPositionRef],
|
||||||
@ -709,7 +749,12 @@ const MessageList = ({
|
|||||||
[getContainerHeight, rememberScrollPositionRef],
|
[getContainerHeight, rememberScrollPositionRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handles updated message list, takes care of scroll repositioning
|
/* Handles updated message list, takes care of scroll repositioning
|
||||||
|
Live tail mode:
|
||||||
|
- When a new typing draft is received, the live tail is revealed
|
||||||
|
- New messages attach to it. New outgoing message kick older out
|
||||||
|
- If outgoing message is tall, we should show at least one line of typing draft that replies to it
|
||||||
|
*/
|
||||||
useLayoutEffectWithPrevDeps(([
|
useLayoutEffectWithPrevDeps(([
|
||||||
prevMessageIds, prevIsViewportNewest, prevCurrentLastMessageOriginalId, prevLiveTailStartOriginalId,
|
prevMessageIds, prevIsViewportNewest, prevCurrentLastMessageOriginalId, prevLiveTailStartOriginalId,
|
||||||
]) => {
|
]) => {
|
||||||
@ -749,7 +794,14 @@ const MessageList = ({
|
|||||||
&& effectiveLiveTailStartOriginalId !== prevLiveTailStartOriginalId,
|
&& effectiveLiveTailStartOriginalId !== prevLiveTailStartOriginalId,
|
||||||
);
|
);
|
||||||
const hasLiveTail = effectiveLiveTailStartOriginalId !== undefined;
|
const hasLiveTail = effectiveLiveTailStartOriginalId !== undefined;
|
||||||
if (wasLiveTailCreated) {
|
const shouldRevealLiveTailTypingDraft = Boolean(
|
||||||
|
wasMessageAdded
|
||||||
|
&& hasLiveTail
|
||||||
|
&& !wasLiveTailCreated
|
||||||
|
&& isCurrentLastMessageIncomingTypingDraft,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (wasLiveTailCreated || shouldRevealLiveTailTypingDraft) {
|
||||||
isLiveTailBottomSnapSuppressedRef.current = true;
|
isLiveTailBottomSnapSuppressedRef.current = true;
|
||||||
} else if (!hasLiveTail) {
|
} else if (!hasLiveTail) {
|
||||||
isLiveTailBottomSnapSuppressedRef.current = false;
|
isLiveTailBottomSnapSuppressedRef.current = false;
|
||||||
@ -804,15 +856,26 @@ const MessageList = ({
|
|||||||
const scrollOffset = scrollOffsetRef.current;
|
const scrollOffset = scrollOffsetRef.current;
|
||||||
|
|
||||||
let bottomOffset = scrollOffset - (prevContainerHeight || offsetHeight);
|
let bottomOffset = scrollOffset - (prevContainerHeight || offsetHeight);
|
||||||
|
const lastItemHeight = wasMessageAdded && lastItemElement ? lastItemElement.offsetHeight : 0;
|
||||||
if (wasMessageAdded) {
|
if (wasMessageAdded) {
|
||||||
// If two new messages come at once (e.g. when bot responds) then the first message will update `scrollOffset`
|
// If two new messages come at once (e.g. when bot responds) then the first message will update `scrollOffset`
|
||||||
// right away (before animation) which creates inconsistency until the animation completes. To work around that,
|
// right away (before animation) which creates inconsistency until the animation completes. To work around that,
|
||||||
// we calculate `isAtBottom` with a "buffer" of the latest message height (this is approximate).
|
// we calculate `isAtBottom` with a "buffer" of the latest message height (this is approximate).
|
||||||
const lastItemHeight = lastItemElement ? lastItemElement.offsetHeight : 0;
|
|
||||||
bottomOffset -= lastItemHeight;
|
bottomOffset -= lastItemHeight;
|
||||||
}
|
}
|
||||||
const isAtBottom = isViewportNewest && prevIsViewportNewest && bottomOffset <= BOTTOM_THRESHOLD;
|
const isAtBottom = isViewportNewest && prevIsViewportNewest && bottomOffset <= BOTTOM_THRESHOLD;
|
||||||
|
const wasAtBottomBeforeTypingDraft = Boolean(
|
||||||
|
shouldRevealLiveTailTypingDraft
|
||||||
|
&& isViewportNewest
|
||||||
|
&& prevIsViewportNewest
|
||||||
|
&& scrollHeight - lastItemHeight - scrollTop - offsetHeight <= BOTTOM_THRESHOLD,
|
||||||
|
);
|
||||||
const shouldFocusLiveTail = wasLiveTailCreated && isAtBottom;
|
const shouldFocusLiveTail = wasLiveTailCreated && isAtBottom;
|
||||||
|
const shouldRevealTypingDraft = Boolean(
|
||||||
|
shouldRevealLiveTailTypingDraft
|
||||||
|
&& (isAtBottom || wasAtBottomBeforeTypingDraft),
|
||||||
|
);
|
||||||
|
|
||||||
const isAlreadyFocusing = messageIds && memoFocusingIdRef.current === messageIds[messageIds.length - 1];
|
const isAlreadyFocusing = messageIds && memoFocusingIdRef.current === messageIds[messageIds.length - 1];
|
||||||
|
|
||||||
// Animate incoming message, but if app is in background mode, scroll to the first unread
|
// Animate incoming message, but if app is in background mode, scroll to the first unread
|
||||||
@ -865,6 +928,53 @@ const MessageList = ({
|
|||||||
shouldReturnMutationFn: true,
|
shouldReturnMutationFn: true,
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const typingDraftTop = shouldRevealTypingDraft && lastItemElement
|
||||||
|
? getOffsetToContainer(lastItemElement, container).top
|
||||||
|
: undefined;
|
||||||
|
const typingDraftBottom = typingDraftTop !== undefined && lastItemElement
|
||||||
|
? typingDraftTop + lastItemElement.offsetHeight
|
||||||
|
: undefined;
|
||||||
|
const scrollTopBeforeUpdate = scrollTopBeforeUpdateRef.current;
|
||||||
|
const viewportBottomBeforeUpdate = (scrollTopBeforeUpdate ?? scrollTop) + offsetHeight;
|
||||||
|
const typingDraftElement = typingDraftBottom !== undefined && typingDraftBottom > viewportBottomBeforeUpdate
|
||||||
|
? lastItemElement
|
||||||
|
: undefined;
|
||||||
|
const typingDraftScrollTop = typingDraftElement && typingDraftTop !== undefined
|
||||||
|
? typingDraftTop + typingDraftElement.offsetHeight - offsetHeight
|
||||||
|
: undefined;
|
||||||
|
const shouldRestoreBeforeTypingDraftAnimation = Boolean(
|
||||||
|
typingDraftElement
|
||||||
|
&& scrollTopBeforeUpdate !== undefined
|
||||||
|
&& scrollTopBeforeUpdate < scrollTop
|
||||||
|
&& typingDraftScrollTop !== undefined
|
||||||
|
&& scrollTopBeforeUpdate < typingDraftScrollTop,
|
||||||
|
);
|
||||||
|
|
||||||
|
let animateTypingDraftScroll: NoneToVoidFunction | undefined;
|
||||||
|
if (typingDraftElement) {
|
||||||
|
animateTypingDraftScroll = shouldRestoreBeforeTypingDraftAnimation ? () => {
|
||||||
|
resetScroll(container, scrollTopBeforeUpdate);
|
||||||
|
requestMeasure(() => {
|
||||||
|
const mutate = animateScroll({
|
||||||
|
container,
|
||||||
|
element: typingDraftElement,
|
||||||
|
position: 'end',
|
||||||
|
maxDistance: Number.MAX_SAFE_INTEGER,
|
||||||
|
forceDuration: noMessageSendingAnimation ? 0 : undefined,
|
||||||
|
shouldReturnMutationFn: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
requestMutation(mutate!);
|
||||||
|
});
|
||||||
|
} : animateScroll({
|
||||||
|
container,
|
||||||
|
element: typingDraftElement,
|
||||||
|
position: 'end',
|
||||||
|
maxDistance: Number.MAX_SAFE_INTEGER,
|
||||||
|
forceDuration: noMessageSendingAnimation ? 0 : undefined,
|
||||||
|
shouldReturnMutationFn: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let newScrollTop!: number;
|
let newScrollTop!: number;
|
||||||
if (liveTailElement) {
|
if (liveTailElement) {
|
||||||
@ -872,6 +982,10 @@ const MessageList = ({
|
|||||||
newScrollTop = liveTailOffset + liveTailElement.offsetHeight - offsetHeight;
|
newScrollTop = liveTailOffset + liveTailElement.offsetHeight - offsetHeight;
|
||||||
} else if (shouldFocusLiveTail) {
|
} else if (shouldFocusLiveTail) {
|
||||||
newScrollTop = scrollHeight - offsetHeight;
|
newScrollTop = scrollHeight - offsetHeight;
|
||||||
|
} else if (typingDraftScrollTop !== undefined) {
|
||||||
|
newScrollTop = typingDraftScrollTop;
|
||||||
|
} else if (shouldRevealTypingDraft) {
|
||||||
|
newScrollTop = scrollTop;
|
||||||
} else if (isAtBottom && isResized) {
|
} else if (isAtBottom && isResized) {
|
||||||
newScrollTop = scrollHeight - offsetHeight;
|
newScrollTop = scrollHeight - offsetHeight;
|
||||||
} else if (anchor) {
|
} else if (anchor) {
|
||||||
@ -887,12 +1001,17 @@ const MessageList = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (animateLiveTailScroll) {
|
const animateScrollMutation = animateLiveTailScroll || animateTypingDraftScroll;
|
||||||
if (Math.abs(newScrollTop - scrollTop) >= 1) {
|
if (animateScrollMutation) {
|
||||||
|
const animationStartScrollTop = shouldRestoreBeforeTypingDraftAnimation && scrollTopBeforeUpdate !== undefined
|
||||||
|
? scrollTopBeforeUpdate
|
||||||
|
: scrollTop;
|
||||||
|
|
||||||
|
if (Math.abs(newScrollTop - animationStartScrollTop) >= 1) {
|
||||||
isLiveTailAutoScrollingRef.current = true;
|
isLiveTailAutoScrollingRef.current = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
animateLiveTailScroll();
|
animateScrollMutation();
|
||||||
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
|
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
|
||||||
requestMeasure(() => {
|
requestMeasure(() => {
|
||||||
isReplacingHistoryRef.current = false;
|
isReplacingHistoryRef.current = false;
|
||||||
@ -901,6 +1020,7 @@ const MessageList = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetScroll(container, Math.ceil(newScrollTop));
|
resetScroll(container, Math.ceil(newScrollTop));
|
||||||
|
|
||||||
requestMeasure(() => {
|
requestMeasure(() => {
|
||||||
isReplacingHistoryRef.current = false;
|
isReplacingHistoryRef.current = false;
|
||||||
});
|
});
|
||||||
@ -931,6 +1051,7 @@ const MessageList = ({
|
|||||||
currentLastMessageOriginalId,
|
currentLastMessageOriginalId,
|
||||||
effectiveLiveTailStartOriginalId,
|
effectiveLiveTailStartOriginalId,
|
||||||
isCurrentLastMessageTypingDraft,
|
isCurrentLastMessageTypingDraft,
|
||||||
|
isCurrentLastMessageIncomingTypingDraft,
|
||||||
getContainerHeight,
|
getContainerHeight,
|
||||||
prevContainerHeightRef,
|
prevContainerHeightRef,
|
||||||
noMessageSendingAnimation,
|
noMessageSendingAnimation,
|
||||||
|
|||||||
@ -156,7 +156,7 @@ addCallback((global: GlobalState) => {
|
|||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
'--composer-text-size', `${Math.max(messageTextSize, IS_IOS ? 16 : 15)}px`,
|
'--composer-text-size', `${Math.max(messageTextSize, IS_IOS ? 16 : 15)}px`,
|
||||||
);
|
);
|
||||||
document.documentElement.style.setProperty('--message-meta-height', `${Math.floor(messageTextSize * 1.3125)}px`);
|
document.documentElement.style.setProperty('--message-meta-height', `${Math.floor(messageTextSize * 1.25)}px`);
|
||||||
document.documentElement.style.setProperty('--message-text-size', `${messageTextSize}px`);
|
document.documentElement.style.setProperty('--message-text-size', `${messageTextSize}px`);
|
||||||
document.documentElement.setAttribute('data-message-text-size', messageTextSize.toString());
|
document.documentElement.setAttribute('data-message-text-size', messageTextSize.toString());
|
||||||
document.body.classList.add('initial');
|
document.body.classList.add('initial');
|
||||||
|
|||||||
@ -69,7 +69,7 @@ addCallback((global: GlobalState) => {
|
|||||||
'--composer-text-size', `${Math.max(sharedSettings.messageTextSize, IS_IOS ? 16 : 15)}px`,
|
'--composer-text-size', `${Math.max(sharedSettings.messageTextSize, IS_IOS ? 16 : 15)}px`,
|
||||||
);
|
);
|
||||||
document.documentElement.style.setProperty('--message-meta-height',
|
document.documentElement.style.setProperty('--message-meta-height',
|
||||||
`${Math.floor(sharedSettings.messageTextSize * 1.3125)}px`);
|
`${Math.floor(sharedSettings.messageTextSize * 1.25)}px`);
|
||||||
document.documentElement.style.setProperty('--message-text-size', `${sharedSettings.messageTextSize}px`);
|
document.documentElement.style.setProperty('--message-text-size', `${sharedSettings.messageTextSize}px`);
|
||||||
document.documentElement.setAttribute('data-message-text-size', sharedSettings.messageTextSize.toString());
|
document.documentElement.setAttribute('data-message-text-size', sharedSettings.messageTextSize.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user