mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 11:22:31 +00:00
Configures what buttons can be visible inside Toolbox and OverflowMenu, based on priority and config overrides, just like web does.
317 lines
10 KiB
TypeScript
317 lines
10 KiB
TypeScript
import { throttle } from 'lodash-es';
|
|
import React, { RefObject } from 'react';
|
|
import { scrollIntoView } from 'seamless-scroll-polyfill';
|
|
|
|
import { MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from '../../constants';
|
|
import AbstractMessageContainer, { IProps } from '../AbstractMessageContainer';
|
|
|
|
import ChatMessageGroup from './ChatMessageGroup';
|
|
import NewMessagesButton from './NewMessagesButton';
|
|
|
|
interface IState {
|
|
|
|
/**
|
|
* Whether or not message container has received new messages.
|
|
*/
|
|
hasNewMessages: boolean;
|
|
|
|
/**
|
|
* Whether or not scroll position is at the bottom of container.
|
|
*/
|
|
isScrolledToBottom: boolean;
|
|
|
|
/**
|
|
* The id of the last read message.
|
|
*/
|
|
lastReadMessageId: string | null;
|
|
}
|
|
|
|
/**
|
|
* Displays all received chat messages, grouped by sender.
|
|
*
|
|
* @augments AbstractMessageContainer
|
|
*/
|
|
export default class MessageContainer extends AbstractMessageContainer<IProps, IState> {
|
|
/**
|
|
* Component state used to decide when the hasNewMessages button to appear
|
|
* and where to scroll when click on hasNewMessages button.
|
|
*/
|
|
state: IState = {
|
|
hasNewMessages: false,
|
|
isScrolledToBottom: true,
|
|
lastReadMessageId: ''
|
|
};
|
|
|
|
/**
|
|
* Reference to the HTML element at the end of the list of displayed chat
|
|
* messages. Used for scrolling to the end of the chat messages.
|
|
*/
|
|
_messagesListEndRef: RefObject<HTMLDivElement>;
|
|
|
|
/**
|
|
* A React ref to the HTML element containing all {@code ChatMessageGroup}
|
|
* instances.
|
|
*/
|
|
_messageListRef: RefObject<HTMLDivElement>;
|
|
|
|
/**
|
|
* Intersection observer used to detect intersections of messages with the bottom of the message container.
|
|
*/
|
|
_bottomListObserver: IntersectionObserver;
|
|
|
|
/**
|
|
* Initializes a new {@code MessageContainer} instance.
|
|
*
|
|
* @param {IProps} props - The React {@code Component} props to initialize
|
|
* the new {@code MessageContainer} instance with.
|
|
*/
|
|
constructor(props: IProps) {
|
|
super(props);
|
|
|
|
this._messageListRef = React.createRef<HTMLDivElement>();
|
|
this._messagesListEndRef = React.createRef<HTMLDivElement>();
|
|
|
|
// Bind event handlers so they are only bound once for every instance.
|
|
this._handleIntersectBottomList = this._handleIntersectBottomList.bind(this);
|
|
this._findFirstUnreadMessage = this._findFirstUnreadMessage.bind(this);
|
|
this._isMessageVisible = this._isMessageVisible.bind(this);
|
|
this._onChatScroll = throttle(this._onChatScroll.bind(this), 300, { leading: true });
|
|
this._onGoToFirstUnreadMessage = this._onGoToFirstUnreadMessage.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Implements {@code Component#render}.
|
|
*
|
|
* @inheritdoc
|
|
*/
|
|
render() {
|
|
const groupedMessages = this._getMessagesGroupedBySender();
|
|
const messages = groupedMessages.map((group, index) => {
|
|
const messageType = group[0]?.messageType;
|
|
|
|
return (
|
|
<ChatMessageGroup
|
|
className = { messageType || MESSAGE_TYPE_REMOTE }
|
|
key = { index }
|
|
messages = { group } />
|
|
);
|
|
});
|
|
|
|
return (
|
|
<div id = 'chat-conversation-container'>
|
|
<div
|
|
aria-labelledby = 'chat-header'
|
|
id = 'chatconversation'
|
|
onScroll = { this._onChatScroll }
|
|
ref = { this._messageListRef }
|
|
role = 'log'
|
|
tabIndex = { 0 }>
|
|
{ messages }
|
|
|
|
{ !this.state.isScrolledToBottom && this.state.hasNewMessages
|
|
&& <NewMessagesButton
|
|
onGoToFirstUnreadMessage = { this._onGoToFirstUnreadMessage } /> }
|
|
<div
|
|
id = 'messagesListEnd'
|
|
ref = { this._messagesListEndRef } />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* Implements {@code Component#componentDidMount}.
|
|
* When Component mount scroll message container to bottom.
|
|
* Create observer to react when scroll position is at bottom or leave the bottom.
|
|
*
|
|
* @inheritdoc
|
|
*/
|
|
componentDidMount() {
|
|
this.scrollToElement(false, null);
|
|
this._createBottomListObserver();
|
|
}
|
|
|
|
/**
|
|
* Implements {@code Component#componentDidUpdate}.
|
|
* If the user receive a new message or the local user send a new message,
|
|
* scroll automatically to the bottom if scroll position was at the bottom.
|
|
* Otherwise update hasNewMessages from component state.
|
|
*
|
|
* @inheritdoc
|
|
* @returns {void}
|
|
*/
|
|
componentDidUpdate(prevProps: IProps) {
|
|
const newMessages = this.props.messages.filter(message => !prevProps.messages.includes(message));
|
|
const hasLocalMessage = newMessages.map(message => message.messageType).includes(MESSAGE_TYPE_LOCAL);
|
|
|
|
if (newMessages.length > 0) {
|
|
if (this.state.isScrolledToBottom || hasLocalMessage) {
|
|
this.scrollToElement(false, null);
|
|
} else {
|
|
// eslint-disable-next-line react/no-did-update-set-state
|
|
this.setState({ hasNewMessages: true });
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implements React's {@link Component#componentWillUnmount()}. Invoked
|
|
* immediately before this component is unmounted and destroyed.
|
|
*
|
|
* @inheritdoc
|
|
*/
|
|
componentWillUnmount() {
|
|
const target = document.querySelector('#messagesListEnd');
|
|
|
|
this._bottomListObserver.unobserve(target as Element);
|
|
}
|
|
|
|
/**
|
|
* Automatically scrolls the displayed chat messages to bottom or to a specific element if it is provided.
|
|
*
|
|
* @param {boolean} withAnimation - Whether or not to show a scrolling.
|
|
* @param {TMLElement} element - Where to scroll.
|
|
* Animation.
|
|
* @returns {void}
|
|
*/
|
|
scrollToElement(withAnimation: boolean, element: Element | null) {
|
|
const scrollTo = element ? element : this._messagesListEndRef.current;
|
|
const block = element ? 'center' : 'nearest';
|
|
|
|
scrollIntoView(scrollTo as Element, {
|
|
behavior: withAnimation ? 'smooth' : 'auto',
|
|
block
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Callback invoked to listen to current scroll position and update next unread message.
|
|
* The callback is invoked inside a throttle with 300 ms to decrease the number of function calls.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onChatScroll() {
|
|
const firstUnreadMessage = this._findFirstUnreadMessage();
|
|
|
|
if (firstUnreadMessage && firstUnreadMessage.id !== this.state.lastReadMessageId) {
|
|
this.setState({ lastReadMessageId: firstUnreadMessage?.id });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find the first unread message.
|
|
* Update component state and scroll to element.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_onGoToFirstUnreadMessage() {
|
|
const firstUnreadMessage = this._findFirstUnreadMessage();
|
|
|
|
this.setState({ lastReadMessageId: firstUnreadMessage?.id || null });
|
|
this.scrollToElement(true, firstUnreadMessage as Element);
|
|
}
|
|
|
|
/**
|
|
* Create observer to react when scroll position is at bottom or leave the bottom.
|
|
*
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_createBottomListObserver() {
|
|
const options = {
|
|
root: document.querySelector('#chatconversation'),
|
|
rootMargin: '35px',
|
|
threshold: 0.5
|
|
};
|
|
|
|
const target = document.querySelector('#messagesListEnd');
|
|
|
|
if (target) {
|
|
this._bottomListObserver = new IntersectionObserver(this._handleIntersectBottomList, options);
|
|
this._bottomListObserver.observe(target);
|
|
}
|
|
}
|
|
|
|
/** .
|
|
* _HandleIntersectBottomList.
|
|
* When entry is intersecting with bottom of container set last message as last read message.
|
|
* When entry is not intersecting update only isScrolledToBottom with false value.
|
|
*
|
|
* @param {Array} entries - List of entries.
|
|
* @private
|
|
* @returns {void}
|
|
*/
|
|
_handleIntersectBottomList(entries: IntersectionObserverEntry[]) {
|
|
entries.forEach((entry: IntersectionObserverEntry) => {
|
|
if (entry.isIntersecting && this.props.messages.length) {
|
|
const lastMessageIndex = this.props.messages.length - 1;
|
|
const lastMessage = this.props.messages[lastMessageIndex];
|
|
const lastReadMessageId = lastMessage.messageId;
|
|
|
|
this.setState(
|
|
{
|
|
isScrolledToBottom: true,
|
|
hasNewMessages: false,
|
|
lastReadMessageId
|
|
});
|
|
}
|
|
|
|
if (!entry.isIntersecting) {
|
|
this.setState(
|
|
{
|
|
isScrolledToBottom: false
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Find first unread message.
|
|
* MessageIsAfterLastSeenMessage filter elements which are not visible but are before the last read message.
|
|
*
|
|
* @private
|
|
* @returns {Element}
|
|
*/
|
|
_findFirstUnreadMessage() {
|
|
const messagesNodeList = document.querySelectorAll('.chatmessage-wrapper');
|
|
|
|
// @ts-ignore
|
|
const messagesToArray = [ ...messagesNodeList ];
|
|
|
|
const previousIndex = messagesToArray.findIndex((message: Element) =>
|
|
message.id === this.state.lastReadMessageId);
|
|
|
|
if (previousIndex !== -1) {
|
|
for (let i = previousIndex; i < messagesToArray.length; i++) {
|
|
if (!this._isMessageVisible(messagesToArray[i])) {
|
|
return messagesToArray[i];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a message is visible in view.
|
|
*
|
|
* @param {Element} message - The message.
|
|
*
|
|
* @returns {boolean}
|
|
*/
|
|
_isMessageVisible(message: Element): boolean {
|
|
const { bottom, height, top } = message.getBoundingClientRect();
|
|
|
|
if (this._messageListRef.current) {
|
|
const containerRect = this._messageListRef.current.getBoundingClientRect();
|
|
|
|
return top <= containerRect.top
|
|
? containerRect.top - top <= height : bottom - containerRect.bottom <= height;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|