fix(chat): Keep message list scroll stuck at the bottom when either reply preview or edit warning are open

This commit is contained in:
João Victor 2024-10-17 22:11:47 -03:00
parent f0636ca40b
commit c2de62e065
8 changed files with 143 additions and 108 deletions

View File

@ -12,10 +12,6 @@ export const Container = styled.div`
align-items: center;
flex-wrap: nowrap;
color: ${colorGrayLight};
position: absolute;
right: 0;
left: 0;
bottom: 100%;
z-index: 10;
background-color: ${colorWhite};

View File

@ -189,12 +189,22 @@ const ChatMessageList: React.FC<ChatListProps> = ({
parentRefProxy: messageListContainerRefProxy,
} = useIntersectionObserver(messageListContainerRef, sentinelRef);
const {
startObserving,
stopObserving,
startObserving: startObservingMessageListStickyScroll,
stopObserving: stopObservingMessageListStickyScroll,
} = useStickyScroll(currentMessageListContainer, currentMessageList);
const {
startObserving: startObservingMessageListContainerStickyScroll,
stopObserving: stopObservingMessageListContainerStickyScroll,
} = useStickyScroll(currentMessageListContainer, currentMessageListContainer);
useEffect(() => {
if (isSentinelVisible) startObserving(); else stopObserving();
if (isSentinelVisible) {
startObservingMessageListStickyScroll();
startObservingMessageListContainerStickyScroll();
} else {
stopObservingMessageListStickyScroll();
stopObservingMessageListContainerStickyScroll();
}
toggleFollowingTail(isSentinelVisible);
}, [isSentinelVisible]);

View File

@ -3,7 +3,6 @@ import { defineMessages, useIntl } from 'react-intl';
import Styled, { DeleteMessage } from './styles';
import Storage from '/imports/ui/services/storage/in-memory';
import { ChatEvents } from '/imports/ui/core/enums/chat';
import ChatMessageTextContent from '../message-content/text-content/component';
const intlMessages = defineMessages({
deleteMessage: {
@ -25,6 +24,7 @@ const ChatMessageReplied: React.FC<MessageRepliedProps> = (props) => {
} = props;
const intl = useIntl();
const messageChunks = message.split('\n');
return (
<Styled.Container
@ -42,11 +42,9 @@ const ChatMessageReplied: React.FC<MessageRepliedProps> = (props) => {
>
{!deletedByUser && (
<Styled.Message>
<ChatMessageTextContent
text={message}
emphasizedMessage={emphasizedMessage}
dataTest={null}
/>
<Styled.Markdown $emphasizedMessage={emphasizedMessage}>
{messageChunks[0]}
</Styled.Markdown>
</Styled.Message>
)}
{deletedByUser && (

View File

@ -1,16 +1,17 @@
import styled from 'styled-components';
import {
colorGrayLightest, colorPrimary, colorText,
colorWhite,
colorGrayLight,
colorGrayLightest, colorPrimary, colorText, colorWhite,
} from '/imports/ui/stylesheets/styled-components/palette';
import { $3xlPadding, lgPadding } from '/imports/ui/stylesheets/styled-components/general';
import { $3xlPadding, smPadding } from '/imports/ui/stylesheets/styled-components/general';
import ReactMarkdown from 'react-markdown';
const Container = styled.div`
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
background-color: ${colorWhite};
box-shadow: inset 0 0 0 1px ${colorGrayLightest};
padding: ${lgPadding} ${$3xlPadding};
padding: ${smPadding} ${$3xlPadding};
position: relative;
overflow: hidden;
cursor: pointer;
@ -24,34 +25,49 @@ const Container = styled.div`
}
`;
const Typography = styled.div`
overflow: hidden;
`;
const Username = styled(Typography)`
font-weight: bold;
color: ${colorPrimary};
line-height: 1rem;
font-size: 1rem;
white-space: nowrap;
text-overflow: ellipsis;
`;
const Message = styled(Typography)`
max-height: 1rem;
line-height: 1rem;
const Message = styled.div`
line-height: normal;
overflow: hidden;
`;
export const DeleteMessage = styled.span`
font-style: italic;
font-weight: bold;
color: ${colorGrayLight};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
export const Markdown = styled(ReactMarkdown)<{
$emphasizedMessage: boolean;
}>`
color: ${colorText};
${({ $emphasizedMessage }) => $emphasizedMessage && `
font-weight: bold;
`}
& img {
max-width: 100%;
max-height: 100%;
}
& p {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
& code {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`;
export default {
Container,
Username,
Message,
DeleteMessage,
Markdown,
};

View File

@ -3,7 +3,6 @@ import Styled from './styles';
import useSettings from '/imports/ui/services/settings/hooks/useSettings';
import { SETTINGS } from '/imports/ui/services/settings/enums';
import { ChatEvents } from '/imports/ui/core/enums/chat';
import ChatMessageTextContent from '../chat-message-list/page/chat-message/message-content/text-content/component';
import Storage from '/imports/ui/services/storage/in-memory';
const ChatReplyIntention = () => {
@ -45,44 +44,43 @@ const ChatReplyIntention = () => {
};
const hidden = !username || !message;
const messageChunks = message ? message.split('\n') : null;
return (
<Styled.Root>
<Styled.Container
$hidden={hidden}
$animations={animations}
onClick={() => {
<Styled.Container
$hidden={hidden}
$animations={animations}
onClick={() => {
window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, {
detail: {
sequence,
},
}),
);
Storage.removeItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST);
if (sequence) Storage.setItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, sequence);
}}
>
<Styled.Message>
<Styled.Markdown
$emphasizedMessage={!!emphasizedMessage}
>
{messageChunks ? messageChunks[0] : ''}
</Styled.Markdown>
</Styled.Message>
<Styled.CloseBtn
onClick={(e) => {
e.stopPropagation();
window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, {
detail: {
sequence,
},
}),
new CustomEvent(ChatEvents.CHAT_CANCEL_REPLY_INTENTION),
);
Storage.removeItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST);
if (sequence) Storage.setItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, sequence);
}}
>
<Styled.Message>
<ChatMessageTextContent
text={message || ''}
emphasizedMessage={!!emphasizedMessage}
dataTest={null}
/>
</Styled.Message>
<Styled.CloseBtn
onClick={(e) => {
e.stopPropagation();
window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_CANCEL_REPLY_INTENTION),
);
}}
icon="close"
tabIndex={hidden ? -1 : 0}
aria-hidden={hidden}
/>
</Styled.Container>
</Styled.Root>
icon="close"
tabIndex={hidden ? -1 : 0}
aria-hidden={hidden}
/>
</Styled.Container>
);
};

