styles(chat): a new chat UI

This commit is contained in:
João Victor Nunes 2024-10-16 16:10:27 -03:00 committed by João Victor
parent f91903945d
commit 83514efe58
35 changed files with 1238 additions and 820 deletions

View File

@ -0,0 +1,70 @@
import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { ChatEvents } from '/imports/ui/core/enums/chat';
import Icon from '/imports/ui/components/common/icon/component';
import { Highlighted, Left, Root } from './styles';
const intlMessages = defineMessages({
editing: {
id: 'app.chat.toolbar.edit.editing',
description: '',
},
cancel: {
id: 'app.chat.toolbar.edit.cancel',
description: '',
},
});
const CANCEL_KEY_LABEL = 'esc';
const ChatEditingWarning = () => {
const [show, setShow] = useState(false);
const intl = useIntl();
useEffect(() => {
const handleEditingMessage = (e: Event) => {
if (e instanceof CustomEvent) {
setShow(true);
}
};
const handleCancelEditingMessage = (e: Event) => {
if (e instanceof CustomEvent) {
setShow(false);
}
};
window.addEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleEditingMessage);
window.addEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelEditingMessage);
return () => {
window.removeEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleEditingMessage);
window.removeEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelEditingMessage);
};
}, []);
if (!show) return null;
const cancelMessage = intl.formatMessage(intlMessages.cancel, { key: CANCEL_KEY_LABEL });
const editingMessage = intl.formatMessage(intlMessages.editing);
return (
<Root role="note" aria-describedby="cancel-editing-msg">
<Left>
<Icon iconName="pen_tool" />
{editingMessage}
</Left>
<span>
{cancelMessage.split(CANCEL_KEY_LABEL)[0]}
&nbsp;
<Highlighted>{CANCEL_KEY_LABEL}</Highlighted>
&nbsp;
{cancelMessage.split(CANCEL_KEY_LABEL)[1]}
</span>
<span className="sr-only" id="cancel-editing-msg">
{`${editingMessage} ${cancelMessage}`}
</span>
</Root>
);
};
export default ChatEditingWarning;

View File

