From 4fbb43f38c11813ee921d1296b5b0283187c0486 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 17 Nov 2025 13:44:59 +0100 Subject: [PATCH 01/13] Fix SIP contacts filter --- .../Localizable/en.lproj/Localizable.strings | 1 + .../Localizable/fr.lproj/Localizable.strings | 1 + .../Fragments/ContactsInnerFragment.swift | 143 ++++++++++-------- Linphone/UI/Main/ContentView.swift | 2 +- .../ConversationForwardMessageFragment.swift | 84 +++++----- .../Fragments/StartConversationFragment.swift | 56 ++++--- .../History/Fragments/StartCallFragment.swift | 138 +++++++++-------- .../Fragments/AddParticipantsFragment.swift | 122 ++++++++------- Linphone/Utils/MagicSearchSingleton.swift | 12 +- 9 files changed, 305 insertions(+), 254 deletions(-) diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index 78479300d..a8f3001d7 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -188,6 +188,7 @@ "contacts_list_favourites_title" = "Favourites"; "contacts_list_filter_popup_see_all" = "See all"; "contacts_list_filter_popup_see_linphone_only" = "See %@ contacts"; +"contacts_list_filter_popup_see_sip_only" = "See SIP contacts"; "conversation_action_call" = "Call"; "conversation_action_configure_ephemeral_messages" = "Configure ephemeral messages"; "conversation_action_delete" = "Delete conversation"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index 58961a458..9d05cf509 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -188,6 +188,7 @@ "contacts_list_favourites_title" = "Favoris"; "contacts_list_filter_popup_see_all" = "Tous les contacts"; "contacts_list_filter_popup_see_linphone_only" = "Contacts %@"; +"contacts_list_filter_popup_see_sip_only" = "Contacts SIP"; "conversation_action_call" = "Appeler"; "conversation_action_configure_ephemeral_messages" = "Configurer les messages éphémères"; "conversation_action_delete" = "Supprimer la conversation"; diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift index 8d6c362d9..dc52c0c62 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift @@ -24,6 +24,7 @@ struct ContactsInnerFragment: View { @ObservedObject var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject var contactsManager = ContactsManager.shared + @ObservedObject var magicSearch = MagicSearchSingleton.shared @EnvironmentObject var contactsListViewModel: ContactsListViewModel @@ -33,76 +34,84 @@ struct ContactsInnerFragment: View { @Binding var text: String var body: some View { - VStack(alignment: .leading) { - if contactsManager.avatarListModel.contains(where: { $0.starred }) { - HStack(alignment: .center) { - Text("contacts_list_favourites_title") - .default_text_style_800(styleSize: 16) - - Spacer() - - Image(isFavoriteOpen ? "caret-up" : "caret-down") - .renderingMode(.template) - .resizable() - .foregroundStyle(Color.grayMain2c600) - .frame(width: 25, height: 25, alignment: .leading) - .padding(.all, 10) - } - .padding(.top, 10) - .padding(.horizontal, 16) - .background(.white) - .onTapGesture { - withAnimation { - isFavoriteOpen.toggle() + ZStack { + VStack(alignment: .leading) { + if contactsManager.avatarListModel.contains(where: { $0.starred }) { + HStack(alignment: .center) { + Text("contacts_list_favourites_title") + .default_text_style_800(styleSize: 16) + + Spacer() + + Image(isFavoriteOpen ? "caret-up" : "caret-down") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.all, 10) } - } - - if isFavoriteOpen { - FavoriteContactsListFragment(showingSheet: $showingSheet) - .zIndex(-1) - .transition(.move(edge: .top)) - } - - HStack(alignment: .center) { - Text("contacts_list_all_contacts_title") - .default_text_style_800(styleSize: 16) - - Spacer() - } - .padding(.top, 10) - .padding(.horizontal, 16) - } - - VStack { - List { - ContactsListFragment(showingSheet: $showingSheet, startCallFunc: {_ in })} - .safeAreaInset(edge: .top, content: { - Spacer() - .frame(height: 12) - }) - .listStyle(.plain) - .if(sharedMainViewModel.cardDavFriendsListsCount > 0) { view in - view.refreshable { - contactsManager.refreshCardDavContacts() - } - } - .overlay( - VStack { - if contactsManager.avatarListModel.isEmpty { - Spacer() - Image("illus-belledonne") - .resizable() - .scaledToFit() - .clipped() - .padding(.all) - Text(!text.isEmpty ? "list_filter_no_result_found" : "contacts_list_empty") - .default_text_style_800(styleSize: 16) - Spacer() - Spacer() + .padding(.top, 10) + .padding(.horizontal, 16) + .background(.white) + .onTapGesture { + withAnimation { + isFavoriteOpen.toggle() } } - .padding(.all) - ) + + if isFavoriteOpen { + FavoriteContactsListFragment(showingSheet: $showingSheet) + .zIndex(-1) + .transition(.move(edge: .top)) + } + + HStack(alignment: .center) { + Text("contacts_list_all_contacts_title") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.top, 10) + .padding(.horizontal, 16) + } + + VStack { + List { + ContactsListFragment(showingSheet: $showingSheet, startCallFunc: {_ in })} + .safeAreaInset(edge: .top, content: { + Spacer() + .frame(height: 12) + }) + .listStyle(.plain) + .if(sharedMainViewModel.cardDavFriendsListsCount > 0) { view in + view.refreshable { + contactsManager.refreshCardDavContacts() + } + } + .overlay( + VStack { + if contactsManager.avatarListModel.isEmpty { + Spacer() + Image("illus-belledonne") + .resizable() + .scaledToFit() + .clipped() + .padding(.all) + Text(!text.isEmpty ? "list_filter_no_result_found" : "contacts_list_empty") + .default_text_style_800(styleSize: 16) + Spacer() + Spacer() + } + } + .padding(.all) + ) + } + } + + if magicSearch.isLoading { + ProgressView() + .controlSize(.large) + .progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500)) } } .navigationBarHidden(true) diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 3fba105a3..590ec4130 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -544,7 +544,7 @@ struct ContentView: View { magicSearch.searchForContacts() } label: { HStack { - Text(String(format: String(localized: "contacts_list_filter_popup_see_linphone_only"), Bundle.main.displayName)) + Text(magicSearch.domainDefaultAccount == "*" ? String(localized: "contacts_list_filter_popup_see_sip_only") : String(format: String(localized: "contacts_list_filter_popup_see_linphone_only"), Bundle.main.displayName)) Spacer() if !magicSearch.allContact { Image("green-check") diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationForwardMessageFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationForwardMessageFragment.swift index 5a41b038d..403f0a14b 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationForwardMessageFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationForwardMessageFragment.swift @@ -141,50 +141,58 @@ struct ConversationForwardMessageFragment: View { .padding(.vertical) .padding(.horizontal) - ScrollView { - if !conversationForwardMessageViewModel.conversationsList.isEmpty { - HStack(alignment: .center) { - Text("bottom_navigation_conversations_label") - .default_text_style_800(styleSize: 16) + ZStack { + ScrollView { + if !conversationForwardMessageViewModel.conversationsList.isEmpty { + HStack(alignment: .center) { + Text("bottom_navigation_conversations_label") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) - Spacer() + conversationsList } - .padding(.vertical, 10) + + if !ContactsManager.shared.lastSearch.isEmpty { + HStack(alignment: .center) { + Text("contacts_list_all_contacts_title") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + } + + ContactsListFragment(showingSheet: .constant(false), startCallFunc: { addr in + withAnimation { + conversationForwardMessageViewModel.createOneToOneChatRoomWith(remote: addr) + } + + }) .padding(.horizontal, 16) - conversationsList + if !contactsManager.lastSearchSuggestions.isEmpty { + HStack(alignment: .center) { + Text("generic_address_picker_suggestions_list_title") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + + suggestionsList + } } - if !ContactsManager.shared.lastSearch.isEmpty { - HStack(alignment: .center) { - Text("contacts_list_all_contacts_title") - .default_text_style_800(styleSize: 16) - - Spacer() - } - .padding(.vertical, 10) - .padding(.horizontal, 16) - } - - ContactsListFragment(showingSheet: .constant(false), startCallFunc: { addr in - withAnimation { - conversationForwardMessageViewModel.createOneToOneChatRoomWith(remote: addr) - } - - }) - .padding(.horizontal, 16) - - if !contactsManager.lastSearchSuggestions.isEmpty { - HStack(alignment: .center) { - Text("generic_address_picker_suggestions_list_title") - .default_text_style_800(styleSize: 16) - - Spacer() - } - .padding(.vertical, 10) - .padding(.horizontal, 16) - - suggestionsList + if magicSearch.isLoading { + ProgressView() + .controlSize(.large) + .progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500)) } } } diff --git a/Linphone/UI/Main/Conversations/Fragments/StartConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/StartConversationFragment.swift index 55d5dc64b..2a81a536f 100644 --- a/Linphone/UI/Main/Conversations/Fragments/StartConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/StartConversationFragment.swift @@ -170,34 +170,42 @@ struct StartConversationFragment: View { ) } - ScrollView { - if !ContactsManager.shared.lastSearch.isEmpty { - HStack(alignment: .center) { - Text("contacts_list_all_contacts_title") - .default_text_style_800(styleSize: 16) - - Spacer() + ZStack { + ScrollView { + if !ContactsManager.shared.lastSearch.isEmpty { + HStack(alignment: .center) { + Text("contacts_list_all_contacts_title") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) } - .padding(.vertical, 10) - .padding(.horizontal, 16) - } - - ContactsListFragment(showingSheet: .constant(false), startCallFunc: { addr in + + ContactsListFragment(showingSheet: .constant(false), startCallFunc: { addr in startConversationViewModel.createOneToOneChatRoomWith(remote: addr) - }) - .padding(.horizontal, 16) - - if !contactsManager.lastSearchSuggestions.isEmpty { - HStack(alignment: .center) { - Text("generic_address_picker_suggestions_list_title") - .default_text_style_800(styleSize: 16) - - Spacer() - } - .padding(.vertical, 10) + }) .padding(.horizontal, 16) - suggestionsList + if !contactsManager.lastSearchSuggestions.isEmpty { + HStack(alignment: .center) { + Text("generic_address_picker_suggestions_list_title") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + + suggestionsList + } + } + + if magicSearch.isLoading { + ProgressView() + .controlSize(.large) + .progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500)) } } } diff --git a/Linphone/UI/Main/History/Fragments/StartCallFragment.swift b/Linphone/UI/Main/History/Fragments/StartCallFragment.swift index 735f3928a..6e526c32c 100644 --- a/Linphone/UI/Main/History/Fragments/StartCallFragment.swift +++ b/Linphone/UI/Main/History/Fragments/StartCallFragment.swift @@ -235,75 +235,83 @@ struct StartCallFragment: View { ) } - ScrollView { - if !ContactsManager.shared.lastSearch.isEmpty { - HStack(alignment: .center) { - Text("contacts_list_all_contacts_title") - .default_text_style_800(styleSize: 16) - - Spacer() + ZStack { + ScrollView { + if !ContactsManager.shared.lastSearch.isEmpty { + HStack(alignment: .center) { + Text("contacts_list_all_contacts_title") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) } - .padding(.vertical, 10) - .padding(.horizontal, 16) - } - - ContactsListFragment(showingSheet: .constant(false) - , startCallFunc: { addr in - if callViewModel.isTransferInsteadCall { - showingDialer = false - - startCallViewModel.searchField = "" - magicSearch.currentFilter = "" - - magicSearch.searchForContacts() - - if callViewModel.isTransferInsteadCall == true { - callViewModel.isTransferInsteadCall = false + + ContactsListFragment(showingSheet: .constant(false) + , startCallFunc: { addr in + if callViewModel.isTransferInsteadCall { + showingDialer = false + + startCallViewModel.searchField = "" + magicSearch.currentFilter = "" + + magicSearch.searchForContacts() + + if callViewModel.isTransferInsteadCall == true { + callViewModel.isTransferInsteadCall = false + } + + resetCallView() + + delayColorDismiss() + + withAnimation { + isShowStartCallFragment.toggle() + callViewModel.blindTransferCallTo(toAddress: addr) + } + } else { + showingDialer = false + + startCallViewModel.searchField = "" + magicSearch.currentFilter = "" + + magicSearch.searchForContacts() + + if callViewModel.isTransferInsteadCall == true { + callViewModel.isTransferInsteadCall = false + } + + resetCallView() + + delayColorDismiss() + + withAnimation { + isShowStartCallFragment.toggle() + telecomManager.doCallOrJoinConf(address: addr) + } } - - resetCallView() - - delayColorDismiss() - - withAnimation { - isShowStartCallFragment.toggle() - callViewModel.blindTransferCallTo(toAddress: addr) - } - } else { - showingDialer = false - - startCallViewModel.searchField = "" - magicSearch.currentFilter = "" - - magicSearch.searchForContacts() - - if callViewModel.isTransferInsteadCall == true { - callViewModel.isTransferInsteadCall = false - } - - resetCallView() - - delayColorDismiss() - - withAnimation { - isShowStartCallFragment.toggle() - telecomManager.doCallOrJoinConf(address: addr) - } - } - }) - .padding(.horizontal, 16) - - if !contactsManager.lastSearchSuggestions.isEmpty { - HStack(alignment: .center) { - Text("generic_address_picker_suggestions_list_title") - .default_text_style_800(styleSize: 16) - - Spacer() - } - .padding(.vertical, 10) + }) .padding(.horizontal, 16) - suggestionsList + if !contactsManager.lastSearchSuggestions.isEmpty { + HStack(alignment: .center) { + Text("generic_address_picker_suggestions_list_title") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + + suggestionsList + } + } + + if magicSearch.isLoading { + ProgressView() + .controlSize(.large) + .progressViewStyle(CircularProgressViewStyle(tint: .orangeMain500)) } } } diff --git a/Linphone/UI/Main/Meetings/Fragments/AddParticipantsFragment.swift b/Linphone/UI/Main/Meetings/Fragments/AddParticipantsFragment.swift index 92cdfb634..7d2df6c33 100644 --- a/Linphone/UI/Main/Meetings/Fragments/AddParticipantsFragment.swift +++ b/Linphone/UI/Main/Meetings/Fragments/AddParticipantsFragment.swift @@ -167,76 +167,84 @@ struct AddParticipantsFragment: View { .padding(.bottom) .padding(.horizontal) - ScrollView { - ForEach(0.. Date: Mon, 17 Nov 2025 15:29:45 +0100 Subject: [PATCH 02/13] Fix the prefix handling in interpretUrl when calling a phone number and add this in Settings --- Linphone/Contacts/ContactsManager.swift | 2 +- Linphone/Localizable/en.lproj/Localizable.strings | 2 ++ Linphone/Localizable/fr.lproj/Localizable.strings | 2 ++ .../Contacts/Fragments/ContactInnerActionsFragment.swift | 2 +- .../UI/Main/Contacts/Fragments/ContactInnerFragment.swift | 6 +++--- .../UI/Main/History/ViewModel/StartCallViewModel.swift | 2 +- .../Main/Settings/Fragments/AccountSettingsFragment.swift | 6 ++++++ .../Main/Settings/ViewModel/AccountSettingsViewModel.swift | 7 +++++++ 8 files changed, 23 insertions(+), 6 deletions(-) diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index 194e7a556..1338effcc 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -318,7 +318,7 @@ final class ContactsManager: ObservableObject { // Clear existing addresses and add new ones friend.addresses.forEach { friend.removeAddress(address: $0) } for sipAddress in contact.sipAddresses where !sipAddress.isEmpty { - if let address = core.interpretUrl(url: sipAddress, applyInternationalPrefix: true), + if let address = core.interpretUrl(url: sipAddress, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)), !friend.addresses.contains(where: { $0.asString() == address.asString() }) { friend.addAddress(address: address) } diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index a8f3001d7..0a30c7940 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -47,6 +47,8 @@ "account_settings_im_encryption_mandatory_title" = "IM encryption mandatory"; "account_settings_lime_server_url_title" = "E2E encryption keys server URL"; "account_settings_mwi_uri_title" = "MWI server URI (Message Waiting Indicator)"; +"account_settings_apply_international_prefix_title" = "Format phone numbers using international prefix"; +"account_settings_replace_plus_by_00_title" = "Replace + by 00 when formatting phone numbers"; "account_settings_nat_policy_title" = "NAT policy settings"; "account_settings_outbound_proxy_title" = "Outbound proxy"; "account_settings_push_notification_not_available_title" = "Push notifications aren't available!"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index 9d05cf509..02d7afec5 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -47,6 +47,8 @@ "account_settings_im_encryption_mandatory_title" = "Chiffrement obligatoire des conversations"; "account_settings_lime_server_url_title" = "URL du serveur d'échange de clés de chiffrement"; "account_settings_mwi_uri_title" = "URI du serveur MWI (Message Waiting Indicator)"; +"account_settings_apply_international_prefix_title" = "Formater les numéros en utilisant l'indicatif international"; +"account_settings_replace_plus_by_00_title" = "Remplacer + par 00 lors du formatage des numéros de téléphone"; "account_settings_nat_policy_title" = "Paramètres de politique NAT"; "account_settings_outbound_proxy_title" = "Serveur mandataire sortant"; "account_settings_push_notification_not_available_title" = "Notifications push non disponibles"; diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift index cdd0f4b98..fe1c0c62d 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift @@ -156,7 +156,7 @@ struct ContactInnerActionsFragment: View { .background(.white) .onTapGesture { CoreContext.shared.doOnCoreQueue { core in - let address = core.interpretUrl(url: contactAvatarModel.phoneNumbersWithLabel[index].phoneNumber, applyInternationalPrefix: true) + let address = core.interpretUrl(url: contactAvatarModel.phoneNumbersWithLabel[index].phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) if address != nil { TelecomManager.shared.doCallOrJoinConf(address: address!) } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift index c920aba7b..bece283f3 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift @@ -153,7 +153,7 @@ struct ContactInnerFragment: View { Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ") } } else if contactAvatarModel.addresses.count < 1 && contactAvatarModel.phoneNumbersWithLabel.count == 1 { - if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: true) { + if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) { telecomManager.doCallOrJoinConf(address: address, isVideo: false) } } else { @@ -194,7 +194,7 @@ struct ContactInnerFragment: View { Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ") } } else if contactAvatarModel.addresses.count < 1 && contactAvatarModel.phoneNumbersWithLabel.count == 1 { - if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: true) { + if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) { contactsListViewModel.createOneToOneChatRoomWith(remote: address) } } else { @@ -235,7 +235,7 @@ struct ContactInnerFragment: View { Log.error("[ContactInnerFragment] unable to create address for a new outgoing call : \(contactAvatarModel.address) \(error) ") } } else if contactAvatarModel.addresses.count < 1 && contactAvatarModel.phoneNumbersWithLabel.count == 1 { - if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: true) { + if let firstPhoneNumbersWithLabel = contactAvatarModel.phoneNumbersWithLabel.first, let address = core.interpretUrl(url: firstPhoneNumbersWithLabel.phoneNumber, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) { telecomManager.doCallOrJoinConf(address: address, isVideo: true) } } else { diff --git a/Linphone/UI/Main/History/ViewModel/StartCallViewModel.swift b/Linphone/UI/Main/History/ViewModel/StartCallViewModel.swift index 1b3580a95..3b69db4cf 100644 --- a/Linphone/UI/Main/History/ViewModel/StartCallViewModel.swift +++ b/Linphone/UI/Main/History/ViewModel/StartCallViewModel.swift @@ -157,7 +157,7 @@ class StartCallViewModel: ObservableObject { func interpretAndStartCall() { CoreContext.shared.doOnCoreQueue { core in - let address = core.interpretUrl(url: self.searchField, applyInternationalPrefix: true) + let address = core.interpretUrl(url: self.searchField, applyInternationalPrefix: LinphoneUtils.applyInternationalPrefix(core: core)) if address != nil { TelecomManager.shared.doCallOrJoinConf(address: address!) } diff --git a/Linphone/UI/Main/Settings/Fragments/AccountSettingsFragment.swift b/Linphone/UI/Main/Settings/Fragments/AccountSettingsFragment.swift index 7b8732cc5..82ac76af4 100644 --- a/Linphone/UI/Main/Settings/Fragments/AccountSettingsFragment.swift +++ b/Linphone/UI/Main/Settings/Fragments/AccountSettingsFragment.swift @@ -132,6 +132,12 @@ struct AccountSettingsFragment: View { ) .focused($isMwiUriFocused) } + + Toggle("account_settings_apply_international_prefix_title", isOn: $accountSettingsViewModel.applyInternationalPrefix) + .default_text_style_700(styleSize: 15) + + Toggle("account_settings_replace_plus_by_00_title", isOn: $accountSettingsViewModel.replacePlusBy00) + .default_text_style_700(styleSize: 15) } .padding(.vertical, 30) .padding(.horizontal, 20) diff --git a/Linphone/UI/Main/Settings/ViewModel/AccountSettingsViewModel.swift b/Linphone/UI/Main/Settings/ViewModel/AccountSettingsViewModel.swift index 011edb193..2670e4ec5 100644 --- a/Linphone/UI/Main/Settings/ViewModel/AccountSettingsViewModel.swift +++ b/Linphone/UI/Main/Settings/ViewModel/AccountSettingsViewModel.swift @@ -30,6 +30,8 @@ class AccountSettingsViewModel: ObservableObject { @Published var imEncryptionMandatory: Bool @Published var voicemailUri: String @Published var mwiUri: String + @Published var applyInternationalPrefix: Bool + @Published var replacePlusBy00: Bool @Published var stunServerUrl: String @Published var enableIce: Bool @Published var enableTurn: Bool @@ -57,6 +59,8 @@ class AccountSettingsViewModel: ObservableObject { self.imEncryptionMandatory = accountModel.account.params?.instantMessagingEncryptionMandatory ?? false self.voicemailUri = accountModel.account.params?.voicemailAddress?.asStringUriOnly() ?? "" self.mwiUri = accountModel.account.params?.mwiServerAddress?.asStringUriOnly() ?? "" + self.applyInternationalPrefix = accountModel.account.params?.useInternationalPrefixForCallsAndChats ?? false + self.replacePlusBy00 = accountModel.account.params?.dialEscapePlusEnabled ?? false self.natPolicy = accountModel.account.params?.natPolicy self.stunServerUrl = accountModel.account.params?.natPolicy?.stunServer ?? "" @@ -190,6 +194,9 @@ class AccountSettingsViewModel: ObservableObject { newParams.voicemailAddress = nil } + newParams.useInternationalPrefixForCallsAndChats = self.applyInternationalPrefix + newParams.dialEscapePlusEnabled = self.replacePlusBy00 + let expireInt: Int = { if !self.expire.isEmpty { return Int(self.expire) ?? 31536000 From b904f71f794c7bf92a4b070035c2d401ef5e7357 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 18 Nov 2025 10:52:41 +0100 Subject: [PATCH 03/13] Displaying core call logs instead of account call logs when the user has only one account --- .../UI/Main/History/ViewModel/HistoryListViewModel.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift index 90aff417b..ceb6df8ea 100644 --- a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift +++ b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift @@ -45,7 +45,12 @@ class HistoryListViewModel: ObservableObject { func computeCallLogsList() { coreContext.doOnCoreQueue { core in let account = core.defaultAccount - let logs = account?.callLogs != nil ? account!.callLogs : core.callLogs + + // Fetch all call logs if only one account to workaround no history issue + // TODO FIXME: remove workaround later + let logs = (core.accountList.count > 1) + ? (account?.callLogs ?? core.callLogs) + : core.callLogs var callLogsBis: [HistoryModel] = [] var callLogsTmpBis: [HistoryModel] = [] From a421d90d0c4815e394822cfa310ea6713aa950d0 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 20 Nov 2025 10:37:25 +0100 Subject: [PATCH 04/13] Refresh displayed friend when the contacts list is updated --- Linphone/Utils/MagicSearchSingleton.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Linphone/Utils/MagicSearchSingleton.swift b/Linphone/Utils/MagicSearchSingleton.swift index 54c8d3e31..783b64947 100644 --- a/Linphone/Utils/MagicSearchSingleton.swift +++ b/Linphone/Utils/MagicSearchSingleton.swift @@ -172,7 +172,13 @@ final class MagicSearchSingleton: ObservableObject { lastSearchSuggestions: [SearchResult], addedAvatarListModel: [ContactAvatarModel] ) { - DispatchQueue.main.async { + DispatchQueue.main.async { + if SharedMainViewModel.shared.displayedFriend != nil { + if let avatarModel = addedAvatarListModel.first(where: { $0.address == SharedMainViewModel.shared.displayedFriend?.address }) { + SharedMainViewModel.shared.displayedFriend = avatarModel + } + } + self.contactsManager.lastSearch = sortedLastSearch self.contactsManager.lastSearchSuggestions = lastSearchSuggestions From fa1f8386b4b541fd1ef903661d0796961649a510 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 20 Nov 2025 17:49:14 +0100 Subject: [PATCH 05/13] Refresh presence info in history detail --- .../Fragments/HistoryContactFragment.swift | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift index 8ecbc13ad..132d4442d 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift @@ -196,14 +196,8 @@ struct HistoryContactFragment: View { .padding(.top, 5) } - if let avatarModel = historyModel.avatarModel { - Text(avatarModel.lastPresenceInfo) - .foregroundStyle(avatarModel.lastPresenceInfo == "Online" ? Color.greenSuccess500 : Color.orangeWarning600) - .multilineTextAlignment(.center) - .default_text_style_300(styleSize: 12) - .frame(maxWidth: .infinity) - .frame(height: 20) - .padding(.top, 5) + if let avatar = historyModel.avatarModel { + AvatarPresenceView(avatarModel: avatar) } else { Text("") .multilineTextAlignment(.center) @@ -423,6 +417,21 @@ struct HistoryContactFragment: View { } } +struct AvatarPresenceView: View { + @ObservedObject var avatarModel: ContactAvatarModel + + var body: some View { + Text(avatarModel.lastPresenceInfo) + .foregroundStyle(avatarModel.lastPresenceInfo == "Online" ? Color.greenSuccess500 : Color.orangeWarning600) + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + .padding(.top, 5) + } +} + + #Preview { HistoryContactFragment( isShowDeleteAllHistoryPopup: .constant(false), From 7972fd7c1f985b5e3a7ce69aff6cbe57a3458d0c Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 6 Nov 2025 16:11:40 +0100 Subject: [PATCH 06/13] 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() } From 07dbf407b044ad1ce7542adb3ecb9b9d8295496d Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 20 Nov 2025 15:54:22 +0100 Subject: [PATCH 07/13] Update PopupView UI --- .../Localizable/cs.lproj/Localizable.strings | 2 +- .../Localizable/de.lproj/Localizable.strings | 2 +- .../Localizable/en.lproj/Localizable.strings | 2 +- .../Localizable/fr.lproj/Localizable.strings | 2 +- .../Localizable/ru.lproj/Localizable.strings | 2 +- .../Localizable/sk.lproj/Localizable.strings | 2 +- .../Localizable/uk.lproj/Localizable.strings | 2 +- .../zh-Hans.lproj/Localizable.strings | 2 +- .../Assistant/Fragments/LoginFragment.swift | 36 ++-- .../Fragments/ProfileModeFragment.swift | 29 ++-- .../Fragments/RegisterFragment.swift | 11 +- .../UI/Call/Fragments/CallsListFragment.swift | 26 +-- .../Fragments/ParticipantsListFragment.swift | 26 +-- Linphone/UI/Main/ContentView.swift | 160 +++++++++++------- .../StartGroupConversationFragment.swift | 2 +- .../Main/Fragments/PopupUpdatePassword.swift | 2 +- Linphone/UI/Main/Fragments/PopupView.swift | 94 ++++++---- .../Fragments/PopupViewWithTextField.swift | 2 +- .../UI/Main/Help/Fragments/HelpFragment.swift | 10 +- .../Fragments/ScheduleMeetingFragment.swift | 2 +- .../Fragments/AccountProfileFragment.swift | 21 +-- 21 files changed, 258 insertions(+), 179 deletions(-) diff --git a/Linphone/Localizable/cs.lproj/Localizable.strings b/Linphone/Localizable/cs.lproj/Localizable.strings index 27e239ef7..631ccadd3 100644 --- a/Linphone/Localizable/cs.lproj/Localizable.strings +++ b/Linphone/Localizable/cs.lproj/Localizable.strings @@ -2,7 +2,7 @@ "assistant_sip_account_transport_protocol" = "Transport"; "contact_call_action" = "Volat"; "conversation_action_call" = "Volat"; -"dialog_ok" = "OK"; +"dialog_confirm" = "Confirm"; "drawer_menu_manage_account" = "Spravovat profil"; "help_error_checking_version_toast_message" = "Během kontroly aktualizací nastala chyba"; "settings_contacts_carddav_name_title" = "Zobrazené jméno"; diff --git a/Linphone/Localizable/de.lproj/Localizable.strings b/Linphone/Localizable/de.lproj/Localizable.strings index 97d4d7e1c..8d4ff7de9 100644 --- a/Linphone/Localizable/de.lproj/Localizable.strings +++ b/Linphone/Localizable/de.lproj/Localizable.strings @@ -192,7 +192,7 @@ "dialog_deny" = "Ablehnen"; "dialog_install" = "Installieren"; "dialog_no" = "Nein"; -"dialog_ok" = "OK"; +"dialog_confirm" = "Confirm"; "dialog_yes" = "Ja"; "drawer_menu_account_connection_status_cleared" = "Deaktiviert"; "drawer_menu_account_connection_status_connected" = "Verbunden"; diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index c68cdeafa..d7050448e 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -272,7 +272,7 @@ "dialog_deny" = "Deny"; "dialog_install" = "Install"; "dialog_no" = "No"; -"dialog_ok" = "OK"; +"dialog_confirm" = "Confirm"; "dialog_yes" = "Yes"; "drawer_menu_account_connection_status_cleared" = "Disabled"; "drawer_menu_account_connection_status_connected" = "Connected"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index b6008c570..5f09441a0 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -272,7 +272,7 @@ "dialog_deny" = "Refuser"; "dialog_install" = "Installer"; "dialog_no" = "Non"; -"dialog_ok" = "OK"; +"dialog_confirm" = "Confirmer"; "dialog_yes" = "Oui"; "drawer_menu_account_connection_status_cleared" = "Désactivé"; "drawer_menu_account_connection_status_connected" = "Connecté"; diff --git a/Linphone/Localizable/ru.lproj/Localizable.strings b/Linphone/Localizable/ru.lproj/Localizable.strings index 0f6b7d6f9..bd4edb2a2 100644 --- a/Linphone/Localizable/ru.lproj/Localizable.strings +++ b/Linphone/Localizable/ru.lproj/Localizable.strings @@ -187,7 +187,7 @@ "dialog_deny" = "Отказать"; "dialog_install" = "Установить"; "dialog_no" = "Нет"; -"dialog_ok" = "ОК"; +"dialog_confirm" = "ОК"; "dialog_yes" = "Да"; "drawer_menu_account_connection_status_cleared" = "Отключить"; "drawer_menu_account_connection_status_connected" = "Подключен"; diff --git a/Linphone/Localizable/sk.lproj/Localizable.strings b/Linphone/Localizable/sk.lproj/Localizable.strings index 7a04eba8f..582bba3a4 100644 --- a/Linphone/Localizable/sk.lproj/Localizable.strings +++ b/Linphone/Localizable/sk.lproj/Localizable.strings @@ -171,7 +171,7 @@ "dialog_deny" = "Odmietnuť"; "dialog_install" = "Inštalovať"; "dialog_no" = "Nie"; -"dialog_ok" = "OK"; +"dialog_confirm" = "Confirm"; "dialog_yes" = "Áno"; "meeting_waiting_room_cancel" = "Zrušiť"; "menu_delete_selected_item" = "Vymazať"; diff --git a/Linphone/Localizable/uk.lproj/Localizable.strings b/Linphone/Localizable/uk.lproj/Localizable.strings index b965bbb6f..3e7a019ab 100644 --- a/Linphone/Localizable/uk.lproj/Localizable.strings +++ b/Linphone/Localizable/uk.lproj/Localizable.strings @@ -377,7 +377,7 @@ "conversation_ephemeral_messages_duration_one_week" = "1 тиждень"; "conversation_ephemeral_messages_duration_three_days" = "3 доби"; "conversation_ephemeral_messages_duration_one_day" = "1 доба"; -"dialog_ok" = "ОК"; +"dialog_confirm" = "ОК"; "welcome_page_title" = "Ласкаво просимо"; ": %@" = ": %@"; "[https://sip.linphone.org](https://sip.linphone.org)" = "[https://sip.linphone.org](https://sip.linphone.org)"; diff --git a/Linphone/Localizable/zh-Hans.lproj/Localizable.strings b/Linphone/Localizable/zh-Hans.lproj/Localizable.strings index 853d1469c..b3b295824 100644 --- a/Linphone/Localizable/zh-Hans.lproj/Localizable.strings +++ b/Linphone/Localizable/zh-Hans.lproj/Localizable.strings @@ -181,7 +181,7 @@ "dialog_deny" = "拒绝"; "dialog_install" = "安装"; "dialog_no" = "否"; -"dialog_ok" = "OK"; +"dialog_confirm" = "Confirm"; "dialog_yes" = "是"; "drawer_menu_account_connection_status_cleared" = "禁用"; "drawer_menu_account_connection_status_connected" = "已连接"; diff --git a/Linphone/UI/Assistant/Fragments/LoginFragment.swift b/Linphone/UI/Assistant/Fragments/LoginFragment.swift index 3fde7171e..655dd638d 100644 --- a/Linphone/UI/Assistant/Fragments/LoginFragment.swift +++ b/Linphone/UI/Assistant/Fragments/LoginFragment.swift @@ -70,25 +70,33 @@ struct LoginFragment: View { let contentPopup3 = Text(.init(splitMsg[1])) let contentPopup4 = Text(.init(privacyPolicy)).underline() let contentPopup5 = Text(.init(splitMsg[2])) - PopupView(isShowPopup: $isShowPopup, - title: Text("assistant_dialog_general_terms_and_privacy_policy_title"), - content: contentPopup1 + contentPopup2 + contentPopup3 + contentPopup4 + contentPopup5, - titleFirstButton: Text("dialog_deny"), - actionFirstButton: {self.isShowPopup.toggle()}, - titleSecondButton: Text("dialog_accept"), - actionSecondButton: {acceptGeneralTerms()}) + PopupView( + isShowPopup: $isShowPopup, + title: Text("assistant_dialog_general_terms_and_privacy_policy_title"), + content: contentPopup1 + contentPopup2 + contentPopup3 + contentPopup4 + contentPopup5, + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: Text("dialog_accept"), + actionSecondButton: { acceptGeneralTerms() }, + titleThirdButton: Text("dialog_deny"), + actionThirdButton: { self.isShowPopup.toggle() } + ) .background(.black.opacity(0.65)) .onTapGesture { self.isShowPopup.toggle() } } else { // backup just in case - PopupView(isShowPopup: $isShowPopup, - title: Text("assistant_dialog_general_terms_and_privacy_policy_title"), - content: Text(.init(String(format: String(localized: "assistant_dialog_general_terms_and_privacy_policy_message"), generalTerms, privacyPolicy))), - titleFirstButton: Text("dialog_deny"), - actionFirstButton: {self.isShowPopup.toggle()}, - titleSecondButton: Text("dialog_accept"), - actionSecondButton: {acceptGeneralTerms()}) + PopupView( + isShowPopup: $isShowPopup, + title: Text("assistant_dialog_general_terms_and_privacy_policy_title"), + content: Text(.init(String(format: String(localized: "assistant_dialog_general_terms_and_privacy_policy_message"), generalTerms, privacyPolicy))), + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: Text("dialog_accept"), + actionSecondButton: { acceptGeneralTerms() }, + titleThirdButton: Text("dialog_deny"), + actionThirdButton: { self.isShowPopup.toggle() } + ) .background(.black.opacity(0.65)) .onTapGesture { self.isShowPopup.toggle() diff --git a/Linphone/UI/Assistant/Fragments/ProfileModeFragment.swift b/Linphone/UI/Assistant/Fragments/ProfileModeFragment.swift index b5aad06a6..bfde7c666 100644 --- a/Linphone/UI/Assistant/Fragments/ProfileModeFragment.swift +++ b/Linphone/UI/Assistant/Fragments/ProfileModeFragment.swift @@ -145,20 +145,21 @@ struct ProfileModeFragment: View { } if self.isShowPopup { - PopupView(isShowPopup: $isShowPopup, - title: Text(isShowPopupForDefault ? "Default mode" : "Interoperable mode"), - content: Text( - isShowPopupForDefault - ? "Texte explicatif du default mode : lorem ipsum dolor sit amet, consectetur adipiscing elit." - + "Etiam velit sapien, egestas sit amet dictum eget, condimentum a ligula." - : "Texte explicatif du interoperable mode : lorem ipsum dolor sit amet, consectetur adipiscing elit." - + " Etiam velit sapien, egestas sit amet dictum eget, condimentum a ligula."), - titleFirstButton: nil, - actionFirstButton: {}, - titleSecondButton: Text("dialog_close"), - actionSecondButton: { - self.isShowPopup.toggle() - } + PopupView( + isShowPopup: $isShowPopup, + title: Text(isShowPopupForDefault ? "Default mode" : "Interoperable mode"), + content: Text( + isShowPopupForDefault + ? "Texte explicatif du default mode : lorem ipsum dolor sit amet, consectetur adipiscing elit." + + "Etiam velit sapien, egestas sit amet dictum eget, condimentum a ligula." + : "Texte explicatif du interoperable mode : lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Etiam velit sapien, egestas sit amet dictum eget, condimentum a ligula."), + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: nil, + actionSecondButton: {}, + titleThirdButton: Text("dialog_close"), + actionThirdButton: { self.isShowPopup.toggle() } ) .background(.black.opacity(0.65)) .onTapGesture { diff --git a/Linphone/UI/Assistant/Fragments/RegisterFragment.swift b/Linphone/UI/Assistant/Fragments/RegisterFragment.swift index 69f42192e..2d5179eb7 100644 --- a/Linphone/UI/Assistant/Fragments/RegisterFragment.swift +++ b/Linphone/UI/Assistant/Fragments/RegisterFragment.swift @@ -57,22 +57,21 @@ struct RegisterFragment: View { let titlePopup = Text("assistant_dialog_confirm_phone_number_title") let contentPopup = Text(String(format: String(localized: "assistant_dialog_confirm_phone_number_message"), registerViewModel.phoneNumber)) - PopupView( isShowPopup: $isShowPopup, title: titlePopup, content: contentPopup, - titleFirstButton: Text("dialog_cancel"), - actionFirstButton: { - self.isShowPopup = false - }, + titleFirstButton: nil, + actionFirstButton: {}, titleSecondButton: Text("dialog_continue"), actionSecondButton: { self.isShowPopup = false registerViewModel.createInProgress = true registerViewModel.startAccountCreation() registerViewModel.phoneNumberConfirmedByUser() - } + }, + titleThirdButton: Text("dialog_cancel"), + actionThirdButton: { self.isShowPopup = false }, ) .background(.black.opacity(0.65)) .onTapGesture { diff --git a/Linphone/UI/Call/Fragments/CallsListFragment.swift b/Linphone/UI/Call/Fragments/CallsListFragment.swift index 2c670cb87..d6211a6a4 100644 --- a/Linphone/UI/Call/Fragments/CallsListFragment.swift +++ b/Linphone/UI/Call/Fragments/CallsListFragment.swift @@ -103,17 +103,21 @@ struct CallsListFragment: View { .background(.white) if self.isShowPopup { - PopupView(isShowPopup: $isShowPopup, - title: Text("calls_list_dialog_merge_into_conference_title"), - content: nil, - titleFirstButton: Text("dialog_cancel"), - actionFirstButton: {self.isShowPopup.toggle()}, - titleSecondButton: Text("calls_list_dialog_merge_into_conference_label"), - actionSecondButton: { - callViewModel.mergeCallsIntoConference() - self.isShowPopup.toggle() - isShowCallsListFragment.toggle() - }) + PopupView( + isShowPopup: $isShowPopup, + title: Text("calls_list_dialog_merge_into_conference_title"), + content: nil, + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: Text("calls_list_dialog_merge_into_conference_label"), + actionSecondButton: { + callViewModel.mergeCallsIntoConference() + self.isShowPopup.toggle() + isShowCallsListFragment.toggle() + }, + titleThirdButton: Text("dialog_cancel"), + actionThirdButton: { self.isShowPopup.toggle() }, + ) .background(.black.opacity(0.65)) .onTapGesture { self.isShowPopup.toggle() diff --git a/Linphone/UI/Call/Fragments/ParticipantsListFragment.swift b/Linphone/UI/Call/Fragments/ParticipantsListFragment.swift index 756670ee5..5478a6533 100644 --- a/Linphone/UI/Call/Fragments/ParticipantsListFragment.swift +++ b/Linphone/UI/Call/Fragments/ParticipantsListFragment.swift @@ -108,17 +108,21 @@ struct ParticipantsListFragment: View { if self.isShowPopup { let contentPopup = Text(String(format: String(localized: "meeting_call_remove_participant_confirmation_message"), callViewModel.participantList[indexToRemove].name)) - PopupView(isShowPopup: $isShowPopup, - title: Text("meeting_call_remove_participant_confirmation_title"), - content: contentPopup, - titleFirstButton: Text("dialog_no"), - actionFirstButton: {self.isShowPopup.toggle()}, - titleSecondButton: Text("dialog_yes"), - actionSecondButton: { - callViewModel.removeParticipant(index: indexToRemove) - self.isShowPopup.toggle() - indexToRemove = -1 - }) + PopupView( + isShowPopup: $isShowPopup, + title: Text("meeting_call_remove_participant_confirmation_title"), + content: contentPopup, + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: Text("dialog_yes"), + actionSecondButton: { + callViewModel.removeParticipant(index: indexToRemove) + self.isShowPopup.toggle() + indexToRemove = -1 + }, + titleThirdButton: Text("dialog_no"), + actionThirdButton: { self.isShowPopup.toggle() } + ) .background(.black.opacity(0.65)) .onTapGesture { self.isShowPopup.toggle() diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 590ec4130..a2b19818a 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -1111,14 +1111,16 @@ struct ContentView: View { ) ), content: Text("contact_dialog_delete_message"), - titleFirstButton: Text("dialog_cancel"), - actionFirstButton: { - self.isShowDeleteContactPopup.toggle()}, - titleSecondButton: Text("dialog_ok"), + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: Text("dialog_confirm"), actionSecondButton: { contactsListVM.deleteSelectedContact() self.isShowDeleteContactPopup.toggle() - }) + }, + titleThirdButton: Text("dialog_cancel"), + actionThirdButton: { self.isShowDeleteContactPopup.toggle() } + ) .background(.black.opacity(0.65)) .zIndex(3) .onTapGesture { @@ -1130,27 +1132,31 @@ struct ContentView: View { } if isShowDeleteAllHistoryPopup { - PopupView(isShowPopup: $isShowDeleteContactPopup, - title: Text("history_dialog_delete_all_call_logs_title"), - content: Text("history_dialog_delete_all_call_logs_message"), - titleFirstButton: Text("dialog_cancel"), - actionFirstButton: { - self.isShowDeleteAllHistoryPopup.toggle() - if let historyListVM = historyListViewModel { - historyListVM.callLogsAddressToDelete = "" + PopupView( + isShowPopup: $isShowDeleteContactPopup, + title: Text("history_dialog_delete_all_call_logs_title"), + content: Text("history_dialog_delete_all_call_logs_message"), + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: Text("dialog_confirm"), + actionSecondButton: { + if let historyListVM = historyListViewModel { + historyListVM.removeCallLogs() + } + self.isShowDeleteAllHistoryPopup.toggle() + sharedMainViewModel.displayedCall = nil + + ToastViewModel.shared.toastMessage = "Success_remove_call_logs" + ToastViewModel.shared.displayToast.toggle() + }, + titleThirdButton: Text("dialog_cancel"), + actionThirdButton: { + self.isShowDeleteAllHistoryPopup.toggle() + if let historyListVM = historyListViewModel { + historyListVM.callLogsAddressToDelete = "" + } } - }, - titleSecondButton: Text("dialog_ok"), - actionSecondButton: { - if let historyListVM = historyListViewModel { - historyListVM.removeCallLogs() - } - self.isShowDeleteAllHistoryPopup.toggle() - sharedMainViewModel.displayedCall = nil - - ToastViewModel.shared.toastMessage = "Success_remove_call_logs" - ToastViewModel.shared.displayToast.toggle() - }) + ) .background(.black.opacity(0.65)) .zIndex(3) .onTapGesture { @@ -1159,20 +1165,24 @@ struct ContentView: View { } if isShowDismissPopup { - PopupView(isShowPopup: $isShowDismissPopup, - title: Text("contact_editor_dialog_abort_confirmation_title"), - content: Text("contact_editor_dialog_abort_confirmation_message"), - titleFirstButton: Text("dialog_cancel"), - actionFirstButton: {self.isShowDismissPopup.toggle()}, - titleSecondButton: Text("dialog_ok"), - actionSecondButton: { - self.isShowDismissPopup.toggle() - if isShowEditContactFragment { - isShowEditContactFragment = false - } else { - isShowEditContactFragmentInContactDetails = false - } - }) + PopupView( + isShowPopup: $isShowDismissPopup, + title: Text("contact_editor_dialog_abort_confirmation_title"), + content: Text("contact_editor_dialog_abort_confirmation_message"), + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: Text("dialog_confirm"), + actionSecondButton: { + self.isShowDismissPopup.toggle() + if isShowEditContactFragment { + isShowEditContactFragment = false + } else { + isShowEditContactFragmentInContactDetails = false + } + }, + titleThirdButton: Text("dialog_cancel"), + actionThirdButton: { self.isShowDismissPopup.toggle() } + ) .background(.black.opacity(0.65)) .zIndex(3) .onTapGesture { @@ -1294,21 +1304,25 @@ struct ContentView: View { } if let meetingsListVM = meetingsListViewModel, isShowSendCancelMeetingNotificationPopup { - PopupView(isShowPopup: $isShowSendCancelMeetingNotificationPopup, - title: Text("meeting_schedule_cancel_dialog_title"), - content: !sharedMainViewModel.disableChatFeature ? Text("meeting_schedule_cancel_dialog_message") : Text(""), - titleFirstButton: Text("dialog_cancel"), - actionFirstButton: { - sharedMainViewModel.displayedMeeting = nil - meetingsListVM.deleteSelectedMeeting() - self.isShowSendCancelMeetingNotificationPopup.toggle( - ) }, - titleSecondButton: Text("dialog_ok"), - actionSecondButton: { - sharedMainViewModel.displayedMeeting = nil - meetingsListVM.cancelMeetingWithNotifications() - self.isShowSendCancelMeetingNotificationPopup.toggle() - }) + PopupView( + isShowPopup: $isShowSendCancelMeetingNotificationPopup, + title: Text("meeting_schedule_cancel_dialog_title"), + content: !sharedMainViewModel.disableChatFeature ? Text("meeting_schedule_cancel_dialog_message") : Text(""), + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: Text("dialog_confirm"), + actionSecondButton: { + sharedMainViewModel.displayedMeeting = nil + meetingsListVM.cancelMeetingWithNotifications() + self.isShowSendCancelMeetingNotificationPopup.toggle() + }, + titleThirdButton: Text("dialog_cancel"), + actionThirdButton: { + sharedMainViewModel.displayedMeeting = nil + meetingsListVM.deleteSelectedMeeting() + self.isShowSendCancelMeetingNotificationPopup.toggle() + } + ) .background(.black.opacity(0.65)) .zIndex(3) .onTapGesture { @@ -1321,17 +1335,17 @@ struct ContentView: View { 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_ok"), + titleFirstButton: nil, + actionFirstButton: {}, + titleSecondButton: Text("dialog_confirm"), actionSecondButton: { if sharedMainViewModel.displayedConversation != nil { sharedMainViewModel.displayedConversation!.createGroupCall() } self.isShowStartCallGroupPopup.toggle() - } + }, + titleThirdButton: Text("dialog_cancel"), + actionThirdButton: { self.isShowStartCallGroupPopup.toggle() } ) .background(.black.opacity(0.65)) .zIndex(3) @@ -1340,6 +1354,32 @@ struct ContentView: View { } } + /* + if isShowStartCallGroupPopup { + 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"), + actionSecondButton: { + if sharedMainViewModel.displayedConversation != nil { + sharedMainViewModel.displayedConversation!.createGroupCall() + } + self.isShowStartCallGroupPopup.toggle() + }, + titleThirdButton: Text("dialog_cancel"), + actionThirdButton: { self.isShowStartCallGroupPopup.toggle() } + ) + .background(.black.opacity(0.65)) + .zIndex(3) + .onTapGesture { + self.isShowStartCallGroupPopup.toggle() + } + } + */ + if isShowConversationInfoPopup { PopupViewWithTextField( isShowConversationInfoPopup: $isShowConversationInfoPopup, diff --git a/Linphone/UI/Main/Conversations/Fragments/StartGroupConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/StartGroupConversationFragment.swift index 1edbaf478..6963e60c3 100644 --- a/Linphone/UI/Main/Conversations/Fragments/StartGroupConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/StartGroupConversationFragment.swift @@ -111,7 +111,7 @@ struct StartGroupConversationFragment: View { Button(action: { startConversationViewModel.createGroupChatRoom() }, label: { - Text("dialog_ok") + Text("dialog_confirm") .default_text_style_white_600(styleSize: 20) .frame(height: 35) .frame(maxWidth: .infinity) diff --git a/Linphone/UI/Main/Fragments/PopupUpdatePassword.swift b/Linphone/UI/Main/Fragments/PopupUpdatePassword.swift index d7ca7d013..d61e153d6 100644 --- a/Linphone/UI/Main/Fragments/PopupUpdatePassword.swift +++ b/Linphone/UI/Main/Fragments/PopupUpdatePassword.swift @@ -105,7 +105,7 @@ struct PopupUpdatePassword: View { updateAuthInfo() isShowUpdatePasswordPopup = false }, label: { - Text("dialog_ok") + Text("dialog_confirm") .default_text_style_white_600(styleSize: 20) .frame(height: 35) .frame(maxWidth: .infinity) diff --git a/Linphone/UI/Main/Fragments/PopupView.swift b/Linphone/UI/Main/Fragments/PopupView.swift index a1bd4ac5a..784ed58ef 100644 --- a/Linphone/UI/Main/Fragments/PopupView.swift +++ b/Linphone/UI/Main/Fragments/PopupView.swift @@ -34,6 +34,9 @@ struct PopupView: View { var titleSecondButton: Text? var actionSecondButton: () -> Void + var titleThirdButton: Text? + var actionThirdButton: () -> Void + var body: some View { GeometryReader { geometry in VStack(alignment: .leading) { @@ -49,40 +52,57 @@ struct PopupView: View { .padding(.bottom, 20) } - if titleFirstButton != nil { - Button(action: { - actionFirstButton() - }, label: { - titleFirstButton - .default_text_style_orange_600(styleSize: 20) - .frame(height: 35) - .frame(maxWidth: .infinity) - }) - .padding(.horizontal, 20) - .padding(.vertical, 10) - .cornerRadius(60) - .overlay( - RoundedRectangle(cornerRadius: 60) - .inset(by: 0.5) - .stroke(Color.orangeMain500, lineWidth: 1) - ) - .padding(.bottom, 10) - } - - if titleSecondButton != nil { - Button(action: { - actionSecondButton() - }, label: { - titleSecondButton - .default_text_style_white_600(styleSize: 20) - .frame(height: 35) - .frame(maxWidth: .infinity) - }) - .padding(.horizontal, 20) - .padding(.vertical, 10) - .background(Color.orangeMain500) - .cornerRadius(60) + HStack { + if titleFirstButton != nil { + Button(action: { + actionFirstButton() + }, label: { + titleFirstButton + .default_text_style_white_600(styleSize: 14) + .frame(height: 30) + }) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background(Color.orangeMain500) + .cornerRadius(60) + .padding(.horizontal, 2) + } + + if titleSecondButton != nil { + Button(action: { + actionSecondButton() + }, label: { + titleSecondButton + .default_text_style_white_600(styleSize: 14) + .frame(height: 30) + }) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .background(Color.orangeMain500) + .cornerRadius(60) + .padding(.horizontal, 2) + } + + if titleThirdButton != nil { + Button(action: { + actionThirdButton() + }, label: { + titleThirdButton + .default_text_style_orange_600(styleSize: 14) + .frame(height: 30) + }) + .padding(.horizontal, 20) + .padding(.vertical, 10) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(Color.orangeMain500, lineWidth: 1) + ) + .padding(.horizontal, 2) + } } + .frame(maxWidth: .infinity, alignment: .trailing) } .padding(.horizontal, 20) .padding(.vertical, 20) @@ -101,9 +121,11 @@ struct PopupView: View { PopupView(isShowPopup: .constant(true), title: Text("Title"), content: Text("Content"), - titleFirstButton: Text("Deny all"), + titleFirstButton: Text("Accept all"), actionFirstButton: {}, - titleSecondButton: Text("Accept all"), - actionSecondButton: {}) + titleSecondButton: Text("dialog_confirm"), + actionSecondButton: {}, + titleThirdButton: Text("dialog_cancel"), + actionThirdButton: {}) .background(.black.opacity(0.65)) } diff --git a/Linphone/UI/Main/Fragments/PopupViewWithTextField.swift b/Linphone/UI/Main/Fragments/PopupViewWithTextField.swift index 58d22c217..7f8f58bfc 100644 --- a/Linphone/UI/Main/Fragments/PopupViewWithTextField.swift +++ b/Linphone/UI/Main/Fragments/PopupViewWithTextField.swift @@ -72,7 +72,7 @@ struct PopupViewWithTextField: View { setNewChatRoomSubject() isShowConversationInfoPopup = false }, label: { - Text("dialog_ok") + Text("dialog_confirm") .default_text_style_white_600(styleSize: 20) .frame(height: 35) .frame(maxWidth: .infinity) diff --git a/Linphone/UI/Main/Help/Fragments/HelpFragment.swift b/Linphone/UI/Main/Help/Fragments/HelpFragment.swift index 18c55d618..052dc0103 100644 --- a/Linphone/UI/Main/Help/Fragments/HelpFragment.swift +++ b/Linphone/UI/Main/Help/Fragments/HelpFragment.swift @@ -306,17 +306,17 @@ struct HelpFragment: View { isShowPopup: $helpViewModel.checkUpdateAvailable, title: Text("help_dialog_update_available_title"), content: Text(String(format: String(localized: "help_dialog_update_available_message"), helpViewModel.versionAvailable)), - titleFirstButton: Text("dialog_cancel"), - actionFirstButton: { - helpViewModel.checkUpdateAvailable = false - }, + titleFirstButton: nil, + actionFirstButton: {}, titleSecondButton: Text("dialog_install"), actionSecondButton: { helpViewModel.checkUpdateAvailable = false if let url = URL(string: helpViewModel.urlVersionAvailable) { UIApplication.shared.open(url) } - } + }, + titleThirdButton: Text("dialog_cancel"), + actionThirdButton: { helpViewModel.checkUpdateAvailable = false } ) .background(.black.opacity(0.65)) .zIndex(3) diff --git a/Linphone/UI/Main/Meetings/Fragments/ScheduleMeetingFragment.swift b/Linphone/UI/Main/Meetings/Fragments/ScheduleMeetingFragment.swift index f5c9d53f8..41db8fc3e 100644 --- a/Linphone/UI/Main/Meetings/Fragments/ScheduleMeetingFragment.swift +++ b/Linphone/UI/Main/Meetings/Fragments/ScheduleMeetingFragment.swift @@ -468,7 +468,7 @@ struct ScheduleMeetingFragment: View { showDatePicker.toggle() } } - Text("dialog_ok") + Text("dialog_confirm") .default_text_style_orange_500(styleSize: 16) .onTapGesture { pickDate() diff --git a/Linphone/UI/Main/Settings/Fragments/AccountProfileFragment.swift b/Linphone/UI/Main/Settings/Fragments/AccountProfileFragment.swift index 2485681a0..90d4fbd6b 100644 --- a/Linphone/UI/Main/Settings/Fragments/AccountProfileFragment.swift +++ b/Linphone/UI/Main/Settings/Fragments/AccountProfileFragment.swift @@ -606,15 +606,16 @@ struct AccountProfileFragment: View { .background(Color.gray100) if self.isShowPopup { - PopupView(isShowPopup: $isShowPopup, + PopupView( + isShowPopup: $isShowPopup, title: Text("manage_account_international_prefix"), content: Text("manage_account_dialog_international_prefix_help_message"), titleFirstButton: nil, actionFirstButton: {}, - titleSecondButton: Text("dialog_ok"), - actionSecondButton: { - self.isShowPopup.toggle() - } + titleSecondButton: Text("dialog_confirm"), + actionSecondButton: { self.isShowPopup.toggle() }, + titleThirdButton: nil, + actionThirdButton: {} ) .background(.black.opacity(0.65)) .onTapGesture { @@ -635,10 +636,8 @@ struct AccountProfileFragment: View { isShowPopup: $isShowLogoutPopup, title: Text("manage_account_dialog_remove_account_title"), content: contentPopup1 + contentPopup2, - titleFirstButton: Text("dialog_cancel"), - actionFirstButton: { - self.isShowLogoutPopup.toggle() - }, + titleFirstButton: nil, + actionFirstButton: {}, titleSecondButton: Text("manage_account_delete"), actionSecondButton: { if accountProfileViewModel.accountModelIndex != nil { @@ -650,7 +649,9 @@ struct AccountProfileFragment: View { } } } - } + }, + titleThirdButton: Text("dialog_cancel"), + actionThirdButton: { self.isShowLogoutPopup.toggle() } ) .background(.black.opacity(0.65)) .onTapGesture { From 0daba4fe03db480a99bf9450a25c570cd257cb76 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 20 Nov 2025 16:48:38 +0100 Subject: [PATCH 08/13] New message deletion feature --- .../Localizable/cs.lproj/Localizable.strings | 6 +- .../Localizable/de.lproj/Localizable.strings | 6 +- .../Localizable/en.lproj/Localizable.strings | 11 ++- .../Localizable/fr.lproj/Localizable.strings | 11 ++- .../Localizable/uk.lproj/Localizable.strings | 6 +- Linphone/UI/Main/ContentView.swift | 31 +++--- .../Fragments/ChatBubbleView.swift | 14 ++- .../Fragments/ConversationFragment.swift | 87 +++++++++------- .../Fragments/ConversationsListFragment.swift | 28 ++++-- .../Main/Conversations/Fragments/UIList.swift | 6 ++ .../Model/ConversationModel.swift | 15 +++ .../UI/Main/Conversations/Model/Message.swift | 26 ++++- .../ViewModel/ConversationViewModel.swift | 99 +++++++++++++++++-- .../ConversationsListViewModel.swift | 12 ++- 14 files changed, 272 insertions(+), 86 deletions(-) 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 { From 7bdb8fa92dc7454ace8eaa1f90a7819395061729 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Wed, 26 Nov 2025 09:47:16 +0100 Subject: [PATCH 09/13] Update last message text in conversation list --- .../calendar.imageset/calendar.svg | 2 +- .../trash.imageset/Contents.json | 21 +++++++++ .../Assets.xcassets/trash.imageset/trash.svg | 1 + .../Fragments/ConversationsListFragment.swift | 46 +++++++++++++------ .../Model/ConversationModel.swift | 28 +++++++++-- .../ConversationsListViewModel.swift | 24 ++++------ 6 files changed, 89 insertions(+), 33 deletions(-) create mode 100644 Linphone/Assets.xcassets/trash.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/trash.imageset/trash.svg diff --git a/Linphone/Assets.xcassets/calendar.imageset/calendar.svg b/Linphone/Assets.xcassets/calendar.imageset/calendar.svg index 5caacdbef..c066f4a4b 100644 --- a/Linphone/Assets.xcassets/calendar.imageset/calendar.svg +++ b/Linphone/Assets.xcassets/calendar.imageset/calendar.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/Linphone/Assets.xcassets/trash.imageset/Contents.json b/Linphone/Assets.xcassets/trash.imageset/Contents.json new file mode 100644 index 000000000..4c3681644 --- /dev/null +++ b/Linphone/Assets.xcassets/trash.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "trash.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/trash.imageset/trash.svg b/Linphone/Assets.xcassets/trash.imageset/trash.svg new file mode 100644 index 000000000..b2d5dfe18 --- /dev/null +++ b/Linphone/Assets.xcassets/trash.imageset/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift index 40bde0799..d39a0835d 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationsListFragment.swift @@ -105,26 +105,46 @@ struct ConversationRow: View { .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) + HStack(spacing: 0) { + Text(conversation.lastMessagePrefixText) .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) + .layoutPriority(1) + + if !conversation.lastMessageIcon.isEmpty { + Image(conversation.lastMessageIcon) + .resizable() + .frame(width: 16, height: 16) + .layoutPriority(0) + .padding(.trailing, 2) + } + + if conversation.lastMessageInItalic { + Text(conversation.lastMessageText) + .italic() + .if(conversation.unreadMessagesCount > 0) { view in + view.bold() + } + .foregroundStyle(Color.grayMain2c400) + .font(.system(size: 14)) + .lineLimit(1) + .layoutPriority(-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) + .lineLimit(1) + .layoutPriority(-1) + } } + .frame(maxWidth: .infinity, alignment: .leading) Spacer() } diff --git a/Linphone/UI/Main/Conversations/Model/ConversationModel.swift b/Linphone/UI/Main/Conversations/Model/ConversationModel.swift index a878debe0..8b5bca791 100644 --- a/Linphone/UI/Main/Conversations/Model/ConversationModel.swift +++ b/Linphone/UI/Main/Conversations/Model/ConversationModel.swift @@ -46,7 +46,9 @@ class ConversationModel: ObservableObject, Identifiable { @Published var isMuted: Bool @Published var isEphemeral: Bool @Published var encryptionEnabled: Bool + @Published var lastMessagePrefixText: String @Published var lastMessageText: String + @Published var lastMessageIcon: String @Published var lastMessageIsOutgoing: Bool @Published var lastMessageState: Int @Published var lastMessageInItalic: Bool @@ -139,8 +141,12 @@ class ConversationModel: ObservableObject, Identifiable { self.lastMessage = nil + self.lastMessagePrefixText = "" + self.lastMessageText = "" + self.lastMessageIcon = "" + self.lastMessageIsOutgoing = false self.lastMessageState = 0 @@ -297,9 +303,9 @@ class ConversationModel: ObservableObject, Identifiable { fromAddressFriend = nil } - var lastMessageTextTmp = (fromAddressFriend ?? "") - + (lastMessage!.contents.first(where: {$0.isText == true})?.utf8Text ?? (lastMessage!.contents.first(where: {$0.isFile == true || $0.isFileTransfer == true})?.name ?? "")) - + let lastMessagePrefixTextTmp = (fromAddressFriend ?? "") + var lastMessageTextTmp = (lastMessage!.contents.first(where: {$0.isText == true})?.utf8Text ?? (lastMessage!.contents.first(where: {$0.isFile == true || $0.isFileTransfer == true})?.name ?? "")) + var lastMessageIconTmp = "" var lastMessageInItalicTmp = false if lastMessage!.contents.first != nil && lastMessage!.contents.first!.isIcalendar == true { @@ -314,6 +320,8 @@ class ConversationModel: ObservableObject, Identifiable { lastMessageTextTmp = String(localized: "message_meeting_invitation_cancelled_notification") } + lastMessageIconTmp = "calendar" + lastMessageInItalicTmp = true } } @@ -322,9 +330,19 @@ class ConversationModel: ObservableObject, Identifiable { if lastMessage!.isRetracted { lastMessageTextTmp += lastMessage!.isOutgoing ? String(localized: "conversation_message_content_deleted_by_us_label") : String(localized: "conversation_message_content_deleted_label") + lastMessageIconTmp = "trash" + lastMessageInItalicTmp = true } + if (lastMessage!.contents.first(where: {$0.isFile == true || $0.isFileTransfer == true})?.name != nil) { + lastMessageIconTmp = "file" + } else if lastMessage!.isReply { + lastMessageIconTmp = "reply" + } else if lastMessage!.isForward { + lastMessageIconTmp = "forward" + } + let lastMessageIsOutgoingTmp = lastMessage?.isOutgoing ?? false let lastUpdateTimeTmp = lastMessage?.time ?? chatRoom.lastUpdateTime @@ -332,7 +350,11 @@ class ConversationModel: ObservableObject, Identifiable { let lastMessageStateTmp = lastMessage?.state.rawValue ?? 0 DispatchQueue.main.async { + self.lastMessagePrefixText = lastMessagePrefixTextTmp + self.lastMessageText = lastMessageTextTmp + + self.lastMessageIcon = lastMessageIconTmp self.lastMessageIsOutgoing = lastMessageIsOutgoingTmp diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift index 6b42a64d8..49de08869 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift @@ -89,20 +89,16 @@ class ConversationsListViewModel: ObservableObject { fromAddressFriend = nil } - 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") - } + let lastMessagePrefixTextTmp = (fromAddressFriend ?? "") if let index = self.conversationsList.firstIndex(where: { $0.chatRoom === conversationModel.chatRoom }) { DispatchQueue.main.async { - conversationModel.lastMessageText = lastMessageTextTmp - self.conversationsList[index].lastMessageText = lastMessageTextTmp + conversationModel.lastMessagePrefixText = lastMessagePrefixTextTmp + self.conversationsList[index].lastMessagePrefixText = lastMessagePrefixTextTmp } } else { DispatchQueue.main.async { - conversationModel.lastMessageText = lastMessageTextTmp + conversationModel.lastMessagePrefixText = lastMessagePrefixTextTmp } } } @@ -152,20 +148,16 @@ class ConversationsListViewModel: ObservableObject { fromAddressFriend = nil } - 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") - } + let lastMessagePrefixTextTmp = (fromAddressFriend ?? "") if let index = self.conversationsList.firstIndex(where: { $0.chatRoom === conversationModel.chatRoom }) { DispatchQueue.main.async { - conversationModel.lastMessageText = lastMessageTextTmp - self.conversationsList[index].lastMessageText = lastMessageTextTmp + conversationModel.lastMessagePrefixText = lastMessagePrefixTextTmp + self.conversationsList[index].lastMessagePrefixText = lastMessagePrefixTextTmp } } else { DispatchQueue.main.async { - conversationModel.lastMessageText = lastMessageTextTmp + conversationModel.lastMessagePrefixText = lastMessagePrefixTextTmp } } } From 5d13a2b49d50b6f78fa6c79c3d7fd926a3f17c4f Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Wed, 26 Nov 2025 10:32:39 +0100 Subject: [PATCH 10/13] Stop composing when the user stops typing --- .../Fragments/ConversationFragment.swift | 12 ++++++------ .../ViewModel/ConversationViewModel.swift | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index 9bb39c865..8faf53e6f 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -234,6 +234,8 @@ struct ConversationFragment: View { if SharedMainViewModel.shared.displayedConversation != nil && (navigationManager.peerAddr == nil || navigationManager.peerAddr!.contains(SharedMainViewModel.shared.displayedConversation!.remoteSipUri)) { conversationViewModel.resetDisplayedChatRoom() } + } else { + conversationViewModel.compose(stop: true, cachedConversation: cachedConversation) } } } @@ -461,6 +463,7 @@ struct ConversationFragment: View { } } .onDisappear { + conversationViewModel.compose(stop: true, cachedConversation: cachedConversation) conversationViewModel.resetMessage() } } else { @@ -556,6 +559,7 @@ struct ConversationFragment: View { conversationViewModel.getMessages() } .onDisappear { + conversationViewModel.compose(stop: true, cachedConversation: cachedConversation) conversationViewModel.resetMessage() } } @@ -885,9 +889,7 @@ struct ConversationFragment: View { .focused($isMessageTextFocused) .padding(.vertical, 5) .onChange(of: messageText) { text in - if !text.isEmpty { - conversationViewModel.compose() - } + conversationViewModel.compose(stop: text.isEmpty) } } else { ZStack(alignment: .leading) { @@ -898,9 +900,7 @@ struct ConversationFragment: View { .default_text_style(styleSize: 15) .focused($isMessageTextFocused) .onChange(of: messageText) { text in - if !text.isEmpty { - conversationViewModel.compose() - } + conversationViewModel.compose(stop: text.isEmpty) } if messageText.isEmpty { diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift index 88e9df3d5..d944cadc2 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationViewModel.swift @@ -2608,10 +2608,20 @@ class ConversationViewModel: ObservableObject { } } - func compose() { + func compose(stop: Bool, cachedConversation: ConversationModel? = nil) { coreContext.doOnCoreQueue { _ in - if self.sharedMainViewModel.displayedConversation != nil { - self.sharedMainViewModel.displayedConversation!.chatRoom.compose() + if let displayedConversation = self.sharedMainViewModel.displayedConversation { + if stop { + displayedConversation.chatRoom.stopComposing() + } else { + displayedConversation.chatRoom.composeTextMessage() + } + } else if let displayedConversation = cachedConversation { + if stop { + displayedConversation.chatRoom.stopComposing() + } else { + displayedConversation.chatRoom.composeTextMessage() + } } } } From 773102e4bd4435bd39c270bd449f0d045075f6e8 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 1 Dec 2025 13:45:49 +0100 Subject: [PATCH 11/13] Fix the French translation of message_content_deleted --- .../Localizable/fr.lproj/Localizable.strings | 4 ++-- .../Conversations/Model/ConversationModel.swift | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index 2d10e1209..d9b064b5d 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -209,8 +209,8 @@ "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_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."; diff --git a/Linphone/UI/Main/Conversations/Model/ConversationModel.swift b/Linphone/UI/Main/Conversations/Model/ConversationModel.swift index 8b5bca791..584e1c7d9 100644 --- a/Linphone/UI/Main/Conversations/Model/ConversationModel.swift +++ b/Linphone/UI/Main/Conversations/Model/ConversationModel.swift @@ -327,14 +327,6 @@ class ConversationModel: ObservableObject, Identifiable { } } - if lastMessage!.isRetracted { - lastMessageTextTmp += lastMessage!.isOutgoing ? String(localized: "conversation_message_content_deleted_by_us_label") : String(localized: "conversation_message_content_deleted_label") - - lastMessageIconTmp = "trash" - - lastMessageInItalicTmp = true - } - if (lastMessage!.contents.first(where: {$0.isFile == true || $0.isFileTransfer == true})?.name != nil) { lastMessageIconTmp = "file" } else if lastMessage!.isReply { @@ -343,6 +335,14 @@ class ConversationModel: ObservableObject, Identifiable { lastMessageIconTmp = "forward" } + if lastMessage!.isRetracted { + lastMessageTextTmp += lastMessage!.isOutgoing ? String(localized: "conversation_message_content_deleted_by_us_label") : String(localized: "conversation_message_content_deleted_label") + + lastMessageIconTmp = "trash" + + lastMessageInItalicTmp = true + } + let lastMessageIsOutgoingTmp = lastMessage?.isOutgoing ?? false let lastUpdateTimeTmp = lastMessage?.time ?? chatRoom.lastUpdateTime From 5492a3e3a99cb0c8000cf721a928c4530f460b92 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 1 Dec 2025 14:33:59 +0100 Subject: [PATCH 12/13] Update the unread message counter in onMessageRetracted --- .../ConversationsListViewModel.swift | 129 +++++++++--------- Linphone/UI/Main/Viewmodel/AccountModel.swift | 2 + 2 files changed, 70 insertions(+), 61 deletions(-) diff --git a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift index 49de08869..41e30f53e 100644 --- a/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift +++ b/Linphone/UI/Main/Conversations/ViewModel/ConversationsListViewModel.swift @@ -183,69 +183,76 @@ class ConversationsListViewModel: ObservableObject { func addConversationDelegate() { coreContext.doOnCoreQueue { core in - self.coreConversationDelegate = CoreDelegateStub(onMessagesReceived: { (core: Core, chatRoom: ChatRoom, _: [ChatMessage]) in - if let defaultAddress = core.defaultAccount?.contactAddress, - let localAddress = chatRoom.localAddress, - defaultAddress.weakEqual(address2: localAddress) { - let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom) - let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom) - model.getContentTextMessage(chatRoom: chatRoom) - let index = self.conversationsList.firstIndex(where: { $0.id == idTmp }) - DispatchQueue.main.async { - if index != nil { - self.conversationsList.remove(at: index!) - } - self.conversationsList.insert(model, at: 0) - } - SharedMainViewModel.shared.updateUnreadMessagesCount() - } - }, onMessageSent: { (_: Core, chatRoom: ChatRoom, _: ChatMessage) in - if let defaultAddress = core.defaultAccount?.contactAddress, - let localAddress = chatRoom.localAddress, - defaultAddress.weakEqual(address2: localAddress) { - let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom) - let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom) - model.getContentTextMessage(chatRoom: chatRoom) - let index = self.conversationsList.firstIndex(where: { $0.id == idTmp }) - if index != nil { - self.conversationsList[index!].chatMessageRemoveDelegate() - } - DispatchQueue.main.async { - if index != nil { - self.conversationsList.remove(at: index!) - } - self.conversationsList.insert(model, at: 0) - } - SharedMainViewModel.shared.updateUnreadMessagesCount() - } - }, onChatRoomRead: { (_: Core, chatRoom: ChatRoom) in - let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom) - let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom) - model.getContentTextMessage(chatRoom: chatRoom) - if let index = self.conversationsList.firstIndex(where: { $0.id == idTmp }) { - DispatchQueue.main.async { - self.conversationsList.remove(at: index) - self.conversationsList.insert(model, at: index) + self.coreConversationDelegate = CoreDelegateStub( + onMessagesReceived: { (core: Core, chatRoom: ChatRoom, _: [ChatMessage]) in + if let defaultAddress = core.defaultAccount?.contactAddress, + let localAddress = chatRoom.localAddress, + defaultAddress.weakEqual(address2: localAddress) { + let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom) + let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom) + model.getContentTextMessage(chatRoom: chatRoom) + let index = self.conversationsList.firstIndex(where: { $0.id == idTmp }) + DispatchQueue.main.async { + if index != nil { + self.conversationsList.remove(at: index!) + } + self.conversationsList.insert(model, at: 0) + } + SharedMainViewModel.shared.updateUnreadMessagesCount() } + }, onMessageSent: { (_: Core, chatRoom: ChatRoom, _: ChatMessage) in + if let defaultAddress = core.defaultAccount?.contactAddress, + let localAddress = chatRoom.localAddress, + defaultAddress.weakEqual(address2: localAddress) { + let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom) + let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom) + model.getContentTextMessage(chatRoom: chatRoom) + let index = self.conversationsList.firstIndex(where: { $0.id == idTmp }) + if index != nil { + self.conversationsList[index!].chatMessageRemoveDelegate() + } + DispatchQueue.main.async { + if index != nil { + self.conversationsList.remove(at: index!) + } + self.conversationsList.insert(model, at: 0) + } + SharedMainViewModel.shared.updateUnreadMessagesCount() + } + }, onChatRoomRead: { (_: Core, chatRoom: ChatRoom) in + let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom) + let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom) + model.getContentTextMessage(chatRoom: chatRoom) + if let index = self.conversationsList.firstIndex(where: { $0.id == idTmp }) { + DispatchQueue.main.async { + self.conversationsList.remove(at: index) + self.conversationsList.insert(model, at: index) + } + } + SharedMainViewModel.shared.updateUnreadMessagesCount() + }, onChatRoomStateChanged: { (core: Core, chatroom: ChatRoom, state: ChatRoom.State) in + // Log.info("[ConversationsListViewModel] Conversation [${LinphoneUtils.getChatRoomId(chatRoom)}] state changed [$state]") + if let defaultAddress = core.defaultAccount?.contactAddress, + let localAddress = chatroom.localAddress, + defaultAddress.weakEqual(address2: localAddress) { + if core.globalState == .On { + switch state { + case .Created: + self.addChatRoom(chatRoom: chatroom) + case .Deleted: + self.removeChatRoom(chatRoom: chatroom) + default: + break + } + } + } + }, onMessageRetracted: { (core: Core, chatRoom: ChatRoom, message: ChatMessage) in + let idTmp = LinphoneUtils.getChatRoomId(room: chatRoom) + let model = self.conversationsList.first(where: { $0.id == idTmp }) ?? ConversationModel(chatRoom: chatRoom) + model.getContentTextMessage(chatRoom: chatRoom) + SharedMainViewModel.shared.updateUnreadMessagesCount() } - SharedMainViewModel.shared.updateUnreadMessagesCount() - }, onChatRoomStateChanged: { (core: Core, chatroom: ChatRoom, state: ChatRoom.State) in - // Log.info("[ConversationsListViewModel] Conversation [${LinphoneUtils.getChatRoomId(chatRoom)}] state changed [$state]") - if let defaultAddress = core.defaultAccount?.contactAddress, - let localAddress = chatroom.localAddress, - defaultAddress.weakEqual(address2: localAddress) { - if core.globalState == .On { - switch state { - case .Created: - self.addChatRoom(chatRoom: chatroom) - case .Deleted: - self.removeChatRoom(chatRoom: chatroom) - default: - break - } - } - } - }) + ) core.addDelegate(delegate: self.coreConversationDelegate!) } } diff --git a/Linphone/UI/Main/Viewmodel/AccountModel.swift b/Linphone/UI/Main/Viewmodel/AccountModel.swift index 484f5ab3e..ae363b49a 100644 --- a/Linphone/UI/Main/Viewmodel/AccountModel.swift +++ b/Linphone/UI/Main/Viewmodel/AccountModel.swift @@ -65,6 +65,8 @@ class AccountModel: ObservableObject { self.computeNotificationsCount() }, onChatRoomRead: { (_: Core, _: ChatRoom) in self.computeNotificationsCount() + }, onMessageRetracted: { (_: Core, _: ChatRoom, _: ChatMessage) in + self.computeNotificationsCount() }) core.addDelegate(delegate: coreDelegate!) From 2b64c26518efa5bbbbf13e4fa98d714d1cc45024 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 1 Dec 2025 15:12:03 +0100 Subject: [PATCH 13/13] Add automatic Git commit, branch, and tag info for Help views --- Linphone/Core/CoreContext.swift | 16 +++++++++--- Linphone/GeneratedGit.swift | 5 ++++ .../UI/Main/Help/Fragments/HelpFragment.swift | 2 +- .../Main/Help/ViewModel/HelpViewModel.swift | 17 ++++++++---- LinphoneApp.xcodeproj/project.pbxproj | 26 +++++++++++++++++-- .../xcshareddata/swiftpm/Package.resolved | 2 +- 6 files changed, 56 insertions(+), 12 deletions(-) create mode 100644 Linphone/GeneratedGit.swift diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index c2cd38132..b24ef3f8c 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -148,11 +148,21 @@ class CoreContext: ObservableObject { self.mCore.callkitEnabled = true self.mCore.pushNotificationEnabled = true - let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + let appGitVersion = APP_GIT_COMMIT + let appGitBranch = APP_GIT_BRANCH + let appGitTag = APP_GIT_TAG + let sdkGitVersion = linphonesw.sdkVersion + var sdkGitBranch = linphonesw.sdkBranch - let userAgent = "LinphoneiOS/\(version ?? "6.0.0") (\(UIDevice.current.localizedModel.replacingOccurrences(of: "'", with: ""))) LinphoneSDK" + if sdkGitBranch.hasPrefix("remotes/origin/") { + sdkGitBranch = String(sdkGitBranch.dropFirst("remotes/origin/".count)) + } + + Log.info("Git Info — App: \(appGitTag + "-" + appGitVersion) [\(appGitBranch)] | SDK: \(sdkGitVersion) [\(sdkGitBranch)]") + + let userAgent = "LinphoneiOS/\(appGitTag) (\(UIDevice.current.localizedModel.replacingOccurrences(of: "'", with: ""))) LinphoneSDK" self.mCore.setUserAgent(name: userAgent, version: self.coreVersion) + self.mCore.videoCaptureEnabled = true self.mCore.videoDisplayEnabled = true self.mCore.videoPreviewEnabled = false diff --git a/Linphone/GeneratedGit.swift b/Linphone/GeneratedGit.swift new file mode 100644 index 000000000..e6941c350 --- /dev/null +++ b/Linphone/GeneratedGit.swift @@ -0,0 +1,5 @@ +import Foundation + +public let APP_GIT_BRANCH = "master" +public let APP_GIT_COMMIT = "5492a3e3a" +public let APP_GIT_TAG = "6.1.0-alpha" diff --git a/Linphone/UI/Main/Help/Fragments/HelpFragment.swift b/Linphone/UI/Main/Help/Fragments/HelpFragment.swift index 052dc0103..92f3da5f6 100644 --- a/Linphone/UI/Main/Help/Fragments/HelpFragment.swift +++ b/Linphone/UI/Main/Help/Fragments/HelpFragment.swift @@ -181,7 +181,7 @@ struct HelpFragment: View { .frame(maxWidth: .infinity, alignment: .leading) .multilineTextAlignment(.leading) - Text(helpViewModel.version) + Text(helpViewModel.appVersion) .default_text_style(styleSize: 14) .frame(maxWidth: .infinity, alignment: .leading) .multilineTextAlignment(.leading) diff --git a/Linphone/UI/Main/Help/ViewModel/HelpViewModel.swift b/Linphone/UI/Main/Help/ViewModel/HelpViewModel.swift index 75d4b0456..f8d71ad84 100644 --- a/Linphone/UI/Main/Help/ViewModel/HelpViewModel.swift +++ b/Linphone/UI/Main/Help/ViewModel/HelpViewModel.swift @@ -38,13 +38,20 @@ class HelpViewModel: ObservableObject { private var coreDelegate: CoreDelegate? init() { - let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String - let versionTmp = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String - let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String + let appGitVersion = APP_GIT_COMMIT + let appGitBranch = APP_GIT_BRANCH + let appGitTag = APP_GIT_TAG + let sdkGitVersion = linphonesw.sdkVersion + var sdkGitBranch = linphonesw.sdkBranch - self.version = (versionTmp ?? "6.0.0") + if sdkGitBranch.hasPrefix("remotes/origin/") { + sdkGitBranch = String(sdkGitBranch.dropFirst("remotes/origin/".count)) + } - self.sdkVersion = Core.getVersion + self.appVersion = appGitTag + self.version = appGitTag + "-" + appGitVersion + "\n(\(appGitBranch))" + + self.sdkVersion = sdkGitVersion + "\n(\(sdkGitBranch))" if let path = Bundle.main.path(forResource: "GoogleService-Info", ofType: "plist"), let plist = NSDictionary(contentsOfFile: path) as? [String: Any], diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj index d958e07c6..e972a7bac 100644 --- a/LinphoneApp.xcodeproj/project.pbxproj +++ b/LinphoneApp.xcodeproj/project.pbxproj @@ -175,6 +175,7 @@ D7CEE03B2B7A234200FD79B7 /* ConversationsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */; }; D7CEE03D2B7A23B200FD79B7 /* ConversationsListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */; }; D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */; }; + D7D1F5452EDDBBA70034EEB0 /* GeneratedGit.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D1F5442EDDBBA70034EEB0 /* GeneratedGit.swift */; }; D7D24D132AC1B4E800C6F35B /* NotoSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */; }; D7D24D142AC1B4E800C6F35B /* NotoSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */; }; D7D24D152AC1B4E800C6F35B /* NotoSans-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = D7D24D0F2AC1B4E800C6F35B /* NotoSans-Light.ttf */; }; @@ -408,6 +409,7 @@ D7CEE03A2B7A234200FD79B7 /* ConversationsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsFragment.swift; sourceTree = ""; }; D7CEE03C2B7A23B200FD79B7 /* ConversationsListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationsListFragment.swift; sourceTree = ""; }; D7D1698B2AE66FA500109A5C /* MagicSearchSingleton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicSearchSingleton.swift; sourceTree = ""; }; + D7D1F5442EDDBBA70034EEB0 /* GeneratedGit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneratedGit.swift; sourceTree = ""; }; D7D24D0D2AC1B4E800C6F35B /* NotoSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Medium.ttf"; sourceTree = ""; }; D7D24D0E2AC1B4E800C6F35B /* NotoSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Regular.ttf"; sourceTree = ""; }; D7D24D0F2AC1B4E800C6F35B /* NotoSans-Light.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "NotoSans-Light.ttf"; sourceTree = ""; }; @@ -662,6 +664,7 @@ D719ABBD2ABC67BF00B41C10 /* Preview Content */, D7D24D0C2AC1B4C700C6F35B /* Fonts */, D7ADF6012AFE5C7C00212231 /* Ressources */, + D7D1F5442EDDBBA70034EEB0 /* GeneratedGit.swift */, ); path = Linphone; sourceTree = ""; @@ -1108,6 +1111,7 @@ 660AAF802B839272004C0FA6 /* Embed Foundation Extensions */, D719ABB12ABC67BF00B41C10 /* Resources */, D72F04F52DDB2CB800F4C713 /* ShellScript */, + D7D1F5432EDDB9C20034EEB0 /* ShellScript */, ); buildRules = ( ); @@ -1184,7 +1188,7 @@ D7D5AD7B2DD34E4D00016721 /* XCRemoteSwiftPackageReference "AppAuth-iOS" */, D7D5AD7C2DD34E7C00016721 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, D7DF8BE42E2104D0003A3BC7 /* XCRemoteSwiftPackageReference "Elegant-Emoji-Picker" */, - D7690B3B2EAF8878009CB3B7 /* XCRemoteSwiftPackageReference "linphone-sdk-swift-ios" */, + D7D1F5482EDDD8D30034EEB0 /* XCRemoteSwiftPackageReference "linphone-sdk-swift-ios" */, ); productRefGroup = D719ABB42ABC67BF00B41C10 /* Products */; projectDirPath = ""; @@ -1257,6 +1261,23 @@ shellPath = /bin/sh; shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run\"\n"; }; + D7D1F5432EDDB9C20034EEB0 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n#!/bin/bash\n\nbranch=$(git rev-parse --abbrev-ref HEAD)\ncommit=$(git rev-parse --short HEAD)\ntag=$(git describe --tags --abbrev=0 2>/dev/null || echo \"no-tag\")\n\ncat < \"$SRCROOT/Linphone/GeneratedGit.swift\"\nimport Foundation\n\npublic let APP_GIT_BRANCH = \"$branch\"\npublic let APP_GIT_COMMIT = \"$commit\"\npublic let APP_GIT_TAG = \"$tag\"\nEOF\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1348,6 +1369,7 @@ C628172E2C1C3A3600DBA646 /* AccountExtension.swift in Sources */, 66C491FF2B24D4AC00CEA16D /* FileUtils.swift in Sources */, C62817322C1C400A00DBA646 /* StringExtension.swift in Sources */, + D7D1F5452EDDBBA70034EEB0 /* GeneratedGit.swift in Sources */, D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */, D7B99E992B29B39000BE7BF2 /* CallViewModel.swift in Sources */, D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */, @@ -1932,7 +1954,7 @@ kind = branch; }; }; - D7690B3B2EAF8878009CB3B7 /* XCRemoteSwiftPackageReference "linphone-sdk-swift-ios" */ = { + D7D1F5482EDDD8D30034EEB0 /* XCRemoteSwiftPackageReference "linphone-sdk-swift-ios" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://gitlab.linphone.org/BC/public/linphone-sdk-swift-ios.git"; requirement = { diff --git a/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 91670816a..197fc9498 100644 --- a/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -124,7 +124,7 @@ "location" : "https://gitlab.linphone.org/BC/public/linphone-sdk-swift-ios.git", "state" : { "branch" : "alpha", - "revision" : "b92c41b87c69771ccd276a957ab02c20178dffeb" + "revision" : "43ee1a062ef73808e27afe3c5341a27c1b82aae7" } }, {