move mentions and emojify to a functional component

pull/2164/head
Audric Ackermann 3 years ago
parent 19722b6bdc
commit 2478a78794
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -21,6 +21,10 @@
.module-quote__primary__author {
color: var(--color-sent-message-text);
font-weight: bold;
.module-contact-name {
font-weight: bold;
}
}
.module-quote__primary__text {
color: var(--color-sent-message-text);
@ -35,6 +39,9 @@
.module-quote__primary__author {
color: var(--color-received-message-text);
font-weight: bold;
.module-contact-name {
font-weight: bold;
}
}
.module-quote__primary__text {
color: var(--color-received-message-text);

@ -9,8 +9,8 @@ import {
renderTextDefault,
} from '../conversation/message/message-content/MessageBody';
const renderNewLines: RenderTextCallbackType = ({ text, key }) => (
<AddNewLines key={key} text={text} renderNonNewLine={renderTextDefault} />
const renderNewLines: RenderTextCallbackType = ({ text, key, isGroup }) => (
<AddNewLines key={key} text={text} renderNonNewLine={renderTextDefault} isGroup={isGroup} />
);
const SnippetHighlight = styled.span`
@ -23,15 +23,25 @@ const renderEmoji = ({
key,
sizeClass,
renderNonEmoji,
isGroup,
}: {
text: string;
key: number;
isGroup: boolean;
sizeClass: SizeClassType;
renderNonEmoji: RenderTextCallbackType;
}) => <Emojify key={key} text={text} sizeClass={sizeClass} renderNonEmoji={renderNonEmoji} />;
}) => (
<Emojify
key={key}
text={text}
sizeClass={sizeClass}
renderNonEmoji={renderNonEmoji}
isGroup={isGroup}
/>
);
export const MessageBodyHighlight = (props: { text: string }) => {
const { text } = props;
export const MessageBodyHighlight = (props: { text: string; isGroup: boolean }) => {
const { text, isGroup } = props;
const results: Array<JSX.Element> = [];
// this is matching what sqlite fts5 is giving us back
const FIND_BEGIN_END = /<<left>>(.+?)<<right>>/g;
@ -41,7 +51,9 @@ export const MessageBodyHighlight = (props: { text: string }) => {
let count = 1;
if (!match) {
return <MessageBody disableJumbomoji={true} disableLinks={true} text={text} />;
return (
<MessageBody disableJumbomoji={true} disableLinks={true} text={text} isGroup={isGroup} />
);
}
const sizeClass = 'default';
@ -55,6 +67,7 @@ export const MessageBodyHighlight = (props: { text: string }) => {
sizeClass,
key: count++,
renderNonEmoji: renderNewLines,
isGroup,
})
);
}
@ -67,6 +80,7 @@ export const MessageBodyHighlight = (props: { text: string }) => {
sizeClass,
key: count++,
renderNonEmoji: renderNewLines,
isGroup,
})}
</SnippetHighlight>
);
@ -83,6 +97,7 @@ export const MessageBodyHighlight = (props: { text: string }) => {
sizeClass,
key: count++,
renderNonEmoji: renderNewLines,
isGroup,
})
);
}

@ -8,7 +8,6 @@ import React from 'react';
interface MentionProps {
key: string;
text: string;
convoId: string;
}
const Mention = (props: MentionProps) => {
@ -29,55 +28,48 @@ const Mention = (props: MentionProps) => {
}
};
interface Props {
type Props = {
text: string;
renderOther?: RenderTextCallbackType;
convoId: string;
}
export class AddMentions extends React.Component<Props> {
public static defaultProps: Partial<Props> = {
renderOther: ({ text }) => text,
};
isGroup: boolean;
};
public render() {
const { text, renderOther, convoId } = this.props;
const results: Array<any> = [];
const FIND_MENTIONS = new RegExp(`@${PubKey.regexForPubkeys}`, 'g');
const defaultRenderOther = ({ text }: { text: string }) => <>{text}</>;
// We have to do this, because renderOther is not required in our Props object,
// but it is always provided via defaultProps.
if (!renderOther) {
return;
}
export const AddMentions = (props: Props): JSX.Element => {
const { text, renderOther, isGroup } = props;
let match = FIND_MENTIONS.exec(text);
let last = 0;
let count = 1000;
const results: Array<JSX.Element> = [];
const FIND_MENTIONS = new RegExp(`@${PubKey.regexForPubkeys}`, 'g');
if (!match) {
return renderOther({ text, key: 0 });
}
const renderWith = renderOther || defaultRenderOther;
while (match) {
count++;
const key = count;
if (last < match.index) {
const otherText = text.slice(last, match.index);
results.push(renderOther({ text: otherText, key }));
}
let match = FIND_MENTIONS.exec(text);
let last = 0;
let count = 1000;
const pubkey = text.slice(match.index, FIND_MENTIONS.lastIndex);
results.push(<Mention text={pubkey} key={`${key}`} convoId={convoId} />);
if (!match) {
return renderWith({ text, key: 0, isGroup });
}
last = FIND_MENTIONS.lastIndex;
match = FIND_MENTIONS.exec(text);
while (match) {
count++;
const key = count;
if (last < match.index) {
const otherText = text.slice(last, match.index);
results.push(renderWith({ text: otherText, key, isGroup }));
}
if (last < text.length) {
results.push(renderOther({ text: text.slice(last), key: count++ }));
}
const pubkey = text.slice(match.index, FIND_MENTIONS.lastIndex);
results.push(<Mention text={pubkey} key={`${key}`} />);
return results;
last = FIND_MENTIONS.lastIndex;
match = FIND_MENTIONS.exec(text);
}
}
if (last < text.length) {
results.push(renderWith({ text: text.slice(last), key: count++, isGroup }));
}
return <>{results}</>;
};

@ -5,11 +5,12 @@ type Props = {
text: string;
/** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
renderNonNewLine: RenderTextCallbackType;
isGroup: boolean;
};
export const AddNewLines = (props: Props) => {
const { text, renderNonNewLine } = props;
const rendered = renderNonNewLine({ text, key: 0 });
const { text, renderNonNewLine, isGroup } = props;
const rendered = renderNonNewLine({ text, key: 0, isGroup });
if (typeof rendered === 'string') {
return <>{rendered}</>;
}

@ -2,7 +2,7 @@ import React from 'react';
import classNames from 'classnames';
import { Emojify } from './Emojify';
import { useConversationUsernameOrShorten } from '../../hooks/useParamSelector';
import { useConversationUsernameOrShorten, useIsPrivate } from '../../hooks/useParamSelector';
type Props = {
pubkey: string;
@ -19,7 +19,7 @@ export const ContactName = (props: Props) => {
const prefix = module ? module : 'module-contact-name';
const convoName = useConversationUsernameOrShorten(pubkey);
const isPrivate = useIsPrivate(pubkey);
const shouldShowProfile = Boolean(convoName || profileName || name);
const styles = (boldProfileName
? {
@ -32,7 +32,7 @@ export const ContactName = (props: Props) => {
<span className={classNames(prefix, compact && 'compact')} dir="auto">
{shouldShowProfile ? (
<span style={styles as any} className={`${prefix}__profile-name`}>
<Emojify text={textProfile} sizeClass="small" />
<Emojify text={textProfile} sizeClass="small" isGroup={!isPrivate} />
</span>
) : null}
{shouldShowProfile ? ' ' : null}

@ -10,16 +10,17 @@ type Props = {
sizeClass: SizeClassType;
/** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
renderNonEmoji?: RenderTextCallbackType;
isGroup: boolean;
};
const defaultRenderNonEmoji = (text: string | undefined) => text || '';
const defaultRenderNonEmoji = (text: string | undefined) => <>{text || ''}</>;
export const Emojify = (props: Props): JSX.Element => {
const { text, renderNonEmoji, sizeClass } = props;
const { text, renderNonEmoji, sizeClass, isGroup } = props;
if (!renderNonEmoji) {
return <>{defaultRenderNonEmoji(text)}</>;
}
const rendered = renderNonEmoji?.({ text: text || '', key: 1 });
const rendered = renderNonEmoji?.({ text: text || '', key: 1, isGroup });
let size = 1.0;
switch (sizeClass) {
case 'jumbo':

@ -7,71 +7,31 @@ import { isLinkSneaky } from '../../../js/modules/link_previews';
import { updateConfirmModal } from '../../state/ducks/modalDialog';
import { shell } from 'electron';
import { MessageInteraction } from '../../interactions';
import { useDispatch } from 'react-redux';
const linkify = LinkifyIt();
interface Props {
type Props = {
text: string;
/** Allows you to customize now non-links are rendered. Simplest is just a <span>. */
renderNonLink?: RenderTextCallbackType;
}
isGroup: boolean;
};
const SUPPORTED_PROTOCOLS = /^(http|https):/i;
export class Linkify extends React.Component<Props> {
public static defaultProps: Partial<Props> = {
renderNonLink: ({ text }) => text,
};
public render() {
const { text, renderNonLink } = this.props;
const results: Array<any> = [];
let count = 1;
const matchData = linkify.match(text) || [];
let last = 0;
// We have to do this, because renderNonLink is not required in our Props object,
// but it is always provided via defaultProps.
if (!renderNonLink) {
return;
}
if (matchData.length === 0) {
return renderNonLink({ text, key: 0 });
}
matchData.forEach((match: { index: number; url: string; lastIndex: number; text: string }) => {
if (last < match.index) {
const textWithNoLink = text.slice(last, match.index);
results.push(renderNonLink({ text: textWithNoLink, key: count++ }));
}
const { url, text: originalText } = match;
const isLink = SUPPORTED_PROTOCOLS.test(url) && !isLinkSneaky(url);
if (isLink) {
results.push(
<a key={count++} href={url} onClick={this.handleClick}>
{originalText}
</a>
);
} else {
results.push(renderNonLink({ text: originalText, key: count++ }));
}
last = match.lastIndex;
});
if (last < text.length) {
results.push(renderNonLink({ text: text.slice(last), key: count++ }));
}
return results;
}
const defaultRenderNonLink = ({ text }: { text: string }) => <>{text}</>;
export const Linkify = (props: Props): JSX.Element => {
const { text, isGroup, renderNonLink } = props;
const results: Array<any> = [];
let count = 1;
const dispatch = useDispatch();
const matchData = linkify.match(text) || [];
let last = 0;
// disable click on <a> elements so clicking a message containing a link doesn't
// select the message.The link will still be opened in the browser.
public handleClick = (e: any) => {
const handleClick = (e: any) => {
e.preventDefault();
e.stopPropagation();
@ -81,7 +41,7 @@ export class Linkify extends React.Component<Props> {
void shell.openExternal(url);
};
window.inboxStore?.dispatch(
dispatch(
updateConfirmModal({
title: window.i18n('linkVisitWarningTitle'),
message: window.i18n('linkVisitWarningMessage', url),
@ -90,7 +50,7 @@ export class Linkify extends React.Component<Props> {
showExitIcon: true,
onClickOk: openLink,
onClickClose: () => {
window.inboxStore?.dispatch(updateConfirmModal(null));
dispatch(updateConfirmModal(null));
},
onClickCancel: () => {
@ -99,4 +59,37 @@ export class Linkify extends React.Component<Props> {
})
);
};
}
const renderWith = renderNonLink || defaultRenderNonLink;
if (matchData.length === 0) {
return renderWith({ text, key: 0, isGroup });
}
matchData.forEach((match: { index: number; url: string; lastIndex: number; text: string }) => {
if (last < match.index) {
const textWithNoLink = text.slice(last, match.index);
results.push(renderWith({ text: textWithNoLink, isGroup, key: count++ }));
}
const { url, text: originalText } = match;
const isLink = SUPPORTED_PROTOCOLS.test(url) && !isLinkSneaky(url);
if (isLink) {
results.push(
<a key={count++} href={url} onClick={handleClick}>
{originalText}
</a>
);
} else {
results.push(renderWith({ text: originalText, isGroup, key: count++ }));
}
last = match.lastIndex;
});
if (last < text.length) {
results.push(renderWith({ text: text.slice(last), isGroup, key: count++ }));
}
return <>{results}</>;
};

@ -12,18 +12,26 @@ type Props = {
disableJumbomoji: boolean;
/** If set, links will be left alone instead of turned into clickable `<a>` tags. */
disableLinks: boolean;
isGroup: boolean;
};
const renderMentions: RenderTextCallbackType = ({ text, key }) => (
<AddMentions key={key} text={text} />
const renderMentions: RenderTextCallbackType = ({ text, key, isGroup }) => (
<AddMentions key={key} text={text} isGroup={isGroup} />
);
export const renderTextDefault: RenderTextCallbackType = ({ text }) => text;
export const renderTextDefault: RenderTextCallbackType = ({ text }) => <>{text}</>;
const renderNewLines: RenderTextCallbackType = ({ text: textWithNewLines, key, isGroup }) => {
const renderOther = isGroup ? renderMentions : renderTextDefault;
return <AddNewLines key={key} text={textWithNewLines} renderNonNewLine={renderOther} />;
return (
<AddNewLines
key={key}
text={textWithNewLines}
renderNonNewLine={renderOther}
isGroup={isGroup}
/>
);
};
const renderEmoji = ({
@ -31,12 +39,22 @@ const renderEmoji = ({
key,
sizeClass,
renderNonEmoji,
isGroup,
}: {
text: string;
key: number;
sizeClass: SizeClassType;
renderNonEmoji: RenderTextCallbackType;
}) => <Emojify key={key} text={text} sizeClass={sizeClass} renderNonEmoji={renderNonEmoji} />;
isGroup: boolean;
}) => (
<Emojify
key={key}
text={text}
sizeClass={sizeClass}
renderNonEmoji={renderNonEmoji}
isGroup={isGroup}
/>
);
/**
* This component makes it very easy to use all three of our message formatting
@ -60,7 +78,7 @@ const JsxSelectable = (jsx: JSX.Element): JSX.Element => {
);
};
export const MessageBody = (props: Props) => {
const { text, disableJumbomoji, disableLinks } = props;
const { text, disableJumbomoji, disableLinks, isGroup } = props;
const sizeClass: SizeClassType = disableJumbomoji ? 'default' : getEmojiSizeClass(text);
if (disableLinks) {
@ -70,24 +88,26 @@ export const MessageBody = (props: Props) => {
sizeClass,
key: 0,
renderNonEmoji: renderNewLines,
isGroup,
})
);
}
if (text && text.startsWith('```') && text.endsWith('```')) {
const length = text.length;
return <pre className="text-selectable">{text.substring(4, length - 3)}</pre>;
return <pre className="text-selectable">{text.substring(4, text.length - 3)}</pre>;
}
return JsxSelectable(
<Linkify
text={text}
isGroup={isGroup}
renderNonLink={({ key, text: nonLinkText }) => {
return renderEmoji({
text: nonLinkText,
sizeClass,
key,
renderNonEmoji: renderNewLines,
isGroup,
});
}}
/>

@ -15,7 +15,7 @@ type Props = {
export type MessageTextSelectorProps = Pick<
MessageRenderingProps,
'text' | 'direction' | 'status' | 'isDeleted'
'text' | 'direction' | 'status' | 'isDeleted' | 'conversationType'
>;
export const MessageText = (props: Props) => {
@ -25,7 +25,7 @@ export const MessageText = (props: Props) => {
if (!selected) {
return null;
}
const { text, direction, status, isDeleted } = selected;
const { text, direction, status, isDeleted, conversationType } = selected;
const contents = isDeleted
? window.i18n('messageDeletedPlaceholder')
@ -47,7 +47,12 @@ export const MessageText = (props: Props) => {
)}
>
{isDeleted && <SessionIcon iconType="delete" iconSize="small" />}
<MessageBody text={contents || ''} disableLinks={multiSelectMode} disableJumbomoji={false} />
<MessageBody
text={contents || ''}
disableLinks={multiSelectMode}
disableJumbomoji={false}
isGroup={conversationType === 'group'}
/>
</div>
);
};

@ -10,9 +10,13 @@ import { noop } from 'lodash';
import { useDisableDrag } from '../../../../hooks/useDisableDrag';
import { useEncryptedFileFetch } from '../../../../hooks/useEncryptedFileFetch';
import { PubKey } from '../../../../session/types';
import { isPublicGroupConversation } from '../../../../state/selectors/conversations';
import {
getSelectedConversationKey,
isPublicGroupConversation,
} from '../../../../state/selectors/conversations';
import { ContactName } from '../../ContactName';
import { MessageBody } from './MessageBody';
import { useIsPrivate } from '../../../../hooks/useParamSelector';
export type QuotePropsWithoutListener = {
attachment?: QuotedAttachmentType;
@ -225,6 +229,9 @@ export const QuoteText = (
) => {
const { text, attachment, isIncoming } = props;
const convoId = useSelector(getSelectedConversationKey);
const isGroup = useIsPrivate(convoId);
if (text) {
return (
<div
@ -234,7 +241,7 @@ export const QuoteText = (
isIncoming ? 'module-quote__primary__text--incoming' : null
)}
>
<MessageBody text={text} disableLinks={true} disableJumbomoji={true} />
<MessageBody text={text} disableLinks={true} disableJumbomoji={true} isGroup={isGroup} />
</div>
);
}

@ -1,6 +1,6 @@
import React from 'react';
import classNames from 'classnames';
import { useConversationUsername, useIsMe } from '../../hooks/useParamSelector';
import { useConversationUsername, useIsMe, useIsPrivate } from '../../hooks/useParamSelector';
import { Avatar, AvatarSize } from '../avatar/Avatar';
import { Emojify } from '../conversation/Emojify';
@ -20,6 +20,7 @@ export const ContactListItem = (props: Props) => {
const name = useConversationUsername(pubkey);
const isMe = useIsMe(pubkey);
const isGroup = !useIsPrivate(pubkey);
const title = name ? name : pubkey;
const displayName = isMe ? window.i18n('me') : title;
@ -36,7 +37,7 @@ export const ContactListItem = (props: Props) => {
<AvatarItem pubkey={pubkey} />
<div className="module-contact-list-item__text">
<div className="module-contact-list-item__text__name">
<Emojify text={displayName} sizeClass="small" renderNonEmoji={({ text }) => text || ''} />
<Emojify text={displayName} sizeClass="small" isGroup={isGroup} />
</div>
</div>
</div>

@ -1,7 +1,7 @@
import classNames from 'classnames';
import React, { useContext } from 'react';
import { isEmpty } from 'lodash';
import { useConversationPropsById } from '../../../hooks/useParamSelector';
import { useConversationPropsById, useIsPrivate } from '../../../hooks/useParamSelector';
import { MessageBody } from '../../conversation/message/message-content/MessageBody';
import { OutgoingMessageStatus } from '../../conversation/message/message-content/OutgoingMessageStatus';
import { TypingAnimation } from '../../conversation/TypingAnimation';
@ -26,6 +26,8 @@ export const MessageItem = (props: { isMessageRequest: boolean }) => {
const conversationId = useContext(ContextConversationId);
const convoProps = useMessageItemProps(conversationId);
const isGroup = !!useIsPrivate(conversationId);
const isSearchingMode = useSelector(isSearching);
if (!convoProps) {
return null;
@ -52,7 +54,7 @@ export const MessageItem = (props: { isMessageRequest: boolean }) => {
{isTyping ? (
<TypingAnimation />
) : (
<MessageBody text={text} disableJumbomoji={true} disableLinks={true} />
<MessageBody text={text} disableJumbomoji={true} disableLinks={true} isGroup={isGroup} />
)}
</div>
<MessageRequestButtons isMessageRequest={props.isMessageRequest} />

@ -230,7 +230,7 @@ export const MessageSearchResult = (props: MessageResultProps) => {
</ResultsHeader>
<ResultBody>
<FromUserInGroup authorPubkey={source} conversationId={conversationId} />
<MessageBodyHighlight text={snippet || ''} />
<MessageBodyHighlight text={snippet || ''} isGroup={!convoIsPrivate} />
</ResultBody>
</StyledResultText>
</StyledSearchResulsts>

@ -577,7 +577,7 @@ export const isRightPanelShowing = createSelector(
export const isMessageSelectionMode = createSelector(
getConversations,
(state: ConversationsStateType): boolean => state.selectedMessageIds.length > 0
(state: ConversationsStateType): boolean => Boolean(state.selectedMessageIds.length > 0)
);
export const getSelectedMessageIds = createSelector(
@ -907,13 +907,14 @@ export const getMessageTextProps = createSelector(getMessagePropsByMessageId, (p
return undefined;
}
const { direction, status, text, isDeleted } = props.propsForMessage;
const { direction, status, text, isDeleted, conversationType } = props.propsForMessage;
const msgProps: MessageTextSelectorProps = {
direction,
status,
text,
isDeleted,
conversationType,
};
return msgProps;

@ -3,7 +3,7 @@ import { LocalizerKeys } from './LocalizerKeys';
export type RenderTextCallbackType = (options: {
text: string;
key: number;
isGroup?: boolean;
}) => JSX.Element | string;
isGroup: boolean;
}) => JSX.Element;
export type LocalizerType = (key: LocalizerKeys, values?: Array<string>) => string;

Loading…
Cancel
Save