diff --git a/web/chat/chat-message-list.react.js b/web/chat/chat-message-list.react.js --- a/web/chat/chat-message-list.react.js +++ b/web/chat/chat-message-list.react.js @@ -29,7 +29,12 @@ } from 'lib/utils/action-utils.js'; import css from './chat-message-list.css'; +import type { + EditState, + ScrollToMessageCallback, +} from './edit-message-provider.js'; import { useEditModalContext } from './edit-message-provider.js'; +import { maximumEditTextHeight } from './edit-text-message.react.js'; import { MessageListContext } from './message-list-types.js'; import Message from './message.react.js'; import RelationshipPrompt from './relationship-prompt/relationship-prompt.js'; @@ -64,18 +69,40 @@ +clearTooltip: () => mixed, +oldestMessageServerID: ?string, +isEditState: boolean, + +renderEditModal: (params: EditState) => mixed, + +addScrollToMessageListener: ScrollToMessageCallback => mixed, + +removeScrollToMessageListener: ScrollToMessageCallback => mixed, }; type Snapshot = { +scrollTop: number, +scrollHeight: number, }; -class ChatMessageList extends React.PureComponent { + +type State = { + +scrollTimeoutID: ?TimeoutID, + +scrollingEndCallback: ?() => mixed, +}; + +class ChatMessageList extends React.PureComponent { container: ?HTMLDivElement; messageContainer: ?HTMLDivElement; loadingFromScroll = false; + constructor(props: Props) { + super(props); + this.state = { + scrollTimeoutID: null, + scrollingEndCallback: null, + }; + } + componentDidMount() { this.scrollToBottom(); + this.props.addScrollToMessageListener(this.scrollToMessage); + } + + componentWillUnmount() { + this.props.removeScrollToMessageListener(this.scrollToMessage); } getSnapshotBeforeUpdate(prevProps: Props) { @@ -169,15 +196,57 @@ const { threadInfo } = this.props; invariant(threadInfo, 'ThreadInfo should be set if messageListData is'); return ( - + > + + ); }; + scrollToMessage = (messageID: string, callback: () => mixed) => { + const element = document.getElementById(messageID); + if (!element) { + return; + } + if (this.willMessageEditWindowOverflow(messageID)) { + callback(); + return; + } + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + this.setState({ scrollingEndCallback: callback }); + }; + + willMessageEditWindowOverflow(messageID: string) { + const { messageContainer } = this; + if (!messageContainer) { + return false; + } + const messageElement = document.getElementById(messageID); + if (!messageElement) { + console.log(`couldn't find the message element`); + return false; + } + + const msgPos = messageElement.getBoundingClientRect(); + const containerPos = messageContainer.getBoundingClientRect(); + + const messageHeight = msgPos.height; + const offset = Math.max(0, maximumEditTextHeight - messageHeight + 10); + + const messageTop = msgPos.top - offset; + const messageBottom = msgPos.bottom; + const containerTop = containerPos.top; + const containerBottom = containerPos.bottom; + + return messageBottom <= containerBottom && messageTop >= containerTop; + } + render() { const { messageListData, threadInfo, inputState } = this.props; if (!messageListData) { @@ -221,6 +290,17 @@ } this.props.clearTooltip(); this.possiblyLoadMoreMessages(); + + if (this.state.scrollTimeoutID) { + clearTimeout(this.state.scrollTimeoutID); + } + const scrollTimeoutID = setTimeout(() => { + if (this.state.scrollingEndCallback) { + this.state.scrollingEndCallback(); + } + this.setState({ scrollingEndCallback: null }); + }, 100); + this.setState({ scrollTimeoutID }); }; async possiblyLoadMoreMessages() { @@ -313,7 +393,12 @@ const oldestMessageServerID = useOldestMessageServerID(threadInfo.id); - const { editState } = useEditModalContext(); + const { + editState, + renderEditModal, + addScrollToMessageListener, + removeScrollToMessageListener, + } = useEditModalContext(); const isEditState = editState !== null; return ( @@ -330,6 +415,9 @@ clearTooltip={clearTooltip} oldestMessageServerID={oldestMessageServerID} isEditState={isEditState} + renderEditModal={renderEditModal} + addScrollToMessageListener={addScrollToMessageListener} + removeScrollToMessageListener={removeScrollToMessageListener} /> ); diff --git a/web/chat/edit-message-provider.js b/web/chat/edit-message-provider.js --- a/web/chat/edit-message-provider.js +++ b/web/chat/edit-message-provider.js @@ -24,6 +24,11 @@ +position?: ModalPosition, }; +export type ScrollToMessageCallback = ( + messageKey: string, + callback: () => void, +) => void; + type EditModalContextType = { +renderEditModal: (params: EditState) => void, +clearEditModal: () => void, @@ -31,6 +36,9 @@ +setDraft: string => void, +setError: boolean => void, +updatePosition: ModalPosition => void, + +scrollToMessage: ScrollToMessageCallback, + +addScrollToMessageListener: ScrollToMessageCallback => void, + +removeScrollToMessageListener: ScrollToMessageCallback => void, }; const EditModalContext: React.Context = @@ -41,6 +49,9 @@ setDraft: () => {}, setError: () => {}, updatePosition: () => {}, + scrollToMessage: () => {}, + addScrollToMessageListener: () => {}, + removeScrollToMessageListener: () => {}, }); type Props = { @@ -51,6 +62,9 @@ const [editState, setEditState] = React.useState(null); + const [scrollToMessageCallbacks, setScrollToMessageCallbacks] = + React.useState>([]); + const clearEditModal = React.useCallback(() => { setEditState(null); }, []); @@ -118,6 +132,31 @@ [editState, setEditState], ); + const scrollToMessage: ScrollToMessageCallback = React.useCallback( + (messageKey: string, callback: () => void) => { + scrollToMessageCallbacks.forEach((callback2: ScrollToMessageCallback) => + callback2(messageKey, callback), + ); + }, + [scrollToMessageCallbacks], + ); + + const addScrollToMessageListener = React.useCallback( + (callback: ScrollToMessageCallback): void => { + setScrollToMessageCallbacks([...scrollToMessageCallbacks, callback]); + }, + [scrollToMessageCallbacks], + ); + + const removeScrollToMessageListener = React.useCallback( + (callback: ScrollToMessageCallback) => { + setScrollToMessageCallbacks( + scrollToMessageCallbacks.filter(candidate => candidate !== callback), + ); + }, + [scrollToMessageCallbacks], + ); + const value = React.useMemo( () => ({ renderEditModal, @@ -126,6 +165,9 @@ setDraft, setError, updatePosition, + scrollToMessage, + addScrollToMessageListener, + removeScrollToMessageListener, }), [ renderEditModal, @@ -134,6 +176,9 @@ setDraft, setError, updatePosition, + scrollToMessage, + addScrollToMessageListener, + removeScrollToMessageListener, ], ); diff --git a/web/chat/edit-text-message.react.js b/web/chat/edit-text-message.react.js --- a/web/chat/edit-text-message.react.js +++ b/web/chat/edit-text-message.react.js @@ -28,6 +28,8 @@ backgroundColor: 'transparent', }; +export const maximumEditTextHeight = 234; + function EditTextMessage(props: Props): React.Node { const { background, threadInfo, item } = props; const { editState, clearEditModal, setDraft, setError, updatePosition } = diff --git a/web/utils/tooltip-action-utils.js b/web/utils/tooltip-action-utils.js --- a/web/utils/tooltip-action-utils.js +++ b/web/utils/tooltip-action-utils.js @@ -7,7 +7,7 @@ import { useModalContext } from 'lib/components/modal-provider.react.js'; import type { ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js'; import { useCanEditMessage } from 'lib/shared/edit-messages-utils.js'; -import { createMessageReply } from 'lib/shared/message-utils.js'; +import { createMessageReply, messageKey } from 'lib/shared/message-utils.js'; import { useCanCreateReactionFromMessage } from 'lib/shared/reaction-utils.js'; import { threadHasPermission, @@ -235,6 +235,7 @@ const canEditMessage = useCanEditMessage(threadInfo, messageInfo); const { renderEditModal } = useEditModalContext(); const { clearTooltip } = useTooltipContext(); + const { scrollToMessage } = useEditModalContext(); return React.useMemo(() => { if (!canEditMessage) { @@ -242,13 +243,15 @@ } const buttonContent = ; const onClickEdit = () => { + const callback = () => + renderEditModal({ + messageInfo: item, + threadInfo, + isError: false, + editedMessageDraft: messageInfo.text, + }); clearTooltip(); - renderEditModal({ - messageInfo: item, - threadInfo, - isError: false, - editedMessageDraft: messageInfo.text, - }); + scrollToMessage(messageKey(messageInfo), callback); }; return { actionButtonContent: buttonContent, @@ -259,8 +262,9 @@ canEditMessage, clearTooltip, item, - messageInfo.text, + messageInfo, renderEditModal, + scrollToMessage, threadInfo, ]); }