styles(chat): a new chat UI
This commit is contained in:
parent
f91903945d
commit
83514efe58
@ -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]}
|
||||
|
||||
<Highlighted>{CANCEL_KEY_LABEL}</Highlighted>
|
||||
|
||||
{cancelMessage.split(CANCEL_KEY_LABEL)[1]}
|
||||
</span>
|
||||
<span className="sr-only" id="cancel-editing-msg">
|
||||
{`${editingMessage} ${cancelMessage}`}
|
||||
</span>
|
||||
</Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatEditingWarning;
|
@ -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,
|
||||
};
|
@ -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 && (
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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" />,
|
||||
]
|
||||
}
|
||||
</>
|
||||
|
@ -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);
|
||||
|
@ -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;
|
@ -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,
|
||||
};
|
@ -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}
|
||||
|
@ -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;
|
||||
`}
|
||||
|
@ -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;
|
@ -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,
|
||||
};
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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) => {
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
`;
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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;
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
||||
|
@ -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};
|
||||
`;
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -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};
|
||||
`}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -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',
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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,
|
||||
|
63
bigbluebutton-html5/package-lock.json
generated
63
bigbluebutton-html5/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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.",
|
||||
|
Loading…
Reference in New Issue
Block a user