diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 4b1aaea38..3fb14dff9 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -1,18 +1,16 @@ -// tslint:disable: no-backbone-get-set-outside-model - import React from 'react'; import classNames from 'classnames'; import { SessionCompositionBox, StagedAttachmentType } from './SessionCompositionBox'; -import { ClosedGroup, Constants, Utils } from '../../../session'; +import { Constants } from '../../../session'; import _ from 'lodash'; import { AttachmentUtil, GoogleChrome } from '../../../util'; import { ConversationHeaderWithDetails } from '../../conversation/ConversationHeader'; import { SessionRightPanelWithDetails } from './SessionRightPanel'; import { SessionTheme } from '../../../state/ducks/SessionTheme'; -import { DefaultTheme, useTheme } from 'styled-components'; +import { DefaultTheme } from 'styled-components'; import { SessionMessagesList } from './SessionMessagesList'; import { LightboxGallery, MediaItemType } from '../../LightboxGallery'; import { Message } from '../../conversation/media-gallery/types/Message'; @@ -21,7 +19,11 @@ import { AttachmentType, AttachmentTypeWithPath, save } from '../../../types/Att import { ToastUtils, UserUtils } from '../../../session/utils'; import * as MIME from '../../../types/MIME'; import { SessionFileDropzone } from './SessionFileDropzone'; -import { ConversationType, PropsForMessage } from '../../../state/ducks/conversations'; +import { + ConversationType, + PropsForMessage, + SortedMessageModelProps, +} from '../../../state/ducks/conversations'; import { MessageView } from '../../MainViewController'; import { pushUnblockToSend } from '../../../session/utils/Toast'; import { MessageDetail } from '../../conversation/MessageDetail'; @@ -39,7 +41,6 @@ import { updateMentionsMembers } from '../../../state/ducks/mentionsInput'; import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage'; import { SessionButtonColor } from '../SessionButton'; -import { usingClosedConversationDetails } from '../usingClosedConversationDetails'; interface State { // Message sending progress messageProgressVisible: boolean; @@ -83,7 +84,7 @@ interface Props { selectedConversationKey: string; selectedConversation?: ConversationType; theme: DefaultTheme; - messages: Array; + messagesProps: Array; actions: any; } @@ -216,9 +217,9 @@ export class SessionConversation extends React.Component { } = this.state; const selectionMode = !!selectedMessages.length; - const { selectedConversation, selectedConversationKey, messages, actions } = this.props; + const { selectedConversation, selectedConversationKey, messagesProps, actions } = this.props; - if (!selectedConversation || !messages) { + if (!selectedConversation || !messagesProps) { // return an empty message view return ; } @@ -414,7 +415,7 @@ export class SessionConversation extends React.Component { selectedConversation, selectedConversationKey, ourNumber, - messages, + messagesProps, actions, } = this.props; const { quotedMessageTimestamp, selectedMessages } = this.state; @@ -423,7 +424,7 @@ export class SessionConversation extends React.Component { selectedMessages, ourPrimary: ourNumber, conversationKey: selectedConversationKey, - messages, + messagesProps, resetSelection: this.resetSelection, quotedMessageTimestamp, conversation: selectedConversation as ConversationType, @@ -529,15 +530,15 @@ export class SessionConversation extends React.Component { public async deleteMessagesById(messageIds: Array, askUserForConfirmation: boolean) { // Get message objects - const { selectedConversationKey, selectedConversation, messages } = this.props; + const { selectedConversationKey, selectedConversation, messagesProps } = this.props; const conversationModel = getConversationController().getOrThrow(selectedConversationKey); if (!selectedConversation) { window?.log?.info('No valid selected conversation.'); return; } - const selectedMessages = messages.filter(message => - messageIds.find(selectedMessage => selectedMessage === message.id) + const selectedMessages = messagesProps.filter(message => + messageIds.find(selectedMessage => selectedMessage === message.propsForMessage.id) ); const multiple = selectedMessages.length > 1; @@ -557,7 +558,7 @@ export class SessionConversation extends React.Component { })(); const doDelete = async () => { - let toDeleteLocally; + let toDeleteLocallyIds: Array; if (selectedConversation.isPublic) { // Get our Moderator status @@ -568,7 +569,7 @@ export class SessionConversation extends React.Component { const isAdmin = conversationModel.isAdmin(ourDevicePubkey); const isAllOurs = selectedMessages.every( - message => ourDevicePubkey === message.attributes.source + message => ourDevicePubkey === message.propsForMessage.authorPhoneNumber ); if (!isAllOurs && !isAdmin) { @@ -578,18 +579,18 @@ export class SessionConversation extends React.Component { return; } - toDeleteLocally = await deleteOpenGroupMessages(selectedMessages, conversationModel); - if (toDeleteLocally.length === 0) { + toDeleteLocallyIds = await deleteOpenGroupMessages(selectedMessages, conversationModel); + if (toDeleteLocallyIds.length === 0) { // Message failed to delete from server, show error? return; } } else { - toDeleteLocally = selectedMessages; + toDeleteLocallyIds = selectedMessages.map(pro => pro.propsForMessage.id); } await Promise.all( - toDeleteLocally.map(async message => { - await conversationModel.removeMessage(message.id); + toDeleteLocallyIds.map(async msgId => { + await conversationModel.removeMessage(msgId); }) ); @@ -684,15 +685,19 @@ export class SessionConversation extends React.Component { return; } if (!_.isEqual(this.state.quotedMessageTimestamp, quotedMessageTimestamp)) { - const { messages, selectedConversationKey } = this.props; + const { messagesProps, selectedConversationKey } = this.props; const conversationModel = getConversationController().getOrThrow(selectedConversationKey); let quotedMessageProps = null; if (quotedMessageTimestamp) { - const quotedMessage = messages.find(m => m.attributes.sent_at === quotedMessageTimestamp); + const quotedMessage = messagesProps.find( + m => + m.propsForMessage.timestamp === quotedMessageTimestamp || + m.propsForMessage.serverTimestamp === quotedMessageTimestamp + ); if (quotedMessage) { - const quotedMessageModel = await getMessageById(quotedMessage.id); + const quotedMessageModel = await getMessageById(quotedMessage.propsForMessage.id); if (quotedMessageModel) { quotedMessageProps = await conversationModel.makeQuote(quotedMessageModel); } diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index bc035f61f..4c27410b8 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -35,7 +35,7 @@ interface State { interface Props { selectedMessages: Array; conversationKey: string; - messages: Array; + messagesProps: Array; conversation: ConversationType; ourPrimary: string; messageContainerRef: React.RefObject; @@ -98,8 +98,11 @@ export class SessionMessagesList extends React.Component { public componentDidUpdate(prevProps: Props, _prevState: State) { const isSameConvo = prevProps.conversationKey === this.props.conversationKey; - const messageLengthChanged = prevProps.messages.length !== this.props.messages.length; - if (!isSameConvo || (prevProps.messages.length === 0 && this.props.messages.length !== 0)) { + const messageLengthChanged = prevProps.messagesProps.length !== this.props.messagesProps.length; + if ( + !isSameConvo || + (prevProps.messagesProps.length === 0 && this.props.messagesProps.length !== 0) + ) { // displayed conversation changed. We have a bit of cleaning to do here this.scrollOffsetBottomPx = Number.MAX_VALUE; this.ignoreScrollEvents = true; @@ -133,7 +136,7 @@ export class SessionMessagesList extends React.Component { } public render() { - const { conversationKey, conversation, messages } = this.props; + const { conversationKey, conversation, messagesProps } = this.props; const { showScrollButton } = this.state; let displayedName = null; @@ -157,7 +160,7 @@ export class SessionMessagesList extends React.Component { key="typing-bubble" /> - {this.renderMessages(messages)} + {this.renderMessages()} { return findFirstUnreadIndex; } - private renderMessages(messagesProps: Array) { - const { selectedMessages } = this.props; + private renderMessages() { + const { selectedMessages, messagesProps } = this.props; const multiSelectMode = Boolean(selectedMessages.length); let currentMessageIndex = 0; let playableMessageIndex = 0; @@ -367,9 +370,9 @@ export class SessionMessagesList extends React.Component { // ~~~~~~~~~~~~~ MESSAGE HANDLING ~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ private updateReadMessages() { - const { messages, conversationKey } = this.props; + const { messagesProps, conversationKey } = this.props; - if (!messages || messages.length === 0) { + if (!messagesProps || messagesProps.length === 0) { return; } @@ -384,7 +387,7 @@ export class SessionMessagesList extends React.Component { } if (this.getScrollOffsetBottomPx() === 0) { - void conversation.markRead(messages[0].propsForMessage.receivedAt); + void conversation.markRead(messagesProps[0].propsForMessage.receivedAt); } } @@ -393,11 +396,11 @@ export class SessionMessagesList extends React.Component { * @param index index of message that just completed */ private readonly playNextMessage = (index: any) => { - const { messages } = this.props; + const { messagesProps } = this.props; let nextIndex: number | undefined = index - 1; // to prevent autoplaying as soon as a message is received. - const latestMessagePlayed = index <= 0 || messages.length < index - 1; + const latestMessagePlayed = index <= 0 || messagesProps.length < index - 1; if (latestMessagePlayed) { nextIndex = undefined; this.setState({ @@ -407,8 +410,8 @@ export class SessionMessagesList extends React.Component { } // stop auto-playing when the audio messages change author. - const prevAuthorNumber = messages[index].propsForMessage.authorPhoneNumber; - const nextAuthorNumber = messages[index - 1].propsForMessage.authorPhoneNumber; + const prevAuthorNumber = messagesProps[index].propsForMessage.authorPhoneNumber; + const nextAuthorNumber = messagesProps[index - 1].propsForMessage.authorPhoneNumber; const differentAuthor = prevAuthorNumber !== nextAuthorNumber; if (differentAuthor) { nextIndex = undefined; @@ -464,28 +467,27 @@ export class SessionMessagesList extends React.Component { const shouldFetchMoreMessages = scrollTop <= Constants.UI.MESSAGE_CONTAINER_BUFFER_OFFSET_PX; if (shouldFetchMoreMessages) { - const { messages } = this.props; - const numMessages = - this.props.messages.length + Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT; - const oldLen = messages.length; - const previousTopMessage = messages[oldLen - 1]?.propsForMessage.id; + const { messagesProps } = this.props; + const numMessages = messagesProps.length + Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT; + const oldLen = messagesProps.length; + const previousTopMessage = messagesProps[oldLen - 1]?.propsForMessage.id; fetchMessagesForConversation({ conversationKey, count: numMessages }); - if (previousTopMessage && oldLen !== messages.length) { + if (previousTopMessage && oldLen !== messagesProps.length) { this.scrollToMessage(previousTopMessage); } } } private scrollToUnread() { - const { messages, conversation } = this.props; + const { messagesProps, conversation } = this.props; if (conversation.unreadCount > 0) { let message; - if (messages.length > conversation.unreadCount) { + if (messagesProps.length > conversation.unreadCount) { // if we have enough message to show one more message, show one more to include the unread banner - message = messages[conversation.unreadCount - 1]; + message = messagesProps[conversation.unreadCount - 1]; } else { - message = messages[conversation.unreadCount - 1]; + message = messagesProps[conversation.unreadCount - 1]; } if (message) { @@ -493,7 +495,7 @@ export class SessionMessagesList extends React.Component { } } - if (this.ignoreScrollEvents && messages.length > 0) { + if (this.ignoreScrollEvents && messagesProps.length > 0) { this.ignoreScrollEvents = false; this.updateReadMessages(); } @@ -563,6 +565,8 @@ export class SessionMessagesList extends React.Component { private async scrollToQuoteMessage(options: QuoteClickOptions) { const { quoteAuthor, quoteId, referencedMessageNotFound } = options; + const { messagesProps } = this.props; + // For simplicity's sake, we show the 'not found' toast no matter what if we were // not able to find the referenced message when the quote was received. if (referencedMessageNotFound) { @@ -570,7 +574,7 @@ export class SessionMessagesList extends React.Component { return; } // Look for message in memory first, which would tell us if we could scroll to it - const targetMessage = this.props.messages.find(item => { + const targetMessage = messagesProps.find(item => { const messageAuthor = item.propsForMessage?.authorPhoneNumber; if (!messageAuthor || quoteAuthor !== messageAuthor) { diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 395310ed9..d21741b79 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -31,7 +31,7 @@ import { lastAvatarUploadTimestamp, removeAllMessagesInConversation, } from '../data/data'; -import { conversationReset } from '../state/ducks/conversations'; +import { conversationReset, SortedMessageModelProps } from '../state/ducks/conversations'; import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager'; import { IMAGE_JPEG } from '../types/MIME'; import { FSv2 } from '../fileserver'; @@ -87,9 +87,9 @@ export async function copyPublicKeyByConvoId(convoId: string) { * @param convo the conversation to delete from (only v2 opengroups are supported) */ export async function deleteOpenGroupMessages( - messages: Array, + messages: Array, convo: ConversationModel -): Promise> { +): Promise> { if (!convo.isPublic()) { throw new Error('cannot delete public message on a non public groups'); } @@ -100,14 +100,13 @@ export async function deleteOpenGroupMessages( // so logic here is to delete each messages and get which one where not removed const validServerIdsToRemove = _.compact( messages.map(msg => { - const serverId = msg.get('serverId'); - return serverId; + return msg.propsForMessage.serverId; }) ); const validMessageModelsToRemove = _.compact( messages.map(msg => { - const serverId = msg.get('serverId'); + const serverId = msg.propsForMessage.serverId; if (serverId) { return msg; } @@ -125,7 +124,7 @@ export async function deleteOpenGroupMessages( // remove only the messages we managed to remove on the server if (allMessagesAreDeleted) { window?.log?.info('Removed all those serverIds messages successfully'); - return validMessageModelsToRemove; + return validMessageModelsToRemove.map(m => m.propsForMessage.id); } else { window?.log?.info( 'failed to remove all those serverIds message. not removing them locally neither' diff --git a/ts/models/message.ts b/ts/models/message.ts index 64f79650c..bb8321918 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -551,6 +551,7 @@ export class MessageModel extends Backbone.Model { timestamp: this.get('sent_at'), receivedAt: this.get('received_at'), serverTimestamp: this.get('serverTimestamp'), + serverId: this.get('serverId'), status: this.getMessagePropStatus(), authorName: senderContact.name, authorProfileName: senderContact.profileName, diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index b1b90652c..78234e87a 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -11,7 +11,6 @@ import { MessageModelType, PropsForDataExtractionNotification, } from '../../models/messageType'; -import { AttachmentType } from '../../types/Attachment'; export type MessageModelProps = { propsForMessage: PropsForMessage; @@ -137,6 +136,7 @@ export type PropsForMessage = { timestamp: number | undefined; receivedAt: number | undefined; serverTimestamp: number | undefined; + serverId: number | undefined; status: LastMessageStatusType; authorName: string | null; authorProfileName: string | null; diff --git a/ts/state/smart/SessionConversation.tsx b/ts/state/smart/SessionConversation.tsx index b05332ee0..bfd8e2bad 100644 --- a/ts/state/smart/SessionConversation.tsx +++ b/ts/state/smart/SessionConversation.tsx @@ -15,7 +15,7 @@ const mapStateToProps = (state: StateType) => { selectedConversation: getSelectedConversation(state), selectedConversationKey: getSelectedConversationKey(state), theme: getTheme(state), - messages: getMessagesOfSelectedConversation(state), + messagesProps: getMessagesOfSelectedConversation(state), ourNumber: getOurNumber(state), }; };