Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
70768c5
[MessageThread.stories] Introduce new button to present message with …
JoshuaLai Mar 21, 2023
87e3dcf
[placeholdermessages] Updating mock messages we are sending
JoshuaLai Mar 21, 2023
b218249
[ChatMessage.ts] Adding the ChatAttachment interface as a beta type
JoshuaLai Mar 22, 2023
719f85a
[index.ts] updating the export types list
JoshuaLai Mar 22, 2023
91da5d5
[index.ts] adding ChatAttachmentStatus as export type
JoshuaLai Mar 22, 2023
03e0bc9
[index.ts] moving the export into its own block
JoshuaLai Mar 22, 2023
5d38a5e
Change files
JoshuaLai Mar 22, 2023
a0a7082
Duplicate change files for beta release
JoshuaLai Mar 22, 2023
361d8db
Merge branch 'main' into storybook/messagethreadwithinlineimage
dmceachernmsft Mar 22, 2023
79a6c47
[placeholdermessages.ts] updating the placeholder to more accurately …
JoshuaLai Mar 22, 2023
dadaf51
Merge branch 'main' of github.com:Azure/communication-ui-library into…
JoshuaLai Mar 24, 2023
16322e3
Merge branch 'main' into storybook/messagethreadwithinlineimage
JoshuaLai Mar 25, 2023
ece8a03
Merge branch 'storybook/messagethreadwithinlineimage' of github.com:A…
JoshuaLai Mar 25, 2023
243418b
[changelog] removing old change log files
JoshuaLai Mar 25, 2023
5a7ea7e
[FileMetadataAttachmentType] export the new type
JoshuaLai Mar 27, 2023
141953e
[Tests] fixing the mocks we are passing into tests
JoshuaLai Mar 27, 2023
6916e5f
[MessageWithFile.snippet.tsx] Updating snippet with new properties
JoshuaLai Mar 27, 2023
9db5c43
Change files
JoshuaLai Mar 27, 2023
3d7e802
Duplicate change files for beta release
JoshuaLai Mar 27, 2023
33d4cb8
[apiview] stable api view updated
JoshuaLai Mar 27, 2023
6dd5bab
[Tests] updating test to include missing argument
JoshuaLai Mar 28, 2023
13529d6
[ChatMessageComponent.tsx] adding the callback method flow from Messa…
JoshuaLai Mar 23, 2023
14bf3a6
[ChatMessageContent.tsx] introduce a new inlineattachment ui component
JoshuaLai Mar 24, 2023
96d388c
merge main to branch
jimchou-dev Mar 30, 2023
ec66559
add inline image ui support
jimchou-dev Mar 31, 2023
6f8fe32
back merge main
jimchou-dev Apr 5, 2023
c867bdd
added docs for inline image in storybook
jimchou-dev Apr 5, 2023
ea2ae61
Merge branch 'main' into storybook/onFetchImageCallback
jimchou-dev Apr 5, 2023
27b0b43
Change files
jimchou-dev Apr 5, 2023
5fd02fd
Duplicate change files for beta release
jimchou-dev Apr 5, 2023
77fd145
Merge branch 'main' into storybook/onFetchImageCallback
jimchou-dev Apr 5, 2023
90a51bb
[MessageThread.stories.tsx] updating some content in the docs
JoshuaLai Apr 5, 2023
296337a
add unit test for inline image
jimchou-dev Apr 6, 2023
f287644
Merge branch 'main' into storybook/onFetchImageCallback
jimchou-dev Apr 10, 2023
4e8e489
remove .only for test
jimchou-dev Apr 10, 2023
e64c46c
Merge branch 'main' into storybook/onFetchImageCallback
jimchou-dev Apr 10, 2023
ed62388
Merge branch 'main' into storybook/onFetchImageCallback
jimchou-dev Apr 11, 2023
0b078a8
resolve PR comments
jimchou-dev Apr 11, 2023
32290bc
Merge branch 'main' into storybook/onFetchImageCallback
jimchou-dev Apr 11, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Implement inline image attachment for chat UI component",
"packageName": "@azure/communication-react",
"email": "77021369+jimchou-dev@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Implement inline image attachment for chat UI component",
"packageName": "@azure/communication-react",
"email": "77021369+jimchou-dev@users.noreply.github.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -2583,6 +2583,7 @@ export type MessageThreadProps = {
onLoadPreviousChatMessages?: (messagesToLoad: number) => Promise<boolean>;
onRenderMessage?: (messageProps: MessageProps, messageRenderer?: MessageRenderer) => JSX.Element;
onRenderFileDownloads?: (userId: string, message: ChatMessage) => JSX.Element;
onFetchAttachments?: (attachment: FileMetadata) => Promise<string>;
Comment thread
jimchou-dev marked this conversation as resolved.
Outdated
onUpdateMessage?: UpdateMessageCallback;
onCancelMessageEdit?: CancelEditCallback;
onDeleteMessage?: (messageId: string) => Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1146,6 +1146,7 @@ export type MessageThreadProps = {
onLoadPreviousChatMessages?: (messagesToLoad: number) => Promise<boolean>;
onRenderMessage?: (messageProps: MessageProps, messageRenderer?: MessageRenderer) => JSX.Element;
onRenderFileDownloads?: (userId: string, message: ChatMessage) => JSX.Element;
onFetchAttachments?: (attachment: FileMetadata) => Promise<string>;
onUpdateMessage?: UpdateMessageCallback;
onCancelMessageEdit?: CancelEditCallback;
onDeleteMessage?: (messageId: string) => Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ type ChatMessageComponentProps = {
* @beta
*/
onDisplayDateTimeString?: (messageDate: Date) => string;
/* @conditional-compile-remove(teams-inline-images) */
/**
* Optional function to fetch attachments.
* @beta
*/
onFetchAttachments?: (attachment: FileMetadata) => Promise<void>;
/* @conditional-compile-remove(teams-inline-images) */
/**
* Optional map of attachment ids to blob urls.
* @beta
*/
attachmentsMap?: Record<string, string>;
Comment thread
jimchou-dev marked this conversation as resolved.
};

/**
Expand Down Expand Up @@ -139,6 +151,10 @@ export const ChatMessageComponent = (props: ChatMessageComponentProps): JSX.Elem
/* @conditional-compile-remove(date-time-customization) */
onDisplayDateTimeString={props.onDisplayDateTimeString}
strings={props.strings}
/* @conditional-compile-remove(teams-inline-images) */
onFetchAttachments={props.onFetchAttachments}
/* @conditional-compile-remove(teams-inline-images) */
attachmentsMap={props.attachmentsMap}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import { useIdentifiers } from '../../identifiers/IdentifierProvider';
import { useTheme } from '../../theming';
import { ChatMessageActionFlyout } from './ChatMessageActionsFlyout';
import { ChatMessageContent } from './ChatMessageContent';
import { ChatMessage } from '../../types/ChatMessage';
/* @conditional-compile-remove(teams-inline-images) */
import { FileMetadata } from '../FileDownloadCards';
/* @conditional-compile-remove(data-loss-prevention) */
import { BlockedMessageContent } from './ChatMessageContent';
import { ChatMessage } from '../../types/ChatMessage';
/* @conditional-compile-remove(data-loss-prevention) */
import { BlockedMessage } from '../../types/ChatMessage';
import { MessageThreadStrings } from '../MessageThread';
Expand Down Expand Up @@ -66,6 +68,18 @@ type ChatMessageComponentAsMessageBubbleProps = {
* @beta
*/
onDisplayDateTimeString?: (messageDate: Date) => string;
/* @conditional-compile-remove(teams-inline-images) */
/**
* Optional function to fetch attachments.
* @beta
*/
onFetchAttachments?: (attachment: FileMetadata) => Promise<void>;
/* @conditional-compile-remove(teams-inline-images) */
/**
* Optional map of attachment ids to blob urls.
* @beta
*/
attachmentsMap?: Record<string, string>;
Comment thread
jimchou-dev marked this conversation as resolved.
};

const generateDefaultTimestamp = (
Expand Down Expand Up @@ -210,7 +224,14 @@ const MessageBubble = (props: ChatMessageComponentAsMessageBubbleProps): JSX.Ele
}
return (
<div tabIndex={0}>
<ChatMessageContent message={message} strings={strings} />
<ChatMessageContent
message={message}
strings={strings}
/* @conditional-compile-remove(teams-inline-images) */
onFetchAttachment={props.onFetchAttachments}
/* @conditional-compile-remove(teams-inline-images) */
attachmentsMap={props.attachmentsMap}
/>
{props.onRenderFileDownloads ? props.onRenderFileDownloads(userId, message) : defaultOnRenderFileDownloads()}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import React from 'react';
import { _formatString } from '@internal/acs-ui-common';
import { Parser } from 'html-to-react';
import { Parser, ProcessNodeDefinitions, ProcessingInstructions } from 'html-to-react';
import Linkify from 'react-linkify';
import { ChatMessage } from '../../types/ChatMessage';
/* @conditional-compile-remove(data-loss-prevention) */
Expand All @@ -13,10 +13,16 @@ import { Link } from '@fluentui/react';
/* @conditional-compile-remove(data-loss-prevention) */
import { FontIcon, Stack } from '@fluentui/react';
import { MessageThreadStrings } from '../MessageThread';
/* @conditional-compile-remove(teams-inline-images) */
import { FileMetadata } from '../FileDownloadCards';

type ChatMessageContentProps = {
message: ChatMessage;
strings: MessageThreadStrings;
/* @conditional-compile-remove(teams-inline-images) */
attachmentsMap?: Record<string, string>;
/* @conditional-compile-remove(teams-inline-images) */
onFetchAttachment?: (attachment: FileMetadata) => Promise<void>;
};

/* @conditional-compile-remove(data-loss-prevention) */
Expand All @@ -32,6 +38,9 @@ type MessageContentWithLiveAriaProps = {
content: JSX.Element;
};

const processNodeDefinitions: ProcessNodeDefinitions = new ProcessNodeDefinitions(React);
const isValidNode = (): boolean => true;

/** @private */
export const ChatMessageContent = (props: ChatMessageContentProps): JSX.Element => {
switch (props.message.contentType) {
Expand All @@ -57,14 +66,13 @@ const MessageContentWithLiveAria = (props: MessageContentWithLiveAriaProps): JSX
};

const MessageContentAsRichTextHTML = (props: ChatMessageContentProps): JSX.Element => {
const htmlToReactParser = new Parser();
const liveAuthor = _formatString(props.strings.liveAuthorIntro, { author: `${props.message.senderDisplayName}` });
return (
<MessageContentWithLiveAria
message={props.message}
liveMessage={`${props.message.mine ? '' : liveAuthor} ${extractContent(props.message.content || '')}`}
ariaLabel={messageContentAriaText(props)}
content={htmlToReactParser.parse(props.message.content)}
content={processHtmlToReact(props)}
/>
);
};
Expand Down Expand Up @@ -152,3 +160,63 @@ const messageContentAriaText = (props: ChatMessageContentProps): string | undefi
})
: undefined;
};

