UNPKG

stream-chat-react

Version:

React components to create chat conversations or livestream style chat

277 lines (276 loc) 11.8 kB
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))); };