@azure/communication-react
Version:
React library for building modern communication user experiences utilizing Azure Communication Services
226 lines • 13.9 kB
JavaScript
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { isIOS } from '@fluentui/react';
import { mergeStyles, Stack } from '@fluentui/react';
import { PersonaSize } from '@fluentui/react';
import { ErrorBar, MessageThread, TypingIndicator, useTheme } from "../../../../react-components/src";
import React, { useCallback, useEffect, useMemo } from 'react';
import { useState } from 'react';
import { AvatarPersona } from '../common/AvatarPersona';
import { useAdapter } from './adapter/ChatAdapterProvider';
import { ChatHeader, getHeaderProps } from './ChatHeader';
import { useAdaptedSelector } from './hooks/useAdaptedSelector';
import { usePropsFor } from './hooks/usePropsFor';
import { chatArea, chatContainer, chatWrapper, messageThreadChatCompositeStyles, sendboxContainerStyles, typingIndicatorContainerStyles } from './styles/Chat.styles';
import { participantListContainerPadding } from '../common/styles/ParticipantContainer.styles';
import { toFlatCommunicationIdentifier } from "../../../../acs-ui-common/src";
import { ImageOverlay } from "../../../../react-components/src";
import { SendBoxPicker } from '../common/SendBoxPicker';
import { useSelector } from './hooks/useSelector';
import { getChatMessages, getThreadId, getUserId } from './selectors/baseSelectors';
/**
* @private
*/
export const ChatScreen = (props) => {
const { onFetchAvatarPersonaData, onRenderMessage, onRenderTypingIndicator, options, styles, formFactor } = props;
const defaultNumberOfChatMessagesToReload = 5;
const [overlayImageItem, setOverlayImageItem] = useState();
const [isImageOverlayOpen, setIsImageOverlayOpen] = useState(false);
const adapter = useAdapter();
const theme = useTheme();
useEffect(() => {
// Initial data should be always fetched by the composite(or external caller) instead of the adapter
const fetchData = () => __awaiter(void 0, void 0, void 0, function* () {
// Fetch initial data for adapter
yield adapter.fetchInitialData();
// Fetch initial set of messages. Without fetching messages here, if the Composite's adapter is changed the message thread does not load new messages.
yield adapter.loadPreviousChatMessages(defaultNumberOfChatMessagesToReload);
});
fetchData();
}, [adapter]);
const messageThreadProps = usePropsFor(MessageThread);
const typingIndicatorProps = usePropsFor(TypingIndicator);
const headerProps = useAdaptedSelector(getHeaderProps);
const errorBarProps = usePropsFor(ErrorBar);
const overlayImageItemRef = React.useRef(overlayImageItem);
overlayImageItemRef.current = overlayImageItem;
const adapterChatMessages = useSelector(getChatMessages);
useEffect(() => {
var _a;
if (overlayImageItemRef.current === undefined) {
return;
}
const message = adapterChatMessages[overlayImageItemRef.current.messageId];
if (message === undefined) {
return;
}
const resourceFetchResult = (_a = message.resourceCache) === null || _a === void 0 ? void 0 : _a[overlayImageItemRef.current.imageUrl];
if (overlayImageItemRef.current.imageSrc === '' && resourceFetchResult) {
const fullSizeImageSrc = getResourceSourceUrl(resourceFetchResult);
if (fullSizeImageSrc === undefined || fullSizeImageSrc === '' || overlayImageItemRef.current.imageSrc === fullSizeImageSrc) {
return;
}
setOverlayImageItem(Object.assign(Object.assign({}, overlayImageItemRef.current), { imageSrc: fullSizeImageSrc }));
}
}, [adapterChatMessages]);
const getResourceSourceUrl = (result) => {
let src = '';
if (result.error || !result.sourceUrl) {
src = 'blob://';
}
else {
src = result.sourceUrl;
}
return src;
};
const onRenderAvatarCallback = useCallback((userId, defaultOptions) => {
return React.createElement(AvatarPersona, Object.assign({ userId: userId, hidePersonaDetails: true }, defaultOptions, { dataProvider: onFetchAvatarPersonaData }));
}, [onFetchAvatarPersonaData]);
const messageThreadStyles = useMemo(() => {
return Object.assign({}, messageThreadChatCompositeStyles(theme.semanticColors.bodyBackground), styles === null || styles === void 0 ? void 0 : styles.messageThread);
}, [styles === null || styles === void 0 ? void 0 : styles.messageThread, theme.semanticColors.bodyBackground]);
const typingIndicatorStyles = useMemo(() => {
return Object.assign({}, styles === null || styles === void 0 ? void 0 : styles.typingIndicator);
}, [styles === null || styles === void 0 ? void 0 : styles.typingIndicator]);
const sendBoxStyles = useMemo(() => {
return Object.assign({}, styles === null || styles === void 0 ? void 0 : styles.sendBox);
}, [styles === null || styles === void 0 ? void 0 : styles.sendBox]);
const userIdObject = useSelector(getUserId);
const userId = toFlatCommunicationIdentifier(userIdObject);
const threadId = useSelector(getThreadId);
const onInlineImageClicked = useCallback((attachmentId, messageId) => {
var _a, _b, _c;
const message = adapterChatMessages[messageId];
const inlinedImages = (_b = (_a = message === null || message === void 0 ? void 0 : message.content) === null || _a === void 0 ? void 0 : _a.attachments) === null || _b === void 0 ? void 0 : _b.filter(attachment => {
return attachment.attachmentType === 'image' && attachment.id === attachmentId;
});
const attachment = inlinedImages === null || inlinedImages === void 0 ? void 0 : inlinedImages[0];
if (!attachment) {
return;
}
let imageSrc = '';
if (attachment.url) {
const resourceFetchResult = (_c = message === null || message === void 0 ? void 0 : message.resourceCache) === null || _c === void 0 ? void 0 : _c[attachment.url];
if (resourceFetchResult) {
imageSrc = getResourceSourceUrl(resourceFetchResult);
}
else {
adapter.downloadResourceToCache({
threadId,
messageId,
resourceUrl: attachment.url
});
}
}
const titleIconRenderOptions = {
text: message === null || message === void 0 ? void 0 : message.senderDisplayName,
size: PersonaSize.size32,
showOverflowTooltip: false,
imageAlt: message === null || message === void 0 ? void 0 : message.senderDisplayName
};
const messageSenderId = (message === null || message === void 0 ? void 0 : message.sender) !== undefined ? toFlatCommunicationIdentifier(message.sender) : userId;
const titleIcon = onRenderAvatarCallback && onRenderAvatarCallback(messageSenderId, titleIconRenderOptions);
const overlayImage = {
title: (message === null || message === void 0 ? void 0 : message.senderDisplayName) || '',
titleIcon: titleIcon,
attachmentId: attachment.id,
imageSrc: imageSrc,
messageId: messageId,
imageUrl: attachment.url || ''
};
setIsImageOverlayOpen(true);
setOverlayImageItem(overlayImage);
}, [adapter, adapterChatMessages, onRenderAvatarCallback, userId, threadId]);
const onRenderInlineImage = useCallback((inlineImage, defaultOnRender) => {
var _a, _b, _c;
const message = adapterChatMessages[inlineImage.messageId];
const attachment = (_b = (_a = message === null || message === void 0 ? void 0 : message.content) === null || _a === void 0 ? void 0 : _a.attachments) === null || _b === void 0 ? void 0 : _b.find(attachment => attachment.id === inlineImage.imageAttributes.id);
if (attachment === undefined) {
return defaultOnRender(inlineImage);
}
let pointerEvents = inlineImage.imageAttributes.src === '' ? 'none' : 'auto';
const resourceCache = message === null || message === void 0 ? void 0 : message.resourceCache;
if (resourceCache && attachment.previewUrl && resourceCache[attachment.previewUrl] && ((_c = resourceCache[attachment.previewUrl]) === null || _c === void 0 ? void 0 : _c.error)) {
pointerEvents = 'none';
}
return React.createElement("span", { key: inlineImage.imageAttributes.id, onClick: () => onInlineImageClicked(inlineImage.imageAttributes.id || '', inlineImage.messageId), tabIndex: 0, role: "button", onKeyDown: e => {
if (e.key === 'Enter') {
onInlineImageClicked(inlineImage.imageAttributes.id || '', inlineImage.messageId);
}
}, style: {
cursor: 'pointer',
pointerEvents
} }, defaultOnRender(inlineImage));
}, [adapterChatMessages, onInlineImageClicked]);
const inlineImageOptions = useMemo(() => {
return {
onRenderInlineImage: onRenderInlineImage
};
}, [onRenderInlineImage]);
const onDownloadButtonClicked = useCallback((imageSrc) => {
if (imageSrc === '') {
return;
}
if (isIOS()) {
window.open(imageSrc, '_blank');
}
else {
// Create a new anchor element
const a = document.createElement('a');
// Set the href and download attributes for the anchor element
a.href = imageSrc;
a.download = (overlayImageItem === null || overlayImageItem === void 0 ? void 0 : overlayImageItem.attachmentId) || '';
a.rel = 'noopener noreferrer';
a.target = '_blank';
// Programmatically click the anchor element to trigger the download
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
}, [overlayImageItem === null || overlayImageItem === void 0 ? void 0 : overlayImageItem.attachmentId]);
const onSendMessageHandler = useCallback(function (content) {
return __awaiter(this, void 0, void 0, function* () {
yield adapter.sendMessage(content, {});
});
}, [adapter]);
const onUpdateMessageHandler = useCallback(function (messageId, content) {
return __awaiter(this, void 0, void 0, function* () {
yield messageThreadProps.onUpdateMessage(messageId, content);
});
}, [messageThreadProps]);
return React.createElement(Stack, { className: chatContainer, grow: true },
(options === null || options === void 0 ? void 0 : options.topic) !== false && React.createElement(ChatHeader, Object.assign({}, headerProps)),
React.createElement(Stack, { className: chatArea, tokens: participantListContainerPadding, horizontal: true, grow: true },
React.createElement(Stack, { className: chatWrapper, grow: true },
(options === null || options === void 0 ? void 0 : options.errorBar) !== false && React.createElement(ErrorBar, Object.assign({}, errorBarProps)),
React.createElement(MessageThread, Object.assign({}, messageThreadProps, { onUpdateMessage: onUpdateMessageHandler, onRenderAvatar: onRenderAvatarCallback, onRenderMessage: onRenderMessage, inlineImageOptions: inlineImageOptions, numberOfChatMessagesToReload: defaultNumberOfChatMessagesToReload, styles: messageThreadStyles })),
React.createElement(Stack, { className: mergeStyles(sendboxContainerStyles) },
React.createElement("div", { className: mergeStyles(typingIndicatorContainerStyles) }, onRenderTypingIndicator ? onRenderTypingIndicator(typingIndicatorProps.typingUsers) : React.createElement(TypingIndicator, Object.assign({}, typingIndicatorProps, { styles: typingIndicatorStyles }))),
React.createElement(Stack, { horizontal: formFactor === 'mobile' },
formFactor === 'mobile',
React.createElement(Stack, { grow: true },
React.createElement(SendBoxPicker, { styles: sendBoxStyles, autoFocus: options === null || options === void 0 ? void 0 : options.autoFocus,
// we need to overwrite onSendMessage for SendBox because we need to clear attachment state
// when submit button is clicked
onSendMessage: onSendMessageHandler })),
formFactor !== 'mobile')))),
overlayImageItem && React.createElement(ImageOverlay, Object.assign({}, overlayImageItem, { isOpen: isImageOverlayOpen, onDismiss: () => {
setOverlayImageItem(undefined);
setIsImageOverlayOpen(false);
adapter.removeResourceFromCache({
threadId,
messageId: overlayImageItem.messageId,
resourceUrl: overlayImageItem.imageUrl
});
}, onDownloadButtonClicked: onDownloadButtonClicked })));
};
//# sourceMappingURL=ChatScreen.js.map