type NodeProcessInstruction = {
shouldProcessNode: unknown;
processNode: unknown;
};

const processHtmlToReact = (props: ChatMessageContentProps): JSX.Element => {
const htmlToReactParser = new Parser();

/* @conditional-compile-remove(teams-inline-images) */
const processInlineImage: NodeProcessInstruction = {
Comment thread
jimchou-dev marked this conversation as resolved.
// Custom <img> processing
shouldProcessNode: (node): boolean => {
// Process img node with id in attachments list
return (
node.name &&
node.name === 'img' &&
node.attribs &&
node.attribs.id &&
props.message.attachedFilesMetadata?.find((f) => f.id === node.attribs.id)
);
},
processNode: (node, children, index): HTMLElement => {
// logic to check id in map/list
const fileMetadata = props.message.attachedFilesMetadata?.find((f) => f.id === node.attribs.id);
// if in cache, early return
if (props.attachmentsMap && node.attribs.id in props.attachmentsMap) {
node.attribs = { ...node.attribs, src: props.attachmentsMap[node.attribs.id] };
return processNodeDefinitions.processDefaultNode(node, children, index);
}
// not yet in cache
if (fileMetadata && props.onFetchAttachment && props.attachmentsMap) {
props.onFetchAttachment(fileMetadata);
Comment thread
jimchou-dev marked this conversation as resolved.
if (node.attribs.id in props.attachmentsMap) {
node.attribs = { ...node.attribs, src: props.attachmentsMap[node.attribs.id] };
}
}
return processNodeDefinitions.processDefaultNode(node, children, index);
}
};

const addProcessingStep = (): NodeProcessInstruction[] => {
const steps: NodeProcessInstruction[] = [];
/* @conditional-compile-remove(teams-inline-images) */
steps.push(processInlineImage);
return steps;
};

const processingInstructions: ProcessingInstructions = [
...addProcessingStep(),
{
shouldProcessNode: (): boolean => {
return true;
},
processNode: processNodeDefinitions.processDefaultNode
}
];

return htmlToReactParser.parseWithInstructions(props.message.content, isValidNode, processingInstructions);
};
47 changes: 47 additions & 0 deletions packages/react-components/src/components/MessageThread.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import { MessageThread } from './MessageThread';
import { ChatMessage } from '../types';
/* @conditional-compile-remove(data-loss-prevention) */
import { BlockedMessage } from '../types';
/* @conditional-compile-remove(teams-inline-images) */
import { FileMetadata } from './FileDownloadCards';
/* @conditional-compile-remove(teams-inline-images) */
import { act } from 'react-dom/test-utils';
import Enzyme from 'enzyme';
/* @conditional-compile-remove(teams-inline-images) */
import { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { mountWithLocalization, createTestLocale } from './utils/testUtils';
/* @conditional-compile-remove(date-time-customization) @conditional-compile-remove(data-loss-prevention) */
Expand Down Expand Up @@ -146,3 +152,44 @@ describe('Message blocked should display default blocked text correctly', () =>
expect(component.text()).toContain(testLocale.strings.messageThread.blockedWarningText);
});
});

