From ad175fafd7ae1418c24ba3303a76c3240ea672d0 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Mon, 21 Jun 2021 14:43:27 +1000 Subject: [PATCH 1/7] WIP refactoring message component. --- ts/components/conversation/Message.tsx | 1359 ++++++++++++------------ 1 file changed, 684 insertions(+), 675 deletions(-) diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index aa74447ec..8a40cb9d3 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -93,6 +93,7 @@ import { } from '../../interactions/messageInteractions'; import { updateUserDetailsModal } from '../../state/ducks/modalDialog'; import { MessageInteraction } from '../../interactions'; +import { useState } from 'react'; // Same as MIN_WIDTH in ImageGrid.tsx const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200; @@ -107,706 +108,746 @@ interface State { const EXPIRATION_CHECK_MINIMUM = 2000; const EXPIRED_DELAY = 600; -class MessageInner extends React.PureComponent { - public handleImageErrorBound: () => void; - - public expirationCheckInterval: any; - public expiredTimeout: any; - public ctxMenuID: string; - - public constructor(props: MessageRegularProps) { - super(props); - - this.handleImageErrorBound = this.handleImageError.bind(this); - this.onReplyPrivate = this.onReplyPrivate.bind(this); - this.handleContextMenu = this.handleContextMenu.bind(this); - this.onAddModerator = this.onAddModerator.bind(this); - this.onRemoveFromModerator = this.onRemoveFromModerator.bind(this); - this.updatePlaybackSpeed = this.updatePlaybackSpeed.bind(this); - - this.state = { - expiring: false, - expired: false, - imageBroken: false, - playbackSpeed: 1, - }; - this.ctxMenuID = `ctx-menu-message-${uuid()}`; - } +const MessageInner = (props: MessageRegularProps) => { + + let handleImageErrorBound: () => void; + + let expirationCheckInterval: any; + let expiredTimeout: any; + let ctxMenuID: string; + + // public constructor(props: MessageRegularProps) { + // super(props); + + // this.handleImageErrorBound = this.handleImageError.bind(this); + // this.onReplyPrivate = this.onReplyPrivate.bind(this); + // this.handleContextMenu = this.handleContextMenu.bind(this); + // this.onAddModerator = this.onAddModerator.bind(this); + // this.onRemoveFromModerator = this.onRemoveFromModerator.bind(this); + // this.updatePlaybackSpeed = this.updatePlaybackSpeed.bind(this); + + // this.state = { + // expiring: false, + // expired: false, + // imageBroken: false, + // playbackSpeed: 1, + // }; + // this.ctxMenuID = `ctx-menu-message-${uuid()}`; + // } - public componentDidMount() { - const { expirationLength } = this.props; - if (!expirationLength) { + const [expiring, setExpiring] = useState(false); + const [expired, setExpired] = useState(false); + const [image, setImageBroken] = useState(false); + const [playbackSpeed, setPlaybackSpeed] = useState(1); + + ctxMenuID = `ctx-menu-message-${uuid()}`; + + + useEffect(() => { + if (!props.expirationLength) { return; } + const { expirationLength } = props; + const increment = getIncrement(expirationLength); const checkFrequency = Math.max(EXPIRATION_CHECK_MINIMUM, increment); - this.checkExpired(); + checkExpired(); - this.expirationCheckInterval = setInterval(() => { - this.checkExpired(); + expirationCheckInterval = setInterval(() => { + checkExpired(); }, checkFrequency); - } - public componentWillUnmount() { - if (this.expirationCheckInterval) { - clearInterval(this.expirationCheckInterval); - } - if (this.expiredTimeout) { - clearTimeout(this.expiredTimeout); - } - } + }, []) - public componentDidUpdate() { - this.checkExpired(); - } - public checkExpired() { + // equivalent to componentWillUpdate + useEffect(() => { + checkExpired(); + + // return occurs on unmount equivalent to componentWillUnmount + return () => { + if (expirationCheckInterval) { + clearInterval(expirationCheckInterval); + } + if (expiredTimeout) { + clearTimeout(expiredTimeout); + } + } + }) + + const checkExpired = () => { const now = Date.now(); - const { isExpired, expirationTimestamp, expirationLength } = this.props; + const { isExpired, expirationTimestamp, expirationLength } = props; if (!expirationTimestamp || !expirationLength) { return; } - if (this.expiredTimeout) { + if (expiredTimeout) { return; } if (isExpired || now >= expirationTimestamp) { - this.setState({ - expiring: true, - }); + setExpiring(true); - const setExpired = () => { - this.setState({ - expired: true, - }); + const triggerSetExpired = () => { + setExpired(true) }; - this.expiredTimeout = setTimeout(setExpired, EXPIRED_DELAY); + expiredTimeout = setTimeout(triggerSetExpired(), EXPIRED_DELAY); } } - public handleImageError() { - this.setState({ - imageBroken: true, - }); - } + const handleImageError = () => { + setImageBroken(true); +} // tslint:disable-next-line max-func-body-length cyclomatic-complexity public renderAttachment() { - const { - id, - attachments, - text, - collapseMetadata, - conversationType, - direction, - quote, - onClickAttachment, - multiSelectMode, - onSelectMessage, - } = this.props; - const { imageBroken } = this.state; - - if (!attachments || !attachments[0]) { - return null; - } - const firstAttachment = attachments[0]; - - // For attachments which aren't full-frame - const withContentBelow = Boolean(text); - const withContentAbove = - Boolean(quote) || (conversationType === 'group' && direction === 'incoming'); - const displayImage = canDisplayImage(attachments); - - if ( - displayImage && - !imageBroken && - ((isImage(attachments) && hasImage(attachments)) || - (isVideo(attachments) && hasVideoScreenshot(attachments))) - ) { - return ( -
- { - if (multiSelectMode) { - onSelectMessage(id); - } else if (onClickAttachment) { - onClickAttachment(attachment); - } - }} - /> -
- ); - } else if (!firstAttachment.pending && isAudio(attachments)) { - return ( -
{ - e.stopPropagation(); + const { + id, + attachments, + text, + collapseMetadata, + conversationType, + direction, + quote, + onClickAttachment, + multiSelectMode, + onSelectMessage, + } = this.props; + const { imageBroken } = this.state; + + if (!attachments || !attachments[0]) { + return null; + } + const firstAttachment = attachments[0]; + + // For attachments which aren't full-frame + const withContentBelow = Boolean(text); + const withContentAbove = + Boolean(quote) || (conversationType === 'group' && direction === 'incoming'); + const displayImage = canDisplayImage(attachments); + + if ( + displayImage && + !imageBroken && + ((isImage(attachments) && hasImage(attachments)) || + (isVideo(attachments) && hasVideoScreenshot(attachments))) + ) { + return ( +
+ { + if (multiSelectMode) { + onSelectMessage(id); + } else if (onClickAttachment) { + onClickAttachment(attachment); + } }} - > - -
- ); - } else { - const { pending, fileName, fileSize, contentType } = firstAttachment; - const extension = getExtensionForDisplay({ contentType, fileName }); - const isDangerous = isFileDangerous(fileName || ''); + /> +
+ ); + } else if (!firstAttachment.pending && isAudio(attachments)) { + return ( +
{ + e.stopPropagation(); + }} + > + +
+ ); + } else { + const { pending, fileName, fileSize, contentType } = firstAttachment; + const extension = getExtensionForDisplay({ contentType, fileName }); + const isDangerous = isFileDangerous(fileName || ''); - return ( -
- {pending ? ( -
- -
- ) : ( -
-
{ - if (this.props?.onDownload) { - e.stopPropagation(); - this.props.onDownload(firstAttachment); - } - }} - > - {extension ? ( -
- {extension} -
- ) : null} -
- {isDangerous ? ( -
-
-
- ) : null} -
- )} -
-
- {fileName} -
+ return ( +
+ {pending ? ( +
+ +
+ ) : ( +
{ + if (this.props?.onDownload) { + e.stopPropagation(); + this.props.onDownload(firstAttachment); + } + }} > - {fileSize} + {extension ? ( +
+ {extension} +
+ ) : null}
+ {isDangerous ? ( +
+
+
+ ) : null} +
+ )} +
+
+ {fileName} +
+
+ {fileSize}
- ); - } +
+ ); } +} // tslint:disable-next-line cyclomatic-complexity public renderPreview() { - const { - attachments, - conversationType, - direction, - onClickLinkPreview, - previews, - quote, - } = this.props; - - // Attachments take precedence over Link Previews - if (attachments && attachments.length) { - return null; - } + const { + attachments, + conversationType, + direction, + onClickLinkPreview, + previews, + quote, + } = this.props; + + // Attachments take precedence over Link Previews + if (attachments && attachments.length) { + return null; + } - if (!previews || previews.length < 1) { - return null; - } + if (!previews || previews.length < 1) { + return null; + } - const first = previews[0]; - if (!first) { - return null; - } + const first = previews[0]; + if (!first) { + return null; + } - const withContentAbove = - Boolean(quote) || (conversationType === 'group' && direction === 'incoming'); + const withContentAbove = + Boolean(quote) || (conversationType === 'group' && direction === 'incoming'); - const previewHasImage = first.image && isImageAttachment(first.image); - const width = first.image && first.image.width; - const isFullSizeImage = width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH; + const previewHasImage = first.image && isImageAttachment(first.image); + const width = first.image && first.image.width; + const isFullSizeImage = width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH; - return ( + return ( +
{ + if (onClickLinkPreview) { + onClickLinkPreview(first.url); + } + }} + > + {first.image && previewHasImage && isFullSizeImage ? ( + + ) : null}
{ - if (onClickLinkPreview) { - onClickLinkPreview(first.url); - } - }} > - {first.image && previewHasImage && isFullSizeImage ? ( - + {first.image && previewHasImage && !isFullSizeImage ? ( +
+ {window.i18n('previewThumbnail', +
) : null}
- {first.image && previewHasImage && !isFullSizeImage ? ( -
- {window.i18n('previewThumbnail', -
- ) : null} -
-
{first.title}
-
{first.domain}
-
+
{first.title}
+
{first.domain}
- ); - } +
+ ); +} public renderQuote() { - const { - conversationType, - direction, - quote, - isPublic, - convoId, - id, - multiSelectMode, - } = this.props; - - if (!quote || !quote.authorPhoneNumber) { - return null; - } + const { + conversationType, + direction, + quote, + isPublic, + convoId, + id, + multiSelectMode, + } = this.props; + + if (!quote || !quote.authorPhoneNumber) { + return null; + } - const withContentAbove = conversationType === 'group' && direction === 'incoming'; + const withContentAbove = conversationType === 'group' && direction === 'incoming'; - const shortenedPubkey = PubKey.shorten(quote.authorPhoneNumber); + const shortenedPubkey = PubKey.shorten(quote.authorPhoneNumber); - const displayedPubkey = quote.authorProfileName ? shortenedPubkey : quote.authorPhoneNumber; + const displayedPubkey = quote.authorProfileName ? shortenedPubkey : quote.authorPhoneNumber; - return ( - { - e.preventDefault(); - e.stopPropagation(); - if (multiSelectMode && id) { - this.props.onSelectMessage(id); - return; - } - const { authorPhoneNumber, messageId: quoteId, referencedMessageNotFound } = quote; - quote?.onClick({ - quoteAuthor: authorPhoneNumber, - quoteId, - referencedMessageNotFound, - }); - }} - text={quote.text} - attachment={quote.attachment} - isIncoming={direction === 'incoming'} - conversationType={conversationType} - convoId={convoId} - isPublic={isPublic} - authorPhoneNumber={displayedPubkey} - authorProfileName={quote.authorProfileName} - authorName={quote.authorName} - referencedMessageNotFound={quote.referencedMessageNotFound} - isFromMe={quote.isFromMe} - withContentAbove={withContentAbove} - /> - ); - } + return ( + { + e.preventDefault(); + e.stopPropagation(); + if (multiSelectMode && id) { + this.props.onSelectMessage(id); + return; + } + const { authorPhoneNumber, messageId: quoteId, referencedMessageNotFound } = quote; + quote?.onClick({ + quoteAuthor: authorPhoneNumber, + quoteId, + referencedMessageNotFound, + }); + }} + text={quote.text} + attachment={quote.attachment} + isIncoming={direction === 'incoming'} + conversationType={conversationType} + convoId={convoId} + isPublic={isPublic} + authorPhoneNumber={displayedPubkey} + authorProfileName={quote.authorProfileName} + authorName={quote.authorName} + referencedMessageNotFound={quote.referencedMessageNotFound} + isFromMe={quote.isFromMe} + withContentAbove={withContentAbove} + /> + ); +} public renderAvatar() { - const { - authorAvatarPath, - authorName, - authorPhoneNumber, - authorProfileName, - collapseMetadata, - isAdmin, - conversationType, - direction, - isPublic, - firstMessageOfSeries, - } = this.props; - - if (collapseMetadata || conversationType !== 'group' || direction === 'outgoing') { - return; - } - const userName = authorName || authorProfileName || authorPhoneNumber; - - if (!firstMessageOfSeries) { - return
; - } + const { + authorAvatarPath, + authorName, + authorPhoneNumber, + authorProfileName, + collapseMetadata, + isAdmin, + conversationType, + direction, + isPublic, + firstMessageOfSeries, + } = this.props; + + if (collapseMetadata || conversationType !== 'group' || direction === 'outgoing') { + return; + } + const userName = authorName || authorProfileName || authorPhoneNumber; - return ( -
- { - window.inboxStore?.dispatch( - updateUserDetailsModal({ - conversationId: this.props.convoId, - userName, - authorAvatarPath, - }) - ); - }} - pubkey={authorPhoneNumber} - /> - {isPublic && isAdmin && ( -
-
-
- )} -
- ); + if (!firstMessageOfSeries) { + return
; } - public renderText() { - const { text, direction, status, conversationType, convoId, multiSelectMode } = this.props; + return ( +
+ { + window.inboxStore?.dispatch( + updateUserDetailsModal({ + conversationId: this.props.convoId, + userName, + authorAvatarPath, + }) + ); + }} + pubkey={authorPhoneNumber} + /> + {isPublic && isAdmin && ( +
+
+
+ )} +
+ ); +} - const contents = - direction === 'incoming' && status === 'error' ? window.i18n('incomingError') : text; + public renderText() { + const { text, direction, status, conversationType, convoId, multiSelectMode } = this.props; - if (!contents) { - return null; - } + const contents = + direction === 'incoming' && status === 'error' ? window.i18n('incomingError') : text; - return ( -
- -
- ); + if (!contents) { + return null; } - public renderError(isCorrectSide: boolean) { - const { status, direction } = this.props; + return ( +
+ +
+ ); +} - if (!isCorrectSide || status !== 'error') { - return null; - } + public renderError(isCorrectSide: boolean) { + const { status, direction } = this.props; - return ( -
-
-
- ); + if (!isCorrectSide || status !== 'error') { + return null; } - public renderContextMenu() { - const { - attachments, - authorPhoneNumber, - convoId, - onCopyText, - direction, - status, - isDeletable, - id, - onSelectMessage, - onDeleteMessage, - onDownload, - onRetrySend, - onShowDetail, - isPublic, - isOpenGroupV2, - weAreAdmin, - isAdmin, - } = this.props; - - const showRetry = status === 'error' && direction === 'outgoing'; - const multipleAttachments = attachments && attachments.length > 1; - - const onContextMenuShown = () => { - window.contextMenuShown = true; - }; - - const onContextMenuHidden = () => { - // This function will called before the click event - // on the message would trigger (and I was unable to - // prevent propagation in this case), so use a short timeout - setTimeout(() => { - window.contextMenuShown = false; - }, 100); - }; - - const selectMessageText = window.i18n('selectMessage'); - const deleteMessageText = window.i18n('deleteMessage'); + return ( +
+
+
+ ); +} - return ( - - {!multipleAttachments && attachments && attachments[0] ? ( - { - if (onDownload) { - onDownload(attachments[0]); - } - }} - > - {window.i18n('downloadAttachment')} - - ) : null} + public renderContextMenu() { + const { + attachments, + authorPhoneNumber, + convoId, + onCopyText, + direction, + status, + isDeletable, + id, + onSelectMessage, + onDeleteMessage, + onDownload, + onRetrySend, + onShowDetail, + isPublic, + isOpenGroupV2, + weAreAdmin, + isAdmin, + } = this.props; + + const showRetry = status === 'error' && direction === 'outgoing'; + const multipleAttachments = attachments && attachments.length > 1; + + const onContextMenuShown = () => { + window.contextMenuShown = true; + }; + + const onContextMenuHidden = () => { + // This function will called before the click event + // on the message would trigger (and I was unable to + // prevent propagation in this case), so use a short timeout + setTimeout(() => { + window.contextMenuShown = false; + }, 100); + }; + + const selectMessageText = window.i18n('selectMessage'); + const deleteMessageText = window.i18n('deleteMessage'); - {isAudio(attachments) ? ( - - {window.i18n('playAtCustomSpeed', this.state.playbackSpeed === 1 ? 2 : 1)} - - ) : null} - {window.i18n('copyMessage')} - {window.i18n('replyToMessage')} - {window.i18n('moreInformation')} - {showRetry ? {window.i18n('resend')} : null} - {isDeletable ? ( - <> - { - onSelectMessage(id); - }} - > - {selectMessageText} - - { - onDeleteMessage(id); - }} - > - {deleteMessageText} - - - ) : null} - {weAreAdmin && isPublic ? ( + return ( + + {!multipleAttachments && attachments && attachments[0] ? ( + { + if (onDownload) { + onDownload(attachments[0]); + } + }} + > + {window.i18n('downloadAttachment')} + + ) : null} + + {isAudio(attachments) ? ( + + {window.i18n('playAtCustomSpeed', this.state.playbackSpeed === 1 ? 2 : 1)} + + ) : null} + {window.i18n('copyMessage')} + {window.i18n('replyToMessage')} + {window.i18n('moreInformation')} + {showRetry ? {window.i18n('resend')} : null} + {isDeletable ? ( + <> { - MessageInteraction.banUser(authorPhoneNumber, convoId); + onSelectMessage(id); }} > - {window.i18n('banUser')} + {selectMessageText} - ) : null} - {weAreAdmin && isOpenGroupV2 ? ( { - MessageInteraction.unbanUser(authorPhoneNumber, convoId); + onDeleteMessage(id); }} > - {window.i18n('unbanUser')} + {deleteMessageText} - ) : null} - {weAreAdmin && isPublic && !isAdmin ? ( - {window.i18n('addAsModerator')} - ) : null} - {weAreAdmin && isPublic && isAdmin ? ( - {window.i18n('removeFromModerators')} - ) : null} - - ); - } + + ) : null} + {weAreAdmin && isPublic ? ( + { + MessageInteraction.banUser(authorPhoneNumber, convoId); + }} + > + {window.i18n('banUser')} + + ) : null} + {weAreAdmin && isOpenGroupV2 ? ( + { + MessageInteraction.unbanUser(authorPhoneNumber, convoId); + }} + > + {window.i18n('unbanUser')} + + ) : null} + {weAreAdmin && isPublic && !isAdmin ? ( + {window.i18n('addAsModerator')} + ) : null} + {weAreAdmin && isPublic && isAdmin ? ( + {window.i18n('removeFromModerators')} + ) : null} + + ); +} public getWidth(): number | undefined { - const { attachments, previews } = this.props; + const { attachments, previews } = this.props; - if (attachments && attachments.length) { - const dimensions = getGridDimensions(attachments); - if (dimensions) { - return dimensions.width; - } + if (attachments && attachments.length) { + const dimensions = getGridDimensions(attachments); + if (dimensions) { + return dimensions.width; } + } - if (previews && previews.length) { - const first = previews[0]; + if (previews && previews.length) { + const first = previews[0]; - if (!first || !first.image) { - return; - } - const { width } = first.image; + if (!first || !first.image) { + return; + } + const { width } = first.image; - if (isImageAttachment(first.image) && width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH) { - const dimensions = getImageDimensions(first.image); - if (dimensions) { - return dimensions.width; - } + if (isImageAttachment(first.image) && width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH) { + const dimensions = getImageDimensions(first.image); + if (dimensions) { + return dimensions.width; } } - - return; } - public isShowingImage(): boolean { - const { attachments, previews } = this.props; - const { imageBroken } = this.state; + return; +} - if (imageBroken) { - return false; - } + public isShowingImage(): boolean { + const { attachments, previews } = this.props; + const { imageBroken } = this.state; - if (attachments && attachments.length) { - const displayImage = canDisplayImage(attachments); + if (imageBroken) { + return false; + } - return Boolean( - displayImage && - ((isImage(attachments) && hasImage(attachments)) || - (isVideo(attachments) && hasVideoScreenshot(attachments))) - ); - } + if (attachments && attachments.length) { + const displayImage = canDisplayImage(attachments); - if (previews && previews.length) { - const first = previews[0]; - const { image } = first; + return Boolean( + displayImage && + ((isImage(attachments) && hasImage(attachments)) || + (isVideo(attachments) && hasVideoScreenshot(attachments))) + ); + } - if (!image) { - return false; - } + if (previews && previews.length) { + const first = previews[0]; + const { image } = first; - return isImageAttachment(image); + if (!image) { + return false; } - return false; + return isImageAttachment(image); } + return false; +} + // tslint:disable-next-line: cyclomatic-complexity public render() { - const { - direction, - id, - selected, - multiSelectMode, - conversationType, - isPublic, - text, - isUnread, - markRead, - } = this.props; - const { expired, expiring } = this.state; - - if (expired) { - return null; - } + const { + direction, + id, + selected, + multiSelectMode, + conversationType, + isPublic, + text, + isUnread, + markRead, + } = this.props; + const { expired, expiring } = this.state; + + if (expired) { + return null; + } - const width = this.getWidth(); - const isShowingImage = this.isShowingImage(); + const width = this.getWidth(); + const isShowingImage = this.isShowingImage(); - const isIncoming = direction === 'incoming'; - const shouldMarkReadWhenVisible = isIncoming && isUnread; - const divClasses = ['session-message-wrapper']; + const isIncoming = direction === 'incoming'; + const shouldMarkReadWhenVisible = isIncoming && isUnread; + const divClasses = ['session-message-wrapper']; - if (selected) { - divClasses.push('message-selected'); - } + if (selected) { + divClasses.push('message-selected'); + } - if (conversationType === 'group') { - divClasses.push('public-chat-message-wrapper'); - } + if (conversationType === 'group') { + divClasses.push('public-chat-message-wrapper'); + } - if (this.props.isQuotedMessageToAnimate) { - divClasses.push('flash-green-once'); + if (this.props.isQuotedMessageToAnimate) { + divClasses.push('flash-green-once'); + } + + const onVisible = (inView: boolean) => { + if (inView && shouldMarkReadWhenVisible) { + // mark the message as read. + // this will trigger the expire timer. + void markRead(Date.now()); } + }; - const onVisible = (inView: boolean) => { - if (inView && shouldMarkReadWhenVisible) { - // mark the message as read. - // this will trigger the expire timer. - void markRead(Date.now()); - } - }; + return ( + + {this.renderAvatar()} +
{ + const selection = window.getSelection(); + // Text is being selected + if (selection && selection.type === 'Range') { + return; + } - return ( - - {this.renderAvatar()} + {this.renderError(isIncoming)} +
{ const selection = window.getSelection(); @@ -817,10 +858,7 @@ class MessageInner extends React.PureComponent { // User clicked on message body const target = event.target as HTMLDivElement; - if ( - (!multiSelectMode && target.className === 'text-selectable') || - window.contextMenuShown - ) { + if (target.className === 'text-selectable' || window.contextMenuShown) { return; } @@ -829,122 +867,93 @@ class MessageInner extends React.PureComponent { } }} > - {this.renderError(isIncoming)} - -
{ - const selection = window.getSelection(); - // Text is being selected - if (selection && selection.type === 'Range') { - return; - } - - // User clicked on message body - const target = event.target as HTMLDivElement; - if (target.className === 'text-selectable' || window.contextMenuShown) { - return; - } - - if (id) { - this.props.onSelectMessage(id); - } - }} - > - {this.renderAuthor()} - {this.renderQuote()} - {this.renderAttachment()} - {this.renderPreview()} - {this.renderText()} - -
- {this.renderError(!isIncoming)} - {this.renderContextMenu()} + {this.renderAuthor()} + {this.renderQuote()} + {this.renderAttachment()} + {this.renderPreview()} + {this.renderText()} +
-
- ); - } + {this.renderError(!isIncoming)} + {this.renderContextMenu()} +
+
+ ); +} /** * Doubles / halves the playback speed based on the current playback speed. */ private updatePlaybackSpeed() { - this.setState({ - ...this.state, - playbackSpeed: this.state.playbackSpeed === 1 ? 2 : 1, - }); - } + this.setState({ + ...this.state, + playbackSpeed: this.state.playbackSpeed === 1 ? 2 : 1, + }); +} private handleContextMenu(e: any) { - e.preventDefault(); - e.stopPropagation(); - const { multiSelectMode, isKickedFromGroup } = this.props; - const enableContextMenu = !multiSelectMode && !isKickedFromGroup; - - if (enableContextMenu) { - // Don't forget to pass the id and the event and voila! - contextMenu.hideAll(); - contextMenu.show({ - id: this.ctxMenuID, - event: e, - }); - } + e.preventDefault(); + e.stopPropagation(); + const { multiSelectMode, isKickedFromGroup } = this.props; + const enableContextMenu = !multiSelectMode && !isKickedFromGroup; + + if (enableContextMenu) { + // Don't forget to pass the id and the event and voila! + contextMenu.hideAll(); + contextMenu.show({ + id: this.ctxMenuID, + event: e, + }); } +} private renderAuthor() { - const { - authorName, - authorPhoneNumber, - authorProfileName, - conversationType, - direction, - isPublic, - } = this.props; - - const title = authorName ? authorName : authorPhoneNumber; - - if (direction !== 'incoming' || conversationType !== 'group' || !title) { - return null; - } + const { + authorName, + authorPhoneNumber, + authorProfileName, + conversationType, + direction, + isPublic, + } = this.props; + + const title = authorName ? authorName : authorPhoneNumber; + + if (direction !== 'incoming' || conversationType !== 'group' || !title) { + return null; + } - const shortenedPubkey = PubKey.shorten(authorPhoneNumber); + const shortenedPubkey = PubKey.shorten(authorPhoneNumber); - const displayedPubkey = authorProfileName ? shortenedPubkey : authorPhoneNumber; + const displayedPubkey = authorProfileName ? shortenedPubkey : authorPhoneNumber; - return ( -
- -
- ); - } + return ( +
+ +
+ ); +} private onReplyPrivate(e: any) { - if (this.props && this.props.onReply) { - this.props.onReply(this.props.timestamp); - } + if (this.props && this.props.onReply) { + this.props.onReply(this.props.timestamp); } +} private async onAddModerator() { - await addSenderAsModerator(this.props.authorPhoneNumber, this.props.convoId); - } + await addSenderAsModerator(this.props.authorPhoneNumber, this.props.convoId); +} private async onRemoveFromModerator() { - await removeSenderFromModerator(this.props.authorPhoneNumber, this.props.convoId); - } + await removeSenderFromModerator(this.props.authorPhoneNumber, this.props.convoId); +} } export const Message = withTheme(MessageInner); From 4776c6bd576147086bc397ea14fc3aaa7f302ba3 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Mon, 21 Jun 2021 16:37:54 +1000 Subject: [PATCH 2/7] Revert "WIP refactoring message component." This reverts commit ad175fafd7ae1418c24ba3303a76c3240ea672d0. --- ts/components/conversation/Message.tsx | 1359 ++++++++++++------------ 1 file changed, 675 insertions(+), 684 deletions(-) diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 8a40cb9d3..aa74447ec 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -93,7 +93,6 @@ import { } from '../../interactions/messageInteractions'; import { updateUserDetailsModal } from '../../state/ducks/modalDialog'; import { MessageInteraction } from '../../interactions'; -import { useState } from 'react'; // Same as MIN_WIDTH in ImageGrid.tsx const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200; @@ -108,746 +107,706 @@ interface State { const EXPIRATION_CHECK_MINIMUM = 2000; const EXPIRED_DELAY = 600; -const MessageInner = (props: MessageRegularProps) => { - - let handleImageErrorBound: () => void; - - let expirationCheckInterval: any; - let expiredTimeout: any; - let ctxMenuID: string; - - // public constructor(props: MessageRegularProps) { - // super(props); - - // this.handleImageErrorBound = this.handleImageError.bind(this); - // this.onReplyPrivate = this.onReplyPrivate.bind(this); - // this.handleContextMenu = this.handleContextMenu.bind(this); - // this.onAddModerator = this.onAddModerator.bind(this); - // this.onRemoveFromModerator = this.onRemoveFromModerator.bind(this); - // this.updatePlaybackSpeed = this.updatePlaybackSpeed.bind(this); - - // this.state = { - // expiring: false, - // expired: false, - // imageBroken: false, - // playbackSpeed: 1, - // }; - // this.ctxMenuID = `ctx-menu-message-${uuid()}`; - // } - - const [expiring, setExpiring] = useState(false); - const [expired, setExpired] = useState(false); - const [image, setImageBroken] = useState(false); - const [playbackSpeed, setPlaybackSpeed] = useState(1); - - ctxMenuID = `ctx-menu-message-${uuid()}`; - +class MessageInner extends React.PureComponent { + public handleImageErrorBound: () => void; + + public expirationCheckInterval: any; + public expiredTimeout: any; + public ctxMenuID: string; + + public constructor(props: MessageRegularProps) { + super(props); + + this.handleImageErrorBound = this.handleImageError.bind(this); + this.onReplyPrivate = this.onReplyPrivate.bind(this); + this.handleContextMenu = this.handleContextMenu.bind(this); + this.onAddModerator = this.onAddModerator.bind(this); + this.onRemoveFromModerator = this.onRemoveFromModerator.bind(this); + this.updatePlaybackSpeed = this.updatePlaybackSpeed.bind(this); + + this.state = { + expiring: false, + expired: false, + imageBroken: false, + playbackSpeed: 1, + }; + this.ctxMenuID = `ctx-menu-message-${uuid()}`; + } - useEffect(() => { - if (!props.expirationLength) { + public componentDidMount() { + const { expirationLength } = this.props; + if (!expirationLength) { return; } - const { expirationLength } = props; - const increment = getIncrement(expirationLength); const checkFrequency = Math.max(EXPIRATION_CHECK_MINIMUM, increment); - checkExpired(); + this.checkExpired(); - expirationCheckInterval = setInterval(() => { - checkExpired(); + this.expirationCheckInterval = setInterval(() => { + this.checkExpired(); }, checkFrequency); + } - }, []) - - - // equivalent to componentWillUpdate - useEffect(() => { - checkExpired(); - - // return occurs on unmount equivalent to componentWillUnmount - return () => { - if (expirationCheckInterval) { - clearInterval(expirationCheckInterval); - } - if (expiredTimeout) { - clearTimeout(expiredTimeout); - } + public componentWillUnmount() { + if (this.expirationCheckInterval) { + clearInterval(this.expirationCheckInterval); + } + if (this.expiredTimeout) { + clearTimeout(this.expiredTimeout); } - }) + } - const checkExpired = () => { + public componentDidUpdate() { + this.checkExpired(); + } + + public checkExpired() { const now = Date.now(); - const { isExpired, expirationTimestamp, expirationLength } = props; + const { isExpired, expirationTimestamp, expirationLength } = this.props; if (!expirationTimestamp || !expirationLength) { return; } - if (expiredTimeout) { + if (this.expiredTimeout) { return; } if (isExpired || now >= expirationTimestamp) { - setExpiring(true); + this.setState({ + expiring: true, + }); - const triggerSetExpired = () => { - setExpired(true) + const setExpired = () => { + this.setState({ + expired: true, + }); }; - expiredTimeout = setTimeout(triggerSetExpired(), EXPIRED_DELAY); + this.expiredTimeout = setTimeout(setExpired, EXPIRED_DELAY); } } - const handleImageError = () => { - setImageBroken(true); -} + public handleImageError() { + this.setState({ + imageBroken: true, + }); + } // tslint:disable-next-line max-func-body-length cyclomatic-complexity public renderAttachment() { - const { - id, - attachments, - text, - collapseMetadata, - conversationType, - direction, - quote, - onClickAttachment, - multiSelectMode, - onSelectMessage, - } = this.props; - const { imageBroken } = this.state; - - if (!attachments || !attachments[0]) { - return null; - } - const firstAttachment = attachments[0]; - - // For attachments which aren't full-frame - const withContentBelow = Boolean(text); - const withContentAbove = - Boolean(quote) || (conversationType === 'group' && direction === 'incoming'); - const displayImage = canDisplayImage(attachments); - - if ( - displayImage && - !imageBroken && - ((isImage(attachments) && hasImage(attachments)) || - (isVideo(attachments) && hasVideoScreenshot(attachments))) - ) { - return ( -
- { - if (multiSelectMode) { - onSelectMessage(id); - } else if (onClickAttachment) { - onClickAttachment(attachment); - } + const { + id, + attachments, + text, + collapseMetadata, + conversationType, + direction, + quote, + onClickAttachment, + multiSelectMode, + onSelectMessage, + } = this.props; + const { imageBroken } = this.state; + + if (!attachments || !attachments[0]) { + return null; + } + const firstAttachment = attachments[0]; + + // For attachments which aren't full-frame + const withContentBelow = Boolean(text); + const withContentAbove = + Boolean(quote) || (conversationType === 'group' && direction === 'incoming'); + const displayImage = canDisplayImage(attachments); + + if ( + displayImage && + !imageBroken && + ((isImage(attachments) && hasImage(attachments)) || + (isVideo(attachments) && hasVideoScreenshot(attachments))) + ) { + return ( +
+ { + if (multiSelectMode) { + onSelectMessage(id); + } else if (onClickAttachment) { + onClickAttachment(attachment); + } + }} + /> +
+ ); + } else if (!firstAttachment.pending && isAudio(attachments)) { + return ( +
{ + e.stopPropagation(); }} - /> -
- ); - } else if (!firstAttachment.pending && isAudio(attachments)) { - return ( -
{ - e.stopPropagation(); - }} - > - -
- ); - } else { - const { pending, fileName, fileSize, contentType } = firstAttachment; - const extension = getExtensionForDisplay({ contentType, fileName }); - const isDangerous = isFileDangerous(fileName || ''); + > + +
+ ); + } else { + const { pending, fileName, fileSize, contentType } = firstAttachment; + const extension = getExtensionForDisplay({ contentType, fileName }); + const isDangerous = isFileDangerous(fileName || ''); - return ( -
- {pending ? ( -
- -
- ) : ( -
-
{ - if (this.props?.onDownload) { - e.stopPropagation(); - this.props.onDownload(firstAttachment); - } - }} - > - {extension ? ( -
- {extension} + return ( +
+ {pending ? ( +
+ +
+ ) : ( +
+
{ + if (this.props?.onDownload) { + e.stopPropagation(); + this.props.onDownload(firstAttachment); + } + }} + > + {extension ? ( +
+ {extension} +
+ ) : null} +
+ {isDangerous ? ( +
+
) : null}
- {isDangerous ? ( -
-
-
- ) : null} -
- )} -
-
- {fileName} -
-
- {fileSize} + )} +
+
+ {fileName} +
+
+ {fileSize} +
-
- ); + ); + } } -} // tslint:disable-next-line cyclomatic-complexity public renderPreview() { - const { - attachments, - conversationType, - direction, - onClickLinkPreview, - previews, - quote, - } = this.props; - - // Attachments take precedence over Link Previews - if (attachments && attachments.length) { - return null; - } + const { + attachments, + conversationType, + direction, + onClickLinkPreview, + previews, + quote, + } = this.props; + + // Attachments take precedence over Link Previews + if (attachments && attachments.length) { + return null; + } - if (!previews || previews.length < 1) { - return null; - } + if (!previews || previews.length < 1) { + return null; + } - const first = previews[0]; - if (!first) { - return null; - } + const first = previews[0]; + if (!first) { + return null; + } - const withContentAbove = - Boolean(quote) || (conversationType === 'group' && direction === 'incoming'); + const withContentAbove = + Boolean(quote) || (conversationType === 'group' && direction === 'incoming'); - const previewHasImage = first.image && isImageAttachment(first.image); - const width = first.image && first.image.width; - const isFullSizeImage = width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH; + const previewHasImage = first.image && isImageAttachment(first.image); + const width = first.image && first.image.width; + const isFullSizeImage = width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH; - return ( -
{ - if (onClickLinkPreview) { - onClickLinkPreview(first.url); - } - }} - > - {first.image && previewHasImage && isFullSizeImage ? ( - - ) : null} + return (
{ + if (onClickLinkPreview) { + onClickLinkPreview(first.url); + } + }} > - {first.image && previewHasImage && !isFullSizeImage ? ( -
- {window.i18n('previewThumbnail', -
+ {first.image && previewHasImage && isFullSizeImage ? ( + ) : null}
-
{first.title}
-
{first.domain}
+ {first.image && previewHasImage && !isFullSizeImage ? ( +
+ {window.i18n('previewThumbnail', +
+ ) : null} +
+
{first.title}
+
{first.domain}
+
-
- ); -} + ); + } public renderQuote() { - const { - conversationType, - direction, - quote, - isPublic, - convoId, - id, - multiSelectMode, - } = this.props; - - if (!quote || !quote.authorPhoneNumber) { - return null; - } + const { + conversationType, + direction, + quote, + isPublic, + convoId, + id, + multiSelectMode, + } = this.props; + + if (!quote || !quote.authorPhoneNumber) { + return null; + } - const withContentAbove = conversationType === 'group' && direction === 'incoming'; + const withContentAbove = conversationType === 'group' && direction === 'incoming'; - const shortenedPubkey = PubKey.shorten(quote.authorPhoneNumber); + const shortenedPubkey = PubKey.shorten(quote.authorPhoneNumber); - const displayedPubkey = quote.authorProfileName ? shortenedPubkey : quote.authorPhoneNumber; + const displayedPubkey = quote.authorProfileName ? shortenedPubkey : quote.authorPhoneNumber; - return ( - { - e.preventDefault(); - e.stopPropagation(); - if (multiSelectMode && id) { - this.props.onSelectMessage(id); - return; - } - const { authorPhoneNumber, messageId: quoteId, referencedMessageNotFound } = quote; - quote?.onClick({ - quoteAuthor: authorPhoneNumber, - quoteId, - referencedMessageNotFound, - }); - }} - text={quote.text} - attachment={quote.attachment} - isIncoming={direction === 'incoming'} - conversationType={conversationType} - convoId={convoId} - isPublic={isPublic} - authorPhoneNumber={displayedPubkey} - authorProfileName={quote.authorProfileName} - authorName={quote.authorName} - referencedMessageNotFound={quote.referencedMessageNotFound} - isFromMe={quote.isFromMe} - withContentAbove={withContentAbove} - /> - ); -} + return ( + { + e.preventDefault(); + e.stopPropagation(); + if (multiSelectMode && id) { + this.props.onSelectMessage(id); + return; + } + const { authorPhoneNumber, messageId: quoteId, referencedMessageNotFound } = quote; + quote?.onClick({ + quoteAuthor: authorPhoneNumber, + quoteId, + referencedMessageNotFound, + }); + }} + text={quote.text} + attachment={quote.attachment} + isIncoming={direction === 'incoming'} + conversationType={conversationType} + convoId={convoId} + isPublic={isPublic} + authorPhoneNumber={displayedPubkey} + authorProfileName={quote.authorProfileName} + authorName={quote.authorName} + referencedMessageNotFound={quote.referencedMessageNotFound} + isFromMe={quote.isFromMe} + withContentAbove={withContentAbove} + /> + ); + } public renderAvatar() { - const { - authorAvatarPath, - authorName, - authorPhoneNumber, - authorProfileName, - collapseMetadata, - isAdmin, - conversationType, - direction, - isPublic, - firstMessageOfSeries, - } = this.props; - - if (collapseMetadata || conversationType !== 'group' || direction === 'outgoing') { - return; - } - const userName = authorName || authorProfileName || authorPhoneNumber; + const { + authorAvatarPath, + authorName, + authorPhoneNumber, + authorProfileName, + collapseMetadata, + isAdmin, + conversationType, + direction, + isPublic, + firstMessageOfSeries, + } = this.props; + + if (collapseMetadata || conversationType !== 'group' || direction === 'outgoing') { + return; + } + const userName = authorName || authorProfileName || authorPhoneNumber; - if (!firstMessageOfSeries) { - return
; - } + if (!firstMessageOfSeries) { + return
; + } - return ( -
- { - window.inboxStore?.dispatch( - updateUserDetailsModal({ - conversationId: this.props.convoId, - userName, - authorAvatarPath, - }) - ); - }} - pubkey={authorPhoneNumber} - /> - {isPublic && isAdmin && ( -
-
-
- )} -
- ); -} + return ( +
+ { + window.inboxStore?.dispatch( + updateUserDetailsModal({ + conversationId: this.props.convoId, + userName, + authorAvatarPath, + }) + ); + }} + pubkey={authorPhoneNumber} + /> + {isPublic && isAdmin && ( +
+
+
+ )} +
+ ); + } public renderText() { - const { text, direction, status, conversationType, convoId, multiSelectMode } = this.props; + const { text, direction, status, conversationType, convoId, multiSelectMode } = this.props; - const contents = - direction === 'incoming' && status === 'error' ? window.i18n('incomingError') : text; + const contents = + direction === 'incoming' && status === 'error' ? window.i18n('incomingError') : text; - if (!contents) { - return null; - } + if (!contents) { + return null; + } - return ( -
- -
- ); -} + return ( +
+ +
+ ); + } public renderError(isCorrectSide: boolean) { - const { status, direction } = this.props; + const { status, direction } = this.props; - if (!isCorrectSide || status !== 'error') { - return null; - } + if (!isCorrectSide || status !== 'error') { + return null; + } - return ( -
-
-
- ); -} + return ( +
+
+
+ ); + } public renderContextMenu() { - const { - attachments, - authorPhoneNumber, - convoId, - onCopyText, - direction, - status, - isDeletable, - id, - onSelectMessage, - onDeleteMessage, - onDownload, - onRetrySend, - onShowDetail, - isPublic, - isOpenGroupV2, - weAreAdmin, - isAdmin, - } = this.props; - - const showRetry = status === 'error' && direction === 'outgoing'; - const multipleAttachments = attachments && attachments.length > 1; - - const onContextMenuShown = () => { - window.contextMenuShown = true; - }; - - const onContextMenuHidden = () => { - // This function will called before the click event - // on the message would trigger (and I was unable to - // prevent propagation in this case), so use a short timeout - setTimeout(() => { - window.contextMenuShown = false; - }, 100); - }; - - const selectMessageText = window.i18n('selectMessage'); - const deleteMessageText = window.i18n('deleteMessage'); + const { + attachments, + authorPhoneNumber, + convoId, + onCopyText, + direction, + status, + isDeletable, + id, + onSelectMessage, + onDeleteMessage, + onDownload, + onRetrySend, + onShowDetail, + isPublic, + isOpenGroupV2, + weAreAdmin, + isAdmin, + } = this.props; + + const showRetry = status === 'error' && direction === 'outgoing'; + const multipleAttachments = attachments && attachments.length > 1; + + const onContextMenuShown = () => { + window.contextMenuShown = true; + }; + + const onContextMenuHidden = () => { + // This function will called before the click event + // on the message would trigger (and I was unable to + // prevent propagation in this case), so use a short timeout + setTimeout(() => { + window.contextMenuShown = false; + }, 100); + }; + + const selectMessageText = window.i18n('selectMessage'); + const deleteMessageText = window.i18n('deleteMessage'); - return ( - - {!multipleAttachments && attachments && attachments[0] ? ( - { - if (onDownload) { - onDownload(attachments[0]); - } - }} - > - {window.i18n('downloadAttachment')} - - ) : null} - - {isAudio(attachments) ? ( - - {window.i18n('playAtCustomSpeed', this.state.playbackSpeed === 1 ? 2 : 1)} - - ) : null} - {window.i18n('copyMessage')} - {window.i18n('replyToMessage')} - {window.i18n('moreInformation')} - {showRetry ? {window.i18n('resend')} : null} - {isDeletable ? ( - <> + return ( + + {!multipleAttachments && attachments && attachments[0] ? ( + { + if (onDownload) { + onDownload(attachments[0]); + } + }} + > + {window.i18n('downloadAttachment')} + + ) : null} + + {isAudio(attachments) ? ( + + {window.i18n('playAtCustomSpeed', this.state.playbackSpeed === 1 ? 2 : 1)} + + ) : null} + {window.i18n('copyMessage')} + {window.i18n('replyToMessage')} + {window.i18n('moreInformation')} + {showRetry ? {window.i18n('resend')} : null} + {isDeletable ? ( + <> + { + onSelectMessage(id); + }} + > + {selectMessageText} + + { + onDeleteMessage(id); + }} + > + {deleteMessageText} + + + ) : null} + {weAreAdmin && isPublic ? ( { - onSelectMessage(id); + MessageInteraction.banUser(authorPhoneNumber, convoId); }} > - {selectMessageText} + {window.i18n('banUser')} + ) : null} + {weAreAdmin && isOpenGroupV2 ? ( { - onDeleteMessage(id); + MessageInteraction.unbanUser(authorPhoneNumber, convoId); }} > - {deleteMessageText} + {window.i18n('unbanUser')} - - ) : null} - {weAreAdmin && isPublic ? ( - { - MessageInteraction.banUser(authorPhoneNumber, convoId); - }} - > - {window.i18n('banUser')} - - ) : null} - {weAreAdmin && isOpenGroupV2 ? ( - { - MessageInteraction.unbanUser(authorPhoneNumber, convoId); - }} - > - {window.i18n('unbanUser')} - - ) : null} - {weAreAdmin && isPublic && !isAdmin ? ( - {window.i18n('addAsModerator')} - ) : null} - {weAreAdmin && isPublic && isAdmin ? ( - {window.i18n('removeFromModerators')} - ) : null} - - ); -} + ) : null} + {weAreAdmin && isPublic && !isAdmin ? ( + {window.i18n('addAsModerator')} + ) : null} + {weAreAdmin && isPublic && isAdmin ? ( + {window.i18n('removeFromModerators')} + ) : null} + + ); + } public getWidth(): number | undefined { - const { attachments, previews } = this.props; + const { attachments, previews } = this.props; - if (attachments && attachments.length) { - const dimensions = getGridDimensions(attachments); - if (dimensions) { - return dimensions.width; + if (attachments && attachments.length) { + const dimensions = getGridDimensions(attachments); + if (dimensions) { + return dimensions.width; + } } - } - if (previews && previews.length) { - const first = previews[0]; + if (previews && previews.length) { + const first = previews[0]; - if (!first || !first.image) { - return; - } - const { width } = first.image; + if (!first || !first.image) { + return; + } + const { width } = first.image; - if (isImageAttachment(first.image) && width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH) { - const dimensions = getImageDimensions(first.image); - if (dimensions) { - return dimensions.width; + if (isImageAttachment(first.image) && width && width >= MINIMUM_LINK_PREVIEW_IMAGE_WIDTH) { + const dimensions = getImageDimensions(first.image); + if (dimensions) { + return dimensions.width; + } } } - } - return; -} + return; + } public isShowingImage(): boolean { - const { attachments, previews } = this.props; - const { imageBroken } = this.state; + const { attachments, previews } = this.props; + const { imageBroken } = this.state; - if (imageBroken) { - return false; - } + if (imageBroken) { + return false; + } - if (attachments && attachments.length) { - const displayImage = canDisplayImage(attachments); + if (attachments && attachments.length) { + const displayImage = canDisplayImage(attachments); - return Boolean( - displayImage && - ((isImage(attachments) && hasImage(attachments)) || - (isVideo(attachments) && hasVideoScreenshot(attachments))) - ); - } + return Boolean( + displayImage && + ((isImage(attachments) && hasImage(attachments)) || + (isVideo(attachments) && hasVideoScreenshot(attachments))) + ); + } - if (previews && previews.length) { - const first = previews[0]; - const { image } = first; + if (previews && previews.length) { + const first = previews[0]; + const { image } = first; - if (!image) { - return false; + if (!image) { + return false; + } + + return isImageAttachment(image); } - return isImageAttachment(image); + return false; } - return false; -} - // tslint:disable-next-line: cyclomatic-complexity public render() { - const { - direction, - id, - selected, - multiSelectMode, - conversationType, - isPublic, - text, - isUnread, - markRead, - } = this.props; - const { expired, expiring } = this.state; - - if (expired) { - return null; - } - - const width = this.getWidth(); - const isShowingImage = this.isShowingImage(); + const { + direction, + id, + selected, + multiSelectMode, + conversationType, + isPublic, + text, + isUnread, + markRead, + } = this.props; + const { expired, expiring } = this.state; + + if (expired) { + return null; + } - const isIncoming = direction === 'incoming'; - const shouldMarkReadWhenVisible = isIncoming && isUnread; - const divClasses = ['session-message-wrapper']; + const width = this.getWidth(); + const isShowingImage = this.isShowingImage(); - if (selected) { - divClasses.push('message-selected'); - } - - if (conversationType === 'group') { - divClasses.push('public-chat-message-wrapper'); - } + const isIncoming = direction === 'incoming'; + const shouldMarkReadWhenVisible = isIncoming && isUnread; + const divClasses = ['session-message-wrapper']; - if (this.props.isQuotedMessageToAnimate) { - divClasses.push('flash-green-once'); - } + if (selected) { + divClasses.push('message-selected'); + } - const onVisible = (inView: boolean) => { - if (inView && shouldMarkReadWhenVisible) { - // mark the message as read. - // this will trigger the expire timer. - void markRead(Date.now()); + if (conversationType === 'group') { + divClasses.push('public-chat-message-wrapper'); } - }; - return ( - - {this.renderAvatar()} -
{ - const selection = window.getSelection(); - // Text is being selected - if (selection && selection.type === 'Range') { - return; - } + if (this.props.isQuotedMessageToAnimate) { + divClasses.push('flash-green-once'); + } - // User clicked on message body - const target = event.target as HTMLDivElement; - if ( - (!multiSelectMode && target.className === 'text-selectable') || - window.contextMenuShown - ) { - return; - } + const onVisible = (inView: boolean) => { + if (inView && shouldMarkReadWhenVisible) { + // mark the message as read. + // this will trigger the expire timer. + void markRead(Date.now()); + } + }; - if (id) { - this.props.onSelectMessage(id); - } - }} + return ( + - {this.renderError(isIncoming)} - + {this.renderAvatar()}
{ const selection = window.getSelection(); @@ -858,7 +817,10 @@ const MessageInner = (props: MessageRegularProps) => { // User clicked on message body const target = event.target as HTMLDivElement; - if (target.className === 'text-selectable' || window.contextMenuShown) { + if ( + (!multiSelectMode && target.className === 'text-selectable') || + window.contextMenuShown + ) { return; } @@ -867,93 +829,122 @@ const MessageInner = (props: MessageRegularProps) => { } }} > - {this.renderAuthor()} - {this.renderQuote()} - {this.renderAttachment()} - {this.renderPreview()} - {this.renderText()} - + {this.renderError(isIncoming)} + +
{ + const selection = window.getSelection(); + // Text is being selected + if (selection && selection.type === 'Range') { + return; + } + + // User clicked on message body + const target = event.target as HTMLDivElement; + if (target.className === 'text-selectable' || window.contextMenuShown) { + return; + } + + if (id) { + this.props.onSelectMessage(id); + } + }} + > + {this.renderAuthor()} + {this.renderQuote()} + {this.renderAttachment()} + {this.renderPreview()} + {this.renderText()} + +
+ {this.renderError(!isIncoming)} + {this.renderContextMenu()}
- {this.renderError(!isIncoming)} - {this.renderContextMenu()} -
-
- ); -} + + ); + } /** * Doubles / halves the playback speed based on the current playback speed. */ private updatePlaybackSpeed() { - this.setState({ - ...this.state, - playbackSpeed: this.state.playbackSpeed === 1 ? 2 : 1, - }); -} + this.setState({ + ...this.state, + playbackSpeed: this.state.playbackSpeed === 1 ? 2 : 1, + }); + } private handleContextMenu(e: any) { - e.preventDefault(); - e.stopPropagation(); - const { multiSelectMode, isKickedFromGroup } = this.props; - const enableContextMenu = !multiSelectMode && !isKickedFromGroup; - - if (enableContextMenu) { - // Don't forget to pass the id and the event and voila! - contextMenu.hideAll(); - contextMenu.show({ - id: this.ctxMenuID, - event: e, - }); + e.preventDefault(); + e.stopPropagation(); + const { multiSelectMode, isKickedFromGroup } = this.props; + const enableContextMenu = !multiSelectMode && !isKickedFromGroup; + + if (enableContextMenu) { + // Don't forget to pass the id and the event and voila! + contextMenu.hideAll(); + contextMenu.show({ + id: this.ctxMenuID, + event: e, + }); + } } -} private renderAuthor() { - const { - authorName, - authorPhoneNumber, - authorProfileName, - conversationType, - direction, - isPublic, - } = this.props; - - const title = authorName ? authorName : authorPhoneNumber; - - if (direction !== 'incoming' || conversationType !== 'group' || !title) { - return null; - } + const { + authorName, + authorPhoneNumber, + authorProfileName, + conversationType, + direction, + isPublic, + } = this.props; + + const title = authorName ? authorName : authorPhoneNumber; + + if (direction !== 'incoming' || conversationType !== 'group' || !title) { + return null; + } - const shortenedPubkey = PubKey.shorten(authorPhoneNumber); + const shortenedPubkey = PubKey.shorten(authorPhoneNumber); - const displayedPubkey = authorProfileName ? shortenedPubkey : authorPhoneNumber; + const displayedPubkey = authorProfileName ? shortenedPubkey : authorPhoneNumber; - return ( -
- -
- ); -} + return ( +
+ +
+ ); + } private onReplyPrivate(e: any) { - if (this.props && this.props.onReply) { - this.props.onReply(this.props.timestamp); + if (this.props && this.props.onReply) { + this.props.onReply(this.props.timestamp); + } } -} private async onAddModerator() { - await addSenderAsModerator(this.props.authorPhoneNumber, this.props.convoId); -} + await addSenderAsModerator(this.props.authorPhoneNumber, this.props.convoId); + } private async onRemoveFromModerator() { - await removeSenderFromModerator(this.props.authorPhoneNumber, this.props.convoId); -} + await removeSenderFromModerator(this.props.authorPhoneNumber, this.props.convoId); + } } export const Message = withTheme(MessageInner); From a9913d29f7a8f1fc9b24cf30ed2980c23dd87a71 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Mon, 21 Jun 2021 17:09:15 +1000 Subject: [PATCH 3/7] Link guard working. --- ts/components/conversation/Linkify.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx index 630c4e43d..22187a3f5 100644 --- a/ts/components/conversation/Linkify.tsx +++ b/ts/components/conversation/Linkify.tsx @@ -5,6 +5,8 @@ import LinkifyIt from 'linkify-it'; import { RenderTextCallbackType } from '../../types/Util'; import { isLinkSneaky } from '../../../js/modules/link_previews'; import { SessionHtmlRenderer } from '../session/SessionHTMLRenderer'; +import { updateConfirmModal } from '../../state/ducks/modalDialog'; +import { shell } from 'electron'; const linkify = LinkifyIt(); @@ -70,6 +72,23 @@ export class Linkify extends React.Component { // 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) => { + e.preventDefault(); e.stopPropagation(); + + const url = e.target.href; + + const openLink = () => { + // window.open(e.target.href); + void shell.openExternal(url); + } + + window.inboxStore?.dispatch(updateConfirmModal({ + title: "Hello", + message: "Are you sure you want to open this link?", + onClickOk: openLink, + onClickClose: () => { + window.inboxStore?.dispatch(updateConfirmModal(null)); + } + })) }; } From 23c83662e73963970ee5ef834d1a14f6cc1fd3eb Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Tue, 22 Jun 2021 09:15:47 +1000 Subject: [PATCH 4/7] Added message entries for message link visit warning. --- _locales/en/messages.json | 5 ++++- ts/components/conversation/Linkify.tsx | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 31681bb6d..1bd1a6906 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -419,5 +419,8 @@ "device": "Device", "destination": "Destination", "learnMore": "Learn more", - "playAtCustomSpeed": "Play at $multipler$x speed" + "playAtCustomSpeed": "Play at $multipler$x speed", + "linkVisitWarningTitle": "Open this URL in your browser?", + "linkVisitWarningMessage": "Are you sure you wish to open $url$ in your browser?", + "open": "Open" } diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx index 22187a3f5..7362cd38e 100644 --- a/ts/components/conversation/Linkify.tsx +++ b/ts/components/conversation/Linkify.tsx @@ -78,13 +78,13 @@ export class Linkify extends React.Component { const url = e.target.href; const openLink = () => { - // window.open(e.target.href); void shell.openExternal(url); } window.inboxStore?.dispatch(updateConfirmModal({ - title: "Hello", - message: "Are you sure you want to open this link?", + title: window.i18n('linkVisitWarningTitle'), + message: window.i18n("linkVisitWarningMessage", url), + okText: window.i18n("open"), onClickOk: openLink, onClickClose: () => { window.inboxStore?.dispatch(updateConfirmModal(null)); From 877bd342717329c714cec78143b2806c79c9d770 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Tue, 22 Jun 2021 09:30:12 +1000 Subject: [PATCH 5/7] Minor formatting --- ts/components/conversation/Linkify.tsx | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx index 7362cd38e..f3dc90017 100644 --- a/ts/components/conversation/Linkify.tsx +++ b/ts/components/conversation/Linkify.tsx @@ -79,16 +79,18 @@ export class Linkify extends React.Component { const openLink = () => { void shell.openExternal(url); - } - - window.inboxStore?.dispatch(updateConfirmModal({ - title: window.i18n('linkVisitWarningTitle'), - message: window.i18n("linkVisitWarningMessage", url), - okText: window.i18n("open"), - onClickOk: openLink, - onClickClose: () => { - window.inboxStore?.dispatch(updateConfirmModal(null)); - } - })) + }; + + window.inboxStore?.dispatch( + updateConfirmModal({ + title: window.i18n('linkVisitWarningTitle'), + message: window.i18n('linkVisitWarningMessage', url), + okText: window.i18n('open'), + onClickOk: openLink, + onClickClose: () => { + window.inboxStore?.dispatch(updateConfirmModal(null)); + }, + }) + ); }; } From 82d79accdb3af408eb5ae99d90bc7de9536633ac Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Tue, 22 Jun 2021 09:34:09 +1000 Subject: [PATCH 6/7] remove unused import --- ts/components/conversation/Linkify.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx index f3dc90017..86658f1ab 100644 --- a/ts/components/conversation/Linkify.tsx +++ b/ts/components/conversation/Linkify.tsx @@ -4,7 +4,6 @@ import LinkifyIt from 'linkify-it'; import { RenderTextCallbackType } from '../../types/Util'; import { isLinkSneaky } from '../../../js/modules/link_previews'; -import { SessionHtmlRenderer } from '../session/SessionHTMLRenderer'; import { updateConfirmModal } from '../../state/ducks/modalDialog'; import { shell } from 'electron'; From 4d985e84b2f1c0357bea52a34bb3132d5ac624e9 Mon Sep 17 00:00:00 2001 From: Warrick Corfe-Tan Date: Tue, 22 Jun 2021 09:58:49 +1000 Subject: [PATCH 7/7] minor message rewording --- _locales/en/messages.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1bd1a6906..4bae8aaad 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -420,7 +420,7 @@ "destination": "Destination", "learnMore": "Learn more", "playAtCustomSpeed": "Play at $multipler$x speed", - "linkVisitWarningTitle": "Open this URL in your browser?", - "linkVisitWarningMessage": "Are you sure you wish to open $url$ in your browser?", + "linkVisitWarningTitle": "Open this link in your browser?", + "linkVisitWarningMessage": "Are you sure you want to open $url$ in your browser?", "open": "Open" }