diff --git a/packages/mgt-chat/src/components/Chat/Chat.tsx b/packages/mgt-chat/src/components/Chat/Chat.tsx index b9067b8c29..3ab602f54e 100644 --- a/packages/mgt-chat/src/components/Chat/Chat.tsx +++ b/packages/mgt-chat/src/components/Chat/Chat.tsx @@ -69,6 +69,10 @@ const messageThreadStyles: MessageThreadStyles = { chatContainer: { '& .ui-box': { zIndex: 'unset' + }, + '& p': { + display: 'inline-flex', + justifyContent: 'center' } }, chatMessageContainer: { diff --git a/packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts b/packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts index f154ca61cd..2f4c241ce9 100644 --- a/packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts +++ b/packages/mgt-chat/src/statefulClient/StatefulGraphChatClient.ts @@ -60,7 +60,7 @@ import { } from './graph.chat'; import { updateMessageContentWithImage } from '../utils/updateMessageContentWithImage'; import { isChatMessage } from '../utils/types'; -import { rewriteEmojiContent } from '../utils/rewriteEmojiContent'; +import { rewriteEmojiContentToHTML } from '../utils/rewriteEmojiContent'; // 1x1 grey pixel const placeholderImageContent = @@ -1020,8 +1020,7 @@ detail: ${JSON.stringify(eventDetail)}`); } let content = graphMessage.body?.content ?? 'undefined'; let result: MessageConversion = {}; - // do simple emoji replacement first - content = rewriteEmojiContent(content); + content = rewriteEmojiContentToHTML(content); // Handle any mentions in the content content = this.updateMentionsContent(content); diff --git a/packages/mgt-chat/src/utils/rewriteEmojiContent.tests.ts b/packages/mgt-chat/src/utils/rewriteEmojiContent.tests.ts index 4f6351fe55..a8424ae13a 100644 --- a/packages/mgt-chat/src/utils/rewriteEmojiContent.tests.ts +++ b/packages/mgt-chat/src/utils/rewriteEmojiContent.tests.ts @@ -1,23 +1,40 @@ +/** + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- + */ + import { expect } from '@open-wc/testing'; -import { rewriteEmojiContent } from './rewriteEmojiContent'; +import { rewriteEmojiContentToHTML } from './rewriteEmojiContent'; -describe('emoji rewrite tests', () => { +describe('rewrite emoji to standard HTML', () => { it('rewrites an emoji correctly', async () => { - const result = rewriteEmojiContent(``); - await expect(result).to.be.equal('😎'); + const result = rewriteEmojiContentToHTML(``); + await expect(result).to.be.equal( + `😎` + ); }); it('rewrites an emoji in a p tag correctly', async () => { - const result = rewriteEmojiContent(`

`); - await expect(result).to.be.equal('

😎

'); + const result = rewriteEmojiContentToHTML(`

`); + await expect(result).to.be.equal( + `

😎

` + ); }); - it('rewrites multiple emoji in a p correctly', async () => { - const result = rewriteEmojiContent( - `

` + + it('rewrites an emoji in a p tag with additional content correctly', async () => { + const result = rewriteEmojiContentToHTML(`

Hello

`); + await expect(result).to.be.equal( + `

Hello 😎

` ); - await expect(result).to.be.equal('

😎ðŸĪŠ

'); }); - it('returns the original value if there is no emoji', async () => { - const result = rewriteEmojiContent('

Seb is cool

'); - await expect(result).to.be.equal('

Seb is cool

'); + + it('rewrites emojis in multiple p tags correctly', async () => { + const result = rewriteEmojiContentToHTML( + `

` + ); + await expect(result).to.be.equal( + `

😍

ðŸĪŠ

😎

` + ); }); }); diff --git a/packages/mgt-chat/src/utils/rewriteEmojiContent.ts b/packages/mgt-chat/src/utils/rewriteEmojiContent.ts index 114982d377..57b23434df 100644 --- a/packages/mgt-chat/src/utils/rewriteEmojiContent.ts +++ b/packages/mgt-chat/src/utils/rewriteEmojiContent.ts @@ -1,32 +1,63 @@ /** - * 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 angle bracket. - * ><\/emoji>: Matches the remaining part of the tag. + * ------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. + * See License in the project root for license information. + * ------------------------------------------------------------------------------------------- */ -const emojiRegex = /(]+)alt=["'](\w*[^"']*)["'](.[^>]+)><\/emoji>/; -const emojiMatch = (messageContent: string): RegExpMatchArray | null => { - return messageContent.match(emojiRegex); -}; -// iterative repave the emoji custom element with the content of the alt attribute -// on the emoji element -const processEmojiContent = (messageContent: string): string => { - let result = messageContent; - let match = emojiMatch(result); - while (match) { - result = result.replace(emojiRegex, '$2'); - match = emojiMatch(result); + +/** + * Checks if DOM content has emojis and other HTML content. + * @param dom the html content parsed into HTMLDocument. + * @param emojisCount number of emojis in the content. + * @returns true if only one emoji is in the content without other content, otherwise false. + */ +const hasOtherContent = (dom: Document, emojisCount: number): boolean => { + const isPtag = dom.body.firstChild?.nodeName === 'P'; + if (isPtag) { + const firstChildNodes = dom.body.firstChild?.childNodes; + return firstChildNodes?.length !== emojisCount; } - return result; + return false; }; /** - * if the content contains an tag with an alt attribute the content is replaced by replacing the emoji tags with the content of their alt attribute. - * @param {string} content - * @returns {string} the content with any emoji tags replaced by the content of their alt attribute. + * Parses html content string into HTMLDocument, then replaces instances of the + * emoji tag. + * @param content the HTML string. + * @returns HTML string with emoji tags changed to the HTML representation. */ -export const rewriteEmojiContent = (content: string): string => - emojiMatch(content) ? processEmojiContent(content) : content; +export const rewriteEmojiContentToHTML = (content: string): string => { + const parser = new DOMParser(); + const dom = parser.parseFromString(content, 'text/html'); + const emojis = dom.querySelectorAll('emoji'); + const emojisCount = emojis.length; + const size = emojisCount > 1 || hasOtherContent(dom, emojisCount) ? 20 : 50; + + for (const emoji of emojis) { + const id = emoji.getAttribute('id') ?? ''; + const alt = emoji.getAttribute('alt') ?? ''; + const title = emoji.getAttribute('title') ?? ''; + + const span = document.createElement('span'); + span.setAttribute('contentEditable', 'false'); + span.setAttribute('title', title); + span.setAttribute('type', `(${id})`); + span.setAttribute('class', `animated-emoticon-${size}-cool`); + + const img = document.createElement('img'); + img.setAttribute('itemscope', ''); + img.setAttribute('itemtype', 'http://schema.skype.com/Emoji'); + img.setAttribute('itemid', id); + img.setAttribute( + 'src', + `https://statics.teams.cdn.office.net/evergreen-assets/personal-expressions/v2/assets/emoticons/${id}/default/${size}_f.png` + ); + img.setAttribute('title', title); + img.setAttribute('alt', alt); + img.setAttribute('style', `width:${size}px;height:${size}px;`); + + span.appendChild(img); + emoji.replaceWith(span); + } + return dom.body.innerHTML; +};