diff --git a/stylesheets/_quote.scss b/stylesheets/_quote.scss index 44b947cc7..31a990a60 100644 --- a/stylesheets/_quote.scss +++ b/stylesheets/_quote.scss @@ -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); diff --git a/ts/components/basic/MessageBodyHighlight.tsx b/ts/components/basic/MessageBodyHighlight.tsx index 504f87f27..986289e81 100644 --- a/ts/components/basic/MessageBodyHighlight.tsx +++ b/ts/components/basic/MessageBodyHighlight.tsx @@ -9,8 +9,8 @@ import { renderTextDefault, } from '../conversation/message/message-content/MessageBody'; -const renderNewLines: RenderTextCallbackType = ({ text, key }) => ( - +const renderNewLines: RenderTextCallbackType = ({ text, key, 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; -}) => ; +}) => ( + +); -export const MessageBodyHighlight = (props: { text: string }) => { - const { text } = props; +export const MessageBodyHighlight = (props: { text: string; isGroup: boolean }) => { + const { text, isGroup } = props; const results: Array = []; // this is matching what sqlite fts5 is giving us back const FIND_BEGIN_END = /<>(.+?)<>/g; @@ -41,7 +51,9 @@ export const MessageBodyHighlight = (props: { text: string }) => { let count = 1; if (!match) { - return ; + return ( + + ); } 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, })} ); @@ -83,6 +97,7 @@ export const MessageBodyHighlight = (props: { text: string }) => { sizeClass, key: count++, renderNonEmoji: renderNewLines, + isGroup, }) ); } diff --git a/ts/components/conversation/AddMentions.tsx b/ts/components/conversation/AddMentions.tsx index 58085b9e1..e14b3fbd1 100644 --- a/ts/components/conversation/AddMentions.tsx +++ b/ts/components/conversation/AddMentions.tsx @@ -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 { - public static defaultProps: Partial = { - renderOther: ({ text }) => text, - }; + isGroup: boolean; +}; - public render() { - const { text, renderOther, convoId } = this.props; - const results: Array = []; - 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 = []; + 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(); + 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(); - 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}; +}; diff --git a/ts/components/conversation/AddNewLines.tsx b/ts/components/conversation/AddNewLines.tsx index 01dbacd91..303d87a74 100644 --- a/ts/components/conversation/AddNewLines.tsx +++ b/ts/components/conversation/AddNewLines.tsx @@ -5,11 +5,12 @@ type Props = { text: string; /** Allows you to customize now non-newlines are rendered. Simplest is just a . */ 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}; } diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index cff79e884..a09051fa6 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -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) => { {shouldShowProfile ? ( - + ) : null} {shouldShowProfile ? ' ' : null} diff --git a/ts/components/conversation/Emojify.tsx b/ts/components/conversation/Emojify.tsx index 1458dd494..7ae61584d 100644 --- a/ts/components/conversation/Emojify.tsx +++ b/ts/components/conversation/Emojify.tsx @@ -10,16 +10,17 @@ type Props = { sizeClass: SizeClassType; /** Allows you to customize now non-newlines are rendered. Simplest is just a . */ 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': diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx index 9e17463bd..7da46700f 100644 --- a/ts/components/conversation/Linkify.tsx +++ b/ts/components/conversation/Linkify.tsx @@ -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 . */ renderNonLink?: RenderTextCallbackType; -} + isGroup: boolean; +}; const SUPPORTED_PROTOCOLS = /^(http|https):/i; -export class Linkify extends React.Component { - public static defaultProps: Partial = { - renderNonLink: ({ text }) => text, - }; - - public render() { - const { text, renderNonLink } = this.props; - const results: Array = []; - 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( - - {originalText} - - ); - } 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 = []; + let count = 1; + const dispatch = useDispatch(); + const matchData = linkify.match(text) || []; + let last = 0; // disable click on 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 { 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 { showExitIcon: true, onClickOk: openLink, onClickClose: () => { - window.inboxStore?.dispatch(updateConfirmModal(null)); + dispatch(updateConfirmModal(null)); }, onClickCancel: () => { @@ -99,4 +59,37 @@ export class Linkify extends React.Component { }) ); }; -} + + 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( + + {originalText} + + ); + } 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}; +}; diff --git a/ts/components/conversation/message/message-content/MessageBody.tsx b/ts/components/conversation/message/message-content/MessageBody.tsx index 02993e686..50f72c6f2 100644 --- a/ts/components/conversation/message/message-content/MessageBody.tsx +++ b/ts/components/conversation/message/message-content/MessageBody.tsx @@ -12,18 +12,26 @@ type Props = { disableJumbomoji: boolean; /** If set, links will be left alone instead of turned into clickable `` tags. */ disableLinks: boolean; + isGroup: boolean; }; -const renderMentions: RenderTextCallbackType = ({ text, key }) => ( - +const renderMentions: RenderTextCallbackType = ({ text, key, 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 ; + return ( + + ); }; const renderEmoji = ({ @@ -31,12 +39,22 @@ const renderEmoji = ({ key, sizeClass, renderNonEmoji, + isGroup, }: { text: string; key: number; sizeClass: SizeClassType; renderNonEmoji: RenderTextCallbackType; -}) => ; + isGroup: boolean; +}) => ( + +); /** * 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
{text.substring(4, length - 3)}
; + return
{text.substring(4, text.length - 3)}
; } return JsxSelectable( { return renderEmoji({ text: nonLinkText, sizeClass, key, renderNonEmoji: renderNewLines, + isGroup, }); }} /> diff --git a/ts/components/conversation/message/message-content/MessageText.tsx b/ts/components/conversation/message/message-content/MessageText.tsx index 136d68a7d..aee070f52 100644 --- a/ts/components/conversation/message/message-content/MessageText.tsx +++ b/ts/components/conversation/message/message-content/MessageText.tsx @@ -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 && } - + ); }; diff --git a/ts/components/conversation/message/message-content/Quote.tsx b/ts/components/conversation/message/message-content/Quote.tsx index e7f44e185..d3e2c2b23 100644 --- a/ts/components/conversation/message/message-content/Quote.tsx +++ b/ts/components/conversation/message/message-content/Quote.tsx @@ -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 (
- +
); } diff --git a/ts/components/leftpane/ContactListItem.tsx b/ts/components/leftpane/ContactListItem.tsx index 679046dc3..a294d0d96 100644 --- a/ts/components/leftpane/ContactListItem.tsx +++ b/ts/components/leftpane/ContactListItem.tsx @@ -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) => {
- text || ''} /> +
diff --git a/ts/components/leftpane/conversation-list-item/MessageItem.tsx b/ts/components/leftpane/conversation-list-item/MessageItem.tsx index 71883272b..a87384cfe 100644 --- a/ts/components/leftpane/conversation-list-item/MessageItem.tsx +++ b/ts/components/leftpane/conversation-list-item/MessageItem.tsx @@ -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 ? ( ) : ( - + )} diff --git a/ts/components/search/MessageSearchResults.tsx b/ts/components/search/MessageSearchResults.tsx index 926dbf32b..546226330 100644 --- a/ts/components/search/MessageSearchResults.tsx +++ b/ts/components/search/MessageSearchResults.tsx @@ -230,7 +230,7 @@ export const MessageSearchResult = (props: MessageResultProps) => { - + diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 51206eeb6..563d55d88 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -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; diff --git a/ts/types/Util.ts b/ts/types/Util.ts index 5f17f5e81..ccea6b792 100644 --- a/ts/types/Util.ts +++ b/ts/types/Util.ts @@ -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;