UNPKG

@azure/communication-react

Version:

React library for building modern communication user experiences utilizing Azure Communication Services

269 lines • 13.6 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { getChatMessages, getIsLargeGroup, getLatestReadTime, getParticipants, getReadReceipts, getUserId } from './baseSelectors'; import { toFlatCommunicationIdentifier } from "../../acs-ui-common/src"; import { memoizeFnAll } from "../../acs-ui-common/src"; import { createSelector } from 'reselect'; import { ACSKnownMessageType } from './utils/constants'; import { updateMessagesWithAttached } from './utils/updateMessagesWithAttached'; const memoizedAllConvertChatMessage = memoizeFnAll((_key, chatMessage, userId, isSeen, isLargeGroup) => { const messageType = chatMessage.type.toLowerCase(); if (messageType === ACSKnownMessageType.text || messageType === ACSKnownMessageType.richtextHtml || messageType === ACSKnownMessageType.html) { return convertToUiChatMessage(chatMessage, userId, isSeen, isLargeGroup); } else { return convertToUiSystemMessage(chatMessage); } }); const extractAttachmentMetadata = (metadata) => { const attachmentMetadata = metadata.fileSharingMetadata; if (!attachmentMetadata) { return []; } try { return JSON.parse(attachmentMetadata); } catch (e) { console.error(e); return []; } }; const extractTeamsAttachmentsMetadata = (rawAttachments) => { const attachments = []; rawAttachments.forEach(rawAttachment => { var _a; const attachmentType = rawAttachment.attachmentType; if (attachmentType === 'file') { attachments.push({ id: rawAttachment.id, name: (_a = rawAttachment.name) !== null && _a !== void 0 ? _a : '', url: extractAttachmentUrl(rawAttachment) }); } }); return { attachments }; }; const extractAttachmentUrl = (attachment) => { return attachment.previewUrl ? attachment.previewUrl : attachment.url || ''; }; const processChatMessageContent = (message) => { var _a, _b, _c, _d; let content = (_a = message.content) === null || _a === void 0 ? void 0 : _a.message; if (((_b = message.content) === null || _b === void 0 ? void 0 : _b.attachments) && ((_c = message.content) === null || _c === void 0 ? void 0 : _c.attachments.length) > 0 && sanitizedMessageContentType(message.type).includes('html')) { const attachments = (_d = message.content) === null || _d === void 0 ? void 0 : _d.attachments; // Fill in the src here if (content) { const document = new DOMParser().parseFromString(content !== null && content !== void 0 ? content : '', 'text/html'); document.querySelectorAll('img').forEach(img => { var _a, _b, _c; const attachmentPreviewUrl = (_a = attachments.find(attachment => attachment.id === img.id)) === null || _a === void 0 ? void 0 : _a.previewUrl; if (attachmentPreviewUrl) { const resourceCache = (_b = message.resourceCache) === null || _b === void 0 ? void 0 : _b[attachmentPreviewUrl]; const src = getResourceSourceUrl(resourceCache); // if in error state if (src === undefined) { const brokenImageView = getBrokenImageViewNode(img); (_c = img.parentElement) === null || _c === void 0 ? void 0 : _c.replaceChild(brokenImageView, img); } else { // else in loading or success state img.setAttribute('src', src); } setImageWidthAndHeight(img); } }); content = document.body.innerHTML; } const teamsImageHtmlContent = attachments.filter(attachment => { var _a, _b; return attachment.attachmentType === 'image' && attachment.previewUrl !== undefined && !((_b = (_a = message.content) === null || _a === void 0 ? void 0 : _a.message) === null || _b === void 0 ? void 0 : _b.includes(attachment.id)); }).map(attachment => generateImageAttachmentImgHtml(message, attachment)).join(''); if (teamsImageHtmlContent) { return (content !== null && content !== void 0 ? content : '') + teamsImageHtmlContent; } } return content; }; const generateImageAttachmentImgHtml = (message, attachment) => { var _a; if (attachment.previewUrl !== undefined) { const contentType = extractAttachmentContentTypeFromName(attachment.name); const resourceCache = (_a = message.resourceCache) === null || _a === void 0 ? void 0 : _a[attachment.previewUrl]; const src = getResourceSourceUrl(resourceCache); // if in error state if (src === undefined) { return `\r\n<p>${getBrokenImageViewNode().outerHTML}</p>`; } // else in loading or success state return `\r\n<p><img alt="image" src="${src}" itemscope="${contentType}" id="${attachment.id}"></p>`; } return ''; }; const getResourceSourceUrl = (result) => { if (result) { if (!result.error && result.sourceUrl) { // return sourceUrl for success state return result.sourceUrl; } else { // return undefined for error state return undefined; } } // return empty string for loading state return ''; }; const extractAttachmentContentTypeFromName = (name) => { if (name === undefined) { return ''; } const indexOfLastDot = name.lastIndexOf('.'); if (indexOfLastDot === undefined || indexOfLastDot < 0) { return ''; } const contentType = name.substring(indexOfLastDot + 1); return contentType; }; const setImageWidthAndHeight = (img) => { if (img) { // define aspect ratio explicitly to prevent image not being displayed correctly // in safari, this includes image placeholder for loading state const width = img.width; const height = img.height; img.style.aspectRatio = `${width}/${height}`; } }; const extractAttachmentsMetadata = (message) => { var _a, _b; let attachments = []; if (message.metadata) { attachments = attachments.concat(extractAttachmentMetadata(message.metadata)); } if ((_a = message.content) === null || _a === void 0 ? void 0 : _a.attachments) { const teamsAttachments = extractTeamsAttachmentsMetadata((_b = message.content) === null || _b === void 0 ? void 0 : _b.attachments); attachments = attachments.concat(teamsAttachments.attachments); } return { attachments: attachments.length > 0 ? attachments : undefined }; }; const convertToUiChatMessage = (message, userId, isSeen, isLargeGroup) => { const messageSenderId = message.sender !== undefined ? toFlatCommunicationIdentifier(message.sender) : userId; const { attachments } = extractAttachmentsMetadata(message); return { messageType: 'chat', createdOn: message.createdOn, content: processChatMessageContent(message), contentType: sanitizedMessageContentType(message.type), status: !isLargeGroup && message.status === 'delivered' && isSeen ? 'seen' : message.status, senderDisplayName: message.senderDisplayName, senderId: messageSenderId, messageId: message.id, clientMessageId: message.clientMessageId, editedOn: message.editedOn, deletedOn: message.deletedOn, mine: messageSenderId === userId, metadata: message.metadata, attachments }; }; const convertToUiSystemMessage = (message) => { var _a, _b, _c, _d, _e; const systemMessageType = message.type; if (systemMessageType === 'participantAdded' || systemMessageType === 'participantRemoved') { return { messageType: 'system', systemMessageType, createdOn: message.createdOn, participants: (_c = (_b = (_a = message.content) === null || _a === void 0 ? void 0 : _a.participants) === null || _b === void 0 ? void 0 : _b.filter((participant) => participant.displayName && participant.displayName !== '').map((participant) => ({ userId: toFlatCommunicationIdentifier(participant.id), displayName: participant.displayName }))) !== null && _c !== void 0 ? _c : [], messageId: message.id, iconName: systemMessageType === 'participantAdded' ? 'PeopleAdd' : 'PeopleBlock' }; } else { // Only topic updated type left, according to ACSKnown type return { messageType: 'system', systemMessageType: 'topicUpdated', createdOn: message.createdOn, topic: (_e = (_d = message.content) === null || _d === void 0 ? void 0 : _d.topic) !== null && _e !== void 0 ? _e : '', messageId: message.id, iconName: 'Edit' }; } }; /** Returns `true` if the message has participants and at least one participant has a display name. */ const hasValidParticipant = (chatMessage) => { var _a; return !!((_a = chatMessage.content) === null || _a === void 0 ? void 0 : _a.participants) && chatMessage.content.participants.some((p) => !!p.displayName); }; /** * * @private */ export const messageThreadSelectorWithThread = () => createSelector([getUserId, getChatMessages, getLatestReadTime, getIsLargeGroup, getReadReceipts, getParticipants], (userId, chatMessages, latestReadTime, isLargeGroup, readReceipts = [], participants) => { // We can't get displayName in teams meeting interop for now, disable rr feature when it is teams interop const isTeamsInterop = Object.values(participants).find(p => 'microsoftTeamsUserId' in p.id) !== undefined; // get number of participants // filter out the non valid participants (no display name) // Read Receipt details will be disabled when participant count is 0 const participantCount = isTeamsInterop ? undefined : Object.values(participants).filter(p => p.displayName && p.displayName !== '').length; // creating key value pairs of senderID: last read message information const readReceiptsBySenderId = {}; // readReceiptsBySenderId[senderID] gets updated every time a new message is read by this sender // in this way we can make sure that we are only saving the latest read message id and read on time for each sender readReceipts.filter(r => r.sender && toFlatCommunicationIdentifier(r.sender) !== userId).forEach(r => { var _a, _b; readReceiptsBySenderId[toFlatCommunicationIdentifier(r.sender)] = { lastReadMessage: r.chatMessageId, displayName: (_b = (_a = participants[toFlatCommunicationIdentifier(r.sender)]) === null || _a === void 0 ? void 0 : _a.displayName) !== null && _b !== void 0 ? _b : '' }; }); // A function takes parameter above and generate return value const convertedMessages = memoizedAllConvertChatMessage(memoizedFn => Object.values(chatMessages).filter(message => message.type.toLowerCase() === ACSKnownMessageType.text || message.type.toLowerCase() === ACSKnownMessageType.richtextHtml || message.type.toLowerCase() === ACSKnownMessageType.html || message.type === ACSKnownMessageType.participantAdded && hasValidParticipant(message) || message.type === ACSKnownMessageType.participantRemoved && hasValidParticipant(message) || // TODO: Add support for topicUpdated system messages in MessageThread component. // message.type === ACSKnownMessageType.topicUpdated || message.clientMessageId !== undefined).filter(isMessageValidToRender).map(message => { var _a; return memoizedFn((_a = message.id) !== null && _a !== void 0 ? _a : message.clientMessageId, message, userId, message.createdOn <= latestReadTime, isLargeGroup); })); updateMessagesWithAttached(convertedMessages); return { userId, showMessageStatus: true, messages: convertedMessages, participantCount, readReceiptsBySenderId }; }); const sanitizedMessageContentType = (type) => { const lowerCaseType = type.toLowerCase(); return lowerCaseType === 'text' || lowerCaseType === 'html' || lowerCaseType === 'richtext/html' ? lowerCaseType : 'unknown'; }; const getBrokenImageViewNode = (img) => { var _a; const wrapper = document.createElement('div'); Array.from((_a = img === null || img === void 0 ? void 0 : img.attributes) !== null && _a !== void 0 ? _a : []).forEach(attr => { var _a; wrapper.setAttribute(attr.nodeName, (_a = attr.nodeValue) !== null && _a !== void 0 ? _a : ''); }); wrapper.setAttribute('class', 'broken-image-wrapper'); wrapper.setAttribute('data-ui-id', 'broken-image-icon'); return wrapper; }; const isMessageValidToRender = (message) => { var _a, _b, _c, _d; if (message.deletedOn) { return false; } if (((_a = message.metadata) === null || _a === void 0 ? void 0 : _a.fileSharingMetadata) || ((_c = (_b = message.content) === null || _b === void 0 ? void 0 : _b.attachments) === null || _c === void 0 ? void 0 : _c.length)) { return true; } return !!(message.content && ((_d = message.content) === null || _d === void 0 ? void 0 : _d.message) !== ''); }; /** * Selector for {@link MessageThread} component. * * @public */ export const messageThreadSelector = messageThreadSelectorWithThread(); //# sourceMappingURL=messageThreadSelector.js.map