View File

@ -1,7 +1,9 @@
import styled, { css } from 'styled-components';
import ReactMarkdown from 'react-markdown';
import {
colorGrayLightest,
colorPrimary,
colorText,
colorWhite,
} from '/imports/ui/stylesheets/styled-components/palette';
import {
@ -12,14 +14,10 @@ import EmojiButton from '../chat-message-list/page/chat-message/message-toolbar/
const Container = styled.div<{ $hidden: boolean; $animations: boolean }>`
border-radius: 0.375rem;
background-color: ${colorWhite};
position: absolute;
right: 0;
left: 0;
bottom: 100%;
z-index: 10;
overflow: hidden;
box-shadow: inset 0 0 0 1px ${colorGrayLightest};
display: flex;
align-items: center;
overflow: hidden;
[dir='ltr'] & {
border-right: 0.375rem solid ${colorPrimary};
@ -35,8 +33,8 @@ const Container = styled.div<{ $hidden: boolean; $animations: boolean }>`
min-height: 0;
`
: css`
min-height: calc(1rem + ${mdPadding} * 2);
height: calc(1rem + ${mdPadding} * 2);
min-height: calc(1rlh + ${mdPadding} * 2);
height: calc(1rlh + ${mdPadding} * 2);
padding: ${mdPadding} calc(${smPaddingX} * 1.25);
margin-bottom: ${smPadding};
@ -57,20 +55,39 @@ const Container = styled.div<{ $hidden: boolean; $animations: boolean }>`
`}
`;
const Typography = styled.div`
line-height: 1;
font-size: 1rem;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
const Message = styled.div`
line-height: 1rlh;
flex-grow: 1;
`;
const Message = styled(Typography)`
font-size: 1rem;
line-height: 1;
white-space: nowrap;
overflow: hidden;
flex-grow: 1;
const Markdown = styled(ReactMarkdown)<{
$emphasizedMessage: boolean;
}>`
color: ${colorText};
${({ $emphasizedMessage }) => $emphasizedMessage && `
font-weight: bold;
`}
& img {
max-width: 100%;
max-height: 100%;
}
& p {
line-height: 1rlh;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
& code {
line-height: 1rlh;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`;
const CloseBtn = styled(EmojiButton)`
@ -79,13 +96,9 @@ const CloseBtn = styled(EmojiButton)`
padding: 0;
`;
const Root = styled.div`
position: relative;
`;
export default {
Container,
CloseBtn,
Message,
Root,
Markdown,
};

View File

@ -1,5 +1,5 @@
import {
useCallback, useEffect, useMemo, useRef,
useCallback, useEffect, useRef,
} from 'react';
interface Handlers {
@ -10,17 +10,22 @@ interface Handlers {
const useStickyScroll = (stickyElement: HTMLElement | null, onResizeOf: HTMLElement | null) => {
const elHeight = useRef(0);
const timeout = useRef<ReturnType<typeof setTimeout>>();
const observer = useRef<ResizeObserver | null>(null);
const handlers = useRef<Handlers>({
startObserving: () => {},
stopObserving: () => {},
});
const observer = useMemo(
() => new ResizeObserver((entries) => {
useEffect(() => {
if (observer.current) {
observer.current.disconnect();
}
observer.current = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { target } = entry;
if (target instanceof HTMLElement) {
if (target.offsetHeight > elHeight.current) {
if (target.offsetHeight !== elHeight.current) {
elHeight.current = target.offsetHeight;
if (stickyElement) {
// eslint-disable-next-line no-param-reassign
@ -31,26 +36,25 @@ const useStickyScroll = (stickyElement: HTMLElement | null, onResizeOf: HTMLElem
}
}
});
}),
[],
);
});
}, [stickyElement]);
handlers.current.startObserving = useCallback(() => {
if (!onResizeOf) return;
clearTimeout(timeout.current);
observer.observe(onResizeOf);
}, [onResizeOf]);
observer.current?.observe(onResizeOf);
}, [onResizeOf, observer.current]);
handlers.current.stopObserving = useCallback(() => {
if (!onResizeOf) return;
timeout.current = setTimeout(() => {
observer.unobserve(onResizeOf);
observer.current?.unobserve(onResizeOf);
}, 500);
}, [onResizeOf]);
}, [onResizeOf, observer.current]);
useEffect(
() => () => {
observer.disconnect();
observer.current?.disconnect();
},
[],
);

View File

@ -770,7 +770,7 @@ public:
allowedElements: ['a', 'code', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'ol', 'ul', 'p', 'strong']
# available options for toolbar: reply, edit, delete, reactions
# example: ['reply', 'delete']
toolbar: []
toolbar: ['reply', 'edit', 'delete', 'reactions']
userReaction:
enabled: true
expire: 30