@ -0,0 +1,34 @@
import styled from 'styled-components';
import { colorGrayLight } from '/imports/ui/stylesheets/styled-components/palette';
import { xlPadding, xsPadding } from '/imports/ui/stylesheets/styled-components/general';
export const Root = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
color: ${colorGrayLight};
[dir='ltr'] & {
padding-right: ${xlPadding};
}
[dir='rtl'] & {
padding-left: ${xlPadding};
}
`;
export const Highlighted = styled.span`
font-weight: bold;
`;
export const Left = styled.span`
display: flex;
align-items: center;
gap: ${xsPadding};
`;
export default {
Root,
Left,
};

View File

@ -6,6 +6,7 @@ import React, {
useRef, useRef,
useMemo, useMemo,
} from 'react'; } from 'react';
import { useMutation } from '@apollo/client';
import TextareaAutosize from 'react-autosize-textarea'; import TextareaAutosize from 'react-autosize-textarea';
import { ChatFormCommandsEnum } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-commands/chat/form/enums'; import { ChatFormCommandsEnum } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-commands/chat/form/enums';
import { FillChatFormCommandArguments } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-commands/chat/form/types'; import { FillChatFormCommandArguments } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-commands/chat/form/types';
@ -32,13 +33,13 @@ import useMeeting from '/imports/ui/core/hooks/useMeeting';
import ChatOfflineIndicator from './chat-offline-indicator/component'; import ChatOfflineIndicator from './chat-offline-indicator/component';
import { ChatEvents } from '/imports/ui/core/enums/chat'; import { ChatEvents } from '/imports/ui/core/enums/chat';
import { useMutation } from '@apollo/client';
import { CHAT_SEND_MESSAGE, CHAT_SET_TYPING } from './mutations'; import { CHAT_SEND_MESSAGE, CHAT_SET_TYPING } from './mutations';
import Storage from '/imports/ui/services/storage/session'; import Storage from '/imports/ui/services/storage/session';
import { indexOf, without } from '/imports/utils/array-utils'; import { indexOf, without } from '/imports/utils/array-utils';
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook'; import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
import { throttle } from '/imports/utils/throttle'; import { throttle } from '/imports/utils/throttle';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
import { CHAT_EDIT_MESSAGE_MUTATION } from '../chat-message-list/page/chat-message/mutations';
const CLOSED_CHAT_LIST_KEY = 'closedChatList'; const CLOSED_CHAT_LIST_KEY = 'closedChatList';
const START_TYPING_THROTTLE_INTERVAL = 1000; const START_TYPING_THROTTLE_INTERVAL = 1000;
@ -113,6 +114,8 @@ const messages = defineMessages({
}, },
}); });
type EditingMessage = { chatId: string; messageId: string, message: string };
const ChatMessageForm: React.FC<ChatMessageFormProps> = ({ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
title, title,
disabled, disabled,
@ -135,6 +138,7 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
const emojiPickerButtonRef = useRef(null); const emojiPickerButtonRef = useRef(null);
const [isTextAreaFocused, setIsTextAreaFocused] = React.useState(false); const [isTextAreaFocused, setIsTextAreaFocused] = React.useState(false);
const [repliedMessageId, setRepliedMessageId] = React.useState<string>(); const [repliedMessageId, setRepliedMessageId] = React.useState<string>();
const [editingMessage, setEditingMessage] = React.useState<EditingMessage | null>(null);
const textAreaRef: RefObject<TextareaAutosize> = useRef<TextareaAutosize>(null); const textAreaRef: RefObject<TextareaAutosize> = useRef<TextareaAutosize>(null);
const { isMobile } = deviceInfo; const { isMobile } = deviceInfo;
const prevChatId = usePreviousValue(chatId); const prevChatId = usePreviousValue(chatId);
@ -152,6 +156,10 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
const [chatSendMessage, { const [chatSendMessage, {
loading: chatSendMessageLoading, error: chatSendMessageError, loading: chatSendMessageLoading, error: chatSendMessageError,
}] = useMutation(CHAT_SEND_MESSAGE); }] = useMutation(CHAT_SEND_MESSAGE);
const [
chatEditMessage,
{ loading: chatEditMessageLoading },
] = useMutation(CHAT_EDIT_MESSAGE_MUTATION);
const CHAT_CONFIG = window.meetingClientSettings.public.chat; const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id; const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
@ -284,17 +292,37 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
}, [message]); }, [message]);
useEffect(() => { useEffect(() => {
const handler = (e: Event) => { const handleReplyIntention = (e: Event) => {
if (e instanceof CustomEvent) { if (e instanceof CustomEvent) {
setRepliedMessageId(e.detail.messageId); setRepliedMessageId(e.detail.messageId);
textAreaRef.current?.textarea.focus(); textAreaRef.current?.textarea.focus();
} }
}; };
window.addEventListener(ChatEvents.CHAT_REPLY_INTENTION, handler); const handleEditingMessage = (e: Event) => {
if (e instanceof CustomEvent) {
if (textAreaRef.current) {
setMessage(e.detail.message);
setEditingMessage(e.detail);
}
}
};
const handleCancelEditingMessage = (e: Event) => {
if (e instanceof CustomEvent) {
setMessage('');
setEditingMessage(null);
}
};
window.addEventListener(ChatEvents.CHAT_REPLY_INTENTION, handleReplyIntention);
window.addEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleEditingMessage);
window.addEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelEditingMessage);
return () => { return () => {
window.removeEventListener(ChatEvents.CHAT_REPLY_INTENTION, handler); window.removeEventListener(ChatEvents.CHAT_REPLY_INTENTION, handleReplyIntention);
window.removeEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleEditingMessage);
window.removeEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelEditingMessage);
}; };
}, []); }, []);
@ -314,25 +342,30 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
return; return;
} }
if (!chatSendMessageLoading) { if (editingMessage && !chatEditMessageLoading) {
chatEditMessage({
variables: {
chatId: editingMessage.chatId,
messageId: editingMessage.messageId,
chatMessageInMarkdownFormat: msg,
},
}).then(() => {
window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_CANCEL_EDIT_REQUEST),
);
});
} else if (!chatSendMessageLoading) {
chatSendMessage({ chatSendMessage({
variables: { variables: {
chatMessageInMarkdownFormat: msg, chatMessageInMarkdownFormat: msg,
chatId: chatId === PUBLIC_CHAT_ID ? PUBLIC_GROUP_CHAT_ID : chatId, chatId: chatId === PUBLIC_CHAT_ID ? PUBLIC_GROUP_CHAT_ID : chatId,
replyToMessageId: repliedMessageId, replyToMessageId: repliedMessageId,
}, },
}).then(() => {
window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_CANCEL_REPLY_INTENTION),
);
}); });
window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_REPLY_INTENTION, {
detail: {
username: undefined,
message: undefined,
messageId: undefined,
chatId: undefined,
},
}),
);
} }
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY); const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY);
@ -459,65 +492,69 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
</Styled.EmojiPickerWrapper> </Styled.EmojiPickerWrapper>
) : null} ) : null}
<Styled.Wrapper> <Styled.Wrapper>
<Styled.Input <Styled.InputWrapper>
id="message-input" <Styled.Input
ref={textAreaRef} id="message-input"
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: title })} ref={textAreaRef}
aria-label={intl.formatMessage(messages.inputLabel, { 0: title })} placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: title })}
aria-invalid={hasErrors ? 'true' : 'false'} aria-label={intl.formatMessage(messages.inputLabel, { 0: title })}
autoCorrect="off" aria-invalid={hasErrors ? 'true' : 'false'}
autoComplete="off" autoCorrect="off"
spellCheck="true" autoComplete="off"
disabled={disabled || partnerIsLoggedOut} spellCheck="true"
value={message} disabled={disabled || partnerIsLoggedOut}
onFocus={() => { value={message}
window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormUiDataNames.CHAT_INPUT_IS_FOCUSED, { onFocus={() => {
detail: { window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormUiDataNames.CHAT_INPUT_IS_FOCUSED, {
value: true, detail: {
}, value: true,
})); },
setIsTextAreaFocused(true); }));
}} setIsTextAreaFocused(true);
onBlur={() => { }}
window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormUiDataNames.CHAT_INPUT_IS_FOCUSED, { onBlur={() => {
detail: { window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormUiDataNames.CHAT_INPUT_IS_FOCUSED, {
value: false, detail: {
}, value: false,
})); },
}} }));
onChange={handleMessageChange} }}
onKeyDown={handleMessageKeyDown} onChange={handleMessageChange}
onPaste={(e) => { e.stopPropagation(); }} onKeyDown={handleMessageKeyDown}
onCut={(e) => { e.stopPropagation(); }} onPaste={(e) => { e.stopPropagation(); }}
onCopy={(e) => { e.stopPropagation(); }} onCut={(e) => { e.stopPropagation(); }}
async onCopy={(e) => { e.stopPropagation(); }}
/> async
{ENABLE_EMOJI_PICKER ? (
<Styled.EmojiButton
ref={emojiPickerButtonRef}
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
icon="happy"
color="light"
ghost
type="button"
circle
hideLabel
label={intl.formatMessage(messages.emojiButtonLabel)}
data-test="emojiPickerButton"
/> />
) : null} {ENABLE_EMOJI_PICKER ? (
<Styled.SendButton <Styled.EmojiButton
hideLabel ref={emojiPickerButtonRef}
circle onClick={() => setShowEmojiPicker(!showEmojiPicker)}
aria-label={intl.formatMessage(messages.submitLabel)} icon="happy"
type="submit" color="light"
disabled={disabled || partnerIsLoggedOut || chatSendMessageLoading} ghost
label={intl.formatMessage(messages.submitLabel)} type="button"
color="primary" circle
icon="send" hideLabel
onClick={() => { }} label={intl.formatMessage(messages.emojiButtonLabel)}
data-test="sendMessageButton" data-test="emojiPickerButton"
/> />
) : null}
</Styled.InputWrapper>
<div style={{ zIndex: 10 }}>
<Styled.SendButton
hideLabel
circle
aria-label={intl.formatMessage(messages.submitLabel)}
type="submit"
disabled={disabled || partnerIsLoggedOut || chatSendMessageLoading}
label={intl.formatMessage(messages.submitLabel)}
color="primary"
icon="send"
onClick={() => { }}
data-test="sendMessageButton"
/>
</div>
</Styled.Wrapper> </Styled.Wrapper>
{ {
error && ( error && (

View File

@ -1,18 +1,18 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
colorBlueLight, colorBlueLight,
colorText, colorText,
colorGrayLighter, colorGrayLighter,
colorPrimary,
colorDanger, colorDanger,
colorGrayDark, colorGrayDark,
colorGrayLightest,
} from '/imports/ui/stylesheets/styled-components/palette'; } from '/imports/ui/stylesheets/styled-components/palette';
import { import {
smPaddingX, smPaddingX,
smPaddingY, smPaddingY,
borderRadius, borderRadius,
borderSize, borderSize,
xsPadding,
} from '/imports/ui/stylesheets/styled-components/general'; } from '/imports/ui/stylesheets/styled-components/general';
import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography'; import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
import TextareaAutosize from 'react-autosize-textarea'; import TextareaAutosize from 'react-autosize-textarea';
@ -43,13 +43,15 @@ const Form = styled.form<FormProps>`
const Wrapper = styled.div` const Wrapper = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
box-shadow: inset 0 0 0 1px ${colorGrayLightest};
border-radius: 0.75rem;
`; `;
const Input = styled(TextareaAutosize)` const Input = styled(TextareaAutosize)`
flex: 1; flex: 1;
background: #fff; background: #fff;
background-clip: padding-box; background-clip: padding-box;
margin: 0; margin: ${xsPadding} 0 ${xsPadding} ${xsPadding};
color: ${colorText}; color: ${colorText};
-webkit-appearance: none; -webkit-appearance: none;
padding: calc(${smPaddingY} * 2.5) calc(${smPaddingX} * 1.25); padding: calc(${smPaddingY} * 2.5) calc(${smPaddingX} * 1.25);
@ -60,8 +62,17 @@ const Input = styled(TextareaAutosize)`
line-height: 1; line-height: 1;
min-height: 2.5rem; min-height: 2.5rem;
max-height: 10rem; max-height: 10rem;
border: 1px solid ${colorGrayLighter}; border: none;
box-shadow: 0 0 0 1px ${colorGrayLighter}; box-shadow: none;
outline: none;
[dir='ltr'] & {
border-radius: 0.75rem 0 0 0.75rem;
}
[dir='rtl'] & {
border-radius: 0 0.75rem 0.75rem 0;
}
&:disabled, &:disabled,
&[disabled] { &[disabled] {
@ -69,29 +80,22 @@ const Input = styled(TextareaAutosize)`
opacity: .75; opacity: .75;
background-color: rgba(167,179,189,0.25); background-color: rgba(167,179,189,0.25);
} }
&:focus {
border-radius: ${borderSize};
box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary};
}
&:hover,
&:active,
&:focus {
outline: transparent;
outline-style: dotted;
outline-width: ${borderSize};
}
`; `;
// @ts-ignore - as button comes from JS, we can't provide its props // @ts-ignore - as button comes from JS, we can't provide its props
const SendButton = styled(Button)` const SendButton = styled(Button)`
margin:0 0 0 ${smPaddingX};
align-self: center; align-self: center;
font-size: 0.9rem; font-size: 0.9rem;
height: 100%;
& > span {
height: 100%;
display: flex;
align-items: center;
border-radius: 0 0.75rem 0.75rem 0;
}
[dir="rtl"] & { [dir="rtl"] & {
margin: 0 ${smPaddingX} 0 0;
-webkit-transform: scale(-1, 1); -webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1); -moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1); -ms-transform: scale(-1, 1);
@ -161,6 +165,28 @@ const EmojiPicker = styled(EmojiPickerComponent)`
position: relative; position: relative;
`; `;
const InputWrapper = styled.div`
display: flex;
flex-direction: row;
flex-grow: 1;
min-width: 0;
z-index: 0;
[dir='ltr'] & {
border-radius: 0.75rem 0 0 0.75rem;
margin-right: ${xsPadding};
}
[dir='rtl'] & {
border-radius: 0 0.75rem 0.75rem 0;
margin-left: ${xsPadding};
}
&:focus-within {
box-shadow: 0 0 0 ${xsPadding} ${colorBlueLight};
}
`;
export default { export default {
Form, Form,
Wrapper, Wrapper,
@ -171,4 +197,5 @@ export default {
EmojiPicker, EmojiPicker,
EmojiPickerWrapper, EmojiPickerWrapper,
ChatMessageError, ChatMessageError,
InputWrapper,
}; };

View File

@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
useState, useState,
useMemo, useMemo,
KeyboardEventHandler,
} from 'react'; } from 'react';
import { makeVar, useMutation } from '@apollo/client'; import { makeVar, useMutation } from '@apollo/client';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
@ -26,6 +26,8 @@ import {
import useReactiveRef from '/imports/ui/hooks/useReactiveRef'; import useReactiveRef from '/imports/ui/hooks/useReactiveRef';
import useStickyScroll from '/imports/ui/hooks/useStickyScroll'; import useStickyScroll from '/imports/ui/hooks/useStickyScroll';
import ChatReplyIntention from '../chat-reply-intention/component'; import ChatReplyIntention from '../chat-reply-intention/component';
import ChatEditingWarning from '../chat-editing-warning/component';
import KEY_CODES from '/imports/utils/keyCodes';
const PAGE_SIZE = 50; const PAGE_SIZE = 50;
@ -104,6 +106,60 @@ const dispatchLastSeen = () => setTimeout(() => {
} }
}, 500); }, 500);
const roving = (
event: React.KeyboardEvent<HTMLElement>,
changeState: (el: HTMLElement | null) => void,
elementsList: HTMLElement,
element: HTMLElement | null,
) => {
const numberOfChilds = elementsList.childElementCount;
if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.keyCode)) {
changeState(null);
}
if (event.keyCode === KEY_CODES.ARROW_DOWN) {
const firstElement = elementsList.firstChild as HTMLElement;
let elRef = element && numberOfChilds > 1 ? (element.nextSibling as HTMLElement) : firstElement;
while (elRef && elRef.dataset.focusable !== 'true' && elRef.nextSibling) {
elRef = elRef.nextSibling as HTMLElement;
}
elRef = (elRef && elRef.dataset.focusable === 'true') ? elRef : firstElement;
changeState(elRef);
}
if (event.keyCode === KEY_CODES.ARROW_UP) {
const lastElement = elementsList.lastChild as HTMLElement;
let elRef = element ? (element.previousSibling as HTMLElement) : lastElement;
while (elRef && elRef.dataset.focusable !== 'true' && elRef.previousSibling) {
elRef = elRef.previousSibling as HTMLElement;
}
elRef = (elRef && elRef.dataset.focusable === 'true') ? elRef : lastElement;
changeState(elRef);
}
if ([KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event.keyCode)) {
const elRef = document.activeElement?.firstChild as HTMLElement;
changeState(elRef);
}
if ([KEY_CODES.ARROW_RIGHT].includes(event.keyCode)) {
if (element?.dataset) {
const { sequence } = element.dataset;
window.dispatchEvent(new CustomEvent(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_REQUEST, {
detail: {
sequence,
},
}));
}
}
};
const ChatMessageList: React.FC<ChatListProps> = ({ const ChatMessageList: React.FC<ChatListProps> = ({
totalPages, totalPages,
chatId, chatId,
@ -126,6 +182,7 @@ const ChatMessageList: React.FC<ChatListProps> = ({
const [userLoadedBackUntilPage, setUserLoadedBackUntilPage] = useState<number | null>(null); const [userLoadedBackUntilPage, setUserLoadedBackUntilPage] = useState<number | null>(null);
const [lastMessageCreatedAt, setLastMessageCreatedAt] = useState<string>(''); const [lastMessageCreatedAt, setLastMessageCreatedAt] = useState<string>('');
const [followingTail, setFollowingTail] = React.useState(true); const [followingTail, setFollowingTail] = React.useState(true);
const [selectedMessage, setSelectedMessage] = React.useState<HTMLElement | null>(null);
const { const {
childRefProxy: sentinelRefProxy, childRefProxy: sentinelRefProxy,
intersecting: isSentinelVisible, intersecting: isSentinelVisible,
@ -259,6 +316,17 @@ const ChatMessageList: React.FC<ChatListProps> = ({
: Math.max(totalPages - 2, 0); : Math.max(totalPages - 2, 0);
const pagesToLoad = (totalPages - firstPageToLoad) || 1; const pagesToLoad = (totalPages - firstPageToLoad) || 1;
const rove: KeyboardEventHandler<HTMLElement> = (e) => {
if (messageListRef.current) {
roving(
e,
setSelectedMessage,
messageListRef.current,
selectedMessage,
);
}
};
return ( return (
<> <>
{ {
@ -276,7 +344,15 @@ const ChatMessageList: React.FC<ChatListProps> = ({
isRTL={isRTL} isRTL={isRTL}
ref={messageListContainerRefProxy} ref={messageListContainerRefProxy}
> >
<div ref={messageListRef}> <div
role="listbox"
ref={messageListRef}
tabIndex={0}
onKeyDown={rove}
onBlur={() => {
setSelectedMessage(null);
}}
>
{userLoadedBackUntilPage ? ( {userLoadedBackUntilPage ? (
<ButtonLoadMore <ButtonLoadMore
onClick={() => { onClick={() => {
@ -301,6 +377,9 @@ const ChatMessageList: React.FC<ChatListProps> = ({
chatId={chatId} chatId={chatId}
markMessageAsSeen={markMessageAsSeen} markMessageAsSeen={markMessageAsSeen}
scrollRef={messageListContainerRefProxy} scrollRef={messageListContainerRefProxy}
focusedId={selectedMessage?.dataset.sequence
? Number.parseInt(selectedMessage?.dataset.sequence, 10)
: null}
/> />
); );
})} })}
@ -311,10 +390,13 @@ const ChatMessageList: React.FC<ChatListProps> = ({
height: 1, height: 1,
background: 'none', background: 'none',
}} }}
tabIndex={-1}
aria-hidden
/> />
</MessageList>, </MessageList>,
renderUnreadNotification, renderUnreadNotification,
<ChatReplyIntention />, <ChatReplyIntention key="chatReplyIntention" />,
<ChatEditingWarning key="chatEditingWarning" />,
] ]
} }
</> </>

View File

@ -19,7 +19,9 @@ import {
MessageItemWrapper, MessageItemWrapper,
Container, Container,
DeleteMessage, DeleteMessage,
ChatEditTime, ChatHeading,
EditLabel,
EditLabelWrapper,
} from './styles'; } from './styles';
import { ChatEvents, ChatMessageType } from '/imports/ui/core/enums/chat'; import { ChatEvents, ChatMessageType } from '/imports/ui/core/enums/chat';
import MessageReadConfirmation from './message-read-confirmation/component'; import MessageReadConfirmation from './message-read-confirmation/component';
@ -27,7 +29,6 @@ import ChatMessageToolbar from './message-toolbar/component';
import ChatMessageReactions from './message-reactions/component'; import ChatMessageReactions from './message-reactions/component';
import ChatMessageReplied from './message-replied/component'; import ChatMessageReplied from './message-replied/component';
import { STORAGES, useStorageKey } from '/imports/ui/services/storage/hooks'; import { STORAGES, useStorageKey } from '/imports/ui/services/storage/hooks';
import ChatEditMessageForm from './message-edit-form/component';
import useMeeting from '/imports/ui/core/hooks/useMeeting'; import useMeeting from '/imports/ui/core/hooks/useMeeting';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import { layoutSelect } from '/imports/ui/components/layout/context'; import { layoutSelect } from '/imports/ui/components/layout/context';
@ -36,6 +37,15 @@ import useChat from '/imports/ui/core/hooks/useChat';
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook'; import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
import { Chat } from '/imports/ui/Types/chat'; import { Chat } from '/imports/ui/Types/chat';
import { CHAT_DELETE_REACTION_MUTATION, CHAT_SEND_REACTION_MUTATION } from './mutations'; import { CHAT_DELETE_REACTION_MUTATION, CHAT_SEND_REACTION_MUTATION } from './mutations';
import Icon from '/imports/ui/components/common/icon/component';
import { colorBlueLightestChannel } from '/imports/ui/stylesheets/styled-components/palette';
import {
useIsReplyChatMessageEnabled,
useIsChatMessageReactionsEnabled,
useIsEditChatMessageEnabled,
useIsDeleteChatMessageEnabled,
} from '/imports/ui/services/features';
import ChatMessageNotificationContent from './message-content/notification-content/component';
interface ChatMessageProps { interface ChatMessageProps {
message: Message; message: Message;
@ -45,6 +55,8 @@ interface ChatMessageProps {
scrollRef: React.RefObject<HTMLDivElement>; scrollRef: React.RefObject<HTMLDivElement>;
markMessageAsSeen: (message: Message) => void; markMessageAsSeen: (message: Message) => void;
messageReadFeedbackEnabled: boolean; messageReadFeedbackEnabled: boolean;
focused: boolean;
keyboardFocused: boolean;
} }
const intlMessages = defineMessages({ const intlMessages = defineMessages({
@ -76,6 +88,10 @@ const intlMessages = defineMessages({
id: 'app.chat.deleteMessage', id: 'app.chat.deleteMessage',
description: '', description: '',
}, },
edited: {
id: 'app.chat.toolbar.edit.edited',
description: 'edited message label',
},
}); });
function isInViewport(el: HTMLDivElement) { function isInViewport(el: HTMLDivElement) {
@ -98,6 +114,8 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
setMessagesRequestedFromPlugin, setMessagesRequestedFromPlugin,
markMessageAsSeen, markMessageAsSeen,
messageReadFeedbackEnabled, messageReadFeedbackEnabled,
focused,
keyboardFocused,
}) => { }) => {
const idChatOpen: string = layoutSelect((i: Layout) => i.idChatOpen); const idChatOpen: string = layoutSelect((i: Layout) => i.idChatOpen);
const { data: meeting } = useMeeting((m) => ({ const { data: meeting } = useMeeting((m) => ({
@ -122,7 +140,6 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
}, [message, messageRef]); }, [message, messageRef]);
const messageContentRef = React.createRef<HTMLDivElement>(); const messageContentRef = React.createRef<HTMLDivElement>();
const [editing, setEditing] = React.useState(false); const [editing, setEditing] = React.useState(false);
const [isToolbarMenuOpen, setIsToolbarMenuOpen] = React.useState(false);
const [isToolbarReactionPopoverOpen, setIsToolbarReactionPopoverOpen] = React.useState(false); const [isToolbarReactionPopoverOpen, setIsToolbarReactionPopoverOpen] = React.useState(false);
const chatFocusMessageRequest = useStorageKey(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, STORAGES.IN_MEMORY); const chatFocusMessageRequest = useStorageKey(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, STORAGES.IN_MEMORY);
const containerRef = React.useRef<HTMLDivElement>(null); const containerRef = React.useRef<HTMLDivElement>(null);
@ -150,7 +167,6 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
}); });
}, []); }, []);
const CHAT_TOOLBAR_CONFIG = window.meetingClientSettings.public.chat.toolbar;
const isModerator = currentUser?.isModerator; const isModerator = currentUser?.isModerator;
const isPublicChat = chat?.public; const isPublicChat = chat?.public;
const isLocked = currentUser?.locked || currentUser?.userLockSettings?.disablePublicChat; const isLocked = currentUser?.locked || currentUser?.userLockSettings?.disablePublicChat;
@ -168,11 +184,17 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
} }
} }
const hasToolbar = CHAT_TOOLBAR_CONFIG.length > 0 const CHAT_REPLY_ENABLED = useIsReplyChatMessageEnabled();
&& message.deletedAt === null const CHAT_REACTIONS_ENABLED = useIsChatMessageReactionsEnabled();
&& !editing const CHAT_EDIT_ENABLED = useIsEditChatMessageEnabled();
&& !!message.user const CHAT_DELETE_ENABLED = useIsDeleteChatMessageEnabled();
&& !locked;
const hasToolbar = !!message.user && [
CHAT_REPLY_ENABLED,
CHAT_REACTIONS_ENABLED,
CHAT_EDIT_ENABLED,
CHAT_DELETE_ENABLED,
].some((config) => config);
const startScrollAnimation = (timestamp: number) => { const startScrollAnimation = (timestamp: number) => {
if (scrollRef.current && containerRef.current) { if (scrollRef.current && containerRef.current) {
@ -184,7 +206,7 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
}; };
useEffect(() => { useEffect(() => {
const handler = (e: Event) => { const handleFocusMessageRequest = (e: Event) => {
if (e instanceof CustomEvent) { if (e instanceof CustomEvent) {
if (e.detail.sequence === message.messageSequence) { if (e.detail.sequence === message.messageSequence) {
requestAnimationFrame(startScrollAnimation); requestAnimationFrame(startScrollAnimation);
@ -192,10 +214,27 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
} }
}; };
window.addEventListener(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, handler); const handleChatEditRequest = (e: Event) => {
if (e instanceof CustomEvent) {
const editing = e.detail.messageId === message.messageId;
setEditing(editing);
}
};
const handleCancelChatEditRequest = (e: Event) => {
if (e instanceof CustomEvent) {
setEditing(false);
}
};
window.addEventListener(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, handleFocusMessageRequest);
window.addEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleChatEditRequest);
window.addEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelChatEditRequest);
return () => { return () => {
window.removeEventListener(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, handler); window.removeEventListener(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, handleFocusMessageRequest);
window.removeEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleChatEditRequest);
window.removeEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelChatEditRequest);
}; };
}, []); }, []);
@ -209,7 +248,7 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
if (!containerRef.current) return; if (!containerRef.current) return;
const value = (timestamp - animationInitialTimestamp.current) / ANIMATION_DURATION; const value = (timestamp - animationInitialTimestamp.current) / ANIMATION_DURATION;
if (value < 1) { if (value < 1) {
containerRef.current.style.backgroundColor = `rgba(243, 246, 249, ${1 - value})`; containerRef.current.style.backgroundColor = `rgb(${colorBlueLightestChannel} / ${1 - value})`;
requestAnimationFrame(animate); requestAnimationFrame(animate);
} else { } else {
containerRef.current.style.backgroundColor = 'unset'; containerRef.current.style.backgroundColor = 'unset';
@ -258,6 +297,7 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
const formattedTime = intl.formatTime(dateTime, { const formattedTime = intl.formatTime(dateTime, {
hour: 'numeric', hour: 'numeric',
minute: 'numeric', minute: 'numeric',
hour12: false,
}); });
const editTime = message.editedAt ? new Date(message.editedAt) : null; const editTime = message.editedAt ? new Date(message.editedAt) : null;
const deleteTime = message.deletedAt ? new Date(message.deletedAt) : null; const deleteTime = message.deletedAt ? new Date(message.deletedAt) : null;
@ -266,12 +306,15 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
const clearMessage = `${msgTime} ${intl.formatMessage(intlMessages.chatClear)}`; const clearMessage = `${msgTime} ${intl.formatMessage(intlMessages.chatClear)}`;
const messageContent: { const messageContent: {
name: string, name: string;
color: string, color: string;
isModerator: boolean, isModerator: boolean;
isPresentationUpload?: boolean, isPresentationUpload?: boolean;
component: React.ReactElement, component: React.ReactNode;
avatarIcon?: string, avatarIcon?: string;
isSystemSender?: boolean;
showAvatar: boolean;
showHeading: boolean;
} = useMemo(() => { } = useMemo(() => {
switch (message.messageType) { switch (message.messageType) {
case ChatMessageType.POLL: case ChatMessageType.POLL:
@ -283,6 +326,8 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
<ChatPollContent metadata={message.messageMetadata} /> <ChatPollContent metadata={message.messageMetadata} />
), ),
avatarIcon: 'icon-bbb-polling', avatarIcon: 'icon-bbb-polling',
showAvatar: true,
showHeading: true,
}; };
case ChatMessageType.PRESENTATION: case ChatMessageType.PRESENTATION:
return { return {
@ -296,6 +341,8 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
/> />
), ),
avatarIcon: 'icon-bbb-download', avatarIcon: 'icon-bbb-download',
showAvatar: true,
showHeading: true,
}; };
case ChatMessageType.CHAT_CLEAR: case ChatMessageType.CHAT_CLEAR:
return { return {
@ -304,26 +351,28 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
isModerator: false, isModerator: false,
isSystemSender: true, isSystemSender: true,
component: ( component: (
<ChatMessageTextContent <ChatMessageNotificationContent
emphasizedMessage={false} iconName="delete"
text={clearMessage} text={clearMessage}
systemMsg
/> />
), ),
showAvatar: false,
showHeading: false,
}; };
case ChatMessageType.BREAKOUT_ROOM: case ChatMessageType.BREAKOUT_ROOM:
return { return {
name: message.senderName, name: message.senderName,
color: '#0F70D7', color: '#0F70D7',
isModerator: true, isModerator: true,
isSystemSender: true, isSystemSender: false,
component: ( component: (
<ChatMessageTextContent <ChatMessageTextContent
systemMsg={false}
emphasizedMessage emphasizedMessage
text={message.message} text={message.message}
/> />
), ),
showAvatar: true,
showHeading: true,
}; };
case ChatMessageType.API: case ChatMessageType.API:
return { return {
@ -332,30 +381,31 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
isModerator: true, isModerator: true,
isSystemSender: true, isSystemSender: true,
component: ( component: (
<ChatMessageTextContent <ChatMessageNotificationContent
systemMsg
emphasizedMessage
text={message.message} text={message.message}
/> />
), ),
showAvatar: false,
showHeading: false,
}; };
case ChatMessageType.USER_AWAY_STATUS_MSG: { case ChatMessageType.USER_AWAY_STATUS_MSG: {
const { away } = JSON.parse(message.messageMetadata); const { away } = JSON.parse(message.messageMetadata);
const awayMessage = (away) const awayMessage = (away)
? `${intl.formatMessage(intlMessages.userAway)}` ? intl.formatMessage(intlMessages.userAway, { user: message.senderName })
: `${intl.formatMessage(intlMessages.userNotAway)}`; : intl.formatMessage(intlMessages.userNotAway, { user: message.senderName });
return { return {
name: message.senderName, name: message.senderName,
color: '#0F70D7', color: '#0F70D7',
isModerator: true, isModerator: true,
isSystemSender: true, isSystemSender: true,
component: ( component: (
<ChatMessageTextContent <ChatMessageNotificationContent
emphasizedMessage={false} iconName="time"
text={awayMessage} text={awayMessage}
systemMsg
/> />
), ),
showAvatar: false,
showHeading: false,
}; };
} }
case ChatMessageType.PLUGIN: { case ChatMessageType.PLUGIN: {
@ -364,13 +414,14 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
color: message.user?.color, color: message.user?.color,
isModerator: message.user?.isModerator, isModerator: message.user?.isModerator,
isSystemSender: false, isSystemSender: false,
showAvatar: true,
showHeading: true,
component: currentPluginMessageMetadata.custom component: currentPluginMessageMetadata.custom
? (<></>) ? null
: ( : (
<ChatMessageTextContent <ChatMessageTextContent
emphasizedMessage={message.chatEmphasizedText} emphasizedMessage={message.chatEmphasizedText}
text={message.message} text={message.message}
systemMsg={false}
/> />
), ),
}; };
@ -381,104 +432,115 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
name: message.user?.name, name: message.user?.name,
color: message.user?.color, color: message.user?.color,
isModerator: message.user?.isModerator, isModerator: message.user?.isModerator,
isSystemSender: ChatMessageType.BREAKOUT_ROOM, isSystemSender: false,
showAvatar: true,
showHeading: true,
component: ( component: (
<ChatMessageTextContent <ChatMessageTextContent
emphasizedMessage={message.chatEmphasizedText} emphasizedMessage={message.chatEmphasizedText}
text={message.message} text={message.message}
systemMsg={false}
/> />
), ),
}; };
} }
}, [message.message]); }, [message.message]);
const shouldRenderAvatar = messageContent.showAvatar
&& !sameSender
&& !isCustomPluginMessage;
const shouldRenderHeader = messageContent.showHeading
&& !sameSender
&& !isCustomPluginMessage;
return ( return (
<Container ref={containerRef}> <Container
ref={containerRef}
$sequence={message.messageSequence}
data-sequence={message.messageSequence}
data-focusable={!deleteTime && !messageContent.isSystemSender}
>
<ChatWrapper <ChatWrapper
id="chat-message-wrapper" className={`chat-message-wrapper ${focused ? 'chat-message-wrapper-focused' : ''} ${keyboardFocused ? 'chat-message-wrapper-keyboard-focused' : ''}`}
isSystemSender={isSystemSender} isSystemSender={isSystemSender}
sameSender={sameSender} sameSender={sameSender}
ref={messageRef} ref={messageRef}
isPresentationUpload={messageContent.isPresentationUpload} isPresentationUpload={messageContent.isPresentationUpload}
isCustomPluginMessage={isCustomPluginMessage} isCustomPluginMessage={isCustomPluginMessage}
$highlight={hasToolbar}
$toolbarMenuIsOpen={isToolbarMenuOpen}
$reactionPopoverIsOpen={isToolbarReactionPopoverOpen}
> >
{hasToolbar && ( <ChatMessageToolbar
<ChatMessageToolbar keyboardFocused={keyboardFocused}
messageId={message.messageId} hasToolbar={hasToolbar}
chatId={message.chatId} locked={locked}
username={message.user.name} deleted={!!deleteTime}
own={message.user.userId === currentUser?.userId} messageId={message.messageId}
amIModerator={Boolean(currentUser?.isModerator)} chatId={message.chatId}
message={message.message} username={message.user?.name}
messageSequence={message.messageSequence} own={message.user?.userId === currentUser?.userId}
emphasizedMessage={message.chatEmphasizedText} amIModerator={Boolean(currentUser?.isModerator)}
onEmojiSelected={(emoji) => { message={message.message}
sendReaction(emoji.native); messageSequence={message.messageSequence}
setIsToolbarReactionPopoverOpen(false); emphasizedMessage={message.chatEmphasizedText}
}} onEmojiSelected={(emoji) => {
onEditRequest={() => { sendReaction(emoji.native);
setEditing(true); setIsToolbarReactionPopoverOpen(false);
}} }}
onMenuOpenChange={setIsToolbarMenuOpen} onReactionPopoverOpenChange={setIsToolbarReactionPopoverOpen}
menuIsOpen={isToolbarMenuOpen} reactionPopoverIsOpen={isToolbarReactionPopoverOpen}
onReactionPopoverOpenChange={setIsToolbarReactionPopoverOpen} chatDeleteEnabled={CHAT_DELETE_ENABLED}
reactionPopoverIsOpen={isToolbarReactionPopoverOpen} chatEditEnabled={CHAT_EDIT_ENABLED}
/> chatReactionsEnabled={CHAT_REACTIONS_ENABLED}
)} chatReplyEnabled={CHAT_REPLY_ENABLED}
{((!message?.user || !sameSender) && ( />
message.messageType !== ChatMessageType.USER_AWAY_STATUS_MSG {(shouldRenderAvatar || shouldRenderHeader) && (
&& message.messageType !== ChatMessageType.API <ChatHeading>
&& message.messageType !== ChatMessageType.CHAT_CLEAR {shouldRenderAvatar && (
&& !isCustomPluginMessage) <ChatAvatar
) && ( avatar={message.user?.avatar}
<ChatAvatar color={messageContent.color}
avatar={message.user?.avatar} moderator={messageContent.isModerator}
color={messageContent.color} >
moderator={messageContent.isModerator} {!messageContent.avatarIcon ? (
> !message.user || (message.user?.avatar.length === 0 ? messageContent.name.toLowerCase().slice(0, 2) : '')
{!messageContent.avatarIcon ? ( ) : (
!message.user || (message.user?.avatar.length === 0 ? messageContent.name.toLowerCase().slice(0, 2) : '') <i className={messageContent.avatarIcon} />
) : ( )}
<i className={messageContent.avatarIcon} /> </ChatAvatar>
)} )}
</ChatAvatar> {shouldRenderHeader && (
)}
{!editing && (
<ChatContent
ref={messageContentRef}
sameSender={message?.user ? sameSender : false}
isCustomPluginMessage={isCustomPluginMessage}
data-chat-message-id={message?.messageId}
$highlight={hasToolbar}
$toolbarMenuIsOpen={isToolbarMenuOpen}
$reactionPopoverIsOpen={isToolbarReactionPopoverOpen}
>
{message.messageType !== ChatMessageType.CHAT_CLEAR
&& !isCustomPluginMessage
&& (
<ChatMessageHeader <ChatMessageHeader
sameSender={message?.user ? sameSender : false} sameSender={message?.user ? sameSender : false}
name={messageContent.name} name={messageContent.name}
currentlyInMeeting={message.user?.currentlyInMeeting ?? true} currentlyInMeeting={message.user?.currentlyInMeeting ?? true}
dateTime={dateTime} dateTime={dateTime}
deleteTime={deleteTime} deleteTime={deleteTime}
editTime={editTime}
/> />
)} )}
{message.replyToMessage && ( </ChatHeading>
)}
<ChatContent
className="chat-message-content"
ref={messageContentRef}
sameSender={message?.user ? sameSender : false}
isCustomPluginMessage={isCustomPluginMessage}
data-chat-message-id={message?.messageId}
$highlight={hasToolbar && !deleteTime}
$editing={editing}
$focused={focused}
$keyboardFocused={keyboardFocused}
$reactionPopoverIsOpen={isToolbarReactionPopoverOpen}
>
{message.replyToMessage && !deleteTime && (
<ChatMessageReplied <ChatMessageReplied
message={message.replyToMessage.message} message={message.replyToMessage.message}
username={message.replyToMessage.user.name}
sequence={message.replyToMessage.messageSequence} sequence={message.replyToMessage.messageSequence}
userColor={message.replyToMessage.user.color}
emphasizedMessage={message.replyToMessage.chatEmphasizedText} emphasizedMessage={message.replyToMessage.chatEmphasizedText}
deletedByUser={message.replyToMessage.deletedBy?.name ?? null} deletedByUser={message.replyToMessage.deletedBy?.name ?? null}
/> />
)} )}
{!deleteTime && ( {!deleteTime && (
<MessageItemWrapper> <MessageItemWrapper $edited={!!editTime} $sameSender={sameSender}>
{messageContent.component} {messageContent.component}
{messageReadFeedbackEnabled && ( {messageReadFeedbackEnabled && (
<MessageReadConfirmation <MessageReadConfirmation
@ -487,34 +549,26 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
)} )}
</MessageItemWrapper> </MessageItemWrapper>
)} )}
{!deleteTime && editTime && ( {!deleteTime && editTime && sameSender && (
<ChatEditTime> <EditLabelWrapper>
{`(${intl.formatMessage(intlMessages.editTime, { 0: intl.formatTime(editTime) })})`} <EditLabel>
</ChatEditTime> <Icon iconName="pen_tool" />
<span>{intl.formatMessage(intlMessages.edited)}</span>
</EditLabel>
</EditLabelWrapper>
)} )}
{deleteTime && ( {deleteTime && (
<DeleteMessage> <DeleteMessage>
{intl.formatMessage(intlMessages.deleteMessage, { 0: message.deletedBy?.name })} {intl.formatMessage(intlMessages.deleteMessage, { 0: message.deletedBy?.name })}
</DeleteMessage> </DeleteMessage>
)} )}
{!deleteTime && (
<ChatMessageReactions
reactions={message.reactions}
deleteReaction={deleteReaction}
sendReaction={sendReaction}
/>
)}
</ChatContent> </ChatContent>
)} {!deleteTime && (
{editing && ( <ChatMessageReactions
<ChatEditMessageForm reactions={message.reactions}
chatId={message.chatId} deleteReaction={deleteReaction}
initialMessage={message.message} sendReaction={sendReaction}
messageId={message.messageId} />
onCancel={() => setEditing(false)}
onAfterSubmit={() => setEditing(false)}
sameSender={message?.user ? sameSender : false}
/>
)} )}
</ChatWrapper> </ChatWrapper>
</Container> </Container>
@ -528,7 +582,9 @@ function areChatMessagesEqual(prevProps: ChatMessageProps, nextProps: ChatMessag
&& prevMessage?.user?.currentlyInMeeting === nextMessage?.user?.currentlyInMeeting && prevMessage?.user?.currentlyInMeeting === nextMessage?.user?.currentlyInMeeting
&& prevMessage?.recipientHasSeen === nextMessage.recipientHasSeen && prevMessage?.recipientHasSeen === nextMessage.recipientHasSeen
&& prevMessage?.message === nextMessage.message && prevMessage?.message === nextMessage.message
&& prevMessage?.reactions?.length === nextMessage?.reactions?.length; && prevMessage?.reactions?.length === nextMessage?.reactions?.length
&& prevProps.focused === nextProps.focused
&& prevProps.keyboardFocused === nextProps.keyboardFocused;
} }
export default memo(ChatMesssage, areChatMessagesEqual); export default memo(ChatMesssage, areChatMessagesEqual);

View File

@ -0,0 +1,21 @@
import React from 'react';
import Styled from './styles';
interface ChatMessageNotificationContentProps {
text: string;
iconName?: string;
}
const ChatMessageNotificationContent: React.FC<ChatMessageNotificationContentProps> = (props) => {
const { text, iconName } = props;
return (
<Styled.Root>
{iconName && <Styled.Icon iconName={iconName} />}
<Styled.Typography>
{text}
</Styled.Typography>
</Styled.Root>
);
};
export default ChatMessageNotificationContent;

View File

@ -0,0 +1,35 @@
import styled from 'styled-components';
import BaseIcon from '/imports/ui/components/common/icon/component';
import { colorGray } from '/imports/ui/stylesheets/styled-components/palette';
import { $3xlPadding, smPadding } from '/imports/ui/stylesheets/styled-components/general';
export const Root = styled.div`
color: ${colorGray};
padding: 0 ${$3xlPadding};
width: 100%;
text-align: center;
`;
export const Icon = styled(BaseIcon)`
vertical-align: baseline;
[dir='ltr'] & {
margin-right: ${smPadding};
}
[dir='rtl'] & {
margin-left: ${smPadding};
}
`;
export const Typography = styled.span`
vertical-align: baseline;
overflow-wrap: break-word;
white-space: pre-wrap;
`;
export default {
Root,
Icon,
Typography,
};

View File

@ -5,19 +5,17 @@ import Styled from './styles';
interface ChatMessageTextContentProps { interface ChatMessageTextContentProps {
text: string; text: string;
emphasizedMessage: boolean; emphasizedMessage: boolean;
systemMsg: boolean;
dataTest?: string | null; dataTest?: string | null;
} }
const ChatMessageTextContent: React.FC<ChatMessageTextContentProps> = ({ const ChatMessageTextContent: React.FC<ChatMessageTextContentProps> = ({
text, text,
emphasizedMessage, emphasizedMessage,
systemMsg,
dataTest = 'messageContent', dataTest = 'messageContent',
}) => { }) => {
const { allowedElements } = window.meetingClientSettings.public.chat; const { allowedElements } = window.meetingClientSettings.public.chat;
return ( return (
<Styled.ChatMessage systemMsg={systemMsg} emphasizedMessage={emphasizedMessage} data-test={dataTest}> <Styled.ChatMessage emphasizedMessage={emphasizedMessage} data-test={dataTest}>
<ReactMarkdown <ReactMarkdown
linkTarget="_blank" linkTarget="_blank"
allowedElements={allowedElements} allowedElements={allowedElements}

View File

@ -1,11 +1,5 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { import { colorText } from '/imports/ui/stylesheets/styled-components/palette';
systemMessageBackgroundColor,
systemMessageBorderColor,
colorText,
} from '/imports/ui/stylesheets/styled-components/palette';
import { borderRadius } from '/imports/ui/stylesheets/styled-components/general';
import { fontSizeBase, btnFontWeight } from '/imports/ui/stylesheets/styled-components/typography';
interface ChatMessageProps { interface ChatMessageProps {
emphasizedMessage: boolean; emphasizedMessage: boolean;
@ -19,17 +13,7 @@ export const ChatMessage = styled.div<ChatMessageProps>`
flex-direction: column; flex-direction: column;
color: ${colorText}; color: ${colorText};
word-break: break-word; word-break: break-word;
${({ systemMsg }) => systemMsg && `
background: ${systemMessageBackgroundColor};
border: 1px solid ${systemMessageBorderColor};
border-radius: ${borderRadius};
font-weight: ${btnFontWeight};
padding: ${fontSizeBase};
text-color: #1f252b;
margin-top: 0;
margin-bottom: 0;
overflow-wrap: break-word;
`}
${({ emphasizedMessage }) => emphasizedMessage && ` ${({ emphasizedMessage }) => emphasizedMessage && `
font-weight: bold; font-weight: bold;
`} `}

View File

@ -1,160 +0,0 @@
import React, { useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { checkText } from 'smile2emoji';
import { useMutation } from '@apollo/client';
import Button from '/imports/ui/components/common/button/component';
import { textToMarkdown } from '/imports/ui/components/chat/chat-graphql/chat-message-form/service';
import { CHAT_EDIT_MESSAGE_MUTATION } from '../mutations';
import Styled from './styles';
import logger from '/imports/startup/client/logger';
const intlMessages = defineMessages({
errorMaxMessageLength: {
id: 'app.chat.errorMaxMessageLength',
},
errorOnUpdateMessage: {
id: 'app.chat.errorOnUpdateMessage',
},
});
interface ChatEditMessageFormProps {
onCancel(): void;
onAfterSubmit(): void;
initialMessage: string;
chatId: string;
messageId: string;
sameSender: boolean;
}
const ChatEditMessageForm: React.FC<ChatEditMessageFormProps> = (props) => {
const {
onCancel, chatId, initialMessage, messageId, onAfterSubmit, sameSender,
} = props;
const [editedMessage, setEditedMessage] = React.useState(initialMessage);
const [hasErrors, setHasErrors] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const intl = useIntl();
const [chatEditMessage, {
loading: chatEditMessageLoading, error: chatEditMessageError,
}] = useMutation(CHAT_EDIT_MESSAGE_MUTATION);
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const AUTO_CONVERT_EMOJI = CHAT_CONFIG.autoConvertEmoji;
const MIN_MESSAGE_LENGTH = CHAT_CONFIG.min_message_length;
const MAX_MESSAGE_LENGTH = CHAT_CONFIG.max_message_length;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
useEffect(() => {
if (chatEditMessageError && error == null) {
logger.error({
logCode: 'update_message_error',
extraInfo: {
errorName: chatEditMessageError.name,
errorMessage: chatEditMessageError.message,
},
}, 'Updating chat message failed');
setError(intl.formatMessage(intlMessages.errorOnUpdateMessage));
}
}, [chatEditMessageError]);
const handleMessageChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
let newMessage = null;
let newError = null;
if (AUTO_CONVERT_EMOJI) {
newMessage = checkText(e.target.value);
} else {
newMessage = e.target.value;
}
if (newMessage.length > MAX_MESSAGE_LENGTH) {
newError = intl.formatMessage(
intlMessages.errorMaxMessageLength,
{ 0: MAX_MESSAGE_LENGTH },
);
newMessage = newMessage.substring(0, MAX_MESSAGE_LENGTH);
}
setEditedMessage(newMessage);
setError(newError);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement> | React.KeyboardEvent<HTMLInputElement> | Event) => {
e.preventDefault();
const msg = textToMarkdown(editedMessage);
if (msg.length < MIN_MESSAGE_LENGTH || chatEditMessageLoading) return;
if (msg.length > MAX_MESSAGE_LENGTH) {
setHasErrors(true);
return;
}
if (!chatEditMessageLoading) {
chatEditMessage({
variables: {
chatMessageInMarkdownFormat: msg,
chatId: chatId === PUBLIC_CHAT_ID ? PUBLIC_GROUP_CHAT_ID : chatId,
messageId,
},
});
}
setError(null);
setHasErrors(false);
onAfterSubmit();
};
const handleMessageKeyDown: React.KeyboardEventHandler<HTMLInputElement> = (e) => {
if (e.code === 'Enter' && !e.shiftKey) {
e.preventDefault();
const event = new Event('submit', {
bubbles: true,
cancelable: true,
});
handleSubmit(event);
}
};
return (
<form onSubmit={handleSubmit} style={{ width: '100%' }}>
<Styled.Section $sameSender={sameSender}>
<div>
<input
aria-invalid={hasErrors ? 'true' : 'false'}
value={editedMessage}
onChange={handleMessageChange}
onKeyDown={handleMessageKeyDown}
onClick={(e) => e.stopPropagation()}
style={{
width: '100%',
minWidth: 0,
}}
/>
{error && (
<Styled.ChatMessageError data-test="updateMessageIndicatorError">
{error}
</Styled.ChatMessageError>
)}
</div>
<Styled.Actions>
<Button
label="Cancel"
onClick={onCancel}
/>
<Button
label="Save"
color="primary"
type="submit"
onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => e.stopPropagation()}
/>
</Styled.Actions>
</Styled.Section>
</form>
);
};
export default ChatEditMessageForm;

View File

@ -1,34 +0,0 @@
import styled from 'styled-components';
import { borderSize } from '/imports/ui/stylesheets/styled-components/general';
import { colorDanger, colorGrayDark } from '/imports/ui/stylesheets/styled-components/palette';
import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
const Actions = styled.div`
display: flex;
justify-content: flex-end;
padding-top: 8px;
`;
const Section = styled.section<{ $sameSender: boolean }>`
${({ $sameSender }) => $sameSender && `
margin-left: 2.6rem;
`}
`;
const ChatMessageError = styled.div`
color: ${colorDanger};
font-size: calc(${fontSizeBase} * .75);
color: ${colorGrayDark};
text-align: left;
padding: ${borderSize} 0;
word-break: break-word;
position: relative;
margin-right: 0.05rem;
margin-left: 0.05rem;
`;
export default {
Actions,
Section,
ChatMessageError,
};

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { useIntl, defineMessages, FormattedTime } from 'react-intl'; import { useIntl, defineMessages, FormattedTime } from 'react-intl';
import Icon from '/imports/ui/components/common/icon/component';
import Styled from './styles'; import Styled from './styles';
const intlMessages = defineMessages({ const intlMessages = defineMessages({
@ -7,6 +8,10 @@ const intlMessages = defineMessages({
id: 'app.chat.offline', id: 'app.chat.offline',
description: 'Offline', description: 'Offline',
}, },
edited: {
id: 'app.chat.toolbar.edit.edited',
description: 'Edited',
},
}); });
interface ChatMessageHeaderProps { interface ChatMessageHeaderProps {
@ -15,6 +20,7 @@ interface ChatMessageHeaderProps {
dateTime: Date; dateTime: Date;
sameSender: boolean; sameSender: boolean;
deleteTime: Date | null; deleteTime: Date | null;
editTime: Date | null;
} }
const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({ const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
@ -23,6 +29,7 @@ const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
currentlyInMeeting, currentlyInMeeting,
dateTime, dateTime,
deleteTime, deleteTime,
editTime,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
if (sameSender) return null; if (sameSender) return null;
@ -40,11 +47,20 @@ const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
</Styled.ChatUserOffline> </Styled.ChatUserOffline>
) )
} }
{!deleteTime && ( {!deleteTime && editTime && (
<Styled.ChatTime> <Styled.EditLabel>
<FormattedTime value={dateTime} /> <Icon iconName="pen_tool" />
</Styled.ChatTime> <span>{intl.formatMessage(intlMessages.edited)}</span>
</Styled.EditLabel>
)} )}
{deleteTime && (
<Styled.EditLabel>
<Icon iconName="delete" />
</Styled.EditLabel>
)}
<Styled.ChatTime>
<FormattedTime value={dateTime} hour12={false} />
</Styled.ChatTime>
</Styled.ChatHeaderText> </Styled.ChatHeaderText>
</Styled.HeaderContent> </Styled.HeaderContent>
); );

View File

@ -2,10 +2,10 @@ import styled from 'styled-components';
import { import {
colorHeading, colorHeading,
palettePlaceholderText,
colorGrayLight, colorGrayLight,
colorGrayDark,
} from '/imports/ui/stylesheets/styled-components/palette'; } from '/imports/ui/stylesheets/styled-components/palette';
import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography'; import { fontSizeSmaller, lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
interface ChatUserNameProps { interface ChatUserNameProps {
currentlyInMeeting: boolean; currentlyInMeeting: boolean;
@ -14,6 +14,7 @@ interface ChatUserNameProps {
export const HeaderContent = styled.div` export const HeaderContent = styled.div`
display: flex; display: flex;
flex-flow: row; flex-flow: row;
align-items: center;
width: 100%; width: 100%;
`; `;
@ -30,6 +31,7 @@ export const ChatUserName = styled.div<ChatUserNameProps>`
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
flex-grow: 1;
${({ currentlyInMeeting }) => currentlyInMeeting && ` ${({ currentlyInMeeting }) => currentlyInMeeting && `
color: ${colorHeading}; color: ${colorHeading};
@ -65,8 +67,8 @@ export const ChatUserOffline = styled.span`
export const ChatTime = styled.time` export const ChatTime = styled.time`
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
flex-basis: 3.5rem; flex-basis: max-content;
color: ${palettePlaceholderText}; color: ${colorGrayDark};
text-transform: uppercase; text-transform: uppercase;
font-size: 75%; font-size: 75%;
[dir='rtl'] & { [dir='rtl'] & {
@ -84,10 +86,27 @@ export const ChatHeaderText = styled.div`
width: 100%; width: 100%;
`; `;
export const EditLabel = styled.span`
color: ${colorGrayLight};
font-size: ${fontSizeSmaller};
display: flex;
align-items: center;
gap: calc(${lineHeightComputed} / 4);
[dir='ltr'] & {
margin-right: calc(${lineHeightComputed} / 2);
}
[dir='rtl'] & {
margin-left: calc(${lineHeightComputed} / 2);
}
`;
export default { export default {
HeaderContent, HeaderContent,
ChatTime, ChatTime,
ChatUserOffline, ChatUserOffline,
ChatUserName, ChatUserName,
ChatHeaderText, ChatHeaderText,
EditLabel,
}; };

View File

@ -38,6 +38,8 @@ const ChatMessageReactions: React.FC<ChatMessageReactionsProps> = (props) => {
const { data: currentUser } = useCurrentUser((u) => ({ userId: u.userId })); const { data: currentUser } = useCurrentUser((u) => ({ userId: u.userId }));
const intl = useIntl(); const intl = useIntl();
if (reactions.length === 0) return null;
const reactionItems: Record<string, { count: number; userNames: string[]; reactedByMe: boolean }> = {}; const reactionItems: Record<string, { count: number; userNames: string[]; reactedByMe: boolean }> = {};
reactions.forEach((reaction) => { reactions.forEach((reaction) => {

View File

@ -1,29 +1,41 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { colorBlueLighter, colorBlueLightest, colorGray } from '/imports/ui/stylesheets/styled-components/palette'; import {
colorGrayLighter, colorGrayLightest, colorOffWhite,
} from '/imports/ui/stylesheets/styled-components/palette';
const EmojiWrapper = styled.button<{ highlighted: boolean }>` const EmojiWrapper = styled.button<{ highlighted: boolean }>`
background-color: ${colorBlueLightest}; background: none;
border-radius: 10px; border-radius: 1rem;
margin-left: 3px; padding: 0.375rem 1rem;
margin-top: 3px; line-height: 1;
padding: 3px;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
border: 1px solid transparent; border: 1px solid ${colorGrayLightest};
cursor: pointer; cursor: pointer;
${({ highlighted }) => highlighted && ` ${({ highlighted }) => highlighted && `
background-color: ${colorBlueLighter}; background-color: ${colorOffWhite};
`} `}
em-emoji {
[dir='ltr'] & {
margin-right: 0.25rem;
}
[dir='rtl'] & {
margin-left: 0.25rem;
}
}
&:hover { &:hover {
border: 1px solid ${colorGray}; border: 1px solid ${colorGrayLighter};
} }
`; `;
const ReactionsWrapper = styled.div` const ReactionsWrapper = styled.div`
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.25rem;
`; `;
export default { export default {

View File

@ -13,24 +13,21 @@ const intlMessages = defineMessages({
}); });
interface MessageRepliedProps { interface MessageRepliedProps {
username: string;
message: string; message: string;
sequence: number; sequence: number;
userColor: string;
emphasizedMessage: boolean; emphasizedMessage: boolean;
deletedByUser: string | null; deletedByUser: string | null;
} }
const ChatMessageReplied: React.FC<MessageRepliedProps> = (props) => { const ChatMessageReplied: React.FC<MessageRepliedProps> = (props) => {
const { const {
message, username, sequence, userColor, emphasizedMessage, deletedByUser, message, sequence, emphasizedMessage, deletedByUser,
} = props; } = props;
const intl = useIntl(); const intl = useIntl();
return ( return (
<Styled.Container <Styled.Container
$userColor={userColor}
onClick={() => { onClick={() => {
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, { new CustomEvent(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, {
@ -43,13 +40,11 @@ const ChatMessageReplied: React.FC<MessageRepliedProps> = (props) => {
Storage.setItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, sequence); Storage.setItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, sequence);
}} }}
> >
<Styled.Username $userColor={userColor}>{username}</Styled.Username>
{!deletedByUser && ( {!deletedByUser && (
<Styled.Message> <Styled.Message>
<ChatMessageTextContent <ChatMessageTextContent
text={message} text={message}
emphasizedMessage={emphasizedMessage} emphasizedMessage={emphasizedMessage}
systemMsg={false}
dataTest={null} dataTest={null}
/> />
</Styled.Message> </Styled.Message>

View File

@ -1,24 +1,35 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { colorOffWhite, colorText } from '/imports/ui/stylesheets/styled-components/palette'; import {
colorGrayLightest, colorOffWhite, colorPrimary, colorText,
} from '/imports/ui/stylesheets/styled-components/palette';
import { $3xlPadding, lgPadding } from '/imports/ui/stylesheets/styled-components/general';
const Container = styled.div<{ $userColor: string }>` const Container = styled.div`
border-radius: 4px; border-top-left-radius: 0.5rem;
border-left: 4px solid ${({ $userColor }) => $userColor}; border-top-right-radius: 0.5rem;
background-color: ${colorOffWhite}; background-color: ${colorOffWhite};
padding: 6px; box-shadow: inset 0 0 0 1px ${colorGrayLightest};
padding: ${lgPadding} ${$3xlPadding};
position: relative; position: relative;
margin: 0.25rem 0 0.25rem 0;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
[dir='ltr'] & {
border-right: 0.5rem solid ${colorPrimary};
}
[dir='rtl'] & {
border-left: 0.5rem solid ${colorPrimary};
}
`; `;
const Typography = styled.div` const Typography = styled.div`
overflow: hidden; overflow: hidden;
`; `;
const Username = styled(Typography)<{ $userColor: string }>` const Username = styled(Typography)`
font-weight: bold; font-weight: bold;
color: ${({ $userColor }) => $userColor}; color: ${colorPrimary};
line-height: 1rem; line-height: 1rem;
font-size: 1rem; font-size: 1rem;
white-space: nowrap; white-space: nowrap;
@ -26,8 +37,8 @@ const Username = styled(Typography)<{ $userColor: string }>`
`; `;
const Message = styled(Typography)` const Message = styled(Typography)`
max-height: 3.6rem; max-height: 1rem;
line-height: 1.2rem; line-height: 1rem;
overflow: hidden; overflow: hidden;
`; `;

View File

@ -1,26 +1,21 @@
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import Popover from '@mui/material/Popover'; import Popover from '@mui/material/Popover';
import { FocusTrap } from '@mui/base/FocusTrap';
import { layoutSelect } from '/imports/ui/components/layout/context'; import { layoutSelect } from '/imports/ui/components/layout/context';
import Button from '/imports/ui/components/common/button/component';
import BBBMenu from '/imports/ui/components/common/menu/component';
import { Layout } from '/imports/ui/components/layout/layoutTypes'; import { Layout } from '/imports/ui/components/layout/layoutTypes';
import { ChatEvents } from '/imports/ui/core/enums/chat'; import { ChatEvents } from '/imports/ui/core/enums/chat';
import { defineMessages, useIntl } from 'react-intl'; import ConfirmationModal from '/imports/ui/components/common/modal/confirmation/component';
import { import {
Container, Container,
Divider,
EmojiButton,
EmojiPicker, EmojiPicker,
EmojiPickerWrapper, EmojiPickerWrapper,
EmojiButton, Root,
} from './styles'; } from './styles';
import { colorDanger, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import { CHAT_DELETE_MESSAGE_MUTATION } from '../mutations'; import { CHAT_DELETE_MESSAGE_MUTATION } from '../mutations';
import {
useIsChatMessageReactionsEnabled,
useIsDeleteChatMessageEnabled,
useIsEditChatMessageEnabled,
useIsReplyChatMessageEnabled,
} from '/imports/ui/services/features';
const intlMessages = defineMessages({ const intlMessages = defineMessages({
reply: { reply: {
@ -35,6 +30,18 @@ const intlMessages = defineMessages({
id: 'app.chat.toolbar.delete', id: 'app.chat.toolbar.delete',
description: 'delete label', description: 'delete label',
}, },
cancelLabel: {
id: 'app.chat.toolbar.delete.cancelLabel',
description: '',
},
confirmationTitle: {
id: 'app.chat.toolbar.delete.confirmationTitle',
description: '',
},
confirmationDescription: {
id: 'app.chat.toolbar.delete.confimationDescription',
description: '',
},
}); });
interface ChatMessageToolbarProps { interface ChatMessageToolbarProps {
@ -47,198 +54,183 @@ interface ChatMessageToolbarProps {
messageSequence: number; messageSequence: number;
emphasizedMessage: boolean; emphasizedMessage: boolean;
onEmojiSelected(emoji: { id: string; native: string }): void; onEmojiSelected(emoji: { id: string; native: string }): void;
onEditRequest(): void;
onMenuOpenChange(open: boolean): void;
menuIsOpen: boolean;
onReactionPopoverOpenChange(open: boolean): void; onReactionPopoverOpenChange(open: boolean): void;
reactionPopoverIsOpen: boolean; reactionPopoverIsOpen: boolean;
hasToolbar: boolean;
locked: boolean;
deleted: boolean;
chatReplyEnabled: boolean;
chatReactionsEnabled: boolean;
chatEditEnabled: boolean;
chatDeleteEnabled: boolean;
keyboardFocused: boolean;
} }
const ChatMessageToolbar: React.FC<ChatMessageToolbarProps> = (props) => { const ChatMessageToolbar: React.FC<ChatMessageToolbarProps> = (props) => {
const { const {
messageId, chatId, message, username, onEmojiSelected, onMenuOpenChange, messageId, chatId, message, username, onEmojiSelected, deleted,
messageSequence, emphasizedMessage, onEditRequest, own, amIModerator, menuIsOpen, messageSequence, emphasizedMessage, own, amIModerator, locked,
onReactionPopoverOpenChange, reactionPopoverIsOpen, onReactionPopoverOpenChange, reactionPopoverIsOpen, hasToolbar, keyboardFocused,
chatDeleteEnabled, chatEditEnabled, chatReactionsEnabled, chatReplyEnabled,
} = props; } = props;
const [reactionsAnchor, setReactionsAnchor] = React.useState<Element | null>( const [reactionsAnchor, setReactionsAnchor] = React.useState<Element | null>(
null, null,
); );
const [isTryingToDelete, setIsTryingToDelete] = React.useState(false);
const intl = useIntl(); const intl = useIntl();
const [chatDeleteMessage] = useMutation(CHAT_DELETE_MESSAGE_MUTATION); const [chatDeleteMessage] = useMutation(CHAT_DELETE_MESSAGE_MUTATION);
const isRTL = layoutSelect((i: Layout) => i.isRTL); const isRTL = layoutSelect((i: Layout) => i.isRTL);
const CHAT_REPLIES_ENABLED = useIsReplyChatMessageEnabled();
const CHAT_REACTIONS_ENABLED = useIsChatMessageReactionsEnabled();
const CHAT_EDIT_ENABLED = useIsEditChatMessageEnabled();
const CHAT_DELETE_ENABLED = useIsDeleteChatMessageEnabled();
const actions = [];
if (CHAT_EDIT_ENABLED && own) {
actions.push({
key: 'edit',
icon: 'pen_tool',
label: intl.formatMessage(intlMessages.edit),
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
onEditRequest();
window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_EDIT_REQUEST, {
detail: {
messageId,
chatId,
},
}),
);
},
});
}
if (CHAT_DELETE_ENABLED && (own || amIModerator)) {
const customStyles = { background: colorDanger, color: colorWhite };
actions.push({
key: 'delete',
icon: 'delete',
label: intl.formatMessage(intlMessages.delete),
customStyles,
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.stopPropagation();
chatDeleteMessage({
variables: {
chatId,
messageId,
},
});
},
});
}
if ([ if ([
CHAT_REPLIES_ENABLED, chatReplyEnabled,
CHAT_REACTIONS_ENABLED, chatReactionsEnabled,
CHAT_EDIT_ENABLED, chatEditEnabled,
CHAT_DELETE_ENABLED, chatDeleteEnabled,
].every((config) => !config)) return null; ].every((config) => !config) || !hasToolbar || locked || deleted) return null;
const showReplyButton = chatReplyEnabled;
const showReactionsButton = chatReactionsEnabled;
const showEditButton = chatEditEnabled && own;
const showDeleteButton = chatDeleteEnabled && (own || amIModerator);
const showDivider = (showReplyButton || showReactionsButton) && (showEditButton || showDeleteButton);
return ( return (
<Container <Root
className="chat-message-toolbar"
$sequence={messageSequence}
$menuIsOpen={menuIsOpen}
$reactionPopoverIsOpen={reactionPopoverIsOpen} $reactionPopoverIsOpen={reactionPopoverIsOpen}
onKeyDown={(e) => {
if (e.key === 'Escape' && keyboardFocused) {
window.dispatchEvent(new CustomEvent(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_CANCEL));
}
}}
> >
{CHAT_REPLIES_ENABLED && ( <FocusTrap open={keyboardFocused}>
<> <Container className="chat-message-toolbar">
<Button {showReplyButton && (
circle <>
ghost <EmojiButton
aria-describedby={`chat-reply-btn-label-${messageSequence}`} aria-describedby={`chat-reply-btn-label-${messageSequence}`}
icon="undo" icon="undo"
color="light" color="light"
size="sm" onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
type="button" e.stopPropagation();
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { window.dispatchEvent(
e.stopPropagation(); new CustomEvent(ChatEvents.CHAT_REPLY_INTENTION, {
window.dispatchEvent( detail: {
new CustomEvent(ChatEvents.CHAT_REPLY_INTENTION, { username,
detail: { message,
username, messageId,
message, chatId,
messageId, emphasizedMessage,
chatId, sequence: messageSequence,
emphasizedMessage, },
sequence: messageSequence, }),
}, );
}), window.dispatchEvent(
); new CustomEvent(ChatEvents.CHAT_CANCEL_EDIT_REQUEST),
}} );
/> }}
<span id={`chat-reply-btn-label-${messageSequence}`} className="sr-only"> />
{intl.formatMessage(intlMessages.reply, { 0: messageSequence })} <span id={`chat-reply-btn-label-${messageSequence}`} className="sr-only">
</span> {intl.formatMessage(intlMessages.reply, { 0: messageSequence })}
</> </span>
)} </>
{CHAT_REACTIONS_ENABLED && ( )}
<EmojiButton {showReactionsButton && (
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { <EmojiButton
e.stopPropagation(); onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
onReactionPopoverOpenChange(true); e.stopPropagation();
}} onReactionPopoverOpenChange(true);
setRef={setReactionsAnchor}
size="sm"
icon="happy"
color="light"
ghost
type="button"
circle
data-test="emojiPickerButton"
/>
)}
{actions.length > 0 && (
<BBBMenu
trigger={(
<Button
onClick={() => {
onMenuOpenChange(true);
}} }}
size="sm" icon="happy"
icon="more"
color="light" color="light"
ghost data-test="emojiPickerButton"
type="button" ref={setReactionsAnchor}
circle
/> />
)} )}
actions={actions} {showDivider && <Divider role="separator" />}
onCloseCallback={() => { {showEditButton && (
onMenuOpenChange(false); <EmojiButton
}} onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
opts={{ e.stopPropagation();
id: 'app-settings-dropdown-menu', window.dispatchEvent(
keepMounted: true, new CustomEvent(ChatEvents.CHAT_EDIT_REQUEST, {
transitionDuration: 0, detail: {
elevation: 3, messageId,
getcontentanchorel: null, chatId,
fullwidth: 'true', message,
anchorOrigin: { },
vertical: 'bottom', }),
horizontal: isRTL ? 'left' : 'right', );
}, window.dispatchEvent(
transformorigin: { new CustomEvent(ChatEvents.CHAT_CANCEL_REPLY_INTENTION),
);
}}
icon="pen_tool"
color="light"
data-test="editMessageButton"
/>
)}
{showDeleteButton && (
<EmojiButton
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
setIsTryingToDelete(true);
}}
icon="delete"
color="light"
data-test="deleteMessageButton"
/>
)}
<Popover
open={reactionPopoverIsOpen}
anchorEl={reactionsAnchor}
onClose={() => {
onReactionPopoverOpenChange(false);
}}
anchorOrigin={{
vertical: 'top', vertical: 'top',
horizontal: isRTL ? 'left' : 'right', horizontal: isRTL ? 'left' : 'right',
},
}}
/>
)}
<Popover
open={reactionPopoverIsOpen}
anchorEl={reactionsAnchor}
onClose={() => {
setReactionsAnchor(null);
onReactionPopoverOpenChange(false);
}}
anchorOrigin={{
vertical: 'top',
horizontal: isRTL ? 'left' : 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: isRTL ? 'right' : 'left',
}}
>
<EmojiPickerWrapper>
<EmojiPicker
onEmojiSelect={(emojiObject: { id: string; native: string }) => {
onEmojiSelected(emojiObject);
}} }}
showPreview={false} transformOrigin={{
showSkinTones={false} vertical: 'top',
/> horizontal: isRTL ? 'right' : 'left',
</EmojiPickerWrapper> }}
</Popover> >
</Container> <EmojiPickerWrapper>
<EmojiPicker
onEmojiSelect={(emojiObject: { id: string; native: string }) => {
onEmojiSelected(emojiObject);
}}
showPreview={false}
showSkinTones={false}
/>
</EmojiPickerWrapper>
</Popover>
{isTryingToDelete && (
<ConfirmationModal
isOpen={isTryingToDelete}
setIsOpen={setIsTryingToDelete}
onRequestClose={() => setIsTryingToDelete(false)}
onConfirm={() => {
chatDeleteMessage({
variables: {
chatId,
messageId,
},
});
}}
title={intl.formatMessage(intlMessages.confirmationTitle)}
confirmButtonLabel={intl.formatMessage(intlMessages.delete)}
cancelButtonLabel={intl.formatMessage(intlMessages.cancelLabel)}
description={intl.formatMessage(intlMessages.confirmationDescription)}
confirmButtonColor="danger"
priority="low"
/>
)}
</Container>
</FocusTrap>
</Root>
); );
}; };

View File

@ -0,0 +1,25 @@
import React from 'react';
import Styled from './styles';
import Icon from '/imports/ui/components/common/icon/component';
interface EmojiButtonProps extends React.ComponentProps<'button'> {
icon: string;
}
const EmojiButton = React.forwardRef<HTMLButtonElement, EmojiButtonProps>((props, ref) => {
const {
icon,
...buttonProps
} = props;
const IconComponent = (<Icon iconName={icon} />);
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<Styled.EmojiButton {...buttonProps} ref={ref}>
{IconComponent}
</Styled.EmojiButton>
);
});
export default EmojiButton;

View File

@ -0,0 +1,34 @@
import styled from 'styled-components';
import { colorGray } from '/imports/ui/stylesheets/styled-components/palette';
import { lgPadding } from '/imports/ui/stylesheets/styled-components/general';
import { fontSizeSmaller } from '/imports/ui/stylesheets/styled-components/typography';
const EmojiButton = styled.button`
line-height: 1;
font-size: ${fontSizeSmaller};
background: none;
border: none;
outline: none;
padding: ${lgPadding};
color: ${colorGray};
cursor: pointer;
&:focus,
&:hover {
opacity: 0.5;
}
&:active {
transform: scale(0.9);
}
i::before {
display: block;
width: 100%;
height: 100%;
}
`;
export default {
EmojiButton,
};

View File

@ -1,44 +1,55 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { import {
colorGrayLighter, colorGrayLighter,
colorOffWhite, colorGrayLightest,
colorWhite, colorWhite,
} from '/imports/ui/stylesheets/styled-components/palette'; } from '/imports/ui/stylesheets/styled-components/palette';
import { borderRadius, smPaddingX } from '/imports/ui/stylesheets/styled-components/general'; import { $2xlPadding, borderRadius, smPadding } from '/imports/ui/stylesheets/styled-components/general';
import EmojiPickerComponent from '/imports/ui/components/emoji-picker/component'; import EmojiPickerComponent from '/imports/ui/components/emoji-picker/component';
import Button from '/imports/ui/components/common/button/component'; import BaseEmojiButton from './emoji-button/component';
const Container = styled.div<{ $sequence: number, $menuIsOpen: boolean, $reactionPopoverIsOpen: boolean }>` interface RootProps {
height: calc(1.5rem + 12px); $reactionPopoverIsOpen: boolean;
line-height: calc(1.5rem + 8px); }
max-width: 184px;
overflow: hidden; const Root = styled.div<RootProps>`
padding-bottom: ${smPadding};
justify-content: flex-end;
display: none; display: none;
position: absolute; position: absolute;
right: 0; bottom: 100%;
border: 1px solid ${colorOffWhite}; z-index: 10;
border-radius: 8px;
padding: 1px;
background-color: ${colorWhite};
z-index: 2;
#chat-message-wrapper:hover & { [dir='ltr'] & {
padding-left: ${$2xlPadding};
right: 0;
}
[dir='rtl'] & {
padding-right: ${$2xlPadding};
left: 0;
}
.chat-message-wrapper:hover &,
.chat-message-wrapper:focus &,
.chat-message-wrapper-focused &,
.chat-message-wrapper-keyboard-focused &,
&:hover,
&:focus-within {
display: flex; display: flex;
} }
${({ $menuIsOpen, $reactionPopoverIsOpen }) => (($menuIsOpen || $reactionPopoverIsOpen) && css` ${({ $reactionPopoverIsOpen }) => ($reactionPopoverIsOpen && css`
display: flex; display: flex;
`)} `)}
`;
${({ $sequence }) => (($sequence === 1) const Container = styled.div`
? css` max-width: max-content;
bottom: 0; display: flex;
transform: translateY(50%); border-radius: 1rem;
` background-color: ${colorWhite};
: css` box-shadow: 0 0.125rem 0.125rem 0 ${colorGrayLighter};
top: 0;
transform: translateY(-50%);
`)}
`; `;
const EmojiPickerWrapper = styled.div` const EmojiPickerWrapper = styled.div`
@ -47,7 +58,7 @@ const EmojiPickerWrapper = styled.div`
right: 0; right: 0;
border: 1px solid ${colorGrayLighter}; border: 1px solid ${colorGrayLighter};
border-radius: ${borderRadius}; border-radius: ${borderRadius};
box-shadow: 0 2px 10px rgba(0,0,0,0.1); box-shadow: 0 0.125rem 10px rgba(0,0,0,0.1);
z-index: 1000; z-index: 1000;
.emoji-mart { .emoji-mart {
@ -70,29 +81,25 @@ const EmojiPickerWrapper = styled.div`
} }
`; `;
// @ts-ignore const EmojiButton = styled(BaseEmojiButton)``;
const EmojiButton = styled(Button)`
margin:0 0 0 ${smPaddingX};
align-self: center;
font-size: 0.5rem;
[dir="rtl"] & {
margin: 0 ${smPaddingX} 0 0;
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
}
`;
const EmojiPicker = styled(EmojiPickerComponent)` const EmojiPicker = styled(EmojiPickerComponent)`
position: relative; position: relative;
`; `;
const Divider = styled.div`
width: 0.125rem;
height: 75%;
border-radius: 0.5rem;
background-color: ${colorGrayLightest};
align-self: center;
`;
export { export {
Container, Container,
EmojiPicker, EmojiPicker,
EmojiPickerWrapper, EmojiPickerWrapper,
EmojiButton, EmojiButton,
Root,
Divider,
}; };

View File

@ -1,22 +1,26 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { import {
borderSize,
userIndicatorsOffset, userIndicatorsOffset,
smPaddingX, smPaddingX,
smPaddingY,
lgPadding,
$3xlPadding,
xlPadding,
mdPadding,
} from '/imports/ui/stylesheets/styled-components/general'; } from '/imports/ui/stylesheets/styled-components/general';
import { import {
lineHeightComputed,
fontSizeBase, fontSizeBase,
fontSizeSmaller,
} from '/imports/ui/stylesheets/styled-components/typography'; } from '/imports/ui/stylesheets/styled-components/typography';
import { import {
colorWhite, colorWhite,
userListBg, userListBg,
colorSuccess, colorSuccess,
colorOffWhite, colorBlueLightest,
colorText, colorGrayLight,
palettePlaceholderText, colorGrayLightest,
} from '/imports/ui/stylesheets/styled-components/palette'; } from '/imports/ui/stylesheets/styled-components/palette';
import Header from '/imports/ui/components/common/control-header/component'; import Header from '/imports/ui/components/common/control-header/component';
@ -26,17 +30,16 @@ interface ChatWrapperProps {
isSystemSender: boolean; isSystemSender: boolean;
isPresentationUpload?: boolean; isPresentationUpload?: boolean;
isCustomPluginMessage: boolean; isCustomPluginMessage: boolean;
$highlight: boolean;
$toolbarMenuIsOpen: boolean;
$reactionPopoverIsOpen: boolean;
} }
interface ChatContentProps { interface ChatContentProps {
sameSender: boolean; sameSender: boolean;
isCustomPluginMessage: boolean; isCustomPluginMessage: boolean;
$editing: boolean;
$highlight: boolean; $highlight: boolean;
$toolbarMenuIsOpen: boolean;
$reactionPopoverIsOpen: boolean; $reactionPopoverIsOpen: boolean;
$focused: boolean;
$keyboardFocused: boolean;
} }
interface ChatAvatarProps { interface ChatAvatarProps {
@ -48,12 +51,17 @@ interface ChatAvatarProps {
export const ChatWrapper = styled.div<ChatWrapperProps>` export const ChatWrapper = styled.div<ChatWrapperProps>`
pointer-events: auto; pointer-events: auto;
display: flex;
flex-flow: column;
gap: ${smPaddingY};
position: relative;
font-size: ${fontSizeBase};
position: relative;
[dir='rtl'] & { [dir='rtl'] & {
direction: rtl; direction: rtl;
} }
display: flex;
flex-flow: row;
position: relative;
${({ isPresentationUpload }) => isPresentationUpload && ` ${({ isPresentationUpload }) => isPresentationUpload && `
border-left: 2px solid #0F70D7; border-left: 2px solid #0F70D7;
margin-top: 1rem; margin-top: 1rem;
@ -61,18 +69,6 @@ export const ChatWrapper = styled.div<ChatWrapperProps>`
word-break: break-word; word-break: break-word;
background-color: #F3F6F9; background-color: #F3F6F9;
`} `}
${({ sameSender }) => sameSender && `
flex: 1;
margin: ${borderSize} 0 0 ${borderSize};
margin-top: calc(${lineHeightComputed} / 3);
`}
${({ sameSender }) => !sameSender && `
padding-top:${lineHeightComputed};
`}
[dir="rtl"] & {
margin: ${borderSize} ${borderSize} 0 0;
}
font-size: ${fontSizeBase};
${({ isSystemSender }) => isSystemSender && ` ${({ isSystemSender }) => isSystemSender && `
background-color: #fef9f1; background-color: #fef9f1;
border-left: 2px solid #f5c67f; border-left: 2px solid #f5c67f;
@ -83,42 +79,25 @@ export const ChatWrapper = styled.div<ChatWrapperProps>`
margin: 0; margin: 0;
padding: 0; padding: 0;
`} `}
${({ sameSender, $highlight }) => !sameSender && $highlight && `
&:hover {
background-color: ${colorOffWhite};
}
border-radius: 6px;
`}
${({ sameSender, $toolbarMenuIsOpen, $reactionPopoverIsOpen }) => !sameSender
&& ($toolbarMenuIsOpen || $reactionPopoverIsOpen)
&& `
background-color: ${colorOffWhite};
border-radius: 6px;
`}
`; `;
export const ChatContent = styled.div<ChatContentProps>` export const ChatContent = styled.div<ChatContentProps>`
display: flex; display: flex;
flex-flow: column; flex-flow: column;
width: 100%; width: 100%;
border-radius: 0.5rem;
${({ sameSender, isCustomPluginMessage }) => sameSender ${({ $highlight }) => $highlight && `
&& !isCustomPluginMessage && ` .chat-message-wrapper:hover > & {
margin-left: 2.6rem; background-color: ${colorBlueLightest};
`}
${({ sameSender, $highlight }) => sameSender && $highlight && `
&:hover {
background-color: ${colorOffWhite};
} }
border-radius: 6px;
`} `}
${({ sameSender, $toolbarMenuIsOpen, $reactionPopoverIsOpen }) => sameSender ${({
&& ($toolbarMenuIsOpen || $reactionPopoverIsOpen) $editing, $reactionPopoverIsOpen, $focused, $keyboardFocused,
}) => ($reactionPopoverIsOpen || $editing || $focused || $keyboardFocused)
&& ` && `
background-color: ${colorOffWhite}; background-color: ${colorBlueLightest};
border-radius: 6px;
`} `}
`; `;
@ -212,29 +191,46 @@ export const ChatAvatar = styled.div<ChatAvatarProps>`
} }
`; `;
export const Container = styled.div` export const Container = styled.div<{ $sequence: number }>`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&:not(:first-child) {
margin-top: calc((${fontSizeSmaller} + ${lgPadding} * 2) / 2);
}
`; `;
export const MessageItemWrapper = styled.div` export const MessageItemWrapper = styled.div<{ $edited: boolean, $sameSender: boolean }>`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding: ${lgPadding} ${$3xlPadding};
${({ $edited, $sameSender }) => $edited && $sameSender && `
padding-bottom: 0;
`}
`; `;
export const DeleteMessage = styled.span` export const DeleteMessage = styled.span`
font-style: italic; color: ${colorGrayLight};
font-weight: bold; padding: ${mdPadding} ${xlPadding};
color: ${colorText}; border: 1px solid ${colorGrayLightest};
border-radius: 0.375rem;
`; `;
export const ChatEditTime = styled.time` export const ChatHeading = styled.div`
flex-shrink: 1; display: flex;
flex-grow: 0; `;
white-space: nowrap;
text-overflow: ellipsis; export const EditLabel = styled.span`
overflow: hidden; color: ${colorGrayLight};
min-width: 0; font-size: 75%;
font-size: 75%; display: flex;
color: ${palettePlaceholderText}; justify-content: flex-end;
align-items: center;
gap: 2px;
`;
export const EditLabelWrapper = styled.div`
line-height: 1;
padding: ${xlPadding};
`; `;

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, { import React, {
useContext, useContext,
useEffect, useEffect,
@ -20,6 +19,7 @@ import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
import { useCreateUseSubscription } from '/imports/ui/core/hooks/createUseSubscription'; import { useCreateUseSubscription } from '/imports/ui/core/hooks/createUseSubscription';
import { setLoadedMessageGathering } from '/imports/ui/core/hooks/useLoadedChatMessages'; import { setLoadedMessageGathering } from '/imports/ui/core/hooks/useLoadedChatMessages';
import { ChatLoading } from '../../component'; import { ChatLoading } from '../../component';
import { ChatEvents } from '/imports/ui/core/enums/chat';
interface ChatListPageContainerProps { interface ChatListPageContainerProps {
page: number; page: number;
@ -29,6 +29,7 @@ interface ChatListPageContainerProps {
chatId: string; chatId: string;
markMessageAsSeen: (message: Message) => void; markMessageAsSeen: (message: Message) => void;
scrollRef: React.RefObject<HTMLDivElement>; scrollRef: React.RefObject<HTMLDivElement>;
focusedId: number | null;
} }
interface ChatListPageProps { interface ChatListPageProps {
@ -38,6 +39,7 @@ interface ChatListPageProps {
page: number; page: number;
markMessageAsSeen: (message: Message)=> void; markMessageAsSeen: (message: Message)=> void;
scrollRef: React.RefObject<HTMLDivElement>; scrollRef: React.RefObject<HTMLDivElement>;
focusedId: number | null;
} }
const areChatPagesEqual = (prevProps: ChatListPageProps, nextProps: ChatListPageProps) => { const areChatPagesEqual = (prevProps: ChatListPageProps, nextProps: ChatListPageProps) => {
@ -53,7 +55,7 @@ const areChatPagesEqual = (prevProps: ChatListPageProps, nextProps: ChatListPage
&& prevMessage?.message === nextMessage?.message && prevMessage?.message === nextMessage?.message
&& prevMessage?.reactions?.length === nextMessage?.reactions?.length && prevMessage?.reactions?.length === nextMessage?.reactions?.length
); );
}); }) && prevProps.focusedId === nextProps.focusedId;
}; };
const ChatListPage: React.FC<ChatListPageProps> = ({ const ChatListPage: React.FC<ChatListPageProps> = ({
@ -63,6 +65,7 @@ const ChatListPage: React.FC<ChatListPageProps> = ({
page, page,
markMessageAsSeen, markMessageAsSeen,
scrollRef, scrollRef,
focusedId,
}) => { }) => {
const { domElementManipulationIdentifiers } = useContext(PluginsContext); const { domElementManipulationIdentifiers } = useContext(PluginsContext);
@ -83,6 +86,29 @@ const ChatListPage: React.FC<ChatListPageProps> = ({
); );
}, [domElementManipulationIdentifiers, messagesRequestedFromPlugin]); }, [domElementManipulationIdentifiers, messagesRequestedFromPlugin]);
const [keyboardFocusedMessageSequence, setKeyboardFocusedMessageSequence] = useState<number | null>(null);
useEffect(() => {
const handleKeyboardFocusMessageRequest = (e: Event) => {
if (e instanceof CustomEvent) {
setKeyboardFocusedMessageSequence(Number.parseInt(e.detail.sequence, 10));
}
};
const handleKeyboardFocusMessageCancel = (e: Event) => {
if (e instanceof CustomEvent) {
setKeyboardFocusedMessageSequence(null);
}
};
window.addEventListener(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_REQUEST, handleKeyboardFocusMessageRequest);
window.addEventListener(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_CANCEL, handleKeyboardFocusMessageCancel);
return () => {
window.removeEventListener(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_REQUEST, handleKeyboardFocusMessageRequest);
window.removeEventListener(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_CANCEL, handleKeyboardFocusMessageCancel);
};
}, []);
return ( return (
<React.Fragment key={`messagePage-${page}`}> <React.Fragment key={`messagePage-${page}`}>
{messages.map((message, index, messagesArray) => { {messages.map((message, index, messagesArray) => {
@ -99,6 +125,8 @@ const ChatListPage: React.FC<ChatListPageProps> = ({
scrollRef={scrollRef} scrollRef={scrollRef}
markMessageAsSeen={markMessageAsSeen} markMessageAsSeen={markMessageAsSeen}
messageReadFeedbackEnabled={messageReadFeedbackEnabled} messageReadFeedbackEnabled={messageReadFeedbackEnabled}
focused={focusedId === message.messageSequence}
keyboardFocused={keyboardFocusedMessageSequence === message.messageSequence}
/> />
); );
})} })}
@ -116,8 +144,8 @@ const ChatListPageContainer: React.FC<ChatListPageContainerProps> = ({
chatId, chatId,
markMessageAsSeen, markMessageAsSeen,
scrollRef, scrollRef,
focusedId,
}) => { }) => {
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = window.meetingClientSettings.public.chat; const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id; const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
const PRIVATE_MESSAGE_READ_FEEDBACK_ENABLED = CHAT_CONFIG.privateMessageReadFeedback.enabled; const PRIVATE_MESSAGE_READ_FEEDBACK_ENABLED = CHAT_CONFIG.privateMessageReadFeedback.enabled;
@ -157,6 +185,7 @@ const ChatListPageContainer: React.FC<ChatListPageContainerProps> = ({
page={page} page={page}
markMessageAsSeen={markMessageAsSeen} markMessageAsSeen={markMessageAsSeen}
scrollRef={scrollRef} scrollRef={scrollRef}
focusedId={focusedId}
/> />
); );
}; };

View File

@ -1,6 +1,5 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { import {
mdPaddingX,
smPaddingX, smPaddingX,
borderRadius, borderRadius,
} from '/imports/ui/stylesheets/styled-components/general'; } from '/imports/ui/stylesheets/styled-components/general';
@ -15,8 +14,7 @@ interface MessageListProps {
export const MessageList = styled(ScrollboxVertical)<MessageListProps>` export const MessageList = styled(ScrollboxVertical)<MessageListProps>`
flex-flow: column; flex-flow: column;
flex-shrink: 1; flex-shrink: 1;
right: 0 ${mdPaddingX} 0 0; padding-top: 2rem;
padding-top: 0;
outline-style: none; outline-style: none;
overflow-x: hidden; overflow-x: hidden;
user-select: text; user-select: text;
@ -27,11 +25,6 @@ export const MessageList = styled(ScrollboxVertical)<MessageListProps>`
display: flex; display: flex;
padding-bottom: ${smPaddingX}; padding-bottom: ${smPaddingX};
[dir='rtl'] & {
margin: 0 0 0 auto;
padding: 0 0 0 ${mdPaddingX};
}
${({ isRTL }) => isRTL && ` ${({ isRTL }) => isRTL && `
padding-left: ${smPaddingX}; padding-left: ${smPaddingX};
`} `}

View File

@ -13,7 +13,7 @@ const ChatReplyIntention = () => {
const [sequence, setSequence] = useState<number>(); const [sequence, setSequence] = useState<number>();
useEffect(() => { useEffect(() => {
const handler = (e: Event) => { const handleReplyIntention = (e: Event) => {
if (e instanceof CustomEvent) { if (e instanceof CustomEvent) {
setUsername(e.detail.username); setUsername(e.detail.username);
setMessage(e.detail.message); setMessage(e.detail.message);
@ -22,10 +22,21 @@ const ChatReplyIntention = () => {
} }
}; };
window.addEventListener(ChatEvents.CHAT_REPLY_INTENTION, handler); const handleCancelReplyIntention = (e: Event) => {
if (e instanceof CustomEvent) {
setUsername(undefined);
setMessage(undefined);
setEmphasizedMessage(undefined);
setSequence(undefined);
}
};
window.addEventListener(ChatEvents.CHAT_REPLY_INTENTION, handleReplyIntention);
window.addEventListener(ChatEvents.CHAT_CANCEL_REPLY_INTENTION, handleCancelReplyIntention);
return () => { return () => {
window.removeEventListener(ChatEvents.CHAT_REPLY_INTENTION, handler); window.removeEventListener(ChatEvents.CHAT_REPLY_INTENTION, handleReplyIntention);
window.removeEventListener(ChatEvents.CHAT_CANCEL_REPLY_INTENTION, handleCancelReplyIntention);
}; };
}, []); }, []);
@ -33,9 +44,11 @@ const ChatReplyIntention = () => {
animations: boolean; animations: boolean;
}; };
const hidden = !username || !message;
return ( return (
<Styled.Container <Styled.Container
$hidden={!username || !message} $hidden={hidden}
$animations={animations} $animations={animations}
onClick={() => { onClick={() => {
window.dispatchEvent( window.dispatchEvent(
@ -49,25 +62,23 @@ const ChatReplyIntention = () => {
if (sequence) Storage.setItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, sequence); if (sequence) Storage.setItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, sequence);
}} }}
> >
<Styled.Username>{username}</Styled.Username>
<Styled.Message> <Styled.Message>
<ChatMessageTextContent <ChatMessageTextContent
text={message || ''} text={message || ''}
emphasizedMessage={!!emphasizedMessage} emphasizedMessage={!!emphasizedMessage}
systemMsg={false}
dataTest={null} dataTest={null}
/> />
</Styled.Message> </Styled.Message>
<Styled.CloseBtn <Styled.CloseBtn
onClick={() => { onClick={(e) => {
setMessage(undefined); e.stopPropagation();
setUsername(undefined); window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_CANCEL_REPLY_INTENTION),
);
}} }}
icon="close" icon="close"
ghost tabIndex={hidden ? -1 : 0}
circle aria-hidden={hidden}
color="light"
size="sm"
/> />
</Styled.Container> </Styled.Container>
); );

View File

@ -1,68 +1,82 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { import {
colorBlueLight, colorGrayLightest,
colorOffWhite, colorOffWhite,
colorPrimary,
} from '/imports/ui/stylesheets/styled-components/palette'; } from '/imports/ui/stylesheets/styled-components/palette';
import Button from '/imports/ui/components/common/button/component'; import {
mdPadding, smPadding, smPaddingX, xlPadding,
} from '/imports/ui/stylesheets/styled-components/general';
import EmojiButton from '../chat-message-list/page/chat-message/message-toolbar/emoji-button/component';
const Container = styled.div<{ $hidden: boolean; $animations: boolean }>` const Container = styled.div<{ $hidden: boolean; $animations: boolean }>`
border-radius: 4px; border-radius: 0.375rem;
border-left: 4px solid ${colorBlueLight};
background-color: ${colorOffWhite}; background-color: ${colorOffWhite};
position: relative; position: relative;
overflow: hidden; overflow: hidden;
box-shadow: inset 0 0 0 1px ${colorGrayLightest};
display: flex;
[dir='ltr'] & {
border-right: 0.375rem solid ${colorPrimary};
}
[dir='rtl'] & {
border-left: 0.375rem solid ${colorPrimary};
}
${({ $hidden }) => ($hidden ${({ $hidden }) => ($hidden
? css` ? css`
height: 0; height: 0;
min-height: 0;
` `
: css` : css`
height: 6rem; min-height: calc(1rem + ${mdPadding} * 2);
padding: 6px; height: calc(1rem + ${mdPadding} * 2);
margin-right: 0.75rem; padding: ${mdPadding} calc(${smPaddingX} * 1.25);
margin-bottom: 0.25rem; margin-bottom: ${smPadding};
[dir='ltr'] & {
margin-right: ${xlPadding};
}
[dir='rtl'] & {
margin-left: ${xlPadding};
}
` `
)} )}
${({ $animations }) => $animations ${({ $animations }) => $animations
&& css` && css`
transition-property: height; transition-property: height, min-height;
transition-duration: 0.1s; transition-duration: 0.1s;
`} `}
`; `;
const Typography = styled.div` const Typography = styled.div`
line-height: 1rem; line-height: 1;
font-size: 1rem; font-size: 1rem;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
`; `;
const Username = styled(Typography)` const Message = styled(Typography)`
font-weight: bold;
color: ${colorBlueLight};
line-height: 1rem;
height: 1rem;
font-size: 1rem; font-size: 1rem;
`; line-height: 1;
white-space: nowrap;
const Message = styled.div`
// Container height - Username height - vertical padding
max-height: calc(5rem - 12px);
overflow: hidden; overflow: hidden;
flex-grow: 1;
`; `;
// @ts-ignore const CloseBtn = styled(EmojiButton)`
const CloseBtn = styled(Button)` font-size: 75%;
position: absolute; height: 1rem;
top: 2px; padding: 0;
right: 2px;
`; `;
export default { export default {
Container, Container,
Username,
CloseBtn, CloseBtn,
Message, Message,
}; };

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useRef } from 'react';
import { CircularProgress } from '@mui/material'; import { CircularProgress } from '@mui/material';
import ChatHeader from './chat-header/component'; import ChatHeader from './chat-header/component';
import { layoutSelect, layoutSelectInput } from '../../layout/context'; import { layoutSelect, layoutSelectInput } from '../../layout/context';
@ -14,6 +14,7 @@ import { Chat as ChatType } from '/imports/ui/Types/chat';
import { layoutDispatch } from '/imports/ui/components/layout/context'; import { layoutDispatch } from '/imports/ui/components/layout/context';
import browserInfo from '/imports/utils/browserInfo'; import browserInfo from '/imports/utils/browserInfo';
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook'; import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
import { ChatEvents } from '/imports/ui/core/enums/chat';
interface ChatProps { interface ChatProps {
isRTL: boolean; isRTL: boolean;
@ -21,6 +22,7 @@ interface ChatProps {
const Chat: React.FC<ChatProps> = ({ isRTL }) => { const Chat: React.FC<ChatProps> = ({ isRTL }) => {
const { isChrome } = browserInfo; const { isChrome } = browserInfo;
const isEditingMessage = useRef(false);
React.useEffect(() => { React.useEffect(() => {
const handleMouseDown = () => { const handleMouseDown = () => {
@ -32,10 +34,36 @@ const Chat: React.FC<ChatProps> = ({ isRTL }) => {
} }
}; };
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isEditingMessage.current) {
window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_CANCEL_EDIT_REQUEST),
);
}
};
const handleEditingMessage = (e: Event) => {
if (e instanceof CustomEvent) {
isEditingMessage.current = true;
}
};
const handleCancelEditingMessage = (e: Event) => {
if (e instanceof CustomEvent) {
isEditingMessage.current = false;
}
};
document.addEventListener('mousedown', handleMouseDown); document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('keydown', handleKeyDown);
window.addEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleEditingMessage);
window.addEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelEditingMessage);
return () => { return () => {
document.removeEventListener('mousedown', handleMouseDown); document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('keydown', handleKeyDown);
window.removeEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleEditingMessage);
window.removeEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelEditingMessage);
}; };
}, []); }, []);

View File

@ -73,9 +73,9 @@ interface ExternalVideoPlayerProps {
externalVideo: ExternalVideo; externalVideo: ExternalVideo;
playing: boolean; playing: boolean;
playerPlaybackRate: number; playerPlaybackRate: number;
key: string; playerKey: string;
isSidebarContentOpen: boolean; isSidebarContentOpen: boolean;
setKey: (key: string) => void; setPlayerKey: (key: string) => void;
sendMessage: (event: string, data: { sendMessage: (event: string, data: {
rate: number; rate: number;
time: number; time: number;
@ -104,8 +104,8 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
playing, playing,
playerPlaybackRate, playerPlaybackRate,
isEchoTest, isEchoTest,
key, playerKey,
setKey, setPlayerKey,
sendMessage, sendMessage,
getCurrentTime, getCurrentTime,
}) => { }) => {
@ -130,15 +130,15 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
return { return {
// default option for all players, can be overwritten // default option for all players, can be overwritten
playerOptions: { playerOptions: {
autoplay: true, autoPlay: true,
playsinline: true, playsInline: true,
controls: isPresenter, controls: isPresenter,
}, },
file: { file: {
attributes: { attributes: {
controls: isPresenter ? 'controls' : '', controls: isPresenter ? 'controls' : '',
autoplay: 'autoplay', autoPlay: true,
playsinline: 'playsinline', playsInline: true,
}, },
}, },
facebook: { facebook: {
@ -263,16 +263,14 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
useEffect(() => { useEffect(() => {
if (isPresenter !== presenterRef.current) { if (isPresenter !== presenterRef.current) {
setKey(uniqueId('react-player')); setPlayerKey(uniqueId('react-player'));
presenterRef.current = isPresenter; presenterRef.current = isPresenter;
} }
}, [isPresenter]); }, [isPresenter]);
const handleOnStart = () => { const handleOnStart = () => {
if (!isPresenter) { currentTime = getCurrentTime();
currentTime = getCurrentTime(); playerRef.current?.seekTo(truncateTime(currentTime), 'seconds');
playerRef.current?.seekTo(truncateTime(currentTime), 'seconds');
}
}; };
const handleOnPlay = () => { const handleOnPlay = () => {
@ -308,10 +306,6 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
} }
}; };
const handleOnReady = (reactPlayer: ReactPlayer) => {
reactPlayer.seekTo(truncateTime(currentTime), 'seconds');
};
const handleProgress = (state: OnProgressProps) => { const handleProgress = (state: OnProgressProps) => {
setPlayed(state.played); setPlayed(state.played);
setLoaded(state.loaded); setLoaded(state.loaded);
@ -379,12 +373,11 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
url={videoUrl} url={videoUrl}
playing={playing} playing={playing}
playbackRate={playerPlaybackRate} playbackRate={playerPlaybackRate}
key={key} key={playerKey}
height="100%" height="100%"
width="100%" width="100%"
ref={playerRef} ref={playerRef}
volume={volume} volume={volume}
onReady={handleOnReady}
onStart={handleOnStart} onStart={handleOnStart}
onPlay={handleOnPlay} onPlay={handleOnPlay}
onDuration={handleDuration} onDuration={handleDuration}
@ -397,7 +390,7 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
shouldShowTools() ? ( shouldShowTools() ? (
<ExternalVideoPlayerToolbar <ExternalVideoPlayerToolbar
handleOnMuted={(m: boolean) => { setMute(m); }} handleOnMuted={(m: boolean) => { setMute(m); }}
handleReload={() => setKey(uniqueId('react-player'))} handleReload={() => setPlayerKey(uniqueId('react-player'))}
setShowHoverToolBar={setShowHoverToolBar} setShowHoverToolBar={setShowHoverToolBar}
toolbarStyle={toolbarStyle} toolbarStyle={toolbarStyle}
handleVolumeChanged={changeVolume} handleVolumeChanged={changeVolume}
@ -578,8 +571,8 @@ const ExternalVideoPlayerContainer: React.FC = () => {
fullscreenContext={fullscreenContext} fullscreenContext={fullscreenContext}
externalVideo={externalVideo} externalVideo={externalVideo}
getCurrentTime={getCurrentTime} getCurrentTime={getCurrentTime}
key={key} playerKey={key}
setKey={setKey} setPlayerKey={setKey}
sendMessage={sendMessage} sendMessage={sendMessage}
/> />
); );

View File

@ -1,8 +1,12 @@
export const enum ChatEvents { export const enum ChatEvents {
SENT_MESSAGE = 'sentMessage', SENT_MESSAGE = 'sentMessage',
CHAT_FOCUS_MESSAGE_REQUEST = 'ChatFocusMessageRequest', CHAT_FOCUS_MESSAGE_REQUEST = 'ChatFocusMessageRequest',
CHAT_KEYBOARD_FOCUS_MESSAGE_REQUEST = 'ChatKeyboardFocusMessageRequest',
CHAT_KEYBOARD_FOCUS_MESSAGE_CANCEL = 'ChatKeyboardFocusMessageCancel',
CHAT_REPLY_INTENTION = 'ChatReplyIntention', CHAT_REPLY_INTENTION = 'ChatReplyIntention',
CHAT_CANCEL_REPLY_INTENTION = 'ChatCancelReplyIntention',
CHAT_EDIT_REQUEST = 'ChatEditRequest', CHAT_EDIT_REQUEST = 'ChatEditRequest',
CHAT_CANCEL_EDIT_REQUEST = 'ChatCancelEditRequest',
CHAT_DELETE_REQUEST = 'ChatDeleteRequest', CHAT_DELETE_REQUEST = 'ChatDeleteRequest',
} }

View File

@ -11,6 +11,14 @@ const lgPaddingY = '0.6rem';
const jumboPaddingY = '1.5rem'; const jumboPaddingY = '1.5rem';
const jumboPaddingX = '3.025rem'; const jumboPaddingX = '3.025rem';
const xsPadding = '0.125rem';
const smPadding = '0.25rem';
const mdPadding = '0.375rem';
const lgPadding = '0.5rem';
const xlPadding = '0.75rem';
const $2xlPadding = '1rem';
const $3xlPadding = '1.25rem';
const whiteboardToolbarPadding = '.5rem'; const whiteboardToolbarPadding = '.5rem';
const whiteboardToolbarMargin = '.5rem'; const whiteboardToolbarMargin = '.5rem';
const whiteboardToolbarPaddingSm = '.3rem'; const whiteboardToolbarPaddingSm = '.3rem';
@ -170,4 +178,11 @@ export {
presentationMenuHeight, presentationMenuHeight,
styleMenuOffset, styleMenuOffset,
styleMenuOffsetSmall, styleMenuOffsetSmall,
lgPadding,
mdPadding,
smPadding,
$2xlPadding,
$3xlPadding,
xlPadding,
xsPadding,
}; };

View File

@ -12,6 +12,7 @@ const colorGrayLightest = 'var(--color-gray-lightest, #D4D9DF)';
const colorBlueLight = 'var(--color-blue-light, #54a1f3)'; const colorBlueLight = 'var(--color-blue-light, #54a1f3)';
const colorBlueLighter = 'var(--color-blue-lighter, #92BCEA)'; const colorBlueLighter = 'var(--color-blue-lighter, #92BCEA)';
const colorBlueLightest = 'var(--color-blue-lightest, #E4ECF2)'; const colorBlueLightest = 'var(--color-blue-lightest, #E4ECF2)';
const colorBlueLightestChannel = '228 236 242';
const colorTransparent = 'var(--color-transparent, #ff000000)'; const colorTransparent = 'var(--color-transparent, #ff000000)';
@ -135,6 +136,7 @@ export {
colorBlueLight, colorBlueLight,
colorBlueLighter, colorBlueLighter,
colorBlueLightest, colorBlueLightest,
colorBlueLightestChannel,
colorPrimary, colorPrimary,
colorDanger, colorDanger,
colorDangerDark, colorDangerDark,

View File

@ -20,6 +20,7 @@
"@emotion/styled": "^11.10.8", "@emotion/styled": "^11.10.8",
"@jitsi/sdp-interop": "0.1.14", "@jitsi/sdp-interop": "0.1.14",
"@mconf/bbb-diff": "^1.2.0", "@mconf/bbb-diff": "^1.2.0",
"@mui/base": "^5.0.0-beta.58",
"@mui/material": "^5.12.2", "@mui/material": "^5.12.2",
"@mui/system": "^5.12.3", "@mui/system": "^5.12.3",
"@types/node": "^20.5.7", "@types/node": "^20.5.7",
@ -2796,6 +2797,68 @@
"diff": "^5.0.0" "diff": "^5.0.0"
} }
}, },
"node_modules/@mui/base": {
"version": "5.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.58.tgz",
"integrity": "sha512-P0E7ZrxOuyYqBvVv9w8k7wm+Xzx/KRu+BGgFcR2htTsGCpJNQJCSUXNUZ50MUmSU9hzqhwbQWNXhV1MBTl6F7A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.0",
"@floating-ui/react-dom": "^2.1.1",
"@mui/types": "^7.2.15",
"@mui/utils": "6.0.0-rc.0",
"@popperjs/core": "^2.11.8",
"clsx": "^2.1.1",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/base/node_modules/@mui/utils": {
"version": "6.0.0-rc.0",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.0.0-rc.0.tgz",
"integrity": "sha512-tBp0ILEXDL0bbDDT8PnZOjCqSm5Dfk2N0Z45uzRw+wVl6fVvloC9zw8avl+OdX1Bg3ubs/ttKn8nRNv17bpM5A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.0",
"@mui/types": "^7.2.15",
"@types/prop-types": "^15.7.12",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^18.3.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/core-downloads-tracker": { "node_modules/@mui/core-downloads-tracker": {
"version": "5.16.6", "version": "5.16.6",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.6.tgz", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.6.tgz",

View File

@ -49,6 +49,7 @@
"@emotion/styled": "^11.10.8", "@emotion/styled": "^11.10.8",
"@jitsi/sdp-interop": "0.1.14", "@jitsi/sdp-interop": "0.1.14",
"@mconf/bbb-diff": "^1.2.0", "@mconf/bbb-diff": "^1.2.0",
"@mui/base": "^5.0.0-beta.58",
"@mui/material": "^5.12.2", "@mui/material": "^5.12.2",
"@mui/system": "^5.12.3", "@mui/system": "^5.12.3",
"@types/node": "^20.5.7", "@types/node": "^20.5.7",

View File

@ -29,8 +29,8 @@
"app.chat.breakoutDurationUpdated": "Breakout time is now {0} minutes", "app.chat.breakoutDurationUpdated": "Breakout time is now {0} minutes",
"app.chat.breakoutDurationUpdatedModerator": "Breakout rooms time is now {0} minutes, and a notification has been sent.", "app.chat.breakoutDurationUpdatedModerator": "Breakout rooms time is now {0} minutes, and a notification has been sent.",
"app.chat.emptyLogLabel": "Chat log empty", "app.chat.emptyLogLabel": "Chat log empty",
"app.chat.away": "is away", "app.chat.away": "{user} is away",
"app.chat.notAway": "is back", "app.chat.notAway": "{user} is back online",
"app.chat.clearPublicChatMessage": "The public chat history was cleared by a moderator", "app.chat.clearPublicChatMessage": "The public chat history was cleared by a moderator",
"app.chat.multi.typing": "Multiple users are typing", "app.chat.multi.typing": "Multiple users are typing",
"app.chat.someone.typing": "Someone is typing", "app.chat.someone.typing": "Someone is typing",
@ -49,6 +49,12 @@
"app.chat.toolbar.reactions.youLabel": "you", "app.chat.toolbar.reactions.youLabel": "you",
"app.chat.toolbar.reactions.andLabel": "and", "app.chat.toolbar.reactions.andLabel": "and",
"app.chat.toolbar.reactions.findReactionButtonLabel": "Find a reaction", "app.chat.toolbar.reactions.findReactionButtonLabel": "Find a reaction",
"app.chat.toolbar.edit.editing": "Editing message",
"app.chat.toolbar.edit.cancel": "Press {key} to cancel.",
"app.chat.toolbar.edit.edited": "Edited",
"app.chat.toolbar.delete.cancelLabel": "Cancel",
"app.chat.toolbar.delete.confirmationTitle": "Are you sure?",
"app.chat.toolbar.delete.confirmationDescription": "This action is permanent, you will not be able to access this message again.",
"app.timer.toolTipTimerStopped": "The timer has stopped.", "app.timer.toolTipTimerStopped": "The timer has stopped.",
"app.timer.toolTipTimerRunning": "The timer is running.", "app.timer.toolTipTimerRunning": "The timer is running.",
"app.timer.toolTipStopwatchStopped": "The stopwatch has stopped.", "app.timer.toolTipStopwatchStopped": "The stopwatch has stopped.",