/* @conditional-compile-remove(teams-inline-images) */
describe.only('Message should display inline image correctly', () => {
Comment thread
jimchou-dev marked this conversation as resolved.
Outdated
test.only('Message richtext/html img src should be correct', async () => {
const expectedImgSrc = 'someImgSrcUrl';
const sampleMessage: ChatMessage = {
messageType: 'chat',
senderId: 'user3',
content:
'<p>Test</p><p><img alt="image" src="" itemscope="png" width="166.5625" height="250" id="SomeImageId1" style="vertical-align:bottom"></p><p>&nbsp;</p>',
senderDisplayName: 'Miguel Garcia',
messageId: Math.random().toString(),
createdOn: new Date('2019-04-13T00:00:00.000+08:09'),
mine: false,
attached: false,
contentType: 'html',
attachedFilesMetadata: [
{
id: 'SomeImageId1',
name: 'SomeImageId1',
attachmentType: 'teamsInlineImage',
extension: 'png',
url: 'images/inlineImageExample1.png',
previewUrl: expectedImgSrc
}
]
};
const onFetchAttachment = async (attachment: FileMetadata): Promise<string> => {
return attachment.previewUrl ?? '';
};
const component = mount(
<MessageThread userId="user1" messages={[sampleMessage]} onFetchAttachments={onFetchAttachment} />
);
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
Comment thread
jimchou-dev marked this conversation as resolved.
component.update();

Comment thread
jimchou-dev marked this conversation as resolved.
expect(component.find('img').prop('src')).toEqual(expectedImgSrc);
});
});
});
35 changes: 33 additions & 2 deletions packages/react-components/src/components/MessageThread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,13 @@ export type MessageThreadProps = {
* @beta
*/
onRenderFileDownloads?: (userId: string, message: ChatMessage) => JSX.Element;
/* @conditional-compile-remove(teams-inline-images) */
/**
* Optional callback to retrieve the inline image in a message.
* @param attachment - FileMetadata object we want to render
* @beta
*/
onFetchAttachments?: (attachment: FileMetadata) => Promise<string>;
/**
* Optional callback to edit a message.
*
Expand Down Expand Up @@ -795,7 +802,9 @@ export const MessageThread = (props: MessageThreadProps): JSX.Element => {
onDeleteMessage,
onSendMessage,
/* @conditional-compile-remove(date-time-customization) */
onDisplayDateTimeString
onDisplayDateTimeString,
/* @conditional-compile-remove(teams-inline-images) */
onFetchAttachments
} = props;
const onRenderFileDownloads = onRenderFileDownloadsTrampoline(props);

