From a54345a42e8951916b542e55652748d7c2ccbbbf Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 14 Jul 2021 16:36:55 +1000 Subject: [PATCH] put back quote a message logic with hook --- ts/components/conversation/Message.tsx | 10 +++- .../conversation/SessionCompositionBox.tsx | 35 ++++++------ .../conversation/SessionConversation.tsx | 55 ++----------------- .../conversation/SessionMessagesList.tsx | 7 --- .../SessionQuotedMessageComposition.tsx | 34 +++++++----- ts/interactions/conversationInteractions.ts | 21 +++++++ ts/models/conversation.ts | 12 +++- ts/models/message.ts | 5 +- ts/models/messageType.ts | 3 +- ts/state/ducks/conversations.ts | 13 +++++ ts/state/selectors/conversations.ts | 6 ++ 11 files changed, 102 insertions(+), 99 deletions(-) diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 2af472cdf..9719d9d23 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -43,7 +43,7 @@ import autoBind from 'auto-bind'; import { AudioPlayerWithEncryptedFile } from './H5AudioPlayer'; import { ClickToTrustSender } from './message/ClickToTrustSender'; import { getMessageById } from '../../data/data'; -import { deleteMessagesById } from '../../interactions/conversationInteractions'; +import { deleteMessagesById, replyToMessage } from '../../interactions/conversationInteractions'; import { connect } from 'react-redux'; import { StateType } from '../../state/reducer'; import { getSelectedMessageIds } from '../../state/selectors/conversations'; @@ -56,6 +56,7 @@ import { } from '../../state/ducks/conversations'; import { saveAttachmentToDisk } from '../../util/attachmentsUtil'; import { LightBoxOptions } from '../session/conversation/SessionConversation'; +import { pushUnblockToSend } from '../../session/utils/Toast'; // Same as MIN_WIDTH in ImageGrid.tsx const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200; @@ -944,9 +945,12 @@ class MessageInner extends React.PureComponent { } private onReplyPrivate(e: any) { - if (this.props && this.props.onReply) { - this.props.onReply(this.props.timestamp); + if (this.props.isBlocked) { + pushUnblockToSend(); + return; } + + void replyToMessage(this.props.id); } private async onAddModerator() { diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index db8b6b259..3486ca7d3 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -45,6 +45,9 @@ import { getItemById, hasLinkPreviewPopupBeenDisplayed, } from '../../../data/data'; +import { getQuotedMessage } from '../../../state/selectors/conversations'; +import { connect } from 'react-redux'; +import { StateType } from '../../../state/reducer'; export interface ReplyingToMessageProps { convoId: string; @@ -80,10 +83,7 @@ interface Props { selectedConversationKey: string; selectedConversation: ReduxConversationType | undefined; isPublic: boolean; - quotedMessageProps?: ReplyingToMessageProps; - removeQuotedMessage: () => void; - stagedAttachments: Array; clearAttachments: () => any; removeAttachment: (toRemove: AttachmentType) => void; @@ -135,7 +135,7 @@ const getDefaultState = () => { }; }; -export class SessionCompositionBox extends React.Component { +class SessionCompositionBoxInner extends React.Component { private readonly textarea: React.RefObject; private readonly fileInput: React.RefObject; private emojiPanel: any; @@ -188,7 +188,7 @@ export class SessionCompositionBox extends React.Component { return ( - {this.renderQuotedMessage()} + {this.renderStagedLinkPreview()} {this.renderAttachmentsStaged()}
@@ -665,19 +665,6 @@ export class SessionCompositionBox extends React.Component { }); } - private renderQuotedMessage() { - const { quotedMessageProps, removeQuotedMessage } = this.props; - if (quotedMessageProps?.id) { - return ( - - ); - } - return <>; - } - private onClickAttachment(attachment: AttachmentType) { this.setState({ showCaptionEditor: attachment }); } @@ -839,6 +826,8 @@ export class SessionCompositionBox extends React.Component { } const { quotedMessageProps } = this.props; + + console.warn('quotedMessageProps', quotedMessageProps); const { stagedLinkPreview } = this.state; // Send message @@ -999,3 +988,13 @@ export class SessionCompositionBox extends React.Component { this.linkPreviewAbortController?.abort(); } } + +const mapStateToProps = (state: StateType) => { + return { + quotedMessageProps: getQuotedMessage(state), + }; +}; + +const smart = connect(mapStateToProps); + +export const SessionCompositionBox = smart(SessionCompositionBoxInner); diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 0b48b8ef4..89ac46176 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -21,6 +21,7 @@ import { SessionFileDropzone } from './SessionFileDropzone'; import { fetchMessagesForConversation, PropsForMessage, + quoteMessage, ReduxConversationType, resetSelectedMessageIds, showLightBox, @@ -44,10 +45,6 @@ interface State { stagedAttachments: Array; isDraggingFile: boolean; - - // quoted message - quotedMessageTimestamp?: number; - quotedMessageProps?: any; } export interface LightBoxOptions { @@ -151,8 +148,6 @@ export class SessionConversation extends React.Component { showRecordingView: false, stagedAttachments: [], isDraggingFile: false, - quotedMessageProps: undefined, - quotedMessageTimestamp: undefined, }); } } @@ -174,7 +169,7 @@ export class SessionConversation extends React.Component { // ~~~~~~~~~~~~~~ RENDER METHODS ~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ public render() { - const { showRecordingView, quotedMessageProps, isDraggingFile, stagedAttachments } = this.state; + const { showRecordingView, isDraggingFile, stagedAttachments } = this.state; const { selectedConversation, @@ -210,6 +205,8 @@ export class SessionConversation extends React.Component { (this.messageContainerRef .current as any).scrollTop = this.messageContainerRef.current?.scrollHeight; } + + window.inboxStore?.dispatch(quoteMessage(undefined)); }; return ( @@ -231,7 +228,7 @@ export class SessionConversation extends React.Component { {lightBoxOptions?.media && this.renderLightBox(lightBoxOptions)}
- + {showRecordingView &&
} {isDraggingFile && } @@ -249,10 +246,6 @@ export class SessionConversation extends React.Component { stagedAttachments={stagedAttachments} onLoadVoiceNoteView={this.onLoadVoiceNoteView} onExitVoiceNoteView={this.onExitVoiceNoteView} - quotedMessageProps={quotedMessageProps} - removeQuotedMessage={() => { - void this.replyToMessage(undefined); - }} clearAttachments={this.clearAttachments} removeAttachment={this.removeAttachment} onChoseAttachments={this.onChoseAttachments} @@ -292,13 +285,6 @@ export class SessionConversation extends React.Component { ); } - public getMessagesListProps(): SessionMessageListProps { - return { - messageContainerRef: this.messageContainerRef, - replyToMessage: this.replyToMessage, - }; - } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~ MICROPHONE METHODS ~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -315,37 +301,6 @@ export class SessionConversation extends React.Component { }); } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // ~~~~~~~~~~~~~~ MESSAGE QUOTE ~~~~~~~~~~~~~~~ - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - private async replyToMessage(quotedMessageTimestamp?: number) { - if (this.props.selectedConversation?.isBlocked) { - pushUnblockToSend(); - return; - } - if (!_.isEqual(this.state.quotedMessageTimestamp, quotedMessageTimestamp)) { - const { messagesProps, selectedConversationKey } = this.props; - const conversationModel = getConversationController().getOrThrow(selectedConversationKey); - - let quotedMessageProps = null; - if (quotedMessageTimestamp) { - const quotedMessage = messagesProps.find( - m => - m.propsForMessage.timestamp === quotedMessageTimestamp || - m.propsForMessage.serverTimestamp === quotedMessageTimestamp - ); - - if (quotedMessage) { - const quotedMessageModel = await getMessageById(quotedMessage.propsForMessage.id); - if (quotedMessageModel) { - quotedMessageProps = await conversationModel.makeQuote(quotedMessageModel); - } - } - } - this.setState({ quotedMessageTimestamp, quotedMessageProps }); - } - } - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~ KEYBOARD NAVIGATION ~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index 3431ea6f3..23e77590c 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -52,8 +52,6 @@ interface State { export type SessionMessageListProps = { messageContainerRef: React.RefObject; - - replyToMessage: (messageId: number) => Promise; }; type Props = SessionMessageListProps & { @@ -138,10 +136,6 @@ const GenericMessageItem = (props: { console.warn('FIXME audric'); - if (!props.messageProps) { - debugger; - } - // const onQuoteClick = props.messageProps.propsForMessage.quote // ? this.scrollToQuoteMessage // : async () => {}; @@ -152,7 +146,6 @@ const GenericMessageItem = (props: { multiSelectMode, // isQuotedMessageToAnimate: messageId === this.state.animateQuotedMessageId, // nextMessageToPlay: this.state.nextMessageToPlay, - onReply: props.replyToMessage, // playNextMessage: this.playNextMessage, // onQuoteClick, }; diff --git a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx index 902381307..eea2b1100 100644 --- a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx +++ b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx @@ -1,17 +1,13 @@ -import React, { useContext } from 'react'; +import React, { useCallback } from 'react'; import { Flex } from '../../basic/Flex'; import { SessionIcon, SessionIconButton, SessionIconSize, SessionIconType } from '../icon'; -import { ReplyingToMessageProps } from './SessionCompositionBox'; -import styled, { DefaultTheme, ThemeContext } from 'styled-components'; -import { getAlt, isAudio, isImageAttachment } from '../../../types/Attachment'; +import styled, { useTheme } from 'styled-components'; +import { getAlt, isAudio } from '../../../types/Attachment'; import { Image } from '../../conversation/Image'; import { AUDIO_MP3 } from '../../../types/MIME'; - -// tslint:disable: react-unused-props-and-state -interface Props { - quotedMessageProps: ReplyingToMessageProps; - removeQuotedMessage: any; -} +import { useDispatch, useSelector } from 'react-redux'; +import { getQuotedMessage } from '../../../state/selectors/conversations'; +import { quoteMessage } from '../../../state/ducks/conversations'; const QuotedMessageComposition = styled.div` width: 100%; @@ -41,11 +37,13 @@ const ReplyingTo = styled.div` color: ${props => props.theme.colors.textColor}; `; -export const SessionQuotedMessageComposition = (props: Props) => { - const { quotedMessageProps, removeQuotedMessage } = props; - const theme = useContext(ThemeContext); +export const SessionQuotedMessageComposition = () => { + const theme = useTheme(); + const quotedMessageProps = useSelector(getQuotedMessage); + + const dispatch = useDispatch(); - const { text: body, attachments } = quotedMessageProps; + const { text: body, attachments } = quotedMessageProps || {}; const hasAttachments = attachments && attachments.length > 0; let hasImageAttachment = false; @@ -61,6 +59,14 @@ export const SessionQuotedMessageComposition = (props: Props) => { const hasAudioAttachment = hasAttachments && attachments && attachments.length > 0 && isAudio(attachments); + const removeQuotedMessage = useCallback(() => { + dispatch(quoteMessage(undefined)); + }, []); + + if (!quotedMessageProps?.id) { + return null; + } + return ( { if (convoId.match(openGroupV2ConversationIdRegex)) { @@ -532,3 +534,22 @@ export async function deleteMessagesById( void doDelete(); } } + +export async function replyToMessage(messageId: string) { + const quotedMessageModel = await getMessageById(messageId); + if (!quotedMessageModel) { + window.log.warn('Failed to find message to reply to'); + return; + } + const conversationModel = getConversationController().getOrThrow( + quotedMessageModel.get('conversationId') + ); + + const quotedMessageProps = await conversationModel.makeQuote(quotedMessageModel); + + if (quotedMessageProps) { + window.inboxStore?.dispatch(quoteMessage(quotedMessageProps)); + } else { + window.inboxStore?.dispatch(quoteMessage(undefined)); + } +} diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 1b9d83d42..a1f9de274 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -43,6 +43,7 @@ import { OpenGroupRequestCommonType } from '../opengroup/opengroupV2/ApiUtil'; import { getOpenGroupV2FromConversationId } from '../opengroup/utils/OpenGroupUtils'; import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout'; import { perfEnd, perfStart } from '../session/utils/Performance'; +import { ReplyingToMessageProps } from '../components/session/conversation/SessionCompositionBox'; export enum ConversationTypeEnum { GROUP = 'group', @@ -566,17 +567,24 @@ export class ConversationModel extends Backbone.Model { return []; } - public async makeQuote(quotedMessage: MessageModel) { + public async makeQuote(quotedMessage: MessageModel): Promise { const attachments = quotedMessage.get('attachments'); const preview = quotedMessage.get('preview'); const body = quotedMessage.get('body'); const quotedAttachments = await this.getQuoteAttachment(attachments, preview); + + if (!quotedMessage.get('sent_at')) { + window.log.warn('tried to make a quote without a sent_at timestamp'); + return null; + } return { author: quotedMessage.getSource(), - id: quotedMessage.get('sent_at'), + id: `${quotedMessage.get('sent_at')}` || '', text: body, attachments: quotedAttachments, + timestamp: quotedMessage.get('sent_at') || 0, + convoId: this.id, }; } diff --git a/ts/models/message.ts b/ts/models/message.ts index 86b56c6d8..664f5baf5 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -529,6 +529,7 @@ export class MessageModel extends Backbone.Model { const conversation = this.getConversation(); const isGroup = !!conversation && !conversation.isPrivate(); + const isBlocked = conversation?.isBlocked() || false; const isPublic = !!this.get('isPublic'); const isPublicOpenGroupV2 = isOpenGroupV2(this.getConversation()?.id || ''); @@ -568,6 +569,7 @@ export class MessageModel extends Backbone.Model { expirationLength, expirationTimestamp, isPublic, + isBlocked, isOpenGroupV2: isPublicOpenGroupV2, isKickedFromGroup: conversation?.get('isKickedFromGroup'), isTrustedForAttachmentDownload, @@ -774,9 +776,6 @@ export class MessageModel extends Backbone.Model { conversationType: ConversationTypeEnum.PRIVATE, multiSelectMode: false, firstMessageOfSeries: false, - onReply: noop, - // tslint:disable-next-line: no-async-without-await no-empty - onQuoteClick: async () => {}, }, errors, contacts: sortedContacts || [], diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index 50f26c4c3..ee9168342 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -237,6 +237,7 @@ export interface MessageRegularProps { expirationTimestamp?: number; convoId: string; isPublic?: boolean; + isBlocked: boolean; isOpenGroupV2?: boolean; isKickedFromGroup: boolean; // whether or not to show check boxes @@ -245,8 +246,6 @@ export interface MessageRegularProps { isUnread: boolean; isQuotedMessageToAnimate?: boolean; isTrustedForAttachmentDownload: boolean; - - onReply: (messagId: number) => void; onQuoteClick: (options: QuoteClickOptions) => Promise; playableMessageIndex?: number; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 22b2066ad..962989bfe 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -16,6 +16,7 @@ import { } from '../../models/messageType'; import { NotificationForConvoOption } from '../../components/conversation/ConversationHeader'; import { LightBoxOptions } from '../../components/session/conversation/SessionConversation'; +import { ReplyingToMessageProps } from '../../components/session/conversation/SessionCompositionBox'; export type MessageModelProps = { propsForMessage: PropsForMessage; @@ -181,6 +182,7 @@ export type PropsForMessage = { isSenderAdmin: boolean; isDeletable: boolean; isExpired: boolean; + isBlocked: boolean; }; export type LastMessageType = { @@ -235,6 +237,7 @@ export type ConversationsStateType = { showRightPanel: boolean; selectedMessageIds: Array; lightBox?: LightBoxOptions; + quotedMessage?: ReplyingToMessageProps; }; async function getMessages( @@ -708,6 +711,8 @@ const conversationsSlice = createSlice({ state.selectedMessageIds = []; state.selectedConversation = action.payload.id; state.messages = []; + state.quotedMessage = undefined; + state.lightBox = undefined; return state; }, showLightBox( @@ -717,6 +722,13 @@ const conversationsSlice = createSlice({ state.lightBox = action.payload; return state; }, + quoteMessage( + state: ConversationsStateType, + action: PayloadAction + ) { + state.quotedMessage = action.payload; + return state; + }, }, extraReducers: (builder: any) => { // Add reducers for additional action types here, and handle loading state as needed @@ -762,4 +774,5 @@ export const { resetSelectedMessageIds, toggleSelectedMessageId, showLightBox, + quoteMessage, } = actions; diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 073f7585d..3b04be7b8 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -18,6 +18,7 @@ import { ConversationHeaderTitleProps, } from '../../components/conversation/ConversationHeader'; import { LightBoxOptions } from '../../components/session/conversation/SessionConversation'; +import { ReplyingToMessageProps } from '../../components/session/conversation/SessionCompositionBox'; export const getConversations = (state: StateType): ConversationsStateType => state.conversations; @@ -289,3 +290,8 @@ export const getLightBoxOptions = createSelector( getConversations, (state: ConversationsStateType): LightBoxOptions | undefined => state.lightBox ); + +export const getQuotedMessage = createSelector( + getConversations, + (state: ConversationsStateType): ReplyingToMessageProps | undefined => state.quotedMessage +);