diff --git a/Linphone/Localizable/cs.lproj/Localizable.strings b/Linphone/Localizable/cs.lproj/Localizable.strings index 631ccadd3..7f6df198e 100644 --- a/Linphone/Localizable/cs.lproj/Localizable.strings +++ b/Linphone/Localizable/cs.lproj/Localizable.strings @@ -474,9 +474,9 @@ "message_copied_to_clipboard_toast" = "Zpráva zkopírována do schránky"; "message_delivery_info_read_title" = "Přečteno"; "message_delivery_info_received_title" = "Přijato"; -"message_meeting_invitation_cancelled_notification" = "📅 Schůzka byla zrušena"; -"message_meeting_invitation_notification" = "📅 Jste pozváni na schůzku"; -"message_meeting_invitation_updated_notification" = "📅 Schůzka byla aktualizována"; +"message_meeting_invitation_cancelled_notification" = "Schůzka byla zrušena"; +"message_meeting_invitation_notification" = "Jste pozváni na schůzku"; +"message_meeting_invitation_updated_notification" = "Schůzka byla aktualizována"; "message_reactions_info_all_title" = "Reakce"; "network_reachable_again" = "Síť je znovu dostupná"; "menu_block_number" = "Blokovat číslo"; diff --git a/Linphone/Localizable/de.lproj/Localizable.strings b/Linphone/Localizable/de.lproj/Localizable.strings index 8d4ff7de9..158351512 100644 --- a/Linphone/Localizable/de.lproj/Localizable.strings +++ b/Linphone/Localizable/de.lproj/Localizable.strings @@ -467,9 +467,9 @@ "message_delivery_info_read_title" = "Gelesen"; "message_delivery_info_received_title" = "Empfangen"; "message_delivery_info_sent_title" = "Gesendet"; -"message_meeting_invitation_cancelled_notification" = "📅 Die Besprechung wurde abgesagt"; -"message_meeting_invitation_notification" = "📅 Sie sind zu einer Besprechung eingeladen"; -"message_meeting_invitation_updated_notification" = "📅 Besprechung wurde aktualisiert"; +"message_meeting_invitation_cancelled_notification" = "Die Besprechung wurde abgesagt"; +"message_meeting_invitation_notification" = "Sie sind zu einer Besprechung eingeladen"; +"message_meeting_invitation_updated_notification" = "Besprechung wurde aktualisiert"; "message_reactions_info_all_title" = "Reaktionen"; "network_reachable_again" = "Netzwerk ist nun wieder erreichbar"; "None" = "Kein"; diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index d7050448e..cf1b49e09 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -206,6 +206,11 @@ "conversation_dialog_subject_hint" = "Conversation subject"; "conversation_editing_message_title" = "Message being edited"; "conversation_message_edited_label" = "Edited"; +"conversation_dialog_delete_chat_message_title" = "Delete this message?"; +"conversation_dialog_delete_locally_label" = "For me"; +"conversation_dialog_delete_for_everyone_label" = "For everyone"; +"conversation_message_content_deleted_label" = "This message has been deleted"; +"conversation_message_content_deleted_by_us_label" = "You have deleted this message"; "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."; @@ -407,9 +412,9 @@ "message_delivery_info_received_title" = "Received"; "message_delivery_info_sent_title" = "Sent"; "message_forwarded_label" = "Forwarded"; -"message_meeting_invitation_cancelled_notification" = "📅 Meeting has been cancelled"; -"message_meeting_invitation_notification" = "📅 You are invited to a meeting"; -"message_meeting_invitation_updated_notification" = "📅 Meeting has been updated"; +"message_meeting_invitation_cancelled_notification" = "Meeting has been cancelled"; +"message_meeting_invitation_notification" = "You are invited to a meeting"; +"message_meeting_invitation_updated_notification" = "Meeting has been updated"; "message_reaction_click_to_remove_label" = "Click to remove"; "message_reactions_info_all_title" = "Reactions"; "network_not_reachable" = "You aren't connected to internet"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index 5f09441a0..2d10e1209 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -206,6 +206,11 @@ "conversation_dialog_subject_hint" = "Nom de la conversation"; "conversation_editing_message_title" = "Modification du message"; "conversation_message_edited_label" = "Modifié"; +"conversation_dialog_delete_chat_message_title" = "Supprimer le message ?"; +"conversation_dialog_delete_locally_label" = "Pour moi"; +"conversation_dialog_delete_for_everyone_label" = "Pour tout le monde"; +"conversation_message_content_deleted_label" = "*Le message a été supprimé*"; +"conversation_message_content_deleted_by_us_label" = "*Vous avez supprimé le message*"; "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."; @@ -407,9 +412,9 @@ "message_delivery_info_received_title" = "Reçu"; "message_delivery_info_sent_title" = "Envoyé"; "message_forwarded_label" = "Transféré"; -"message_meeting_invitation_cancelled_notification" = "📅 Réunion annulée"; -"message_meeting_invitation_notification" = "📅 Invitation à une réunion"; -"message_meeting_invitation_updated_notification" = "📅 Réunion mise à jour"; +"message_meeting_invitation_cancelled_notification" = "Réunion annulée"; +"message_meeting_invitation_notification" = "Invitation à une réunion"; +"message_meeting_invitation_updated_notification" = "Réunion mise à jour"; "message_reaction_click_to_remove_label" = "Cliquez pour supprimer"; "message_reactions_info_all_title" = "Réactions"; "network_not_reachable" = "Vous n’êtes pas connecté à internet"; diff --git a/Linphone/Localizable/uk.lproj/Localizable.strings b/Linphone/Localizable/uk.lproj/Localizable.strings index 3e7a019ab..0bfb10b18 100644 --- a/Linphone/Localizable/uk.lproj/Localizable.strings +++ b/Linphone/Localizable/uk.lproj/Localizable.strings @@ -456,9 +456,9 @@ "message_delivery_info_read_title" = "Читати"; "message_delivery_info_received_title" = "Отримано"; "message_delivery_info_sent_title" = "Відправлено"; -"message_meeting_invitation_cancelled_notification" = "📅 Нараду скасовано"; -"message_meeting_invitation_notification" = "📅 Вас запрошено на нараду"; -"message_meeting_invitation_updated_notification" = "📅 Нараду оновлено"; +"message_meeting_invitation_cancelled_notification" = "Нараду скасовано"; +"message_meeting_invitation_notification" = "Вас запрошено на нараду"; +"message_meeting_invitation_updated_notification" = "Нараду оновлено"; "message_reactions_info_all_title" = "Реакції"; "network_reachable_again" = "Мережа знову доступна"; "None" = "Жоден"; diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index a2b19818a..b8100eee1 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -60,6 +60,7 @@ struct ContentView: View { @State var isShowDismissPopup = false @State var isShowSendCancelMeetingNotificationPopup = false @State var isShowStartCallGroupPopup = false + @State var isShowDeleteMessagePopup = false @State var isShowSipAddressesPopup = false @State var isShowSipAddressesPopupType = 0 // 0 to call, 1 to message, 2 to video call @State var isShowConversationFragment = false @@ -987,6 +988,7 @@ struct ContentView: View { ConversationFragment( isShowConversationFragment: $isShowConversationFragment, isShowStartCallGroupPopup: $isShowStartCallGroupPopup, + isShowDeleteMessagePopup: $isShowDeleteMessagePopup, isShowEditContactFragment: $isShowEditContactFragment, isShowEditContactFragmentAddress: $isShowEditContactFragmentAddress, isShowScheduleMeetingFragment: $isShowScheduleMeetingFragment, @@ -1354,31 +1356,30 @@ struct ContentView: View { } } - /* - if isShowStartCallGroupPopup { + if isShowDeleteMessagePopup { PopupView( - isShowPopup: $isShowStartCallGroupPopup, - title: Text("conversation_info_confirm_start_group_call_dialog_title"), - content: Text("conversation_info_confirm_start_group_call_dialog_message"), - titleFirstButton: Text("dialog_cancel"), - actionFirstButton: { self.isShowStartCallGroupPopup.toggle() }, - titleSecondButton: Text("dialog_confirm"), + isShowPopup: $isShowDeleteMessagePopup, + title: Text("conversation_dialog_delete_chat_message_title"), + content: nil, + titleFirstButton: Text("conversation_dialog_delete_for_everyone_label"), + actionFirstButton: { + NotificationCenter.default.post(name: NSNotification.Name("DeleteMessageForEveryone"), object: nil) + self.isShowDeleteMessagePopup.toggle() + }, + titleSecondButton: Text("conversation_dialog_delete_locally_label"), actionSecondButton: { - if sharedMainViewModel.displayedConversation != nil { - sharedMainViewModel.displayedConversation!.createGroupCall() - } - self.isShowStartCallGroupPopup.toggle() + NotificationCenter.default.post(name: NSNotification.Name("DeleteMessageForMe"), object: nil) + self.isShowDeleteMessagePopup.toggle() }, titleThirdButton: Text("dialog_cancel"), - actionThirdButton: { self.isShowStartCallGroupPopup.toggle() } + actionThirdButton: { self.isShowDeleteMessagePopup.toggle() } ) .background(.black.opacity(0.65)) .zIndex(3) .onTapGesture { - self.isShowStartCallGroupPopup.toggle() + self.isShowDeleteMessagePopup.toggle() } } - */ if isShowConversationInfoPopup { PopupViewWithTextField( diff --git a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift index c3ab77c7a..5c489df4a 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ChatBubbleView.swift @@ -52,7 +52,7 @@ struct ChatBubbleView: View { HStack { if eventLogMessage.eventModel.eventLogType == .ConferenceChatMessage { VStack { - if !eventLogMessage.message.text.isEmpty || !eventLogMessage.message.attachments.isEmpty || eventLogMessage.message.isIcalendar { + if !eventLogMessage.message.text.isEmpty || !eventLogMessage.message.attachments.isEmpty || eventLogMessage.message.isIcalendar || eventLogMessage.message.isRetracted { HStack(alignment: .top, content: { if eventLogMessage.message.isOutgoing { Spacer() @@ -137,6 +137,12 @@ struct ChatBubbleView: View { .foregroundStyle(Color.grayMain2c700) .default_text_style(styleSize: 14) .lineLimit(/*@START_MENU_TOKEN@*/2/*@END_MENU_TOKEN@*/) + } else if eventLogMessage.message.replyMessage!.isRetracted { + Text(eventLogMessage.message.replyMessage!.isOutgoing ? "conversation_message_content_deleted_by_us_label" : "conversation_message_content_deleted_label") + .italic() + .foregroundStyle(Color.grayMain2c500) + .font(.system(size: 14)) + .lineLimit(1) } } .padding(.all, 15) @@ -174,6 +180,12 @@ struct ChatBubbleView: View { if !eventLogMessage.message.text.isEmpty { DynamicLinkText(text: eventLogMessage.message.text) + } else if eventLogMessage.message.isRetracted { + Text(eventLogMessage.message.isOutgoing ? "conversation_message_content_deleted_by_us_label" : "conversation_message_content_deleted_label") + .italic() + .foregroundStyle(Color.grayMain2c500) + .font(.system(size: 14)) + .lineLimit(1) } if eventLogMessage.message.isIcalendar && eventLogMessage.message.messageConferenceInfo != nil { diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index 51b68ac0f..9bb39c865 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -64,6 +64,7 @@ struct ConversationFragment: View { @Binding var isShowConversationFragment: Bool @Binding var isShowStartCallGroupPopup: Bool + @Binding var isShowDeleteMessagePopup: Bool @State private var selectedCategoryIndex = 0 @@ -1194,28 +1195,30 @@ struct ConversationFragment: View { Divider() } - Button { - let indexMessage = conversationViewModel.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.id == conversationViewModel.selectedMessage!.message.id}) - conversationViewModel.selectedMessage = nil - conversationViewModel.replyToMessage(index: indexMessage ?? 0, isMessageTextFocused: Binding( - get: { isMessageTextFocused }, - set: { isMessageTextFocused = $0 } - )) - } label: { - HStack { - Text("menu_reply_to_chat_message") - .default_text_style(styleSize: 15) - Spacer() - Image("reply") - .resizable() - .frame(width: 20, height: 20, alignment: .leading) + if !conversationViewModel.selectedMessage!.message.isRetracted { + Button { + let indexMessage = conversationViewModel.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.id == conversationViewModel.selectedMessage!.message.id}) + conversationViewModel.selectedMessage = nil + conversationViewModel.replyToMessage(index: indexMessage ?? 0, isMessageTextFocused: Binding( + get: { isMessageTextFocused }, + set: { isMessageTextFocused = $0 } + )) + } label: { + HStack { + Text("menu_reply_to_chat_message") + .default_text_style(styleSize: 15) + Spacer() + Image("reply") + .resizable() + .frame(width: 20, height: 20, alignment: .leading) + } + .padding(.vertical, 5) + .padding(.horizontal, 20) } - .padding(.vertical, 5) - .padding(.horizontal, 20) + + Divider() } - Divider() - if !conversationViewModel.selectedMessage!.message.text.isEmpty { Button { UIPasteboard.general.setValue( @@ -1243,27 +1246,35 @@ struct ConversationFragment: View { Divider() } - Button { - withAnimation { - isShowConversationForwardMessageFragment = true + if !conversationViewModel.selectedMessage!.message.isRetracted { + Button { + withAnimation { + isShowConversationForwardMessageFragment = true + } + } label: { + HStack { + Text("menu_forward_chat_message") + .default_text_style(styleSize: 15) + Spacer() + Image("forward") + .resizable() + .frame(width: 20, height: 20, alignment: .leading) + } + .padding(.vertical, 5) + .padding(.horizontal, 20) } - } label: { - HStack { - Text("menu_forward_chat_message") - .default_text_style(styleSize: 15) - Spacer() - Image("forward") - .resizable() - .frame(width: 20, height: 20, alignment: .leading) - } - .padding(.vertical, 5) - .padding(.horizontal, 20) + + Divider() } - Divider() - Button { - conversationViewModel.deleteMessage() + if conversationViewModel.selectedMessage!.message.isOutgoing + && !(SharedMainViewModel.shared.displayedConversation?.isReadOnly ?? cachedConversation!.isReadOnly) + && conversationViewModel.selectedMessage!.message.isRetractable && !conversationViewModel.selectedMessage!.message.isRetracted { + isShowDeleteMessagePopup = true + } else { + conversationViewModel.deleteMessage() + } } label: { HStack { Text("menu_delete_selected_item") @@ -1316,6 +1327,10 @@ struct ConversationFragment: View { if conversationViewModel.selectedMessage != nil { conversationViewModel.selectedMessage = nil } + }.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("DeleteMessageForMe"))) { _ in + conversationViewModel.deleteMessage() + }.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("DeleteMessageForEveryone"))) { _ in + conversationViewModel.deleteMessageForEveryone() } } diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift index e9c40e871..40bde0799 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift @@ -105,14 +105,26 @@ struct ConversationRow: View { .frame(maxWidth: .infinity, alignment: .leading) .lineLimit(1) - Text(conversation.lastMessageText) - .foregroundStyle(Color.grayMain2c400) - .if(conversation.unreadMessagesCount > 0) { view in - view.default_text_style_700(styleSize: 14) - } - .default_text_style(styleSize: 14) - .frame(maxWidth: .infinity, alignment: .leading) - .lineLimit(1) + if conversation.lastMessageInItalic { + Text(conversation.lastMessageText) + .italic() + .if(conversation.unreadMessagesCount > 0) { view in + view.bold() + } + .foregroundStyle(Color.grayMain2c400) + .font(.system(size: 14)) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else { + Text(conversation.lastMessageText) + .foregroundStyle(Color.grayMain2c400) + .if(conversation.unreadMessagesCount > 0) { view in + view.default_text_style_700(styleSize: 14) + } + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } Spacer() } diff --git a/Linphone/UI/Main/Conversations/Fragments/UIList.swift b/Linphone/UI/Main/Conversations/Fragments/UIList.swift index 59622cec2..2b116e379 100644 --- a/Linphone/UI/Main/Conversations/Fragments/UIList.swift +++ b/Linphone/UI/Main/Conversations/Fragments/UIList.swift @@ -551,6 +551,12 @@ struct UIList: UIViewRepresentable { } func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + let eventLogMessage = parent.conversationViewModel.conversationMessagesSection[0].rows[indexPath.row] + + guard !eventLogMessage.message.isRetracted && eventLogMessage.eventModel.eventLogType == .ConferenceChatMessage else { + return nil + } + let archiveAction = UIContextualAction(style: .normal, title: "") { _, _, completionHandler in self.parent.conversationViewModel.replyToMessage(index: indexPath.row, isMessageTextFocused: Binding( get: { self.parent.isMessageTextFocused }, diff --git a/Linphone/UI/Main/Conversations/Model/ConversationModel.swift b/Linphone/UI/Main/Conversations/Model/ConversationModel.swift index 399fbede4..a878debe0 100644 --- a/Linphone/UI/Main/Conversations/Model/ConversationModel.swift +++ b/Linphone/UI/Main/Conversations/Model/ConversationModel.swift @@ -49,6 +49,7 @@ class ConversationModel: ObservableObject, Identifiable { @Published var lastMessageText: String @Published var lastMessageIsOutgoing: Bool @Published var lastMessageState: Int + @Published var lastMessageInItalic: Bool @Published var unreadMessagesCount: Int @Published var avatarModel: ContactAvatarModel @@ -143,6 +144,8 @@ class ConversationModel: ObservableObject, Identifiable { self.lastMessageIsOutgoing = false self.lastMessageState = 0 + + self.lastMessageInItalic = false self.unreadMessagesCount = chatRoom.unreadMessagesCount @@ -297,6 +300,8 @@ class ConversationModel: ObservableObject, Identifiable { var lastMessageTextTmp = (fromAddressFriend ?? "") + (lastMessage!.contents.first(where: {$0.isText == true})?.utf8Text ?? (lastMessage!.contents.first(where: {$0.isFile == true || $0.isFileTransfer == true})?.name ?? "")) + var lastMessageInItalicTmp = false + if lastMessage!.contents.first != nil && lastMessage!.contents.first!.isIcalendar == true { if let conferenceInfo = try? Factory.Instance.createConferenceInfoFromIcalendarContent(content: lastMessage!.contents.first!) { if conferenceInfo.uri != nil { @@ -308,10 +313,18 @@ class ConversationModel: ObservableObject, Identifiable { } else if conferenceInfo.state == .Cancelled { lastMessageTextTmp = String(localized: "message_meeting_invitation_cancelled_notification") } + + lastMessageInItalicTmp = true } } } + if lastMessage!.isRetracted { + lastMessageTextTmp += lastMessage!.isOutgoing ? String(localized: "conversation_message_content_deleted_by_us_label") : String(localized: "conversation_message_content_deleted_label") + + lastMessageInItalicTmp = true + } + let lastMessageIsOutgoingTmp = lastMessage?.isOutgoing ?? false let lastUpdateTimeTmp = lastMessage?.time ?? chatRoom.lastUpdateTime @@ -326,6 +339,8 @@ class ConversationModel: ObservableObject, Identifiable { self.lastUpdateTime = lastUpdateTimeTmp self.lastMessageState = lastMessageStateTmp + + self.lastMessageInItalic = lastMessageInItalicTmp } getUnreadMessagesCount() diff --git a/Linphone/UI/Main/Conversations/Model/Message.swift b/Linphone/UI/Main/Conversations/Model/Message.swift index 6a4a4089c..c0096e42e 100644 --- a/Linphone/UI/Main/Conversations/Model/Message.swift +++ b/Linphone/UI/Main/Conversations/Model/Message.swift @@ -69,7 +69,9 @@ public struct Message: Identifiable, Hashable { public var createdAt: Date public var isOutgoing: Bool public var isEditable: Bool + public var isRetractable: Bool public var isEdited: Bool + public var isRetracted: Bool public var dateReceived: time_t public var address: String @@ -97,7 +99,9 @@ public struct Message: Identifiable, Hashable { createdAt: Date = Date(), isOutgoing: Bool, isEditable: Bool, + isRetractable: Bool, isEdited: Bool, + isRetracted: Bool, dateReceived: time_t, address: String, isFirstMessage: Bool = false, @@ -121,7 +125,9 @@ public struct Message: Identifiable, Hashable { self.createdAt = createdAt self.isOutgoing = isOutgoing self.isEditable = isEditable + self.isRetractable = isRetractable self.isEdited = isEdited + self.isRetracted = isRetracted self.dateReceived = dateReceived self.isFirstMessage = isFirstMessage self.address = address @@ -170,7 +176,9 @@ public struct Message: Identifiable, Hashable { createdAt: draft.createdAt, isOutgoing: draft.isOutgoing, isEditable: draft.isEditable, + isRetractable: draft.isRetractable, isEdited: draft.isEdited, + isRetracted: draft.isRetracted, dateReceived: draft.dateReceived, address: draft.address, isFirstMessage: draft.isFirstMessage, @@ -192,7 +200,7 @@ extension Message { extension Message: Equatable { public static func == (lhs: Message, rhs: Message) -> Bool { - 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 + lhs.id == rhs.id && lhs.status == rhs.status && lhs.isEdited == rhs.isEdited && lhs.isRetracted == rhs.isRetracted && lhs.isFirstMessage == rhs.isFirstMessage && lhs.text == rhs.text && lhs.attachments == rhs.attachments && lhs.replyMessage?.text == rhs.replyMessage?.text && lhs.replyMessage?.isRetracted == rhs.replyMessage?.isRetracted && lhs.ownReaction == rhs.ownReaction && lhs.reactions == rhs.reactions && lhs.ephemeralExpireTime == rhs.ephemeralExpireTime } } @@ -220,7 +228,9 @@ public struct ReplyMessage: Codable, Identifiable, Hashable { public var text: String public var isOutgoing: Bool public var isEditable: Bool + public var isRetractable: Bool public var isEdited: Bool + public var isRetracted: Bool public var dateReceived: time_t public var attachmentsNames: String public var attachments: [Attachment] @@ -232,7 +242,9 @@ public struct ReplyMessage: Codable, Identifiable, Hashable { text: String = "", isOutgoing: Bool, isEditable: Bool, + isRetractable: Bool, isEdited: Bool, + isRetracted: Bool, dateReceived: time_t, attachmentsNames: String = "", attachments: [Attachment] = [], @@ -244,7 +256,9 @@ public struct ReplyMessage: Codable, Identifiable, Hashable { self.text = text self.isOutgoing = isOutgoing self.isEditable = isEditable + self.isRetractable = isRetractable self.isEdited = isEdited + self.isRetracted = isRetracted self.dateReceived = dateReceived self.attachmentsNames = attachmentsNames self.attachments = attachments @@ -252,14 +266,14 @@ public struct ReplyMessage: Codable, Identifiable, Hashable { } func toMessage() -> Message { - Message(id: id, isOutgoing: isOutgoing, isEditable: isEditable, isEdited: isEdited, dateReceived: dateReceived, address: address, isFirstMessage: isFirstMessage, text: text, attachments: attachments, recording: recording) + Message(id: id, isOutgoing: isOutgoing, isEditable: isEditable, isRetractable: isRetractable, isEdited: isEdited, isRetracted: isRetracted, 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, isEditable: isEditable, isEdited: isEdited, dateReceived: dateReceived, attachments: attachments, recording: recording) + ReplyMessage(id: id, address: address, isFirstMessage: isFirstMessage, text: text, isOutgoing: isOutgoing, isEditable: isEditable, isRetractable: isRetractable, isEdited: isEdited, isRetracted: isRetracted, dateReceived: dateReceived, attachments: attachments, recording: recording) } } @@ -267,7 +281,9 @@ public struct DraftMessage { public var id: String? public let isOutgoing: Bool public let isEditable: Bool + public let isRetractable: Bool public let isEdited: Bool + public let isRetracted: Bool public var dateReceived: time_t public let address: String public let isFirstMessage: Bool @@ -282,7 +298,9 @@ public struct DraftMessage { public init(id: String? = nil, isOutgoing: Bool, isEditable: Bool, + isRetractable: Bool, isEdited: Bool, + isRetracted: Bool, dateReceived: time_t, address: String, isFirstMessage: Bool, @@ -297,7 +315,9 @@ public struct DraftMessage { self.id = id self.isOutgoing = isOutgoing self.isEditable = isEditable + self.isRetractable = isRetractable self.isEdited = isEdited + self.isRetracted = isRetracted 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 5064cdf09..88e9df3d5 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -175,6 +175,10 @@ class ConversationViewModel: ObservableObject { }, onMessageContentEdited: {(chatRoom: ChatRoom, message: ChatMessage) in let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.eventModel.eventLogId == message.messageId}) + if let displayedConversation = self.sharedMainViewModel.displayedConversation { + displayedConversation.getContentTextMessage(chatRoom: displayedConversation.chatRoom) + } + var attachmentNameList: String = "" var attachmentList: [Attachment] = [] var contentText = "" @@ -290,7 +294,28 @@ class ConversationViewModel: ObservableObject { } } }, onMessageRetracted: {(chatRoom: ChatRoom, message: ChatMessage) in - // TODO + let indexMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.eventModel.eventLogId == message.messageId}) + let indexReplyMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.replyMessage?.id == message.messageId}) + + if let displayedConversation = self.sharedMainViewModel.displayedConversation { + displayedConversation.getContentTextMessage(chatRoom: displayedConversation.chatRoom) + } + + DispatchQueue.main.async { + if indexMessage != nil { + self.conversationMessagesSection[0].rows[indexMessage!].message.text = "" + self.conversationMessagesSection[0].rows[indexMessage!].message.isRetracted = true + self.conversationMessagesSection[0].rows[indexMessage!].message.attachments = [] + self.conversationMessagesSection[0].rows[indexMessage!].message.attachmentsNames = "" + } + + if indexReplyMessage != nil { + self.conversationMessagesSection[0].rows[indexReplyMessage!].message.replyMessage?.text = "" + self.conversationMessagesSection[0].rows[indexReplyMessage!].message.replyMessage?.isRetracted = true + self.conversationMessagesSection[0].rows[indexReplyMessage!].message.replyMessage?.attachments = [] + self.conversationMessagesSection[0].rows[indexReplyMessage!].message.replyMessage?.attachmentsNames = "" + } + } }) self.chatRoomDelegateHolder = ChatRoomDelegateHolder(chatroom: chatRoom, delegate: chatRoomDelegate) @@ -666,7 +691,9 @@ class ConversationViewModel: ObservableObject { status: nil, isOutgoing: false, isEditable: false, + isRetractable: false, isEdited: false, + isRetracted: false, dateReceived: 0, address: "", isFirstMessage: false, @@ -827,6 +854,8 @@ class ConversationViewModel: ObservableObject { let contentReplyText = chatMessage.replyMessage?.utf8Text ?? "" + let isReplyRetracted = chatMessage.replyMessage?.isRetracted ?? false + var attachmentNameReplyList: String = "" chatMessage.replyMessage?.contents.forEach { content in @@ -844,9 +873,11 @@ class ConversationViewModel: ObservableObject { address: addressReplyCleaned?.asStringUriOnly() ?? "", isFirstMessage: false, text: contentReplyText, - isOutgoing: false, + isOutgoing: chatMessage.replyMessage!.isOutgoing, isEditable: false, + isRetractable: false, isEdited: false, + isRetracted: isReplyRetracted, dateReceived: 0, attachmentsNames: attachmentNameReplyList, attachments: [] @@ -861,7 +892,9 @@ class ConversationViewModel: ObservableObject { status: statusTmp, isOutgoing: chatMessage.isOutgoing, isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false, + isRetractable: chatMessage.isOutgoing ? chatMessage.isRetractable : false, isEdited: chatMessage.isEdited, + isRetracted: chatMessage.isRetracted, dateReceived: chatMessage.time, address: addressCleaned?.asStringUriOnly() ?? "", isFirstMessage: isFirstMessageTmp, @@ -916,7 +949,9 @@ class ConversationViewModel: ObservableObject { status: nil, isOutgoing: false, isEditable: false, + isRetractable: false, isEdited: false, + isRetracted: false, dateReceived: 0, address: "", isFirstMessage: false, @@ -1076,6 +1111,8 @@ class ConversationViewModel: ObservableObject { let contentReplyText = chatMessage.replyMessage?.utf8Text ?? "" + let isReplyRetracted = chatMessage.replyMessage?.isRetracted ?? false + var attachmentNameReplyList: String = "" chatMessage.replyMessage?.contents.forEach { content in @@ -1093,9 +1130,11 @@ class ConversationViewModel: ObservableObject { address: addressReplyCleaned?.asStringUriOnly() ?? "", isFirstMessage: false, text: contentReplyText, - isOutgoing: false, + isOutgoing: chatMessage.replyMessage!.isOutgoing, isEditable: false, + isRetractable: false, isEdited: false, + isRetracted: isReplyRetracted, dateReceived: 0, attachmentsNames: attachmentNameReplyList, attachments: [] @@ -1110,7 +1149,9 @@ class ConversationViewModel: ObservableObject { status: statusTmp, isOutgoing: chatMessage.isOutgoing, isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false, + isRetractable: chatMessage.isOutgoing ? chatMessage.isRetractable : false, isEdited: chatMessage.isEdited, + isRetracted: chatMessage.isRetracted, dateReceived: chatMessage.time, address: addressCleaned?.asStringUriOnly() ?? "", isFirstMessage: isFirstMessageTmp, @@ -1182,7 +1223,9 @@ class ConversationViewModel: ObservableObject { status: nil, isOutgoing: false, isEditable: false, + isRetractable: false, isEdited: false, + isRetracted: false, dateReceived: 0, address: "", isFirstMessage: false, @@ -1356,6 +1399,8 @@ class ConversationViewModel: ObservableObject { let contentReplyText = chatMessage.replyMessage?.utf8Text ?? "" + let isReplyRetracted = chatMessage.replyMessage?.isRetracted ?? false + var attachmentNameReplyList: String = "" chatMessage.replyMessage?.contents.forEach { content in @@ -1373,9 +1418,11 @@ class ConversationViewModel: ObservableObject { address: addressReplyCleaned != nil ? addressReplyCleaned!.asStringUriOnly() : "", isFirstMessage: false, text: contentReplyText, - isOutgoing: false, + isOutgoing: chatMessage.replyMessage!.isOutgoing, isEditable: false, + isRetractable: false, isEdited: false, + isRetracted: isReplyRetracted, dateReceived: 0, attachmentsNames: attachmentNameReplyList, attachments: [] @@ -1391,7 +1438,9 @@ class ConversationViewModel: ObservableObject { status: statusTmp, isOutgoing: chatMessage.isOutgoing, isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false, + isRetractable: chatMessage.isOutgoing ? chatMessage.isRetractable : false, isEdited: chatMessage.isEdited, + isRetracted: chatMessage.isRetracted, dateReceived: chatMessage.time, address: addressCleaned != nil ? addressCleaned!.asStringUriOnly() : "", isFirstMessage: isFirstMessageTmp, @@ -1592,6 +1641,8 @@ class ConversationViewModel: ObservableObject { let contentReplyText = chatMessage.replyMessage?.utf8Text ?? "" + let isReplyRetracted = chatMessage.replyMessage?.isRetracted ?? false + var attachmentNameReplyList: String = "" chatMessage.replyMessage?.contents.forEach { content in @@ -1609,9 +1660,11 @@ class ConversationViewModel: ObservableObject { address: addressReplyCleaned != nil ? addressReplyCleaned!.asStringUriOnly() : "", isFirstMessage: false, text: contentReplyText, - isOutgoing: false, + isOutgoing: chatMessage.replyMessage!.isOutgoing, isEditable: false, + isRetractable: false, isEdited: false, + isRetracted: isReplyRetracted, dateReceived: 0, attachmentsNames: attachmentNameReplyList, attachments: [] @@ -1627,7 +1680,9 @@ class ConversationViewModel: ObservableObject { status: statusTmp, isOutgoing: chatMessage.isOutgoing, isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false, + isRetractable: chatMessage.isOutgoing ? chatMessage.isRetractable : false, isEdited: chatMessage.isEdited, + isRetracted: chatMessage.isRetracted, dateReceived: chatMessage.time, address: addressCleaned != nil ? addressCleaned!.asStringUriOnly() : "", isFirstMessage: isFirstMessageTmp, @@ -1670,7 +1725,9 @@ class ConversationViewModel: ObservableObject { status: nil, isOutgoing: false, isEditable: false, + isRetractable: false, isEdited: false, + isRetracted: false, dateReceived: 0, address: "", isFirstMessage: false, @@ -1783,7 +1840,9 @@ class ConversationViewModel: ObservableObject { status: nil, isOutgoing: false, isEditable: false, + isRetractable: false, isEdited: false, + isRetracted: false, dateReceived: 0, address: "", isFirstMessage: false, @@ -1943,6 +2002,8 @@ class ConversationViewModel: ObservableObject { let contentReplyText = chatMessage.replyMessage?.utf8Text ?? "" + let isReplyRetracted = chatMessage.replyMessage?.isRetracted ?? false + var attachmentNameReplyList: String = "" chatMessage.replyMessage?.contents.forEach { content in @@ -1960,9 +2021,11 @@ class ConversationViewModel: ObservableObject { address: addressReplyCleaned?.asStringUriOnly() ?? "", isFirstMessage: false, text: contentReplyText, - isOutgoing: false, + isOutgoing: chatMessage.replyMessage!.isOutgoing, isEditable: false, + isRetractable: false, isEdited: false, + isRetracted: isReplyRetracted, dateReceived: 0, attachmentsNames: attachmentNameReplyList, attachments: [] @@ -1977,7 +2040,9 @@ class ConversationViewModel: ObservableObject { status: statusTmp, isOutgoing: chatMessage.isOutgoing, isEditable: chatMessage.isOutgoing ? chatMessage.isEditable : false, + isRetractable: chatMessage.isOutgoing ? chatMessage.isRetractable : false, isEdited: chatMessage.isEdited, + isRetracted: chatMessage.isRetracted, dateReceived: chatMessage.time, address: addressCleaned?.asStringUriOnly() ?? "", isFirstMessage: isFirstMessageTmp, @@ -2845,17 +2910,39 @@ class ConversationViewModel: ObservableObject { if let displayedConversation = self.sharedMainViewModel.displayedConversation, let selectedMessage = self.selectedMessage, let chatMessage = selectedMessage.eventModel.eventLog.chatMessage { + + let indexReplyMessage = self.conversationMessagesSection[0].rows.firstIndex(where: {$0.message.replyMessage?.id == chatMessage.messageId}) displayedConversation.chatRoom.deleteMessage(message: chatMessage) + + displayedConversation.getContentTextMessage(chatRoom: displayedConversation.chatRoom) + DispatchQueue.main.async { if let sectionIndex = self.conversationMessagesSection.firstIndex(where: { $0.chatRoomID == displayedConversation.id }), let rowIndex = self.conversationMessagesSection[sectionIndex].rows.firstIndex(of: selectedMessage) { self.conversationMessagesSection[sectionIndex].rows.remove(at: rowIndex) + + if indexReplyMessage != nil { + self.conversationMessagesSection[0].rows[indexReplyMessage!].message.replyMessage = nil + } } self.selectedMessage = nil } } } } + + func deleteMessageForEveryone(){ + coreContext.doOnCoreQueue { _ in + if let displayedConversation = self.sharedMainViewModel.displayedConversation, + let selectedMessage = self.selectedMessage, + let chatMessage = selectedMessage.eventModel.eventLog.chatMessage { + displayedConversation.chatRoom.retractMessage(message: chatMessage) + DispatchQueue.main.async { + self.selectedMessage = nil + } + } + } + } } // swiftlint:enable line_length // swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift index f6c6b5bad..6b42a64d8 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift @@ -89,7 +89,11 @@ class ConversationsListViewModel: ObservableObject { fromAddressFriend = nil } - let lastMessageTextTmp = (fromAddressFriend ?? "") + (lastMessage.contents.first(where: { $0.isText })?.utf8Text ?? (lastMessage.contents.first(where: { $0.isFile || $0.isFileTransfer })?.name ?? "")) + var lastMessageTextTmp = (fromAddressFriend ?? "") + (lastMessage.contents.first(where: { $0.isText })?.utf8Text ?? (lastMessage.contents.first(where: { $0.isFile || $0.isFileTransfer })?.name ?? "")) + + if lastMessage.isRetracted { + lastMessageTextTmp += lastMessage.isOutgoing ? String(localized: "conversation_message_content_deleted_by_us_label") : String(localized: "conversation_message_content_deleted_label") + } if let index = self.conversationsList.firstIndex(where: { $0.chatRoom === conversationModel.chatRoom }) { DispatchQueue.main.async { @@ -148,7 +152,11 @@ class ConversationsListViewModel: ObservableObject { fromAddressFriend = nil } - let lastMessageTextTmp = (fromAddressFriend ?? "") + (lastMessage.contents.first(where: { $0.isText })?.utf8Text ?? (lastMessage.contents.first(where: { $0.isFile || $0.isFileTransfer })?.name ?? "")) + var lastMessageTextTmp = (fromAddressFriend ?? "") + (lastMessage.contents.first(where: { $0.isText })?.utf8Text ?? (lastMessage.contents.first(where: { $0.isFile || $0.isFileTransfer })?.name ?? "")) + + if lastMessage.isRetracted { + lastMessageTextTmp += lastMessage.isOutgoing ? String(localized: "conversation_message_content_deleted_by_us_label") : String(localized: "conversation_message_content_deleted_label") + } if let index = self.conversationsList.firstIndex(where: { $0.chatRoom === conversationModel.chatRoom }) { DispatchQueue.main.async {