Expand All @@ -818,6 +827,20 @@ export const MessageThread = (props: MessageThreadProps): JSX.Element => {
// readCount and participantCount will only need to be updated on-fly when user hover on an indicator
const [readCountForHoveredIndicator, setReadCountForHoveredIndicator] = useState<number | undefined>(undefined);

/* @conditional-compile-remove(teams-inline-images) */
const [inlineAttachments, setInlineAttachments] = useState<Record<string, string>>({});
/* @conditional-compile-remove(teams-inline-images) */
const onFetchInlineAttachment = useCallback(
async (attachment: FileMetadata): Promise<void> => {
if (!onFetchAttachments || attachment.id in inlineAttachments) {
return;
}
const url = await onFetchAttachments(attachment);
setInlineAttachments((prev) => ({ ...prev, [attachment.id]: url }));
},
[inlineAttachments, onFetchAttachments]
);

const isAllChatMessagesLoadedRef = useRef(false);
// isAllChatMessagesLoadedRef needs to be updated every time when a new adapter is set in order to display correct data
// onLoadPreviousChatMessages is updated when a new adapter is set
Expand Down Expand Up @@ -1079,6 +1102,10 @@ export const MessageThread = (props: MessageThreadProps): JSX.Element => {
onActionButtonClick={onActionButtonClickMemo}
/* @conditional-compile-remove(date-time-customization) */
onDisplayDateTimeString={onDisplayDateTimeString}
/* @conditional-compile-remove(teams-inline-images) */
onFetchAttachments={onFetchInlineAttachment}
/* @conditional-compile-remove(teams-inline-images) */
attachmentsMap={inlineAttachments}
/>
);
}
Expand All @@ -1095,7 +1122,11 @@ export const MessageThread = (props: MessageThreadProps): JSX.Element => {
showMessageStatus,
onActionButtonClickMemo,
/* @conditional-compile-remove(date-time-customization) */
onDisplayDateTimeString
onDisplayDateTimeString,
/* @conditional-compile-remove(teams-inline-images) */
onFetchInlineAttachment,
/* @conditional-compile-remove(teams-inline-images) */
inlineAttachments
]
);

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading