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,
useMemo,
} from 'react';
import { useMutation } from '@apollo/client';
import TextareaAutosize from 'react-autosize-textarea';
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';
@ -32,13 +33,13 @@ import useMeeting from '/imports/ui/core/hooks/useMeeting';
import ChatOfflineIndicator from './chat-offline-indicator/component';
import { ChatEvents } from '/imports/ui/core/enums/chat';
import { useMutation } from '@apollo/client';
import { CHAT_SEND_MESSAGE, CHAT_SET_TYPING } from './mutations';
import Storage from '/imports/ui/services/storage/session';
import { indexOf, without } from '/imports/utils/array-utils';
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
import { throttle } from '/imports/utils/throttle';
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 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> = ({
title,
disabled,
@ -135,6 +138,7 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
const emojiPickerButtonRef = useRef(null);
const [isTextAreaFocused, setIsTextAreaFocused] = React.useState(false);
const [repliedMessageId, setRepliedMessageId] = React.useState<string>();
const [editingMessage, setEditingMessage] = React.useState<EditingMessage | null>(null);
const textAreaRef: RefObject<TextareaAutosize> = useRef<TextareaAutosize>(null);
const { isMobile } = deviceInfo;
const prevChatId = usePreviousValue(chatId);
@ -152,6 +156,10 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
const [chatSendMessage, {
loading: chatSendMessageLoading, error: chatSendMessageError,
}] = useMutation(CHAT_SEND_MESSAGE);
const [
chatEditMessage,
{ loading: chatEditMessageLoading },
] = useMutation(CHAT_EDIT_MESSAGE_MUTATION);
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
@ -284,17 +292,37 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
}, [message]);
useEffect(() => {
const handler = (e: Event) => {
const handleReplyIntention = (e: Event) => {
if (e instanceof CustomEvent) {
setRepliedMessageId(e.detail.messageId);
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 () => {
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;
}
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({
variables: {
chatMessageInMarkdownFormat: msg,
chatId: chatId === PUBLIC_CHAT_ID ? PUBLIC_GROUP_CHAT_ID : chatId,
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);
@ -459,65 +492,69 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
</Styled.EmojiPickerWrapper>
) : null}
<Styled.Wrapper>
<Styled.Input
id="message-input"
ref={textAreaRef}
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: title })}
aria-label={intl.formatMessage(messages.inputLabel, { 0: title })}
aria-invalid={hasErrors ? 'true' : 'false'}
autoCorrect="off"
autoComplete="off"
spellCheck="true"
disabled={disabled || partnerIsLoggedOut}
value={message}
onFocus={() => {
window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormUiDataNames.CHAT_INPUT_IS_FOCUSED, {
detail: {
value: true,
},
}));
setIsTextAreaFocused(true);
}}
onBlur={() => {
window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormUiDataNames.CHAT_INPUT_IS_FOCUSED, {
detail: {
value: false,
},
}));
}}
onChange={handleMessageChange}
onKeyDown={handleMessageKeyDown}
onPaste={(e) => { e.stopPropagation(); }}
onCut={(e) => { e.stopPropagation(); }}
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"
<Styled.InputWrapper>
<Styled.Input
id="message-input"
ref={textAreaRef}
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: title })}
aria-label={intl.formatMessage(messages.inputLabel, { 0: title })}
aria-invalid={hasErrors ? 'true' : 'false'}
autoCorrect="off"
autoComplete="off"
spellCheck="true"
disabled={disabled || partnerIsLoggedOut}
value={message}
onFocus={() => {
window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormUiDataNames.CHAT_INPUT_IS_FOCUSED, {
detail: {
value: true,
},
}));
setIsTextAreaFocused(true);
}}
onBlur={() => {
window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormUiDataNames.CHAT_INPUT_IS_FOCUSED, {
detail: {
value: false,
},
}));
}}
onChange={handleMessageChange}
onKeyDown={handleMessageKeyDown}
onPaste={(e) => { e.stopPropagation(); }}
onCut={(e) => { e.stopPropagation(); }}
onCopy={(e) => { e.stopPropagation(); }}
async
/>
) : null}
<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"
/>
{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}
</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>
{
error && (

View File

@ -1,18 +1,18 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import styled from 'styled-components';
import {
colorBlueLight,
colorText,
colorGrayLighter,
colorPrimary,
colorDanger,
colorGrayDark,
colorGrayLightest,
} from '/imports/ui/stylesheets/styled-components/palette';
import {
smPaddingX,
smPaddingY,
borderRadius,
borderSize,
xsPadding,
} from '/imports/ui/stylesheets/styled-components/general';
import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
import TextareaAutosize from 'react-autosize-textarea';
@ -43,13 +43,15 @@ const Form = styled.form<FormProps>`
const Wrapper = styled.div`
display: flex;
flex-direction: row;
box-shadow: inset 0 0 0 1px ${colorGrayLightest};
border-radius: 0.75rem;
`;
const Input = styled(TextareaAutosize)`
flex: 1;
background: #fff;
background-clip: padding-box;
margin: 0;
margin: ${xsPadding} 0 ${xsPadding} ${xsPadding};
color: ${colorText};
-webkit-appearance: none;
padding: calc(${smPaddingY} * 2.5) calc(${smPaddingX} * 1.25);
@ -60,8 +62,17 @@ const Input = styled(TextareaAutosize)`
line-height: 1;
min-height: 2.5rem;
max-height: 10rem;
border: 1px solid ${colorGrayLighter};
box-shadow: 0 0 0 1px ${colorGrayLighter};
border: none;
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] {
@ -69,29 +80,22 @@ const Input = styled(TextareaAutosize)`
opacity: .75;
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
const SendButton = styled(Button)`
margin:0 0 0 ${smPaddingX};
align-self: center;
font-size: 0.9rem;
height: 100%;
& > span {
height: 100%;
display: flex;
align-items: center;
border-radius: 0 0.75rem 0.75rem 0;
}
[dir="rtl"] & {
margin: 0 ${smPaddingX} 0 0;
-webkit-transform: scale(-1, 1);
-moz-transform: scale(-1, 1);
-ms-transform: scale(-1, 1);
@ -161,6 +165,28 @@ const EmojiPicker = styled(EmojiPickerComponent)`
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 {
Form,
Wrapper,
@ -171,4 +197,5 @@ export default {
EmojiPicker,
EmojiPickerWrapper,
ChatMessageError,
InputWrapper,
};

View File

@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, {
useCallback,
useEffect,
useState,
useMemo,
KeyboardEventHandler,
} from 'react';
import { makeVar, useMutation } from '@apollo/client';
import { defineMessages, useIntl } from 'react-intl';
@ -26,6 +26,8 @@ import {
import useReactiveRef from '/imports/ui/hooks/useReactiveRef';
import useStickyScroll from '/imports/ui/hooks/useStickyScroll';
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;
@ -104,6 +106,60 @@ const dispatchLastSeen = () => setTimeout(() => {
}
}, 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> = ({
totalPages,
chatId,
@ -126,6 +182,7 @@ const ChatMessageList: React.FC<ChatListProps> = ({
const [userLoadedBackUntilPage, setUserLoadedBackUntilPage] = useState<number | null>(null);
const [lastMessageCreatedAt, setLastMessageCreatedAt] = useState<string>('');
const [followingTail, setFollowingTail] = React.useState(true);
const [selectedMessage, setSelectedMessage] = React.useState<HTMLElement | null>(null);
const {
childRefProxy: sentinelRefProxy,
intersecting: isSentinelVisible,
@ -259,6 +316,17 @@ const ChatMessageList: React.FC<ChatListProps> = ({
: Math.max(totalPages - 2, 0);
const pagesToLoad = (totalPages - firstPageToLoad) || 1;
const rove: KeyboardEventHandler<HTMLElement> = (e) => {
if (messageListRef.current) {
roving(
e,
setSelectedMessage,
messageListRef.current,
selectedMessage,
);
}
};
return (
<>
{
@ -276,7 +344,15 @@ const ChatMessageList: React.FC<ChatListProps> = ({
isRTL={isRTL}
ref={messageListContainerRefProxy}
>
<div ref={messageListRef}>
<div
role="listbox"
ref={messageListRef}
tabIndex={0}
onKeyDown={rove}
onBlur={() => {
setSelectedMessage(null);
}}
>
{userLoadedBackUntilPage ? (
<ButtonLoadMore
onClick={() => {
@ -301,6 +377,9 @@ const ChatMessageList: React.FC<ChatListProps> = ({
chatId={chatId}
markMessageAsSeen={markMessageAsSeen}
scrollRef={messageListContainerRefProxy}
focusedId={selectedMessage?.dataset.sequence
? Number.parseInt(selectedMessage?.dataset.sequence, 10)
: null}
/>
);
})}
@ -311,10 +390,13 @@ const ChatMessageList: React.FC<ChatListProps> = ({
height: 1,
background: 'none',
}}
tabIndex={-1}
aria-hidden
/>
</MessageList>,
renderUnreadNotification,
<ChatReplyIntention />,
<ChatReplyIntention key="chatReplyIntention" />,
<ChatEditingWarning key="chatEditingWarning" />,
]
}
</>

View File

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

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 {
text: string;
emphasizedMessage: boolean;
systemMsg: boolean;
dataTest?: string | null;
}
const ChatMessageTextContent: React.FC<ChatMessageTextContentProps> = ({
text,
emphasizedMessage,
systemMsg,
dataTest = 'messageContent',
}) => {
const { allowedElements } = window.meetingClientSettings.public.chat;
return (
<Styled.ChatMessage systemMsg={systemMsg} emphasizedMessage={emphasizedMessage} data-test={dataTest}>
<Styled.ChatMessage emphasizedMessage={emphasizedMessage} data-test={dataTest}>
<ReactMarkdown
linkTarget="_blank"
allowedElements={allowedElements}

View File

@ -1,11 +1,5 @@
import styled from 'styled-components';
import {
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';
import { colorText } from '/imports/ui/stylesheets/styled-components/palette';
interface ChatMessageProps {
emphasizedMessage: boolean;
@ -19,17 +13,7 @@ export const ChatMessage = styled.div<ChatMessageProps>`
flex-direction: column;
color: ${colorText};
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 && `
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 { useIntl, defineMessages, FormattedTime } from 'react-intl';
import Icon from '/imports/ui/components/common/icon/component';
import Styled from './styles';
const intlMessages = defineMessages({
@ -7,6 +8,10 @@ const intlMessages = defineMessages({
id: 'app.chat.offline',
description: 'Offline',
},
edited: {
id: 'app.chat.toolbar.edit.edited',
description: 'Edited',
},
});
interface ChatMessageHeaderProps {
@ -15,6 +20,7 @@ interface ChatMessageHeaderProps {
dateTime: Date;
sameSender: boolean;
deleteTime: Date | null;
editTime: Date | null;
}
const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
@ -23,6 +29,7 @@ const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
currentlyInMeeting,
dateTime,
deleteTime,
editTime,
}) => {
const intl = useIntl();
if (sameSender) return null;
@ -40,11 +47,20 @@ const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
</Styled.ChatUserOffline>
)
}
{!deleteTime && (
<Styled.ChatTime>
<FormattedTime value={dateTime} />
</Styled.ChatTime>
{!deleteTime && editTime && (
<Styled.EditLabel>
<Icon iconName="pen_tool" />
<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.HeaderContent>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,21 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useMutation } from '@apollo/client';
import Popover from '@mui/material/Popover';
import { FocusTrap } from '@mui/base/FocusTrap';
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 { ChatEvents } from '/imports/ui/core/enums/chat';
import { defineMessages, useIntl } from 'react-intl';
import ConfirmationModal from '/imports/ui/components/common/modal/confirmation/component';
import {
Container,
Divider,
EmojiButton,
EmojiPicker,
EmojiPickerWrapper,
EmojiButton,
Root,
} from './styles';
import { colorDanger, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
import { CHAT_DELETE_MESSAGE_MUTATION } from '../mutations';
import {
useIsChatMessageReactionsEnabled,
useIsDeleteChatMessageEnabled,
useIsEditChatMessageEnabled,
useIsReplyChatMessageEnabled,
} from '/imports/ui/services/features';
const intlMessages = defineMessages({
reply: {
@ -35,6 +30,18 @@ const intlMessages = defineMessages({
id: 'app.chat.toolbar.delete',
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 {
@ -47,198 +54,183 @@ interface ChatMessageToolbarProps {
messageSequence: number;
emphasizedMessage: boolean;
onEmojiSelected(emoji: { id: string; native: string }): void;
onEditRequest(): void;
onMenuOpenChange(open: boolean): void;
menuIsOpen: boolean;
onReactionPopoverOpenChange(open: boolean): void;
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 {
messageId, chatId, message, username, onEmojiSelected, onMenuOpenChange,
messageSequence, emphasizedMessage, onEditRequest, own, amIModerator, menuIsOpen,
onReactionPopoverOpenChange, reactionPopoverIsOpen,
messageId, chatId, message, username, onEmojiSelected, deleted,
messageSequence, emphasizedMessage, own, amIModerator, locked,
onReactionPopoverOpenChange, reactionPopoverIsOpen, hasToolbar, keyboardFocused,
chatDeleteEnabled, chatEditEnabled, chatReactionsEnabled, chatReplyEnabled,
} = props;
const [reactionsAnchor, setReactionsAnchor] = React.useState<Element | null>(
null,
);
const [isTryingToDelete, setIsTryingToDelete] = React.useState(false);
const intl = useIntl();
const [chatDeleteMessage] = useMutation(CHAT_DELETE_MESSAGE_MUTATION);
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 ([
CHAT_REPLIES_ENABLED,
CHAT_REACTIONS_ENABLED,
CHAT_EDIT_ENABLED,
CHAT_DELETE_ENABLED,
].every((config) => !config)) return null;
chatReplyEnabled,
chatReactionsEnabled,
chatEditEnabled,
chatDeleteEnabled,
].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 (
<Container
className="chat-message-toolbar"
$sequence={messageSequence}
$menuIsOpen={menuIsOpen}
<Root
$reactionPopoverIsOpen={reactionPopoverIsOpen}
onKeyDown={(e) => {
if (e.key === 'Escape' && keyboardFocused) {
window.dispatchEvent(new CustomEvent(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_CANCEL));
}
}}
>
{CHAT_REPLIES_ENABLED && (
<>
<Button
circle
ghost
aria-describedby={`chat-reply-btn-label-${messageSequence}`}
icon="undo"
color="light"
size="sm"
type="button"
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_REPLY_INTENTION, {
detail: {
username,
message,
messageId,
chatId,
emphasizedMessage,
sequence: messageSequence,
},
}),
);
}}
/>
<span id={`chat-reply-btn-label-${messageSequence}`} className="sr-only">
{intl.formatMessage(intlMessages.reply, { 0: messageSequence })}
</span>
</>
)}
{CHAT_REACTIONS_ENABLED && (
<EmojiButton
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
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);
<FocusTrap open={keyboardFocused}>
<Container className="chat-message-toolbar">
{showReplyButton && (
<>
<EmojiButton
aria-describedby={`chat-reply-btn-label-${messageSequence}`}
icon="undo"
color="light"
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_REPLY_INTENTION, {
detail: {
username,
message,
messageId,
chatId,
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>
</>
)}
{showReactionsButton && (
<EmojiButton
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
onReactionPopoverOpenChange(true);
}}
size="sm"
icon="more"
icon="happy"
color="light"
ghost
type="button"
circle
data-test="emojiPickerButton"
ref={setReactionsAnchor}
/>
)}
actions={actions}
onCloseCallback={() => {
onMenuOpenChange(false);
}}
opts={{
id: 'app-settings-dropdown-menu',
keepMounted: true,
transitionDuration: 0,
elevation: 3,
getcontentanchorel: null,
fullwidth: 'true',
anchorOrigin: {
vertical: 'bottom',
horizontal: isRTL ? 'left' : 'right',
},
transformorigin: {
{showDivider && <Divider role="separator" />}
{showEditButton && (
<EmojiButton
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
window.dispatchEvent(
new CustomEvent(ChatEvents.CHAT_EDIT_REQUEST, {
detail: {
messageId,
chatId,
message,
},
}),
);
window.dispatchEvent(
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',
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}
showSkinTones={false}
/>
</EmojiPickerWrapper>
</Popover>
</Container>
transformOrigin={{
vertical: 'top',
horizontal: isRTL ? 'right' : 'left',
}}
>
<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 {
colorGrayLighter,
colorOffWhite,
colorGrayLightest,
colorWhite,
} 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 Button from '/imports/ui/components/common/button/component';
import BaseEmojiButton from './emoji-button/component';
const Container = styled.div<{ $sequence: number, $menuIsOpen: boolean, $reactionPopoverIsOpen: boolean }>`
height: calc(1.5rem + 12px);
line-height: calc(1.5rem + 8px);
max-width: 184px;
overflow: hidden;
interface RootProps {
$reactionPopoverIsOpen: boolean;
}
const Root = styled.div<RootProps>`
padding-bottom: ${smPadding};
justify-content: flex-end;
display: none;
position: absolute;
right: 0;
border: 1px solid ${colorOffWhite};
border-radius: 8px;
padding: 1px;
background-color: ${colorWhite};
z-index: 2;
bottom: 100%;
z-index: 10;
#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;
}
${({ $menuIsOpen, $reactionPopoverIsOpen }) => (($menuIsOpen || $reactionPopoverIsOpen) && css`
${({ $reactionPopoverIsOpen }) => ($reactionPopoverIsOpen && css`
display: flex;
`)}
`;
${({ $sequence }) => (($sequence === 1)
? css`
bottom: 0;
transform: translateY(50%);
`
: css`
top: 0;
transform: translateY(-50%);
`)}
const Container = styled.div`
max-width: max-content;
display: flex;
border-radius: 1rem;
background-color: ${colorWhite};
box-shadow: 0 0.125rem 0.125rem 0 ${colorGrayLighter};
`;
const EmojiPickerWrapper = styled.div`
@ -47,7 +58,7 @@ const EmojiPickerWrapper = styled.div`
right: 0;
border: 1px solid ${colorGrayLighter};
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;
.emoji-mart {
@ -70,29 +81,25 @@ const EmojiPickerWrapper = styled.div`
}
`;
// @ts-ignore
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 EmojiButton = styled(BaseEmojiButton)``;
const EmojiPicker = styled(EmojiPickerComponent)`
position: relative;
`;
const Divider = styled.div`
width: 0.125rem;
height: 75%;
border-radius: 0.5rem;
background-color: ${colorGrayLightest};
align-self: center;
`;
export {
Container,
EmojiPicker,
EmojiPickerWrapper,
EmojiButton,
Root,
Divider,
};

View File

@ -1,22 +1,26 @@
import styled, { css } from 'styled-components';
import {
borderSize,
userIndicatorsOffset,
smPaddingX,
smPaddingY,
lgPadding,
$3xlPadding,
xlPadding,
mdPadding,
} from '/imports/ui/stylesheets/styled-components/general';
import {
lineHeightComputed,
fontSizeBase,
fontSizeSmaller,
} from '/imports/ui/stylesheets/styled-components/typography';
import {
colorWhite,
userListBg,
colorSuccess,
colorOffWhite,
colorText,
palettePlaceholderText,
colorBlueLightest,
colorGrayLight,
colorGrayLightest,
} from '/imports/ui/stylesheets/styled-components/palette';
import Header from '/imports/ui/components/common/control-header/component';
@ -26,17 +30,16 @@ interface ChatWrapperProps {
isSystemSender: boolean;
isPresentationUpload?: boolean;
isCustomPluginMessage: boolean;
$highlight: boolean;
$toolbarMenuIsOpen: boolean;
$reactionPopoverIsOpen: boolean;
}
interface ChatContentProps {
sameSender: boolean;
isCustomPluginMessage: boolean;
$editing: boolean;
$highlight: boolean;
$toolbarMenuIsOpen: boolean;
$reactionPopoverIsOpen: boolean;
$focused: boolean;
$keyboardFocused: boolean;
}
interface ChatAvatarProps {
@ -48,12 +51,17 @@ interface ChatAvatarProps {
export const ChatWrapper = styled.div<ChatWrapperProps>`
pointer-events: auto;
display: flex;
flex-flow: column;
gap: ${smPaddingY};
position: relative;
font-size: ${fontSizeBase};
position: relative;
[dir='rtl'] & {
direction: rtl;
}
display: flex;
flex-flow: row;
position: relative;
${({ isPresentationUpload }) => isPresentationUpload && `
border-left: 2px solid #0F70D7;
margin-top: 1rem;
@ -61,18 +69,6 @@ export const ChatWrapper = styled.div<ChatWrapperProps>`
word-break: break-word;
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 && `
background-color: #fef9f1;
border-left: 2px solid #f5c67f;
@ -83,42 +79,25 @@ export const ChatWrapper = styled.div<ChatWrapperProps>`
margin: 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>`
display: flex;
flex-flow: column;
width: 100%;
border-radius: 0.5rem;
${({ sameSender, isCustomPluginMessage }) => sameSender
&& !isCustomPluginMessage && `
margin-left: 2.6rem;
`}
${({ sameSender, $highlight }) => sameSender && $highlight && `
&:hover {
background-color: ${colorOffWhite};
${({ $highlight }) => $highlight && `
.chat-message-wrapper:hover > & {
background-color: ${colorBlueLightest};
}
border-radius: 6px;
`}
${({ sameSender, $toolbarMenuIsOpen, $reactionPopoverIsOpen }) => sameSender
&& ($toolbarMenuIsOpen || $reactionPopoverIsOpen)
${({
$editing, $reactionPopoverIsOpen, $focused, $keyboardFocused,
}) => ($reactionPopoverIsOpen || $editing || $focused || $keyboardFocused)
&& `
background-color: ${colorOffWhite};
border-radius: 6px;
background-color: ${colorBlueLightest};
`}
`;
@ -212,29 +191,46 @@ export const ChatAvatar = styled.div<ChatAvatarProps>`
}
`;
export const Container = styled.div`
export const Container = styled.div<{ $sequence: number }>`
display: flex;
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;
flex-direction: row;
padding: ${lgPadding} ${$3xlPadding};
${({ $edited, $sameSender }) => $edited && $sameSender && `
padding-bottom: 0;
`}
`;
export const DeleteMessage = styled.span`
font-style: italic;
font-weight: bold;
color: ${colorText};
color: ${colorGrayLight};
padding: ${mdPadding} ${xlPadding};
border: 1px solid ${colorGrayLightest};
border-radius: 0.375rem;
`;
export const ChatEditTime = styled.time`
flex-shrink: 1;
flex-grow: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
min-width: 0;
font-size: 75%;
color: ${palettePlaceholderText};
export const ChatHeading = styled.div`
display: flex;
`;
export const EditLabel = styled.span`
color: ${colorGrayLight};
font-size: 75%;
display: flex;
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, {
useContext,
useEffect,
@ -20,6 +19,7 @@ import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
import { useCreateUseSubscription } from '/imports/ui/core/hooks/createUseSubscription';
import { setLoadedMessageGathering } from '/imports/ui/core/hooks/useLoadedChatMessages';
import { ChatLoading } from '../../component';
import { ChatEvents } from '/imports/ui/core/enums/chat';
interface ChatListPageContainerProps {
page: number;
@ -29,6 +29,7 @@ interface ChatListPageContainerProps {
chatId: string;
markMessageAsSeen: (message: Message) => void;
scrollRef: React.RefObject<HTMLDivElement>;
focusedId: number | null;
}
interface ChatListPageProps {
@ -38,6 +39,7 @@ interface ChatListPageProps {
page: number;
markMessageAsSeen: (message: Message)=> void;
scrollRef: React.RefObject<HTMLDivElement>;
focusedId: number | null;
}
const areChatPagesEqual = (prevProps: ChatListPageProps, nextProps: ChatListPageProps) => {
@ -53,7 +55,7 @@ const areChatPagesEqual = (prevProps: ChatListPageProps, nextProps: ChatListPage
&& prevMessage?.message === nextMessage?.message
&& prevMessage?.reactions?.length === nextMessage?.reactions?.length
);
});
}) && prevProps.focusedId === nextProps.focusedId;
};
const ChatListPage: React.FC<ChatListPageProps> = ({
@ -63,6 +65,7 @@ const ChatListPage: React.FC<ChatListPageProps> = ({
page,
markMessageAsSeen,
scrollRef,
focusedId,
}) => {
const { domElementManipulationIdentifiers } = useContext(PluginsContext);
@ -83,6 +86,29 @@ const ChatListPage: React.FC<ChatListPageProps> = ({
);
}, [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 (
<React.Fragment key={`messagePage-${page}`}>
{messages.map((message, index, messagesArray) => {
@ -99,6 +125,8 @@ const ChatListPage: React.FC<ChatListPageProps> = ({
scrollRef={scrollRef}
markMessageAsSeen={markMessageAsSeen}
messageReadFeedbackEnabled={messageReadFeedbackEnabled}
focused={focusedId === message.messageSequence}
keyboardFocused={keyboardFocusedMessageSequence === message.messageSequence}
/>
);
})}
@ -116,8 +144,8 @@ const ChatListPageContainer: React.FC<ChatListPageContainerProps> = ({
chatId,
markMessageAsSeen,
scrollRef,
focusedId,
}) => {
// @ts-ignore - temporary, while meteor exists in the project
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
const PRIVATE_MESSAGE_READ_FEEDBACK_ENABLED = CHAT_CONFIG.privateMessageReadFeedback.enabled;
@ -157,6 +185,7 @@ const ChatListPageContainer: React.FC<ChatListPageContainerProps> = ({
page={page}
markMessageAsSeen={markMessageAsSeen}
scrollRef={scrollRef}
focusedId={focusedId}
/>
);
};

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useRef } from 'react';
import { CircularProgress } from '@mui/material';
import ChatHeader from './chat-header/component';
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 browserInfo from '/imports/utils/browserInfo';
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
import { ChatEvents } from '/imports/ui/core/enums/chat';
interface ChatProps {
isRTL: boolean;
@ -21,6 +22,7 @@ interface ChatProps {
const Chat: React.FC<ChatProps> = ({ isRTL }) => {
const { isChrome } = browserInfo;
const isEditingMessage = useRef(false);
React.useEffect(() => {
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('keydown', handleKeyDown);
window.addEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleEditingMessage);
window.addEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelEditingMessage);
return () => {
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;
playing: boolean;
playerPlaybackRate: number;
key: string;
playerKey: string;
isSidebarContentOpen: boolean;
setKey: (key: string) => void;
setPlayerKey: (key: string) => void;
sendMessage: (event: string, data: {
rate: number;
time: number;
@ -104,8 +104,8 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
playing,
playerPlaybackRate,
isEchoTest,
key,
setKey,
playerKey,
setPlayerKey,
sendMessage,
getCurrentTime,
}) => {
@ -130,15 +130,15 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
return {
// default option for all players, can be overwritten
playerOptions: {
autoplay: true,
playsinline: true,
autoPlay: true,
playsInline: true,
controls: isPresenter,
},
file: {
attributes: {
controls: isPresenter ? 'controls' : '',
autoplay: 'autoplay',
playsinline: 'playsinline',
autoPlay: true,
playsInline: true,
},
},
facebook: {
@ -263,16 +263,14 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
useEffect(() => {
if (isPresenter !== presenterRef.current) {
setKey(uniqueId('react-player'));
setPlayerKey(uniqueId('react-player'));
presenterRef.current = isPresenter;
}
}, [isPresenter]);
const handleOnStart = () => {
if (!isPresenter) {
currentTime = getCurrentTime();
playerRef.current?.seekTo(truncateTime(currentTime), 'seconds');
}
currentTime = getCurrentTime();
playerRef.current?.seekTo(truncateTime(currentTime), 'seconds');
};
const handleOnPlay = () => {
@ -308,10 +306,6 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
}
};
const handleOnReady = (reactPlayer: ReactPlayer) => {
reactPlayer.seekTo(truncateTime(currentTime), 'seconds');
};
const handleProgress = (state: OnProgressProps) => {
setPlayed(state.played);
setLoaded(state.loaded);
@ -379,12 +373,11 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
url={videoUrl}
playing={playing}
playbackRate={playerPlaybackRate}
key={key}
key={playerKey}
height="100%"
width="100%"
ref={playerRef}
volume={volume}
onReady={handleOnReady}
onStart={handleOnStart}
onPlay={handleOnPlay}
onDuration={handleDuration}
@ -397,7 +390,7 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
shouldShowTools() ? (
<ExternalVideoPlayerToolbar
handleOnMuted={(m: boolean) => { setMute(m); }}
handleReload={() => setKey(uniqueId('react-player'))}
handleReload={() => setPlayerKey(uniqueId('react-player'))}
setShowHoverToolBar={setShowHoverToolBar}
toolbarStyle={toolbarStyle}
handleVolumeChanged={changeVolume}
@ -578,8 +571,8 @@ const ExternalVideoPlayerContainer: React.FC = () => {
fullscreenContext={fullscreenContext}
externalVideo={externalVideo}
getCurrentTime={getCurrentTime}
key={key}
setKey={setKey}
playerKey={key}
setPlayerKey={setKey}
sendMessage={sendMessage}
/>
);

View File

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

View File

@ -11,6 +11,14 @@ const lgPaddingY = '0.6rem';
const jumboPaddingY = '1.5rem';
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 whiteboardToolbarMargin = '.5rem';
const whiteboardToolbarPaddingSm = '.3rem';
@ -170,4 +178,11 @@ export {
presentationMenuHeight,
styleMenuOffset,
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 colorBlueLighter = 'var(--color-blue-lighter, #92BCEA)';
const colorBlueLightest = 'var(--color-blue-lightest, #E4ECF2)';
const colorBlueLightestChannel = '228 236 242';
const colorTransparent = 'var(--color-transparent, #ff000000)';
@ -135,6 +136,7 @@ export {
colorBlueLight,
colorBlueLighter,
colorBlueLightest,
colorBlueLightestChannel,
colorPrimary,
colorDanger,
colorDangerDark,

View File

@ -20,6 +20,7 @@
"@emotion/styled": "^11.10.8",
"@jitsi/sdp-interop": "0.1.14",
"@mconf/bbb-diff": "^1.2.0",
"@mui/base": "^5.0.0-beta.58",
"@mui/material": "^5.12.2",
"@mui/system": "^5.12.3",
"@types/node": "^20.5.7",
@ -2796,6 +2797,68 @@
"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": {
"version": "5.16.6",
"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",
"@jitsi/sdp-interop": "0.1.14",
"@mconf/bbb-diff": "^1.2.0",
"@mui/base": "^5.0.0-beta.58",
"@mui/material": "^5.12.2",
"@mui/system": "^5.12.3",
"@types/node": "^20.5.7",

View File

@ -29,8 +29,8 @@
"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.emptyLogLabel": "Chat log empty",
"app.chat.away": "is away",
"app.chat.notAway": "is back",
"app.chat.away": "{user} is away",
"app.chat.notAway": "{user} is back online",
"app.chat.clearPublicChatMessage": "The public chat history was cleared by a moderator",
"app.chat.multi.typing": "Multiple users are typing",
"app.chat.someone.typing": "Someone is typing",
@ -49,6 +49,12 @@
"app.chat.toolbar.reactions.youLabel": "you",
"app.chat.toolbar.reactions.andLabel": "and",
"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.toolTipTimerRunning": "The timer is running.",
"app.timer.toolTipStopwatchStopped": "The stopwatch has stopped.",