Add message editing feature

This commit is contained in:
Benoit Martins 2025-11-06 16:11:40 +01:00
parent fa1f8386b4
commit 7972fd7c1f
6 changed files with 343 additions and 28 deletions

View file

@ -204,6 +204,8 @@
"conversation_dialog_edit_subject" = "Edit conversation subject"; "conversation_dialog_edit_subject" = "Edit conversation subject";
"conversation_dialog_set_subject" = "Set conversation subject"; "conversation_dialog_set_subject" = "Set conversation subject";
"conversation_dialog_subject_hint" = "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_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_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."; "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_delete_selected_item" = "Delete";
"menu_forward_chat_message" = "Forward"; "menu_forward_chat_message" = "Forward";
"menu_invite" = "Invite"; "menu_invite" = "Invite";
"menu_edit_chat_message" = "Edit";
"menu_reply_to_chat_message" = "Reply"; "menu_reply_to_chat_message" = "Reply";
"menu_resend_chat_message" = "Re-send"; "menu_resend_chat_message" = "Re-send";
"menu_see_existing_contact" = "See contact"; "menu_see_existing_contact" = "See contact";

View file

@ -204,6 +204,8 @@
"conversation_dialog_edit_subject" = "Renommer la conversation"; "conversation_dialog_edit_subject" = "Renommer la conversation";
"conversation_dialog_set_subject" = "Nommer la conversation"; "conversation_dialog_set_subject" = "Nommer la conversation";
"conversation_dialog_subject_hint" = "Nom de 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_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_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."; "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_delete_selected_item" = "Supprimer";
"menu_forward_chat_message" = "Transférer"; "menu_forward_chat_message" = "Transférer";
"menu_invite" = "Inviter"; "menu_invite" = "Inviter";
"menu_edit_chat_message" = "Modifier";
"menu_reply_to_chat_message" = "Répondre"; "menu_reply_to_chat_message" = "Répondre";
"menu_resend_chat_message" = "Ré-envoyer"; "menu_resend_chat_message" = "Ré-envoyer";
"menu_see_existing_contact" = "Voir le contact"; "menu_see_existing_contact" = "Voir le contact";

View file

