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,
|
useRef,
|
||||||
useMemo,
|
useMemo,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { useMutation } from '@apollo/client';
|
||||||
import TextareaAutosize from 'react-autosize-textarea';
|
import TextareaAutosize from 'react-autosize-textarea';
|
||||||
import { ChatFormCommandsEnum } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-commands/chat/form/enums';
|
import { ChatFormCommandsEnum } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-commands/chat/form/enums';
|
||||||
import { FillChatFormCommandArguments } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-commands/chat/form/types';
|
import { FillChatFormCommandArguments } from 'bigbluebutton-html-plugin-sdk/dist/cjs/ui-commands/chat/form/types';
|
||||||
@ -32,13 +33,13 @@ import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
|||||||
|
|
||||||
import ChatOfflineIndicator from './chat-offline-indicator/component';
|
import ChatOfflineIndicator from './chat-offline-indicator/component';
|
||||||
import { ChatEvents } from '/imports/ui/core/enums/chat';
|
import { ChatEvents } from '/imports/ui/core/enums/chat';
|
||||||
import { useMutation } from '@apollo/client';
|
|
||||||
import { CHAT_SEND_MESSAGE, CHAT_SET_TYPING } from './mutations';
|
import { CHAT_SEND_MESSAGE, CHAT_SET_TYPING } from './mutations';
|
||||||
import Storage from '/imports/ui/services/storage/session';
|
import Storage from '/imports/ui/services/storage/session';
|
||||||
import { indexOf, without } from '/imports/utils/array-utils';
|
import { indexOf, without } from '/imports/utils/array-utils';
|
||||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||||
import { throttle } from '/imports/utils/throttle';
|
import { throttle } from '/imports/utils/throttle';
|
||||||
import logger from '/imports/startup/client/logger';
|
import logger from '/imports/startup/client/logger';
|
||||||
|
import { CHAT_EDIT_MESSAGE_MUTATION } from '../chat-message-list/page/chat-message/mutations';
|
||||||
|
|
||||||
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
|
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
|
||||||
const START_TYPING_THROTTLE_INTERVAL = 1000;
|
const START_TYPING_THROTTLE_INTERVAL = 1000;
|
||||||
@ -113,6 +114,8 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type EditingMessage = { chatId: string; messageId: string, message: string };
|
||||||
|
|
||||||
const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
||||||
title,
|
title,
|
||||||
disabled,
|
disabled,
|
||||||
@ -135,6 +138,7 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
|||||||
const emojiPickerButtonRef = useRef(null);
|
const emojiPickerButtonRef = useRef(null);
|
||||||
const [isTextAreaFocused, setIsTextAreaFocused] = React.useState(false);
|
const [isTextAreaFocused, setIsTextAreaFocused] = React.useState(false);
|
||||||
const [repliedMessageId, setRepliedMessageId] = React.useState<string>();
|
const [repliedMessageId, setRepliedMessageId] = React.useState<string>();
|
||||||
|
const [editingMessage, setEditingMessage] = React.useState<EditingMessage | null>(null);
|
||||||
const textAreaRef: RefObject<TextareaAutosize> = useRef<TextareaAutosize>(null);
|
const textAreaRef: RefObject<TextareaAutosize> = useRef<TextareaAutosize>(null);
|
||||||
const { isMobile } = deviceInfo;
|
const { isMobile } = deviceInfo;
|
||||||
const prevChatId = usePreviousValue(chatId);
|
const prevChatId = usePreviousValue(chatId);
|
||||||
@ -152,6 +156,10 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
|||||||
const [chatSendMessage, {
|
const [chatSendMessage, {
|
||||||
loading: chatSendMessageLoading, error: chatSendMessageError,
|
loading: chatSendMessageLoading, error: chatSendMessageError,
|
||||||
}] = useMutation(CHAT_SEND_MESSAGE);
|
}] = useMutation(CHAT_SEND_MESSAGE);
|
||||||
|
const [
|
||||||
|
chatEditMessage,
|
||||||
|
{ loading: chatEditMessageLoading },
|
||||||
|
] = useMutation(CHAT_EDIT_MESSAGE_MUTATION);
|
||||||
|
|
||||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||||
@ -284,17 +292,37 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
|||||||
}, [message]);
|
}, [message]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: Event) => {
|
const handleReplyIntention = (e: Event) => {
|
||||||
if (e instanceof CustomEvent) {
|
if (e instanceof CustomEvent) {
|
||||||
setRepliedMessageId(e.detail.messageId);
|
setRepliedMessageId(e.detail.messageId);
|
||||||
textAreaRef.current?.textarea.focus();
|
textAreaRef.current?.textarea.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener(ChatEvents.CHAT_REPLY_INTENTION, handler);
|
const handleEditingMessage = (e: Event) => {
|
||||||
|
if (e instanceof CustomEvent) {
|
||||||
|
if (textAreaRef.current) {
|
||||||
|
setMessage(e.detail.message);
|
||||||
|
setEditingMessage(e.detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelEditingMessage = (e: Event) => {
|
||||||
|
if (e instanceof CustomEvent) {
|
||||||
|
setMessage('');
|
||||||
|
setEditingMessage(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener(ChatEvents.CHAT_REPLY_INTENTION, handleReplyIntention);
|
||||||
|
window.addEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleEditingMessage);
|
||||||
|
window.addEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelEditingMessage);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener(ChatEvents.CHAT_REPLY_INTENTION, handler);
|
window.removeEventListener(ChatEvents.CHAT_REPLY_INTENTION, handleReplyIntention);
|
||||||
|
window.removeEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleEditingMessage);
|
||||||
|
window.removeEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelEditingMessage);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -314,25 +342,30 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chatSendMessageLoading) {
|
if (editingMessage && !chatEditMessageLoading) {
|
||||||
|
chatEditMessage({
|
||||||
|
variables: {
|
||||||
|
chatId: editingMessage.chatId,
|
||||||
|
messageId: editingMessage.messageId,
|
||||||
|
chatMessageInMarkdownFormat: msg,
|
||||||
|
},
|
||||||
|
}).then(() => {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(ChatEvents.CHAT_CANCEL_EDIT_REQUEST),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else if (!chatSendMessageLoading) {
|
||||||
chatSendMessage({
|
chatSendMessage({
|
||||||
variables: {
|
variables: {
|
||||||
chatMessageInMarkdownFormat: msg,
|
chatMessageInMarkdownFormat: msg,
|
||||||
chatId: chatId === PUBLIC_CHAT_ID ? PUBLIC_GROUP_CHAT_ID : chatId,
|
chatId: chatId === PUBLIC_CHAT_ID ? PUBLIC_GROUP_CHAT_ID : chatId,
|
||||||
replyToMessageId: repliedMessageId,
|
replyToMessageId: repliedMessageId,
|
||||||
},
|
},
|
||||||
|
}).then(() => {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(ChatEvents.CHAT_CANCEL_REPLY_INTENTION),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent(ChatEvents.CHAT_REPLY_INTENTION, {
|
|
||||||
detail: {
|
|
||||||
username: undefined,
|
|
||||||
message: undefined,
|
|
||||||
messageId: undefined,
|
|
||||||
chatId: undefined,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY);
|
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY);
|
||||||
|
|
||||||
@ -459,65 +492,69 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
|||||||
</Styled.EmojiPickerWrapper>
|
</Styled.EmojiPickerWrapper>
|
||||||
) : null}
|
) : null}
|
||||||
<Styled.Wrapper>
|
<Styled.Wrapper>
|
||||||
<Styled.Input
|
<Styled.InputWrapper>
|
||||||
id="message-input"
|
<Styled.Input
|
||||||
ref={textAreaRef}
|
id="message-input"
|
||||||
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: title })}
|
ref={textAreaRef}
|
||||||
aria-label={intl.formatMessage(messages.inputLabel, { 0: title })}
|
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: title })}
|
||||||
aria-invalid={hasErrors ? 'true' : 'false'}
|
aria-label={intl.formatMessage(messages.inputLabel, { 0: title })}
|
||||||
autoCorrect="off"
|
aria-invalid={hasErrors ? 'true' : 'false'}
|
||||||
autoComplete="off"
|
autoCorrect="off"
|
||||||
spellCheck="true"
|
autoComplete="off"
|
||||||
disabled={disabled || partnerIsLoggedOut}
|
spellCheck="true"
|
||||||
value={message}
|
disabled={disabled || partnerIsLoggedOut}
|
||||||
onFocus={() => {
|
value={message}
|
||||||
window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormUiDataNames.CHAT_INPUT_IS_FOCUSED, {
|
onFocus={() => {
|
||||||
detail: {
|
window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormUiDataNames.CHAT_INPUT_IS_FOCUSED, {
|
||||||
value: true,
|
detail: {
|
||||||
},
|
value: true,
|
||||||
}));
|
},
|
||||||
setIsTextAreaFocused(true);
|
}));
|
||||||
}}
|
setIsTextAreaFocused(true);
|
||||||
onBlur={() => {
|
}}
|
||||||
window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormUiDataNames.CHAT_INPUT_IS_FOCUSED, {
|
onBlur={() => {
|
||||||
detail: {
|
window.dispatchEvent(new CustomEvent(PluginSdk.ChatFormUiDataNames.CHAT_INPUT_IS_FOCUSED, {
|
||||||
value: false,
|
detail: {
|
||||||
},
|
value: false,
|
||||||
}));
|
},
|
||||||
}}
|
}));
|
||||||
onChange={handleMessageChange}
|
}}
|
||||||
onKeyDown={handleMessageKeyDown}
|
onChange={handleMessageChange}
|
||||||
onPaste={(e) => { e.stopPropagation(); }}
|
onKeyDown={handleMessageKeyDown}
|
||||||
onCut={(e) => { e.stopPropagation(); }}
|
onPaste={(e) => { e.stopPropagation(); }}
|
||||||
onCopy={(e) => { e.stopPropagation(); }}
|
onCut={(e) => { e.stopPropagation(); }}
|
||||||
async
|
onCopy={(e) => { e.stopPropagation(); }}
|
||||||
/>
|
async
|
||||||
{ENABLE_EMOJI_PICKER ? (
|
|
||||||
<Styled.EmojiButton
|
|
||||||
ref={emojiPickerButtonRef}
|
|
||||||
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
|
||||||
icon="happy"
|
|
||||||
color="light"
|
|
||||||
ghost
|
|
||||||
type="button"
|
|
||||||
circle
|
|
||||||
hideLabel
|
|
||||||
label={intl.formatMessage(messages.emojiButtonLabel)}
|
|
||||||
data-test="emojiPickerButton"
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
{ENABLE_EMOJI_PICKER ? (
|
||||||
<Styled.SendButton
|
<Styled.EmojiButton
|
||||||
hideLabel
|
ref={emojiPickerButtonRef}
|
||||||
circle
|
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
||||||
aria-label={intl.formatMessage(messages.submitLabel)}
|
icon="happy"
|
||||||
type="submit"
|
color="light"
|
||||||
disabled={disabled || partnerIsLoggedOut || chatSendMessageLoading}
|
ghost
|
||||||
label={intl.formatMessage(messages.submitLabel)}
|
type="button"
|
||||||
color="primary"
|
circle
|
||||||
icon="send"
|
hideLabel
|
||||||
onClick={() => { }}
|
label={intl.formatMessage(messages.emojiButtonLabel)}
|
||||||
data-test="sendMessageButton"
|
data-test="emojiPickerButton"
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
|
</Styled.InputWrapper>
|
||||||
|
<div style={{ zIndex: 10 }}>
|
||||||
|
<Styled.SendButton
|
||||||
|
hideLabel
|
||||||
|
circle
|
||||||
|
aria-label={intl.formatMessage(messages.submitLabel)}
|
||||||
|
type="submit"
|
||||||
|
disabled={disabled || partnerIsLoggedOut || chatSendMessageLoading}
|
||||||
|
label={intl.formatMessage(messages.submitLabel)}
|
||||||
|
color="primary"
|
||||||
|
icon="send"
|
||||||
|
onClick={() => { }}
|
||||||
|
data-test="sendMessageButton"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Styled.Wrapper>
|
</Styled.Wrapper>
|
||||||
{
|
{
|
||||||
error && (
|
error && (
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import {
|
||||||
colorBlueLight,
|
colorBlueLight,
|
||||||
colorText,
|
colorText,
|
||||||
colorGrayLighter,
|
colorGrayLighter,
|
||||||
colorPrimary,
|
|
||||||
colorDanger,
|
colorDanger,
|
||||||
colorGrayDark,
|
colorGrayDark,
|
||||||
|
colorGrayLightest,
|
||||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||||
import {
|
import {
|
||||||
smPaddingX,
|
smPaddingX,
|
||||||
smPaddingY,
|
smPaddingY,
|
||||||
borderRadius,
|
borderRadius,
|
||||||
borderSize,
|
borderSize,
|
||||||
|
xsPadding,
|
||||||
} from '/imports/ui/stylesheets/styled-components/general';
|
} from '/imports/ui/stylesheets/styled-components/general';
|
||||||
import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
|
import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
|
||||||
import TextareaAutosize from 'react-autosize-textarea';
|
import TextareaAutosize from 'react-autosize-textarea';
|
||||||
@ -43,13 +43,15 @@ const Form = styled.form<FormProps>`
|
|||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
box-shadow: inset 0 0 0 1px ${colorGrayLightest};
|
||||||
|
border-radius: 0.75rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Input = styled(TextareaAutosize)`
|
const Input = styled(TextareaAutosize)`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
margin: 0;
|
margin: ${xsPadding} 0 ${xsPadding} ${xsPadding};
|
||||||
color: ${colorText};
|
color: ${colorText};
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
padding: calc(${smPaddingY} * 2.5) calc(${smPaddingX} * 1.25);
|
padding: calc(${smPaddingY} * 2.5) calc(${smPaddingX} * 1.25);
|
||||||
@ -60,8 +62,17 @@ const Input = styled(TextareaAutosize)`
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
min-height: 2.5rem;
|
min-height: 2.5rem;
|
||||||
max-height: 10rem;
|
max-height: 10rem;
|
||||||
border: 1px solid ${colorGrayLighter};
|
border: none;
|
||||||
box-shadow: 0 0 0 1px ${colorGrayLighter};
|
box-shadow: none;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
[dir='ltr'] & {
|
||||||
|
border-radius: 0.75rem 0 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] & {
|
||||||
|
border-radius: 0 0.75rem 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
&:disabled,
|
&:disabled,
|
||||||
&[disabled] {
|
&[disabled] {
|
||||||
@ -69,29 +80,22 @@ const Input = styled(TextareaAutosize)`
|
|||||||
opacity: .75;
|
opacity: .75;
|
||||||
background-color: rgba(167,179,189,0.25);
|
background-color: rgba(167,179,189,0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border-radius: ${borderSize};
|
|
||||||
box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary};
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:active,
|
|
||||||
&:focus {
|
|
||||||
outline: transparent;
|
|
||||||
outline-style: dotted;
|
|
||||||
outline-width: ${borderSize};
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// @ts-ignore - as button comes from JS, we can't provide its props
|
// @ts-ignore - as button comes from JS, we can't provide its props
|
||||||
const SendButton = styled(Button)`
|
const SendButton = styled(Button)`
|
||||||
margin:0 0 0 ${smPaddingX};
|
|
||||||
align-self: center;
|
align-self: center;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 0 0.75rem 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
[dir="rtl"] & {
|
[dir="rtl"] & {
|
||||||
margin: 0 ${smPaddingX} 0 0;
|
|
||||||
-webkit-transform: scale(-1, 1);
|
-webkit-transform: scale(-1, 1);
|
||||||
-moz-transform: scale(-1, 1);
|
-moz-transform: scale(-1, 1);
|
||||||
-ms-transform: scale(-1, 1);
|
-ms-transform: scale(-1, 1);
|
||||||
@ -161,6 +165,28 @@ const EmojiPicker = styled(EmojiPickerComponent)`
|
|||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const InputWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-grow: 1;
|
||||||
|
min-width: 0;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
|
[dir='ltr'] & {
|
||||||
|
border-radius: 0.75rem 0 0 0.75rem;
|
||||||
|
margin-right: ${xsPadding};
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] & {
|
||||||
|
border-radius: 0 0.75rem 0.75rem 0;
|
||||||
|
margin-left: ${xsPadding};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
box-shadow: 0 0 0 ${xsPadding} ${colorBlueLight};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Form,
|
Form,
|
||||||
Wrapper,
|
Wrapper,
|
||||||
@ -171,4 +197,5 @@ export default {
|
|||||||
EmojiPicker,
|
EmojiPicker,
|
||||||
EmojiPickerWrapper,
|
EmojiPickerWrapper,
|
||||||
ChatMessageError,
|
ChatMessageError,
|
||||||
|
InputWrapper,
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
KeyboardEventHandler,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { makeVar, useMutation } from '@apollo/client';
|
import { makeVar, useMutation } from '@apollo/client';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
@ -26,6 +26,8 @@ import {
|
|||||||
import useReactiveRef from '/imports/ui/hooks/useReactiveRef';
|
import useReactiveRef from '/imports/ui/hooks/useReactiveRef';
|
||||||
import useStickyScroll from '/imports/ui/hooks/useStickyScroll';
|
import useStickyScroll from '/imports/ui/hooks/useStickyScroll';
|
||||||
import ChatReplyIntention from '../chat-reply-intention/component';
|
import ChatReplyIntention from '../chat-reply-intention/component';
|
||||||
|
import ChatEditingWarning from '../chat-editing-warning/component';
|
||||||
|
import KEY_CODES from '/imports/utils/keyCodes';
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
@ -104,6 +106,60 @@ const dispatchLastSeen = () => setTimeout(() => {
|
|||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
const roving = (
|
||||||
|
event: React.KeyboardEvent<HTMLElement>,
|
||||||
|
changeState: (el: HTMLElement | null) => void,
|
||||||
|
elementsList: HTMLElement,
|
||||||
|
element: HTMLElement | null,
|
||||||
|
) => {
|
||||||
|
const numberOfChilds = elementsList.childElementCount;
|
||||||
|
|
||||||
|
if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.keyCode)) {
|
||||||
|
changeState(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.keyCode === KEY_CODES.ARROW_DOWN) {
|
||||||
|
const firstElement = elementsList.firstChild as HTMLElement;
|
||||||
|
let elRef = element && numberOfChilds > 1 ? (element.nextSibling as HTMLElement) : firstElement;
|
||||||
|
|
||||||
|
while (elRef && elRef.dataset.focusable !== 'true' && elRef.nextSibling) {
|
||||||
|
elRef = elRef.nextSibling as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
elRef = (elRef && elRef.dataset.focusable === 'true') ? elRef : firstElement;
|
||||||
|
changeState(elRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.keyCode === KEY_CODES.ARROW_UP) {
|
||||||
|
const lastElement = elementsList.lastChild as HTMLElement;
|
||||||
|
let elRef = element ? (element.previousSibling as HTMLElement) : lastElement;
|
||||||
|
|
||||||
|
while (elRef && elRef.dataset.focusable !== 'true' && elRef.previousSibling) {
|
||||||
|
elRef = elRef.previousSibling as HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
elRef = (elRef && elRef.dataset.focusable === 'true') ? elRef : lastElement;
|
||||||
|
changeState(elRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event.keyCode)) {
|
||||||
|
const elRef = document.activeElement?.firstChild as HTMLElement;
|
||||||
|
changeState(elRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([KEY_CODES.ARROW_RIGHT].includes(event.keyCode)) {
|
||||||
|
if (element?.dataset) {
|
||||||
|
const { sequence } = element.dataset;
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_REQUEST, {
|
||||||
|
detail: {
|
||||||
|
sequence,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const ChatMessageList: React.FC<ChatListProps> = ({
|
const ChatMessageList: React.FC<ChatListProps> = ({
|
||||||
totalPages,
|
totalPages,
|
||||||
chatId,
|
chatId,
|
||||||
@ -126,6 +182,7 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
|||||||
const [userLoadedBackUntilPage, setUserLoadedBackUntilPage] = useState<number | null>(null);
|
const [userLoadedBackUntilPage, setUserLoadedBackUntilPage] = useState<number | null>(null);
|
||||||
const [lastMessageCreatedAt, setLastMessageCreatedAt] = useState<string>('');
|
const [lastMessageCreatedAt, setLastMessageCreatedAt] = useState<string>('');
|
||||||
const [followingTail, setFollowingTail] = React.useState(true);
|
const [followingTail, setFollowingTail] = React.useState(true);
|
||||||
|
const [selectedMessage, setSelectedMessage] = React.useState<HTMLElement | null>(null);
|
||||||
const {
|
const {
|
||||||
childRefProxy: sentinelRefProxy,
|
childRefProxy: sentinelRefProxy,
|
||||||
intersecting: isSentinelVisible,
|
intersecting: isSentinelVisible,
|
||||||
@ -259,6 +316,17 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
|||||||
: Math.max(totalPages - 2, 0);
|
: Math.max(totalPages - 2, 0);
|
||||||
const pagesToLoad = (totalPages - firstPageToLoad) || 1;
|
const pagesToLoad = (totalPages - firstPageToLoad) || 1;
|
||||||
|
|
||||||
|
const rove: KeyboardEventHandler<HTMLElement> = (e) => {
|
||||||
|
if (messageListRef.current) {
|
||||||
|
roving(
|
||||||
|
e,
|
||||||
|
setSelectedMessage,
|
||||||
|
messageListRef.current,
|
||||||
|
selectedMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
@ -276,7 +344,15 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
|||||||
isRTL={isRTL}
|
isRTL={isRTL}
|
||||||
ref={messageListContainerRefProxy}
|
ref={messageListContainerRefProxy}
|
||||||
>
|
>
|
||||||
<div ref={messageListRef}>
|
<div
|
||||||
|
role="listbox"
|
||||||
|
ref={messageListRef}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={rove}
|
||||||
|
onBlur={() => {
|
||||||
|
setSelectedMessage(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
{userLoadedBackUntilPage ? (
|
{userLoadedBackUntilPage ? (
|
||||||
<ButtonLoadMore
|
<ButtonLoadMore
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -301,6 +377,9 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
|||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
markMessageAsSeen={markMessageAsSeen}
|
markMessageAsSeen={markMessageAsSeen}
|
||||||
scrollRef={messageListContainerRefProxy}
|
scrollRef={messageListContainerRefProxy}
|
||||||
|
focusedId={selectedMessage?.dataset.sequence
|
||||||
|
? Number.parseInt(selectedMessage?.dataset.sequence, 10)
|
||||||
|
: null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -311,10 +390,13 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
|||||||
height: 1,
|
height: 1,
|
||||||
background: 'none',
|
background: 'none',
|
||||||
}}
|
}}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
</MessageList>,
|
</MessageList>,
|
||||||
renderUnreadNotification,
|
renderUnreadNotification,
|
||||||
<ChatReplyIntention />,
|
<ChatReplyIntention key="chatReplyIntention" />,
|
||||||
|
<ChatEditingWarning key="chatEditingWarning" />,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
@ -19,7 +19,9 @@ import {
|
|||||||
MessageItemWrapper,
|
MessageItemWrapper,
|
||||||
Container,
|
Container,
|
||||||
DeleteMessage,
|
DeleteMessage,
|
||||||
ChatEditTime,
|
ChatHeading,
|
||||||
|
EditLabel,
|
||||||
|
EditLabelWrapper,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
import { ChatEvents, ChatMessageType } from '/imports/ui/core/enums/chat';
|
import { ChatEvents, ChatMessageType } from '/imports/ui/core/enums/chat';
|
||||||
import MessageReadConfirmation from './message-read-confirmation/component';
|
import MessageReadConfirmation from './message-read-confirmation/component';
|
||||||
@ -27,7 +29,6 @@ import ChatMessageToolbar from './message-toolbar/component';
|
|||||||
import ChatMessageReactions from './message-reactions/component';
|
import ChatMessageReactions from './message-reactions/component';
|
||||||
import ChatMessageReplied from './message-replied/component';
|
import ChatMessageReplied from './message-replied/component';
|
||||||
import { STORAGES, useStorageKey } from '/imports/ui/services/storage/hooks';
|
import { STORAGES, useStorageKey } from '/imports/ui/services/storage/hooks';
|
||||||
import ChatEditMessageForm from './message-edit-form/component';
|
|
||||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||||
import { layoutSelect } from '/imports/ui/components/layout/context';
|
import { layoutSelect } from '/imports/ui/components/layout/context';
|
||||||
@ -36,6 +37,15 @@ import useChat from '/imports/ui/core/hooks/useChat';
|
|||||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||||
import { Chat } from '/imports/ui/Types/chat';
|
import { Chat } from '/imports/ui/Types/chat';
|
||||||
import { CHAT_DELETE_REACTION_MUTATION, CHAT_SEND_REACTION_MUTATION } from './mutations';
|
import { CHAT_DELETE_REACTION_MUTATION, CHAT_SEND_REACTION_MUTATION } from './mutations';
|
||||||
|
import Icon from '/imports/ui/components/common/icon/component';
|
||||||
|
import { colorBlueLightestChannel } from '/imports/ui/stylesheets/styled-components/palette';
|
||||||
|
import {
|
||||||
|
useIsReplyChatMessageEnabled,
|
||||||
|
useIsChatMessageReactionsEnabled,
|
||||||
|
useIsEditChatMessageEnabled,
|
||||||
|
useIsDeleteChatMessageEnabled,
|
||||||
|
} from '/imports/ui/services/features';
|
||||||
|
import ChatMessageNotificationContent from './message-content/notification-content/component';
|
||||||
|
|
||||||
interface ChatMessageProps {
|
interface ChatMessageProps {
|
||||||
message: Message;
|
message: Message;
|
||||||
@ -45,6 +55,8 @@ interface ChatMessageProps {
|
|||||||
scrollRef: React.RefObject<HTMLDivElement>;
|
scrollRef: React.RefObject<HTMLDivElement>;
|
||||||
markMessageAsSeen: (message: Message) => void;
|
markMessageAsSeen: (message: Message) => void;
|
||||||
messageReadFeedbackEnabled: boolean;
|
messageReadFeedbackEnabled: boolean;
|
||||||
|
focused: boolean;
|
||||||
|
keyboardFocused: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
@ -76,6 +88,10 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.chat.deleteMessage',
|
id: 'app.chat.deleteMessage',
|
||||||
description: '',
|
description: '',
|
||||||
},
|
},
|
||||||
|
edited: {
|
||||||
|
id: 'app.chat.toolbar.edit.edited',
|
||||||
|
description: 'edited message label',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function isInViewport(el: HTMLDivElement) {
|
function isInViewport(el: HTMLDivElement) {
|
||||||
@ -98,6 +114,8 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
setMessagesRequestedFromPlugin,
|
setMessagesRequestedFromPlugin,
|
||||||
markMessageAsSeen,
|
markMessageAsSeen,
|
||||||
messageReadFeedbackEnabled,
|
messageReadFeedbackEnabled,
|
||||||
|
focused,
|
||||||
|
keyboardFocused,
|
||||||
}) => {
|
}) => {
|
||||||
const idChatOpen: string = layoutSelect((i: Layout) => i.idChatOpen);
|
const idChatOpen: string = layoutSelect((i: Layout) => i.idChatOpen);
|
||||||
const { data: meeting } = useMeeting((m) => ({
|
const { data: meeting } = useMeeting((m) => ({
|
||||||
@ -122,7 +140,6 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
}, [message, messageRef]);
|
}, [message, messageRef]);
|
||||||
const messageContentRef = React.createRef<HTMLDivElement>();
|
const messageContentRef = React.createRef<HTMLDivElement>();
|
||||||
const [editing, setEditing] = React.useState(false);
|
const [editing, setEditing] = React.useState(false);
|
||||||
const [isToolbarMenuOpen, setIsToolbarMenuOpen] = React.useState(false);
|
|
||||||
const [isToolbarReactionPopoverOpen, setIsToolbarReactionPopoverOpen] = React.useState(false);
|
const [isToolbarReactionPopoverOpen, setIsToolbarReactionPopoverOpen] = React.useState(false);
|
||||||
const chatFocusMessageRequest = useStorageKey(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, STORAGES.IN_MEMORY);
|
const chatFocusMessageRequest = useStorageKey(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, STORAGES.IN_MEMORY);
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
@ -150,7 +167,6 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const CHAT_TOOLBAR_CONFIG = window.meetingClientSettings.public.chat.toolbar;
|
|
||||||
const isModerator = currentUser?.isModerator;
|
const isModerator = currentUser?.isModerator;
|
||||||
const isPublicChat = chat?.public;
|
const isPublicChat = chat?.public;
|
||||||
const isLocked = currentUser?.locked || currentUser?.userLockSettings?.disablePublicChat;
|
const isLocked = currentUser?.locked || currentUser?.userLockSettings?.disablePublicChat;
|
||||||
@ -168,11 +184,17 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasToolbar = CHAT_TOOLBAR_CONFIG.length > 0
|
const CHAT_REPLY_ENABLED = useIsReplyChatMessageEnabled();
|
||||||
&& message.deletedAt === null
|
const CHAT_REACTIONS_ENABLED = useIsChatMessageReactionsEnabled();
|
||||||
&& !editing
|
const CHAT_EDIT_ENABLED = useIsEditChatMessageEnabled();
|
||||||
&& !!message.user
|
const CHAT_DELETE_ENABLED = useIsDeleteChatMessageEnabled();
|
||||||
&& !locked;
|
|
||||||
|
const hasToolbar = !!message.user && [
|
||||||
|
CHAT_REPLY_ENABLED,
|
||||||
|
CHAT_REACTIONS_ENABLED,
|
||||||
|
CHAT_EDIT_ENABLED,
|
||||||
|
CHAT_DELETE_ENABLED,
|
||||||
|
].some((config) => config);
|
||||||
|
|
||||||
const startScrollAnimation = (timestamp: number) => {
|
const startScrollAnimation = (timestamp: number) => {
|
||||||
if (scrollRef.current && containerRef.current) {
|
if (scrollRef.current && containerRef.current) {
|
||||||
@ -184,7 +206,7 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: Event) => {
|
const handleFocusMessageRequest = (e: Event) => {
|
||||||
if (e instanceof CustomEvent) {
|
if (e instanceof CustomEvent) {
|
||||||
if (e.detail.sequence === message.messageSequence) {
|
if (e.detail.sequence === message.messageSequence) {
|
||||||
requestAnimationFrame(startScrollAnimation);
|
requestAnimationFrame(startScrollAnimation);
|
||||||
@ -192,10 +214,27 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, handler);
|
const handleChatEditRequest = (e: Event) => {
|
||||||
|
if (e instanceof CustomEvent) {
|
||||||
|
const editing = e.detail.messageId === message.messageId;
|
||||||
|
setEditing(editing);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelChatEditRequest = (e: Event) => {
|
||||||
|
if (e instanceof CustomEvent) {
|
||||||
|
setEditing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, handleFocusMessageRequest);
|
||||||
|
window.addEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleChatEditRequest);
|
||||||
|
window.addEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelChatEditRequest);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, handler);
|
window.removeEventListener(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, handleFocusMessageRequest);
|
||||||
|
window.removeEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleChatEditRequest);
|
||||||
|
window.removeEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelChatEditRequest);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -209,7 +248,7 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
const value = (timestamp - animationInitialTimestamp.current) / ANIMATION_DURATION;
|
const value = (timestamp - animationInitialTimestamp.current) / ANIMATION_DURATION;
|
||||||
if (value < 1) {
|
if (value < 1) {
|
||||||
containerRef.current.style.backgroundColor = `rgba(243, 246, 249, ${1 - value})`;
|
containerRef.current.style.backgroundColor = `rgb(${colorBlueLightestChannel} / ${1 - value})`;
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
} else {
|
} else {
|
||||||
containerRef.current.style.backgroundColor = 'unset';
|
containerRef.current.style.backgroundColor = 'unset';
|
||||||
@ -258,6 +297,7 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
const formattedTime = intl.formatTime(dateTime, {
|
const formattedTime = intl.formatTime(dateTime, {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
|
hour12: false,
|
||||||
});
|
});
|
||||||
const editTime = message.editedAt ? new Date(message.editedAt) : null;
|
const editTime = message.editedAt ? new Date(message.editedAt) : null;
|
||||||
const deleteTime = message.deletedAt ? new Date(message.deletedAt) : null;
|
const deleteTime = message.deletedAt ? new Date(message.deletedAt) : null;
|
||||||
@ -266,12 +306,15 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
const clearMessage = `${msgTime} ${intl.formatMessage(intlMessages.chatClear)}`;
|
const clearMessage = `${msgTime} ${intl.formatMessage(intlMessages.chatClear)}`;
|
||||||
|
|
||||||
const messageContent: {
|
const messageContent: {
|
||||||
name: string,
|
name: string;
|
||||||
color: string,
|
color: string;
|
||||||
isModerator: boolean,
|
isModerator: boolean;
|
||||||
isPresentationUpload?: boolean,
|
isPresentationUpload?: boolean;
|
||||||
component: React.ReactElement,
|
component: React.ReactNode;
|
||||||
avatarIcon?: string,
|
avatarIcon?: string;
|
||||||
|
isSystemSender?: boolean;
|
||||||
|
showAvatar: boolean;
|
||||||
|
showHeading: boolean;
|
||||||
} = useMemo(() => {
|
} = useMemo(() => {
|
||||||
switch (message.messageType) {
|
switch (message.messageType) {
|
||||||
case ChatMessageType.POLL:
|
case ChatMessageType.POLL:
|
||||||
@ -283,6 +326,8 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
<ChatPollContent metadata={message.messageMetadata} />
|
<ChatPollContent metadata={message.messageMetadata} />
|
||||||
),
|
),
|
||||||
avatarIcon: 'icon-bbb-polling',
|
avatarIcon: 'icon-bbb-polling',
|
||||||
|
showAvatar: true,
|
||||||
|
showHeading: true,
|
||||||
};
|
};
|
||||||
case ChatMessageType.PRESENTATION:
|
case ChatMessageType.PRESENTATION:
|
||||||
return {
|
return {
|
||||||
@ -296,6 +341,8 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
avatarIcon: 'icon-bbb-download',
|
avatarIcon: 'icon-bbb-download',
|
||||||
|
showAvatar: true,
|
||||||
|
showHeading: true,
|
||||||
};
|
};
|
||||||
case ChatMessageType.CHAT_CLEAR:
|
case ChatMessageType.CHAT_CLEAR:
|
||||||
return {
|
return {
|
||||||
@ -304,26 +351,28 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
isModerator: false,
|
isModerator: false,
|
||||||
isSystemSender: true,
|
isSystemSender: true,
|
||||||
component: (
|
component: (
|
||||||
<ChatMessageTextContent
|
<ChatMessageNotificationContent
|
||||||
emphasizedMessage={false}
|
iconName="delete"
|
||||||
text={clearMessage}
|
text={clearMessage}
|
||||||
systemMsg
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
showAvatar: false,
|
||||||
|
showHeading: false,
|
||||||
};
|
};
|
||||||
case ChatMessageType.BREAKOUT_ROOM:
|
case ChatMessageType.BREAKOUT_ROOM:
|
||||||
return {
|
return {
|
||||||
name: message.senderName,
|
name: message.senderName,
|
||||||
color: '#0F70D7',
|
color: '#0F70D7',
|
||||||
isModerator: true,
|
isModerator: true,
|
||||||
isSystemSender: true,
|
isSystemSender: false,
|
||||||
component: (
|
component: (
|
||||||
<ChatMessageTextContent
|
<ChatMessageTextContent
|
||||||
systemMsg={false}
|
|
||||||
emphasizedMessage
|
emphasizedMessage
|
||||||
text={message.message}
|
text={message.message}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
showAvatar: true,
|
||||||
|
showHeading: true,
|
||||||
};
|
};
|
||||||
case ChatMessageType.API:
|
case ChatMessageType.API:
|
||||||
return {
|
return {
|
||||||
@ -332,30 +381,31 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
isModerator: true,
|
isModerator: true,
|
||||||
isSystemSender: true,
|
isSystemSender: true,
|
||||||
component: (
|
component: (
|
||||||
<ChatMessageTextContent
|
<ChatMessageNotificationContent
|
||||||
systemMsg
|
|
||||||
emphasizedMessage
|
|
||||||
text={message.message}
|
text={message.message}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
showAvatar: false,
|
||||||
|
showHeading: false,
|
||||||
};
|
};
|
||||||
case ChatMessageType.USER_AWAY_STATUS_MSG: {
|
case ChatMessageType.USER_AWAY_STATUS_MSG: {
|
||||||
const { away } = JSON.parse(message.messageMetadata);
|
const { away } = JSON.parse(message.messageMetadata);
|
||||||
const awayMessage = (away)
|
const awayMessage = (away)
|
||||||
? `${intl.formatMessage(intlMessages.userAway)}`
|
? intl.formatMessage(intlMessages.userAway, { user: message.senderName })
|
||||||
: `${intl.formatMessage(intlMessages.userNotAway)}`;
|
: intl.formatMessage(intlMessages.userNotAway, { user: message.senderName });
|
||||||
return {
|
return {
|
||||||
name: message.senderName,
|
name: message.senderName,
|
||||||
color: '#0F70D7',
|
color: '#0F70D7',
|
||||||
isModerator: true,
|
isModerator: true,
|
||||||
isSystemSender: true,
|
isSystemSender: true,
|
||||||
component: (
|
component: (
|
||||||
<ChatMessageTextContent
|
<ChatMessageNotificationContent
|
||||||
emphasizedMessage={false}
|
iconName="time"
|
||||||
text={awayMessage}
|
text={awayMessage}
|
||||||
systemMsg
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
showAvatar: false,
|
||||||
|
showHeading: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case ChatMessageType.PLUGIN: {
|
case ChatMessageType.PLUGIN: {
|
||||||
@ -364,13 +414,14 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
color: message.user?.color,
|
color: message.user?.color,
|
||||||
isModerator: message.user?.isModerator,
|
isModerator: message.user?.isModerator,
|
||||||
isSystemSender: false,
|
isSystemSender: false,
|
||||||
|
showAvatar: true,
|
||||||
|
showHeading: true,
|
||||||
component: currentPluginMessageMetadata.custom
|
component: currentPluginMessageMetadata.custom
|
||||||
? (<></>)
|
? null
|
||||||
: (
|
: (
|
||||||
<ChatMessageTextContent
|
<ChatMessageTextContent
|
||||||
emphasizedMessage={message.chatEmphasizedText}
|
emphasizedMessage={message.chatEmphasizedText}
|
||||||
text={message.message}
|
text={message.message}
|
||||||
systemMsg={false}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@ -381,104 +432,115 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
name: message.user?.name,
|
name: message.user?.name,
|
||||||
color: message.user?.color,
|
color: message.user?.color,
|
||||||
isModerator: message.user?.isModerator,
|
isModerator: message.user?.isModerator,
|
||||||
isSystemSender: ChatMessageType.BREAKOUT_ROOM,
|
isSystemSender: false,
|
||||||
|
showAvatar: true,
|
||||||
|
showHeading: true,
|
||||||
component: (
|
component: (
|
||||||
<ChatMessageTextContent
|
<ChatMessageTextContent
|
||||||
emphasizedMessage={message.chatEmphasizedText}
|
emphasizedMessage={message.chatEmphasizedText}
|
||||||
text={message.message}
|
text={message.message}
|
||||||
systemMsg={false}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [message.message]);
|
}, [message.message]);
|
||||||
|
|
||||||
|
const shouldRenderAvatar = messageContent.showAvatar
|
||||||
|
&& !sameSender
|
||||||
|
&& !isCustomPluginMessage;
|
||||||
|
|
||||||
|
const shouldRenderHeader = messageContent.showHeading
|
||||||
|
&& !sameSender
|
||||||
|
&& !isCustomPluginMessage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container ref={containerRef}>
|
<Container
|
||||||
|
ref={containerRef}
|
||||||
|
$sequence={message.messageSequence}
|
||||||
|
data-sequence={message.messageSequence}
|
||||||
|
data-focusable={!deleteTime && !messageContent.isSystemSender}
|
||||||
|
>
|
||||||
<ChatWrapper
|
<ChatWrapper
|
||||||
id="chat-message-wrapper"
|
className={`chat-message-wrapper ${focused ? 'chat-message-wrapper-focused' : ''} ${keyboardFocused ? 'chat-message-wrapper-keyboard-focused' : ''}`}
|
||||||
isSystemSender={isSystemSender}
|
isSystemSender={isSystemSender}
|
||||||
sameSender={sameSender}
|
sameSender={sameSender}
|
||||||
ref={messageRef}
|
ref={messageRef}
|
||||||
isPresentationUpload={messageContent.isPresentationUpload}
|
isPresentationUpload={messageContent.isPresentationUpload}
|
||||||
isCustomPluginMessage={isCustomPluginMessage}
|
isCustomPluginMessage={isCustomPluginMessage}
|
||||||
$highlight={hasToolbar}
|
|
||||||
$toolbarMenuIsOpen={isToolbarMenuOpen}
|
|
||||||
$reactionPopoverIsOpen={isToolbarReactionPopoverOpen}
|
|
||||||
>
|
>
|
||||||
{hasToolbar && (
|
<ChatMessageToolbar
|
||||||
<ChatMessageToolbar
|
keyboardFocused={keyboardFocused}
|
||||||
messageId={message.messageId}
|
hasToolbar={hasToolbar}
|
||||||
chatId={message.chatId}
|
locked={locked}
|
||||||
username={message.user.name}
|
deleted={!!deleteTime}
|
||||||
own={message.user.userId === currentUser?.userId}
|
messageId={message.messageId}
|
||||||
amIModerator={Boolean(currentUser?.isModerator)}
|
chatId={message.chatId}
|
||||||
message={message.message}
|
username={message.user?.name}
|
||||||
messageSequence={message.messageSequence}
|
own={message.user?.userId === currentUser?.userId}
|
||||||
emphasizedMessage={message.chatEmphasizedText}
|
amIModerator={Boolean(currentUser?.isModerator)}
|
||||||
onEmojiSelected={(emoji) => {
|
message={message.message}
|
||||||
sendReaction(emoji.native);
|
messageSequence={message.messageSequence}
|
||||||
setIsToolbarReactionPopoverOpen(false);
|
emphasizedMessage={message.chatEmphasizedText}
|
||||||
}}
|
onEmojiSelected={(emoji) => {
|
||||||
onEditRequest={() => {
|
sendReaction(emoji.native);
|
||||||
setEditing(true);
|
setIsToolbarReactionPopoverOpen(false);
|
||||||
}}
|
}}
|
||||||
onMenuOpenChange={setIsToolbarMenuOpen}
|
onReactionPopoverOpenChange={setIsToolbarReactionPopoverOpen}
|
||||||
menuIsOpen={isToolbarMenuOpen}
|
reactionPopoverIsOpen={isToolbarReactionPopoverOpen}
|
||||||
onReactionPopoverOpenChange={setIsToolbarReactionPopoverOpen}
|
chatDeleteEnabled={CHAT_DELETE_ENABLED}
|
||||||
reactionPopoverIsOpen={isToolbarReactionPopoverOpen}
|
chatEditEnabled={CHAT_EDIT_ENABLED}
|
||||||
/>
|
chatReactionsEnabled={CHAT_REACTIONS_ENABLED}
|
||||||
)}
|
chatReplyEnabled={CHAT_REPLY_ENABLED}
|
||||||
{((!message?.user || !sameSender) && (
|
/>
|
||||||
message.messageType !== ChatMessageType.USER_AWAY_STATUS_MSG
|
{(shouldRenderAvatar || shouldRenderHeader) && (
|
||||||
&& message.messageType !== ChatMessageType.API
|
<ChatHeading>
|
||||||
&& message.messageType !== ChatMessageType.CHAT_CLEAR
|
{shouldRenderAvatar && (
|
||||||
&& !isCustomPluginMessage)
|
<ChatAvatar
|
||||||
) && (
|
avatar={message.user?.avatar}
|
||||||
<ChatAvatar
|
color={messageContent.color}
|
||||||
avatar={message.user?.avatar}
|
moderator={messageContent.isModerator}
|
||||||
color={messageContent.color}
|
>
|
||||||
moderator={messageContent.isModerator}
|
{!messageContent.avatarIcon ? (
|
||||||
>
|
!message.user || (message.user?.avatar.length === 0 ? messageContent.name.toLowerCase().slice(0, 2) : '')
|
||||||
{!messageContent.avatarIcon ? (
|
) : (
|
||||||
!message.user || (message.user?.avatar.length === 0 ? messageContent.name.toLowerCase().slice(0, 2) : '')
|
<i className={messageContent.avatarIcon} />
|
||||||
) : (
|
)}
|
||||||
<i className={messageContent.avatarIcon} />
|
</ChatAvatar>
|
||||||
)}
|
)}
|
||||||
</ChatAvatar>
|
{shouldRenderHeader && (
|
||||||
)}
|
|
||||||
{!editing && (
|
|
||||||
<ChatContent
|
|
||||||
ref={messageContentRef}
|
|
||||||
sameSender={message?.user ? sameSender : false}
|
|
||||||
isCustomPluginMessage={isCustomPluginMessage}
|
|
||||||
data-chat-message-id={message?.messageId}
|
|
||||||
$highlight={hasToolbar}
|
|
||||||
$toolbarMenuIsOpen={isToolbarMenuOpen}
|
|
||||||
$reactionPopoverIsOpen={isToolbarReactionPopoverOpen}
|
|
||||||
>
|
|
||||||
{message.messageType !== ChatMessageType.CHAT_CLEAR
|
|
||||||
&& !isCustomPluginMessage
|
|
||||||
&& (
|
|
||||||
<ChatMessageHeader
|
<ChatMessageHeader
|
||||||
sameSender={message?.user ? sameSender : false}
|
sameSender={message?.user ? sameSender : false}
|
||||||
name={messageContent.name}
|
name={messageContent.name}
|
||||||
currentlyInMeeting={message.user?.currentlyInMeeting ?? true}
|
currentlyInMeeting={message.user?.currentlyInMeeting ?? true}
|
||||||
dateTime={dateTime}
|
dateTime={dateTime}
|
||||||
deleteTime={deleteTime}
|
deleteTime={deleteTime}
|
||||||
|
editTime={editTime}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{message.replyToMessage && (
|
</ChatHeading>
|
||||||
|
)}
|
||||||
|
<ChatContent
|
||||||
|
className="chat-message-content"
|
||||||
|
ref={messageContentRef}
|
||||||
|
sameSender={message?.user ? sameSender : false}
|
||||||
|
isCustomPluginMessage={isCustomPluginMessage}
|
||||||
|
data-chat-message-id={message?.messageId}
|
||||||
|
$highlight={hasToolbar && !deleteTime}
|
||||||
|
$editing={editing}
|
||||||
|
$focused={focused}
|
||||||
|
$keyboardFocused={keyboardFocused}
|
||||||
|
$reactionPopoverIsOpen={isToolbarReactionPopoverOpen}
|
||||||
|
>
|
||||||
|
{message.replyToMessage && !deleteTime && (
|
||||||
<ChatMessageReplied
|
<ChatMessageReplied
|
||||||
message={message.replyToMessage.message}
|
message={message.replyToMessage.message}
|
||||||
username={message.replyToMessage.user.name}
|
|
||||||
sequence={message.replyToMessage.messageSequence}
|
sequence={message.replyToMessage.messageSequence}
|
||||||
userColor={message.replyToMessage.user.color}
|
|
||||||
emphasizedMessage={message.replyToMessage.chatEmphasizedText}
|
emphasizedMessage={message.replyToMessage.chatEmphasizedText}
|
||||||
deletedByUser={message.replyToMessage.deletedBy?.name ?? null}
|
deletedByUser={message.replyToMessage.deletedBy?.name ?? null}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!deleteTime && (
|
{!deleteTime && (
|
||||||
<MessageItemWrapper>
|
<MessageItemWrapper $edited={!!editTime} $sameSender={sameSender}>
|
||||||
{messageContent.component}
|
{messageContent.component}
|
||||||
{messageReadFeedbackEnabled && (
|
{messageReadFeedbackEnabled && (
|
||||||
<MessageReadConfirmation
|
<MessageReadConfirmation
|
||||||
@ -487,34 +549,26 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
|||||||
)}
|
)}
|
||||||
</MessageItemWrapper>
|
</MessageItemWrapper>
|
||||||
)}
|
)}
|
||||||
{!deleteTime && editTime && (
|
{!deleteTime && editTime && sameSender && (
|
||||||
<ChatEditTime>
|
<EditLabelWrapper>
|
||||||
{`(${intl.formatMessage(intlMessages.editTime, { 0: intl.formatTime(editTime) })})`}
|
<EditLabel>
|
||||||
</ChatEditTime>
|
<Icon iconName="pen_tool" />
|
||||||
|
<span>{intl.formatMessage(intlMessages.edited)}</span>
|
||||||
|
</EditLabel>
|
||||||
|
</EditLabelWrapper>
|
||||||
)}
|
)}
|
||||||
{deleteTime && (
|
{deleteTime && (
|
||||||
<DeleteMessage>
|
<DeleteMessage>
|
||||||
{intl.formatMessage(intlMessages.deleteMessage, { 0: message.deletedBy?.name })}
|
{intl.formatMessage(intlMessages.deleteMessage, { 0: message.deletedBy?.name })}
|
||||||
</DeleteMessage>
|
</DeleteMessage>
|
||||||
)}
|
)}
|
||||||
{!deleteTime && (
|
|
||||||
<ChatMessageReactions
|
|
||||||
reactions={message.reactions}
|
|
||||||
deleteReaction={deleteReaction}
|
|
||||||
sendReaction={sendReaction}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ChatContent>
|
</ChatContent>
|
||||||
)}
|
{!deleteTime && (
|
||||||
{editing && (
|
<ChatMessageReactions
|
||||||
<ChatEditMessageForm
|
reactions={message.reactions}
|
||||||
chatId={message.chatId}
|
deleteReaction={deleteReaction}
|
||||||
initialMessage={message.message}
|
sendReaction={sendReaction}
|
||||||
messageId={message.messageId}
|
/>
|
||||||
onCancel={() => setEditing(false)}
|
|
||||||
onAfterSubmit={() => setEditing(false)}
|
|
||||||
sameSender={message?.user ? sameSender : false}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</ChatWrapper>
|
</ChatWrapper>
|
||||||
</Container>
|
</Container>
|
||||||
@ -528,7 +582,9 @@ function areChatMessagesEqual(prevProps: ChatMessageProps, nextProps: ChatMessag
|
|||||||
&& prevMessage?.user?.currentlyInMeeting === nextMessage?.user?.currentlyInMeeting
|
&& prevMessage?.user?.currentlyInMeeting === nextMessage?.user?.currentlyInMeeting
|
||||||
&& prevMessage?.recipientHasSeen === nextMessage.recipientHasSeen
|
&& prevMessage?.recipientHasSeen === nextMessage.recipientHasSeen
|
||||||
&& prevMessage?.message === nextMessage.message
|
&& prevMessage?.message === nextMessage.message
|
||||||
&& prevMessage?.reactions?.length === nextMessage?.reactions?.length;
|
&& prevMessage?.reactions?.length === nextMessage?.reactions?.length
|
||||||
|
&& prevProps.focused === nextProps.focused
|
||||||
|
&& prevProps.keyboardFocused === nextProps.keyboardFocused;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(ChatMesssage, areChatMessagesEqual);
|
export default memo(ChatMesssage, areChatMessagesEqual);
|
||||||
|
@ -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 {
|
interface ChatMessageTextContentProps {
|
||||||
text: string;
|
text: string;
|
||||||
emphasizedMessage: boolean;
|
emphasizedMessage: boolean;
|
||||||
systemMsg: boolean;
|
|
||||||
dataTest?: string | null;
|
dataTest?: string | null;
|
||||||
}
|
}
|
||||||
const ChatMessageTextContent: React.FC<ChatMessageTextContentProps> = ({
|
const ChatMessageTextContent: React.FC<ChatMessageTextContentProps> = ({
|
||||||
text,
|
text,
|
||||||
emphasizedMessage,
|
emphasizedMessage,
|
||||||
systemMsg,
|
|
||||||
dataTest = 'messageContent',
|
dataTest = 'messageContent',
|
||||||
}) => {
|
}) => {
|
||||||
const { allowedElements } = window.meetingClientSettings.public.chat;
|
const { allowedElements } = window.meetingClientSettings.public.chat;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Styled.ChatMessage systemMsg={systemMsg} emphasizedMessage={emphasizedMessage} data-test={dataTest}>
|
<Styled.ChatMessage emphasizedMessage={emphasizedMessage} data-test={dataTest}>
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
linkTarget="_blank"
|
linkTarget="_blank"
|
||||||
allowedElements={allowedElements}
|
allowedElements={allowedElements}
|
||||||
|
@ -1,11 +1,5 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import { colorText } from '/imports/ui/stylesheets/styled-components/palette';
|
||||||
systemMessageBackgroundColor,
|
|
||||||
systemMessageBorderColor,
|
|
||||||
colorText,
|
|
||||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
|
||||||
import { borderRadius } from '/imports/ui/stylesheets/styled-components/general';
|
|
||||||
import { fontSizeBase, btnFontWeight } from '/imports/ui/stylesheets/styled-components/typography';
|
|
||||||
|
|
||||||
interface ChatMessageProps {
|
interface ChatMessageProps {
|
||||||
emphasizedMessage: boolean;
|
emphasizedMessage: boolean;
|
||||||
@ -19,17 +13,7 @@ export const ChatMessage = styled.div<ChatMessageProps>`
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
color: ${colorText};
|
color: ${colorText};
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
${({ systemMsg }) => systemMsg && `
|
|
||||||
background: ${systemMessageBackgroundColor};
|
|
||||||
border: 1px solid ${systemMessageBorderColor};
|
|
||||||
border-radius: ${borderRadius};
|
|
||||||
font-weight: ${btnFontWeight};
|
|
||||||
padding: ${fontSizeBase};
|
|
||||||
text-color: #1f252b;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
`}
|
|
||||||
${({ emphasizedMessage }) => emphasizedMessage && `
|
${({ emphasizedMessage }) => emphasizedMessage && `
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
`}
|
`}
|
||||||
|
@ -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 React from 'react';
|
||||||
import { useIntl, defineMessages, FormattedTime } from 'react-intl';
|
import { useIntl, defineMessages, FormattedTime } from 'react-intl';
|
||||||
|
import Icon from '/imports/ui/components/common/icon/component';
|
||||||
import Styled from './styles';
|
import Styled from './styles';
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
@ -7,6 +8,10 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.chat.offline',
|
id: 'app.chat.offline',
|
||||||
description: 'Offline',
|
description: 'Offline',
|
||||||
},
|
},
|
||||||
|
edited: {
|
||||||
|
id: 'app.chat.toolbar.edit.edited',
|
||||||
|
description: 'Edited',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ChatMessageHeaderProps {
|
interface ChatMessageHeaderProps {
|
||||||
@ -15,6 +20,7 @@ interface ChatMessageHeaderProps {
|
|||||||
dateTime: Date;
|
dateTime: Date;
|
||||||
sameSender: boolean;
|
sameSender: boolean;
|
||||||
deleteTime: Date | null;
|
deleteTime: Date | null;
|
||||||
|
editTime: Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
|
const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
|
||||||
@ -23,6 +29,7 @@ const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
|
|||||||
currentlyInMeeting,
|
currentlyInMeeting,
|
||||||
dateTime,
|
dateTime,
|
||||||
deleteTime,
|
deleteTime,
|
||||||
|
editTime,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
if (sameSender) return null;
|
if (sameSender) return null;
|
||||||
@ -40,11 +47,20 @@ const ChatMessageHeader: React.FC<ChatMessageHeaderProps> = ({
|
|||||||
</Styled.ChatUserOffline>
|
</Styled.ChatUserOffline>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{!deleteTime && (
|
{!deleteTime && editTime && (
|
||||||
<Styled.ChatTime>
|
<Styled.EditLabel>
|
||||||
<FormattedTime value={dateTime} />
|
<Icon iconName="pen_tool" />
|
||||||
</Styled.ChatTime>
|
<span>{intl.formatMessage(intlMessages.edited)}</span>
|
||||||
|
</Styled.EditLabel>
|
||||||
)}
|
)}
|
||||||
|
{deleteTime && (
|
||||||
|
<Styled.EditLabel>
|
||||||
|
<Icon iconName="delete" />
|
||||||
|
</Styled.EditLabel>
|
||||||
|
)}
|
||||||
|
<Styled.ChatTime>
|
||||||
|
<FormattedTime value={dateTime} hour12={false} />
|
||||||
|
</Styled.ChatTime>
|
||||||
</Styled.ChatHeaderText>
|
</Styled.ChatHeaderText>
|
||||||
</Styled.HeaderContent>
|
</Styled.HeaderContent>
|
||||||
);
|
);
|
||||||
|
@ -2,10 +2,10 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
colorHeading,
|
colorHeading,
|
||||||
palettePlaceholderText,
|
|
||||||
colorGrayLight,
|
colorGrayLight,
|
||||||
|
colorGrayDark,
|
||||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||||
import { lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
|
import { fontSizeSmaller, lineHeightComputed } from '/imports/ui/stylesheets/styled-components/typography';
|
||||||
|
|
||||||
interface ChatUserNameProps {
|
interface ChatUserNameProps {
|
||||||
currentlyInMeeting: boolean;
|
currentlyInMeeting: boolean;
|
||||||
@ -14,6 +14,7 @@ interface ChatUserNameProps {
|
|||||||
export const HeaderContent = styled.div`
|
export const HeaderContent = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row;
|
flex-flow: row;
|
||||||
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ export const ChatUserName = styled.div<ChatUserNameProps>`
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
${({ currentlyInMeeting }) => currentlyInMeeting && `
|
${({ currentlyInMeeting }) => currentlyInMeeting && `
|
||||||
color: ${colorHeading};
|
color: ${colorHeading};
|
||||||
@ -65,8 +67,8 @@ export const ChatUserOffline = styled.span`
|
|||||||
export const ChatTime = styled.time`
|
export const ChatTime = styled.time`
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-basis: 3.5rem;
|
flex-basis: max-content;
|
||||||
color: ${palettePlaceholderText};
|
color: ${colorGrayDark};
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
[dir='rtl'] & {
|
[dir='rtl'] & {
|
||||||
@ -84,10 +86,27 @@ export const ChatHeaderText = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const EditLabel = styled.span`
|
||||||
|
color: ${colorGrayLight};
|
||||||
|
font-size: ${fontSizeSmaller};
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(${lineHeightComputed} / 4);
|
||||||
|
|
||||||
|
[dir='ltr'] & {
|
||||||
|
margin-right: calc(${lineHeightComputed} / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] & {
|
||||||
|
margin-left: calc(${lineHeightComputed} / 2);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
HeaderContent,
|
HeaderContent,
|
||||||
ChatTime,
|
ChatTime,
|
||||||
ChatUserOffline,
|
ChatUserOffline,
|
||||||
ChatUserName,
|
ChatUserName,
|
||||||
ChatHeaderText,
|
ChatHeaderText,
|
||||||
|
EditLabel,
|
||||||
};
|
};
|
||||||
|
@ -38,6 +38,8 @@ const ChatMessageReactions: React.FC<ChatMessageReactionsProps> = (props) => {
|
|||||||
const { data: currentUser } = useCurrentUser((u) => ({ userId: u.userId }));
|
const { data: currentUser } = useCurrentUser((u) => ({ userId: u.userId }));
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
if (reactions.length === 0) return null;
|
||||||
|
|
||||||
const reactionItems: Record<string, { count: number; userNames: string[]; reactedByMe: boolean }> = {};
|
const reactionItems: Record<string, { count: number; userNames: string[]; reactedByMe: boolean }> = {};
|
||||||
|
|
||||||
reactions.forEach((reaction) => {
|
reactions.forEach((reaction) => {
|
||||||
|
@ -1,29 +1,41 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { colorBlueLighter, colorBlueLightest, colorGray } from '/imports/ui/stylesheets/styled-components/palette';
|
import {
|
||||||
|
colorGrayLighter, colorGrayLightest, colorOffWhite,
|
||||||
|
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||||
|
|
||||||
const EmojiWrapper = styled.button<{ highlighted: boolean }>`
|
const EmojiWrapper = styled.button<{ highlighted: boolean }>`
|
||||||
background-color: ${colorBlueLightest};
|
background: none;
|
||||||
border-radius: 10px;
|
border-radius: 1rem;
|
||||||
margin-left: 3px;
|
padding: 0.375rem 1rem;
|
||||||
margin-top: 3px;
|
line-height: 1;
|
||||||
padding: 3px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
border: 1px solid transparent;
|
border: 1px solid ${colorGrayLightest};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
${({ highlighted }) => highlighted && `
|
${({ highlighted }) => highlighted && `
|
||||||
background-color: ${colorBlueLighter};
|
background-color: ${colorOffWhite};
|
||||||
`}
|
`}
|
||||||
|
|
||||||
|
em-emoji {
|
||||||
|
[dir='ltr'] & {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] & {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border: 1px solid ${colorGray};
|
border: 1px solid ${colorGrayLighter};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ReactionsWrapper = styled.div`
|
const ReactionsWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -13,24 +13,21 @@ const intlMessages = defineMessages({
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface MessageRepliedProps {
|
interface MessageRepliedProps {
|
||||||
username: string;
|
|
||||||
message: string;
|
message: string;
|
||||||
sequence: number;
|
sequence: number;
|
||||||
userColor: string;
|
|
||||||
emphasizedMessage: boolean;
|
emphasizedMessage: boolean;
|
||||||
deletedByUser: string | null;
|
deletedByUser: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatMessageReplied: React.FC<MessageRepliedProps> = (props) => {
|
const ChatMessageReplied: React.FC<MessageRepliedProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
message, username, sequence, userColor, emphasizedMessage, deletedByUser,
|
message, sequence, emphasizedMessage, deletedByUser,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Styled.Container
|
<Styled.Container
|
||||||
$userColor={userColor}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
new CustomEvent(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, {
|
new CustomEvent(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, {
|
||||||
@ -43,13 +40,11 @@ const ChatMessageReplied: React.FC<MessageRepliedProps> = (props) => {
|
|||||||
Storage.setItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, sequence);
|
Storage.setItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, sequence);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Styled.Username $userColor={userColor}>{username}</Styled.Username>
|
|
||||||
{!deletedByUser && (
|
{!deletedByUser && (
|
||||||
<Styled.Message>
|
<Styled.Message>
|
||||||
<ChatMessageTextContent
|
<ChatMessageTextContent
|
||||||
text={message}
|
text={message}
|
||||||
emphasizedMessage={emphasizedMessage}
|
emphasizedMessage={emphasizedMessage}
|
||||||
systemMsg={false}
|
|
||||||
dataTest={null}
|
dataTest={null}
|
||||||
/>
|
/>
|
||||||
</Styled.Message>
|
</Styled.Message>
|
||||||
|
@ -1,24 +1,35 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { colorOffWhite, colorText } from '/imports/ui/stylesheets/styled-components/palette';
|
import {
|
||||||
|
colorGrayLightest, colorOffWhite, colorPrimary, colorText,
|
||||||
|
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||||
|
import { $3xlPadding, lgPadding } from '/imports/ui/stylesheets/styled-components/general';
|
||||||
|
|
||||||
const Container = styled.div<{ $userColor: string }>`
|
const Container = styled.div`
|
||||||
border-radius: 4px;
|
border-top-left-radius: 0.5rem;
|
||||||
border-left: 4px solid ${({ $userColor }) => $userColor};
|
border-top-right-radius: 0.5rem;
|
||||||
background-color: ${colorOffWhite};
|
background-color: ${colorOffWhite};
|
||||||
padding: 6px;
|
box-shadow: inset 0 0 0 1px ${colorGrayLightest};
|
||||||
|
padding: ${lgPadding} ${$3xlPadding};
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 0.25rem 0 0.25rem 0;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
[dir='ltr'] & {
|
||||||
|
border-right: 0.5rem solid ${colorPrimary};
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] & {
|
||||||
|
border-left: 0.5rem solid ${colorPrimary};
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Typography = styled.div`
|
const Typography = styled.div`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Username = styled(Typography)<{ $userColor: string }>`
|
const Username = styled(Typography)`
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: ${({ $userColor }) => $userColor};
|
color: ${colorPrimary};
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -26,8 +37,8 @@ const Username = styled(Typography)<{ $userColor: string }>`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const Message = styled(Typography)`
|
const Message = styled(Typography)`
|
||||||
max-height: 3.6rem;
|
max-height: 1rem;
|
||||||
line-height: 1.2rem;
|
line-height: 1rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -1,26 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import Popover from '@mui/material/Popover';
|
import Popover from '@mui/material/Popover';
|
||||||
|
import { FocusTrap } from '@mui/base/FocusTrap';
|
||||||
import { layoutSelect } from '/imports/ui/components/layout/context';
|
import { layoutSelect } from '/imports/ui/components/layout/context';
|
||||||
import Button from '/imports/ui/components/common/button/component';
|
|
||||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
|
||||||
import { Layout } from '/imports/ui/components/layout/layoutTypes';
|
import { Layout } from '/imports/ui/components/layout/layoutTypes';
|
||||||
import { ChatEvents } from '/imports/ui/core/enums/chat';
|
import { ChatEvents } from '/imports/ui/core/enums/chat';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import ConfirmationModal from '/imports/ui/components/common/modal/confirmation/component';
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
|
Divider,
|
||||||
|
EmojiButton,
|
||||||
EmojiPicker,
|
EmojiPicker,
|
||||||
EmojiPickerWrapper,
|
EmojiPickerWrapper,
|
||||||
EmojiButton,
|
Root,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
import { colorDanger, colorWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
|
||||||
import { CHAT_DELETE_MESSAGE_MUTATION } from '../mutations';
|
import { CHAT_DELETE_MESSAGE_MUTATION } from '../mutations';
|
||||||
import {
|
|
||||||
useIsChatMessageReactionsEnabled,
|
|
||||||
useIsDeleteChatMessageEnabled,
|
|
||||||
useIsEditChatMessageEnabled,
|
|
||||||
useIsReplyChatMessageEnabled,
|
|
||||||
} from '/imports/ui/services/features';
|
|
||||||
|
|
||||||
const intlMessages = defineMessages({
|
const intlMessages = defineMessages({
|
||||||
reply: {
|
reply: {
|
||||||
@ -35,6 +30,18 @@ const intlMessages = defineMessages({
|
|||||||
id: 'app.chat.toolbar.delete',
|
id: 'app.chat.toolbar.delete',
|
||||||
description: 'delete label',
|
description: 'delete label',
|
||||||
},
|
},
|
||||||
|
cancelLabel: {
|
||||||
|
id: 'app.chat.toolbar.delete.cancelLabel',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
confirmationTitle: {
|
||||||
|
id: 'app.chat.toolbar.delete.confirmationTitle',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
confirmationDescription: {
|
||||||
|
id: 'app.chat.toolbar.delete.confimationDescription',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface ChatMessageToolbarProps {
|
interface ChatMessageToolbarProps {
|
||||||
@ -47,198 +54,183 @@ interface ChatMessageToolbarProps {
|
|||||||
messageSequence: number;
|
messageSequence: number;
|
||||||
emphasizedMessage: boolean;
|
emphasizedMessage: boolean;
|
||||||
onEmojiSelected(emoji: { id: string; native: string }): void;
|
onEmojiSelected(emoji: { id: string; native: string }): void;
|
||||||
onEditRequest(): void;
|
|
||||||
onMenuOpenChange(open: boolean): void;
|
|
||||||
menuIsOpen: boolean;
|
|
||||||
onReactionPopoverOpenChange(open: boolean): void;
|
onReactionPopoverOpenChange(open: boolean): void;
|
||||||
reactionPopoverIsOpen: boolean;
|
reactionPopoverIsOpen: boolean;
|
||||||
|
hasToolbar: boolean;
|
||||||
|
locked: boolean;
|
||||||
|
deleted: boolean;
|
||||||
|
chatReplyEnabled: boolean;
|
||||||
|
chatReactionsEnabled: boolean;
|
||||||
|
chatEditEnabled: boolean;
|
||||||
|
chatDeleteEnabled: boolean;
|
||||||
|
keyboardFocused: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatMessageToolbar: React.FC<ChatMessageToolbarProps> = (props) => {
|
const ChatMessageToolbar: React.FC<ChatMessageToolbarProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
messageId, chatId, message, username, onEmojiSelected, onMenuOpenChange,
|
messageId, chatId, message, username, onEmojiSelected, deleted,
|
||||||
messageSequence, emphasizedMessage, onEditRequest, own, amIModerator, menuIsOpen,
|
messageSequence, emphasizedMessage, own, amIModerator, locked,
|
||||||
onReactionPopoverOpenChange, reactionPopoverIsOpen,
|
onReactionPopoverOpenChange, reactionPopoverIsOpen, hasToolbar, keyboardFocused,
|
||||||
|
chatDeleteEnabled, chatEditEnabled, chatReactionsEnabled, chatReplyEnabled,
|
||||||
} = props;
|
} = props;
|
||||||
const [reactionsAnchor, setReactionsAnchor] = React.useState<Element | null>(
|
const [reactionsAnchor, setReactionsAnchor] = React.useState<Element | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [isTryingToDelete, setIsTryingToDelete] = React.useState(false);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [chatDeleteMessage] = useMutation(CHAT_DELETE_MESSAGE_MUTATION);
|
const [chatDeleteMessage] = useMutation(CHAT_DELETE_MESSAGE_MUTATION);
|
||||||
|
|
||||||
const isRTL = layoutSelect((i: Layout) => i.isRTL);
|
const isRTL = layoutSelect((i: Layout) => i.isRTL);
|
||||||
const CHAT_REPLIES_ENABLED = useIsReplyChatMessageEnabled();
|
|
||||||
const CHAT_REACTIONS_ENABLED = useIsChatMessageReactionsEnabled();
|
|
||||||
const CHAT_EDIT_ENABLED = useIsEditChatMessageEnabled();
|
|
||||||
const CHAT_DELETE_ENABLED = useIsDeleteChatMessageEnabled();
|
|
||||||
const actions = [];
|
|
||||||
|
|
||||||
if (CHAT_EDIT_ENABLED && own) {
|
|
||||||
actions.push({
|
|
||||||
key: 'edit',
|
|
||||||
icon: 'pen_tool',
|
|
||||||
label: intl.formatMessage(intlMessages.edit),
|
|
||||||
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onEditRequest();
|
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent(ChatEvents.CHAT_EDIT_REQUEST, {
|
|
||||||
detail: {
|
|
||||||
messageId,
|
|
||||||
chatId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CHAT_DELETE_ENABLED && (own || amIModerator)) {
|
|
||||||
const customStyles = { background: colorDanger, color: colorWhite };
|
|
||||||
actions.push({
|
|
||||||
key: 'delete',
|
|
||||||
icon: 'delete',
|
|
||||||
label: intl.formatMessage(intlMessages.delete),
|
|
||||||
customStyles,
|
|
||||||
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
chatDeleteMessage({
|
|
||||||
variables: {
|
|
||||||
chatId,
|
|
||||||
messageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([
|
if ([
|
||||||
CHAT_REPLIES_ENABLED,
|
chatReplyEnabled,
|
||||||
CHAT_REACTIONS_ENABLED,
|
chatReactionsEnabled,
|
||||||
CHAT_EDIT_ENABLED,
|
chatEditEnabled,
|
||||||
CHAT_DELETE_ENABLED,
|
chatDeleteEnabled,
|
||||||
].every((config) => !config)) return null;
|
].every((config) => !config) || !hasToolbar || locked || deleted) return null;
|
||||||
|
|
||||||
|
const showReplyButton = chatReplyEnabled;
|
||||||
|
const showReactionsButton = chatReactionsEnabled;
|
||||||
|
const showEditButton = chatEditEnabled && own;
|
||||||
|
const showDeleteButton = chatDeleteEnabled && (own || amIModerator);
|
||||||
|
const showDivider = (showReplyButton || showReactionsButton) && (showEditButton || showDeleteButton);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Root
|
||||||
className="chat-message-toolbar"
|
|
||||||
$sequence={messageSequence}
|
|
||||||
$menuIsOpen={menuIsOpen}
|
|
||||||
$reactionPopoverIsOpen={reactionPopoverIsOpen}
|
$reactionPopoverIsOpen={reactionPopoverIsOpen}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Escape' && keyboardFocused) {
|
||||||
|
window.dispatchEvent(new CustomEvent(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_CANCEL));
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{CHAT_REPLIES_ENABLED && (
|
<FocusTrap open={keyboardFocused}>
|
||||||
<>
|
<Container className="chat-message-toolbar">
|
||||||
<Button
|
{showReplyButton && (
|
||||||
circle
|
<>
|
||||||
ghost
|
<EmojiButton
|
||||||
aria-describedby={`chat-reply-btn-label-${messageSequence}`}
|
aria-describedby={`chat-reply-btn-label-${messageSequence}`}
|
||||||
icon="undo"
|
icon="undo"
|
||||||
color="light"
|
color="light"
|
||||||
size="sm"
|
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
type="button"
|
e.stopPropagation();
|
||||||
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
window.dispatchEvent(
|
||||||
e.stopPropagation();
|
new CustomEvent(ChatEvents.CHAT_REPLY_INTENTION, {
|
||||||
window.dispatchEvent(
|
detail: {
|
||||||
new CustomEvent(ChatEvents.CHAT_REPLY_INTENTION, {
|
username,
|
||||||
detail: {
|
message,
|
||||||
username,
|
messageId,
|
||||||
message,
|
chatId,
|
||||||
messageId,
|
emphasizedMessage,
|
||||||
chatId,
|
sequence: messageSequence,
|
||||||
emphasizedMessage,
|
},
|
||||||
sequence: messageSequence,
|
}),
|
||||||
},
|
);
|
||||||
}),
|
window.dispatchEvent(
|
||||||
);
|
new CustomEvent(ChatEvents.CHAT_CANCEL_EDIT_REQUEST),
|
||||||
}}
|
);
|
||||||
/>
|
}}
|
||||||
<span id={`chat-reply-btn-label-${messageSequence}`} className="sr-only">
|
/>
|
||||||
{intl.formatMessage(intlMessages.reply, { 0: messageSequence })}
|
<span id={`chat-reply-btn-label-${messageSequence}`} className="sr-only">
|
||||||
</span>
|
{intl.formatMessage(intlMessages.reply, { 0: messageSequence })}
|
||||||
</>
|
</span>
|
||||||
)}
|
</>
|
||||||
{CHAT_REACTIONS_ENABLED && (
|
)}
|
||||||
<EmojiButton
|
{showReactionsButton && (
|
||||||
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
<EmojiButton
|
||||||
e.stopPropagation();
|
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
onReactionPopoverOpenChange(true);
|
e.stopPropagation();
|
||||||
}}
|
onReactionPopoverOpenChange(true);
|
||||||
setRef={setReactionsAnchor}
|
|
||||||
size="sm"
|
|
||||||
icon="happy"
|
|
||||||
color="light"
|
|
||||||
ghost
|
|
||||||
type="button"
|
|
||||||
circle
|
|
||||||
data-test="emojiPickerButton"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{actions.length > 0 && (
|
|
||||||
<BBBMenu
|
|
||||||
trigger={(
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
onMenuOpenChange(true);
|
|
||||||
}}
|
}}
|
||||||
size="sm"
|
icon="happy"
|
||||||
icon="more"
|
|
||||||
color="light"
|
color="light"
|
||||||
ghost
|
data-test="emojiPickerButton"
|
||||||
type="button"
|
ref={setReactionsAnchor}
|
||||||
circle
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
actions={actions}
|
{showDivider && <Divider role="separator" />}
|
||||||
onCloseCallback={() => {
|
{showEditButton && (
|
||||||
onMenuOpenChange(false);
|
<EmojiButton
|
||||||
}}
|
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
opts={{
|
e.stopPropagation();
|
||||||
id: 'app-settings-dropdown-menu',
|
window.dispatchEvent(
|
||||||
keepMounted: true,
|
new CustomEvent(ChatEvents.CHAT_EDIT_REQUEST, {
|
||||||
transitionDuration: 0,
|
detail: {
|
||||||
elevation: 3,
|
messageId,
|
||||||
getcontentanchorel: null,
|
chatId,
|
||||||
fullwidth: 'true',
|
message,
|
||||||
anchorOrigin: {
|
},
|
||||||
vertical: 'bottom',
|
}),
|
||||||
horizontal: isRTL ? 'left' : 'right',
|
);
|
||||||
},
|
window.dispatchEvent(
|
||||||
transformorigin: {
|
new CustomEvent(ChatEvents.CHAT_CANCEL_REPLY_INTENTION),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
icon="pen_tool"
|
||||||
|
color="light"
|
||||||
|
data-test="editMessageButton"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showDeleteButton && (
|
||||||
|
<EmojiButton
|
||||||
|
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsTryingToDelete(true);
|
||||||
|
}}
|
||||||
|
icon="delete"
|
||||||
|
color="light"
|
||||||
|
data-test="deleteMessageButton"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Popover
|
||||||
|
open={reactionPopoverIsOpen}
|
||||||
|
anchorEl={reactionsAnchor}
|
||||||
|
onClose={() => {
|
||||||
|
onReactionPopoverOpenChange(false);
|
||||||
|
}}
|
||||||
|
anchorOrigin={{
|
||||||
vertical: 'top',
|
vertical: 'top',
|
||||||
horizontal: isRTL ? 'left' : 'right',
|
horizontal: isRTL ? 'left' : 'right',
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Popover
|
|
||||||
open={reactionPopoverIsOpen}
|
|
||||||
anchorEl={reactionsAnchor}
|
|
||||||
onClose={() => {
|
|
||||||
setReactionsAnchor(null);
|
|
||||||
onReactionPopoverOpenChange(false);
|
|
||||||
}}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: isRTL ? 'left' : 'right',
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: isRTL ? 'right' : 'left',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EmojiPickerWrapper>
|
|
||||||
<EmojiPicker
|
|
||||||
onEmojiSelect={(emojiObject: { id: string; native: string }) => {
|
|
||||||
onEmojiSelected(emojiObject);
|
|
||||||
}}
|
}}
|
||||||
showPreview={false}
|
transformOrigin={{
|
||||||
showSkinTones={false}
|
vertical: 'top',
|
||||||
/>
|
horizontal: isRTL ? 'right' : 'left',
|
||||||
</EmojiPickerWrapper>
|
}}
|
||||||
</Popover>
|
>
|
||||||
</Container>
|
<EmojiPickerWrapper>
|
||||||
|
<EmojiPicker
|
||||||
|
onEmojiSelect={(emojiObject: { id: string; native: string }) => {
|
||||||
|
onEmojiSelected(emojiObject);
|
||||||
|
}}
|
||||||
|
showPreview={false}
|
||||||
|
showSkinTones={false}
|
||||||
|
/>
|
||||||
|
</EmojiPickerWrapper>
|
||||||
|
</Popover>
|
||||||
|
{isTryingToDelete && (
|
||||||
|
<ConfirmationModal
|
||||||
|
isOpen={isTryingToDelete}
|
||||||
|
setIsOpen={setIsTryingToDelete}
|
||||||
|
onRequestClose={() => setIsTryingToDelete(false)}
|
||||||
|
onConfirm={() => {
|
||||||
|
chatDeleteMessage({
|
||||||
|
variables: {
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
title={intl.formatMessage(intlMessages.confirmationTitle)}
|
||||||
|
confirmButtonLabel={intl.formatMessage(intlMessages.delete)}
|
||||||
|
cancelButtonLabel={intl.formatMessage(intlMessages.cancelLabel)}
|
||||||
|
description={intl.formatMessage(intlMessages.confirmationDescription)}
|
||||||
|
confirmButtonColor="danger"
|
||||||
|
priority="low"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</FocusTrap>
|
||||||
|
</Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 styled, { css } from 'styled-components';
|
||||||
import {
|
import {
|
||||||
colorGrayLighter,
|
colorGrayLighter,
|
||||||
colorOffWhite,
|
colorGrayLightest,
|
||||||
colorWhite,
|
colorWhite,
|
||||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||||
import { borderRadius, smPaddingX } from '/imports/ui/stylesheets/styled-components/general';
|
import { $2xlPadding, borderRadius, smPadding } from '/imports/ui/stylesheets/styled-components/general';
|
||||||
import EmojiPickerComponent from '/imports/ui/components/emoji-picker/component';
|
import EmojiPickerComponent from '/imports/ui/components/emoji-picker/component';
|
||||||
import Button from '/imports/ui/components/common/button/component';
|
import BaseEmojiButton from './emoji-button/component';
|
||||||
|
|
||||||
const Container = styled.div<{ $sequence: number, $menuIsOpen: boolean, $reactionPopoverIsOpen: boolean }>`
|
interface RootProps {
|
||||||
height: calc(1.5rem + 12px);
|
$reactionPopoverIsOpen: boolean;
|
||||||
line-height: calc(1.5rem + 8px);
|
}
|
||||||
max-width: 184px;
|
|
||||||
overflow: hidden;
|
const Root = styled.div<RootProps>`
|
||||||
|
padding-bottom: ${smPadding};
|
||||||
|
justify-content: flex-end;
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
bottom: 100%;
|
||||||
border: 1px solid ${colorOffWhite};
|
z-index: 10;
|
||||||
border-radius: 8px;
|
|
||||||
padding: 1px;
|
|
||||||
background-color: ${colorWhite};
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
#chat-message-wrapper:hover & {
|
[dir='ltr'] & {
|
||||||
|
padding-left: ${$2xlPadding};
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] & {
|
||||||
|
padding-right: ${$2xlPadding};
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-message-wrapper:hover &,
|
||||||
|
.chat-message-wrapper:focus &,
|
||||||
|
.chat-message-wrapper-focused &,
|
||||||
|
.chat-message-wrapper-keyboard-focused &,
|
||||||
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
${({ $menuIsOpen, $reactionPopoverIsOpen }) => (($menuIsOpen || $reactionPopoverIsOpen) && css`
|
${({ $reactionPopoverIsOpen }) => ($reactionPopoverIsOpen && css`
|
||||||
display: flex;
|
display: flex;
|
||||||
`)}
|
`)}
|
||||||
|
`;
|
||||||
|
|
||||||
${({ $sequence }) => (($sequence === 1)
|
const Container = styled.div`
|
||||||
? css`
|
max-width: max-content;
|
||||||
bottom: 0;
|
display: flex;
|
||||||
transform: translateY(50%);
|
border-radius: 1rem;
|
||||||
`
|
background-color: ${colorWhite};
|
||||||
: css`
|
box-shadow: 0 0.125rem 0.125rem 0 ${colorGrayLighter};
|
||||||
top: 0;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
`)}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const EmojiPickerWrapper = styled.div`
|
const EmojiPickerWrapper = styled.div`
|
||||||
@ -47,7 +58,7 @@ const EmojiPickerWrapper = styled.div`
|
|||||||
right: 0;
|
right: 0;
|
||||||
border: 1px solid ${colorGrayLighter};
|
border: 1px solid ${colorGrayLighter};
|
||||||
border-radius: ${borderRadius};
|
border-radius: ${borderRadius};
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
box-shadow: 0 0.125rem 10px rgba(0,0,0,0.1);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
|
||||||
.emoji-mart {
|
.emoji-mart {
|
||||||
@ -70,29 +81,25 @@ const EmojiPickerWrapper = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// @ts-ignore
|
const EmojiButton = styled(BaseEmojiButton)``;
|
||||||
const EmojiButton = styled(Button)`
|
|
||||||
margin:0 0 0 ${smPaddingX};
|
|
||||||
align-self: center;
|
|
||||||
font-size: 0.5rem;
|
|
||||||
|
|
||||||
[dir="rtl"] & {
|
|
||||||
margin: 0 ${smPaddingX} 0 0;
|
|
||||||
-webkit-transform: scale(-1, 1);
|
|
||||||
-moz-transform: scale(-1, 1);
|
|
||||||
-ms-transform: scale(-1, 1);
|
|
||||||
-o-transform: scale(-1, 1);
|
|
||||||
transform: scale(-1, 1);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const EmojiPicker = styled(EmojiPickerComponent)`
|
const EmojiPicker = styled(EmojiPickerComponent)`
|
||||||
position: relative;
|
position: relative;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const Divider = styled.div`
|
||||||
|
width: 0.125rem;
|
||||||
|
height: 75%;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: ${colorGrayLightest};
|
||||||
|
align-self: center;
|
||||||
|
`;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Container,
|
Container,
|
||||||
EmojiPicker,
|
EmojiPicker,
|
||||||
EmojiPickerWrapper,
|
EmojiPickerWrapper,
|
||||||
EmojiButton,
|
EmojiButton,
|
||||||
|
Root,
|
||||||
|
Divider,
|
||||||
};
|
};
|
||||||
|
@ -1,22 +1,26 @@
|
|||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
borderSize,
|
|
||||||
userIndicatorsOffset,
|
userIndicatorsOffset,
|
||||||
smPaddingX,
|
smPaddingX,
|
||||||
|
smPaddingY,
|
||||||
|
lgPadding,
|
||||||
|
$3xlPadding,
|
||||||
|
xlPadding,
|
||||||
|
mdPadding,
|
||||||
} from '/imports/ui/stylesheets/styled-components/general';
|
} from '/imports/ui/stylesheets/styled-components/general';
|
||||||
import {
|
import {
|
||||||
lineHeightComputed,
|
|
||||||
fontSizeBase,
|
fontSizeBase,
|
||||||
|
fontSizeSmaller,
|
||||||
} from '/imports/ui/stylesheets/styled-components/typography';
|
} from '/imports/ui/stylesheets/styled-components/typography';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
colorWhite,
|
colorWhite,
|
||||||
userListBg,
|
userListBg,
|
||||||
colorSuccess,
|
colorSuccess,
|
||||||
colorOffWhite,
|
colorBlueLightest,
|
||||||
colorText,
|
colorGrayLight,
|
||||||
palettePlaceholderText,
|
colorGrayLightest,
|
||||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||||
|
|
||||||
import Header from '/imports/ui/components/common/control-header/component';
|
import Header from '/imports/ui/components/common/control-header/component';
|
||||||
@ -26,17 +30,16 @@ interface ChatWrapperProps {
|
|||||||
isSystemSender: boolean;
|
isSystemSender: boolean;
|
||||||
isPresentationUpload?: boolean;
|
isPresentationUpload?: boolean;
|
||||||
isCustomPluginMessage: boolean;
|
isCustomPluginMessage: boolean;
|
||||||
$highlight: boolean;
|
|
||||||
$toolbarMenuIsOpen: boolean;
|
|
||||||
$reactionPopoverIsOpen: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatContentProps {
|
interface ChatContentProps {
|
||||||
sameSender: boolean;
|
sameSender: boolean;
|
||||||
isCustomPluginMessage: boolean;
|
isCustomPluginMessage: boolean;
|
||||||
|
$editing: boolean;
|
||||||
$highlight: boolean;
|
$highlight: boolean;
|
||||||
$toolbarMenuIsOpen: boolean;
|
|
||||||
$reactionPopoverIsOpen: boolean;
|
$reactionPopoverIsOpen: boolean;
|
||||||
|
$focused: boolean;
|
||||||
|
$keyboardFocused: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatAvatarProps {
|
interface ChatAvatarProps {
|
||||||
@ -48,12 +51,17 @@ interface ChatAvatarProps {
|
|||||||
|
|
||||||
export const ChatWrapper = styled.div<ChatWrapperProps>`
|
export const ChatWrapper = styled.div<ChatWrapperProps>`
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
gap: ${smPaddingY};
|
||||||
|
position: relative;
|
||||||
|
font-size: ${fontSizeBase};
|
||||||
|
position: relative;
|
||||||
|
|
||||||
[dir='rtl'] & {
|
[dir='rtl'] & {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
}
|
}
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
position: relative;
|
|
||||||
${({ isPresentationUpload }) => isPresentationUpload && `
|
${({ isPresentationUpload }) => isPresentationUpload && `
|
||||||
border-left: 2px solid #0F70D7;
|
border-left: 2px solid #0F70D7;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
@ -61,18 +69,6 @@ export const ChatWrapper = styled.div<ChatWrapperProps>`
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
background-color: #F3F6F9;
|
background-color: #F3F6F9;
|
||||||
`}
|
`}
|
||||||
${({ sameSender }) => sameSender && `
|
|
||||||
flex: 1;
|
|
||||||
margin: ${borderSize} 0 0 ${borderSize};
|
|
||||||
margin-top: calc(${lineHeightComputed} / 3);
|
|
||||||
`}
|
|
||||||
${({ sameSender }) => !sameSender && `
|
|
||||||
padding-top:${lineHeightComputed};
|
|
||||||
`}
|
|
||||||
[dir="rtl"] & {
|
|
||||||
margin: ${borderSize} ${borderSize} 0 0;
|
|
||||||
}
|
|
||||||
font-size: ${fontSizeBase};
|
|
||||||
${({ isSystemSender }) => isSystemSender && `
|
${({ isSystemSender }) => isSystemSender && `
|
||||||
background-color: #fef9f1;
|
background-color: #fef9f1;
|
||||||
border-left: 2px solid #f5c67f;
|
border-left: 2px solid #f5c67f;
|
||||||
@ -83,42 +79,25 @@ export const ChatWrapper = styled.div<ChatWrapperProps>`
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
`}
|
`}
|
||||||
${({ sameSender, $highlight }) => !sameSender && $highlight && `
|
|
||||||
&:hover {
|
|
||||||
background-color: ${colorOffWhite};
|
|
||||||
}
|
|
||||||
border-radius: 6px;
|
|
||||||
`}
|
|
||||||
${({ sameSender, $toolbarMenuIsOpen, $reactionPopoverIsOpen }) => !sameSender
|
|
||||||
&& ($toolbarMenuIsOpen || $reactionPopoverIsOpen)
|
|
||||||
&& `
|
|
||||||
background-color: ${colorOffWhite};
|
|
||||||
border-radius: 6px;
|
|
||||||
`}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ChatContent = styled.div<ChatContentProps>`
|
export const ChatContent = styled.div<ChatContentProps>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
${({ sameSender, isCustomPluginMessage }) => sameSender
|
${({ $highlight }) => $highlight && `
|
||||||
&& !isCustomPluginMessage && `
|
.chat-message-wrapper:hover > & {
|
||||||
margin-left: 2.6rem;
|
background-color: ${colorBlueLightest};
|
||||||
`}
|
|
||||||
|
|
||||||
${({ sameSender, $highlight }) => sameSender && $highlight && `
|
|
||||||
&:hover {
|
|
||||||
background-color: ${colorOffWhite};
|
|
||||||
}
|
}
|
||||||
border-radius: 6px;
|
|
||||||
`}
|
`}
|
||||||
|
|
||||||
${({ sameSender, $toolbarMenuIsOpen, $reactionPopoverIsOpen }) => sameSender
|
${({
|
||||||
&& ($toolbarMenuIsOpen || $reactionPopoverIsOpen)
|
$editing, $reactionPopoverIsOpen, $focused, $keyboardFocused,
|
||||||
|
}) => ($reactionPopoverIsOpen || $editing || $focused || $keyboardFocused)
|
||||||
&& `
|
&& `
|
||||||
background-color: ${colorOffWhite};
|
background-color: ${colorBlueLightest};
|
||||||
border-radius: 6px;
|
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -212,29 +191,46 @@ export const ChatAvatar = styled.div<ChatAvatarProps>`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Container = styled.div`
|
export const Container = styled.div<{ $sequence: number }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: calc((${fontSizeSmaller} + ${lgPadding} * 2) / 2);
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const MessageItemWrapper = styled.div`
|
export const MessageItemWrapper = styled.div<{ $edited: boolean, $sameSender: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
padding: ${lgPadding} ${$3xlPadding};
|
||||||
|
|
||||||
|
${({ $edited, $sameSender }) => $edited && $sameSender && `
|
||||||
|
padding-bottom: 0;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DeleteMessage = styled.span`
|
export const DeleteMessage = styled.span`
|
||||||
font-style: italic;
|
color: ${colorGrayLight};
|
||||||
font-weight: bold;
|
padding: ${mdPadding} ${xlPadding};
|
||||||
color: ${colorText};
|
border: 1px solid ${colorGrayLightest};
|
||||||
|
border-radius: 0.375rem;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ChatEditTime = styled.time`
|
export const ChatHeading = styled.div`
|
||||||
flex-shrink: 1;
|
display: flex;
|
||||||
flex-grow: 0;
|
`;
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
export const EditLabel = styled.span`
|
||||||
overflow: hidden;
|
color: ${colorGrayLight};
|
||||||
min-width: 0;
|
font-size: 75%;
|
||||||
font-size: 75%;
|
display: flex;
|
||||||
color: ${palettePlaceholderText};
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EditLabelWrapper = styled.div`
|
||||||
|
line-height: 1;
|
||||||
|
padding: ${xlPadding};
|
||||||
`;
|
`;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
||||||
import React, {
|
import React, {
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
@ -20,6 +19,7 @@ import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
|||||||
import { useCreateUseSubscription } from '/imports/ui/core/hooks/createUseSubscription';
|
import { useCreateUseSubscription } from '/imports/ui/core/hooks/createUseSubscription';
|
||||||
import { setLoadedMessageGathering } from '/imports/ui/core/hooks/useLoadedChatMessages';
|
import { setLoadedMessageGathering } from '/imports/ui/core/hooks/useLoadedChatMessages';
|
||||||
import { ChatLoading } from '../../component';
|
import { ChatLoading } from '../../component';
|
||||||
|
import { ChatEvents } from '/imports/ui/core/enums/chat';
|
||||||
|
|
||||||
interface ChatListPageContainerProps {
|
interface ChatListPageContainerProps {
|
||||||
page: number;
|
page: number;
|
||||||
@ -29,6 +29,7 @@ interface ChatListPageContainerProps {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
markMessageAsSeen: (message: Message) => void;
|
markMessageAsSeen: (message: Message) => void;
|
||||||
scrollRef: React.RefObject<HTMLDivElement>;
|
scrollRef: React.RefObject<HTMLDivElement>;
|
||||||
|
focusedId: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ChatListPageProps {
|
interface ChatListPageProps {
|
||||||
@ -38,6 +39,7 @@ interface ChatListPageProps {
|
|||||||
page: number;
|
page: number;
|
||||||
markMessageAsSeen: (message: Message)=> void;
|
markMessageAsSeen: (message: Message)=> void;
|
||||||
scrollRef: React.RefObject<HTMLDivElement>;
|
scrollRef: React.RefObject<HTMLDivElement>;
|
||||||
|
focusedId: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const areChatPagesEqual = (prevProps: ChatListPageProps, nextProps: ChatListPageProps) => {
|
const areChatPagesEqual = (prevProps: ChatListPageProps, nextProps: ChatListPageProps) => {
|
||||||
@ -53,7 +55,7 @@ const areChatPagesEqual = (prevProps: ChatListPageProps, nextProps: ChatListPage
|
|||||||
&& prevMessage?.message === nextMessage?.message
|
&& prevMessage?.message === nextMessage?.message
|
||||||
&& prevMessage?.reactions?.length === nextMessage?.reactions?.length
|
&& prevMessage?.reactions?.length === nextMessage?.reactions?.length
|
||||||
);
|
);
|
||||||
});
|
}) && prevProps.focusedId === nextProps.focusedId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatListPage: React.FC<ChatListPageProps> = ({
|
const ChatListPage: React.FC<ChatListPageProps> = ({
|
||||||
@ -63,6 +65,7 @@ const ChatListPage: React.FC<ChatListPageProps> = ({
|
|||||||
page,
|
page,
|
||||||
markMessageAsSeen,
|
markMessageAsSeen,
|
||||||
scrollRef,
|
scrollRef,
|
||||||
|
focusedId,
|
||||||
}) => {
|
}) => {
|
||||||
const { domElementManipulationIdentifiers } = useContext(PluginsContext);
|
const { domElementManipulationIdentifiers } = useContext(PluginsContext);
|
||||||
|
|
||||||
@ -83,6 +86,29 @@ const ChatListPage: React.FC<ChatListPageProps> = ({
|
|||||||
);
|
);
|
||||||
}, [domElementManipulationIdentifiers, messagesRequestedFromPlugin]);
|
}, [domElementManipulationIdentifiers, messagesRequestedFromPlugin]);
|
||||||
|
|
||||||
|
const [keyboardFocusedMessageSequence, setKeyboardFocusedMessageSequence] = useState<number | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyboardFocusMessageRequest = (e: Event) => {
|
||||||
|
if (e instanceof CustomEvent) {
|
||||||
|
setKeyboardFocusedMessageSequence(Number.parseInt(e.detail.sequence, 10));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyboardFocusMessageCancel = (e: Event) => {
|
||||||
|
if (e instanceof CustomEvent) {
|
||||||
|
setKeyboardFocusedMessageSequence(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_REQUEST, handleKeyboardFocusMessageRequest);
|
||||||
|
window.addEventListener(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_CANCEL, handleKeyboardFocusMessageCancel);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_REQUEST, handleKeyboardFocusMessageRequest);
|
||||||
|
window.removeEventListener(ChatEvents.CHAT_KEYBOARD_FOCUS_MESSAGE_CANCEL, handleKeyboardFocusMessageCancel);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={`messagePage-${page}`}>
|
<React.Fragment key={`messagePage-${page}`}>
|
||||||
{messages.map((message, index, messagesArray) => {
|
{messages.map((message, index, messagesArray) => {
|
||||||
@ -99,6 +125,8 @@ const ChatListPage: React.FC<ChatListPageProps> = ({
|
|||||||
scrollRef={scrollRef}
|
scrollRef={scrollRef}
|
||||||
markMessageAsSeen={markMessageAsSeen}
|
markMessageAsSeen={markMessageAsSeen}
|
||||||
messageReadFeedbackEnabled={messageReadFeedbackEnabled}
|
messageReadFeedbackEnabled={messageReadFeedbackEnabled}
|
||||||
|
focused={focusedId === message.messageSequence}
|
||||||
|
keyboardFocused={keyboardFocusedMessageSequence === message.messageSequence}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -116,8 +144,8 @@ const ChatListPageContainer: React.FC<ChatListPageContainerProps> = ({
|
|||||||
chatId,
|
chatId,
|
||||||
markMessageAsSeen,
|
markMessageAsSeen,
|
||||||
scrollRef,
|
scrollRef,
|
||||||
|
focusedId,
|
||||||
}) => {
|
}) => {
|
||||||
// @ts-ignore - temporary, while meteor exists in the project
|
|
||||||
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
|
||||||
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
|
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
|
||||||
const PRIVATE_MESSAGE_READ_FEEDBACK_ENABLED = CHAT_CONFIG.privateMessageReadFeedback.enabled;
|
const PRIVATE_MESSAGE_READ_FEEDBACK_ENABLED = CHAT_CONFIG.privateMessageReadFeedback.enabled;
|
||||||
@ -157,6 +185,7 @@ const ChatListPageContainer: React.FC<ChatListPageContainerProps> = ({
|
|||||||
page={page}
|
page={page}
|
||||||
markMessageAsSeen={markMessageAsSeen}
|
markMessageAsSeen={markMessageAsSeen}
|
||||||
scrollRef={scrollRef}
|
scrollRef={scrollRef}
|
||||||
|
focusedId={focusedId}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import {
|
import {
|
||||||
mdPaddingX,
|
|
||||||
smPaddingX,
|
smPaddingX,
|
||||||
borderRadius,
|
borderRadius,
|
||||||
} from '/imports/ui/stylesheets/styled-components/general';
|
} from '/imports/ui/stylesheets/styled-components/general';
|
||||||
@ -15,8 +14,7 @@ interface MessageListProps {
|
|||||||
export const MessageList = styled(ScrollboxVertical)<MessageListProps>`
|
export const MessageList = styled(ScrollboxVertical)<MessageListProps>`
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
right: 0 ${mdPaddingX} 0 0;
|
padding-top: 2rem;
|
||||||
padding-top: 0;
|
|
||||||
outline-style: none;
|
outline-style: none;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
@ -27,11 +25,6 @@ export const MessageList = styled(ScrollboxVertical)<MessageListProps>`
|
|||||||
display: flex;
|
display: flex;
|
||||||
padding-bottom: ${smPaddingX};
|
padding-bottom: ${smPaddingX};
|
||||||
|
|
||||||
[dir='rtl'] & {
|
|
||||||
margin: 0 0 0 auto;
|
|
||||||
padding: 0 0 0 ${mdPaddingX};
|
|
||||||
}
|
|
||||||
|
|
||||||
${({ isRTL }) => isRTL && `
|
${({ isRTL }) => isRTL && `
|
||||||
padding-left: ${smPaddingX};
|
padding-left: ${smPaddingX};
|
||||||
`}
|
`}
|
||||||
|
@ -13,7 +13,7 @@ const ChatReplyIntention = () => {
|
|||||||
const [sequence, setSequence] = useState<number>();
|
const [sequence, setSequence] = useState<number>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: Event) => {
|
const handleReplyIntention = (e: Event) => {
|
||||||
if (e instanceof CustomEvent) {
|
if (e instanceof CustomEvent) {
|
||||||
setUsername(e.detail.username);
|
setUsername(e.detail.username);
|
||||||
setMessage(e.detail.message);
|
setMessage(e.detail.message);
|
||||||
@ -22,10 +22,21 @@ const ChatReplyIntention = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener(ChatEvents.CHAT_REPLY_INTENTION, handler);
|
const handleCancelReplyIntention = (e: Event) => {
|
||||||
|
if (e instanceof CustomEvent) {
|
||||||
|
setUsername(undefined);
|
||||||
|
setMessage(undefined);
|
||||||
|
setEmphasizedMessage(undefined);
|
||||||
|
setSequence(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener(ChatEvents.CHAT_REPLY_INTENTION, handleReplyIntention);
|
||||||
|
window.addEventListener(ChatEvents.CHAT_CANCEL_REPLY_INTENTION, handleCancelReplyIntention);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener(ChatEvents.CHAT_REPLY_INTENTION, handler);
|
window.removeEventListener(ChatEvents.CHAT_REPLY_INTENTION, handleReplyIntention);
|
||||||
|
window.removeEventListener(ChatEvents.CHAT_CANCEL_REPLY_INTENTION, handleCancelReplyIntention);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -33,9 +44,11 @@ const ChatReplyIntention = () => {
|
|||||||
animations: boolean;
|
animations: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hidden = !username || !message;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Styled.Container
|
<Styled.Container
|
||||||
$hidden={!username || !message}
|
$hidden={hidden}
|
||||||
$animations={animations}
|
$animations={animations}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.dispatchEvent(
|
window.dispatchEvent(
|
||||||
@ -49,25 +62,23 @@ const ChatReplyIntention = () => {
|
|||||||
if (sequence) Storage.setItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, sequence);
|
if (sequence) Storage.setItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, sequence);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Styled.Username>{username}</Styled.Username>
|
|
||||||
<Styled.Message>
|
<Styled.Message>
|
||||||
<ChatMessageTextContent
|
<ChatMessageTextContent
|
||||||
text={message || ''}
|
text={message || ''}
|
||||||
emphasizedMessage={!!emphasizedMessage}
|
emphasizedMessage={!!emphasizedMessage}
|
||||||
systemMsg={false}
|
|
||||||
dataTest={null}
|
dataTest={null}
|
||||||
/>
|
/>
|
||||||
</Styled.Message>
|
</Styled.Message>
|
||||||
<Styled.CloseBtn
|
<Styled.CloseBtn
|
||||||
onClick={() => {
|
onClick={(e) => {
|
||||||
setMessage(undefined);
|
e.stopPropagation();
|
||||||
setUsername(undefined);
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(ChatEvents.CHAT_CANCEL_REPLY_INTENTION),
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
icon="close"
|
icon="close"
|
||||||
ghost
|
tabIndex={hidden ? -1 : 0}
|
||||||
circle
|
aria-hidden={hidden}
|
||||||
color="light"
|
|
||||||
size="sm"
|
|
||||||
/>
|
/>
|
||||||
</Styled.Container>
|
</Styled.Container>
|
||||||
);
|
);
|
||||||
|
@ -1,68 +1,82 @@
|
|||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
import {
|
import {
|
||||||
colorBlueLight,
|
colorGrayLightest,
|
||||||
colorOffWhite,
|
colorOffWhite,
|
||||||
|
colorPrimary,
|
||||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||||
import Button from '/imports/ui/components/common/button/component';
|
import {
|
||||||
|
mdPadding, smPadding, smPaddingX, xlPadding,
|
||||||
|
} from '/imports/ui/stylesheets/styled-components/general';
|
||||||
|
import EmojiButton from '../chat-message-list/page/chat-message/message-toolbar/emoji-button/component';
|
||||||
|
|
||||||
const Container = styled.div<{ $hidden: boolean; $animations: boolean }>`
|
const Container = styled.div<{ $hidden: boolean; $animations: boolean }>`
|
||||||
border-radius: 4px;
|
border-radius: 0.375rem;
|
||||||
border-left: 4px solid ${colorBlueLight};
|
|
||||||
background-color: ${colorOffWhite};
|
background-color: ${colorOffWhite};
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-shadow: inset 0 0 0 1px ${colorGrayLightest};
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
[dir='ltr'] & {
|
||||||
|
border-right: 0.375rem solid ${colorPrimary};
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] & {
|
||||||
|
border-left: 0.375rem solid ${colorPrimary};
|
||||||
|
}
|
||||||
|
|
||||||
${({ $hidden }) => ($hidden
|
${({ $hidden }) => ($hidden
|
||||||
? css`
|
? css`
|
||||||
height: 0;
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
`
|
`
|
||||||
: css`
|
: css`
|
||||||
height: 6rem;
|
min-height: calc(1rem + ${mdPadding} * 2);
|
||||||
padding: 6px;
|
height: calc(1rem + ${mdPadding} * 2);
|
||||||
margin-right: 0.75rem;
|
padding: ${mdPadding} calc(${smPaddingX} * 1.25);
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: ${smPadding};
|
||||||
|
|
||||||
|
[dir='ltr'] & {
|
||||||
|
margin-right: ${xlPadding};
|
||||||
|
}
|
||||||
|
|
||||||
|
[dir='rtl'] & {
|
||||||
|
margin-left: ${xlPadding};
|
||||||
|
}
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
|
||||||
${({ $animations }) => $animations
|
${({ $animations }) => $animations
|
||||||
&& css`
|
&& css`
|
||||||
transition-property: height;
|
transition-property: height, min-height;
|
||||||
transition-duration: 0.1s;
|
transition-duration: 0.1s;
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Typography = styled.div`
|
const Typography = styled.div`
|
||||||
line-height: 1rem;
|
line-height: 1;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Username = styled(Typography)`
|
const Message = styled(Typography)`
|
||||||
font-weight: bold;
|
|
||||||
color: ${colorBlueLight};
|
|
||||||
line-height: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
`;
|
line-height: 1;
|
||||||
|
white-space: nowrap;
|
||||||
const Message = styled.div`
|
|
||||||
// Container height - Username height - vertical padding
|
|
||||||
max-height: calc(5rem - 12px);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// @ts-ignore
|
const CloseBtn = styled(EmojiButton)`
|
||||||
const CloseBtn = styled(Button)`
|
font-size: 75%;
|
||||||
position: absolute;
|
height: 1rem;
|
||||||
top: 2px;
|
padding: 0;
|
||||||
right: 2px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Container,
|
Container,
|
||||||
Username,
|
|
||||||
CloseBtn,
|
CloseBtn,
|
||||||
Message,
|
Message,
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { CircularProgress } from '@mui/material';
|
import { CircularProgress } from '@mui/material';
|
||||||
import ChatHeader from './chat-header/component';
|
import ChatHeader from './chat-header/component';
|
||||||
import { layoutSelect, layoutSelectInput } from '../../layout/context';
|
import { layoutSelect, layoutSelectInput } from '../../layout/context';
|
||||||
@ -14,6 +14,7 @@ import { Chat as ChatType } from '/imports/ui/Types/chat';
|
|||||||
import { layoutDispatch } from '/imports/ui/components/layout/context';
|
import { layoutDispatch } from '/imports/ui/components/layout/context';
|
||||||
import browserInfo from '/imports/utils/browserInfo';
|
import browserInfo from '/imports/utils/browserInfo';
|
||||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||||
|
import { ChatEvents } from '/imports/ui/core/enums/chat';
|
||||||
|
|
||||||
interface ChatProps {
|
interface ChatProps {
|
||||||
isRTL: boolean;
|
isRTL: boolean;
|
||||||
@ -21,6 +22,7 @@ interface ChatProps {
|
|||||||
|
|
||||||
const Chat: React.FC<ChatProps> = ({ isRTL }) => {
|
const Chat: React.FC<ChatProps> = ({ isRTL }) => {
|
||||||
const { isChrome } = browserInfo;
|
const { isChrome } = browserInfo;
|
||||||
|
const isEditingMessage = useRef(false);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleMouseDown = () => {
|
const handleMouseDown = () => {
|
||||||
@ -32,10 +34,36 @@ const Chat: React.FC<ChatProps> = ({ isRTL }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape' && isEditingMessage.current) {
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent(ChatEvents.CHAT_CANCEL_EDIT_REQUEST),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditingMessage = (e: Event) => {
|
||||||
|
if (e instanceof CustomEvent) {
|
||||||
|
isEditingMessage.current = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelEditingMessage = (e: Event) => {
|
||||||
|
if (e instanceof CustomEvent) {
|
||||||
|
isEditingMessage.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleMouseDown);
|
document.addEventListener('mousedown', handleMouseDown);
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
window.addEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleEditingMessage);
|
||||||
|
window.addEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelEditingMessage);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleMouseDown);
|
document.removeEventListener('mousedown', handleMouseDown);
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
window.removeEventListener(ChatEvents.CHAT_EDIT_REQUEST, handleEditingMessage);
|
||||||
|
window.removeEventListener(ChatEvents.CHAT_CANCEL_EDIT_REQUEST, handleCancelEditingMessage);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -73,9 +73,9 @@ interface ExternalVideoPlayerProps {
|
|||||||
externalVideo: ExternalVideo;
|
externalVideo: ExternalVideo;
|
||||||
playing: boolean;
|
playing: boolean;
|
||||||
playerPlaybackRate: number;
|
playerPlaybackRate: number;
|
||||||
key: string;
|
playerKey: string;
|
||||||
isSidebarContentOpen: boolean;
|
isSidebarContentOpen: boolean;
|
||||||
setKey: (key: string) => void;
|
setPlayerKey: (key: string) => void;
|
||||||
sendMessage: (event: string, data: {
|
sendMessage: (event: string, data: {
|
||||||
rate: number;
|
rate: number;
|
||||||
time: number;
|
time: number;
|
||||||
@ -104,8 +104,8 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
|||||||
playing,
|
playing,
|
||||||
playerPlaybackRate,
|
playerPlaybackRate,
|
||||||
isEchoTest,
|
isEchoTest,
|
||||||
key,
|
playerKey,
|
||||||
setKey,
|
setPlayerKey,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
getCurrentTime,
|
getCurrentTime,
|
||||||
}) => {
|
}) => {
|
||||||
@ -130,15 +130,15 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
|||||||
return {
|
return {
|
||||||
// default option for all players, can be overwritten
|
// default option for all players, can be overwritten
|
||||||
playerOptions: {
|
playerOptions: {
|
||||||
autoplay: true,
|
autoPlay: true,
|
||||||
playsinline: true,
|
playsInline: true,
|
||||||
controls: isPresenter,
|
controls: isPresenter,
|
||||||
},
|
},
|
||||||
file: {
|
file: {
|
||||||
attributes: {
|
attributes: {
|
||||||
controls: isPresenter ? 'controls' : '',
|
controls: isPresenter ? 'controls' : '',
|
||||||
autoplay: 'autoplay',
|
autoPlay: true,
|
||||||
playsinline: 'playsinline',
|
playsInline: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
facebook: {
|
facebook: {
|
||||||
@ -263,16 +263,14 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPresenter !== presenterRef.current) {
|
if (isPresenter !== presenterRef.current) {
|
||||||
setKey(uniqueId('react-player'));
|
setPlayerKey(uniqueId('react-player'));
|
||||||
presenterRef.current = isPresenter;
|
presenterRef.current = isPresenter;
|
||||||
}
|
}
|
||||||
}, [isPresenter]);
|
}, [isPresenter]);
|
||||||
|
|
||||||
const handleOnStart = () => {
|
const handleOnStart = () => {
|
||||||
if (!isPresenter) {
|
currentTime = getCurrentTime();
|
||||||
currentTime = getCurrentTime();
|
playerRef.current?.seekTo(truncateTime(currentTime), 'seconds');
|
||||||
playerRef.current?.seekTo(truncateTime(currentTime), 'seconds');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnPlay = () => {
|
const handleOnPlay = () => {
|
||||||
@ -308,10 +306,6 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnReady = (reactPlayer: ReactPlayer) => {
|
|
||||||
reactPlayer.seekTo(truncateTime(currentTime), 'seconds');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProgress = (state: OnProgressProps) => {
|
const handleProgress = (state: OnProgressProps) => {
|
||||||
setPlayed(state.played);
|
setPlayed(state.played);
|
||||||
setLoaded(state.loaded);
|
setLoaded(state.loaded);
|
||||||
@ -379,12 +373,11 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
|||||||
url={videoUrl}
|
url={videoUrl}
|
||||||
playing={playing}
|
playing={playing}
|
||||||
playbackRate={playerPlaybackRate}
|
playbackRate={playerPlaybackRate}
|
||||||
key={key}
|
key={playerKey}
|
||||||
height="100%"
|
height="100%"
|
||||||
width="100%"
|
width="100%"
|
||||||
ref={playerRef}
|
ref={playerRef}
|
||||||
volume={volume}
|
volume={volume}
|
||||||
onReady={handleOnReady}
|
|
||||||
onStart={handleOnStart}
|
onStart={handleOnStart}
|
||||||
onPlay={handleOnPlay}
|
onPlay={handleOnPlay}
|
||||||
onDuration={handleDuration}
|
onDuration={handleDuration}
|
||||||
@ -397,7 +390,7 @@ const ExternalVideoPlayer: React.FC<ExternalVideoPlayerProps> = ({
|
|||||||
shouldShowTools() ? (
|
shouldShowTools() ? (
|
||||||
<ExternalVideoPlayerToolbar
|
<ExternalVideoPlayerToolbar
|
||||||
handleOnMuted={(m: boolean) => { setMute(m); }}
|
handleOnMuted={(m: boolean) => { setMute(m); }}
|
||||||
handleReload={() => setKey(uniqueId('react-player'))}
|
handleReload={() => setPlayerKey(uniqueId('react-player'))}
|
||||||
setShowHoverToolBar={setShowHoverToolBar}
|
setShowHoverToolBar={setShowHoverToolBar}
|
||||||
toolbarStyle={toolbarStyle}
|
toolbarStyle={toolbarStyle}
|
||||||
handleVolumeChanged={changeVolume}
|
handleVolumeChanged={changeVolume}
|
||||||
@ -578,8 +571,8 @@ const ExternalVideoPlayerContainer: React.FC = () => {
|
|||||||
fullscreenContext={fullscreenContext}
|
fullscreenContext={fullscreenContext}
|
||||||
externalVideo={externalVideo}
|
externalVideo={externalVideo}
|
||||||
getCurrentTime={getCurrentTime}
|
getCurrentTime={getCurrentTime}
|
||||||
key={key}
|
playerKey={key}
|
||||||
setKey={setKey}
|
setPlayerKey={setKey}
|
||||||
sendMessage={sendMessage}
|
sendMessage={sendMessage}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
export const enum ChatEvents {
|
export const enum ChatEvents {
|
||||||
SENT_MESSAGE = 'sentMessage',
|
SENT_MESSAGE = 'sentMessage',
|
||||||
CHAT_FOCUS_MESSAGE_REQUEST = 'ChatFocusMessageRequest',
|
CHAT_FOCUS_MESSAGE_REQUEST = 'ChatFocusMessageRequest',
|
||||||
|
CHAT_KEYBOARD_FOCUS_MESSAGE_REQUEST = 'ChatKeyboardFocusMessageRequest',
|
||||||
|
CHAT_KEYBOARD_FOCUS_MESSAGE_CANCEL = 'ChatKeyboardFocusMessageCancel',
|
||||||
CHAT_REPLY_INTENTION = 'ChatReplyIntention',
|
CHAT_REPLY_INTENTION = 'ChatReplyIntention',
|
||||||
|
CHAT_CANCEL_REPLY_INTENTION = 'ChatCancelReplyIntention',
|
||||||
CHAT_EDIT_REQUEST = 'ChatEditRequest',
|
CHAT_EDIT_REQUEST = 'ChatEditRequest',
|
||||||
|
CHAT_CANCEL_EDIT_REQUEST = 'ChatCancelEditRequest',
|
||||||
CHAT_DELETE_REQUEST = 'ChatDeleteRequest',
|
CHAT_DELETE_REQUEST = 'ChatDeleteRequest',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,14 @@ const lgPaddingY = '0.6rem';
|
|||||||
const jumboPaddingY = '1.5rem';
|
const jumboPaddingY = '1.5rem';
|
||||||
const jumboPaddingX = '3.025rem';
|
const jumboPaddingX = '3.025rem';
|
||||||
|
|
||||||
|
const xsPadding = '0.125rem';
|
||||||
|
const smPadding = '0.25rem';
|
||||||
|
const mdPadding = '0.375rem';
|
||||||
|
const lgPadding = '0.5rem';
|
||||||
|
const xlPadding = '0.75rem';
|
||||||
|
const $2xlPadding = '1rem';
|
||||||
|
const $3xlPadding = '1.25rem';
|
||||||
|
|
||||||
const whiteboardToolbarPadding = '.5rem';
|
const whiteboardToolbarPadding = '.5rem';
|
||||||
const whiteboardToolbarMargin = '.5rem';
|
const whiteboardToolbarMargin = '.5rem';
|
||||||
const whiteboardToolbarPaddingSm = '.3rem';
|
const whiteboardToolbarPaddingSm = '.3rem';
|
||||||
@ -170,4 +178,11 @@ export {
|
|||||||
presentationMenuHeight,
|
presentationMenuHeight,
|
||||||
styleMenuOffset,
|
styleMenuOffset,
|
||||||
styleMenuOffsetSmall,
|
styleMenuOffsetSmall,
|
||||||
|
lgPadding,
|
||||||
|
mdPadding,
|
||||||
|
smPadding,
|
||||||
|
$2xlPadding,
|
||||||
|
$3xlPadding,
|
||||||
|
xlPadding,
|
||||||
|
xsPadding,
|
||||||
};
|
};
|
||||||
|
@ -12,6 +12,7 @@ const colorGrayLightest = 'var(--color-gray-lightest, #D4D9DF)';
|
|||||||
const colorBlueLight = 'var(--color-blue-light, #54a1f3)';
|
const colorBlueLight = 'var(--color-blue-light, #54a1f3)';
|
||||||
const colorBlueLighter = 'var(--color-blue-lighter, #92BCEA)';
|
const colorBlueLighter = 'var(--color-blue-lighter, #92BCEA)';
|
||||||
const colorBlueLightest = 'var(--color-blue-lightest, #E4ECF2)';
|
const colorBlueLightest = 'var(--color-blue-lightest, #E4ECF2)';
|
||||||
|
const colorBlueLightestChannel = '228 236 242';
|
||||||
|
|
||||||
const colorTransparent = 'var(--color-transparent, #ff000000)';
|
const colorTransparent = 'var(--color-transparent, #ff000000)';
|
||||||
|
|
||||||
@ -135,6 +136,7 @@ export {
|
|||||||
colorBlueLight,
|
colorBlueLight,
|
||||||
colorBlueLighter,
|
colorBlueLighter,
|
||||||
colorBlueLightest,
|
colorBlueLightest,
|
||||||
|
colorBlueLightestChannel,
|
||||||
colorPrimary,
|
colorPrimary,
|
||||||
colorDanger,
|
colorDanger,
|
||||||
colorDangerDark,
|
colorDangerDark,
|
||||||
|
63
bigbluebutton-html5/package-lock.json
generated
63
bigbluebutton-html5/package-lock.json
generated
@ -20,6 +20,7 @@
|
|||||||
"@emotion/styled": "^11.10.8",
|
"@emotion/styled": "^11.10.8",
|
||||||
"@jitsi/sdp-interop": "0.1.14",
|
"@jitsi/sdp-interop": "0.1.14",
|
||||||
"@mconf/bbb-diff": "^1.2.0",
|
"@mconf/bbb-diff": "^1.2.0",
|
||||||
|
"@mui/base": "^5.0.0-beta.58",
|
||||||
"@mui/material": "^5.12.2",
|
"@mui/material": "^5.12.2",
|
||||||
"@mui/system": "^5.12.3",
|
"@mui/system": "^5.12.3",
|
||||||
"@types/node": "^20.5.7",
|
"@types/node": "^20.5.7",
|
||||||
@ -2796,6 +2797,68 @@
|
|||||||
"diff": "^5.0.0"
|
"diff": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mui/base": {
|
||||||
|
"version": "5.0.0-beta.58",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.58.tgz",
|
||||||
|
"integrity": "sha512-P0E7ZrxOuyYqBvVv9w8k7wm+Xzx/KRu+BGgFcR2htTsGCpJNQJCSUXNUZ50MUmSU9hzqhwbQWNXhV1MBTl6F7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.25.0",
|
||||||
|
"@floating-ui/react-dom": "^2.1.1",
|
||||||
|
"@mui/types": "^7.2.15",
|
||||||
|
"@mui/utils": "6.0.0-rc.0",
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@mui/base/node_modules/@mui/utils": {
|
||||||
|
"version": "6.0.0-rc.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.0.0-rc.0.tgz",
|
||||||
|
"integrity": "sha512-tBp0ILEXDL0bbDDT8PnZOjCqSm5Dfk2N0Z45uzRw+wVl6fVvloC9zw8avl+OdX1Bg3ubs/ttKn8nRNv17bpM5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.25.0",
|
||||||
|
"@mui/types": "^7.2.15",
|
||||||
|
"@types/prop-types": "^15.7.12",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-is": "^18.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mui/core-downloads-tracker": {
|
"node_modules/@mui/core-downloads-tracker": {
|
||||||
"version": "5.16.6",
|
"version": "5.16.6",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.6.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.6.tgz",
|
||||||
|
@ -49,6 +49,7 @@
|
|||||||
"@emotion/styled": "^11.10.8",
|
"@emotion/styled": "^11.10.8",
|
||||||
"@jitsi/sdp-interop": "0.1.14",
|
"@jitsi/sdp-interop": "0.1.14",
|
||||||
"@mconf/bbb-diff": "^1.2.0",
|
"@mconf/bbb-diff": "^1.2.0",
|
||||||
|
"@mui/base": "^5.0.0-beta.58",
|
||||||
"@mui/material": "^5.12.2",
|
"@mui/material": "^5.12.2",
|
||||||
"@mui/system": "^5.12.3",
|
"@mui/system": "^5.12.3",
|
||||||
"@types/node": "^20.5.7",
|
"@types/node": "^20.5.7",
|
||||||
|
@ -29,8 +29,8 @@
|
|||||||
"app.chat.breakoutDurationUpdated": "Breakout time is now {0} minutes",
|
"app.chat.breakoutDurationUpdated": "Breakout time is now {0} minutes",
|
||||||
"app.chat.breakoutDurationUpdatedModerator": "Breakout rooms time is now {0} minutes, and a notification has been sent.",
|
"app.chat.breakoutDurationUpdatedModerator": "Breakout rooms time is now {0} minutes, and a notification has been sent.",
|
||||||
"app.chat.emptyLogLabel": "Chat log empty",
|
"app.chat.emptyLogLabel": "Chat log empty",
|
||||||
"app.chat.away": "is away",
|
"app.chat.away": "{user} is away",
|
||||||
"app.chat.notAway": "is back",
|
"app.chat.notAway": "{user} is back online",
|
||||||
"app.chat.clearPublicChatMessage": "The public chat history was cleared by a moderator",
|
"app.chat.clearPublicChatMessage": "The public chat history was cleared by a moderator",
|
||||||
"app.chat.multi.typing": "Multiple users are typing",
|
"app.chat.multi.typing": "Multiple users are typing",
|
||||||
"app.chat.someone.typing": "Someone is typing",
|
"app.chat.someone.typing": "Someone is typing",
|
||||||
@ -49,6 +49,12 @@
|
|||||||
"app.chat.toolbar.reactions.youLabel": "you",
|
"app.chat.toolbar.reactions.youLabel": "you",
|
||||||
"app.chat.toolbar.reactions.andLabel": "and",
|
"app.chat.toolbar.reactions.andLabel": "and",
|
||||||
"app.chat.toolbar.reactions.findReactionButtonLabel": "Find a reaction",
|
"app.chat.toolbar.reactions.findReactionButtonLabel": "Find a reaction",
|
||||||
|
"app.chat.toolbar.edit.editing": "Editing message",
|
||||||
|
"app.chat.toolbar.edit.cancel": "Press {key} to cancel.",
|
||||||
|
"app.chat.toolbar.edit.edited": "Edited",
|
||||||
|
"app.chat.toolbar.delete.cancelLabel": "Cancel",
|
||||||
|
"app.chat.toolbar.delete.confirmationTitle": "Are you sure?",
|
||||||
|
"app.chat.toolbar.delete.confirmationDescription": "This action is permanent, you will not be able to access this message again.",
|
||||||
"app.timer.toolTipTimerStopped": "The timer has stopped.",
|
"app.timer.toolTipTimerStopped": "The timer has stopped.",
|
||||||
"app.timer.toolTipTimerRunning": "The timer is running.",
|
"app.timer.toolTipTimerRunning": "The timer is running.",
|
||||||
"app.timer.toolTipStopwatchStopped": "The stopwatch has stopped.",
|
"app.timer.toolTipStopwatchStopped": "The stopwatch has stopped.",
|
||||||
|
Loading…
Reference in New Issue
Block a user