Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3488609
D8027.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
18 KB
Referenced Files
None
Subscribers
None
D8027.diff
View Options
diff --git a/web/chat/chat-constants.js b/web/chat/chat-constants.js
--- a/web/chat/chat-constants.js
+++ b/web/chat/chat-constants.js
@@ -1,5 +1,10 @@
// @flow
+import { messageKey } from 'lib/shared/message-utils.js';
+import type { MessageInfo } from 'lib/types/message-types.js';
+
+import type { ComposedMessageID } from './composed-message.react.js';
+
export const tooltipStyle = {
paddingLeft: 5,
paddingRight: 5,
@@ -25,3 +30,26 @@
tooltipTopOffset: 4,
rowHeight: 40,
};
+
+export const getComposedMessageID = (
+ messageInfo: MessageInfo,
+): ComposedMessageID => {
+ return `ComposedMessageBox-${messageKey(messageInfo)}`;
+};
+
+export const defaultMaxTextAreaHeight = 150;
+
+// The editBoxBottomRowHeight is the height of the bottom row in the edit box
+// which is the height of the buttons in the bottom row.
+export const editBoxBottomRowHeight = 22;
+
+// The editBoxHeight is a height of the all elements of the edit box
+// except for the textarea.
+// It consists of:
+// - 2 * 10px: .editMessage padding (edit-text-message.css)
+// - 10px: .bottomRow padding between the bottom row buttons
+// and the textarea (edit-text-message.css)
+// - 2 * 8px: .inputBarTextInput padding (chat-input-bar.css)
+// - 22px: height of the bottom row in the edit box (explained above)
+// - textarea height which is NOT included here
+export const editBoxHeight: number = 3 * 10 + 2 * 8 + editBoxBottomRowHeight;
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
@@ -3,6 +3,7 @@
import invariant from 'invariant';
import * as React from 'react';
+import { defaultMaxTextAreaHeight } from './chat-constants.js';
import css from './chat-input-bar.css';
type Props = {
@@ -12,6 +13,7 @@
+currentText: string,
+setCurrentText: (text: string) => void,
+onChangePosition: () => void,
+ +maxHeight?: number,
};
const ChatInputTextArea: React.ComponentType<Props> = React.memo<Props>(
@@ -23,6 +25,7 @@
send,
setCurrentText,
onChangePosition,
+ maxHeight = defaultMaxTextAreaHeight,
} = props;
const textareaRef = React.useRef(null);
@@ -53,11 +56,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 _debounce from 'lodash/debounce.js';
import * as React from 'react';
import {
@@ -28,7 +29,9 @@
useDispatchActionPromise,
} from 'lib/utils/action-utils.js';
+import { editBoxHeight, defaultMaxTextAreaHeight } from './chat-constants.js';
import css from './chat-message-list.css';
+import type { ScrollToMessageCallback } from './edit-message-provider.js';
import { useEditModalContext } from './edit-message-provider.js';
import { MessageListContext } from './message-list-types.js';
import Message from './message.react.js';
@@ -43,6 +46,10 @@
const supportsReverseFlex =
!browser || browser.name !== 'firefox' || parseInt(browser.version) >= 81;
+// Margin between the top of the maximum height edit box
+// and the top of the container
+const editBoxTopMargin = 10;
+
type BaseProps = {
+threadInfo: ThreadInfo,
};
@@ -64,18 +71,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 +204,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 defaultMaxTextAreaHeight;
+ }
+ const messageElement = document.getElementById(composedMessageID);
+ if (!messageElement) {
+ console.log(`couldn't find the message element`);
+ return defaultMaxTextAreaHeight;
+ }
+
+ 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(
+ defaultMaxTextAreaHeight,
+ 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 +316,8 @@
}
const messageContainerStyle = classNames({
+ [css.disableAnchor]:
+ this.state.scrollingEndCallback !== null || isEditState,
[css.messageContainer]: true,
[css.mirroredMessageContainer]: !supportsReverseFlex,
});
@@ -221,8 +347,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 +447,11 @@
const oldestMessageServerID = useOldestMessageServerID(threadInfo.id);
- const { editState } = useEditModalContext();
+ const {
+ editState,
+ addScrollToMessageListener,
+ removeScrollToMessageListener,
+ } = useEditModalContext();
const isEditState = editState !== null;
return (
@@ -330,6 +468,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
@@ -14,6 +14,7 @@
import { assertComposableMessageType } from 'lib/types/message-types.js';
import { type ThreadInfo } from 'lib/types/thread-types.js';
+import { getComposedMessageID } from './chat-constants.js';
import css from './chat-message-list.css';
import FailedSend from './failed-send.react.js';
import InlineEngagement from './inline-engagement.react.js';
@@ -23,6 +24,8 @@
import { useMessageTooltip } from '../utils/tooltip-action-utils.js';
import { tooltipPositions } from '../utils/tooltip-utils.js';
+export type ComposedMessageID = string;
+
const availableTooltipPositionsForViewerMessage = [
tooltipPositions.LEFT,
tooltipPositions.LEFT_BOTTOM,
@@ -188,7 +191,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,7 @@
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 { editBoxBottomRowHeight } from './chat-constants.js';
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 +28,8 @@
backgroundColor: 'transparent',
};
+const bottomRowStyle = { height: editBoxBottomRowHeight };
+
function EditTextMessage(props: Props): React.Node {
const { background, threadInfo, item } = props;
const { editState, clearEditModal, setDraft, setError, updatePosition } =
@@ -138,18 +140,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={bottomRowStyle}>
{editFailed}
<div className={css.buttons}>
<Button
diff --git a/web/components/button.css b/web/components/button.css
--- a/web/components/button.css
+++ b/web/components/button.css
@@ -8,6 +8,11 @@
border: none;
}
+/*
+ * Changes to the height of the button component should be reflected in
+ * the `chat-constants.js`component in the `editBoxBottomRowHeight`
+ * variable as well.
+ */
.btn {
--border-width: 1px;
--border-radius: 8px;
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/chat-constants.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
Thu, Dec 19, 10:48 AM (20 h, 42 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2676799
Default Alt Text
D8027.diff (18 KB)
Attached To
Mode
D8027: [web] Scrolling to the edited message when it overflows
Attached
Detach File
Event Timeline
Log In to Comment