{ 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, onShowUserDetails, firstMessageOfSeries, } = this.props; if (collapseMetadata || conversationType !== 'group' || direction === 'outgoing') { return; } const userName = authorName || authorProfileName || authorPhoneNumber; if (!firstMessageOfSeries) { return ; } return (); } public renderText() { const { text, direction, status, conversationType, convoId, multiSelectMode } = this.props; const contents = direction === 'incoming' && status === 'error' ? window.i18n('incomingError') : text; if (!contents) { return null; } return ({ onShowUserDetails(authorPhoneNumber); }} pubkey={authorPhoneNumber} /> {isPublic && isAdmin && ( )}); } public renderError(isCorrectSide: boolean) { const { status, direction } = this.props; if (!isCorrectSide || status !== 'error') { return null; } return ( ); } public renderContextMenu() { const { attachments, onCopyText, direction, status, isDeletable, id, onSelectMessage, onDeleteMessage, onDownload, onRetrySend, onShowDetail, isPublic, isOpenGroupV2, weAreAdmin, isAdmin, onBanUser, onUnbanUser, } = 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 ( ); } public getWidth(): number | undefined { const { attachments, previews } = this.props; if (attachments && attachments.length) { const dimensions = getGridDimensions(attachments); if (dimensions) { return dimensions.width; } } if (previews && previews.length) { const first = previews[0]; 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; } } } return; } public isShowingImage(): boolean { const { attachments, previews } = this.props; const { imageBroken } = this.state; if (imageBroken) { return false; } if (attachments && attachments.length) { const displayImage = canDisplayImage(attachments); return Boolean( displayImage && ((isImage(attachments) && hasImage(attachments)) || (isVideo(attachments) && hasVideoScreenshot(attachments))) ); } if (previews && previews.length) { const first = previews[0]; const { image } = first; if (!image) { 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 width = this.getWidth(); const isShowingImage = this.isShowingImage(); // We parse the message later, but we still need to do an early check // to see if the message mentions us, so we can display the entire // message differently const regex = new RegExp(`@${PubKey.regexForPubkeys}`, 'g'); const mentions = (text ? text.match(regex) : []) as Array; const mentionMe = mentions && mentions.some(m => UserUtils.isUsFromCache(m.slice(1))); const isIncoming = direction === 'incoming'; const shouldHightlight = mentionMe && isIncoming && isPublic; const shouldMarkReadWhenVisible = isIncoming && isUnread; const divClasses = ['session-message-wrapper']; if (shouldHightlight) { //divClasses.push('message-highlighted'); } if (selected) { divClasses.push('message-selected'); } if (conversationType === 'group') { divClasses.push('public-chat-message-wrapper'); } 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()); } }; return ( {this.renderAvatar()} ); } 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, }); } } 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 shortenedPubkey = PubKey.shorten(authorPhoneNumber); const displayedPubkey = authorProfileName ? shortenedPubkey : authorPhoneNumber; return ({ 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 ( (!multiSelectMode && target.className === 'text-selectable') || window.contextMenuShown ) { return; } if (id) { this.props.onSelectMessage(id); } }} > {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()} ); } private onReplyPrivate(e: any) { if (this.props && this.props.onReply) { this.props.onReply(this.props.timestamp); } } private async onAddModerator() { await addSenderAsModerator(this.props.authorPhoneNumber, this.props.convoId); } private async onRemoveFromModerator() { await removeSenderFromModerator(this.props.authorPhoneNumber, this.props.convoId); } } export const Message = withTheme(MessageInner);