diff --git a/.yarn/cache/@fluentui-contrib-react-chat-npm-0.1.7-66185e2a2e-41b1a4be48.zip b/.yarn/cache/@fluentui-contrib-react-chat-npm-0.1.7-66185e2a2e-41b1a4be48.zip new file mode 100644 index 0000000000..452c4233f6 Binary files /dev/null and b/.yarn/cache/@fluentui-contrib-react-chat-npm-0.1.7-66185e2a2e-41b1a4be48.zip differ diff --git a/.yarn/cache/@types-linkify-it-npm-3.0.3-cb8d4d3e99-a734becc4e.zip b/.yarn/cache/@types-linkify-it-npm-3.0.3-cb8d4d3e99-a734becc4e.zip new file mode 100644 index 0000000000..533dd7ab1f Binary files /dev/null and b/.yarn/cache/@types-linkify-it-npm-3.0.3-cb8d4d3e99-a734becc4e.zip differ diff --git a/.yarn/cache/@types-markdown-it-npm-13.0.2-4839de9ecd-fe1f6a12ee.zip b/.yarn/cache/@types-markdown-it-npm-13.0.2-4839de9ecd-fe1f6a12ee.zip new file mode 100644 index 0000000000..3f81e423c7 Binary files /dev/null and b/.yarn/cache/@types-markdown-it-npm-13.0.2-4839de9ecd-fe1f6a12ee.zip differ diff --git a/.yarn/cache/@types-mdurl-npm-1.0.3-07a1eff7b0-5bbed4f0eb.zip b/.yarn/cache/@types-mdurl-npm-1.0.3-07a1eff7b0-5bbed4f0eb.zip new file mode 100644 index 0000000000..ad9b0810ac Binary files /dev/null and b/.yarn/cache/@types-mdurl-npm-1.0.3-07a1eff7b0-5bbed4f0eb.zip differ diff --git a/.yarn/cache/adaptivecards-controls-npm-0.10.1-8cec73a1ae-a6bec98489.zip b/.yarn/cache/adaptivecards-controls-npm-0.10.1-8cec73a1ae-a6bec98489.zip new file mode 100644 index 0000000000..74582cdd5f Binary files /dev/null and b/.yarn/cache/adaptivecards-controls-npm-0.10.1-8cec73a1ae-a6bec98489.zip differ diff --git a/.yarn/cache/adaptivecards-designer-npm-2.4.3-943f1e15cc-0428c2eb94.zip b/.yarn/cache/adaptivecards-designer-npm-2.4.3-943f1e15cc-0428c2eb94.zip new file mode 100644 index 0000000000..45ec2a3832 Binary files /dev/null and b/.yarn/cache/adaptivecards-designer-npm-2.4.3-943f1e15cc-0428c2eb94.zip differ diff --git a/.yarn/cache/adaptivecards-npm-3.0.1-cdb87b27d4-b87c2ba37d.zip b/.yarn/cache/adaptivecards-npm-3.0.1-cdb87b27d4-b87c2ba37d.zip new file mode 100644 index 0000000000..52a10f3c8c Binary files /dev/null and b/.yarn/cache/adaptivecards-npm-3.0.1-cdb87b27d4-b87c2ba37d.zip differ diff --git a/.yarn/cache/clipboard-npm-2.0.11-45358b5ae8-413055a603.zip b/.yarn/cache/clipboard-npm-2.0.11-45358b5ae8-413055a603.zip new file mode 100644 index 0000000000..dc934e50f4 Binary files /dev/null and b/.yarn/cache/clipboard-npm-2.0.11-45358b5ae8-413055a603.zip differ diff --git a/.yarn/cache/delegate-npm-3.2.0-d3f849ea99-d943058fe0.zip b/.yarn/cache/delegate-npm-3.2.0-d3f849ea99-d943058fe0.zip new file mode 100644 index 0000000000..b52baa9778 Binary files /dev/null and b/.yarn/cache/delegate-npm-3.2.0-d3f849ea99-d943058fe0.zip differ diff --git a/.yarn/cache/entities-npm-3.0.1-21eeb201ba-aaf7f12033.zip b/.yarn/cache/entities-npm-3.0.1-21eeb201ba-aaf7f12033.zip new file mode 100644 index 0000000000..78991fcd20 Binary files /dev/null and b/.yarn/cache/entities-npm-3.0.1-21eeb201ba-aaf7f12033.zip differ diff --git a/.yarn/cache/good-listener-npm-1.2.2-e7865da849-f39fb82c4e.zip b/.yarn/cache/good-listener-npm-1.2.2-e7865da849-f39fb82c4e.zip new file mode 100644 index 0000000000..5ff451212a Binary files /dev/null and b/.yarn/cache/good-listener-npm-1.2.2-e7865da849-f39fb82c4e.zip differ diff --git a/.yarn/cache/linkify-it-npm-4.0.1-9c7d5a3cd6-3e0a299212.zip b/.yarn/cache/linkify-it-npm-4.0.1-9c7d5a3cd6-3e0a299212.zip new file mode 100644 index 0000000000..372277e403 Binary files /dev/null and b/.yarn/cache/linkify-it-npm-4.0.1-9c7d5a3cd6-3e0a299212.zip differ diff --git a/.yarn/cache/markdown-it-npm-13.0.2-4aeddbcb85-bb4bf2cb3e.zip b/.yarn/cache/markdown-it-npm-13.0.2-4aeddbcb85-bb4bf2cb3e.zip new file mode 100644 index 0000000000..be25ad8313 Binary files /dev/null and b/.yarn/cache/markdown-it-npm-13.0.2-4aeddbcb85-bb4bf2cb3e.zip differ diff --git a/.yarn/cache/mdurl-npm-1.0.1-054d974269-71731ecba9.zip b/.yarn/cache/mdurl-npm-1.0.1-054d974269-71731ecba9.zip new file mode 100644 index 0000000000..e8e8256e0c Binary files /dev/null and b/.yarn/cache/mdurl-npm-1.0.1-054d974269-71731ecba9.zip differ diff --git a/.yarn/cache/select-npm-1.1.2-13cd366fa2-4346151e94.zip b/.yarn/cache/select-npm-1.1.2-13cd366fa2-4346151e94.zip new file mode 100644 index 0000000000..7bae1c0ec1 Binary files /dev/null and b/.yarn/cache/select-npm-1.1.2-13cd366fa2-4346151e94.zip differ diff --git a/.yarn/cache/swiper-npm-10.3.1-76fd5fe27f-9a785930ca.zip b/.yarn/cache/swiper-npm-10.3.1-76fd5fe27f-9a785930ca.zip new file mode 100644 index 0000000000..d99ca9660f Binary files /dev/null and b/.yarn/cache/swiper-npm-10.3.1-76fd5fe27f-9a785930ca.zip differ diff --git a/.yarn/cache/tiny-emitter-npm-2.1.0-2a4d94f487-fbcfb51457.zip b/.yarn/cache/tiny-emitter-npm-2.1.0-2a4d94f487-fbcfb51457.zip new file mode 100644 index 0000000000..00d74e1bc4 Binary files /dev/null and b/.yarn/cache/tiny-emitter-npm-2.1.0-2a4d94f487-fbcfb51457.zip differ diff --git a/package.json b/package.json index e95835135b..e7ea452e3f 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@babel/preset-typescript": "^7.23.0", "@custom-elements-manifest/analyzer": "^0.8.3", "@esm-bundle/chai": "^4.3.4-fix.0", + "@fluentui-contrib/react-chat": "^0.1.7", "@microsoft/eslint-config-msgraph": "^2.0.0", "@octokit/rest": "^18.5.3", "@open-wc/testing": "^3.2.0", diff --git a/packages/mgt-chat/package.json b/packages/mgt-chat/package.json index 8d815a63cf..5e379f0c61 100644 --- a/packages/mgt-chat/package.json +++ b/packages/mgt-chat/package.json @@ -38,7 +38,9 @@ "author": "Microsoft", "license": "MIT", "devDependencies": { + "@types/markdown-it": "^13.0.2", "@types/react": "^17.0.0", + "adaptivecards-designer": "^2.4.3", "react": "^17.0.0", "react-dom": "^17.0.0", "react-scripts": "5.0.1", @@ -63,8 +65,11 @@ "@microsoft/microsoft-graph-types": "^2.0.0", "@microsoft/microsoft-graph-types-beta": "^0.16.0-preview", "@microsoft/signalr": "^7.0.4", + "adaptivecards": "^3.0.1", "immer": "^9.0.6", + "markdown-it": "^13.0.2", "opencrypto": "1.5.5", + "swiper": "^10.3.1", "uuid": "^9.0.0", "web-vitals": "^2.1.4" }, diff --git a/packages/mgt-chat/src/components/Chat/Chat.tsx b/packages/mgt-chat/src/components/Chat/Chat.tsx index 5cb5407318..441ab7f248 100644 --- a/packages/mgt-chat/src/components/Chat/Chat.tsx +++ b/packages/mgt-chat/src/components/Chat/Chat.tsx @@ -5,11 +5,11 @@ import { Person, PersonCardInteraction, Spinner } from '@microsoft/mgt-react'; import React, { useEffect, useState } from 'react'; import { StatefulGraphChatClient } from '../../statefulClient/StatefulGraphChatClient'; import { useGraphChatClient } from '../../statefulClient/useGraphChatClient'; -import { onRenderMessage } from '../../utils/chat'; +import { ChatHeader } from '../ChatHeader/ChatHeader'; import ChatMessageBar from '../ChatMessageBar/ChatMessageBar'; +import { onRenderMessage } from '../../utils/onRenderMessage'; import { renderMGTMention } from '../../utils/mentions'; import { registerAppIcons } from '../styles/registerIcons'; -import { ChatHeader } from '../ChatHeader/ChatHeader'; registerAppIcons(); @@ -23,7 +23,11 @@ const useStyles = makeStyles({ flexDirection: 'column', height: '100%', ...shorthands.overflow('auto'), - paddingBlockEnd: '12px' + paddingBlockEnd: '12px', + + '& p': { + ...shorthands.margin('unset') + } }, chatMessages: { height: 'auto', @@ -69,6 +73,37 @@ const messageThreadStyles: MessageThreadStyles = { chatContainer: { '& .ui-box': { zIndex: 'unset' + }, + '& .fui-ChatMessage': { + marginLeft: 'unset', + width: '100%' + }, + '& .fui-ChatMessage__author': { + fontWeight: 'var(--fontWeightSemibold)', + color: 'var(--colorNeutralForeground1)', + ...shorthands.margin('0px', '0px', 'var(--spacingVerticalXL)', '0px') + }, + '& .fui-ChatMessage__timestamp,.fui-ChatMessage__details': { + fontWeight: 'var(--fontWeightRegular)', + color: 'var(--colorNeutralForeground3)', + ...shorthands.margin('0px', '0px', 'var(--spacingVerticalXL)', '0px') + }, + '& .fui-ChatMyMessage': { + gridTemplateColumns: 'auto auto', + columnGap: 'unset' + }, + '& .fui-ChatMyMessage__body': { + background: '#c7e0f4' // No token found for this color, yet. + }, + '& .fui-ChatMyMessage__author': { + fontWeight: 'var(--fontWeightSemibold)', + color: 'var(--colorNeutralForeground1)', + ...shorthands.margin('0px', '0px', 'var(--spacingVerticalXL)', '0px') + }, + '& span.fui-ChatMyMessage__timestamp,.fui-ChatMyMessage__details': { + fontWeight: 'var(--fontWeightRegular)', + color: 'var(--colorNeutralForeground3)', + ...shorthands.margin('0px', '0px', 'var(--spacingVerticalXL)', '0px') } }, chatMessageContainer: { diff --git a/packages/mgt-chat/src/components/ChatContainer/ChatContainer.tsx b/packages/mgt-chat/src/components/ChatContainer/ChatContainer.tsx new file mode 100644 index 0000000000..c165a3c8dd --- /dev/null +++ b/packages/mgt-chat/src/components/ChatContainer/ChatContainer.tsx @@ -0,0 +1,80 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +import React from 'react'; +import { MessageProps, MessageRenderer } from '@azure/communication-react'; +import { getRelativeDisplayDate } from '@microsoft/mgt-components'; +import { messageContainer } from '../../utils/messageContainer'; +import { isChatMessage } from '../../utils/types'; + +interface MgtMessageContainerProps { + messageProps: MessageProps; + defaultOnRender?: MessageRenderer; +} + +const MgtMessageContainer = ({ messageProps, defaultOnRender }: MgtMessageContainerProps) => { + // TODO: find out how to render emojis + //

