diff --git a/lib/shared/edit-messages-utils.js b/lib/shared/edit-messages-utils.js --- a/lib/shared/edit-messages-utils.js +++ b/lib/shared/edit-messages-utils.js @@ -8,10 +8,12 @@ sendEditMessageActionTypes, sendEditMessage, } from '../actions/message-actions.js'; +import { messageInfoSelector } from '../selectors/chat-selectors.js'; import type { SendEditMessageResult, RobotextMessageInfo, ComposableMessageInfo, + MessageInfo, } from '../types/message-types'; import { messageTypes } from '../types/message-types.js'; import { threadPermissions, type ThreadInfo } from '../types/thread-types.js'; @@ -76,4 +78,40 @@ return hasPermission; } -export { useCanEditMessage, useEditMessage }; +function useGetEditedMessage(targetMessageID: ?string): () => ?MessageInfo { + const messageInfos = useSelector(messageInfoSelector); + const targetMessageInfo = targetMessageID && messageInfos[targetMessageID]; + const threadInfo = useSelector(state => { + if (!targetMessageInfo) { + return null; + } + return state.messageStore.threads[targetMessageInfo.threadID]; + }); + + return React.useCallback(() => { + if (!targetMessageInfo || targetMessageInfo.type !== messageTypes.TEXT) { + return null; + } + const threadMessageInfos = (threadInfo?.messageIDs ?? []) + .map((messageID: string) => messageInfos[messageID]) + .filter(Boolean) + .filter( + message => + message.type === messageTypes.EDIT_MESSAGE && + message.targetMessageID === targetMessageID, + ); + if (threadMessageInfos.length === 0) { + return targetMessageInfo; + } + invariant( + threadMessageInfos[0].type === messageTypes.EDIT_MESSAGE, + 'message should be edit message', + ); + return { + ...targetMessageInfo, + text: threadMessageInfos[0].text, + }; + }, [messageInfos, targetMessageID, targetMessageInfo, threadInfo]); +} + +export { useCanEditMessage, useGetEditedMessage, useEditMessage }; diff --git a/lib/shared/message-utils.js b/lib/shared/message-utils.js --- a/lib/shared/message-utils.js +++ b/lib/shared/message-utils.js @@ -467,7 +467,7 @@ // unread has highest contrast, followed by primary, followed by secondary +style: 'unread' | 'primary' | 'secondary', }; -type MessagePreviewResult = { +export type MessagePreviewResult = { +message: MessagePreviewPart, +username: ?MessagePreviewPart, }; diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js --- a/native/chat/chat-input-bar.react.js +++ b/native/chat/chat-input-bar.react.js @@ -31,13 +31,19 @@ import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { userStoreSearchIndex } from 'lib/selectors/user-selectors.js'; import { colorIsDark } from 'lib/shared/color-utils.js'; +import { useGetEditedMessage } from 'lib/shared/edit-messages-utils.js'; import { getTypeaheadUserSuggestions, getTypeaheadRegexMatches, type Selection, getMentionsCandidates, } from 'lib/shared/mention-utils.js'; -import { localIDPrefix, trimMessage } from 'lib/shared/message-utils.js'; +import { + localIDPrefix, + trimMessage, + useMessagePreview, +} from 'lib/shared/message-utils.js'; +import type { MessagePreviewResult } from 'lib/shared/message-utils.js'; import SearchIndex from 'lib/shared/search-index.js'; import { threadHasPermission, @@ -75,6 +81,7 @@ import type { SyncedSelectionData } from '../components/selectable-text-input.js'; // eslint-disable-next-line import/extensions import SelectableTextInput from '../components/selectable-text-input.react'; +import { SingleLine } from '../components/single-line.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { type InputState, InputStateContext } from '../input/input-state.js'; import KeyboardInputHost from '../keyboard/keyboard-input-host.react.js'; @@ -83,6 +90,7 @@ KeyboardContext, } from '../keyboard/keyboard-state.js'; import { getKeyboardHeight } from '../keyboard/keyboard.js'; +import { getDefaultTextMessageRules } from '../markdown/rules.react.js'; import { nonThreadCalendarQuery, activeThreadSelector, @@ -139,6 +147,7 @@ +userSearchIndex: SearchIndex, +mentionsCandidates: $ReadOnlyArray, +parentThreadInfo: ?ThreadInfo, + +messagePreviewResult: ?MessagePreviewResult, }; type State = { +text: string, @@ -422,6 +431,7 @@ threadPermissions.JOIN_THREAD, ); let joinButton = null; + const threadColor = `#${this.props.threadInfo.color}`; if (!isMember && canJoin && !this.props.threadCreationInProgress) { let buttonContent; if (this.props.joinThreadLoadingStatus === 'loading') { @@ -450,7 +460,7 @@ iosActiveOpacity={0.85} style={[ this.props.styles.joinButton, - { backgroundColor: `#${this.props.threadInfo.color}` }, + { backgroundColor: threadColor }, ]} > {buttonContent} @@ -532,6 +542,35 @@ ); + let editMode; + const isEditMode = this.isEditMode(); + if (isEditMode && this.props.messagePreviewResult) { + const { message } = this.props.messagePreviewResult; + editMode = ( + <> + + + + Editing message + + + {message.text} + + + + + + ); + } + return ( {typeaheadTooltip} {joinButton} + {editMode} {content} {keyboardInputHost} @@ -736,6 +776,17 @@ ); }; + editState = () => this.props.inputState?.editState; + + isEditMode = () => { + const editState = this.editState(); + return editState && editState.editedMessageID !== null; + }; + + onPressExitEditMode = () => { + this.props.inputState?.setEditedMessageID(null); + }; + onPressJoin = () => { this.props.dispatchActionPromise(joinThreadActionTypes, this.joinAction()); }; @@ -844,6 +895,26 @@ height: 48, marginBottom: 8, }, + editView: { + marginLeft: 20, + marginRight: 20, + padding: 10, + flexDirection: 'row', + justifyContent: 'space-between', + }, + editViewContent: { + flex: 1, + paddingRight: 6, + }, + exitEditButton: { + marginTop: 6, + }, + editingLabel: { + paddingBottom: 4, + }, + editingMessagePreview: { + color: 'listForegroundLabel', + }, joinButtonContent: { flexDirection: 'row', justifyContent: 'center', @@ -948,6 +1019,15 @@ parentThreadInfo, ); + const editedMessageID = inputState?.editState.editedMessageID; + const editedMessageInfo = useSelector(useGetEditedMessage(editedMessageID)); + + const messagePreviewResult = useMessagePreview( + editedMessageInfo, + props.threadInfo, + getDefaultTextMessageRules().simpleMarkdownRules, + ); + return ( ); }