Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: render adaptive cards #2746

Open
wants to merge 40 commits into
base: next/mgt-chat
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3427a73
Add adaptive cards and markdownit to render md
musale Oct 3, 2023
69abaed
Render adaptive cards in a chat
musale Oct 3, 2023
2134f17
Render only Action.OpenUrl type of cards
musale Oct 3, 2023
a6c449d
Add types for markdown-it
musale Oct 3, 2023
84b898d
Merge branch 'next/mgt-chat' into adaptive-cards
musale Oct 5, 2023
7594315
WIP: redo rendering the cards from the state to the chat component
musale Oct 6, 2023
b04905b
Merge branch 'next/mgt-chat' into adaptive-cards
musale Oct 12, 2023
c56bc11
Use hooks to render the adaptive card
musale Oct 19, 2023
39d26a5
Merge branch 'next/mgt-chat' into adaptive-cards
musale Oct 19, 2023
9e44636
Merge chat.tsx to onRenderMessage.tsx'
musale Oct 19, 2023
c6ac564
Fine tune the adaptive cards rendered
musale Oct 23, 2023
6c137c4
Remove unnecessary assertions
musale Oct 24, 2023
4379e2f
Fix naming and imports issues
musale Oct 24, 2023
03da770
Update the artifacts
musale Oct 26, 2023
992c356
Merge branch 'next/mgt-chat' into adaptive-cards
musale Oct 26, 2023
deb0051
Render multiple cards in a message and clear div before append
musale Oct 26, 2023
d18846d
Add react-components to the mgt-chat package
musale Oct 31, 2023
062cc3c
Add ChatMessage and ChatMyMessage as wrappers around adaptive card me…
musale Oct 31, 2023
0789365
Add the chat message header props with styling
musale Oct 31, 2023
726cd3d
Merge branch 'next/mgt-chat' into adaptive-cards
musale Oct 31, 2023
dd13a48
Add copyright header text on new files
musale Oct 31, 2023
bfe432d
Update the string descriptions and fix tests
musale Oct 31, 2023
b12f680
Add adaptivecards-designer
musale Nov 1, 2023
8a20450
Add teams light as the default CSS styling
musale Nov 1, 2023
a38da05
Add styling for my chat messages
musale Nov 1, 2023
47af024
Add message details prop to the container
musale Nov 1, 2023
28d56e9
WIP: set icons for status
musale Nov 2, 2023
afebe03
Merge branch 'next/mgt-chat' into adaptive-cards
musale Nov 7, 2023
740d3dd
Fix tests for displayDates
musale Nov 7, 2023
30e12d7
Move Mgt adaptive cards component to it's pwn file
musale Nov 8, 2023
3b7562f
Move message container to it's own file
musale Nov 8, 2023
87d3f0a
WIP: find icons
musale Nov 8, 2023
a84a063
Revert to default rendering for the uncustomized messages
musale Nov 8, 2023
cad5fc7
Remove displayDates util and use utils in mgt-component
musale Nov 8, 2023
5f13cc3
Add tests for getRelativeDisplayDate
musale Nov 8, 2023
7f7dab1
Merge branch 'next/mgt-chat' into adaptive-cards
sebastienlevert Nov 14, 2023
cd86198
Move MgtMessageContainer to it's own file
musale Nov 14, 2023
ea55ed4
WIP: render emojis
musale Nov 14, 2023
fe88388
Merge branch 'adaptive-cards' of github.com:microsoftgraph/microsoft-…
musale Nov 14, 2023
ce90d31
Merge branch 'next/mgt-chat' into adaptive-cards
gavinbarron Nov 15, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified .yarn/install-state.gz
Binary file not shown.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,5 +162,8 @@
"resolutions": {
"responselike": "2.0.0"
},
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"dependencies": {
"@types/markdown-it": "^13.0.2"
musale marked this conversation as resolved.
Show resolved Hide resolved
}
}
3 changes: 3 additions & 0 deletions packages/mgt-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,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"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/mgt-chat/src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ 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 { ManageChatMembers } from '../ManageChatMembers/ManageChatMembers';
import { onRenderMessage } from '../../utils/onRenderMessage';
import { renderMGTMention } from '../../utils/mentions';
import { registerAppIcons } from '../styles/registerIcons';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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',
alignItems: 'center',
...shorthands.margin('18px', '0px', '8px', '0px'),
...shorthands.borderRadius('6px'),
...shorthands.padding('16px'),
Expand Down
23 changes: 19 additions & 4 deletions packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -174,6 +175,7 @@ type MessageEventType =
* Extended Message type with additional properties.
*/
export type GraphChatMessage = Message & {
attachments?: ChatMessageAttachment[];
hasUnsupportedContent: boolean;
rawChatUrl: string;
};
Expand Down Expand Up @@ -626,7 +628,7 @@ class StatefulGraphChatClient implements StatefulClient<GraphChatClient> {
// TODO: move this default case to a console.warn before release and emit an empty message
// it's here to help us catch messages we have't handled yet
default:
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-call
musale marked this conversation as resolved.
Show resolved Hide resolved
warn(`Unknown system message type ${eventDetail['@odata.type']}

detail: ${JSON.stringify(eventDetail)}`);
Expand Down Expand Up @@ -882,6 +884,7 @@ detail: ${JSON.stringify(eventDetail)}`);
// TODO handle the case where there were a lot of missed messages and we ned to get the next page of messages.
// This is not a common case, but we should handle it.
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
musale marked this conversation as resolved.
Show resolved Hide resolved
log('checked for missed messages');
};

Expand Down Expand Up @@ -917,7 +920,6 @@ detail: ${JSON.stringify(eventDetail)}`);
*
* @private
* @param {(GraphChatMessage)} [message]
* @return {*}
* @memberof StatefulGraphChatClient
*/
private updateMessages(message?: GraphChatMessage) {
Expand Down Expand Up @@ -1067,10 +1069,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',
'reference'
];
const isUnsupported: boolean[] = [];
Expand All @@ -1096,10 +1110,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,
Expand All @@ -1113,6 +1127,7 @@ detail: ${JSON.stringify(eventDetail)}`);
mine: senderId === currentUser,
status: 'seen',
attached: 'top',
attachments,
hasUnsupportedContent: this.hasUnsupportedContent(content, attachments),
rawChatUrl: chatUrl
};
Expand Down
28 changes: 0 additions & 28 deletions packages/mgt-chat/src/utils/chat.tsx

This file was deleted.

153 changes: 153 additions & 0 deletions packages/mgt-chat/src/utils/onRenderMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/**
* -------------------------------------------------------------------------------------------
* 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 * as AdaptiveCards from 'adaptivecards';
musale marked this conversation as resolved.
Show resolved Hide resolved
import MarkdownIt from 'markdown-it';
import React, { useEffect, useRef } from 'react';
import { isGraphChatMessage, isActionOpenUrl, isChatMessage } from './types';
import {
IAdaptiveCard,
ISubmitAction,
IOpenUrlAction,
IShowCardAction,
IExecuteAction
} from 'adaptivecards/lib/schema';
import { ChatMessageAttachment } from '@microsoft/microsoft-graph-types';
import produce from 'immer';
import { renderToString } from 'react-dom/server';
import UnsupportedContent from '../components/UnsupportedContent/UnsupportedContent';

type IAction = ISubmitAction | IOpenUrlAction | IShowCardAction | IExecuteAction;

/**
* Props for an adaptive card message.s
*/
interface MGTAdaptiveCardProps {
musale marked this conversation as resolved.
Show resolved Hide resolved
attachments: ChatMessageAttachment[];
defaultOnRender?: (props: MessageProps) => JSX.Element;
messageProps: MessageProps;
}

/**
* Render an adaptive card from the attachments
*/
const MGTAdaptiveCard = (msg: MGTAdaptiveCardProps) => {
musale marked this conversation as resolved.
Show resolved Hide resolved
const cardRef = useRef<HTMLDivElement | null>(null);
const attachments = msg.attachments;
const adaptiveCardAttachment = getAdaptiveCardAttachment(attachments);
musale marked this conversation as resolved.
Show resolved Hide resolved
useEffect(() => {
if (adaptiveCardAttachment) {
const cardHtmlElement = getHtmlElementFromAttachment(adaptiveCardAttachment);
cardRef?.current?.appendChild(cardHtmlElement!);
musale marked this conversation as resolved.
Show resolved Hide resolved
}
}, [cardRef, adaptiveCardAttachment]);
const defaultOnRender = msg?.defaultOnRender;
const messageProps = msg.messageProps;
const defaultRender = defaultOnRender ? defaultOnRender(messageProps) : <></>;
return adaptiveCardAttachment ? <div ref={cardRef}></div> : defaultRender;
};

/**
* 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 = <UnsupportedContent targetUrl={message.rawChatUrl} />;
messageProps = produce(messageProps, (draft: MessageProps) => {
if (isChatMessage(draft.message)) {
draft.message.content = renderToString(unsupportedContentComponent);
}
});
} else if (attachments.length) {
return (
<MGTAdaptiveCard attachments={attachments} defaultOnRender={defaultOnRender} messageProps={messageProps} />
musale marked this conversation as resolved.
Show resolved Hide resolved
);
}
}
return defaultOnRender ? defaultOnRender(messageProps) : <></>;
};

/**
* Filters out the adaptive card attachments.
* @param attachments
* @returns
*/
const getAdaptiveCardAttachment = (attachments: ChatMessageAttachment[]): ChatMessageAttachment | undefined => {
for (const att of attachments) {
const contentType = att?.contentType ?? '';
if (contentType === 'application/vnd.microsoft.card.adaptive') {
return att;
}
}
return;
};

/**
* Process the attachment object and return an HTMLElement or nothing.
* @param attachment
* @returns
*/
const getHtmlElementFromAttachment = (attachment: ChatMessageAttachment | undefined): HTMLElement | undefined => {
/* eslint-disable
@typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-member-access,
@typescript-eslint/no-unsafe-call, react-hooks/rules-of-hooks */
musale marked this conversation as resolved.
Show resolved Hide resolved
const adaptiveCard = new AdaptiveCards.AdaptiveCard();
musale marked this conversation as resolved.
Show resolved Hide resolved
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
AdaptiveCards.AdaptiveCard.onProcessMarkdown = (text: string, result: AdaptiveCards.IMarkdownProcessingResult) => {
musale marked this conversation as resolved.
Show resolved Hide resolved
const md = new MarkdownIt();
result.outputHtml = md.render(text);
result.didProcess = true;
};

adaptiveCard.parse(adaptiveCardContent);
adaptiveCard.onExecuteAction = (action: AdaptiveCards.Action) => {
musale marked this conversation as resolved.
Show resolved Hide resolved
if (isActionOpenUrl(action)) {
const url: string = action?.url ?? '';
window.open(url, '_blank', 'noopener,noreferrer');
}
};
return adaptiveCard.render();
};

export { onRenderMessage };
16 changes: 13 additions & 3 deletions packages/mgt-chat/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
};
Loading