diff --git a/client/components/MarkdownText.js b/client/components/MarkdownText.js deleted file mode 100644 index 76e724a659cb7..0000000000000 --- a/client/components/MarkdownText.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import React, { useMemo } from 'react'; -import marked from 'marked'; -import dompurify from 'dompurify'; - -const renderer = new marked.Renderer(); - -marked.InlineLexer.rules.gfm.strong = /^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/; -marked.InlineLexer.rules.gfm.em = /^__(?=\S)([\s\S]*?\S)__(?!_)|^_(?=\S)([\s\S]*?\S)_(?!_)/; - -const linkRenderer = renderer.link; -renderer.link = function(href, title, text) { - const html = linkRenderer.call(renderer, href, title, text); - return html.replace(/^ { - const html = content && typeof content === 'string' && marked(content, options); - return preserveHtml ? html : html && sanitizer(html, { ADD_ATTR: ['target'] }); - }, [content, preserveHtml, sanitizer]); - return __html ? : null; -} - -export default MarkdownText; diff --git a/client/components/MarkdownText.tsx b/client/components/MarkdownText.tsx new file mode 100644 index 0000000000000..0308b2d37261c --- /dev/null +++ b/client/components/MarkdownText.tsx @@ -0,0 +1,96 @@ +import { Box } from '@rocket.chat/fuselage'; +import React, { FC, useMemo } from 'react'; +import marked from 'marked'; +import dompurify from 'dompurify'; + +type MarkdownTextParams = { + content: string; + variant: 'inline' | 'inlineWithoutBreaks' | 'document'; + preserveHtml: boolean; + withTruncatedText: boolean; +}; + +const documentRenderer = new marked.Renderer(); +const inlineRenderer = new marked.Renderer(); +const inlineWithoutBreaks = new marked.Renderer(); + +marked.InlineLexer.rules.gfm = { + ...marked.InlineLexer.rules.gfm, + strong: /^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/, + em: /^__(?=\S)([\s\S]*?\S)__(?!_)|^_(?=\S)([\s\S]*?\S)_(?!_)/, +}; + +const linkMarked = (href: string | null, _title: string | null, text: string): string => + `${ text } `; +const paragraphMarked = (text: string): string => text; +const brMarked = (): string => ' '; +const listItemMarked = (text: string): string => { + const cleanText = text.replace(/|<\/p>/ig, ''); + return `
  • ${ cleanText }
  • `; +}; + +documentRenderer.link = linkMarked; +documentRenderer.listitem = listItemMarked; + +inlineRenderer.link = linkMarked; +inlineRenderer.paragraph = paragraphMarked; +inlineRenderer.listitem = listItemMarked; + +inlineWithoutBreaks.link = linkMarked; +inlineWithoutBreaks.paragraph = paragraphMarked; +inlineWithoutBreaks.br = brMarked; +inlineWithoutBreaks.listitem = listItemMarked; + +const defaultOptions = { + gfm: true, + headerIds: false, +}; + +const options = { + ...defaultOptions, + renderer: documentRenderer, +}; + +const inlineOptions = { + ...defaultOptions, + renderer: inlineRenderer, +}; + +const inlineWithoutBreaksOptions = { + ...defaultOptions, + renderer: inlineWithoutBreaks, +}; + +const MarkdownText: FC> = ({ + content, + variant = 'document', + withTruncatedText = false, + preserveHtml = false, + ...props +}) => { + const sanitizer = dompurify.sanitize; + + let markedOptions: {}; + + const withRichContent = variant; + switch (variant) { + case 'inline': + markedOptions = inlineOptions; + break; + case 'inlineWithoutBreaks': + markedOptions = inlineWithoutBreaksOptions; + break; + case 'document': + default: + markedOptions = options; + } + + const __html = useMemo(() => { + const html = content && typeof content === 'string' && marked(content, markedOptions); + return preserveHtml ? html : html && sanitizer(html, { ADD_ATTR: ['target'] }); + }, [content, preserveHtml, sanitizer, markedOptions]); + + return __html ? : null; +}; + +export default MarkdownText; diff --git a/client/components/Message/Attachments/DefaultAttachment.tsx b/client/components/Message/Attachments/DefaultAttachment.tsx index 0584770f7a6a1..fa3fb03a3d97c 100644 --- a/client/components/Message/Attachments/DefaultAttachment.tsx +++ b/client/components/Message/Attachments/DefaultAttachment.tsx @@ -42,7 +42,7 @@ export type DefaultAttachmentProps = { const isActionAttachment = (attachment: AttachmentProps): attachment is ActionAttachmentProps => 'actions' in attachment; -const applyMarkdownIfRequires = (list: DefaultAttachmentProps['mrkdwn_in'] = ['text', 'pretext']) => (key: MarkdownFields, text: string): JSX.Element | string => (list?.includes(key) ? : text); +const applyMarkdownIfRequires = (list: DefaultAttachmentProps['mrkdwn_in'] = ['text', 'pretext']) => (key: MarkdownFields, text: string): JSX.Element | string => (list?.includes(key) ? : text); export const DefaultAttachment: FC = (attachment) => { const applyMardownFor = applyMarkdownIfRequires(attachment.mrkdwn_in); @@ -57,7 +57,7 @@ export const DefaultAttachment: FC = (attachment) => { {!collapsed && <> {attachment.text && {applyMardownFor('text', attachment.text)}} {/* {attachment.fields && ({ ...rest, value: })) : attachment.fields} />} */} - {attachment.fields && ({ ...rest, value: }))} />} + {attachment.fields && ({ ...rest, value: }))} />} {attachment.image_url && } {/* DEPRECATED */} {isActionAttachment(attachment) && } diff --git a/client/components/Message/Attachments/Files/GenericFileAttachment.tsx b/client/components/Message/Attachments/Files/GenericFileAttachment.tsx index 77636b0fd6fbb..7c6a336ac77f1 100644 --- a/client/components/Message/Attachments/Files/GenericFileAttachment.tsx +++ b/client/components/Message/Attachments/Files/GenericFileAttachment.tsx @@ -24,7 +24,7 @@ export const GenericFileAttachment: FC = ({ // const [collapsed, collapse] = useCollapse(collapsedDefault); const getURL = useMediaUrl(); return - { description && } + { description && } { hasDownload && link ? : {title} } {size && } diff --git a/client/components/Message/Attachments/Files/ImageAttachment.tsx b/client/components/Message/Attachments/Files/ImageAttachment.tsx index 911e9800d34ff..b71366e36bc75 100644 --- a/client/components/Message/Attachments/Files/ImageAttachment.tsx +++ b/client/components/Message/Attachments/Files/ImageAttachment.tsx @@ -35,7 +35,7 @@ export const ImageAttachment: FC = ({ const [collapsed, collapse] = useCollapse(collapsedDefault); const getURL = useMediaUrl(); return - + {title} {size && } diff --git a/client/components/Message/Attachments/Files/PDFAttachment.tsx b/client/components/Message/Attachments/Files/PDFAttachment.tsx index 6dd877b1ac36a..d10480c75ea95 100644 --- a/client/components/Message/Attachments/Files/PDFAttachment.tsx +++ b/client/components/Message/Attachments/Files/PDFAttachment.tsx @@ -20,7 +20,7 @@ export const PDFAttachment: FC = ({ const t = useTranslation(); const [collapsed, collapse] = useCollapse(collapsedDefault); return - + {t('PDF')} {collapse} diff --git a/client/components/Message/Attachments/QuoteAttachment.tsx b/client/components/Message/Attachments/QuoteAttachment.tsx index 09c7585aec8f7..26a7c139870e6 100644 --- a/client/components/Message/Attachments/QuoteAttachment.tsx +++ b/client/components/Message/Attachments/QuoteAttachment.tsx @@ -39,7 +39,7 @@ export const QuoteAttachment: FC = ({ author_icon: url, au {name} {format(ts)} - + {attachments && } diff --git a/client/components/UserCard.js b/client/components/UserCard.js index bc7c2006b27e6..81d7fa5abefef 100644 --- a/client/components/UserCard.js +++ b/client/components/UserCard.js @@ -78,7 +78,7 @@ const UserCard = forwardRef(({ {nickname && ({ nickname })} - { customStatus && {typeof customStatus === 'string' ? : customStatus} } + { customStatus && {typeof customStatus === 'string' ? : customStatus} } {roles} {localTime} { bio && {typeof bio === 'string' ? : bio} } diff --git a/client/sidebar/header/UserDropdown.js b/client/sidebar/header/UserDropdown.js index d62ad00bf0741..6d48457e3f9a8 100644 --- a/client/sidebar/header/UserDropdown.js +++ b/client/sidebar/header/UserDropdown.js @@ -114,7 +114,7 @@ const UserDropdown = ({ user, onClose }) => { - + diff --git a/client/types/fuselage.d.ts b/client/types/fuselage.d.ts index 9265b0385189c..ca7c6f6a28b70 100644 --- a/client/types/fuselage.d.ts +++ b/client/types/fuselage.d.ts @@ -151,7 +151,7 @@ declare module '@rocket.chat/fuselage' { elevation?: '0' | '1' | '2'; invisible?: boolean; - withRichContent?: boolean; + withRichContent?: boolean | string; withTruncatedText?: boolean; size?: CSSProperties['blockSize']; minSize?: CSSProperties['blockSize']; diff --git a/client/views/admin/cloud/CopyStep.tsx b/client/views/admin/cloud/CopyStep.tsx index b8c07e95a01b1..1382a0a3c7d0f 100644 --- a/client/views/admin/cloud/CopyStep.tsx +++ b/client/views/admin/cloud/CopyStep.tsx @@ -74,7 +74,7 @@ const CopyStep: FC = ({ onNextButtonClick }) => { {t('Copy')} - + diff --git a/client/views/directory/ChannelsTab.js b/client/views/directory/ChannelsTab.js index 07ed3e447865d..053c5d109e02c 100644 --- a/client/views/directory/ChannelsTab.js +++ b/client/views/directory/ChannelsTab.js @@ -78,7 +78,7 @@ function ChannelsTable() { {fname || name} - {topic && } + {topic && } diff --git a/client/views/room/Announcement/Announcement.js b/client/views/room/Announcement/Announcement.tsx similarity index 61% rename from client/views/room/Announcement/Announcement.js rename to client/views/room/Announcement/Announcement.tsx index 7b0acded3f107..7d9ad98cac766 100644 --- a/client/views/room/Announcement/Announcement.js +++ b/client/views/room/Announcement/Announcement.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FC } from 'react'; import { Box } from '@rocket.chat/fuselage'; import { css } from '@rocket.chat/css-in-js'; import colors from '@rocket.chat/fuselage-tokens/colors'; @@ -8,7 +8,17 @@ import AnnouncementModal from './AnnouncementModal'; import { useSetModal } from '../../../contexts/ModalContext'; import MarkdownText from '../../../components/MarkdownText'; -export const Announcement = ({ children, onClickOpen }) => { +type AnnouncementComponentParams = { + children: JSX.Element; + onClickOpen: (e: React.MouseEvent) => void; +}; + +type AnnouncementParams = { + announcement: string; + announcementDetails: () => void; +} + +export const AnnouncementComponent: FC = ({ children, onClickOpen }) => { const announcementBar = css` background-color: ${ colors.b200 }; background-color: var(--rc-color-announcement-background, ${ colors.b200 }); @@ -35,21 +45,22 @@ export const Announcement = ({ children, onClickOpen }) => { return {children}; }; -export default ({ announcement, announcementDetails }) => { +const Announcement: FC = ({ announcement, announcementDetails }) => { const setModal = useSetModal(); - const closeModal = useMutableCallback(() => setModal()); - const handleClick = (e) => { - if (e.target.href) { + const closeModal = useMutableCallback(() => setModal(null)); + const handleClick = (e: React.MouseEvent): void => { + if ((e.target as HTMLAnchorElement).href) { return; } - if (window.getSelection().toString() !== '') { + if (window?.getSelection()?.toString() !== '') { return; } announcementDetails ? announcementDetails() : setModal({announcement}); }; - const announcementWithoutBreaks = announcement && announcement.replace(/(\r\n|\n|\r)/gm, ' '); - return announcementWithoutBreaks ? : false; + return announcement ? ): void => handleClick(e)}> : null; }; + +export default Announcement; diff --git a/client/views/room/Announcement/AnnouncementModal.js b/client/views/room/Announcement/AnnouncementModal.tsx similarity index 69% rename from client/views/room/Announcement/AnnouncementModal.js rename to client/views/room/Announcement/AnnouncementModal.tsx index 596d28804a3e0..98b3a8896d6d8 100644 --- a/client/views/room/Announcement/AnnouncementModal.js +++ b/client/views/room/Announcement/AnnouncementModal.tsx @@ -1,10 +1,16 @@ -import React from 'react'; +import React, { FC } from 'react'; import { Button, ButtonGroup, Box, Modal } from '@rocket.chat/fuselage'; import { useTranslation } from '../../../contexts/TranslationContext'; import MarkdownText from '../../../components/MarkdownText'; -export default ({ +type AnnouncementModalParams = { + onClose: () => void; + confirmLabel?: string; + children?: string; +} + +const AnnouncementModal: FC = ({ onClose, confirmLabel = 'Close', children, @@ -19,7 +25,7 @@ export default ({ - + @@ -29,3 +35,5 @@ export default ({ ); }; + +export default AnnouncementModal; diff --git a/client/views/room/Announcement/index.js b/client/views/room/Announcement/index.tsx similarity index 100% rename from client/views/room/Announcement/index.js rename to client/views/room/Announcement/index.tsx diff --git a/client/views/room/Header/Header.js b/client/views/room/Header/Header.js index 9506c239c08d8..f10b67b9fdea7 100644 --- a/client/views/room/Header/Header.js +++ b/client/views/room/Header/Header.js @@ -78,7 +78,7 @@ const RoomHeader = ({ room, topic }) => { - {topic && } + {topic && } diff --git a/client/views/room/contextualBar/UserInfo/index.js b/client/views/room/contextualBar/UserInfo/index.js index 9a601f4a682ca..0f9645112e234 100644 --- a/client/views/room/contextualBar/UserInfo/index.js +++ b/client/views/room/contextualBar/UserInfo/index.js @@ -96,7 +96,7 @@ export const UserInfo = React.memo(function UserInfo({ {bio && <> - + } {phone && <> diff --git a/package-lock.json b/package-lock.json index e8cea7ac43183..8d37a6a35db52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5950,12 +5950,13 @@ } }, "@rocket.chat/fuselage": { - "version": "0.6.3-dev.179", - "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage/-/fuselage-0.6.3-dev.179.tgz", - "integrity": "sha512-z4Wgk8J34NfriXuh00cJ+J3M7h1fxFtG9ily3cKhYIVNPHyhUR4o3h2MbvPKKE+mBPzuLsOV2kbcb49y4gm76g==", + "version": "0.6.3-dev.188", + "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage/-/fuselage-0.6.3-dev.188.tgz", + "integrity": "sha512-a0XGJ4uayUUsdk7r5aAEdaXa5CmT+Wsx5B3rv4GJqOYGq5NEFzGHxssWaRd9PPVWDd/USYALMlOKqJSxKViSlw==", "requires": { "@rocket.chat/css-in-js": "^0.21.0", "@rocket.chat/fuselage-tokens": "^0.21.0", + "@rocket.chat/memo": "^0.6.3-dev.180", "invariant": "^2.2.4", "react-keyed-flatten-children": "^1.2.0" }, @@ -6088,6 +6089,11 @@ } } }, + "@rocket.chat/memo": { + "version": "0.6.3-dev.180", + "resolved": "https://registry.npmjs.org/@rocket.chat/memo/-/memo-0.6.3-dev.180.tgz", + "integrity": "sha512-3+zoOZW/f2/cwmjDAh8yXvGQvskopSR/QCAUmatqSAiMztFvkBljlVbSx1YUiq4y5t41dzM5m+MKYRZAEgAtLQ==" + }, "@rocket.chat/mp3-encoder": { "version": "0.6.3-dev.178", "resolved": "https://registry.npmjs.org/@rocket.chat/mp3-encoder/-/mp3-encoder-0.6.3-dev.178.tgz", @@ -10580,6 +10586,14 @@ "integrity": "sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==", "dev": true }, + "@types/dompurify": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.2.1.tgz", + "integrity": "sha512-3JwbEeRVQ3n6+JgBW/hCdkydRk9/vWT+UEglcXEJqLJEcUganDH37zlfLznxPKTZZfDqA9K229l1qN458ubcOQ==", + "requires": { + "@types/trusted-types": "*" + } + }, "@types/ejson": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@types/ejson/-/ejson-2.1.2.tgz", @@ -10813,6 +10827,11 @@ "@types/react": "*" } }, + "@types/marked": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/marked/-/marked-1.2.2.tgz", + "integrity": "sha512-wLfw1hnuuDYrFz97IzJja0pdVsC0oedtS4QsKH1/inyW9qkLQbXgMUqEQT0MVtUBx3twjWeInUfjQbhBVLECXw==" + }, "@types/md5-file": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/md5-file/-/md5-file-4.0.2.tgz", @@ -11169,6 +11188,11 @@ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==" }, + "@types/trusted-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.0.tgz", + "integrity": "sha512-I8MnZqNXsOLHsU111oHbn3khtvKMi5Bn4qVFsIWSJcCP1KKDiXX5AEw8UPk0nSopeC+Hvxt6yAy1/a5PailFqg==" + }, "@types/uglify-js": { "version": "3.11.1", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.11.1.tgz", diff --git a/package.json b/package.json index d9707cfc8744c..ec4a328e90ca8 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "@rocket.chat/apps-engine": "1.23.0-alpha.4655", "@rocket.chat/css-in-js": "^0.6.3-dev.178", "@rocket.chat/emitter": "^0.6.3-dev.178", - "@rocket.chat/fuselage": "^0.6.3-dev.179", + "@rocket.chat/fuselage": "^0.6.3-dev.188", "@rocket.chat/fuselage-hooks": "^0.6.3-dev.178", "@rocket.chat/fuselage-polyfills": "^0.6.3-dev.181", "@rocket.chat/fuselage-tokens": "^0.21.0", @@ -146,9 +146,11 @@ "@rocket.chat/mp3-encoder": "^0.6.3-dev.178", "@rocket.chat/ui-kit": "^0.6.3-dev.178", "@slack/client": "^4.12.0", + "@types/dompurify": "^2.2.1", "@types/fibers": "^3.1.0", "@types/imap": "^0.8.33", "@types/mailparser": "^3.0.0", + "@types/marked": "^1.2.2", "@types/mkdirp": "^1.0.1", "@types/nodemailer": "^6.4.0", "@types/string-strip-html": "^5.0.0",