Skip to content

Commit

Permalink
feat: render cta for unsupported content (#2663)
Browse files Browse the repository at this point in the history
detect and display custom message for unsupported content
add a targetUrl for the unsupported content
set the message signals for unsupported content
set the unsupported content component in Chat

---------

Signed-off-by: Musale Martin <[email protected]>
Signed-off-by: Martin Musale <[email protected]>
Co-authored-by: Gavin Barron <[email protected]>
  • Loading branch information
musale and gavinbarron authored Oct 12, 2023
1 parent 74a7608 commit 3c00710
Show file tree
Hide file tree
Showing 92 changed files with 767 additions and 613 deletions.
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 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 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 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 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 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 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 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 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 added .yarn/install-state.gz
Binary file not shown.
2 changes: 1 addition & 1 deletion packages/mgt-chat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"@azure/msal-browser": "2.33.0",
"@fluentui/react": "~8.106.1",
"@fluentui/react-components": "^9.19.1",
"@fluentui/react-icons": "^2.0.200",
"@fluentui/react-icons": "^2.0.210",
"@fluentui/react-northstar": "^0.66.4",
"@microsoft/mgt-components": "*",
"@microsoft/mgt-element": "*",
Expand Down
13 changes: 9 additions & 4 deletions packages/mgt-chat/src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React, { useEffect, useState } from 'react';
import { ErrorBar, FluentThemeProvider, MessageThread, SendBox } from '@azure/communication-react';
import { Person, PersonCardInteraction, Spinner } from '@microsoft/mgt-react';
import { FluentTheme, MessageBarType } from '@fluentui/react';
import { FluentProvider, makeStyles, shorthands, teamsLightTheme } from '@fluentui/react-components';
import { Person, PersonCardInteraction, Spinner } from '@microsoft/mgt-react';
import React, { useEffect, useState } from 'react';
import { StatefulGraphChatClient } from 'src/statefulClient/StatefulGraphChatClient';
import { useGraphChatClient } from '../../statefulClient/useGraphChatClient';
import { onRenderMessage } from '../../utils/chat';
import ChatHeader from '../ChatHeader/ChatHeader';
import ChatMessageBar from '../ChatMessageBar/ChatMessageBar';
import { registerAppIcons } from '../styles/registerIcons';
import { ManageChatMembers } from '../ManageChatMembers/ManageChatMembers';
import { StatefulGraphChatClient } from 'src/statefulClient/StatefulGraphChatClient';
import { registerAppIcons } from '../styles/registerIcons';

registerAppIcons();

Expand Down Expand Up @@ -42,6 +43,9 @@ const useStyles = makeStyles({
display: 'flex',
alignItems: 'center',
height: '100%'
},
unsupportedContent: {
color: 'red'
}
});

Expand Down Expand Up @@ -100,6 +104,7 @@ export const Chat = ({ chatId }: IMgtChatProps) => {
<Person userId={userId} avatarSize="small" personCardInteraction={PersonCardInteraction.hover} />
);
}}
onRenderMessage={onRenderMessage}
/>
</div>
<div className={styles.chatInput}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { List, ListItem } from '@fluentui/react-northstar';
import { Person, PersonViewType } from '@microsoft/mgt-react';
import { AadUserConversationMember } from '@microsoft/microsoft-graph-types';
import { styles } from './manage-chat-members.styles';
import { Dismiss24Regular, bundleIcon } from '@fluentui/react-icons';
import { Dismiss24Regular, Dismiss24Filled, bundleIcon } from '@fluentui/react-icons';

