Files
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

228 lines
6.6 KiB
TypeScript

import React, { Component } from 'react';
import { FlatList, Text, TextStyle, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions.native';
import { groupMessagesBySender } from '../../../base/util/messageGrouping';
import { MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from '../../constants';
import { getActiveChatSearchMatch } from '../../functions';
import { IMessage } from '../../types';
import ChatMessage from './ChatMessage';
import styles from './styles';
/**
* A single flat list row: one message plus its grouping flags.
*/
interface IMessageRow {
message: IMessage;
showAvatar: boolean;
showDisplayName: boolean;
showTimestamp: boolean;
}
interface IProps {
_activeMatch?: IMessage;
messages: IMessage[];
t: Function;
}
/**
* Implements a container to render all the chat messages in a conference.
*/
class MessageContainer extends Component<IProps, any> {
static defaultProps = {
messages: [] as IMessage[]
};
_flatListRef: React.RefObject<FlatList<any>>;
/**
* Instantiates a new instance of the component.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this._flatListRef = React.createRef();
this._keyExtractor = this._keyExtractor.bind(this);
this._renderListEmptyComponent = this._renderListEmptyComponent.bind(this);
this._renderMessage = this._renderMessage.bind(this);
this._getMessageRows = this._getMessageRows.bind(this);
this._onScrollToIndexFailed = this._onScrollToIndexFailed.bind(this);
}
/**
* Implements {@code Component#componentDidUpdate}.
*
* @inheritdoc
*/
override componentDidUpdate(prevProps: IProps) {
const { _activeMatch } = this.props;
if (_activeMatch && _activeMatch.messageId !== prevProps._activeMatch?.messageId) {
this._scrollToActiveMatch();
}
}
/**
* Scrolls the list so the active search match message is visible.
*
* @returns {void}
*/
_scrollToActiveMatch() {
const { _activeMatch } = this.props;
if (!_activeMatch) {
return;
}
const data = this._getMessageRows();
const index = data.findIndex(row => row.message.messageId === _activeMatch.messageId);
if (index === -1) {
return;
}
requestAnimationFrame(() => {
this._flatListRef.current?.scrollToIndex({
animated: true,
index,
viewPosition: 0.5
});
});
}
/**
* Fallback for when scrollToIndex fires before the target row is measured.
*
* @param {Object} info - Info about the failed scroll attempt.
* @returns {void}
*/
_onScrollToIndexFailed(info: { averageItemLength: number; index: number; }) {
// Jump near the target so it renders, then retry.
this._flatListRef.current?.scrollToOffset({
animated: false,
offset: info.averageItemLength * info.index
});
setTimeout(() => {
this._flatListRef.current?.scrollToIndex({
animated: true,
index: info.index,
viewPosition: 0.5
});
}, 100);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
override render() {
const data = this._getMessageRows();
const noMessages = data.length === 0;
return (
<FlatList
ListEmptyComponent = { this._renderListEmptyComponent }
bounces = { false }
// @ts-ignore
contentContainerStyle = { noMessages && styles.emptyListContentContainer }
data = { data }
// Workaround for RN bug:
// https://github.com/facebook/react-native/issues/21196
inverted = { Boolean(data.length) }
keyExtractor = { this._keyExtractor }
keyboardShouldPersistTaps = 'handled'
onScrollToIndexFailed = { this._onScrollToIndexFailed }
ref = { this._flatListRef }
renderItem = { this._renderMessage }
style = { noMessages && styles.emptyListStyle } />
);
}
/**
* Key extractor for the flatlist.
*
* @param {IMessageRow} item - The row whose key we need to generate.
* @returns {string}
*/
_keyExtractor(item: IMessageRow) {
return item.message.messageId;
}
/**
* Renders a message when there are no messages in the chat yet.
*
* @returns {React$Element<any>}
*/
_renderListEmptyComponent() {
const { t } = this.props;
return (
<View
id = 'no-messages-message'
style = { styles.emptyComponentWrapper as ViewStyle }>
<Text style = { styles.emptyComponentText as TextStyle }>
{ t('chat.noMessagesMessage') }
</Text>
</View>
);
}
/**
* Renders a single chat message.
*
* @param {Object} row - The row containing the message and its grouping flags.
* @returns {React$Element<*>}
*/
_renderMessage({ item }: { item: IMessageRow; }) {
return (
<ChatMessage
message = { item.message }
showAvatar = { item.showAvatar }
showDisplayName = { item.showDisplayName }
showTimestamp = { item.showTimestamp } />
);
}
/**
* Builds one row per message with its grouping flags.
*
* @returns {IMessageRow[]}
*/
_getMessageRows(): IMessageRow[] {
const rows: IMessageRow[] = [];
for (const group of groupMessagesBySender(this.props.messages)) {
const groupType = group.messages[0].messageType;
const oldestIndex = group.messages.length - 1;
group.messages.forEach((message, index) => {
rows.push({
message,
showAvatar: groupType !== MESSAGE_TYPE_LOCAL && index === oldestIndex,
showDisplayName: groupType === MESSAGE_TYPE_REMOTE && index === oldestIndex,
showTimestamp: index === 0
});
});
}
return rows;
}
}
function _mapStateToProps(state: IReduxState) {
return {
_activeMatch: getActiveChatSearchMatch(state)
};
}
export default translate(connect(_mapStateToProps)(MessageContainer));