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 };