feat(chat): message replies
This commit is contained in:
parent
4105271660
commit
a4798695b4
@ -526,6 +526,7 @@ export interface Chat {
|
||||
emojiPicker: EmojiPicker
|
||||
disableEmojis: string[]
|
||||
allowedElements: string[]
|
||||
toolbar: string[]
|
||||
}
|
||||
|
||||
export interface SystemMessagesKeys {
|
||||
|
@ -15,4 +15,13 @@ export interface Message {
|
||||
messageMetadata: string;
|
||||
recipientHasSeen: string;
|
||||
user: User;
|
||||
messageSequence: number;
|
||||
replyToMessage: {
|
||||
messageSequence: number;
|
||||
message: string;
|
||||
user: {
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
} | null;
|
||||
}
|
||||
|
@ -134,6 +134,7 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
||||
const emojiPickerRef = useRef<HTMLDivElement>(null);
|
||||
const emojiPickerButtonRef = useRef(null);
|
||||
const [isTextAreaFocused, setIsTextAreaFocused] = React.useState(false);
|
||||
const [repliedMessageId, setRepliedMessageId] = React.useState<string>();
|
||||
const textAreaRef: RefObject<TextareaAutosize> = useRef<TextareaAutosize>(null);
|
||||
const { isMobile } = deviceInfo;
|
||||
const prevChatId = usePreviousValue(chatId);
|
||||
@ -282,6 +283,21 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
||||
}));
|
||||
}, [message]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
if (e instanceof CustomEvent) {
|
||||
setRepliedMessageId(e.detail.messageId);
|
||||
textAreaRef.current?.textarea.focus();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(ChatEvents.CHAT_REPLY_INTENTION, handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(ChatEvents.CHAT_REPLY_INTENTION, handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderForm = () => {
|
||||
const formRef = useRef<HTMLFormElement | null>(null);
|
||||
|
||||
@ -303,8 +319,20 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
||||
variables: {
|
||||
chatMessageInMarkdownFormat: msg,
|
||||
chatId: chatId === PUBLIC_CHAT_ID ? PUBLIC_GROUP_CHAT_ID : chatId,
|
||||
replyToMessageId: repliedMessageId,
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
|
@ -2,10 +2,11 @@ import { gql } from '@apollo/client';
|
||||
|
||||
// Define the GraphQL mutation
|
||||
export const CHAT_SEND_MESSAGE = gql`
|
||||
mutation ChatSendMessage($chatId: String!, $chatMessageInMarkdownFormat: String!) {
|
||||
mutation ChatSendMessage($chatId: String!, $chatMessageInMarkdownFormat: String!, $replyToMessageId: String) {
|
||||
chatSendMessage(
|
||||
chatId: $chatId,
|
||||
chatMessageInMarkdownFormat: $chatMessageInMarkdownFormat
|
||||
chatMessageInMarkdownFormat: $chatMessageInMarkdownFormat,
|
||||
replyToMessageId: $replyToMessageId
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
@ -21,9 +21,7 @@ import LAST_SEEN_MUTATION from './queries';
|
||||
import {
|
||||
ButtonLoadMore,
|
||||
MessageList,
|
||||
MessageListWrapper,
|
||||
UnreadButton,
|
||||
ChatMessages,
|
||||
} from './styles';
|
||||
import useReactiveRef from '/imports/ui/hooks/useReactiveRef';
|
||||
import useStickyScroll from '/imports/ui/hooks/useStickyScroll';
|
||||
@ -113,10 +111,13 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
isRTL,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const contentRef = React.useRef<HTMLDivElement>(null);
|
||||
// I used a ref here because I don't want to re-render the component when the last sender changes
|
||||
const lastSenderPerPage = React.useRef<Map<number, string>>(new Map());
|
||||
const sentinelRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const {
|
||||
ref: messageListContainerRef,
|
||||
current: currentMessageListContainer,
|
||||
} = useReactiveRef<HTMLDivElement>(null);
|
||||
const {
|
||||
ref: messageListRef,
|
||||
current: currentMessageList,
|
||||
@ -127,12 +128,12 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
const {
|
||||
childRefProxy: sentinelRefProxy,
|
||||
intersecting: isSentinelVisible,
|
||||
parentRefProxy: messageListRefProxy,
|
||||
} = useIntersectionObserver(messageListRef, sentinelRef);
|
||||
parentRefProxy: messageListContainerRefProxy,
|
||||
} = useIntersectionObserver(messageListContainerRef, sentinelRef);
|
||||
const {
|
||||
startObserving,
|
||||
stopObserving,
|
||||
} = useStickyScroll(currentMessageList);
|
||||
} = useStickyScroll(currentMessageListContainer, currentMessageList);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSentinelVisible) startObserving(); else stopObserving();
|
||||
@ -159,6 +160,21 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
}
|
||||
}, [lastMessageCreatedAt]);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
if (e instanceof CustomEvent) {
|
||||
toggleFollowingTail(false);
|
||||
setUserLoadedBackUntilPage(Math.ceil(e.detail.sequence / PAGE_SIZE) - 1);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const markMessageAsSeen = useCallback((message: Message) => {
|
||||
if (new Date(message.createdAt).getTime() > new Date((lastMessageCreatedAt || 0)).getTime()) {
|
||||
dispatchLastSeen();
|
||||
@ -179,14 +195,14 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
|
||||
const toggleFollowingTail = (toggle: boolean) => {
|
||||
setFollowingTail(toggle);
|
||||
if (isElement(contentRef.current)) {
|
||||
if (isElement(messageListRef.current)) {
|
||||
if (toggle) {
|
||||
scrollObserver.observe(contentRef.current);
|
||||
scrollObserver.observe(messageListRef.current);
|
||||
} else {
|
||||
if (userLoadedBackUntilPage === null) {
|
||||
setUserLoadedBackUntilPage(Math.max(totalPages - 2, 0));
|
||||
}
|
||||
scrollObserver.unobserve(contentRef.current);
|
||||
scrollObserver.unobserve(messageListRef.current);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -246,67 +262,56 @@ const ChatMessageList: React.FC<ChatListProps> = ({
|
||||
<>
|
||||
{
|
||||
[
|
||||
<MessageListWrapper key="message-list-wrapper" id="chat-list">
|
||||
<MessageList
|
||||
ref={messageListRefProxy}
|
||||
onMouseUp={() => {
|
||||
setScrollToTailEventHandler();
|
||||
<MessageList
|
||||
id="chat-list"
|
||||
key="message-list-wrapper"
|
||||
onMouseUp={() => {
|
||||
setScrollToTailEventHandler();
|
||||
}}
|
||||
onTouchEnd={() => {
|
||||
setScrollToTailEventHandler();
|
||||
}}
|
||||
data-test="chatMessages"
|
||||
isRTL={isRTL}
|
||||
ref={messageListContainerRefProxy}
|
||||
>
|
||||
<div ref={messageListRef}>
|
||||
{userLoadedBackUntilPage ? (
|
||||
<ButtonLoadMore
|
||||
onClick={() => {
|
||||
if (followingTail) {
|
||||
toggleFollowingTail(false);
|
||||
}
|
||||
setUserLoadedBackUntilPage(userLoadedBackUntilPage - 1);
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.loadMoreButtonLabel)}
|
||||
</ButtonLoadMore>
|
||||
) : null}
|
||||
<ChatPopupContainer />
|
||||
{Array.from({ length: pagesToLoad }, (_v, k) => k + (firstPageToLoad)).map((page) => {
|
||||
return (
|
||||
<ChatListPage
|
||||
key={`page-${page}`}
|
||||
page={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
setLastSender={() => setLastSender(lastSenderPerPage.current)}
|
||||
lastSenderPreviousPage={page ? lastSenderPerPage.current.get(page - 1) : undefined}
|
||||
chatId={chatId}
|
||||
markMessageAsSeen={markMessageAsSeen}
|
||||
scrollRef={messageListContainerRefProxy}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
ref={sentinelRefProxy}
|
||||
style={{
|
||||
height: 1,
|
||||
background: 'none',
|
||||
}}
|
||||
onTouchEnd={() => {
|
||||
setScrollToTailEventHandler();
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{
|
||||
(userLoadedBackUntilPage)
|
||||
? (
|
||||
<ButtonLoadMore
|
||||
onClick={() => {
|
||||
if (followingTail) {
|
||||
toggleFollowingTail(false);
|
||||
}
|
||||
setUserLoadedBackUntilPage(userLoadedBackUntilPage - 1);
|
||||
}}
|
||||
>
|
||||
{intl.formatMessage(intlMessages.loadMoreButtonLabel)}
|
||||
</ButtonLoadMore>
|
||||
) : null
|
||||
}
|
||||
</span>
|
||||
<ChatMessages
|
||||
id="contentRef"
|
||||
ref={contentRef}
|
||||
data-test="chatMessages"
|
||||
isRTL={isRTL}
|
||||
>
|
||||
<ChatPopupContainer />
|
||||
{
|
||||
// @ts-ignore
|
||||
Array.from({ length: pagesToLoad }, (v, k) => k + (firstPageToLoad)).map((page) => {
|
||||
return (
|
||||
<ChatListPage
|
||||
key={`page-${page}`}
|
||||
page={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
setLastSender={() => setLastSender(lastSenderPerPage.current)}
|
||||
lastSenderPreviousPage={page ? lastSenderPerPage.current.get(page - 1) : undefined}
|
||||
chatId={chatId}
|
||||
markMessageAsSeen={markMessageAsSeen}
|
||||
scrollRef={messageListRef}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ChatMessages>
|
||||
<div
|
||||
ref={sentinelRefProxy}
|
||||
style={{
|
||||
height: 1,
|
||||
background: 'none',
|
||||
}}
|
||||
/>
|
||||
</MessageList>
|
||||
</MessageListWrapper>,
|
||||
/>
|
||||
</MessageList>,
|
||||
renderUnreadNotification,
|
||||
]
|
||||
}
|
||||
|
@ -11,9 +11,14 @@ import {
|
||||
ChatContent,
|
||||
ChatAvatar,
|
||||
MessageItemWrapper,
|
||||
Container,
|
||||
} from './styles';
|
||||
import { ChatMessageType } from '/imports/ui/core/enums/chat';
|
||||
import { ChatEvents, ChatMessageType } from '/imports/ui/core/enums/chat';
|
||||
import MessageReadConfirmation from './message-read-confirmation/component';
|
||||
import ChatMessageToolbar from './message-toolbar/component';
|
||||
import ChatMessageReactions from './message-reactions/component';
|
||||
import ChatMessageReplied from './message-replied/component';
|
||||
import { STORAGES, useStorageKey } from '/imports/ui/services/storage/hooks';
|
||||
|
||||
interface ChatMessageProps {
|
||||
message: Message;
|
||||
@ -58,6 +63,8 @@ function isInViewport(el: HTMLDivElement) {
|
||||
|
||||
const messageRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
const ANIMATION_DURATION = 2000;
|
||||
|
||||
const ChatMesssage: React.FC<ChatMessageProps> = ({
|
||||
previousMessage,
|
||||
lastSenderPreviousPage,
|
||||
@ -74,6 +81,52 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
||||
}
|
||||
}, [message, messageRef]);
|
||||
const messageContentRef = React.createRef<HTMLDivElement>();
|
||||
const [reactions, setReactions] = React.useState<{ id: string, native: string }[]>([]);
|
||||
const chatFocusMessageRequest = useStorageKey(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, STORAGES.IN_MEMORY);
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const animationInitialTimestamp = React.useRef(0);
|
||||
|
||||
const startScrollAnimation = (timestamp: number) => {
|
||||
if (scrollRef.current && containerRef.current) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
scrollRef.current.scrollTop = containerRef.current.offsetTop;
|
||||
}
|
||||
animationInitialTimestamp.current = timestamp;
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
if (e instanceof CustomEvent) {
|
||||
if (e.detail.sequence === message.messageSequence) {
|
||||
requestAnimationFrame(startScrollAnimation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatFocusMessageRequest === message.messageSequence) {
|
||||
requestAnimationFrame(startScrollAnimation);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const animate = useCallback((timestamp: number) => {
|
||||
if (!containerRef.current) return;
|
||||
const value = (timestamp - animationInitialTimestamp.current) / ANIMATION_DURATION;
|
||||
if (value < 1) {
|
||||
containerRef.current.style.backgroundColor = `rgba(243, 246, 249, ${1 - value})`;
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
containerRef.current.style.backgroundColor = 'unset';
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setMessagesRequestedFromPlugin((messages) => {
|
||||
@ -250,38 +303,64 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<ChatWrapper
|
||||
isSystemSender={isSystemSender}
|
||||
sameSender={sameSender}
|
||||
ref={messageRef}
|
||||
isPresentationUpload={messageContent.isPresentationUpload}
|
||||
isCustomPluginMessage={isCustomPluginMessage}
|
||||
>
|
||||
{((!message?.user || !sameSender) && (
|
||||
message.messageType !== ChatMessageType.USER_AWAY_STATUS_MSG
|
||||
<Container ref={containerRef}>
|
||||
{message.replyToMessage && (
|
||||
<ChatMessageReplied
|
||||
message={message.replyToMessage.message}
|
||||
username={message.replyToMessage.user.name}
|
||||
sequence={message.replyToMessage.messageSequence}
|
||||
userColor={message.replyToMessage.user.color}
|
||||
/>
|
||||
)}
|
||||
<ChatWrapper
|
||||
id="chat-message-wrapper"
|
||||
isSystemSender={isSystemSender}
|
||||
sameSender={sameSender}
|
||||
ref={messageRef}
|
||||
isPresentationUpload={messageContent.isPresentationUpload}
|
||||
isCustomPluginMessage={isCustomPluginMessage}
|
||||
>
|
||||
<ChatMessageToolbar
|
||||
messageId={message.messageId}
|
||||
chatId={message.chatId}
|
||||
username={message.user.name}
|
||||
message={message.message}
|
||||
messageSequence={message.messageSequence}
|
||||
onEmojiSelected={(emoji) => {
|
||||
setReactions((prev) => {
|
||||
return [
|
||||
...prev,
|
||||
emoji,
|
||||
];
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<ChatMessageReactions reactions={reactions} />
|
||||
{((!message?.user || !sameSender) && (
|
||||
message.messageType !== ChatMessageType.USER_AWAY_STATUS_MSG
|
||||
&& message.messageType !== ChatMessageType.API
|
||||
&& message.messageType !== ChatMessageType.CHAT_CLEAR
|
||||
&& !isCustomPluginMessage)
|
||||
) && (
|
||||
<ChatAvatar
|
||||
avatar={message.user?.avatar}
|
||||
color={messageContent.color}
|
||||
moderator={messageContent.isModerator}
|
||||
>
|
||||
{!messageContent.avatarIcon ? (
|
||||
!message.user || (message.user?.avatar.length === 0 ? messageContent.name.toLowerCase().slice(0, 2) : '')
|
||||
) : (
|
||||
<i className={messageContent.avatarIcon} />
|
||||
) && (
|
||||
<ChatAvatar
|
||||
avatar={message.user?.avatar}
|
||||
color={messageContent.color}
|
||||
moderator={messageContent.isModerator}
|
||||
>
|
||||
{!messageContent.avatarIcon ? (
|
||||
!message.user || (message.user?.avatar.length === 0 ? messageContent.name.toLowerCase().slice(0, 2) : '')
|
||||
) : (
|
||||
<i className={messageContent.avatarIcon} />
|
||||
)}
|
||||
</ChatAvatar>
|
||||
)}
|
||||
</ChatAvatar>
|
||||
)}
|
||||
<ChatContent
|
||||
ref={messageContentRef}
|
||||
sameSender={message?.user ? sameSender : false}
|
||||
isCustomPluginMessage={isCustomPluginMessage}
|
||||
data-chat-message-id={message?.messageId}
|
||||
>
|
||||
{message.messageType !== ChatMessageType.CHAT_CLEAR
|
||||
<ChatContent
|
||||
ref={messageContentRef}
|
||||
sameSender={message?.user ? sameSender : false}
|
||||
isCustomPluginMessage={isCustomPluginMessage}
|
||||
data-chat-message-id={message?.messageId}
|
||||
>
|
||||
{message.messageType !== ChatMessageType.CHAT_CLEAR
|
||||
&& !isCustomPluginMessage
|
||||
&& (
|
||||
<ChatMessageHeader
|
||||
@ -291,16 +370,17 @@ const ChatMesssage: React.FC<ChatMessageProps> = ({
|
||||
dateTime={dateTime}
|
||||
/>
|
||||
)}
|
||||
<MessageItemWrapper>
|
||||
{messageContent.component}
|
||||
{messageReadFeedbackEnabled && (
|
||||
<MessageItemWrapper>
|
||||
{messageContent.component}
|
||||
{messageReadFeedbackEnabled && (
|
||||
<MessageReadConfirmation
|
||||
message={message}
|
||||
/>
|
||||
)}
|
||||
</MessageItemWrapper>
|
||||
</ChatContent>
|
||||
</ChatWrapper>
|
||||
)}
|
||||
</MessageItemWrapper>
|
||||
</ChatContent>
|
||||
</ChatWrapper>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import Styled from './styles';
|
||||
|
||||
interface ChatMessageReactionsProps {
|
||||
reactions: {
|
||||
id: string;
|
||||
native: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
const ChatMessageReactions: React.FC<ChatMessageReactionsProps> = (props) => {
|
||||
const { reactions } = props;
|
||||
|
||||
return (
|
||||
<Styled.ReactionsWrapper>
|
||||
{reactions.map((emoji) => {
|
||||
return (
|
||||
<Styled.EmojiWrapper highlighted={false}>
|
||||
<em-emoji
|
||||
emoji={emoji}
|
||||
size={parseFloat(
|
||||
window.getComputedStyle(document.documentElement).fontSize,
|
||||
)}
|
||||
native={emoji.native}
|
||||
/>
|
||||
</Styled.EmojiWrapper>
|
||||
);
|
||||
})}
|
||||
</Styled.ReactionsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatMessageReactions;
|
@ -0,0 +1,35 @@
|
||||
import styled from 'styled-components';
|
||||
import { colorBlueLighter, colorBlueLightest, colorGray } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
|
||||
const EmojiWrapper = styled.div<{ highlighted: boolean }>`
|
||||
background-color: ${colorBlueLightest};
|
||||
border-radius: 10px;
|
||||
margin-left: 3px;
|
||||
margin-top: 3px;
|
||||
padding: 3px;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
${({ highlighted }) => highlighted && `
|
||||
background-color: ${colorBlueLighter};
|
||||
`}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid ${colorGray};
|
||||
}
|
||||
`;
|
||||
|
||||
const ReactionsWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`;
|
||||
|
||||
export default {
|
||||
EmojiWrapper,
|
||||
ReactionsWrapper,
|
||||
};
|
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import Styled from './styles';
|
||||
import Storage from '/imports/ui/services/storage/in-memory';
|
||||
import { ChatEvents } from '/imports/ui/core/enums/chat';
|
||||
|
||||
interface MessageRepliedProps {
|
||||
username: string;
|
||||
message: string;
|
||||
sequence: number;
|
||||
userColor: string;
|
||||
}
|
||||
|
||||
const ChatMessageReplied: React.FC<MessageRepliedProps> = (props) => {
|
||||
const {
|
||||
message, username, sequence, userColor,
|
||||
} = props;
|
||||
return (
|
||||
<Styled.Container
|
||||
$userColor={userColor}
|
||||
onClick={() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, {
|
||||
detail: {
|
||||
sequence,
|
||||
},
|
||||
}),
|
||||
);
|
||||
Storage.removeItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST);
|
||||
Storage.setItem(ChatEvents.CHAT_FOCUS_MESSAGE_REQUEST, sequence);
|
||||
}}
|
||||
>
|
||||
<Styled.Username $userColor={userColor}>{username}</Styled.Username>
|
||||
<Styled.Message>{message}</Styled.Message>
|
||||
</Styled.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatMessageReplied;
|
@ -0,0 +1,35 @@
|
||||
import styled from 'styled-components';
|
||||
import { colorOffWhite } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
|
||||
const Container = styled.div<{ $userColor: string }>`
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid ${({ $userColor }) => $userColor};
|
||||
background-color: ${colorOffWhite};
|
||||
padding: 6px;
|
||||
position: relative;
|
||||
margin: 0.25rem 0 0.25rem 2.6rem;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const Typography = styled.div`
|
||||
line-height: 1rem;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Username = styled(Typography)<{ $userColor: string }>`
|
||||
font-weight: bold;
|
||||
color: ${({ $userColor }) => $userColor};
|
||||
margin-bottom: 6px;
|
||||
`;
|
||||
|
||||
const Message = styled(Typography)``;
|
||||
|
||||
export default {
|
||||
Container,
|
||||
Username,
|
||||
Message,
|
||||
};
|
@ -0,0 +1,176 @@
|
||||
import React from 'react';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import { layoutSelect } from '/imports/ui/components/layout/context';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import { Layout } from '/imports/ui/components/layout/layoutTypes';
|
||||
import { ChatEvents } from '/imports/ui/core/enums/chat';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import {
|
||||
Container,
|
||||
EmojiPicker,
|
||||
EmojiPickerWrapper,
|
||||
EmojiButton,
|
||||
} from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
reply: {
|
||||
id: 'app.chat.toolbar.reply',
|
||||
description: 'reply label',
|
||||
},
|
||||
});
|
||||
|
||||
interface ChatMessageToolbarProps {
|
||||
messageId: string;
|
||||
chatId: string;
|
||||
username: string;
|
||||
message: string;
|
||||
messageSequence: number;
|
||||
onEmojiSelected(emoji: { id: string; native: string }): void;
|
||||
}
|
||||
|
||||
const ChatMessageToolbar: React.FC<ChatMessageToolbarProps> = (props) => {
|
||||
const {
|
||||
messageId, chatId, message, username, onEmojiSelected, messageSequence,
|
||||
} = props;
|
||||
const [reactionsAnchor, setReactionsAnchor] = React.useState<Element | null>(
|
||||
null,
|
||||
);
|
||||
const intl = useIntl();
|
||||
const isRTL = layoutSelect((i: Layout) => i.isRTL);
|
||||
const CHAT_TOOLBAR_CONFIG = window.meetingClientSettings.public.chat.toolbar;
|
||||
const CHAT_REPLIES_ENABLED = CHAT_TOOLBAR_CONFIG.includes('replies');
|
||||
const CHAT_REACTIONS_ENABLED = CHAT_TOOLBAR_CONFIG.includes('reactions');
|
||||
const CHAT_EDIT_ENABLED = CHAT_TOOLBAR_CONFIG.includes('edit');
|
||||
const CHAT_DELETE_ENABLED = CHAT_TOOLBAR_CONFIG.includes('delete');
|
||||
const actions = [];
|
||||
|
||||
if (CHAT_EDIT_ENABLED) {
|
||||
actions.push({
|
||||
key: 'edit',
|
||||
icon: 'pen_tool',
|
||||
label: 'Edit',
|
||||
onClick: () => null,
|
||||
});
|
||||
}
|
||||
|
||||
if (CHAT_DELETE_ENABLED) {
|
||||
actions.push({
|
||||
key: 'delete',
|
||||
icon: 'delete',
|
||||
label: 'Delete',
|
||||
onClick: () => null,
|
||||
});
|
||||
}
|
||||
|
||||
if (!CHAT_TOOLBAR_CONFIG.length) return null;
|
||||
|
||||
return (
|
||||
<Container className="chat-message-toolbar" $sequence={messageSequence}>
|
||||
{CHAT_REPLIES_ENABLED && (
|
||||
<>
|
||||
<Button
|
||||
circle
|
||||
ghost
|
||||
aria-describedby={`chat-reply-btn-label-${messageSequence}`}
|
||||
icon="undo"
|
||||
color="light"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(ChatEvents.CHAT_REPLY_INTENTION, {
|
||||
detail: {
|
||||
username,
|
||||
message,
|
||||
messageId,
|
||||
chatId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span id={`chat-reply-btn-label-${messageSequence}`} className="sr-only">
|
||||
{intl.formatMessage(intlMessages.reply, { 0: messageSequence })}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{CHAT_REACTIONS_ENABLED && (
|
||||
<EmojiButton
|
||||
onClick={(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
setReactionsAnchor(e.currentTarget);
|
||||
}}
|
||||
size="sm"
|
||||
icon="happy"
|
||||
color="light"
|
||||
ghost
|
||||
type="button"
|
||||
circle
|
||||
data-test="emojiPickerButton"
|
||||
/>
|
||||
)}
|
||||
|
||||
{actions.length > 0 && (
|
||||
<BBBMenu
|
||||
trigger={(
|
||||
<Button
|
||||
onClick={() => null}
|
||||
size="sm"
|
||||
icon="more"
|
||||
color="light"
|
||||
ghost
|
||||
type="button"
|
||||
circle
|
||||
/>
|
||||
)}
|
||||
actions={actions}
|
||||
opts={{
|
||||
id: 'app-settings-dropdown-menu',
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
getcontentanchorel: null,
|
||||
fullwidth: 'true',
|
||||
anchorOrigin: {
|
||||
vertical: 'bottom',
|
||||
horizontal: isRTL ? 'left' : 'right',
|
||||
},
|
||||
transformorigin: {
|
||||
vertical: 'top',
|
||||
horizontal: isRTL ? 'left' : 'right',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Popover
|
||||
open={Boolean(reactionsAnchor)}
|
||||
anchorEl={reactionsAnchor}
|
||||
onClose={() => {
|
||||
setReactionsAnchor(null);
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: isRTL ? 'left' : 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: isRTL ? 'right' : 'left',
|
||||
}}
|
||||
>
|
||||
<EmojiPickerWrapper>
|
||||
<EmojiPicker
|
||||
onEmojiSelect={(emojiObject: { id: string; native: string }) => {
|
||||
onEmojiSelected(emojiObject);
|
||||
}}
|
||||
showPreview={false}
|
||||
showSkinTones={false}
|
||||
/>
|
||||
</EmojiPickerWrapper>
|
||||
</Popover>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatMessageToolbar;
|
@ -0,0 +1,94 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
colorGrayLighter,
|
||||
colorOffWhite,
|
||||
colorWhite,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { borderRadius, smPaddingX } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import EmojiPickerComponent from '/imports/ui/components/emoji-picker/component';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
|
||||
const Container = styled.div<{ $sequence: number }>`
|
||||
height: calc(1.5rem + 12px);
|
||||
line-height: calc(1.5rem + 8px);
|
||||
max-width: 184px;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
border: 1px solid ${colorOffWhite};
|
||||
border-radius: 8px;
|
||||
padding: 1px;
|
||||
background-color: ${colorWhite};
|
||||
|
||||
#chat-message-wrapper:hover & {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
${({ $sequence }) => (($sequence === 1)
|
||||
? css`
|
||||
bottom: 0;
|
||||
transform: translateY(50%);
|
||||
`
|
||||
: css`
|
||||
top: 0;
|
||||
transform: translateY(-50%);
|
||||
`)
|
||||
}
|
||||
`;
|
||||
|
||||
const EmojiPickerWrapper = styled.div`
|
||||
bottom: calc(100% + 0.5rem);
|
||||
left: 0;
|
||||
right: 0;
|
||||
border: 1px solid ${colorGrayLighter};
|
||||
border-radius: ${borderRadius};
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
z-index: 1000;
|
||||
|
||||
.emoji-mart {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.emoji-mart-anchor {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.emoji-mart-emoji {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
.emoji-mart-category-list {
|
||||
span {
|
||||
cursor: pointer !important;
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// @ts-ignore
|
||||
const EmojiButton = styled(Button)`
|
||||
margin:0 0 0 ${smPaddingX};
|
||||
align-self: center;
|
||||
font-size: 0.5rem;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 ${smPaddingX} 0 0;
|
||||
-webkit-transform: scale(-1, 1);
|
||||
-moz-transform: scale(-1, 1);
|
||||
-ms-transform: scale(-1, 1);
|
||||
-o-transform: scale(-1, 1);
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
`;
|
||||
|
||||
const EmojiPicker = styled(EmojiPickerComponent)`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export {
|
||||
Container,
|
||||
EmojiPicker,
|
||||
EmojiPickerWrapper,
|
||||
EmojiButton,
|
||||
};
|
@ -14,6 +14,7 @@ import {
|
||||
colorWhite,
|
||||
userListBg,
|
||||
colorSuccess,
|
||||
colorOffWhite,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
|
||||
import Header from '/imports/ui/components/common/control-header/component';
|
||||
@ -74,6 +75,12 @@ export const ChatWrapper = styled.div<ChatWrapperProps>`
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`}
|
||||
${({ sameSender }) => !sameSender && `
|
||||
&:hover {
|
||||
background-color: ${colorOffWhite};
|
||||
}
|
||||
border-radius: 6px;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const ChatContent = styled.div<ChatContentProps>`
|
||||
@ -85,6 +92,13 @@ export const ChatContent = styled.div<ChatContentProps>`
|
||||
&& !isCustomPluginMessage && `
|
||||
margin-left: 2.6rem;
|
||||
`}
|
||||
|
||||
${({ sameSender }) => sameSender && `
|
||||
&:hover {
|
||||
background-color: ${colorOffWhite};
|
||||
}
|
||||
border-radius: 6px;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const ChatHeader = styled(Header)`
|
||||
@ -112,7 +126,6 @@ export const ChatAvatar = styled.div<ChatAvatarProps>`
|
||||
${({ color }) => css`
|
||||
background-color: ${color};
|
||||
`}
|
||||
}
|
||||
|
||||
&:after,
|
||||
&:before {
|
||||
@ -171,13 +184,18 @@ export const ChatAvatar = styled.div<ChatAvatarProps>`
|
||||
justify-content: center;
|
||||
align-items:center;
|
||||
// ================ content ================
|
||||
|
||||
|
||||
& .react-loading-skeleton {
|
||||
height: 2.25rem;
|
||||
width: 2.25rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const MessageItemWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -63,8 +63,7 @@ const ChatListPage: React.FC<ChatListPageProps> = ({
|
||||
}, [domElementManipulationIdentifiers, messagesRequestedFromPlugin]);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-filename-extension
|
||||
<div key={`messagePage-${page}`} id={`${page}`}>
|
||||
<React.Fragment key={`messagePage-${page}`}>
|
||||
{messages.map((message, index, messagesArray) => {
|
||||
const previousMessage = messagesArray[index - 1];
|
||||
return (
|
||||
@ -82,7 +81,7 @@ const ChatListPage: React.FC<ChatListPageProps> = ({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -12,6 +12,15 @@ export const CHAT_MESSAGE_PUBLIC_SUBSCRIPTION = gql`
|
||||
isModerator
|
||||
color
|
||||
}
|
||||
messageSequence
|
||||
replyToMessage {
|
||||
messageSequence
|
||||
message
|
||||
user {
|
||||
name
|
||||
color
|
||||
}
|
||||
}
|
||||
messageType
|
||||
chatEmphasizedText
|
||||
chatId
|
||||
|
@ -8,22 +8,30 @@ import { ScrollboxVertical } from '/imports/ui/stylesheets/styled-components/scr
|
||||
import { colorGrayDark } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { ButtonElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
|
||||
|
||||
interface ChatMessagesProps {
|
||||
interface MessageListProps {
|
||||
isRTL: boolean;
|
||||
}
|
||||
|
||||
export const MessageListWrapper = styled.div`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
export const MessageList = styled(ScrollboxVertical)<MessageListProps>`
|
||||
flex-flow: column;
|
||||
flex-shrink: 1;
|
||||
position: relative;
|
||||
right: 0 ${mdPaddingX} 0 0;
|
||||
padding-top: 0;
|
||||
outline-style: none;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
user-select: text;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
`;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding-bottom: ${smPaddingX};
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin: 0 0 0 auto;
|
||||
padding: 0 0 0 ${mdPaddingX};
|
||||
}
|
||||
|
||||
export const ChatMessages = styled.div<ChatMessagesProps>`
|
||||
${({ isRTL }) => isRTL && `
|
||||
padding-left: ${smPaddingX};
|
||||
`}
|
||||
@ -31,25 +39,6 @@ export const ChatMessages = styled.div<ChatMessagesProps>`
|
||||
${({ isRTL }) => !isRTL && `
|
||||
padding-right: ${smPaddingX};
|
||||
`}
|
||||
|
||||
padding-bottom: ${smPaddingX};
|
||||
user-select: text;
|
||||
`;
|
||||
|
||||
export const MessageList = styled(ScrollboxVertical)`
|
||||
flex-flow: column;
|
||||
flex-shrink: 1;
|
||||
right: 0 ${mdPaddingX} 0 0;
|
||||
padding-top: 0;
|
||||
outline-style: none;
|
||||
overflow-x: hidden;
|
||||
user-select: none;
|
||||
|
||||
[dir='rtl'] & {
|
||||
margin: 0 0 0 auto;
|
||||
padding: 0 0 0 ${mdPaddingX};
|
||||
}
|
||||
display: block;
|
||||
`;
|
||||
|
||||
export const ButtonLoadMore = styled.button`
|
||||
@ -70,8 +59,6 @@ export const UnreadButton = styled(ButtonElipsis)`
|
||||
`;
|
||||
|
||||
export default {
|
||||
MessageListWrapper,
|
||||
MessageList,
|
||||
UnreadButton,
|
||||
ChatMessages,
|
||||
};
|
||||
|
@ -0,0 +1,49 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Styled from './styles';
|
||||
import useSettings from '/imports/ui/services/settings/hooks/useSettings';
|
||||
import { SETTINGS } from '/imports/ui/services/settings/enums';
|
||||
import { ChatEvents } from '/imports/ui/core/enums/chat';
|
||||
|
||||
const ChatReplyIntention = () => {
|
||||
const [username, setUsername] = useState<string>();
|
||||
const [message, setMessage] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
if (e instanceof CustomEvent) {
|
||||
setUsername(e.detail.username);
|
||||
setMessage(e.detail.message);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(ChatEvents.CHAT_REPLY_INTENTION, handler);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(ChatEvents.CHAT_REPLY_INTENTION, handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const { animations } = useSettings(SETTINGS.APPLICATION) as {
|
||||
animations: boolean;
|
||||
};
|
||||
|
||||
return (
|
||||
<Styled.Container $hidden={!username || !message} $animations={animations}>
|
||||
<Styled.Username>{username}</Styled.Username>
|
||||
<Styled.Message>{message}</Styled.Message>
|
||||
<Styled.CloseBtn
|
||||
onClick={() => {
|
||||
setMessage(undefined);
|
||||
setUsername(undefined);
|
||||
}}
|
||||
icon="close"
|
||||
ghost
|
||||
circle
|
||||
color="light"
|
||||
size="sm"
|
||||
/>
|
||||
</Styled.Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatReplyIntention;
|
@ -0,0 +1,62 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
colorBlueLight,
|
||||
colorOffWhite,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
|
||||
const Container = styled.div<{ $hidden: boolean; $animations: boolean }>`
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid ${colorBlueLight};
|
||||
background-color: ${colorOffWhite};
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
${({ $hidden }) => ($hidden
|
||||
? css`
|
||||
height: 0;
|
||||
`
|
||||
: css`
|
||||
height: 4rem;
|
||||
padding: 6px;
|
||||
margin-right: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
`
|
||||
)}
|
||||
|
||||
${({ $animations }) => $animations
|
||||
&& css`
|
||||
transition-property: height;
|
||||
transition-duration: 0.1s;
|
||||
`}
|
||||
`;
|
||||
|
||||
const Typography = styled.div`
|
||||
line-height: 1rem;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Username = styled(Typography)`
|
||||
font-weight: bold;
|
||||
color: ${colorBlueLight};
|
||||
margin-bottom: 6px;
|
||||
`;
|
||||
|
||||
const Message = styled(Typography)``;
|
||||
|
||||
// @ts-ignore
|
||||
const CloseBtn = styled(Button)`
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
`;
|
||||
|
||||
export default {
|
||||
Container,
|
||||
Username,
|
||||
CloseBtn,
|
||||
Message,
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import ChatHeader from './chat-header/component';
|
||||
import { layoutSelect, layoutSelectInput } from '../../layout/context';
|
||||
import { Input, Layout } from '../../layout/layoutTypes';
|
||||
@ -7,13 +8,13 @@ import ChatMessageListContainer from './chat-message-list/component';
|
||||
import ChatMessageFormContainer from './chat-message-form/component';
|
||||
import ChatTypingIndicatorContainer from './chat-typing-indicator/component';
|
||||
import { PANELS, ACTIONS } from '/imports/ui/components/layout/enums';
|
||||
import { CircularProgress } from '@mui/material';
|
||||
import usePendingChat from '/imports/ui/core/local-states/usePendingChat';
|
||||
import useChat from '/imports/ui/core/hooks/useChat';
|
||||
import { Chat as ChatType } from '/imports/ui/Types/chat';
|
||||
import { layoutDispatch } from '/imports/ui/components/layout/context';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import { GraphqlDataHookSubscriptionResponse } from '/imports/ui/Types/hook';
|
||||
import ChatReplyIntention from './chat-reply-intention/component';
|
||||
|
||||
interface ChatProps {
|
||||
isRTL: boolean;
|
||||
@ -43,6 +44,7 @@ const Chat: React.FC<ChatProps> = ({ isRTL }) => {
|
||||
<Styled.Chat isRTL={isRTL} isChrome={isChrome}>
|
||||
<ChatHeader />
|
||||
<ChatMessageListContainer />
|
||||
<ChatReplyIntention />
|
||||
<ChatMessageFormContainer />
|
||||
<ChatTypingIndicatorContainer />
|
||||
</Styled.Chat>
|
||||
|
@ -1,5 +1,7 @@
|
||||
export const enum ChatEvents {
|
||||
SENT_MESSAGE = 'sentMessage',
|
||||
CHAT_FOCUS_MESSAGE_REQUEST = 'ChatFocusMessageRequest',
|
||||
CHAT_REPLY_INTENTION = 'ChatReplyIntention',
|
||||
}
|
||||
|
||||
export const enum ChatCommands {
|
||||
|
@ -569,6 +569,7 @@ export const meetingClientSettingsInitialValues: MeetingClientSettings = {
|
||||
'p',
|
||||
'strong',
|
||||
],
|
||||
toolbar: ['replies'],
|
||||
},
|
||||
userReaction: {
|
||||
enabled: true,
|
||||
|
@ -7,7 +7,7 @@ interface Handlers {
|
||||
stopObserving(): void;
|
||||
}
|
||||
|
||||
const useStickyScroll = (el: HTMLElement | null) => {
|
||||
const useStickyScroll = (stickyElement: HTMLElement | null, onResizeOf: HTMLElement | null) => {
|
||||
const elHeight = useRef(0);
|
||||
const timeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
const handlers = useRef<Handlers>({
|
||||
@ -22,7 +22,10 @@ const useStickyScroll = (el: HTMLElement | null) => {
|
||||
if (target instanceof HTMLElement) {
|
||||
if (target.offsetHeight > elHeight.current) {
|
||||
elHeight.current = target.offsetHeight;
|
||||
target.scrollTop = target.scrollHeight + target.clientHeight;
|
||||
if (stickyElement) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
stickyElement.scrollTop = stickyElement.scrollHeight + stickyElement.clientHeight;
|
||||
}
|
||||
} else {
|
||||
elHeight.current = 0;
|
||||
}
|
||||
@ -33,17 +36,17 @@ const useStickyScroll = (el: HTMLElement | null) => {
|
||||
);
|
||||
|
||||
handlers.current.startObserving = useCallback(() => {
|
||||
if (!el) return;
|
||||
if (!onResizeOf) return;
|
||||
clearTimeout(timeout.current);
|
||||
observer.observe(el);
|
||||
}, [el]);
|
||||
observer.observe(onResizeOf);
|
||||
}, [onResizeOf]);
|
||||
|
||||
handlers.current.stopObserving = useCallback(() => {
|
||||
if (!el) return;
|
||||
if (!onResizeOf) return;
|
||||
timeout.current = setTimeout(() => {
|
||||
observer.unobserve(el);
|
||||
observer.unobserve(onResizeOf);
|
||||
}, 500);
|
||||
}, [el]);
|
||||
}, [onResizeOf]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
|
@ -53,8 +53,10 @@ const useStorageKey = (key: string, storage?: Storage[keyof Storage]) => {
|
||||
|
||||
export {
|
||||
useStorageKey,
|
||||
STORAGES,
|
||||
};
|
||||
|
||||
export default {
|
||||
useStorageKey,
|
||||
STORAGES,
|
||||
};
|
||||
|
@ -768,6 +768,7 @@ public:
|
||||
# e.g.: disableEmojis: ['grin','laughing']
|
||||
disableEmojis: []
|
||||
allowedElements: ['a', 'code', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'ol', 'ul', 'p', 'strong']
|
||||
toolbar: ['replies']
|
||||
userReaction:
|
||||
enabled: true
|
||||
expire: 30
|
||||
|
@ -39,6 +39,7 @@
|
||||
"app.chat.copyErr": "Copy chat transcript failed",
|
||||
"app.chat.messageRead": "Message read by the recipient",
|
||||
"app.chat.closePopup": "Close",
|
||||
"app.chat.toolbar.reply": "Reply to message {0}",
|
||||
"app.timer.toolTipTimerStopped": "The timer has stopped.",
|
||||
"app.timer.toolTipTimerRunning": "The timer is running.",
|
||||
"app.timer.toolTipStopwatchStopped": "The stopwatch has stopped.",
|
||||
|
Loading…
Reference in New Issue
Block a user