Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3496600
D8027.id28356.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
16 KB
Referenced Files
None
Subscribers
None
D8027.id28356.diff
View Options
diff --git a/web/chat/chat-input-bar.css b/web/chat/chat-input-bar.css
--- a/web/chat/chat-input-bar.css
+++ b/web/chat/chat-input-bar.css
@@ -13,6 +13,7 @@
display: flex;
background: var(--text-input-bg);
border-radius: 8px;
+ /* Related to editBoxHeight in the `edit-text-message` component */
padding: 8px;
align-items: center;
flex-grow: 1;
diff --git a/web/chat/chat-input-text-area.react.js b/web/chat/chat-input-text-area.react.js
--- a/web/chat/chat-input-text-area.react.js
+++ b/web/chat/chat-input-text-area.react.js
@@ -12,8 +12,11 @@
+currentText: string,
+setCurrentText: (text: string) => void,
+onChangePosition: () => void,
+ +maxHeight?: number,
};
+export const defaultMaxHeight = 150;
+
const ChatInputTextArea: React.ComponentType<Props> = React.memo<Props>(
function ChatInputTextArea(props: Props) {
const {
@@ -23,6 +26,7 @@
send,
setCurrentText,
onChangePosition,
+ maxHeight = defaultMaxHeight,
} = props;
const textareaRef = React.useRef(null);
@@ -53,11 +57,11 @@
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = 'auto';
- const newHeight = Math.min(textarea.scrollHeight, 150);
+ const newHeight = Math.min(textarea.scrollHeight, maxHeight);
textarea.style.height = `${newHeight}px`;
}
onChangePosition();
- }, [onChangePosition]);
+ }, [maxHeight, onChangePosition]);
React.useEffect(() => {
focusAndUpdateText();
diff --git a/web/chat/chat-message-list.css b/web/chat/chat-message-list.css
--- a/web/chat/chat-message-list.css
+++ b/web/chat/chat-message-list.css
@@ -18,6 +18,9 @@
div.mirroredMessageContainer > div {
transform: scaleY(-1);
}
+div.disableAnchor {
+ overflow-anchor: none;
+}
div.message {
display: flex;
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
@@ -3,6 +3,7 @@
import classNames from 'classnames';
import { detect as detectBrowser } from 'detect-browser';
import invariant from 'invariant';
+import _ from 'lodash';
import * as React from 'react';
import {
@@ -28,8 +29,11 @@
useDispatchActionPromise,
} from 'lib/utils/action-utils.js';
+import { defaultMaxHeight } from './chat-input-text-area.react.js';
import css from './chat-message-list.css';
+import type { ScrollToMessageCallback } from './edit-message-provider.js';
import { useEditModalContext } from './edit-message-provider.js';
+import { editBoxHeight, editBoxTopMargin } 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 +68,37 @@
+clearTooltip: () => mixed,
+oldestMessageServerID: ?string,
+isEditState: boolean,
+ +addScrollToMessageListener: ScrollToMessageCallback => mixed,
+ +removeScrollToMessageListener: ScrollToMessageCallback => mixed,
};
type Snapshot = {
+scrollTop: number,
+scrollHeight: number,
};
-class ChatMessageList extends React.PureComponent<Props> {
+
+type State = {
+ +scrollingEndCallback: ?() => mixed,
+};
+
+class ChatMessageList extends React.PureComponent<Props, State> {
container: ?HTMLDivElement;
messageContainer: ?HTMLDivElement;
loadingFromScroll = false;
+ constructor(props: Props) {
+ super(props);
+ this.state = {
+ scrollingEndCallback: null,
+ };
+ }
+
componentDidMount() {
this.scrollToBottom();
+ this.props.addScrollToMessageListener(this.scrollToMessage);
+ }
+
+ componentWillUnmount() {
+ this.props.removeScrollToMessageListener(this.scrollToMessage);
}
getSnapshotBeforeUpdate(prevProps: Props) {
@@ -178,8 +201,106 @@
);
};
+ scrollingEndCallbackWrapper = (
+ composedMessageID: string,
+ callback: (maxHeight: number) => mixed,
+ ): (() => mixed) => {
+ return () => {
+ const maxHeight = this.getMaxEditTextAreaHeight(composedMessageID);
+ callback(maxHeight);
+ };
+ };
+
+ scrollToMessage = (
+ composedMessageID: string,
+ callback: (maxHeight: number) => mixed,
+ ) => {
+ const element = document.getElementById(composedMessageID);
+ if (!element) {
+ return;
+ }
+ const scrollingEndCallback = this.scrollingEndCallbackWrapper(
+ composedMessageID,
+ callback,
+ );
+ if (!this.willMessageEditWindowOverflow(composedMessageID)) {
+ scrollingEndCallback();
+ return;
+ }
+ this.setState(
+ {
+ scrollingEndCallback,
+ },
+ () => {
+ element.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ // It covers the case when browser decide not to scroll to the message
+ // because it's already in the view.
+ // In this case, the 'scroll' event won't be triggered,
+ // so we need to call the callback manually.
+ this.debounceEditModeAfterScrollToMessage();
+ },
+ );
+ };
+
+ getMaxEditTextAreaHeight = (composedMessageID: string): number => {
+ const { messageContainer } = this;
+ if (!messageContainer) {
+ return defaultMaxHeight;
+ }
+ const messageElement = document.getElementById(composedMessageID);
+ if (!messageElement) {
+ console.log(`couldn't find the message element`);
+ return defaultMaxHeight;
+ }
+
+ const msgPos = messageElement.getBoundingClientRect();
+ const containerPos = messageContainer.getBoundingClientRect();
+
+ const messageBottom = msgPos.bottom;
+ const containerTop = containerPos.top;
+
+ const maxHeight =
+ messageBottom - containerTop - editBoxHeight - editBoxTopMargin;
+
+ return maxHeight;
+ };
+
+ willMessageEditWindowOverflow(composedMessageID: string) {
+ const { messageContainer } = this;
+ if (!messageContainer) {
+ return false;
+ }
+ const messageElement = document.getElementById(composedMessageID);
+ if (!messageElement) {
+ console.log(`couldn't find the message element`);
+ return false;
+ }
+
+ const msgPos = messageElement.getBoundingClientRect();
+ const containerPos = messageContainer.getBoundingClientRect();
+ const containerTop = containerPos.top;
+ const containerBottom = containerPos.bottom;
+
+ const availableTextAreaHeight =
+ (containerBottom - containerTop) / 2 - editBoxHeight;
+ const messageHeight = msgPos.height;
+ const expectedMinimumHeight = Math.min(
+ defaultMaxHeight,
+ availableTextAreaHeight,
+ );
+ const offset = Math.max(
+ 0,
+ expectedMinimumHeight + editBoxHeight + editBoxTopMargin - messageHeight,
+ );
+
+ const messageTop = msgPos.top - offset;
+ const messageBottom = msgPos.bottom;
+
+ return messageBottom > containerBottom || messageTop < containerTop;
+ }
+
render() {
- const { messageListData, threadInfo, inputState } = this.props;
+ const { messageListData, threadInfo, inputState, isEditState } = this.props;
if (!messageListData) {
return <div className={css.container} />;
}
@@ -192,6 +313,8 @@
}
const messageContainerStyle = classNames({
+ [css.disableAnchor]:
+ this.state.scrollingEndCallback !== null || isEditState,
[css.messageContainer]: true,
[css.mirroredMessageContainer]: !supportsReverseFlex,
});
@@ -221,8 +344,16 @@
}
this.props.clearTooltip();
this.possiblyLoadMoreMessages();
+ this.debounceEditModeAfterScrollToMessage();
};
+ debounceEditModeAfterScrollToMessage = _.debounce(() => {
+ if (this.state.scrollingEndCallback) {
+ this.state.scrollingEndCallback();
+ }
+ this.setState({ scrollingEndCallback: null });
+ }, 100);
+
async possiblyLoadMoreMessages() {
if (!this.messageContainer) {
return;
@@ -313,7 +444,11 @@
const oldestMessageServerID = useOldestMessageServerID(threadInfo.id);
- const { editState } = useEditModalContext();
+ const {
+ editState,
+ addScrollToMessageListener,
+ removeScrollToMessageListener,
+ } = useEditModalContext();
const isEditState = editState !== null;
return (
@@ -330,6 +465,8 @@
clearTooltip={clearTooltip}
oldestMessageServerID={oldestMessageServerID}
isEditState={isEditState}
+ addScrollToMessageListener={addScrollToMessageListener}
+ removeScrollToMessageListener={removeScrollToMessageListener}
/>
</MessageListContext.Provider>
);
diff --git a/web/chat/composed-message.react.js b/web/chat/composed-message.react.js
--- a/web/chat/composed-message.react.js
+++ b/web/chat/composed-message.react.js
@@ -11,7 +11,9 @@
import { useStringForUser } from 'lib/hooks/ens-cache.js';
import { type ChatMessageInfoItem } from 'lib/selectors/chat-selectors.js';
import { getMessageLabel } from 'lib/shared/edit-messages-utils.js';
+import { messageKey } from 'lib/shared/message-utils.js';
import { assertComposableMessageType } from 'lib/types/message-types.js';
+import type { MessageInfo } from 'lib/types/message-types.js';
import { type ThreadInfo } from 'lib/types/thread-types.js';
import css from './chat-message-list.css';
@@ -23,6 +25,14 @@
import { useMessageTooltip } from '../utils/tooltip-action-utils.js';
import { tooltipPositions } from '../utils/tooltip-utils.js';
+export type ComposedMessageID = string;
+
+export const getComposedMessageID = (
+ messageInfo: MessageInfo,
+): ComposedMessageID => {
+ return `ComposedMessageBox-${messageKey(messageInfo)}`;
+};
+
const availableTooltipPositionsForViewerMessage = [
tooltipPositions.LEFT,
tooltipPositions.LEFT_BOTTOM,
@@ -188,7 +198,11 @@
onMouseLeave={this.props.onMouseLeave}
>
{pinIcon}
- <div className={messageBoxClassName} style={messageBoxStyle}>
+ <div
+ className={messageBoxClassName}
+ style={messageBoxStyle}
+ id={getComposedMessageID(item.messageInfo)}
+ >
{this.props.children}
</div>
</div>
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
@@ -22,8 +22,14 @@
+editedMessageDraft: ?string,
+isError: boolean,
+position?: ModalPosition,
+ +maxHeight: number,
};
+export type ScrollToMessageCallback = (
+ composedMessageID: string,
+ callback: (maxHeight: number) => void,
+) => void;
+
type EditModalContextType = {
+renderEditModal: (params: EditState) => void,
+clearEditModal: () => void,
@@ -31,6 +37,9 @@
+setDraft: string => void,
+setError: boolean => void,
+updatePosition: ModalPosition => void,
+ +scrollToMessage: ScrollToMessageCallback,
+ +addScrollToMessageListener: ScrollToMessageCallback => void,
+ +removeScrollToMessageListener: ScrollToMessageCallback => void,
};
const EditModalContext: React.Context<EditModalContextType> =
@@ -41,6 +50,9 @@
setDraft: () => {},
setError: () => {},
updatePosition: () => {},
+ scrollToMessage: () => {},
+ addScrollToMessageListener: () => {},
+ removeScrollToMessageListener: () => {},
});
type Props = {
@@ -51,6 +63,9 @@
const [editState, setEditState] = React.useState<?EditState>(null);
+ const [scrollToMessageCallbacks, setScrollToMessageCallbacks] =
+ React.useState<Array<ScrollToMessageCallback>>([]);
+
const clearEditModal = React.useCallback(() => {
setEditState(null);
}, []);
@@ -118,6 +133,36 @@
[editState, setEditState],
);
+ const scrollToMessage: ScrollToMessageCallback = React.useCallback(
+ (messageKey: string, callback: (maxHeight: number) => void) => {
+ scrollToMessageCallbacks.forEach((callback2: ScrollToMessageCallback) =>
+ callback2(messageKey, callback),
+ );
+ },
+ [scrollToMessageCallbacks],
+ );
+
+ const addScrollToMessageListener = React.useCallback(
+ (callback: ScrollToMessageCallback): void => {
+ setScrollToMessageCallbacks(prevScrollToMessageCallbacks => [
+ ...prevScrollToMessageCallbacks,
+ callback,
+ ]);
+ },
+ [],
+ );
+
+ const removeScrollToMessageListener = React.useCallback(
+ (callback: ScrollToMessageCallback) => {
+ setScrollToMessageCallbacks(prevScrollToMessageCallbacks =>
+ prevScrollToMessageCallbacks.filter(
+ candidate => candidate !== callback,
+ ),
+ );
+ },
+ [],
+ );
+
const value = React.useMemo(
() => ({
renderEditModal,
@@ -126,6 +171,9 @@
setDraft,
setError,
updatePosition,
+ scrollToMessage,
+ addScrollToMessageListener,
+ removeScrollToMessageListener,
}),
[
renderEditModal,
@@ -134,6 +182,9 @@
setDraft,
setError,
updatePosition,
+ scrollToMessage,
+ addScrollToMessageListener,
+ removeScrollToMessageListener,
],
);
diff --git a/web/chat/edit-text-message.css b/web/chat/edit-text-message.css
--- a/web/chat/edit-text-message.css
+++ b/web/chat/edit-text-message.css
@@ -1,4 +1,5 @@
.editMessage {
+ /* Related to editBoxHeight in the `edit-text-message` component */
padding: 10px;
background-color: var(--modal-bg);
border-radius: 8px;
@@ -9,6 +10,7 @@
}
.bottomRow {
+ /* Related to editBoxHeight in the `edit-text-message` component */
padding-top: 10px;
display: flex;
align-items: center;
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
@@ -10,7 +10,6 @@
import { trimMessage } from 'lib/shared/message-utils.js';
import { type ThreadInfo } from 'lib/types/thread-types.js';
-import cssInputBar from './chat-input-bar.css';
import ChatInputTextArea from './chat-input-text-area.react.js';
import ComposedMessage from './composed-message.react.js';
import { useEditModalContext } from './edit-message-provider.js';
@@ -28,6 +27,11 @@
backgroundColor: 'transparent',
};
+const bottomRowHeight = 22;
+
+export const editBoxTopMargin = 10;
+export const editBoxHeight: number = 3 * 10 + 2 * 8 + bottomRowHeight;
+
function EditTextMessage(props: Props): React.Node {
const { background, threadInfo, item } = props;
const { editState, clearEditModal, setDraft, setError, updatePosition } =
@@ -138,18 +142,19 @@
[css.backgroundEditMessage]: background,
});
+ const maxTextAreaHeight = editState?.maxHeight;
+
return (
<div className={containerStyle} ref={myRef}>
- <div className={cssInputBar.inputBarTextInput}>
- <ChatInputTextArea
- focus={!background}
- currentText={editedMessageDraft}
- setCurrentText={setDraft}
- onChangePosition={updateDimensions}
- send={checkAndEdit}
- />
- </div>
- <div className={css.bottomRow}>
+ <ChatInputTextArea
+ focus={!background}
+ currentText={editedMessageDraft}
+ setCurrentText={setDraft}
+ onChangePosition={updateDimensions}
+ send={checkAndEdit}
+ maxHeight={maxTextAreaHeight}
+ />
+ <div className={css.bottomRow} style={{ height: bottomRowHeight }}>
{editFailed}
<div className={css.buttons}>
<Button
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
@@ -27,6 +27,7 @@
type TooltipSize,
type TooltipPosition,
} from './tooltip-utils.js';
+import { getComposedMessageID } from '../chat/composed-message.react.js';
import { useEditModalContext } from '../chat/edit-message-provider.js';
import MessageTooltip from '../chat/message-tooltip.react.js';
import type { PositionInfo } from '../chat/position-types.js';
@@ -226,7 +227,7 @@
const { messageInfo } = item;
const canEditMessage = useCanEditMessage(threadInfo, messageInfo);
- const { renderEditModal } = useEditModalContext();
+ const { renderEditModal, scrollToMessage } = useEditModalContext();
const { clearTooltip } = useTooltipContext();
return React.useMemo(() => {
@@ -235,13 +236,16 @@
}
const buttonContent = <CommIcon icon="edit-filled" size={18} />;
const onClickEdit = () => {
+ const callback = (maxHeight: number) =>
+ renderEditModal({
+ messageInfo: item,
+ threadInfo,
+ isError: false,
+ editedMessageDraft: messageInfo.text,
+ maxHeight: maxHeight,
+ });
clearTooltip();
- renderEditModal({
- messageInfo: item,
- threadInfo,
- isError: false,
- editedMessageDraft: messageInfo.text,
- });
+ scrollToMessage(getComposedMessageID(messageInfo), callback);
};
return {
actionButtonContent: buttonContent,
@@ -252,8 +256,9 @@
canEditMessage,
clearTooltip,
item,
- messageInfo.text,
+ messageInfo,
renderEditModal,
+ scrollToMessage,
threadInfo,
]);
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Dec 20, 1:32 PM (16 h, 15 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2681843
Default Alt Text
D8027.id28356.diff (16 KB)
Attached To
Mode
D8027: [web] Scrolling to the edited message when it overflows
Attached
Detach File
Event Timeline
Log In to Comment