@azure/communication-react
Version:
React library for building modern communication user experiences utilizing Azure Communication Services
269 lines • 13.6 kB
JavaScript
// 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