Page MenuHomePhabricator

D8027.diff
No OneTemporary

D8027.diff

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

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)

Event Timeline