interface ListChatMembersProps {
currentUserId: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { makeStyles, shorthands } from '@fluentui/react-components';
import React from 'react';
import { ArrowSquareUpRight24Regular } from '@fluentui/react-icons';

const useStyles = makeStyles({
container: {
backgroundColor: '#ebebeb',
display: 'flex',
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'),
...shorthands.borderRadius('6px'),
...shorthands.padding('16px'),
...shorthands.gap('6px'),
':hover': {
backgroundColor: '#fafafa'
},
':visited': {
color: '#424242'
}
},
cta: {
fontFamily: 'Segoe UI',
fontSize: '12px',
fontStyle: 'normal',
fontWeight: '400',
lineHeight: '16px',
textDecorationLine: 'none'
}
});

interface UnsupportedContentProps {
targetUrl: string;
}

const UnsupportedContent = (props: UnsupportedContentProps) => {
const styles = useStyles();
return (
<a className={styles.container} target="blank" href={props.targetUrl}>
<ArrowSquareUpRight24Regular />
<p className={styles.cta}>View this message in Microsoft Teams.</p>
</a>
);
};

export default UnsupportedContent;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { CacheItem, CacheService } from '@microsoft/mgt-react';
* Defines the expiration time
*/
const getConversationCacheInvalidationTime = (): number => {
return CacheService.config.conversation.invalidationPeriod || CacheService.config.defaultInvalidationPeriod;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const conversation = CacheService.config.conversation;
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return conversation.invalidationPeriod || CacheService.config.defaultInvalidationPeriod;
};

export const cacheEntryIsValid = (cacheEntry: CacheItem | null) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { CacheService } from '@microsoft/mgt-react';

export const isConversationCacheEnabled = (): boolean =>
CacheService.config.conversation.isEnabled && CacheService.config.isEnabled;
export const isConversationCacheEnabled = (): boolean => {
const conversation = CacheService.config.conversation;
return conversation.isEnabled && CacheService.config.isEnabled;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* -------------------------------------------------------------------------------------------
*/

/* eslint-disable no-console */
import { BetaGraph, IGraph, Providers, error, log } from '@microsoft/mgt-element';
import { HubConnection, HubConnectionBuilder, IHttpConnectionOptions, LogLevel } from '@microsoft/signalr';
import { ThreadEventEmitter } from './ThreadEventEmitter';
Expand Down
129 changes: 83 additions & 46 deletions packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,58 @@
*/

import {
MessageThreadProps,
SendBoxProps,
ChatMessage as AcsChatMessage,
ErrorBarProps,
SystemMessage,
ContentSystemMessage,
Message
ErrorBarProps,
Message,
MessageThreadProps,
SendBoxProps,
SystemMessage
} from '@azure/communication-react';
import { IDynamicPerson, getUserWithPhoto } from '@microsoft/mgt-components';
import {
ActiveAccountChanged,
IGraph,
LoginChangedEvent,
ProviderState,
Providers,
log,
warn
} from '@microsoft/mgt-element';
import { GraphError } from '@microsoft/microsoft-graph-client';
import {
AadUserConversationMember,
Chat,
ChatMessage,
ChatMessageAttachment,
ChatRenamedEventMessageDetail,
MembersAddedEventMessageDetail,
MembersDeletedEventMessageDetail
} from '@microsoft/microsoft-graph-types';
import {
ActiveAccountChanged,
IGraph,
log,
LoginChangedEvent,
Providers,
ProviderState,
warn
} from '@microsoft/mgt-element';
import { produce } from 'immer';
import { v4 as uuid } from 'uuid';
import { currentUserId, currentUserName } from '../utils/currentUser';
import { graph } from '../utils/graph';
import { MessageCache } from './Caching/MessageCache';
import { GraphConfig } from './GraphConfig';
import { GraphNotificationClient } from './GraphNotificationClient';
import { ThreadEventEmitter } from './ThreadEventEmitter';
import {
MessageCollection,
addChatMembers,
deleteChatMessage,
loadChat,
loadChatImage,
loadChatThread,
loadChatThreadDelta,
loadMoreChatMessages,
MessageCollection,
removeChatMember,
sendChatMessage,
updateChatMessage,
removeChatMember,
addChatMembers,
loadChatImage,
updateChatTopic,
loadChatThreadDelta
updateChatTopic
} from './graph.chat';
import { getUserWithPhoto } from '@microsoft/mgt-components';
import { GraphNotificationClient } from './GraphNotificationClient';
import { ThreadEventEmitter } from './ThreadEventEmitter';
import { IDynamicPerson } from '@microsoft/mgt-react';
import { updateMessageContentWithImage } from './updateMessageContentWithImage';
import { graph } from '../utils/graph';
import { currentUserId, currentUserName } from '../utils/currentUser';
import { MessageCache } from './Caching/MessageCache';
import { GraphError } from '@microsoft/microsoft-graph-client';
import { GraphConfig } from './GraphConfig';
import { isChatMessage } from '../utils/types';

// 1x1 grey pixel
const placeholderImageContent =
Expand Down Expand Up @@ -166,15 +167,22 @@ type MessageEventType =
| '#microsoft.graph.membersDeletedEventMessageDetail'
| '#microsoft.graph.chatRenamedEventMessageDetail';

/**
* Extended Message type with additional properties.
*/
export type GraphChatMessage = Message & {
hasUnsupportedContent: boolean;
rawChatUrl: string;
};
/**
* Holder type account for async conversion of messages.
* Some messages need to be written to the UI immediately and receive an async update.
* Some messages do not have a current value and will be added after the future value is resolved.
* Some messages do not have a future value and will be added immediately.
*/
interface MessageConversion {
currentValue?: Message;
futureValue?: Promise<Message>;
currentValue?: GraphChatMessage;
futureValue?: Promise<GraphChatMessage>;
}

/**
Expand Down Expand Up @@ -543,7 +551,7 @@ class StatefulGraphChatClient implements StatefulClient<GraphChatClient> {
return this.graphChatMessageToAcsChatMessage(message, this.userId);
case 'systemEventMessage':
case 'unknownFutureValue':
return { futureValue: this.buildSystemContentMessage(message) };
return { futureValue: this.buildSystemContentMessage(message) } as MessageConversion;
default:
throw new Error(`Unknown message type ${message.messageType?.toString() || 'undefined'}`);
}
Expand Down Expand Up @@ -820,7 +828,7 @@ detail: ${JSON.stringify(eventDetail)}`);
.map(m => this.convertChatMessage(m));

// update the state with the current values
const currentValueMessages: Message[] = [];
const currentValueMessages: GraphChatMessage[] = [];
messageConversions
.map(m => m.currentValue)
// need to use a reduce here to filter out undefined values in a way that TypeScript understands
Expand Down Expand Up @@ -877,11 +885,11 @@ detail: ${JSON.stringify(eventDetail)}`);
* Update the state with given message either replacing an existing message matching on the id or adding to the list
*
* @private
* @param {(Message)} [message]
* @param {(GraphChatMessage)} [message]
* @return {*}
* @memberof StatefulGraphChatClient
*/
private updateMessages(message?: Message) {
private updateMessages(message?: GraphChatMessage) {
if (!message) return;
this.notifyStateChange((draft: GraphChatClient) => {
const index = draft.messages.findIndex(m => m.messageId === message.messageId);
Expand Down Expand Up @@ -964,23 +972,18 @@ detail: ${JSON.stringify(eventDetail)}`);
index++;
match = this.graphImageMatch(messageResult);
}
let placeholderMessage = this.buildAcsMessage(
graphMessage,
currentUser,
messageId,
messageResult
) as AcsChatMessage;
let placeholderMessage = this.buildAcsMessage(graphMessage, currentUser, messageId, messageResult);
conversion.currentValue = placeholderMessage;
// local function to update the message with data from each of the resolved image requests
const updateMessage = async () => {
await Promise.all(Object.values(futureImages));
for (const [imageIndex, futureImage] of Object.entries(futureImages)) {
const image = await futureImage;
if (image) {
if (image && isChatMessage(placeholderMessage)) {
placeholderMessage = {
...placeholderMessage,
...{
content: updateMessageContentWithImage(placeholderMessage.content || '', imageIndex, messageId, image)
content: updateMessageContentWithImage(placeholderMessage.content ?? '', imageIndex, messageId, image)
}
};
}
Expand Down Expand Up @@ -1012,9 +1015,41 @@ detail: ${JSON.stringify(eventDetail)}`);
return result;
}

private buildAcsMessage(graphMessage: ChatMessage, currentUser: string, messageId: string, content: string): Message {
private hasUnsupportedContent(content: string, attachments: ChatMessageAttachment[]): boolean {
const unsupportedContentTypes = [
'application/vnd.microsoft.card.codesnippet',
'application/vnd.microsoft.card.fluid',
'reference'
];
const isUnsupported: boolean[] = [];

if (attachments.length) {
for (const attachment of attachments) {
const contentType = attachment?.contentType ?? '';
isUnsupported.push(unsupportedContentTypes.includes(contentType));
}
} else {
// checking content with <attachment> tags
const unsupportedContentRegex = /<\/?attachment>/gim;
const contentUnsupported = Boolean(content) && unsupportedContentRegex.test(content);
isUnsupported.push(contentUnsupported);
}
return isUnsupported.every(e => e === true);
}

private buildAcsMessage(
graphMessage: ChatMessage,
currentUser: string,
messageId: string,
content: string
): GraphChatMessage {
const senderId = graphMessage.from?.user?.id || undefined;
let messageData: Message = {
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,
contentType: graphMessage.body?.contentType ?? 'text',
messageType: 'chat',
Expand All @@ -1025,7 +1060,9 @@ detail: ${JSON.stringify(eventDetail)}`);
senderId,
mine: senderId === currentUser,
status: 'seen',
attached: 'top'
attached: 'top',
hasUnsupportedContent: this.hasUnsupportedContent(content, attachments),
rawChatUrl: chatUrl
};
if (graphMessage?.policyViolation) {
messageData = Object.assign(messageData, {
Expand Down
28 changes: 28 additions & 0 deletions packages/mgt-chat/src/utils/chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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 = <UnsupportedContent targetUrl={message.rawChatUrl} />;
messageProps = produce(messageProps, (draft: MessageProps) => {
if (isChatMessage(draft.message)) {
draft.message.content = renderToString(unsupportedContentComponent);
}
});
}

return defaultOnRender ? defaultOnRender(messageProps) : <></>;
};
export { onRenderMessage };
Loading

0 comments on commit 3c00710

Please sign in to comment.