diff --git a/react/features/chat/actionTypes.ts b/react/features/chat/actionTypes.ts index 1e6d3e562c44..1416181d92aa 100644 --- a/react/features/chat/actionTypes.ts +++ b/react/features/chat/actionTypes.ts @@ -172,3 +172,13 @@ export const SET_CHAT_IS_RESIZING = 'SET_CHAT_IS_RESIZING'; * } */ export const NOTIFY_PRIVATE_RECIPIENTS_CHANGED = 'NOTIFY_PRIVATE_RECIPIENTS_CHANGED'; + +/** + * The type of action which signals to set the reply message. + * + * { + * type: SET_REPLY_MESSAGE, + * message: IMessage + * } + */ +export const SET_REPLY_MESSAGE = 'SET_REPLY_MESSAGE'; diff --git a/react/features/chat/actions.any.ts b/react/features/chat/actions.any.ts index dda4bac8cdc2..0d61b4b240ac 100644 --- a/react/features/chat/actions.any.ts +++ b/react/features/chat/actions.any.ts @@ -18,9 +18,11 @@ import { SET_FOCUSED_TAB, SET_LOBBY_CHAT_ACTIVE_STATE, SET_LOBBY_CHAT_RECIPIENT, - SET_PRIVATE_MESSAGE_RECIPIENT + SET_PRIVATE_MESSAGE_RECIPIENT, + SET_REPLY_MESSAGE } from './actionTypes'; import { ChatTabs } from './constants'; +import { IMessage } from './types'; /** * Adds a chat message to the collection of messages. @@ -129,11 +131,12 @@ export function closeChat() { * message: string * }} */ -export function sendMessage(message: string, ignorePrivacy = false) { +export function sendMessage(message: string, ignorePrivacy = false, replyToMessageId?: string) { return { type: SEND_MESSAGE, ignorePrivacy, - message + message, + replyToMessageId }; } @@ -385,3 +388,19 @@ export function handleLobbyChatInitialized(participantId: string) { return conference?.sendLobbyMessage(payload); }; } + +/** + * Sets the message that is being replied to. + * + * @param {IMessage} message - The message to reply to. + * @returns {{ + * type: SET_REPLY_MESSAGE, + * message: IMessage + * }} + */ +export function setReplyMessage(message?: IMessage) { + return { + type: SET_REPLY_MESSAGE, + message + }; +} diff --git a/react/features/chat/components/web/Chat.tsx b/react/features/chat/components/web/Chat.tsx index 26f1b439d26d..fda938a0b9df 100644 --- a/react/features/chat/components/web/Chat.tsx +++ b/react/features/chat/components/web/Chat.tsx @@ -410,8 +410,8 @@ const Chat = ({ * @returns {void} * @type {Function} */ - const onSendMessage = useCallback((text: string) => { - dispatch(sendMessage(text)); + const onSendMessage = useCallback((text: string, replyToMessageId?: string) => { + dispatch(sendMessage(text, false, replyToMessageId)); }, []); /** diff --git a/react/features/chat/components/web/ChatInput.tsx b/react/features/chat/components/web/ChatInput.tsx index 6fe2568b6172..2cc43cecea60 100644 --- a/react/features/chat/components/web/ChatInput.tsx +++ b/react/features/chat/components/web/ChatInput.tsx @@ -12,6 +12,7 @@ import Button from '../../../base/ui/components/web/Button'; import Input from '../../../base/ui/components/web/Input'; import { CHAT_SIZE } from '../../constants'; import { areSmileysDisabled, isSendGroupChatDisabled } from '../../functions'; +import { setReplyMessage } from '../../actions.any'; import SmileysPanel from './SmileysPanel'; @@ -68,6 +69,8 @@ interface IProps extends WithTranslation { */ _privateMessageRecipientId?: string; + _replyMessage?: any; + /** * An object containing the CSS classes. */ @@ -164,19 +167,56 @@ class ChatInput extends Component { * @returns {ReactElement} */ override render() { + const { _replyMessage, dispatch, t } = this.props; const classes = withStyles.getClasses(this.props); const hideInput = this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId; if (hideInput) { return (
- {this.props.t('chat.disabled')} + {t('chat.disabled')}
); } return (
+ {_replyMessage && ( +
+
+
+ {_replyMessage.displayName ?? 'Unknown'} +
+
+ {_replyMessage.message} +
+
+
dispatch(setReplyMessage(undefined))} + style={{ + cursor: 'pointer', + padding: '4px 8px', + fontSize: '1rem', + color: 'white' + }}> + ✕ +
+
+ )}
{!this.props._areSmileysDisabled && this.state.showSmileysPanel && (
{ maxRows = { 5 } onChange = { this._onMessageChange } onKeyPress = { this._onDetectSubmit } - placeholder = { this.props.t('chat.messagebox') } + placeholder = { t('chat.messagebox') } ref = { this._textArea } textarea = { true } value = { this.state.message } />
)} @@ -379,6 +437,7 @@ const ChatMessage = ({
{showDisplayName && _renderDisplayName()} + {message.replyToMessageId && _renderReplyPreview()}
{isFileMessage(message) ? ( }
diff --git a/react/features/chat/components/web/MessageMenu.tsx b/react/features/chat/components/web/MessageMenu.tsx index 40a7ba3d492c..09b766888e85 100644 --- a/react/features/chat/components/web/MessageMenu.tsx +++ b/react/features/chat/components/web/MessageMenu.tsx @@ -11,7 +11,7 @@ import Popover from '../../../base/popover/components/Popover.web'; import Button from '../../../base/ui/components/web/Button'; import { BUTTON_TYPES } from '../../../base/ui/constants.any'; import { copyText } from '../../../base/util/copyText.web'; -import { handleLobbyChatInitialized, openChat } from '../../actions.web'; +import { handleLobbyChatInitialized, openChat, setReplyMessage } from '../../actions.web'; import logger from '../../logger'; export interface IProps { @@ -22,6 +22,7 @@ export interface IProps { isFromVisitor?: boolean; isLobbyMessage: boolean; message: string; + messageId?: string; participantId: string; } @@ -62,7 +63,7 @@ const useStyles = makeStyles()(theme => { }; }); -const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, enablePrivateChat, displayName, isFileMessage }: IProps) => { +const MessageMenu = ({ message, messageId, participantId, isFromVisitor, isLobbyMessage, enablePrivateChat, displayName, isFileMessage }: IProps) => { const dispatch = useDispatch(); const { classes, cx } = useStyles(); const { t } = useTranslation(); @@ -135,8 +136,27 @@ const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, en handleClose(); }, [ message ]); + const handleReplyClick = useCallback(() => { + handleClose(); + setTimeout(() => { + dispatch(setReplyMessage({ + message, + messageId, + participantId, + displayName + } as any)); + }, 50); +}, [ dispatch, message, messageId, participantId, displayName ]); + const popoverContent = (
+ {!isFileMessage && ( +
+ {t('Reply')} +
+ )} {enablePrivateChat && (
next => action => { _persistSentPrivateMessage(store, lobbyMessageRecipient, action.message, true); } else if (privateMessageRecipient) { conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message, 'body', isVisitorChatParticipant(privateMessageRecipient)); - _persistSentPrivateMessage(store, privateMessageRecipient, action.message); + _persistSentPrivateMessage(store, privateMessageRecipient, action.message, false, action.replyToMessageId); } else { - conference.sendTextMessage(action.message); + conference.sendTextMessage(action.message, 'body', action.replyToMessageId); } } break; @@ -406,7 +406,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) { JitsiConferenceEvents.MESSAGE_RECEIVED, /* eslint-disable max-params */ (participantId: string, message: string, timestamp: number, - displayName: string, isFromVisitor: boolean, messageId: string, source: string) => { + displayName: string, isFromVisitor: boolean, messageId: string, source: string, replyToId?: string) => { /* eslint-enable max-params */ _onConferenceMessageReceived(store, { // in case of messages coming from visitors we can have unknown id @@ -417,7 +417,8 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) { isFromVisitor, messageId, source, - privateMessage: false + privateMessage: false, + replyToId }); if (isSendGroupChatDisabled(store.getState()) && participantId) { @@ -441,7 +442,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) { conference.on( JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED, - (participantId: string, message: string, timestamp: number, messageId: string, displayName?: string, isFromVisitor?: boolean) => { + (participantId: string, message: string, timestamp: number, messageId: string, displayName?: string, isFromVisitor?: boolean, replyToId?: string) => { _onConferenceMessageReceived(store, { participantId, message, @@ -449,7 +450,8 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) { displayName, messageId, privateMessage: true, - isFromVisitor + isFromVisitor, + replyToId }); } ); @@ -468,9 +470,9 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) { * @returns {void} */ function _onConferenceMessageReceived(store: IStore, - { displayName, isFromVisitor, message, messageId, participantId, privateMessage, timestamp, source }: { + { displayName, isFromVisitor, message, messageId, participantId, privateMessage, timestamp, source, replyToId }: { displayName?: string; isFromVisitor?: boolean; message: string; messageId?: string; - participantId: string; privateMessage: boolean; source?: string; timestamp: number; } + participantId: string; privateMessage: boolean; replyToId?: string; source?: string; timestamp: number; } ) { const isGif = isGifEnabled(store.getState()) && isGifMessage(message); @@ -490,7 +492,8 @@ function _onConferenceMessageReceived(store: IStore, lobbyChat: false, timestamp, messageId, - source + source, + replyToId }, true, isGif); } @@ -599,9 +602,9 @@ function getLobbyChatDisplayName(state: IReduxState, participantId: string) { * @returns {void} */ function _handleReceivedMessage({ dispatch, getState }: IStore, - { displayName, isFromVisitor, lobbyChat, message, messageId, participantId, privateMessage, source, timestamp }: { + { displayName, isFromVisitor, lobbyChat, message, messageId, participantId, privateMessage, source, timestamp, replyToId }: { displayName?: string; isFromVisitor?: boolean; lobbyChat: boolean; message: string; - messageId?: string; participantId: string; privateMessage: boolean; source?: string; timestamp: number; }, + messageId?: string; participantId: string; privateMessage: boolean; replyToId?: string; source?: string; timestamp: number; }, shouldPlaySound = true, isReaction = false ) { @@ -654,7 +657,8 @@ function _handleReceivedMessage({ dispatch, getState }: IStore, messageId, isReaction, isFromVisitor, - isFromGuest: source === 'guest' + isFromGuest: source === 'guest', + replyToMessageId: replyToId ?? undefined }; dispatch(addMessage(newMessage)); @@ -715,7 +719,7 @@ interface IRecipient { * @returns {void} */ function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipient: IRecipient, - message: string, isLobbyPrivateMessage = false) { + message: string, isLobbyPrivateMessage = false, replyToMessageId?: string) { const state = getState(); const localParticipant = getLocalParticipant(state); @@ -742,7 +746,8 @@ function _persistSentPrivateMessage({ dispatch, getState }: IStore, recipient: I lobbyChat: isLobbyPrivateMessage, recipient: recipientName, sentToVisitor: recipient.isVisitor, - timestamp: Date.now() + timestamp: Date.now(), + replyToMessageId })); } diff --git a/react/features/chat/reducer.ts b/react/features/chat/reducer.ts index aa1629d5a0e9..ec21ef17dc44 100644 --- a/react/features/chat/reducer.ts +++ b/react/features/chat/reducer.ts @@ -19,6 +19,7 @@ import { SET_LOBBY_CHAT_ACTIVE_STATE, SET_LOBBY_CHAT_RECIPIENT, SET_PRIVATE_MESSAGE_RECIPIENT, + SET_REPLY_MESSAGE, SET_USER_CHAT_WIDTH } from './actionTypes'; import { CHAT_SIZE, ChatTabs } from './constants'; @@ -37,6 +38,7 @@ const DEFAULT_STATE = { isLobbyChatActive: false, focusedTab: undefined, isResizing: false, + replyMessage: undefined, width: { current: CHAT_SIZE, userSet: null @@ -57,6 +59,7 @@ export interface IChatState { messages: IMessage[]; notifyPrivateRecipientsChangedTimestamp?: number; privateMessageRecipient?: IParticipant | IVisitorChatParticipant; + replyMessage?: IMessage; unreadFilesCount: number; unreadMessagesCount: number; width: { @@ -84,7 +87,8 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, ac lobbyChat: action.lobbyChat, recipient: action.recipient, sentToVisitor: Boolean(action.sentToVisitor), - timestamp: action.timestamp + timestamp: action.timestamp, + replyToMessageId: action.replyToMessageId }; // React native, unlike web, needs a reverse sorted message list. @@ -292,6 +296,11 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, ac unreadFilesCount: remoteFilesCount }; } + case SET_REPLY_MESSAGE: + return { + ...state, + replyMessage: action.message + }; } return state; diff --git a/react/features/chat/types.ts b/react/features/chat/types.ts index 1a2b58b13182..a93c84f7d10d 100644 --- a/react/features/chat/types.ts +++ b/react/features/chat/types.ts @@ -19,6 +19,7 @@ export interface IMessage { reactions: Map>; recipient: string; sentToVisitor?: boolean; + replyToMessageId?: string; timestamp: number; }