diff --git a/web/markdown/markdown-chat-mention.react.js b/web/markdown/markdown-chat-mention.react.js index dc868b062..e506413a1 100644 --- a/web/markdown/markdown-chat-mention.react.js +++ b/web/markdown/markdown-chat-mention.react.js @@ -1,31 +1,31 @@ // @flow import * as React from 'react'; import type { ResolvedThreadInfo } from 'lib/types/thread-types.js'; import css from './markdown.css'; import { useOnClickThread } from '../selectors/thread-selectors.js'; type MarkdownChatMentionProps = { +threadInfo: ResolvedThreadInfo, +hasAccessToChat: boolean, +text: string, }; function MarkdownChatMention(props: MarkdownChatMentionProps): React.Node { const { threadInfo, hasAccessToChat, text } = props; const onClick = useOnClickThread(threadInfo); if (!hasAccessToChat) { return text; } return ( - + {text} ); } export default MarkdownChatMention; diff --git a/web/markdown/markdown-user-mention.react.js b/web/markdown/markdown-user-mention.react.js new file mode 100644 index 000000000..eea324483 --- /dev/null +++ b/web/markdown/markdown-user-mention.react.js @@ -0,0 +1,30 @@ +// @flow + +import * as React from 'react'; + +import css from './markdown.css'; +import { usePushUserProfileModal } from '../modals/user-profile/user-profile-utils.js'; + +type MarkdownChatMentionProps = { + +text: string, + +userID: string, +}; + +function MarkdownUserMention(props: MarkdownChatMentionProps): React.Node { + const { text, userID } = props; + + const pushUserProfileModal = usePushUserProfileModal(userID); + + const markdownUserMention = React.useMemo( + () => ( + + {text} + + ), + [pushUserProfileModal, text], + ); + + return markdownUserMention; +} + +export default MarkdownUserMention; diff --git a/web/markdown/markdown.css b/web/markdown/markdown.css index 4fa4ec10f..d7d18e445 100644 --- a/web/markdown/markdown.css +++ b/web/markdown/markdown.css @@ -1,115 +1,115 @@ div.markdown { display: inline; } div.markdown h1, div.markdown h2, div.markdown h3, div.markdown h4, div.markdown h5, div.markdown h6 { display: inline; } div.markdown blockquote { display: inline-block; padding: 0.5em 10px; box-sizing: border-box; width: 100%; margin: 6px 0; border-radius: 8px; border-left: 8px solid #00000066; box-shadow: 0 1px 2px 1px #00000033; } div.markdown > blockquote { background-color: #00000066; } div.markdown code { padding: 0 4px; margin: 0 2px; border-radius: 3px; } div.lightBackground code { background: #dcdcdc; color: #222222; } div.darkBackground code { background: #222222; color: #f3f3f3; } div.markdown pre { padding: 0.5em 10px; border-radius: 5px; margin: 6px 0; } div.lightBackground pre { background: #dcdcdc; color: #222222; } div.darkBackground pre { background: #222222; color: #f3f3f3; } div.markdown pre > code { width: 100%; display: inline-block; box-sizing: border-box; tab-size: 2; overflow-x: auto; } div.markdown ol, div.markdown ul { padding-left: 1em; margin-left: 0.5em; } div.markdown a { text-decoration: underline; } -div.markdown a.chatMention { +div.markdown a.mention { text-decoration: none; color: inherit; font-weight: bold; } -div.lightBackground a.chatMention { +div.lightBackground a.mention { color: black; } div.lightBackground a { color: #2a5db0; } div.darkBackground a { color: white; } span.spoiler { background: var(--spoiler-background-color); color: var(--spoiler-text-color); cursor: pointer; -webkit-touch-callout: none; /* iOS Safari */ -webkit-user-select: none; /* Safari */ -moz-user-select: none; /* Old versions of Firefox */ -ms-user-select: none; /* Internet Explorer/Edge */ user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */ } span.spoiler:not(.revealSpoilerAnimation) a, -span.spoiler:not(.revealSpoilerAnimation) a.chatMention { +span.spoiler:not(.revealSpoilerAnimation) a.mention { pointer-events: none; color: var(--spoiler-text-color); } span.revealSpoilerAnimation, span.revealSpoilerAnimation a { animation: revealSpoiler 1s; } @keyframes revealSpoiler { from { background: var(--spoiler-background-color); color: var(--spoiler-text-color); } to { background: transparent; } } diff --git a/web/markdown/rules.react.js b/web/markdown/rules.react.js index 2301cc448..aca72b4cd 100644 --- a/web/markdown/rules.react.js +++ b/web/markdown/rules.react.js @@ -1,242 +1,248 @@ // @flow import _memoize from 'lodash/memoize.js'; import * as React from 'react'; import * as SimpleMarkdown from 'simple-markdown'; import * as SharedMarkdown from 'lib/shared/markdown.js'; import { chatMentionRegex } from 'lib/shared/mention-utils.js'; import type { RelativeMemberInfo, ThreadInfo, ChatMentionCandidates, } from 'lib/types/thread-types.js'; import MarkdownChatMention from './markdown-chat-mention.react.js'; import MarkdownSpoiler from './markdown-spoiler.react.js'; +import MarkdownUserMention from './markdown-user-mention.react.js'; export type MarkdownRules = { +simpleMarkdownRules: SharedMarkdown.ParserRules, +useDarkStyle: boolean, }; const linkRules: boolean => MarkdownRules = _memoize(useDarkStyle => { const simpleMarkdownRules = { // We are using default simple-markdown rules // For more details, look at native/markdown/rules.react link: { ...SimpleMarkdown.defaultRules.link, match: () => null, // eslint-disable-next-line react/display-name react: ( node: SharedMarkdown.SingleASTNode, output: SharedMarkdown.Output, state: SharedMarkdown.State, ) => ( {output(node.content, state)} ), }, paragraph: { ...SimpleMarkdown.defaultRules.paragraph, match: SimpleMarkdown.blockRegex(SharedMarkdown.paragraphRegex), // eslint-disable-next-line react/display-name react: ( node: SharedMarkdown.SingleASTNode, output: SharedMarkdown.Output, state: SharedMarkdown.State, ) => ( {output(node.content, state)} ), }, text: SimpleMarkdown.defaultRules.text, url: { ...SimpleMarkdown.defaultRules.url, match: SimpleMarkdown.inlineRegex(SharedMarkdown.urlRegex), }, }; return { simpleMarkdownRules: simpleMarkdownRules, useDarkStyle, }; }); const markdownRules: boolean => MarkdownRules = _memoize(useDarkStyle => { const linkMarkdownRules = linkRules(useDarkStyle); const simpleMarkdownRules = { ...linkMarkdownRules.simpleMarkdownRules, autolink: SimpleMarkdown.defaultRules.autolink, link: { ...linkMarkdownRules.simpleMarkdownRules.link, match: SimpleMarkdown.defaultRules.link.match, }, blockQuote: { ...SimpleMarkdown.defaultRules.blockQuote, // match end of blockQuote by either \n\n or end of string match: SharedMarkdown.matchBlockQuote(SharedMarkdown.blockQuoteRegex), parse: SharedMarkdown.parseBlockQuote, }, spoiler: { order: SimpleMarkdown.defaultRules.paragraph.order - 1, match: SimpleMarkdown.inlineRegex(SharedMarkdown.spoilerRegex), parse( capture: SharedMarkdown.Capture, parse: SharedMarkdown.Parser, state: SharedMarkdown.State, ) { const content = capture[1]; return { content: SimpleMarkdown.parseInline(parse, content, state), }; }, // eslint-disable-next-line react/display-name react: ( node: SharedMarkdown.SingleASTNode, output: SharedMarkdown.Output, state: SharedMarkdown.State, ) => ( ), }, inlineCode: SimpleMarkdown.defaultRules.inlineCode, em: SimpleMarkdown.defaultRules.em, strong: SimpleMarkdown.defaultRules.strong, del: SimpleMarkdown.defaultRules.del, u: SimpleMarkdown.defaultRules.u, heading: { ...SimpleMarkdown.defaultRules.heading, match: SimpleMarkdown.blockRegex(SharedMarkdown.headingRegex), }, mailto: SimpleMarkdown.defaultRules.mailto, codeBlock: { ...SimpleMarkdown.defaultRules.codeBlock, match: SimpleMarkdown.blockRegex(SharedMarkdown.codeBlockRegex), parse: (capture: SharedMarkdown.Capture) => ({ content: capture[0].replace(/^ {4}/gm, ''), }), }, fence: { ...SimpleMarkdown.defaultRules.fence, match: SimpleMarkdown.blockRegex(SharedMarkdown.fenceRegex), parse: (capture: SharedMarkdown.Capture) => ({ type: 'codeBlock', content: capture[2], }), }, json: { order: SimpleMarkdown.defaultRules.paragraph.order - 1, match: (source: string, state: SharedMarkdown.State) => { if (state.inline) { return null; } return SharedMarkdown.jsonMatch(source); }, parse: (capture: SharedMarkdown.Capture) => { const jsonCapture: SharedMarkdown.JSONCapture = (capture: any); return { type: 'codeBlock', content: SharedMarkdown.jsonPrint(jsonCapture), }; }, }, list: { ...SimpleMarkdown.defaultRules.list, match: SharedMarkdown.matchList, parse: SharedMarkdown.parseList, }, escape: SimpleMarkdown.defaultRules.escape, }; return { ...linkMarkdownRules, simpleMarkdownRules, useDarkStyle, }; }); function useTextMessageRulesFunc( threadInfo: ThreadInfo, chatMentionCandidates: ChatMentionCandidates, ): boolean => MarkdownRules { const { members } = threadInfo; return React.useMemo( () => _memoize<[boolean], MarkdownRules>((useDarkStyle: boolean) => textMessageRules(members, chatMentionCandidates, useDarkStyle), ), [chatMentionCandidates, members], ); } function textMessageRules( members: $ReadOnlyArray, chatMentionCandidates: ChatMentionCandidates, useDarkStyle: boolean, ): MarkdownRules { const baseRules = markdownRules(useDarkStyle); const membersMap = SharedMarkdown.createMemberMapForUserMentions(members); return { ...baseRules, simpleMarkdownRules: { ...baseRules.simpleMarkdownRules, userMention: { ...SimpleMarkdown.defaultRules.strong, match: SharedMarkdown.matchUserMentions(membersMap), - parse: (capture: SharedMarkdown.Capture) => ({ - content: capture[0], - }), + parse: (capture: SharedMarkdown.Capture) => + SharedMarkdown.parseUserMentions(membersMap, capture), // eslint-disable-next-line react/display-name react: ( node: SharedMarkdown.SingleASTNode, output: SharedMarkdown.Output, state: SharedMarkdown.State, - ) => {node.content}, + ) => ( + + ), }, chatMention: { ...SimpleMarkdown.defaultRules.strong, match: SimpleMarkdown.inlineRegex(chatMentionRegex), parse: (capture: SharedMarkdown.Capture) => SharedMarkdown.parseChatMention(chatMentionCandidates, capture), // eslint-disable-next-line react/display-name react: ( node: SharedMarkdown.SingleASTNode, output: SharedMarkdown.Output, state: SharedMarkdown.State, ) => ( ), }, }, }; } let defaultTextMessageRules = null; function getDefaultTextMessageRules( overrideDefaultChatMentionCandidates: ChatMentionCandidates = {}, ): MarkdownRules { if (Object.keys(overrideDefaultChatMentionCandidates).length > 0) { return textMessageRules([], overrideDefaultChatMentionCandidates, false); } if (!defaultTextMessageRules) { defaultTextMessageRules = textMessageRules([], {}, false); } return defaultTextMessageRules; } export { linkRules, useTextMessageRulesFunc, getDefaultTextMessageRules };