diff --git a/native/chat/chat-item-height-measurer.react.js b/native/chat/chat-item-height-measurer.react.js
index 3e5bc06d9..dc698fb11 100644
--- a/native/chat/chat-item-height-measurer.react.js
+++ b/native/chat/chat-item-height-measurer.react.js
@@ -1,179 +1,179 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import type { ChatMessageItem } from 'lib/selectors/chat-selectors.js';
import { messageID } from 'lib/shared/message-utils.js';
import { messageTypes, type MessageType } from 'lib/types/message-types.js';
import { entityTextToRawString } from 'lib/utils/entity-text.js';
import type { MeasurementTask } from './chat-context-provider.react.js';
import { useComposedMessageMaxWidth } from './composed-message-width.js';
import { dummyNodeForRobotextMessageHeightMeasurement } from './inner-robotext-message.react.js';
import { dummyNodeForTextMessageHeightMeasurement } from './inner-text-message.react.js';
import { MessageListContextProvider } from './message-list-types.js';
import { multimediaMessageContentSizes } from './multimedia-message-utils.js';
import { chatMessageItemKey } from './utils.js';
import NodeHeightMeasurer from '../components/node-height-measurer.react.js';
import { InputStateContext } from '../input/input-state.js';
type Props = {
+measurement: MeasurementTask,
};
const heightMeasurerKey = (item: ChatMessageItem) => {
if (item.itemType !== 'message') {
return null;
}
const { messageInfo } = item;
if (messageInfo.type === messageTypes.TEXT) {
return messageInfo.text;
} else if (item.robotext) {
const { threadID } = item.messageInfo;
return entityTextToRawString(item.robotext, { threadID });
}
return null;
};
const heightMeasurerDummy = (item: ChatMessageItem) => {
invariant(
item.itemType === 'message',
'NodeHeightMeasurer asked for dummy for non-message item',
);
const { messageInfo } = item;
if (messageInfo.type === messageTypes.TEXT) {
return dummyNodeForTextMessageHeightMeasurement(messageInfo.text);
} else if (item.robotext) {
return dummyNodeForRobotextMessageHeightMeasurement(
item.robotext,
item.messageInfo.threadID,
);
}
invariant(false, 'NodeHeightMeasurer asked for dummy for non-text message');
};
function ChatItemHeightMeasurer(props: Props) {
const composedMessageMaxWidth = useComposedMessageMaxWidth();
const inputState = React.useContext(InputStateContext);
const inputStatePendingUploads = inputState?.pendingUploads;
const { measurement } = props;
const { threadInfo } = measurement;
const heightMeasurerMergeItem = React.useCallback(
(item: ChatMessageItem, height: ?number) => {
if (item.itemType !== 'message') {
return item;
}
const { messageInfo } = item;
const messageType: MessageType = messageInfo.type;
invariant(
messageType !== messageTypes.SIDEBAR_SOURCE,
'Sidebar source messages should be replaced by sourceMessage before being measured',
);
if (
messageInfo.type === messageTypes.IMAGES ||
messageInfo.type === messageTypes.MULTIMEDIA
) {
// Conditional due to Flow...
const localMessageInfo = item.localMessageInfo
? item.localMessageInfo
: null;
const id = messageID(messageInfo);
const pendingUploads = inputStatePendingUploads?.[id];
const sizes = multimediaMessageContentSizes(
messageInfo,
composedMessageMaxWidth,
);
return {
itemType: 'message',
messageShapeType: 'multimedia',
messageInfo,
localMessageInfo,
threadInfo,
startsConversation: item.startsConversation,
startsCluster: item.startsCluster,
endsCluster: item.endsCluster,
threadCreatedFromMessage: item.threadCreatedFromMessage,
pendingUploads,
reactions: item.reactions,
...sizes,
};
}
invariant(
height !== null && height !== undefined,
'height should be set',
);
if (messageInfo.type === messageTypes.TEXT) {
// Conditional due to Flow...
const localMessageInfo = item.localMessageInfo
? item.localMessageInfo
: null;
return {
itemType: 'message',
messageShapeType: 'text',
messageInfo,
localMessageInfo,
threadInfo,
startsConversation: item.startsConversation,
startsCluster: item.startsCluster,
endsCluster: item.endsCluster,
threadCreatedFromMessage: item.threadCreatedFromMessage,
contentHeight: height,
reactions: item.reactions,
};
}
invariant(
item.messageInfoType !== 'composable',
'ChatItemHeightMeasurer was handed a messageInfoType=composable, but ' +
`does not know how to handle MessageType ${messageInfo.type}`,
);
invariant(
item.messageInfoType === 'robotext',
'ChatItemHeightMeasurer was handed a messageInfoType that it does ' +
`not recognize: ${item.messageInfoType}`,
);
return {
itemType: 'message',
messageShapeType: 'robotext',
messageInfo,
threadInfo,
startsConversation: item.startsConversation,
startsCluster: item.startsCluster,
endsCluster: item.endsCluster,
threadCreatedFromMessage: item.threadCreatedFromMessage,
robotext: item.robotext,
contentHeight: height,
reactions: item.reactions,
};
},
[composedMessageMaxWidth, inputStatePendingUploads, threadInfo],
);
return (
);
}
const MemoizedChatItemHeightMeasurer: React.ComponentType =
React.memo(ChatItemHeightMeasurer);
export default MemoizedChatItemHeightMeasurer;
diff --git a/native/chat/message-list-container.react.js b/native/chat/message-list-container.react.js
index cf13c9328..ae4889273 100644
--- a/native/chat/message-list-container.react.js
+++ b/native/chat/message-list-container.react.js
@@ -1,363 +1,362 @@
// @flow
import { useNavigationState } from '@react-navigation/native';
import invariant from 'invariant';
import * as React from 'react';
import { View } from 'react-native';
import genesis from 'lib/facts/genesis.js';
import {
type ChatMessageItem,
useMessageListData,
} from 'lib/selectors/chat-selectors.js';
import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
import {
userInfoSelectorForPotentialMembers,
userSearchIndexForPotentialMembers,
} from 'lib/selectors/user-selectors.js';
import { getPotentialMemberItems } from 'lib/shared/search-utils.js';
import {
useExistingThreadInfoFinder,
pendingThreadType,
} from 'lib/shared/thread-utils.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import type { AccountUserInfo, UserListItem } from 'lib/types/user-types.js';
import { type MessagesMeasurer, useHeightMeasurer } from './chat-context.js';
import { ChatInputBar } from './chat-input-bar.react.js';
import type { ChatNavigationProp } from './chat.react.js';
import MessageListThreadSearch from './message-list-thread-search.react.js';
import { MessageListContextProvider } from './message-list-types.js';
import MessageList from './message-list.react.js';
import ParentThreadHeader from './parent-thread-header.react.js';
import ContentLoading from '../components/content-loading.react.js';
import { InputStateContext } from '../input/input-state.js';
import {
OverlayContext,
type OverlayContextType,
} from '../navigation/overlay-context.js';
import type { NavigationRoute } from '../navigation/route-names.js';
import { ThreadSettingsRouteName } from '../navigation/route-names.js';
import { useSelector } from '../redux/redux-utils.js';
import { type Colors, useColors, useStyles } from '../themes/colors.js';
import type { ChatMessageItemWithHeight } from '../types/chat-types.js';
type BaseProps = {
+navigation: ChatNavigationProp<'MessageList'>,
+route: NavigationRoute<'MessageList'>,
};
type Props = {
...BaseProps,
// Redux state
+usernameInputText: string,
+updateUsernameInput: (text: string) => void,
+userInfoInputArray: $ReadOnlyArray,
+updateTagInput: (items: $ReadOnlyArray) => void,
+resolveToUser: (user: AccountUserInfo) => void,
+otherUserInfos: { [id: string]: AccountUserInfo },
+userSearchResults: $ReadOnlyArray,
+threadInfo: ThreadInfo,
+genesisThreadInfo: ?ThreadInfo,
+messageListData: ?$ReadOnlyArray,
+colors: Colors,
+styles: typeof unboundStyles,
// withOverlayContext
+overlayContext: ?OverlayContextType,
+measureMessages: MessagesMeasurer,
};
type State = {
+listDataWithHeights: ?$ReadOnlyArray,
};
class MessageListContainer extends React.PureComponent {
state: State = {
listDataWithHeights: null,
};
pendingListDataWithHeights: ?$ReadOnlyArray;
get frozen() {
const { overlayContext } = this.props;
invariant(
overlayContext,
'MessageListContainer should have OverlayContext',
);
return overlayContext.scrollBlockingModalStatus !== 'closed';
}
setListData = (
listDataWithHeights: $ReadOnlyArray,
) => {
this.setState({ listDataWithHeights });
};
componentDidMount() {
this.props.measureMessages(
this.props.messageListData,
this.props.threadInfo,
this.setListData,
);
}
componentDidUpdate(prevProps: Props) {
const oldListData = prevProps.messageListData;
const newListData = this.props.messageListData;
if (!newListData && oldListData) {
this.setState({ listDataWithHeights: null });
}
if (
oldListData !== newListData ||
prevProps.threadInfo !== this.props.threadInfo ||
prevProps.measureMessages !== this.props.measureMessages
) {
this.props.measureMessages(
newListData,
this.props.threadInfo,
this.allHeightsMeasured,
);
}
if (!this.frozen && this.pendingListDataWithHeights) {
this.setState({ listDataWithHeights: this.pendingListDataWithHeights });
this.pendingListDataWithHeights = undefined;
}
}
render() {
const { threadInfo, styles } = this.props;
const { listDataWithHeights } = this.state;
const { searching } = this.props.route.params;
let searchComponent = null;
if (searching) {
const { userInfoInputArray, genesisThreadInfo } = this.props;
// It's technically possible for the client to be missing the Genesis
// ThreadInfo when it first opens up (before the server delivers it)
let parentThreadHeader;
if (genesisThreadInfo) {
parentThreadHeader = (
);
}
searchComponent = (
<>
{parentThreadHeader}
>
);
}
const showMessageList =
!searching || this.props.userInfoInputArray.length > 0;
let messageList;
if (showMessageList && listDataWithHeights) {
messageList = (
);
} else if (showMessageList) {
messageList = (
);
}
const threadContentStyles = showMessageList
? [styles.threadContent]
: [styles.hiddenThreadContent];
const pointerEvents = showMessageList ? 'auto' : 'none';
const threadContent = (
{messageList}
);
return (
{searchComponent}
{threadContent}
);
}
allHeightsMeasured = (
listDataWithHeights: $ReadOnlyArray,
) => {
if (this.frozen) {
this.pendingListDataWithHeights = listDataWithHeights;
} else {
this.setState({ listDataWithHeights });
}
};
}
const unboundStyles = {
container: {
backgroundColor: 'listBackground',
flex: 1,
},
threadContent: {
flex: 1,
},
hiddenThreadContent: {
height: 0,
opacity: 0,
},
};
const ConnectedMessageListContainer: React.ComponentType =
React.memo(function ConnectedMessageListContainer(
props: BaseProps,
) {
const [usernameInputText, setUsernameInputText] = React.useState('');
const [userInfoInputArray, setUserInfoInputArray] = React.useState<
$ReadOnlyArray,
>([]);
const otherUserInfos = useSelector(userInfoSelectorForPotentialMembers);
const userSearchIndex = useSelector(userSearchIndexForPotentialMembers);
const userSearchResults = React.useMemo(
() =>
getPotentialMemberItems(
usernameInputText,
otherUserInfos,
userSearchIndex,
userInfoInputArray.map(userInfo => userInfo.id),
),
[usernameInputText, otherUserInfos, userSearchIndex, userInfoInputArray],
);
const [baseThreadInfo, setBaseThreadInfo] = React.useState(
props.route.params.threadInfo,
);
const existingThreadInfoFinder =
useExistingThreadInfoFinder(baseThreadInfo);
const isSearching = !!props.route.params.searching;
const threadInfo = React.useMemo(
() =>
existingThreadInfoFinder({
searching: isSearching,
userInfoInputArray,
}),
[existingThreadInfoFinder, isSearching, userInfoInputArray],
);
invariant(
threadInfo,
'threadInfo must be specified in messageListContainer',
);
const { setParams } = props.navigation;
const navigationStack = useNavigationState(state => state.routes);
React.useEffect(() => {
const topRoute = navigationStack[navigationStack.length - 1];
if (topRoute?.name !== ThreadSettingsRouteName) {
return;
}
setBaseThreadInfo(threadInfo);
if (isSearching) {
setParams({ searching: false });
}
}, [isSearching, navigationStack, setParams, threadInfo]);
const inputState = React.useContext(InputStateContext);
invariant(inputState, 'inputState should be set in MessageListContainer');
const hideSearch = React.useCallback(() => {
setBaseThreadInfo(threadInfo);
setParams({ searching: false });
}, [setParams, threadInfo]);
React.useEffect(() => {
if (!isSearching) {
return;
}
inputState.registerSendCallback(hideSearch);
return () => inputState.unregisterSendCallback(hideSearch);
}, [hideSearch, inputState, isSearching]);
React.useEffect(() => {
setParams({ threadInfo });
}, [setParams, threadInfo]);
const updateTagInput = React.useCallback(
(input: $ReadOnlyArray) => setUserInfoInputArray(input),
[],
);
const updateUsernameInput = React.useCallback(
(text: string) => setUsernameInputText(text),
[],
);
const { addReply } = inputState;
const resolveToUser = React.useCallback(
(user: AccountUserInfo) => {
const resolvedThreadInfo = existingThreadInfoFinder({
searching: true,
userInfoInputArray: [user],
});
invariant(
resolvedThreadInfo,
'resolvedThreadInfo must be specified in messageListContainer',
);
addReply('');
setBaseThreadInfo(resolvedThreadInfo);
setParams({ searching: false, threadInfo: resolvedThreadInfo });
},
[setParams, existingThreadInfoFinder, addReply],
);
- const threadID = threadInfo.id;
const messageListData = useMessageListData({
searching: isSearching,
userInfoInputArray,
threadInfo,
});
const colors = useColors();
const styles = useStyles(unboundStyles);
const overlayContext = React.useContext(OverlayContext);
const measureMessages = useHeightMeasurer();
const genesisThreadInfo = useSelector(
state => threadInfoSelector(state)[genesis.id],
);
return (
-
+
);
});
export default ConnectedMessageListContainer;
diff --git a/native/chat/message-list-types.js b/native/chat/message-list-types.js
index edf559505..792a2e9f4 100644
--- a/native/chat/message-list-types.js
+++ b/native/chat/message-list-types.js
@@ -1,86 +1,86 @@
// @flow
import { useNavigation } from '@react-navigation/native';
import invariant from 'invariant';
import * as React from 'react';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import { type UserInfo } from 'lib/types/user-types.js';
import type { MarkdownRules } from '../markdown/rules.react.js';
import { useTextMessageRulesFunc } from '../markdown/rules.react.js';
import { MessageListRouteName } from '../navigation/route-names.js';
export type MessageListParams = {
+threadInfo: ThreadInfo,
+pendingPersonalThreadUserInfo?: UserInfo,
+searching?: boolean,
};
export type MessageListContextType = {
+getTextMessageMarkdownRules: (useDarkStyle: boolean) => MarkdownRules,
};
const MessageListContext: React.Context =
React.createContext();
-function useMessageListContext(threadID: ?string) {
- const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadID);
+function useMessageListContext(threadInfo: ThreadInfo) {
+ const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadInfo);
return React.useMemo(
() => ({
getTextMessageMarkdownRules,
}),
[getTextMessageMarkdownRules],
);
}
type Props = {
+children: React.Node,
- +threadID: ?string,
+ +threadInfo: ThreadInfo,
};
function MessageListContextProvider(props: Props): React.Node {
- const context = useMessageListContext(props.threadID);
+ const context = useMessageListContext(props.threadInfo);
return (
{props.children}
);
}
type NavigateToThreadAction = {
+name: typeof MessageListRouteName,
+params: MessageListParams,
+key: string,
};
function createNavigateToThreadAction(
params: MessageListParams,
): NavigateToThreadAction {
return {
name: MessageListRouteName,
params,
key: `${MessageListRouteName}${params.threadInfo.id}`,
};
}
function useNavigateToThread(): (params: MessageListParams) => void {
const { navigate } = useNavigation();
return React.useCallback(
(params: MessageListParams) => {
navigate<'MessageList'>(createNavigateToThreadAction(params));
},
[navigate],
);
}
function useTextMessageMarkdownRules(useDarkStyle: boolean): MarkdownRules {
const messageListContext = React.useContext(MessageListContext);
invariant(messageListContext, 'DummyTextNode should have MessageListContext');
return messageListContext.getTextMessageMarkdownRules(useDarkStyle);
}
export {
MessageListContextProvider,
createNavigateToThreadAction,
useNavigateToThread,
useTextMessageMarkdownRules,
};
diff --git a/native/chat/text-message-tooltip-button.react.js b/native/chat/text-message-tooltip-button.react.js
index bba360180..ffcf5fd0c 100644
--- a/native/chat/text-message-tooltip-button.react.js
+++ b/native/chat/text-message-tooltip-button.react.js
@@ -1,198 +1,196 @@
// @flow
import * as React from 'react';
import Animated from 'react-native-reanimated';
import EmojiPicker from 'rn-emoji-keyboard';
import { localIDPrefix } from 'lib/shared/message-utils.js';
import { useCanCreateReactionFromMessage } from 'lib/shared/reaction-utils.js';
import { TooltipInlineEngagement } from './inline-engagement.react.js';
import { InnerTextMessage } from './inner-text-message.react.js';
import { MessageHeader } from './message-header.react.js';
import { MessageListContextProvider } from './message-list-types.js';
import { MessagePressResponderContext } from './message-press-responder-context.js';
import {
useSendReaction,
useReactionSelectionPopoverPosition,
} from './reaction-message-utils.js';
import ReactionSelectionPopover from './reaction-selection-popover.react.js';
import SidebarInputBarHeightMeasurer from './sidebar-input-bar-height-measurer.react.js';
import { useAnimatedMessageTooltipButton } from './utils.js';
import type { AppNavigationProp } from '../navigation/app-navigator.react.js';
import { useSelector } from '../redux/redux-utils.js';
import { useTooltipActions } from '../tooltip/tooltip-hooks.js';
import type { TooltipRoute } from '../tooltip/tooltip.react.js';
/* eslint-disable import/no-named-as-default-member */
const { Node, interpolateNode, Extrapolate } = Animated;
/* eslint-enable import/no-named-as-default-member */
type Props = {
+navigation: AppNavigationProp<'TextMessageTooltipModal'>,
+route: TooltipRoute<'TextMessageTooltipModal'>,
+progress: Node,
+isOpeningSidebar: boolean,
};
function TextMessageTooltipButton(props: Props): React.Node {
const { navigation, route, progress, isOpeningSidebar } = props;
const windowWidth = useSelector(state => state.dimensions.width);
const [sidebarInputBarHeight, setSidebarInputBarHeight] =
React.useState(null);
const onInputBarMeasured = React.useCallback((height: number) => {
setSidebarInputBarHeight(height);
}, []);
const { item, verticalBounds, initialCoordinates, margin } = route.params;
const {
style: messageContainerStyle,
threadColorOverride,
isThreadColorDarkOverride,
} = useAnimatedMessageTooltipButton({
sourceMessage: item,
initialCoordinates,
messageListVerticalBounds: verticalBounds,
progress,
targetInputBarHeight: sidebarInputBarHeight,
});
const headerStyle = React.useMemo(() => {
const bottom = initialCoordinates.height;
const opacity = interpolateNode(progress, {
inputRange: [0, 0.05],
outputRange: [0, 1],
extrapolate: Extrapolate.CLAMP,
});
return {
opacity,
position: 'absolute',
left: -initialCoordinates.x,
width: windowWidth,
bottom,
};
}, [initialCoordinates.height, initialCoordinates.x, progress, windowWidth]);
- const threadID = item.threadInfo.id;
-
const messagePressResponderContext = React.useMemo(
() => ({
onPressMessage: navigation.goBackOnce,
}),
[navigation.goBackOnce],
);
const inlineEngagement = React.useMemo(() => {
if (!item.threadCreatedFromMessage) {
return null;
}
return (
);
}, [initialCoordinates, isOpeningSidebar, item, progress, windowWidth]);
const { messageInfo, threadInfo, reactions } = item;
const nextLocalID = useSelector(state => state.nextLocalID);
const localID = `${localIDPrefix}${nextLocalID}`;
const canCreateReactionFromMessage = useCanCreateReactionFromMessage(
threadInfo,
messageInfo,
);
const sendReaction = useSendReaction(
messageInfo.id,
localID,
threadInfo.id,
reactions,
);
const reactionSelectionPopoverPosition = useReactionSelectionPopoverPosition({
initialCoordinates,
verticalBounds,
margin,
});
const [emojiPickerOpen, setEmojiPickerOpen] = React.useState(false);
const openEmojiPicker = React.useCallback(() => {
setEmojiPickerOpen(true);
}, []);
const reactionSelectionPopover = React.useMemo(() => {
if (!canCreateReactionFromMessage) {
return null;
}
return (
);
}, [
navigation,
route,
openEmojiPicker,
canCreateReactionFromMessage,
reactionSelectionPopoverPosition,
sendReaction,
]);
const tooltipRouteKey = route.key;
const { dismissTooltip } = useTooltipActions(navigation, tooltipRouteKey);
const onEmojiSelected = React.useCallback(
emoji => {
sendReaction(emoji.emoji);
dismissTooltip();
},
[sendReaction, dismissTooltip],
);
return (
-
+
{reactionSelectionPopover}
{inlineEngagement}
);
}
export default TextMessageTooltipButton;
diff --git a/native/markdown/rules.react.js b/native/markdown/rules.react.js
index a25ce93f8..595ba0e41 100644
--- a/native/markdown/rules.react.js
+++ b/native/markdown/rules.react.js
@@ -1,423 +1,411 @@
// @flow
import _memoize from 'lodash/memoize.js';
import * as React from 'react';
import { Text, View, Platform } from 'react-native';
-import { createSelector } from 'reselect';
import * as SimpleMarkdown from 'simple-markdown';
-import { relativeMemberInfoSelectorForMembersOfThread } from 'lib/selectors/user-selectors.js';
import * as SharedMarkdown from 'lib/shared/markdown.js';
-import type { RelativeMemberInfo } from 'lib/types/thread-types.js';
+import type { RelativeMemberInfo, ThreadInfo } from 'lib/types/thread-types.js';
import MarkdownLink from './markdown-link.react.js';
import MarkdownParagraph from './markdown-paragraph.react.js';
import MarkdownSpoiler from './markdown-spoiler.react.js';
import { getMarkdownStyles } from './styles.js';
-import { useSelector } from '../redux/redux-utils.js';
export type MarkdownRules = {
+simpleMarkdownRules: SharedMarkdown.ParserRules,
+emojiOnlyFactor: ?number,
// We need to use a Text container for Entry because it needs to match up
// exactly with TextInput. However, if we use a Text container, we can't
// support styles for things like blockQuote, which rely on rendering as a
// View, and Views can't be nested inside Texts without explicit height and
// width
+container: 'View' | 'Text',
};
// Entry requires a seamless transition between Markdown and TextInput
// components, so we can't do anything that would change the position of text
const inlineMarkdownRules: boolean => MarkdownRules = _memoize(useDarkStyle => {
const styles = getMarkdownStyles(useDarkStyle ? 'dark' : 'light');
const simpleMarkdownRules = {
// Matches 'https://google.com' during parse phase and returns a 'link' node
url: {
...SimpleMarkdown.defaultRules.url,
// simple-markdown is case-sensitive, but we don't want to be
match: SimpleMarkdown.inlineRegex(SharedMarkdown.urlRegex),
},
// Matches '[Google](https://google.com)' during parse phase and handles
// rendering all 'link' nodes, including for 'autolink' and 'url'
link: {
...SimpleMarkdown.defaultRules.link,
match: () => null,
react(
node: SharedMarkdown.SingleASTNode,
output: SharedMarkdown.Output,
state: SharedMarkdown.State,
) {
return (
{output(node.content, state)}
);
},
},
// Each line gets parsed into a 'paragraph' node. The AST returned by the
// parser will be an array of one or more 'paragraph' nodes
paragraph: {
...SimpleMarkdown.defaultRules.paragraph,
// simple-markdown's default RegEx collapses multiple newlines into one.
// We want to keep the newlines, but when rendering within a View, we
// strip just one trailing newline off, since the View adds vertical
// spacing between its children
match: (source: string, state: SharedMarkdown.State) => {
if (state.inline) {
return null;
} else if (state.container === 'View') {
return SharedMarkdown.paragraphStripTrailingNewlineRegex.exec(source);
} else {
return SharedMarkdown.paragraphRegex.exec(source);
}
},
parse(
capture: SharedMarkdown.Capture,
parse: SharedMarkdown.Parser,
state: SharedMarkdown.State,
) {
let content = capture[1];
if (state.container === 'View') {
// React Native renders empty lines with less height. We want to
// preserve the newline characters, so we replace empty lines with a
// single space
content = content.replace(/^$/m, ' ');
}
return {
content: SimpleMarkdown.parseInline(parse, content, state),
};
},
// eslint-disable-next-line react/display-name
react: (
node: SharedMarkdown.SingleASTNode,
output: SharedMarkdown.Output,
state: SharedMarkdown.State,
) => (
{output(node.content, state)}
),
},
// This is the leaf node in the AST returned by the parse phase
text: SimpleMarkdown.defaultRules.text,
};
return {
simpleMarkdownRules,
emojiOnlyFactor: null,
container: 'Text',
};
});
// We allow the most markdown features for TextMessage, which doesn't have the
// same requirements as Entry
const fullMarkdownRules: boolean => MarkdownRules = _memoize(useDarkStyle => {
const styles = getMarkdownStyles(useDarkStyle ? 'dark' : 'light');
const inlineRules = inlineMarkdownRules(useDarkStyle);
const simpleMarkdownRules = {
...inlineRules.simpleMarkdownRules,
// Matches '' during parse phase and returns a 'link'
// node
autolink: SimpleMarkdown.defaultRules.autolink,
// Matches '[Google](https://google.com)' during parse phase and handles
// rendering all 'link' nodes, including for 'autolink' and 'url'
link: {
...inlineRules.simpleMarkdownRules.link,
match: SimpleMarkdown.defaultRules.link.match,
},
mailto: SimpleMarkdown.defaultRules.mailto,
em: {
...SimpleMarkdown.defaultRules.em,
// eslint-disable-next-line react/display-name
react: (
node: SharedMarkdown.SingleASTNode,
output: SharedMarkdown.Output,
state: SharedMarkdown.State,
) => (
{output(node.content, state)}
),
},
strong: {
...SimpleMarkdown.defaultRules.strong,
// eslint-disable-next-line react/display-name
react: (
node: SharedMarkdown.SingleASTNode,
output: SharedMarkdown.Output,
state: SharedMarkdown.State,
) => (
{output(node.content, state)}
),
},
u: {
...SimpleMarkdown.defaultRules.u,
// eslint-disable-next-line react/display-name
react: (
node: SharedMarkdown.SingleASTNode,
output: SharedMarkdown.Output,
state: SharedMarkdown.State,
) => (
{output(node.content, state)}
),
},
del: {
...SimpleMarkdown.defaultRules.del,
// eslint-disable-next-line react/display-name
react: (
node: SharedMarkdown.SingleASTNode,
output: SharedMarkdown.Output,
state: SharedMarkdown.State,
) => (
{output(node.content, state)}
),
},
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,
// eslint-disable-next-line react/display-name
react: (
node: SharedMarkdown.SingleASTNode,
output: SharedMarkdown.Output,
state: SharedMarkdown.State,
) => (
{node.content}
),
},
heading: {
...SimpleMarkdown.defaultRules.heading,
match: SimpleMarkdown.blockRegex(
SharedMarkdown.headingStripFollowingNewlineRegex,
),
// eslint-disable-next-line react/display-name
react(
node: SharedMarkdown.SingleASTNode,
output: SharedMarkdown.Output,
state: SharedMarkdown.State,
) {
const headingStyle = styles['h' + node.level];
return (
{output(node.content, state)}
);
},
},
blockQuote: {
...SimpleMarkdown.defaultRules.blockQuote,
// match end of blockQuote by either \n\n or end of string
match: SharedMarkdown.matchBlockQuote(
SharedMarkdown.blockQuoteStripFollowingNewlineRegex,
),
parse: SharedMarkdown.parseBlockQuote,
// eslint-disable-next-line react/display-name
react: (
node: SharedMarkdown.SingleASTNode,
output: SharedMarkdown.Output,
state: SharedMarkdown.State,
) => {
const { isNestedQuote } = state;
const backgroundColor = isNestedQuote ? '#00000000' : '#00000066';
const borderLeftColor = (Platform.select({
ios: '#00000066',
default: isNestedQuote ? '#00000066' : '#000000A3',
}): string);
return (
{output(node.content, { ...state, isNestedQuote: true })}
);
},
},
codeBlock: {
...SimpleMarkdown.defaultRules.codeBlock,
match: SimpleMarkdown.blockRegex(
SharedMarkdown.codeBlockStripTrailingNewlineRegex,
),
parse(capture: SharedMarkdown.Capture) {
return {
content: capture[1].replace(/^ {4}/gm, ''),
};
},
// eslint-disable-next-line react/display-name
react: (
node: SharedMarkdown.SingleASTNode,
output: SharedMarkdown.Output,
state: SharedMarkdown.State,
) => (
{node.content}
),
},
fence: {
...SimpleMarkdown.defaultRules.fence,
match: SimpleMarkdown.blockRegex(
SharedMarkdown.fenceStripTrailingNewlineRegex,
),
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,
react(
node: SharedMarkdown.SingleASTNode,
output: SharedMarkdown.Output,
state: SharedMarkdown.State,
) {
const children = node.items.map((item, i) => {
const content = output(item, state);
const bulletValue = node.ordered ? node.start + i + '. ' : '\u2022 ';
return (
{bulletValue}
{content}
);
});
return {children};
},
},
escape: SimpleMarkdown.defaultRules.escape,
};
return {
...inlineRules,
simpleMarkdownRules,
emojiOnlyFactor: 2,
container: 'View',
};
});
function useTextMessageRulesFunc(
- threadID: ?string,
+ threadInfo: ThreadInfo,
): (useDarkStyle: boolean) => MarkdownRules {
- return useSelector(getTextMessageRulesFunction(threadID));
+ const { members } = threadInfo;
+ return React.useMemo(
+ () =>
+ _memoize<[boolean], MarkdownRules>((useDarkStyle: boolean) =>
+ textMessageRules(members, useDarkStyle),
+ ),
+ [members],
+ );
}
-const getTextMessageRulesFunction = _memoize((threadID: ?string) =>
- createSelector(
- relativeMemberInfoSelectorForMembersOfThread(threadID),
- (
- threadMembers: $ReadOnlyArray,
- ): (boolean => MarkdownRules) => {
- if (!threadID) {
- return fullMarkdownRules;
- }
- return _memoize<[boolean], MarkdownRules>((useDarkStyle: boolean) =>
- textMessageRules(threadMembers, useDarkStyle),
- );
- },
- ),
-);
-
function textMessageRules(
members: $ReadOnlyArray,
useDarkStyle: boolean,
): MarkdownRules {
const styles = getMarkdownStyles(useDarkStyle ? 'dark' : 'light');
const baseRules = fullMarkdownRules(useDarkStyle);
return {
...baseRules,
simpleMarkdownRules: {
...baseRules.simpleMarkdownRules,
mention: {
...SimpleMarkdown.defaultRules.strong,
match: SharedMarkdown.matchMentions(members),
parse: (capture: SharedMarkdown.Capture) => ({
content: capture[0],
}),
// eslint-disable-next-line react/display-name
react: (
node: SharedMarkdown.SingleASTNode,
output: SharedMarkdown.Output,
state: SharedMarkdown.State,
) => (
{node.content}
),
},
},
};
}
let defaultTextMessageRules = null;
function getDefaultTextMessageRules(): MarkdownRules {
if (!defaultTextMessageRules) {
defaultTextMessageRules = textMessageRules([], false);
}
return defaultTextMessageRules;
}
export {
inlineMarkdownRules,
useTextMessageRulesFunc,
getDefaultTextMessageRules,
};