stream-chat-react
Version:
React components to create chat conversations or livestream style chat
277 lines (276 loc) • 11.8 kB
JavaScript
import { nanoid } from 'nanoid';
import { CUSTOM_MESSAGE_TYPE } from '../../constants/messageTypes';
import { isMessageEdited } from '../Message/utils';
import { isDate } from '../../i18n';
/**
* processMessages - Transform the input message list according to config parameters
*
* Inserts date separators btw. messages created on different dates or before unread incoming messages. By default:
* - enabled in main message list
* - disabled in virtualized message list
* - disabled in thread
*
* Allows to filter out deleted messages, contolled by hideDeletedMessages param. This is disabled by default.
*
* Sets Giphy preview message for VirtualizedMessageList
*
* The only required params are messages and userId, the rest are config params:
*
* @return {LocalMessage[]} Transformed list of messages
*/
export const processMessages = (params) => {
const { messages, reviewProcessedMessage, setGiphyPreviewMessage, ...context } = params;
const { enableDateSeparator, hideDeletedMessages, hideNewMessageSeparator, lastRead, userId, } = context;
let unread = false;
let ephemeralMessagePresent = false;
let lastDateSeparator;
const newMessages = [];
for (let i = 0; i < messages.length; i += 1) {
const message = messages[i];
if (hideDeletedMessages && message.type === 'deleted') {
continue;
}
if (setGiphyPreviewMessage &&
message.type === 'ephemeral' &&
message.command === 'giphy') {
ephemeralMessagePresent = true;
setGiphyPreviewMessage(message);
continue;
}
const changes = [];
const messageDate = (message.created_at &&
isDate(message.created_at) &&
message.created_at.toDateString()) ||
'';
const previousMessage = messages[i - 1];
let prevMessageDate = messageDate;
if (enableDateSeparator &&
previousMessage?.created_at &&
isDate(previousMessage.created_at)) {
prevMessageDate = previousMessage.created_at.toDateString();
}
if (!unread && !hideNewMessageSeparator) {
unread =
(lastRead && message.created_at && new Date(lastRead) < message.created_at) ||
false;
// do not show date separator for current user's messages
if (enableDateSeparator && unread && message.user?.id !== userId) {
changes.push({
customType: CUSTOM_MESSAGE_TYPE.date,
date: message.created_at,
id: makeDateMessageId(message.created_at),
unread,
});
}
}
if (enableDateSeparator &&
(i === 0 || // always put date separator before the first message
messageDate !== prevMessageDate || // add date separator btw. 2 messages created on different date
// if hiding deleted messages replace the previous deleted message(s) with A separator if the last rendered message was created on different date
(hideDeletedMessages &&
previousMessage?.type === 'deleted' &&
lastDateSeparator !== messageDate)) &&
!isDateSeparatorMessage(changes[changes.length - 1]) // do not show two date separators in a row)
) {
lastDateSeparator = messageDate;
changes.push({
customType: CUSTOM_MESSAGE_TYPE.date,
date: message.created_at,
id: makeDateMessageId(message.created_at),
}, message);
}
else {
changes.push(message);
}
newMessages.push(...(reviewProcessedMessage?.({
changes,
context,
index: i,
messages,
processedMessages: newMessages,
}) || changes));
}
// clean up the giphy preview component state after a Cancel action
if (setGiphyPreviewMessage && !ephemeralMessagePresent) {
setGiphyPreviewMessage(undefined);
}
return newMessages;
};
export const makeIntroMessage = () => ({
customType: CUSTOM_MESSAGE_TYPE.intro,
id: nanoid(),
});
export const makeDateMessageId = (date) => {
let idSuffix;
try {
idSuffix = !date ? nanoid() : date instanceof Date ? date.toISOString() : date;
}
catch (e) {
idSuffix = nanoid();
}
return `${CUSTOM_MESSAGE_TYPE.date}-${idSuffix}`;
};
// fast since it usually iterates just the last few messages
export const getLastReceived = (messages) => {
for (let i = messages.length - 1; i > 0; i -= 1) {
if (messages[i].status === 'received') {
return messages[i].id;
}
}
return null;
};
export const getReadStates = (messages, read = {}, returnAllReadData) => {
// create object with empty array for each message id
const readData = {};
Object.values(read).forEach((readState) => {
if (!readState.last_read)
return;
let userLastReadMsgId;
// loop messages sent by current user and add read data for other users in channel
messages.forEach((msg) => {
if (msg.created_at && msg.created_at < readState.last_read) {
userLastReadMsgId = msg.id;
// if true, save other user's read data for all messages they've read
if (returnAllReadData) {
if (!readData[userLastReadMsgId]) {
readData[userLastReadMsgId] = [];
}
readData[userLastReadMsgId].push(readState.user);
}
}
});
// if true, only save read data for other user's last read message
if (userLastReadMsgId && !returnAllReadData) {
if (!readData[userLastReadMsgId]) {
readData[userLastReadMsgId] = [];
}
readData[userLastReadMsgId].push(readState.user);
}
});
return readData;
};
export const insertIntro = (messages, headerPosition) => {
const newMessages = messages;
const intro = makeIntroMessage();
// if no headerPosition is set, HeaderComponent will go at the top
if (!headerPosition) {
newMessages.unshift(intro);
return newMessages;
}
// if no messages, intro gets inserted
if (!newMessages.length) {
newMessages.unshift(intro);
return newMessages;
}
// else loop over the messages
for (let i = 0; i < messages.length; i += 1) {
const messageTime = isDate(messages[i].created_at)
? messages[i].created_at.getTime()
: null;
const nextMessageTime = isDate(messages[i + 1].created_at)
? messages[i + 1].created_at.getTime()
: null;
// header position is smaller than message time so comes after;
if (messageTime && messageTime < headerPosition) {
// if header position is also smaller than message time continue;
if (nextMessageTime && nextMessageTime < headerPosition) {
if (messages[i + 1] && isDateSeparatorMessage(messages[i + 1]))
continue;
if (!nextMessageTime) {
newMessages.push(intro);
return newMessages;
}
}
else {
newMessages.splice(i + 1, 0, intro);
return newMessages;
}
}
}
return newMessages;
};
export const getGroupStyles = (message, previousMessage, nextMessage, noGroupByUser, maxTimeBetweenGroupedMessages) => {
if (isDateSeparatorMessage(message) || isIntroMessage(message))
return '';
if (noGroupByUser || message.attachments?.length !== 0)
return 'single';
const isTopMessage = !previousMessage ||
isIntroMessage(previousMessage) ||
isDateSeparatorMessage(previousMessage) ||
previousMessage.type === 'system' ||
previousMessage.type === 'error' ||
previousMessage.attachments?.length !== 0 ||
message.user?.id !== previousMessage.user?.id ||
previousMessage.deleted_at ||
(message.reaction_groups && Object.keys(message.reaction_groups).length > 0) ||
isMessageEdited(previousMessage) ||
(maxTimeBetweenGroupedMessages !== undefined &&
previousMessage.created_at &&
message.created_at &&
new Date(message.created_at).getTime() -
new Date(previousMessage.created_at).getTime() >
maxTimeBetweenGroupedMessages);
const isBottomMessage = !nextMessage ||
isIntroMessage(nextMessage) ||
isDateSeparatorMessage(nextMessage) ||
nextMessage.type === 'system' ||
nextMessage.type === 'error' ||
nextMessage.attachments?.length !== 0 ||
message.user?.id !== nextMessage.user?.id ||
nextMessage.deleted_at ||
(nextMessage.reaction_groups &&
Object.keys(nextMessage.reaction_groups).length > 0) ||
isMessageEdited(message) ||
(maxTimeBetweenGroupedMessages !== undefined &&
nextMessage.created_at &&
message.created_at &&
new Date(nextMessage.created_at).getTime() -
new Date(message.created_at).getTime() >
maxTimeBetweenGroupedMessages);
if (!isTopMessage && !isBottomMessage) {
if (message.deleted_at || message.type === 'error')
return 'single';
return 'middle';
}
if (isBottomMessage) {
if (isTopMessage || message.deleted_at || message.type === 'error')
return 'single';
return 'bottom';
}
if (isTopMessage)
return 'top';
return '';
};
// "Probably" included, because it may happen that the last page was returned and it has exactly the size of the limit
// but the back-end cannot provide us with information on whether it has still more messages in the DB
// FIXME: once the pagination state is moved from Channel to MessageList, these should be moved as well.
// The MessageList should have configurable the limit for performing the requests.
// This parameter would then be used within these functions
export const hasMoreMessagesProbably = (returnedCountMessages, limit) => returnedCountMessages >= limit;
// @deprecated
export const hasNotMoreMessages = (returnedCountMessages, limit) => returnedCountMessages < limit;
export function isIntroMessage(message) {
return message.customType === CUSTOM_MESSAGE_TYPE.intro;
}
export function isDateSeparatorMessage(message) {
return (message !== null &&
typeof message === 'object' &&
message.customType === CUSTOM_MESSAGE_TYPE.date &&
isDate(message.date));
}
export function isLocalMessage(message) {
return !isDateSeparatorMessage(message) && !isIntroMessage(message);
}
export const getIsFirstUnreadMessage = ({ firstUnreadMessageId, isFirstMessage, lastReadDate, lastReadMessageId, message, previousMessage, unreadMessageCount = 0, }) => {
// prevent showing unread indicator in threads
if (message.parent_id)
return false;
const createdAtTimestamp = message.created_at && new Date(message.created_at).getTime();
const lastReadTimestamp = lastReadDate?.getTime();
const messageIsUnread = !!createdAtTimestamp && !!lastReadTimestamp && createdAtTimestamp > lastReadTimestamp;
const previousMessageIsLastRead = !!lastReadMessageId && lastReadMessageId === previousMessage?.id;
return (firstUnreadMessageId === message.id ||
(!!unreadMessageCount &&
messageIsUnread &&
(isFirstMessage || previousMessageIsLastRead)));
};