UNPKG

stream-chat-react

Version:

React components to create chat conversations or livestream style chat

328 lines (327 loc) 12.8 kB
import deepequal from 'react-fast-compare'; import emojiRegex from 'emoji-regex'; /** * Following function validates a function which returns notification message. * It validates if the first parameter is function and also if return value of function is string or no. */ export const validateAndGetMessage = (func, args) => { if (!func || typeof func !== 'function') return null; // below is due to tests passing a single argument // rather than an array. if (!Array.isArray(args)) { args = [args]; } const returnValue = func(...args); if (typeof returnValue !== 'string') return null; return returnValue; }; /** * Tell if the owner of the current message is muted */ export const isUserMuted = (message, mutes) => { if (!mutes || !message) return false; const userMuted = mutes.filter((el) => el.target.id === message.user?.id); return !!userMuted.length; }; export const MESSAGE_ACTIONS = { delete: 'delete', edit: 'edit', flag: 'flag', markUnread: 'markUnread', mute: 'mute', pin: 'pin', quote: 'quote', react: 'react', remindMe: 'remindMe', reply: 'reply', saveForLater: 'saveForLater', }; // @deprecated in favor of `channelCapabilities` - TODO: remove in next major release export const defaultPinPermissions = { commerce: { admin: true, anonymous: false, channel_member: false, channel_moderator: true, guest: false, member: false, moderator: true, owner: true, user: false, }, gaming: { admin: true, anonymous: false, channel_member: false, channel_moderator: true, guest: false, member: false, moderator: true, owner: false, user: false, }, livestream: { admin: true, anonymous: false, channel_member: false, channel_moderator: true, guest: false, member: false, moderator: true, owner: true, user: false, }, messaging: { admin: true, anonymous: false, channel_member: true, channel_moderator: true, guest: false, member: true, moderator: true, owner: true, user: false, }, team: { admin: true, anonymous: false, channel_member: true, channel_moderator: true, guest: false, member: true, moderator: true, owner: true, user: false, }, }; export const getMessageActions = (actions, { canDelete, canEdit, canFlag, canMarkUnread, canMute, canPin, canQuote, canReact, canReply, }, channelConfig) => { const messageActionsAfterPermission = []; let messageActions = []; if (actions && typeof actions === 'boolean') { // If value of actions is true, then populate all the possible values messageActions = Object.keys(MESSAGE_ACTIONS); } else if (actions && actions.length > 0) { messageActions = [...actions]; } else { return []; } if (canDelete && messageActions.indexOf(MESSAGE_ACTIONS.delete) > -1) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.delete); } if (canEdit && messageActions.indexOf(MESSAGE_ACTIONS.edit) > -1) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.edit); } if (canFlag && messageActions.indexOf(MESSAGE_ACTIONS.flag) > -1) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.flag); } if (canMarkUnread && messageActions.indexOf(MESSAGE_ACTIONS.markUnread) > -1) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.markUnread); } if (canMute && messageActions.indexOf(MESSAGE_ACTIONS.mute) > -1) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.mute); } if (canPin && messageActions.indexOf(MESSAGE_ACTIONS.pin) > -1) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.pin); } if (canQuote && messageActions.indexOf(MESSAGE_ACTIONS.quote) > -1) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.quote); } if (canReact && messageActions.indexOf(MESSAGE_ACTIONS.react) > -1) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.react); } if (channelConfig?.['user_message_reminders'] && messageActions.indexOf(MESSAGE_ACTIONS.remindMe)) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.remindMe); } if (canReply && messageActions.indexOf(MESSAGE_ACTIONS.reply) > -1) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.reply); } if (channelConfig?.['user_message_reminders'] && messageActions.indexOf(MESSAGE_ACTIONS.saveForLater)) { messageActionsAfterPermission.push(MESSAGE_ACTIONS.saveForLater); } return messageActionsAfterPermission; }; export const ACTIONS_NOT_WORKING_IN_THREAD = [ MESSAGE_ACTIONS.pin, MESSAGE_ACTIONS.reply, MESSAGE_ACTIONS.markUnread, ]; /** * @deprecated use `shouldRenderMessageActions` instead */ export const showMessageActionsBox = (actions, inThread) => shouldRenderMessageActions({ inThread, messageActions: actions }); export const shouldRenderMessageActions = ({ customMessageActions, CustomMessageActionsList, inThread, messageActions, }) => { if (typeof CustomMessageActionsList !== 'undefined' || typeof customMessageActions !== 'undefined') return true; if (!messageActions.length) return false; if (inThread && messageActions.filter((action) => !ACTIONS_NOT_WORKING_IN_THREAD.includes(action)) .length === 0) { return false; } if (messageActions.length === 1 && (messageActions.includes(MESSAGE_ACTIONS.react) || messageActions.includes(MESSAGE_ACTIONS.reply))) { return false; } if (messageActions.length === 2 && messageActions.includes(MESSAGE_ACTIONS.react) && messageActions.includes(MESSAGE_ACTIONS.reply)) { return false; } return true; }; function areMessagesEqual(prevMessage, nextMessage) { const areBaseMessagesEqual = (prevMessage, nextMessage) => prevMessage.deleted_at === nextMessage.deleted_at && prevMessage.latest_reactions?.length === nextMessage.latest_reactions?.length && prevMessage.own_reactions?.length === nextMessage.own_reactions?.length && prevMessage.pinned === nextMessage.pinned && prevMessage.reply_count === nextMessage.reply_count && prevMessage.status === nextMessage.status && prevMessage.text === nextMessage.text && prevMessage.type === nextMessage.type && prevMessage.updated_at === nextMessage.updated_at && prevMessage.user?.updated_at === nextMessage.user?.updated_at; return (areBaseMessagesEqual(prevMessage, nextMessage) && Boolean(prevMessage.quoted_message) === Boolean(nextMessage.quoted_message) && ((!prevMessage.quoted_message && !nextMessage.quoted_message) || areBaseMessagesEqual(prevMessage.quoted_message, nextMessage.quoted_message))); } export const areMessagePropsEqual = (prevProps, nextProps) => { const { message: prevMessage, Message: prevMessageUI } = prevProps; const { message: nextMessage, Message: nextMessageUI } = nextProps; if (prevMessageUI !== nextMessageUI) return false; if (prevProps.endOfGroup !== nextProps.endOfGroup) return false; if (nextProps.showDetailedReactions !== prevProps.showDetailedReactions) { return false; } if (nextProps.closeReactionSelectorOnClick !== prevProps.closeReactionSelectorOnClick) { return false; } const messagesAreEqual = areMessagesEqual(prevMessage, nextMessage); if (!messagesAreEqual) return false; const deepEqualProps = deepequal(nextProps.messageActions, prevProps.messageActions) && deepequal(nextProps.readBy, prevProps.readBy) && deepequal(nextProps.highlighted, prevProps.highlighted) && deepequal(nextProps.groupStyles, prevProps.groupStyles) && // last 3 messages can have different group styles deepequal(nextProps.mutes, prevProps.mutes) && deepequal(nextProps.lastReceivedId, prevProps.lastReceivedId); if (!deepEqualProps) return false; return (prevProps.messageListRect === nextProps.messageListRect // MessageList wrapper layout changes ); }; export const areMessageUIPropsEqual = (prevProps, nextProps) => { const { lastReceivedId: prevLastReceivedId, message: prevMessage } = prevProps; const { lastReceivedId: nextLastReceivedId, message: nextMessage } = nextProps; if (prevProps.editing !== nextProps.editing) return false; if (prevProps.highlighted !== nextProps.highlighted) return false; if (prevProps.endOfGroup !== nextProps.endOfGroup) return false; if (prevProps.mutes?.length !== nextProps.mutes?.length) return false; if (prevProps.readBy?.length !== nextProps.readBy?.length) return false; if (prevProps.groupStyles !== nextProps.groupStyles) return false; if (prevProps.showDetailedReactions !== nextProps.showDetailedReactions) { return false; } if ((prevMessage.id === prevLastReceivedId || prevMessage.id === nextLastReceivedId) && prevLastReceivedId !== nextLastReceivedId) { return false; } return areMessagesEqual(prevMessage, nextMessage); }; export const messageHasReactions = (message) => Object.values(message?.reaction_groups ?? {}).some(({ count }) => count > 0); export const messageHasAttachments = (message) => !!message?.attachments && !!message.attachments.length; export const getImages = (message) => { if (!message?.attachments) { return []; } return message.attachments.filter((item) => item.type === 'image'); }; export const getNonImageAttachments = (message) => { if (!message?.attachments) { return []; } return message.attachments.filter((item) => item.type !== 'image'); }; /** * Default Tooltip Username mapper implementation. * * @param user the user. */ export const mapToUserNameOrId = (user) => user.name || user.id; export const getReadByTooltipText = (users, t, client, tooltipUserNameMapper) => { let outStr = ''; if (!t) { throw new Error('getReadByTooltipText was called, but translation function is not available'); } if (!tooltipUserNameMapper) { throw new Error('getReadByTooltipText was called, but tooltipUserNameMapper function is not available'); } // first filter out client user, so restLength won't count it const otherUsers = users .filter((item) => item && client?.user && item.id !== client.user.id) .map(tooltipUserNameMapper); const slicedArr = otherUsers.slice(0, 5); const restLength = otherUsers.length - slicedArr.length; if (slicedArr.length === 1) { outStr = `${slicedArr[0]} `; } else if (slicedArr.length === 2) { // joins all with "and" but =no commas // example: "bob and sam" outStr = t('{{ firstUser }} and {{ secondUser }}', { firstUser: slicedArr[0], secondUser: slicedArr[1], }); } else if (slicedArr.length > 2) { // joins all with commas, but last one gets ", and" (oxford comma!) // example: "bob, joe, sam and 4 more" if (restLength === 0) { // mutate slicedArr to remove last user to display it separately const lastUser = slicedArr.splice(slicedArr.length - 1, 1); outStr = t('{{ commaSeparatedUsers }}, and {{ lastUser }}', { commaSeparatedUsers: slicedArr.join(', '), lastUser, }); } else { outStr = t('{{ commaSeparatedUsers }} and {{ moreCount }} more', { commaSeparatedUsers: slicedArr.join(', '), moreCount: restLength, }); } } return outStr; }; export const isOnlyEmojis = (text) => { if (!text) return false; const noEmojis = text.replace(emojiRegex(), ''); const noSpace = noEmojis.replace(/[\s\n]/gm, ''); return !noSpace; }; export const isMessageBounced = (message) => message.type === 'error' && (message.moderation_details?.action === 'MESSAGE_RESPONSE_ACTION_BOUNCE' || message.moderation?.action === 'bounce'); export const isMessageBlocked = (message) => message.type === 'error' && (message.moderation_details?.action === 'MESSAGE_RESPONSE_ACTION_REMOVE' || message.moderation?.action === 'remove'); export const isMessageEdited = (message) => !!message.message_text_updated_at;