From 7972fd7c1f985b5e3a7ce69aff6cbe57a3458d0c Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 6 Nov 2025 16:11:40 +0100 Subject: [PATCH] Add message editing feature --- .../Localizable/en.lproj/Localizable.strings | 3 + .../Localizable/fr.lproj/Localizable.strings | 3 + .../Fragments/ChatBubbleView.swift | 16 ++ .../Fragments/ConversationFragment.swift | 150 ++++++++++++--- .../UI/Main/Conversations/Model/Message.swift | 26 ++- .../ViewModel/ConversationViewModel.swift | 173 ++++++++++++++++++ 6 files changed, 343 insertions(+), 28 deletions(-) diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index 0a30c7940..c68cdeafa 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -204,6 +204,8 @@ "conversation_dialog_edit_subject" = "Edit conversation subject"; "conversation_dialog_set_subject" = "Set conversation subject"; "conversation_dialog_subject_hint" = "Conversation subject"; +"conversation_editing_message_title" = "Message being edited"; +"conversation_message_edited_label" = "Edited"; "conversation_end_to_end_encrypted_bottom_sheet_link" = "https://linphone.org/en/features/#security"; "conversation_end_to_end_encrypted_event_title" = "End-to-end encrypted conversation"; "conversation_end_to_end_encrypted_event_subtitle" = "Messages in this conversation are e2e encrypted. Only your correspondent can decrypt them."; @@ -394,6 +396,7 @@ "menu_delete_selected_item" = "Delete"; "menu_forward_chat_message" = "Forward"; "menu_invite" = "Invite"; +"menu_edit_chat_message" = "Edit"; "menu_reply_to_chat_message" = "Reply"; "menu_resend_chat_message" = "Re-send"; "menu_see_existing_contact" = "See contact"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index 02d7afec5..b6008c570 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -204,6 +204,8 @@ "conversation_dialog_edit_subject" = "Renommer la conversation"; "conversation_dialog_set_subject" = "Nommer la conversation"; "conversation_dialog_subject_hint" = "Nom de la conversation"; +"conversation_editing_message_title" = "Modification du message"; +"conversation_message_edited_label" = "Modifié"; "conversation_end_to_end_encrypted_bottom_sheet_link" = "https://linphone.org/en/features/#security"; "conversation_end_to_end_encrypted_event_title" = "Conversation chiffrée de bout en bout"; "conversation_end_to_end_encrypted_event_subtitle" = "Les messages de cette conversation sont chiffrés de bout en bout. Seul votre correspondant peut les déchiffrer."; @@ -394,6 +396,7 @@ "menu_delete_selected_item" = "Supprimer"; "menu_forward_chat_message" = "Transférer"; "menu_invite" = "Inviter"; +"menu_edit_chat_message" = "Modifier"; "menu_reply_to_chat_message" = "Répondre"; "menu_resend_chat_message" = "Ré-envoyer"; "menu_see_existing_contact" = "Voir le contact"; diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index 368817e12..c3ab77c7a 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -325,6 +325,14 @@ struct ChatBubbleView: View { .padding(.top, 1) } + if eventLogMessage.message.isEdited && eventLogMessage.message.isOutgoing { + Text("conversation_message_edited_label") + .foregroundStyle(Color.grayMain2c500) + .default_text_style_300(styleSize: 12) + .padding(.top, 1) + .padding(.trailing, -4) + } + Text(conversationViewModel.getMessageTime(startDate: eventLogMessage.message.dateReceived)) .foregroundStyle(Color.grayMain2c500) .default_text_style_300(styleSize: 12) @@ -349,6 +357,14 @@ struct ChatBubbleView: View { } } + if eventLogMessage.message.isEdited && !eventLogMessage.message.isOutgoing { + Text("conversation_message_edited_label") + .foregroundStyle(Color.grayMain2c500) + .default_text_style_300(styleSize: 12) + .padding(.top, 1) + .padding(.trailing, -4) + } + if eventLogMessage.message.isEphemeral && !eventLogMessage.message.isOutgoing { Image("clock-countdown") .renderingMode(.template) diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index 25b67299a..51b68ac0f 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -620,6 +620,43 @@ struct ConversationFragment: View { } } .transition(.move(edge: .bottom)) + } else if conversationViewModel.messageToEdit != nil { + ZStack(alignment: .top) { + HStack { + VStack { + Text("conversation_editing_message_title") + .default_text_style_300(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 1) + .lineLimit(1) + + Text("\(conversationViewModel.messageToEdit!.message.text)") + .default_text_style_300(styleSize: 15) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } + } + .frame(maxWidth: .infinity) + .padding(.all, 20) + .background(Color.gray100) + + HStack { + Spacer() + + Button(action: { + messageText = "" + withAnimation { + conversationViewModel.messageToEdit = nil + } + }, label: { + Image("x") + .resizable() + .frame(width: 30, height: 30, alignment: .leading) + .padding(.all, 10) + }) + } + } + .transition(.move(edge: .bottom)) } if !conversationViewModel.mediasToSend.isEmpty || mediasIsLoading { @@ -879,43 +916,66 @@ struct ConversationFragment: View { } } - if messageText.isEmpty && conversationViewModel.mediasToSend.isEmpty { - Button { - voiceRecordingInProgress = true - } label: { - Image("microphone") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c500) - .frame(width: 28, height: 28, alignment: .leading) - .padding(.all, 6) - .padding(.top, 4) + if conversationViewModel.messageToEdit == nil { + if messageText.isEmpty && conversationViewModel.mediasToSend.isEmpty { + Button { + voiceRecordingInProgress = true + } label: { + Image("microphone") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 28, height: 28, alignment: .leading) + .padding(.all, 6) + .padding(.top, 4) + } + } else { + Button { + if conversationViewModel.displayedConversationHistorySize > 1 { + NotificationCenter.default.post(name: .onScrollToBottom, object: nil) + } + + let messageTextTmp = self.messageText + messageText = " " + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + messageText = "" + isMessageTextFocused = true + + conversationViewModel.sendMessage(messageText: messageTextTmp) + } + } label: { + Image("paper-plane-tilt") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 28, height: 28, alignment: .leading) + .padding(.all, 6) + .padding(.top, 4) + .rotationEffect(.degrees(45)) + } + .padding(.trailing, 4) } } else { Button { - if conversationViewModel.displayedConversationHistorySize > 1 { - NotificationCenter.default.post(name: .onScrollToBottom, object: nil) - } - let messageTextTmp = self.messageText - messageText = " " - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - messageText = "" - isMessageTextFocused = true - - conversationViewModel.sendMessage(messageText: messageTextTmp) - } + messageText = " " + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + messageText = "" + isMessageTextFocused = true + + conversationViewModel.sendMessage(messageText: messageTextTmp) + } } label: { - Image("paper-plane-tilt") + Image("pencil-simple") .renderingMode(.template) .resizable() - .foregroundStyle(Color.orangeMain500) + .foregroundStyle(messageText.isEmpty ? Color.gray300 : Color.orangeMain500) .frame(width: 28, height: 28, alignment: .leading) .padding(.all, 6) .padding(.top, 4) - .rotationEffect(.degrees(45)) } .padding(.trailing, 4) + .disabled(messageText.isEmpty) } } .padding(.leading, 15) @@ -1096,6 +1156,43 @@ struct ConversationFragment: View { Divider() } + + if conversationViewModel.selectedMessage!.message.isOutgoing + && !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) + && conversationViewModel.selectedMessage!.message.isEditable { + Button { + if let chatMessage = conversationViewModel.selectedMessage { + if voiceRecordingInProgress { + voiceRecordingInProgress = false + } + + messageText = chatMessage.message.text + conversationViewModel.selectedMessage = nil + conversationViewModel.editMessage( + chatMessage: chatMessage, + isMessageTextFocused: Binding( + get: { isMessageTextFocused }, + set: { isMessageTextFocused = $0 } + ) + ) + } + } label: { + HStack { + Text("menu_edit_chat_message") + .default_text_style(styleSize: 15) + Spacer() + Image("pencil-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 20, height: 20, alignment: .leading) + } + .padding(.vertical, 5) + .padding(.horizontal, 20) + } + + Divider() + } Button { let indexMessage = conversationViewModel.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.id == conversationViewModel.selectedMessage!.message.id}) @@ -1211,6 +1308,9 @@ struct ConversationFragment: View { } .onAppear { touchFeedback() + if isMessageTextFocused { + isMessageTextFocused = false + } } .onDisappear { if conversationViewModel.selectedMessage != nil { diff --git a/Linphone/UI/Main/Conversations/Model/Message.swift b/Linphone/UI/Main/Conversations/Model/Message.swift index 48a36e7a1..6a4a4089c 100644 --- a/Linphone/UI/Main/Conversations/Model/Message.swift +++ b/Linphone/UI/Main/Conversations/Model/Message.swift @@ -68,6 +68,8 @@ public struct Message: Identifiable, Hashable { public var status: Status? public var createdAt: Date public var isOutgoing: Bool + public var isEditable: Bool + public var isEdited: Bool public var dateReceived: time_t public var address: String @@ -94,6 +96,8 @@ public struct Message: Identifiable, Hashable { status: Status? = nil, createdAt: Date = Date(), isOutgoing: Bool, + isEditable: Bool, + isEdited: Bool, dateReceived: time_t, address: String, isFirstMessage: Bool = false, @@ -116,6 +120,8 @@ public struct Message: Identifiable, Hashable { self.status = status self.createdAt = createdAt self.isOutgoing = isOutgoing + self.isEditable = isEditable + self.isEdited = isEdited self.dateReceived = dateReceived self.isFirstMessage = isFirstMessage self.address = address @@ -163,6 +169,8 @@ public struct Message: Identifiable, Hashable { status: status, createdAt: draft.createdAt, isOutgoing: draft.isOutgoing, + isEditable: draft.isEditable, + isEdited: draft.isEdited, dateReceived: draft.dateReceived, address: draft.address, isFirstMessage: draft.isFirstMessage, @@ -184,7 +192,7 @@ extension Message { extension Message: Equatable { public static func == (lhs: Message, rhs: Message) -> Bool { - lhs.id == rhs.id && lhs.status == rhs.status && lhs.isFirstMessage == rhs.isFirstMessage && lhs.ownReaction == rhs.ownReaction && lhs.reactions == rhs.reactions && lhs.ephemeralExpireTime == rhs.ephemeralExpireTime && lhs.attachments == rhs.attachments + lhs.id == rhs.id && lhs.status == rhs.status && lhs.isEdited == rhs.isEdited && lhs.isFirstMessage == rhs.isFirstMessage && lhs.text == rhs.text && lhs.attachments == rhs.attachments && lhs.replyMessage?.text == rhs.replyMessage?.text && lhs.ownReaction == rhs.ownReaction && lhs.reactions == rhs.reactions && lhs.ephemeralExpireTime == rhs.ephemeralExpireTime } } @@ -211,6 +219,8 @@ public struct ReplyMessage: Codable, Identifiable, Hashable { public var isFirstMessage: Bool public var text: String public var isOutgoing: Bool + public var isEditable: Bool + public var isEdited: Bool public var dateReceived: time_t public var attachmentsNames: String public var attachments: [Attachment] @@ -221,6 +231,8 @@ public struct ReplyMessage: Codable, Identifiable, Hashable { isFirstMessage: Bool = false, text: String = "", isOutgoing: Bool, + isEditable: Bool, + isEdited: Bool, dateReceived: time_t, attachmentsNames: String = "", attachments: [Attachment] = [], @@ -231,6 +243,8 @@ public struct ReplyMessage: Codable, Identifiable, Hashable { self.isFirstMessage = isFirstMessage self.text = text self.isOutgoing = isOutgoing + self.isEditable = isEditable + self.isEdited = isEdited self.dateReceived = dateReceived self.attachmentsNames = attachmentsNames self.attachments = attachments @@ -238,20 +252,22 @@ public struct ReplyMessage: Codable, Identifiable, Hashable { } func toMessage() -> Message { - Message(id: id, isOutgoing: isOutgoing, dateReceived: dateReceived, address: address, isFirstMessage: isFirstMessage, text: text, attachments: attachments, recording: recording) + Message(id: id, isOutgoing: isOutgoing, isEditable: isEditable, isEdited: isEdited, dateReceived: dateReceived, address: address, isFirstMessage: isFirstMessage, text: text, attachments: attachments, recording: recording) } } public extension Message { func toReplyMessage() -> ReplyMessage { - ReplyMessage(id: id, address: address, isFirstMessage: isFirstMessage, text: text, isOutgoing: isOutgoing, dateReceived: dateReceived, attachments: attachments, recording: recording) + ReplyMessage(id: id, address: address, isFirstMessage: isFirstMessage, text: text, isOutgoing: isOutgoing, isEditable: isEditable, isEdited: isEdited, dateReceived: dateReceived, attachments: attachments, recording: recording) } } public struct DraftMessage { public var id: String? public let isOutgoing: Bool + public let isEditable: Bool + public let isEdited: Bool public var dateReceived: time_t public let address: String public let isFirstMessage: Bool @@ -265,6 +281,8 @@ public struct DraftMessage { public init(id: String? = nil, isOutgoing: Bool, + isEditable: Bool, + isEdited: Bool, dateReceived: time_t, address: String, isFirstMessage: Bool, @@ -278,6 +296,8 @@ public struct DraftMessage { ) { self.id = id self.isOutgoing = isOutgoing + self.isEditable = isEditable + self.isEdited = isEdited self.dateReceived = dateReceived self.address = address self.isFirstMessage = isFirstMessage diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 50eab7080..5064cdf09 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -93,6 +93,7 @@ class ConversationViewModel: ObservableObject { @Published var selectedMessageToPlayVoiceRecording: EventLogMessage? @Published var selectedMessage: EventLogMessage? @Published var messageToReply: EventLogMessage? + @Published var messageToEdit: EventLogMessage? @Published var sheetCategories: [SheetCategory] = [] @@ -171,7 +172,127 @@ class ConversationViewModel: ObservableObject { self.getEventMessage(eventLog: eventLog) }, onEphemeralMessageDeleted: {(_: ChatRoom, eventLog: EventLog) in self.removeMessage(eventLog) + }, onMessageContentEdited: {(chatRoom: ChatRoom, message: ChatMessage) in + let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.eventModel.eventLogId == message.messageId}) + + var attachmentNameList: String = "" + var attachmentList: [Attachment] = [] + var contentText = "" + + if !message.contents.isEmpty { + message.contents.forEach { content in + if content.isText && content.name == nil { + contentText = content.utf8Text ?? "" + } else if content.name != nil && !content.name!.isEmpty { + if content.filePath == nil || content.filePath!.isEmpty { + let path = URL(string: self.getNewFilePath(name: content.name ?? "")) + + if path != nil { + let attachment = + Attachment( + id: UUID().uuidString, + name: content.name!, + url: path!, + type: .fileTransfer, + size: content.fileSize, + transferProgressIndication: content.filePath != nil && !content.filePath!.isEmpty ? 100 : -1 + ) + attachmentNameList += ", \(content.name!)" + attachmentList.append(attachment) + } + } else { + if content.type != "video" { + let filePathSep = content.filePath!.components(separatedBy: "/Library/Images/") + let path = URL(string: self.getNewFilePath(name: filePathSep[1])) + + var typeTmp: AttachmentType = .other + + switch content.type { + case "image": + typeTmp = (content.name?.lowercased().hasSuffix("gif"))! ? .gif : .image + case "audio": + typeTmp = content.isVoiceRecording ? .voiceRecording : .audio + case "application": + typeTmp = content.subtype.lowercased() == "pdf" ? .pdf : .other + case "text": + typeTmp = .text + default: + typeTmp = .other + } + + if path != nil { + let attachment = + Attachment( + id: UUID().uuidString, + name: content.name!, + url: path!, + type: typeTmp, + duration: typeTmp == .voiceRecording ? content.fileDuration : 0, + size: content.fileSize, + transferProgressIndication: content.filePath != nil && !content.filePath!.isEmpty ? 100 : -1 + ) + attachmentNameList += ", \(content.name!)" + attachmentList.append(attachment) + if typeTmp != .voiceRecording { + DispatchQueue.main.async { + if !attachment.full.pathExtension.isEmpty { + self.attachments.append(attachment) + } + } + } + } + } else if content.type == "video" { + let filePathSep = content.filePath!.components(separatedBy: "/Library/Images/") + let path = URL(string: self.getNewFilePath(name: filePathSep[1])) + let pathThumbnail = URL(string: self.generateThumbnail(name: filePathSep[1])) + + if path != nil && pathThumbnail != nil { + let attachment = + Attachment( + id: UUID().uuidString, + name: content.name!, + thumbnail: pathThumbnail!, + full: path!, + type: .video, + size: content.fileSize, + transferProgressIndication: content.filePath != nil && !content.filePath!.isEmpty ? 100 : -1 + ) + attachmentNameList += ", \(content.name!)" + attachmentList.append(attachment) + DispatchQueue.main.async { + if !attachment.full.pathExtension.isEmpty { + self.attachments.append(attachment) + } + } + } + } + } + } + } + } + + if !attachmentNameList.isEmpty { + attachmentNameList = String(attachmentNameList.dropFirst(2)) + } + + let indexReplyMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.replyMessage?.id == message.messageId}) + + DispatchQueue.main.async { + if indexMessage != nil { + self.conversationMessagesSection[0].rows[indexMessage!].message.text = contentText + self.conversationMessagesSection[0].rows[indexMessage!].message.isEdited = true + self.conversationMessagesSection[0].rows[indexMessage!].message.attachments = attachmentList + self.conversationMessagesSection[0].rows[indexMessage!].message.attachmentsNames = attachmentNameList + } + + if indexReplyMessage != nil { + self.conversationMessagesSection[0].rows[indexReplyMessage!].message.replyMessage?.text = contentText + } + } + }, onMessageRetracted: {(chatRoom: ChatRoom, message: ChatMessage) in + // TODO }) + self.chatRoomDelegateHolder = ChatRoomDelegateHolder(chatroom: chatRoom, delegate: chatRoomDelegate) } @@ -544,6 +665,8 @@ class ConversationViewModel: ObservableObject { id: UUID().uuidString, status: nil, isOutgoing: false, + isEditable: false, + isEdited: false, dateReceived: 0, address: "", isFirstMessage: false, @@ -722,6 +845,8 @@ class ConversationViewModel: ObservableObject { isFirstMessage: false, text: contentReplyText, isOutgoing: false, + isEditable: false, + isEdited: false, dateReceived: 0, attachmentsNames: attachmentNameReplyList, attachments: [] @@ -735,6 +860,8 @@ class ConversationViewModel: ObservableObject { id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString, status: statusTmp, isOutgoing: chatMessage.isOutgoing, + isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false, + isEdited: chatMessage.isEdited, dateReceived: chatMessage.time, address: addressCleaned?.asStringUriOnly() ?? "", isFirstMessage: isFirstMessageTmp, @@ -788,6 +915,8 @@ class ConversationViewModel: ObservableObject { id: UUID().uuidString, status: nil, isOutgoing: false, + isEditable: false, + isEdited: false, dateReceived: 0, address: "", isFirstMessage: false, @@ -965,6 +1094,8 @@ class ConversationViewModel: ObservableObject { isFirstMessage: false, text: contentReplyText, isOutgoing: false, + isEditable: false, + isEdited: false, dateReceived: 0, attachmentsNames: attachmentNameReplyList, attachments: [] @@ -978,6 +1109,8 @@ class ConversationViewModel: ObservableObject { id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString, status: statusTmp, isOutgoing: chatMessage.isOutgoing, + isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false, + isEdited: chatMessage.isEdited, dateReceived: chatMessage.time, address: addressCleaned?.asStringUriOnly() ?? "", isFirstMessage: isFirstMessageTmp, @@ -1048,6 +1181,8 @@ class ConversationViewModel: ObservableObject { id: UUID().uuidString, status: nil, isOutgoing: false, + isEditable: false, + isEdited: false, dateReceived: 0, address: "", isFirstMessage: false, @@ -1239,6 +1374,8 @@ class ConversationViewModel: ObservableObject { isFirstMessage: false, text: contentReplyText, isOutgoing: false, + isEditable: false, + isEdited: false, dateReceived: 0, attachmentsNames: attachmentNameReplyList, attachments: [] @@ -1253,6 +1390,8 @@ class ConversationViewModel: ObservableObject { appData: chatMessage.appdata ?? "", status: statusTmp, isOutgoing: chatMessage.isOutgoing, + isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false, + isEdited: chatMessage.isEdited, dateReceived: chatMessage.time, address: addressCleaned != nil ? addressCleaned!.asStringUriOnly() : "", isFirstMessage: isFirstMessageTmp, @@ -1471,6 +1610,8 @@ class ConversationViewModel: ObservableObject { isFirstMessage: false, text: contentReplyText, isOutgoing: false, + isEditable: false, + isEdited: false, dateReceived: 0, attachmentsNames: attachmentNameReplyList, attachments: [] @@ -1485,6 +1626,8 @@ class ConversationViewModel: ObservableObject { appData: chatMessage.appdata ?? "", status: statusTmp, isOutgoing: chatMessage.isOutgoing, + isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false, + isEdited: chatMessage.isEdited, dateReceived: chatMessage.time, address: addressCleaned != nil ? addressCleaned!.asStringUriOnly() : "", isFirstMessage: isFirstMessageTmp, @@ -1526,6 +1669,8 @@ class ConversationViewModel: ObservableObject { id: UUID().uuidString, status: nil, isOutgoing: false, + isEditable: false, + isEdited: false, dateReceived: 0, address: "", isFirstMessage: false, @@ -1553,6 +1698,9 @@ class ConversationViewModel: ObservableObject { } func replyToMessage(index: Int, isMessageTextFocused: Binding) { + if self.messageToEdit != nil { + self.messageToEdit = nil + } coreContext.doOnCoreQueue { _ in let messageToReplyTmp = self.conversationMessagesSection[0].rows[index] DispatchQueue.main.async { @@ -1564,6 +1712,21 @@ class ConversationViewModel: ObservableObject { } } + func editMessage(chatMessage: EventLogMessage, isMessageTextFocused: Binding) { + if self.messageToReply != nil { + self.messageToReply = nil + } + coreContext.doOnCoreQueue { _ in + let messageToEditTmp = chatMessage + DispatchQueue.main.async { + withAnimation(.linear(duration: 0.15)) { + self.messageToEdit = messageToEditTmp + } + isMessageTextFocused.wrappedValue = true + } + } + } + func resendMessage(chatMessage: EventLogMessage) { coreContext.doOnCoreQueue { _ in if let message = chatMessage.eventModel.eventLog.chatMessage { @@ -1619,6 +1782,8 @@ class ConversationViewModel: ObservableObject { id: UUID().uuidString, status: nil, isOutgoing: false, + isEditable: false, + isEdited: false, dateReceived: 0, address: "", isFirstMessage: false, @@ -1796,6 +1961,8 @@ class ConversationViewModel: ObservableObject { isFirstMessage: false, text: contentReplyText, isOutgoing: false, + isEditable: false, + isEdited: false, dateReceived: 0, attachmentsNames: attachmentNameReplyList, attachments: [] @@ -1809,6 +1976,8 @@ class ConversationViewModel: ObservableObject { id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString, status: statusTmp, isOutgoing: chatMessage.isOutgoing, + isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false, + isEdited: chatMessage.isEdited, dateReceived: chatMessage.time, address: addressCleaned?.asStringUriOnly() ?? "", isFirstMessage: isFirstMessageTmp, @@ -1873,6 +2042,8 @@ class ConversationViewModel: ObservableObject { if chatMessageToReply != nil { message = try self.sharedMainViewModel.displayedConversation!.chatRoom.createReplyMessage(message: chatMessageToReply!) } + } else if let chatMessage = self.messageToEdit?.eventModel.eventLog.chatMessage { + message = try self.sharedMainViewModel.displayedConversation!.chatRoom.createReplacesMessage(message: chatMessage) } else { message = try self.sharedMainViewModel.displayedConversation!.chatRoom.createEmptyMessage() } @@ -1948,12 +2119,14 @@ class ConversationViewModel: ObservableObject { if message != nil && !message!.contents.isEmpty { Log.info("[ConversationViewModel] Sending message") message!.send() + self.sharedMainViewModel.displayedConversation!.chatRoom.stopComposing() } Log.info("[ConversationViewModel] Message sent, re-setting defaults") DispatchQueue.main.async { self.messageToReply = nil + self.messageToEdit = nil withAnimation { self.mediasToSend.removeAll() }