Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions react/features/chat/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
25 changes: 22 additions & 3 deletions react/features/chat/actions.any.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
};
}

Expand Down Expand Up @@ -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
};
}
4 changes: 2 additions & 2 deletions react/features/chat/components/web/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}, []);

/**
Expand Down
55 changes: 50 additions & 5 deletions react/features/chat/components/web/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -68,6 +69,8 @@ interface IProps extends WithTranslation {
*/
_privateMessageRecipientId?: string;

_replyMessage?: any;

/**
* An object containing the CSS classes.
*/
Expand Down Expand Up @@ -164,19 +167,56 @@ class ChatInput extends Component<IProps, IState> {
* @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 (
<div className = { classes.chatDisabled }>
{this.props.t('chat.disabled')}
{t('chat.disabled')}
</div>
);
}

return (
<div className = { `chat-input-container${this.state.message.trim().length ? ' populated' : ''}` }>
{_replyMessage && (
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '6px 12px',
backgroundColor: 'rgba(255,255,255,0.08)',
borderLeft: '3px solid #1e90ff',
marginBottom: '4px',
fontSize: '0.8rem',
color: 'white'
}}>
<div style={{ overflow: 'hidden' }}>
<div style={{ fontWeight: 'bold', color: '#1e90ff' }}>
{_replyMessage.displayName ?? 'Unknown'}
</div>
<div style={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}>
{_replyMessage.message}
</div>
</div>
<div
onClick={() => dispatch(setReplyMessage(undefined))}
style={{
cursor: 'pointer',
padding: '4px 8px',
fontSize: '1rem',
color: 'white'
}}>
</div>
</div>
)}
<div id = 'chat-input' >
{!this.props._areSmileysDisabled && this.state.showSmileysPanel && (
<div
Expand All @@ -196,12 +236,12 @@ class ChatInput extends Component<IProps, IState> {
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 } />
<Button
accessibilityLabel = { this.props.t('chat.sendButton') }
accessibilityLabel = { t('chat.sendButton') }
disabled = { !this.state.message.trim() }
icon = { IconSend }
onClick = { this._onSubmitMessage }
Expand Down Expand Up @@ -240,7 +280,11 @@ class ChatInput extends Component<IProps, IState> {
const trimmed = this.state.message.trim();

if (trimmed) {
onSend(trimmed);
onSend(trimmed, this.props._replyMessage?.messageId);

if (this.props._replyMessage) {
this.props.dispatch(setReplyMessage(undefined));
}

this.setState({ message: '' });

Expand Down Expand Up @@ -342,14 +386,15 @@ class ChatInput extends Component<IProps, IState> {
* }}
*/
const mapStateToProps = (state: IReduxState) => {
const { privateMessageRecipient, width } = state['features/chat'];
const { privateMessageRecipient, width, replyMessage } = state['features/chat'];
const isGroupChatDisabled = isSendGroupChatDisabled(state);

return {
_areSmileysDisabled: areSmileysDisabled(state),
_privateMessageRecipientId: privateMessageRecipient?.id,
_isSendGroupChatDisabled: isGroupChatDisabled,
_chatWidth: width.current ?? CHAT_SIZE,
_replyMessage: replyMessage
};
};

Expand Down
60 changes: 60 additions & 0 deletions react/features/chat/components/web/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,35 @@ const useStyles = makeStyles()((theme: Theme) => {
replyButton: {
padding: '2px'
},
replyPreview: {
display: 'flex',
flexDirection: 'column',
backgroundColor: 'rgba(0, 0, 0, 0.1)',
padding: theme.spacing(1),
borderRadius: '4px',
borderLeft: `3px solid ${theme.palette.chatSenderName}`,
marginBottom: theme.spacing(1),
cursor: 'pointer',
maxWidth: '100%',
overflow: 'hidden'
},
replyPreviewName: {
...theme.typography.labelBold,
color: theme.palette.chatSenderName,
fontSize: '0.75rem',
marginBottom: '2px',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden'
},
replyPreviewText: {
...theme.typography.labelRegular,
color: theme.palette.chatMessageText,
fontSize: '0.85rem',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden'
},
replyWrapper: {
display: 'flex',
flexDirection: 'row' as const,
Expand Down Expand Up @@ -281,6 +310,34 @@ const ChatMessage = ({
);
}

/**
* Renders a preview of the message being replied to.
*
* @returns {React$Element<*>}
*/
function _renderReplyPreview() {
if (!message.replyToMessageId || !state) {
return null;
}

const originalMessage = state['features/chat']?.messages?.find(
(m: any) => m.messageId === message.replyToMessageId
);

return (
<div className = { cx(classes.replyPreview) }>
<span className = { cx(classes.replyPreviewName) }>
{ originalMessage?.displayName ?? 'Unknown' }
</span>
<span className = { cx(classes.replyPreviewText) }>
{ originalMessage
? (isFileMessage(originalMessage) ? '📎 File' : (originalMessage.message ?? 'Original message'))
: 'Original message' }
</span>
</div>
);
}

/**
* Renders the reactions for the message.
*
Expand Down Expand Up @@ -364,6 +421,7 @@ const ChatMessage = ({
isFromVisitor = { message.isFromVisitor }
isLobbyMessage = { message.lobbyChat }
message = { message.message }
messageId = { message.messageId }
participantId = { message.participantId } />}
</div>
)}
Expand All @@ -379,6 +437,7 @@ const ChatMessage = ({
<div className = { classes.replyWrapper }>
<div className = { cx('messagecontent', classes.messageContent) }>
{showDisplayName && _renderDisplayName()}
{message.replyToMessageId && _renderReplyPreview()}
<div className = { cx('usermessage', classes.userMessage) }>
{isFileMessage(message) ? (
<FileMessage
Expand Down Expand Up @@ -433,6 +492,7 @@ const ChatMessage = ({
isFromVisitor = { message.isFromVisitor }
isLobbyMessage = { message.lobbyChat }
message = { message.message }
messageId = { message.messageId }
participantId = { message.participantId } />}
</div>
</div>
Expand Down
24 changes: 22 additions & 2 deletions react/features/chat/components/web/MessageMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -22,6 +22,7 @@ export interface IProps {
isFromVisitor?: boolean;
isLobbyMessage: boolean;
message: string;
messageId?: string;
participantId: string;
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 = (
<div className = { classes.menuPanel }>
{!isFileMessage && (
<div
className = { classes.menuItem }
onClick = { handleReplyClick }>
{t('Reply')}
</div>
)}
{enablePrivateChat && (
<div
className = { classes.menuItem }
Expand Down
Loading