@ -325,6 +325,14 @@ struct ChatBubbleView: View {
.padding(.top, 1) .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)) Text(conversationViewModel.getMessageTime(startDate: eventLogMessage.message.dateReceived))
.foregroundStyle(Color.grayMain2c500) .foregroundStyle(Color.grayMain2c500)
.default_text_style_300(styleSize: 12) .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 { if eventLogMessage.message.isEphemeral && !eventLogMessage.message.isOutgoing {
Image("clock-countdown") Image("clock-countdown")
.renderingMode(.template) .renderingMode(.template)

View file

@ -620,6 +620,43 @@ struct ConversationFragment: View {
} }
} }
.transition(.move(edge: .bottom)) .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 { if !conversationViewModel.mediasToSend.isEmpty || mediasIsLoading {
@ -879,43 +916,66 @@ struct ConversationFragment: View {
} }
} }
if messageText.isEmpty && conversationViewModel.mediasToSend.isEmpty { if conversationViewModel.messageToEdit == nil {
Button { if messageText.isEmpty && conversationViewModel.mediasToSend.isEmpty {
voiceRecordingInProgress = true Button {
} label: { voiceRecordingInProgress = true
Image("microphone") } label: {
.renderingMode(.template) Image("microphone")
.resizable() .renderingMode(.template)
.foregroundStyle(Color.grayMain2c500) .resizable()
.frame(width: 28, height: 28, alignment: .leading) .foregroundStyle(Color.grayMain2c500)
.padding(.all, 6) .frame(width: 28, height: 28, alignment: .leading)
.padding(.top, 4) .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 { } else {
Button { Button {
if conversationViewModel.displayedConversationHistorySize > 1 {
NotificationCenter.default.post(name: .onScrollToBottom, object: nil)
}
let messageTextTmp = self.messageText let messageTextTmp = self.messageText
messageText = " " messageText = " "
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
messageText = "" messageText = ""
isMessageTextFocused = true isMessageTextFocused = true
conversationViewModel.sendMessage(messageText: messageTextTmp) conversationViewModel.sendMessage(messageText: messageTextTmp)
} }
} label: { } label: {
Image("paper-plane-tilt") Image("pencil-simple")
.renderingMode(.template) .renderingMode(.template)
.resizable() .resizable()
.foregroundStyle(Color.orangeMain500) .foregroundStyle(messageText.isEmpty ? Color.gray300 : Color.orangeMain500)
.frame(width: 28, height: 28, alignment: .leading) .frame(width: 28, height: 28, alignment: .leading)
.padding(.all, 6) .padding(.all, 6)
.padding(.top, 4) .padding(.top, 4)
.rotationEffect(.degrees(45))
} }
.padding(.trailing, 4) .padding(.trailing, 4)
.disabled(messageText.isEmpty)
} }
} }
.padding(.leading, 15) .padding(.leading, 15)
@ -1096,6 +1156,43 @@ struct ConversationFragment: View {
Divider() 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 { Button {
let indexMessage = conversationViewModel.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.id == conversationViewModel.selectedMessage!.message.id}) let indexMessage = conversationViewModel.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.id == conversationViewModel.selectedMessage!.message.id})
@ -1211,6 +1308,9 @@ struct ConversationFragment: View {
} }
.onAppear { .onAppear {
touchFeedback() touchFeedback()
if isMessageTextFocused {
isMessageTextFocused = false
}
} }
.onDisappear { .onDisappear {
if conversationViewModel.selectedMessage != nil { if conversationViewModel.selectedMessage != nil {

View file

@ -68,6 +68,8 @@ public struct Message: Identifiable, Hashable {
public var status: Status? public var status: Status?
public var createdAt: Date public var createdAt: Date
public var isOutgoing: Bool public var isOutgoing: Bool
public var isEditable: Bool
public var isEdited: Bool
public var dateReceived: time_t public var dateReceived: time_t
public var address: String public var address: String
@ -94,6 +96,8 @@ public struct Message: Identifiable, Hashable {
status: Status? = nil, status: Status? = nil,
createdAt: Date = Date(), createdAt: Date = Date(),
isOutgoing: Bool, isOutgoing: Bool,
isEditable: Bool,
isEdited: Bool,
dateReceived: time_t, dateReceived: time_t,
address: String, address: String,
isFirstMessage: Bool = false, isFirstMessage: Bool = false,
@ -116,6 +120,8 @@ public struct Message: Identifiable, Hashable {
self.status = status self.status = status
self.createdAt = createdAt self.createdAt = createdAt
self.isOutgoing = isOutgoing self.isOutgoing = isOutgoing
self.isEditable = isEditable
self.isEdited = isEdited
self.dateReceived = dateReceived self.dateReceived = dateReceived
self.isFirstMessage = isFirstMessage self.isFirstMessage = isFirstMessage
self.address = address self.address = address
@ -163,6 +169,8 @@ public struct Message: Identifiable, Hashable {
status: status, status: status,
createdAt: draft.createdAt, createdAt: draft.createdAt,
isOutgoing: draft.isOutgoing, isOutgoing: draft.isOutgoing,
isEditable: draft.isEditable,
isEdited: draft.isEdited,
dateReceived: draft.dateReceived, dateReceived: draft.dateReceived,
address: draft.address, address: draft.address,
isFirstMessage: draft.isFirstMessage, isFirstMessage: draft.isFirstMessage,
@ -184,7 +192,7 @@ extension Message {
extension Message: Equatable { extension Message: Equatable {
public static func == (lhs: Message, rhs: Message) -> Bool { 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 isFirstMessage: Bool
public var text: String public var text: String
public var isOutgoing: Bool public var isOutgoing: Bool
public var isEditable: Bool
public var isEdited: Bool
public var dateReceived: time_t public var dateReceived: time_t
public var attachmentsNames: String public var attachmentsNames: String
public var attachments: [Attachment] public var attachments: [Attachment]
@ -221,6 +231,8 @@ public struct ReplyMessage: Codable, Identifiable, Hashable {
isFirstMessage: Bool = false, isFirstMessage: Bool = false,
text: String = "", text: String = "",
isOutgoing: Bool, isOutgoing: Bool,
isEditable: Bool,
isEdited: Bool,
dateReceived: time_t, dateReceived: time_t,
attachmentsNames: String = "", attachmentsNames: String = "",
attachments: [Attachment] = [], attachments: [Attachment] = [],
@ -231,6 +243,8 @@ public struct ReplyMessage: Codable, Identifiable, Hashable {
self.isFirstMessage = isFirstMessage self.isFirstMessage = isFirstMessage
self.text = text self.text = text
self.isOutgoing = isOutgoing self.isOutgoing = isOutgoing
self.isEditable = isEditable
self.isEdited = isEdited
self.dateReceived = dateReceived self.dateReceived = dateReceived
self.attachmentsNames = attachmentsNames self.attachmentsNames = attachmentsNames
self.attachments = attachments self.attachments = attachments
@ -238,20 +252,22 @@ public struct ReplyMessage: Codable, Identifiable, Hashable {
} }
func toMessage() -> Message { 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 { public extension Message {
func toReplyMessage() -> ReplyMessage { 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 struct DraftMessage {
public var id: String? public var id: String?
public let isOutgoing: Bool public let isOutgoing: Bool
public let isEditable: Bool
public let isEdited: Bool
public var dateReceived: time_t public var dateReceived: time_t
public let address: String public let address: String
public let isFirstMessage: Bool public let isFirstMessage: Bool
@ -265,6 +281,8 @@ public struct DraftMessage {
public init(id: String? = nil, public init(id: String? = nil,
isOutgoing: Bool, isOutgoing: Bool,
isEditable: Bool,
isEdited: Bool,
dateReceived: time_t, dateReceived: time_t,
address: String, address: String,
isFirstMessage: Bool, isFirstMessage: Bool,
@ -278,6 +296,8 @@ public struct DraftMessage {
) { ) {
self.id = id self.id = id
self.isOutgoing = isOutgoing self.isOutgoing = isOutgoing
self.isEditable = isEditable
self.isEdited = isEdited
self.dateReceived = dateReceived self.dateReceived = dateReceived
self.address = address self.address = address
self.isFirstMessage = isFirstMessage self.isFirstMessage = isFirstMessage

View file

@ -93,6 +93,7 @@ class ConversationViewModel: ObservableObject {
@Published var selectedMessageToPlayVoiceRecording: EventLogMessage? @Published var selectedMessageToPlayVoiceRecording: EventLogMessage?
@Published var selectedMessage: EventLogMessage? @Published var selectedMessage: EventLogMessage?
@Published var messageToReply: EventLogMessage? @Published var messageToReply: EventLogMessage?
@Published var messageToEdit: EventLogMessage?
@Published var sheetCategories: [SheetCategory] = [] @Published var sheetCategories: [SheetCategory] = []
@ -171,7 +172,127 @@ class ConversationViewModel: ObservableObject {
self.getEventMessage(eventLog: eventLog) self.getEventMessage(eventLog: eventLog)
}, onEphemeralMessageDeleted: {(_: ChatRoom, eventLog: EventLog) in }, onEphemeralMessageDeleted: {(_: ChatRoom, eventLog: EventLog) in
self.removeMessage(eventLog) 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) self.chatRoomDelegateHolder = ChatRoomDelegateHolder(chatroom: chatRoom, delegate: chatRoomDelegate)
} }
@ -544,6 +665,8 @@ class ConversationViewModel: ObservableObject {
id: UUID().uuidString, id: UUID().uuidString,
status: nil, status: nil,
isOutgoing: false, isOutgoing: false,
isEditable: false,
isEdited: false,
dateReceived: 0, dateReceived: 0,
address: "", address: "",
isFirstMessage: false, isFirstMessage: false,
@ -722,6 +845,8 @@ class ConversationViewModel: ObservableObject {
isFirstMessage: false, isFirstMessage: false,
text: contentReplyText, text: contentReplyText,
isOutgoing: false, isOutgoing: false,
isEditable: false,
isEdited: false,
dateReceived: 0, dateReceived: 0,
attachmentsNames: attachmentNameReplyList, attachmentsNames: attachmentNameReplyList,
attachments: [] attachments: []
@ -735,6 +860,8 @@ class ConversationViewModel: ObservableObject {
id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString, id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString,
status: statusTmp, status: statusTmp,
isOutgoing: chatMessage.isOutgoing, isOutgoing: chatMessage.isOutgoing,
isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false,
isEdited: chatMessage.isEdited,
dateReceived: chatMessage.time, dateReceived: chatMessage.time,
address: addressCleaned?.asStringUriOnly() ?? "", address: addressCleaned?.asStringUriOnly() ?? "",
isFirstMessage: isFirstMessageTmp, isFirstMessage: isFirstMessageTmp,
@ -788,6 +915,8 @@ class ConversationViewModel: ObservableObject {
id: UUID().uuidString, id: UUID().uuidString,
status: nil, status: nil,
isOutgoing: false, isOutgoing: false,
isEditable: false,
isEdited: false,
dateReceived: 0, dateReceived: 0,
address: "", address: "",
isFirstMessage: false, isFirstMessage: false,
@ -965,6 +1094,8 @@ class ConversationViewModel: ObservableObject {
isFirstMessage: false, isFirstMessage: false,
text: contentReplyText, text: contentReplyText,
isOutgoing: false, isOutgoing: false,
isEditable: false,
isEdited: false,
dateReceived: 0, dateReceived: 0,
attachmentsNames: attachmentNameReplyList, attachmentsNames: attachmentNameReplyList,
attachments: [] attachments: []
@ -978,6 +1109,8 @@ class ConversationViewModel: ObservableObject {
id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString, id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString,
status: statusTmp, status: statusTmp,
isOutgoing: chatMessage.isOutgoing, isOutgoing: chatMessage.isOutgoing,
isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false,
isEdited: chatMessage.isEdited,
dateReceived: chatMessage.time, dateReceived: chatMessage.time,
address: addressCleaned?.asStringUriOnly() ?? "", address: addressCleaned?.asStringUriOnly() ?? "",
isFirstMessage: isFirstMessageTmp, isFirstMessage: isFirstMessageTmp,
@ -1048,6 +1181,8 @@ class ConversationViewModel: ObservableObject {
id: UUID().uuidString, id: UUID().uuidString,
status: nil, status: nil,
isOutgoing: false, isOutgoing: false,
isEditable: false,
isEdited: false,
dateReceived: 0, dateReceived: 0,
address: "", address: "",
isFirstMessage: false, isFirstMessage: false,
@ -1239,6 +1374,8 @@ class ConversationViewModel: ObservableObject {
isFirstMessage: false, isFirstMessage: false,
text: contentReplyText, text: contentReplyText,
isOutgoing: false, isOutgoing: false,
isEditable: false,
isEdited: false,
dateReceived: 0, dateReceived: 0,
attachmentsNames: attachmentNameReplyList, attachmentsNames: attachmentNameReplyList,
attachments: [] attachments: []
@ -1253,6 +1390,8 @@ class ConversationViewModel: ObservableObject {
appData: chatMessage.appdata ?? "", appData: chatMessage.appdata ?? "",
status: statusTmp, status: statusTmp,
isOutgoing: chatMessage.isOutgoing, isOutgoing: chatMessage.isOutgoing,
isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false,
isEdited: chatMessage.isEdited,
dateReceived: chatMessage.time, dateReceived: chatMessage.time,
address: addressCleaned != nil ? addressCleaned!.asStringUriOnly() : "", address: addressCleaned != nil ? addressCleaned!.asStringUriOnly() : "",
isFirstMessage: isFirstMessageTmp, isFirstMessage: isFirstMessageTmp,
@ -1471,6 +1610,8 @@ class ConversationViewModel: ObservableObject {
isFirstMessage: false, isFirstMessage: false,
text: contentReplyText, text: contentReplyText,
isOutgoing: false, isOutgoing: false,
isEditable: false,
isEdited: false,
dateReceived: 0, dateReceived: 0,
attachmentsNames: attachmentNameReplyList, attachmentsNames: attachmentNameReplyList,
attachments: [] attachments: []
@ -1485,6 +1626,8 @@ class ConversationViewModel: ObservableObject {
appData: chatMessage.appdata ?? "", appData: chatMessage.appdata ?? "",
status: statusTmp, status: statusTmp,
isOutgoing: chatMessage.isOutgoing, isOutgoing: chatMessage.isOutgoing,
isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false,
isEdited: chatMessage.isEdited,
dateReceived: chatMessage.time, dateReceived: chatMessage.time,
address: addressCleaned != nil ? addressCleaned!.asStringUriOnly() : "", address: addressCleaned != nil ? addressCleaned!.asStringUriOnly() : "",
isFirstMessage: isFirstMessageTmp, isFirstMessage: isFirstMessageTmp,
@ -1526,6 +1669,8 @@ class ConversationViewModel: ObservableObject {
id: UUID().uuidString, id: UUID().uuidString,
status: nil, status: nil,
isOutgoing: false, isOutgoing: false,
isEditable: false,
isEdited: false,
dateReceived: 0, dateReceived: 0,
address: "", address: "",
isFirstMessage: false, isFirstMessage: false,
@ -1553,6 +1698,9 @@ class ConversationViewModel: ObservableObject {
} }
func replyToMessage(index: Int, isMessageTextFocused: Binding<Bool>) { func replyToMessage(index: Int, isMessageTextFocused: Binding<Bool>) {
if self.messageToEdit != nil {
self.messageToEdit = nil
}
coreContext.doOnCoreQueue { _ in coreContext.doOnCoreQueue { _ in
let messageToReplyTmp = self.conversationMessagesSection[0].rows[index] let messageToReplyTmp = self.conversationMessagesSection[0].rows[index]
DispatchQueue.main.async { DispatchQueue.main.async {
@ -1564,6 +1712,21 @@ class ConversationViewModel: ObservableObject {
} }
} }
func editMessage(chatMessage: EventLogMessage, isMessageTextFocused: Binding<Bool>) {
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) { func resendMessage(chatMessage: EventLogMessage) {
coreContext.doOnCoreQueue { _ in coreContext.doOnCoreQueue { _ in
if let message = chatMessage.eventModel.eventLog.chatMessage { if let message = chatMessage.eventModel.eventLog.chatMessage {
@ -1619,6 +1782,8 @@ class ConversationViewModel: ObservableObject {
id: UUID().uuidString, id: UUID().uuidString,
status: nil, status: nil,
isOutgoing: false, isOutgoing: false,
isEditable: false,
isEdited: false,
dateReceived: 0, dateReceived: 0,
address: "", address: "",
isFirstMessage: false, isFirstMessage: false,
@ -1796,6 +1961,8 @@ class ConversationViewModel: ObservableObject {
isFirstMessage: false, isFirstMessage: false,
text: contentReplyText, text: contentReplyText,
isOutgoing: false, isOutgoing: false,
isEditable: false,
isEdited: false,
dateReceived: 0, dateReceived: 0,
attachmentsNames: attachmentNameReplyList, attachmentsNames: attachmentNameReplyList,
attachments: [] attachments: []
@ -1809,6 +1976,8 @@ class ConversationViewModel: ObservableObject {
id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString, id: !chatMessage.messageId.isEmpty ? chatMessage.messageId : UUID().uuidString,
status: statusTmp, status: statusTmp,
isOutgoing: chatMessage.isOutgoing, isOutgoing: chatMessage.isOutgoing,
isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false,
isEdited: chatMessage.isEdited,
dateReceived: chatMessage.time, dateReceived: chatMessage.time,
address: addressCleaned?.asStringUriOnly() ?? "", address: addressCleaned?.asStringUriOnly() ?? "",
isFirstMessage: isFirstMessageTmp, isFirstMessage: isFirstMessageTmp,
@ -1873,6 +2042,8 @@ class ConversationViewModel: ObservableObject {
if chatMessageToReply != nil { if chatMessageToReply != nil {
message = try self.sharedMainViewModel.displayedConversation!.chatRoom.createReplyMessage(message: chatMessageToReply!) 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 { } else {
message = try self.sharedMainViewModel.displayedConversation!.chatRoom.createEmptyMessage() message = try self.sharedMainViewModel.displayedConversation!.chatRoom.createEmptyMessage()
} }
@ -1948,12 +2119,14 @@ class ConversationViewModel: ObservableObject {
if message != nil && !message!.contents.isEmpty { if message != nil && !message!.contents.isEmpty {
Log.info("[ConversationViewModel] Sending message") Log.info("[ConversationViewModel] Sending message")
message!.send() message!.send()
self.sharedMainViewModel.displayedConversation!.chatRoom.stopComposing()
} }
Log.info("[ConversationViewModel] Message sent, re-setting defaults") Log.info("[ConversationViewModel] Message sent, re-setting defaults")
DispatchQueue.main.async { DispatchQueue.main.async {
self.messageToReply = nil self.messageToReply = nil
self.messageToEdit = nil
withAnimation { withAnimation {
self.mediasToSend.removeAll() self.mediasToSend.removeAll()
} }