This is

+ if (isChatMessage(messageProps.message)) { + const author = messageProps.message?.senderDisplayName ?? ''; + const timestamp = getRelativeDisplayDate(new Date(messageProps.message.createdOn)); + const details = messageProps.message?.status ?? ''; + const body: string = messageProps.message?.content ?? ''; + const contentType = messageProps.message.contentType; + const Container = messageContainer(messageProps.message); + + console.log(messageProps); + switch (contentType) { + case 'text': + return ( + + {body} + + ); + case 'html': + case 'richtext/html': { + const bodyContent = processEmojiContent(body); + const html = { __html: bodyContent }; + return ( + +
+
+ ); + } + default: + return defaultOnRender ? defaultOnRender(messageProps) : <>; + } + } + return defaultOnRender ? defaultOnRender(messageProps) : <>; +}; + +/** + * Regex to detect and extract emoji alt text + * + * Pattern breakdown: + * (]+): Captures the opening emoji tag, including any attributes. + * alt=["'](\w*[^"']*)["']: Matches and captures the "alt" attribute value within single or double quotes. The value can contain word characters but not quotes. + * (.*[^>]): Captures any remaining text within the opening emoji tag, excluding the closing tag. + * : Matches the closing emoji tag. + */ +const emojiRegex = /(]+)alt=["'](\w*[^"']*)["'](.*[^>])<\/emoji>/; + +const emojiMatch = (messageContent: string): RegExpMatchArray | null => { + return messageContent.match(emojiRegex); +}; + +const processEmojiContent = (messageContent: string): string => { + let result = messageContent; + let match = emojiMatch(result); + while (match) { + result = result.replace(emojiRegex, '$2'); + match = emojiMatch(result); + } + return result; +}; + +export default MgtMessageContainer; diff --git a/packages/mgt-chat/src/components/MgtAdaptiveCard/MgtAdaptiveCard.tsx b/packages/mgt-chat/src/components/MgtAdaptiveCard/MgtAdaptiveCard.tsx new file mode 100644 index 0000000000..dc518b8335 --- /dev/null +++ b/packages/mgt-chat/src/components/MgtAdaptiveCard/MgtAdaptiveCard.tsx @@ -0,0 +1,152 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +import { MessageProps } from '@azure/communication-react'; +import { Action, AdaptiveCard, IMarkdownProcessingResult } from 'adaptivecards'; +import MarkdownIt from 'markdown-it'; +import React, { useEffect, useRef } from 'react'; +import { isChatMessage, isActionOpenUrl } from '../../utils/types'; +import { ChatMessageAttachment } from '@microsoft/microsoft-graph-types'; +import { FluentIcon } from '@fluentui/react-icons/lib/utils/createFluentIcon'; +import { Eye12Filled, Eye12Regular, Send16Filled, Send16Regular, bundleIcon } from '@fluentui/react-icons'; +import { + IAdaptiveCard, + ISubmitAction, + IOpenUrlAction, + IShowCardAction, + IExecuteAction +} from 'adaptivecards/lib/schema'; +import { messageContainer } from '../../utils/messageContainer'; +import { getRelativeDisplayDate } from '@microsoft/mgt-components'; + +type IAction = ISubmitAction | IOpenUrlAction | IShowCardAction | IExecuteAction; + +/** + * Props for an adaptive card message.s + */ +interface MgtAdaptiveCardProps { + attachments: ChatMessageAttachment[]; + defaultOnRender?: (props: MessageProps) => JSX.Element; + messageProps: MessageProps; +} + +/** + * TODO: find the correct icons, if needed. + * Message status icons. + */ +const detailsIcons: Record = { + seen: bundleIcon(Eye12Filled, Eye12Regular), + delivered: bundleIcon(Eye12Filled, Eye12Regular), + sending: bundleIcon(Send16Filled, Send16Regular), + failed: bundleIcon(Eye12Filled, Eye12Regular), + '': bundleIcon(Eye12Filled, Eye12Regular) +}; + +/** + * Render an adaptive card from the attachments + */ +const MgtAdaptiveCard = (msg: MgtAdaptiveCardProps) => { + const cardRef = useRef(null); + const attachments = msg.attachments; + const adaptiveCardAttachments = getAdaptiveCardAttachments(attachments); + useEffect(() => { + if (adaptiveCardAttachments.length) { + const cardElement = cardRef?.current; + // Remove all children before appending the attachment elements + while (cardElement?.firstChild) cardElement.removeChild(cardElement?.lastChild as Node); + for (const attachment of adaptiveCardAttachments) { + const cardHtmlElement = getHtmlElementFromAttachment(attachment); + cardElement?.appendChild(cardHtmlElement!); + } + } + }, [cardRef, adaptiveCardAttachments]); + const defaultOnRender = msg?.defaultOnRender; + const messageProps = msg.messageProps; + const defaultRender = defaultOnRender ? defaultOnRender(messageProps) : <>; + const Container = messageContainer(msg.messageProps.message); + const author = isChatMessage(msg.messageProps.message) ? msg.messageProps.message?.senderDisplayName : ''; + const timestamp = getRelativeDisplayDate(new Date(msg.messageProps.message.createdOn)); + const details = isChatMessage(msg.messageProps.message) ? msg.messageProps.message?.status : ''; + const DetailsIcon: FluentIcon = detailsIcons[details as string]; + + return adaptiveCardAttachments.length ? ( + }> +
+
+ ) : ( + defaultRender + ); +}; + +/** + * Filters out the adaptive card attachments. + * @param attachments + * @returns + */ +const getAdaptiveCardAttachments = (attachments: ChatMessageAttachment[]): ChatMessageAttachment[] => { + const cardAttachments: ChatMessageAttachment[] = []; + for (const att of attachments) { + const contentType = att?.contentType ?? ''; + if (contentType === 'application/vnd.microsoft.card.adaptive') { + cardAttachments.push(att); + } + } + return cardAttachments; +}; + +/** + * Process the attachment object and return an HTMLElement or nothing. + * @param attachment + * @returns + */ +const getHtmlElementFromAttachment = (attachment: ChatMessageAttachment | undefined): HTMLElement | undefined => { + const adaptiveCard = new AdaptiveCard(); + const adaptiveCardContentString: string = attachment?.content ?? ''; + const adaptiveCardContent = JSON.parse(adaptiveCardContentString) as IAdaptiveCard; + + // Check if the actions property has OpenUrl actions only + const actions = adaptiveCardContent?.actions?.filter(ac => ac.type === 'Action.OpenUrl'); + if (actions) { + // Update actions to only Action.OpenUrl actions. + adaptiveCardContent.actions = actions; + } + + // Check if the body has actionSet actions and filter for OpenUrl only + const actionSetArray = adaptiveCardContent?.body?.filter(ac => Object.values(ac).includes('ActionSet')); + if (actionSetArray) { + const finalInnerActions = []; + for (const actionSet of actionSetArray) { + const innerActions = actionSet?.actions as IAction[]; + const valid = innerActions?.filter(ac => ac?.type === 'Action.OpenUrl'); + if (valid) finalInnerActions.push(...valid); + } + + for (const b of adaptiveCardContent?.body ?? []) { + if (Object.values(b).includes('ActionSet')) { + b.actions = finalInnerActions; + } + } + } + + // markdown support + AdaptiveCard.onProcessMarkdown = (text: string, result: IMarkdownProcessingResult) => { + const md = new MarkdownIt(); + result.outputHtml = md.render(text); + result.didProcess = true; + }; + + adaptiveCard.parse(adaptiveCardContent); + adaptiveCard.onExecuteAction = (action: Action) => { + if (isActionOpenUrl(action)) { + const url: string = action?.url ?? ''; + window.open(url, '_blank', 'noopener,noreferrer'); + } + }; + return adaptiveCard.render(); +}; + +export default MgtAdaptiveCard; diff --git a/packages/mgt-chat/src/components/UnsupportedContent/UnsupportedContent.tsx b/packages/mgt-chat/src/components/UnsupportedContent/UnsupportedContent.tsx index cb59943ea7..0be3a2e28c 100644 --- a/packages/mgt-chat/src/components/UnsupportedContent/UnsupportedContent.tsx +++ b/packages/mgt-chat/src/components/UnsupportedContent/UnsupportedContent.tsx @@ -15,7 +15,8 @@ const useStyles = makeStyles({ boxShadow: '0px 4px 8px 0px rgba(0, 0, 0, 0.14), 0px 0px 2px 0px rgba(0, 0, 0, 0.12)', textDecorationLine: 'none', color: '#424242', - ...shorthands.margin('18px', '0px', '8px', '0px'), + alignItems: 'center', + ...shorthands.margin('0px', '0px', '2px', '0px'), ...shorthands.borderRadius('6px'), ...shorthands.padding('16px'), ...shorthands.gap('6px'), diff --git a/packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts b/packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts index 40c63b4630..2db2de126c 100644 --- a/packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts +++ b/packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts @@ -14,7 +14,7 @@ import { SendBoxProps, SystemMessage } from '@azure/communication-react'; -import { IDynamicPerson, getUserWithPhoto } from '@microsoft/mgt-components'; +import { getUserWithPhoto } from '@microsoft/mgt-components'; import { ActiveAccountChanged, IGraph, @@ -24,6 +24,7 @@ import { log, warn } from '@microsoft/mgt-element'; +import { IDynamicPerson } from '@microsoft/mgt-react'; import { GraphError } from '@microsoft/microsoft-graph-client'; import { AadUserConversationMember, @@ -175,6 +176,7 @@ type MessageEventType = * Extended Message type with additional properties. */ export type GraphChatMessage = Message & { + attachments?: ChatMessageAttachment[]; hasUnsupportedContent: boolean; rawChatUrl: string; }; @@ -917,7 +919,6 @@ detail: ${JSON.stringify(eventDetail)}`); * * @private * @param {(GraphChatMessage)} [message] - * @return {*} * @memberof StatefulGraphChatClient */ private updateMessages(message?: GraphChatMessage) { @@ -1049,10 +1050,22 @@ detail: ${JSON.stringify(eventDetail)}`); return content; } + /** + * Checks through a list of attachments if they are supported. It checks + * the content if it has unsupported text formats when there are no attachments. + * @param content to be rendered. + * @param attachments in the chat. + * @returns {boolean} + */ private hasUnsupportedContent(content: string, attachments: ChatMessageAttachment[]): boolean { const unsupportedContentTypes = [ 'application/vnd.microsoft.card.codesnippet', 'application/vnd.microsoft.card.fluid', + 'application/vnd.microsoft.card.list', + 'application/vnd.microsoft.card.hero', + 'application/vnd.microsoft.card.o365connector', + 'application/vnd.microsoft.card.receipt', + 'application/vnd.microsoft.card.thumbnail', 'application/vnd.microsoft.card.fluidEmbedCard', 'reference' ]; @@ -1079,10 +1092,10 @@ detail: ${JSON.stringify(eventDetail)}`); content: string ): GraphChatMessage { const senderId = graphMessage.from?.user?.id || undefined; + const attachments = graphMessage?.attachments ?? []; const chatId = graphMessage?.chatId ?? ''; const id = graphMessage?.id ?? ''; const chatUrl = `https://teams.microsoft.com/l/message/${chatId}/${id}?context={"contextType":"chat"}`; - const attachments = graphMessage?.attachments ?? []; let messageData: GraphChatMessage = { messageId, @@ -1096,6 +1109,7 @@ detail: ${JSON.stringify(eventDetail)}`); mine: senderId === currentUser, status: 'seen', attached: 'top', + attachments, hasUnsupportedContent: this.hasUnsupportedContent(content, attachments), rawChatUrl: chatUrl }; diff --git a/packages/mgt-chat/src/utils/chat.tsx b/packages/mgt-chat/src/utils/chat.tsx deleted file mode 100644 index 67a73277a7..0000000000 --- a/packages/mgt-chat/src/utils/chat.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { MessageProps, MessageRenderer } from '@azure/communication-react'; -import produce from 'immer'; -import React from 'react'; -import { renderToString } from 'react-dom/server'; -import UnsupportedContent from '../components/UnsupportedContent/UnsupportedContent'; -import { isChatMessage, isGraphChatMessage } from '../utils/types'; - -/** - * Renders the preferred content depending on whether it is supported. - * - * @param messageProps final message values from the state. - * @param defaultOnRender default component to render content. - * @returns - */ -const onRenderMessage = (messageProps: MessageProps, defaultOnRender?: MessageRenderer) => { - const message = messageProps?.message; - if (isGraphChatMessage(message) && message?.hasUnsupportedContent) { - const unsupportedContentComponent = ; - messageProps = produce(messageProps, (draft: MessageProps) => { - if (isChatMessage(draft.message)) { - draft.message.content = renderToString(unsupportedContentComponent); - } - }); - } - - return defaultOnRender ? defaultOnRender(messageProps) : <>; -}; -export { onRenderMessage }; diff --git a/packages/mgt-chat/src/utils/messageContainer.tsx b/packages/mgt-chat/src/utils/messageContainer.tsx new file mode 100644 index 0000000000..f23cb7a471 --- /dev/null +++ b/packages/mgt-chat/src/utils/messageContainer.tsx @@ -0,0 +1,20 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +import { Message } from '@azure/communication-react'; +import { ChatMessage, ChatMyMessage } from '@fluentui-contrib/react-chat'; +import { isChatMessage } from './types'; +/** + * Determine which message container to render. By default use the ChatMessage. + * @param msg is the Message + */ +export const messageContainer = (msg: Message) => { + if (isChatMessage(msg) && msg?.mine) { + return ChatMyMessage; + } + return ChatMessage; +}; diff --git a/packages/mgt-chat/src/utils/onRenderMessage.tsx b/packages/mgt-chat/src/utils/onRenderMessage.tsx new file mode 100644 index 0000000000..a626538dd2 --- /dev/null +++ b/packages/mgt-chat/src/utils/onRenderMessage.tsx @@ -0,0 +1,54 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +import { MessageProps, MessageRenderer } from '@azure/communication-react'; +import produce from 'immer'; +import React from 'react'; +import { isChatMessage, isGraphChatMessage } from './types'; + +import { renderToString } from 'react-dom/server'; +import MgtMessageContainer from '../components/ChatContainer/ChatContainer'; +import MgtAdaptiveCard from '../components/MgtAdaptiveCard/MgtAdaptiveCard'; +import UnsupportedContent from '../components/UnsupportedContent/UnsupportedContent'; +/** + * This is a _dirty_ hack to bundle the teams light CSS used in adaptive cards + * designer. + * THOUGHT: import this on demand based on the set theme? + */ +import 'adaptivecards-designer/dist/containers/teams-container-light.css'; + +/** + * Renders the preferred content depending on whether it is supported. + * + * @param messageProps final message values from the state. + * @param defaultOnRender default component to render content. + * @returns + */ +const onRenderMessage = (messageProps: MessageProps, defaultOnRender?: MessageRenderer) => { + const message = messageProps?.message; + if (isGraphChatMessage(message)) { + const attachments = message?.attachments ?? []; + + if (message?.hasUnsupportedContent) { + const unsupportedContentComponent = ; + messageProps = produce(messageProps, (draft: MessageProps) => { + if (isChatMessage(draft.message)) { + draft.message.content = renderToString(unsupportedContentComponent); + } + }); + } else if (attachments.length) { + return ( + + ); + } + } + + return ; + // return defaultOnRender ? defaultOnRender(messageProps) : <>; +}; + +export { onRenderMessage }; diff --git a/packages/mgt-chat/src/utils/types.ts b/packages/mgt-chat/src/utils/types.ts index 6f2c5f1eda..18ebc2a997 100644 --- a/packages/mgt-chat/src/utils/types.ts +++ b/packages/mgt-chat/src/utils/types.ts @@ -7,11 +7,12 @@ import { ChatMessage, Message } from '@azure/communication-react'; import { GraphChatMessage } from 'src/statefulClient/StatefulGraphChatClient'; +import { Action, OpenUrlAction } from 'adaptivecards'; /** * A typeguard to get the ChatMessage type * @param msg of Message - * @returns ChatMessage + * @returns {ChatMessage} */ export const isChatMessage = (msg: Message): msg is ChatMessage => { return 'content' in msg; @@ -20,8 +21,17 @@ export const isChatMessage = (msg: Message): msg is ChatMessage => { /** * A typeguard to get the GraphChatMessage type * @param msg of Message - * @returns GraphChatMessage + * @returns {GraphChatMessage} */ export const isGraphChatMessage = (msg: Message): msg is GraphChatMessage => { - return 'content' in msg && 'hasUnsupportedContent' in msg && 'rawChatUrl' in msg; + return 'content' in msg && 'hasUnsupportedContent' in msg && 'rawChatUrl' in msg && 'attachments' in msg; +}; + +/** + * A typeguard to get the OpenUrlAction type + * @param o of OpenUrlAction + * @returns {OpenUrlAction} + */ +export const isActionOpenUrl = (o: Action): o is OpenUrlAction => { + return 'url' in o; }; diff --git a/packages/mgt-components/src/utils/Utils.tests.ts b/packages/mgt-components/src/utils/Utils.tests.ts new file mode 100644 index 0000000000..071ed138b2 --- /dev/null +++ b/packages/mgt-components/src/utils/Utils.tests.ts @@ -0,0 +1,34 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + +import { getRelativeDisplayDate } from './Utils'; +import { expect } from '@open-wc/testing'; + +describe('Utils - getRelativeDisplayDate', () => { + it('should render the date today in AM format', async () => { + const today = new Date(); + today.setHours(10, 10); + const result = getRelativeDisplayDate(today); + await expect(result).equal('10:10 AM'); + }); + + it('should render the date today in PM format', async () => { + const today = new Date(); + today.setHours(15, 10); + const result = getRelativeDisplayDate(today); + await expect(result).equal('3:10 PM'); + }); + + it('should show the date in more than two weeks ago format', async () => { + const today = new Date(); + const twoWeeksAgo = today.getDate() - 15; + today.setMonth(1); + today.setDate(twoWeeksAgo); + const result = getRelativeDisplayDate(today); + await expect(result).equal('1/24/2023'); + }); +}); diff --git a/yarn.lock b/yarn.lock index 98bd5ab0d5..da61a69412 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2940,6 +2940,22 @@ __metadata: languageName: node linkType: hard +"@fluentui-contrib/react-chat@npm:^0.1.7": + version: 0.1.7 + resolution: "@fluentui-contrib/react-chat@npm:0.1.7" + dependencies: + "@swc/helpers": ~0.5.1 + peerDependencies: + "@fluentui/react-components": ">=9.25.1 <10.0.0" + "@fluentui/react-icons": ">=2.0.204 <3.0.0" + "@types/react": ">=16.8.0 <19.0.0" + "@types/react-dom": ">=16.8.0 <19.0.0" + react: ">=16.8.0 <19.0.0" + react-dom: ">=16.8.0 <19.0.0" + checksum: 41b1a4be48409d1bfe15f881cf5b6c104b4f445c94e3f5a1d0fb08fa15c8e0f0bc405782c66ac8cf79b27776b2420d800fac866e564befbe4cb093816482a0e9 + languageName: node + linkType: hard + "@fluentui/accessibility@npm:^0.66.5": version: 0.66.5 resolution: "@fluentui/accessibility@npm:0.66.5" @@ -5947,12 +5963,17 @@ __metadata: "@microsoft/microsoft-graph-types": ^2.0.0 "@microsoft/microsoft-graph-types-beta": ^0.16.0-preview "@microsoft/signalr": ^7.0.4 + "@types/markdown-it": ^13.0.2 "@types/react": ^17.0.0 + adaptivecards: ^3.0.1 + adaptivecards-designer: ^2.4.3 immer: ^9.0.6 + markdown-it: ^13.0.2 opencrypto: 1.5.5 react: ^17.0.0 react-dom: ^17.0.0 react-scripts: 5.0.1 + swiper: ^10.3.1 typescript: ^4.9.5 uuid: ^9.0.0 web-vitals: ^2.1.4 @@ -9786,7 +9807,7 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:^0.5.1": +"@swc/helpers@npm:^0.5.1, @swc/helpers@npm:~0.5.1": version: 0.5.3 resolution: "@swc/helpers@npm:0.5.3" dependencies: @@ -10430,6 +10451,13 @@ __metadata: languageName: node linkType: hard +"@types/linkify-it@npm:*": + version: 3.0.3 + resolution: "@types/linkify-it@npm:3.0.3" + checksum: a734becc4e7476833b0e6951ec133c006a34809639c722d3e28b7cf88f5f6ccbb433f195788be5e56209b1e9e6e0778879291dd2db401acee3bb585c44dcc329 + languageName: node + linkType: hard + "@types/lodash@npm:4.14.117": version: 4.14.117 resolution: "@types/lodash@npm:4.14.117" @@ -10444,6 +10472,16 @@ __metadata: languageName: node linkType: hard +"@types/markdown-it@npm:^13.0.2": + version: 13.0.2 + resolution: "@types/markdown-it@npm:13.0.2" + dependencies: + "@types/linkify-it": "*" + "@types/mdurl": "*" + checksum: fe1f6a12ee8ad2246359376431a30d22c9b603e63e93e3e27d6920840934b9764034679a4d0b01ec54b0693c8d5c42012ec34715cba4f5b0736b8a4b66db4c74 + languageName: node + linkType: hard + "@types/mdast@npm:^3.0.0": version: 3.0.14 resolution: "@types/mdast@npm:3.0.14" @@ -10453,6 +10491,13 @@ __metadata: languageName: node linkType: hard +"@types/mdurl@npm:*": + version: 1.0.3 + resolution: "@types/mdurl@npm:1.0.3" + checksum: 5bbed4f0eb9f60040fa26be77aa2158ca468b6423876cec0d2043e7f8298e83b8e5b95fb66056327b02d747c4d376aed16c11ff3fdc4cb3dca327a6931a71f18 + languageName: node + linkType: hard + "@types/mdx@npm:^2.0.0": version: 2.0.9 resolution: "@types/mdx@npm:2.0.9" @@ -12851,6 +12896,37 @@ __metadata: languageName: node linkType: hard +"adaptivecards-controls@npm:^0.10.1": + version: 0.10.1 + resolution: "adaptivecards-controls@npm:0.10.1" + checksum: a6bec984894e35f647b3f6ccdcccbf4d8e263e0970534c007a268e2c044c90a63cb18c43d3c72d953760dfd0d261fcad925527e85941889606028095a7375874 + languageName: node + linkType: hard + +"adaptivecards-designer@npm:^2.4.3": + version: 2.4.3 + resolution: "adaptivecards-designer@npm:2.4.3" + dependencies: + adaptivecards-controls: ^0.10.1 + clipboard: ^2.0.1 + peerDependencies: + adaptive-expressions: ^4.11.0 + adaptivecards: ^2.10.0 + adaptivecards-templating: ^2.2.0 + monaco-editor: ^0.29.1 + checksum: 0428c2eb9458f14d6b9f3e61dc9ae4670fec65bb35ecca06c35012845a6c23f4183b8b4982663e8515f88d2c148d9a991e5fa7df349781fbae3ca232d87b85bd + languageName: node + linkType: hard + +"adaptivecards@npm:^3.0.1": + version: 3.0.1 + resolution: "adaptivecards@npm:3.0.1" + peerDependencies: + swiper: ^8.2.6 + checksum: b87c2ba37d129dc1f732c3101029b670d0f2b39c0b672f67307e5a46a0b2ea6aa780b43451b8819275f51d48d8b0156fa80b424ba9b3cee8e1806b3b93de9c28 + languageName: node + linkType: hard + "add-stream@npm:^1.0.0": version: 1.0.0 resolution: "add-stream@npm:1.0.0" @@ -15854,6 +15930,17 @@ __metadata: languageName: node linkType: hard +"clipboard@npm:^2.0.1": + version: 2.0.11 + resolution: "clipboard@npm:2.0.11" + dependencies: + good-listener: ^1.2.2 + select: ^1.1.2 + tiny-emitter: ^2.0.0 + checksum: 413055a6038e43898e0e895216b58ed54fbf386f091cb00188875ef35b186cefbd258acdf4cb4b0ac87cbc00de936f41b45dde9fe1fd1a57f7babb28363b8748 + languageName: node + linkType: hard + "cliui@npm:^3.2.0": version: 3.2.0 resolution: "cliui@npm:3.2.0" @@ -17835,6 +17922,13 @@ __metadata: languageName: node linkType: hard +"delegate@npm:^3.1.2": + version: 3.2.0 + resolution: "delegate@npm:3.2.0" + checksum: d943058fe05897228b158cbd1bab05164df28c8f54127873231d6b03b0a5acc1b3ee1f98ac70ccc9b79cd84aa47118a7de111fee2923753491583905069da27d + languageName: node + linkType: hard + "delegates@npm:^1.0.0": version: 1.0.0 resolution: "delegates@npm:1.0.0" @@ -18620,6 +18714,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:~3.0.1": + version: 3.0.1 + resolution: "entities@npm:3.0.1" + checksum: aaf7f12033f0939be91f5161593f853f2da55866db55ccbf72f45430b8977e2b79dbd58c53d0fdd2d00bd7d313b75b0968d09f038df88e308aa97e39f9456572 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -21925,6 +22026,15 @@ __metadata: languageName: node linkType: hard +"good-listener@npm:^1.2.2": + version: 1.2.2 + resolution: "good-listener@npm:1.2.2" + dependencies: + delegate: ^3.1.2 + checksum: f39fb82c4e41524f56104cfd2d7aef1a88e72f3f75139115fbdf98cc7d844e0c1b39218b2e83438c6188727bf904ed78c7f0f2feff67b32833bc3af7f0202b33 + languageName: node + linkType: hard + "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -26569,6 +26679,15 @@ __metadata: languageName: node linkType: hard +"linkify-it@npm:^4.0.1": + version: 4.0.1 + resolution: "linkify-it@npm:4.0.1" + dependencies: + uc.micro: ^1.0.1 + checksum: 3e0a29921269c14eb7ac6f5db2da68d4854ea9acca6e9014a323f75f2dd39b197ffab57c1fbd6a906ceb021aad3ee6d7ba7d0181236dd9630ffc452b392f7f71 + languageName: node + linkType: hard + "lit-element@npm:^2.2.1": version: 2.5.1 resolution: "lit-element@npm:2.5.1" @@ -27391,6 +27510,21 @@ __metadata: languageName: node linkType: hard +"markdown-it@npm:^13.0.2": + version: 13.0.2 + resolution: "markdown-it@npm:13.0.2" + dependencies: + argparse: ^2.0.1 + entities: ~3.0.1 + linkify-it: ^4.0.1 + mdurl: ^1.0.1 + uc.micro: ^1.0.5 + bin: + markdown-it: bin/markdown-it.js + checksum: bb4bf2cb3e5d77a7f3dc9cf48e17d050fbcd26d37992204eaa5812220752858fe9debe439b2ae1de06e749a3bba537c0baf6ce7510307cf7163a70f04fafe672 + languageName: node + linkType: hard + "markdown-table@npm:^3.0.0": version: 3.0.3 resolution: "markdown-table@npm:3.0.3" @@ -27634,6 +27768,13 @@ __metadata: languageName: node linkType: hard +"mdurl@npm:^1.0.1": + version: 1.0.1 + resolution: "mdurl@npm:1.0.1" + checksum: 71731ecba943926bfbf9f9b51e28b5945f9411c4eda80894221b47cc105afa43ba2da820732b436f0798fd3edbbffcd1fc1415843c41a87fea08a41cc1e3d02b + languageName: node + linkType: hard + "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -34538,6 +34679,7 @@ __metadata: "@babel/preset-typescript": ^7.23.0 "@custom-elements-manifest/analyzer": ^0.8.3 "@esm-bundle/chai": ^4.3.4-fix.0 + "@fluentui-contrib/react-chat": ^0.1.7 "@microsoft/eslint-config-msgraph": ^2.0.0 "@octokit/rest": ^18.5.3 "@open-wc/testing": ^3.2.0 @@ -34929,6 +35071,13 @@ __metadata: languageName: node linkType: hard +"select@npm:^1.1.2": + version: 1.1.2 + resolution: "select@npm:1.1.2" + checksum: 4346151e94f226ea6131e44e68e6d837f3fdee64831b756dd657cc0b02f4cb5107f867cb34a1d1216ab7737d0bf0645d44546afb030bbd8d64e891f5e4c4814e + languageName: node + linkType: hard + "selfsigned@npm:^2.0.1, selfsigned@npm:^2.1.1": version: 2.4.1 resolution: "selfsigned@npm:2.4.1" @@ -36757,6 +36906,13 @@ __metadata: languageName: node linkType: hard +"swiper@npm:^10.3.1": + version: 10.3.1 + resolution: "swiper@npm:10.3.1" + checksum: 9a785930ca0ab0683d7e5e116c25a38e1f8818d6f9e93848125bdf3e1874e12a6c3b9f4e9c54668e4accc2440a6f031de98cfc01a1b0068c77f8de4115034e4d + languageName: node + linkType: hard + "symbol-tree@npm:^3.2.2, symbol-tree@npm:^3.2.4": version: 3.2.4 resolution: "symbol-tree@npm:3.2.4" @@ -37254,6 +37410,13 @@ __metadata: languageName: node linkType: hard +"tiny-emitter@npm:^2.0.0": + version: 2.1.0 + resolution: "tiny-emitter@npm:2.1.0" + checksum: fbcfb5145751a0e3b109507a828eb6d6d4501352ab7bb33eccef46e22e9d9ad3953158870a6966a59e57ab7c3f9cfac7cab8521db4de6a5e757012f4677df2dd + languageName: node + linkType: hard + "tiny-invariant@npm:^1.3.1": version: 1.3.1 resolution: "tiny-invariant@npm:1.3.1" @@ -37968,7 +38131,7 @@ __metadata: languageName: node linkType: hard -"uc.micro@npm:^1.0.1": +"uc.micro@npm:^1.0.1, uc.micro@npm:^1.0.5": version: 1.0.6 resolution: "uc.micro@npm:1.0.6" checksum: 6898bb556319a38e9cf175e3628689347bd26fec15fc6b29fa38e0045af63075ff3fea4cf1fdba9db46c9f0cbf07f2348cd8844889dd31ebd288c29fe0d27e7a