From f16a0f42ae9ca03d796a5282a1c978d98de721cb Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 9 Dec 2025 10:33:03 +0100 Subject: [PATCH] Add Message Waiting Indication --- .../voicemail.imageset/Contents.json | 21 ++++++ .../voicemail.imageset/voicemail.svg | 1 + Linphone/Core/CoreContext.swift | 25 +++++++ Linphone/GeneratedGit.swift | 4 +- .../Localizable/en.lproj/Localizable.strings | 2 + .../Localizable/fr.lproj/Localizable.strings | 2 + Linphone/UI/Main/ContentView.swift | 48 +++++++++++++ .../Main/Fragments/SideMenuAccountRow.swift | 69 +++++++++++++++---- Linphone/UI/Main/Viewmodel/AccountModel.swift | 62 +++++++++++++---- .../Main/Viewmodel/SharedMainViewModel.swift | 1 + 10 files changed, 204 insertions(+), 31 deletions(-) create mode 100644 Linphone/Assets.xcassets/voicemail.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/voicemail.imageset/voicemail.svg diff --git a/Linphone/Assets.xcassets/voicemail.imageset/Contents.json b/Linphone/Assets.xcassets/voicemail.imageset/Contents.json new file mode 100644 index 000000000..8a08ca7a8 --- /dev/null +++ b/Linphone/Assets.xcassets/voicemail.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "voicemail.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/voicemail.imageset/voicemail.svg b/Linphone/Assets.xcassets/voicemail.imageset/voicemail.svg new file mode 100644 index 000000000..11c1a213c --- /dev/null +++ b/Linphone/Assets.xcassets/voicemail.imageset/voicemail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index a63844bb7..e6bd371a8 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -437,6 +437,31 @@ class CoreContext: ObservableObject { DispatchQueue.main.async { self.accounts = accountModels } + }, onMessageWaitingIndicationChanged: { (core: Core, event: Event, mwi: MessageWaitingIndication) in + if (mwi.hasMessageWaiting()) { + let summaries = mwi.summaries + Log.info( + "[CoreContext][onMessageWaitingIndicationChanged] MWI NOTIFY received, messages are waiting (\(summaries.count) summaries)" + ) + + if let defaultAccount = core.defaultAccount?.params?.identityAddress, let mwiAccount = mwi.accountAddress, defaultAccount.weakEqual(address2: mwiAccount){ + if !summaries.isEmpty { + let summary = summaries.first + DispatchQueue.main.async { + withAnimation { + SharedMainViewModel.shared.waitingMessageCount = Int(summary?.nbNew ?? 0) + } + } + } + } + } else { + Log.info("[CoreContext][onMessageWaitingIndicationChanged] MWI NOTIFY received, no message is waiting") + DispatchQueue.main.async { + withAnimation { + SharedMainViewModel.shared.waitingMessageCount = 0 + } + } + } }) self.mCore.addDelegate(delegate: self.mCoreDelegate) diff --git a/Linphone/GeneratedGit.swift b/Linphone/GeneratedGit.swift index 8319ee0e5..cee2d6942 100644 --- a/Linphone/GeneratedGit.swift +++ b/Linphone/GeneratedGit.swift @@ -1,7 +1,7 @@ import Foundation public enum AppGitInfo { - public static let branch = "master" - public static let commit = "33b379285" + public static let branch = "feature/mwi" + public static let commit = "623d2e067" public static let tag = "6.1.0-alpha" } diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index dabf54ca1..297c15dc6 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -419,6 +419,8 @@ "message_meeting_invitation_updated_notification" = "Meeting has been updated"; "message_reaction_click_to_remove_label" = "Click to remove"; "message_reactions_info_all_title" = "Reactions"; +"mwi_messages_are_waiting_single" = "1 new voice message"; +"mwi_messages_are_waiting_multiple" = "%@ new voice messages"; "network_not_reachable" = "You aren't connected to internet"; "network_reachable_again" = "Network is now reachable again"; "new_conversation_create_group" = "Create a group conversation"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index ab40a6d2a..70e732766 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -419,6 +419,8 @@ "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"; +"mwi_messages_are_waiting_single" = "1 message vocal en attente"; +"mwi_messages_are_waiting_multiple" = "%@ messages vocaux en attente"; "network_not_reachable" = "Vous n’êtes pas connecté à internet"; "network_reachable_again" = "Vous êtes à nouveau connecté à internet"; "new_conversation_create_group" = "Créer une conversation de groupe"; diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index f72ffe2dd..b59099da2 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -140,6 +140,54 @@ struct ContentView: View { .background(Color.redDanger500) } + if sharedMainViewModel.waitingMessageCount > 0 && (!telecomManager.callInProgress || (telecomManager.callInProgress && !telecomManager.callDisplayed)) { + HStack { + Image("voicemail") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 26, height: 26) + .padding(.leading, 10) + + if sharedMainViewModel.waitingMessageCount > 1 { + Text(String(format: String(localized: "mwi_messages_are_waiting_multiple"), sharedMainViewModel.waitingMessageCount.description)) + .default_text_style_white(styleSize: 16) + } else { + Text(String(localized: "mwi_messages_are_waiting_single")) + .default_text_style_white(styleSize: 16) + } + + Spacer() + + Button( + action: { + withAnimation { + sharedMainViewModel.waitingMessageCount = 0 + } + }, label: { + Image("x") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 26, height: 26) + .padding(.trailing, 10) + } + ) + + } + .frame(maxWidth: .infinity) + .frame(height: 40) + .padding(.horizontal, 10) + .background(Color.gray) + .onTapGesture { + if let index = accountProfileViewModel.defaultAccountModelIndex, + index < coreContext.accounts.count { + sharedMainViewModel.waitingMessageCount = 0 + coreContext.accounts[index].callVoicemailUri() + } + } + } + if !sharedMainViewModel.fileUrlsToShare.isEmpty && (!telecomManager.callInProgress || (telecomManager.callInProgress && !telecomManager.callDisplayed)) { HStack { Image("share-network") diff --git a/Linphone/UI/Main/Fragments/SideMenuAccountRow.swift b/Linphone/UI/Main/Fragments/SideMenuAccountRow.swift index 89960f524..007acb328 100644 --- a/Linphone/UI/Main/Fragments/SideMenuAccountRow.swift +++ b/Linphone/UI/Main/Fragments/SideMenuAccountRow.swift @@ -90,15 +90,51 @@ struct SideMenuAccountRow: View { Spacer() HStack { + if model.voicemailCount > 0 { + Button { + model.callVoicemailUri() + } label: { + ZStack(alignment: .top) { + VStack { + Spacer() + + Image("voicemail") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 22, height: 22) + + Spacer() + } + + Text(String(model.voicemailCount)) + .foregroundStyle(Color.redDanger500) + .default_text_style_600(styleSize: 12) + .lineLimit(1) + .frame(maxWidth: .infinity, alignment: .trailing) + .padding(.top, 1) + } + .frame(width: 30, height: 30) + } + .highPriorityGesture( + TapGesture().onEnded { + model.callVoicemailUri() + } + ) + } + if model.notificationsCount > 0 && !CorePreferences.disableChatFeature { - Text(String(model.notificationsCount)) - .foregroundStyle(.white) - .default_text_style(styleSize: 12) - .lineLimit(1) - .frame(width: 20, height: 20) - .background(Color.redDanger500) - .cornerRadius(50) - .frame(maxWidth: .infinity, alignment: .leading) + VStack { + Text(String(model.notificationsCount)) + .foregroundStyle(.white) + .default_text_style(styleSize: 12) + .lineLimit(1) + .frame(width: 20, height: 20) + .background(Color.redDanger500) + .cornerRadius(50) + } + .frame(width: 30, height: 30) + .padding(.trailing, -8) } Menu { @@ -112,15 +148,18 @@ struct SideMenuAccountRow: View { Label("drawer_menu_manage_account", systemImage: "arrow.right.circle") } } label: { - Image("dots-three-vertical") - .renderingMode(.template) - .resizable() - .foregroundColor(Color.gray) - .scaledToFit() - .frame(height: 30) + VStack { + Image("dots-three-vertical") + .renderingMode(.template) + .resizable() + .foregroundColor(Color.grayMain2c500) + .scaledToFit() + .frame(height: 25) + } + .frame(width: 30, height: 30) } } - .frame(width: 64, alignment: .trailing) + .frame(alignment: .trailing) .padding(.top, 12) .padding(.bottom, 12) } diff --git a/Linphone/UI/Main/Viewmodel/AccountModel.swift b/Linphone/UI/Main/Viewmodel/AccountModel.swift index ae363b49a..26fc277c5 100644 --- a/Linphone/UI/Main/Viewmodel/AccountModel.swift +++ b/Linphone/UI/Main/Viewmodel/AccountModel.swift @@ -32,6 +32,8 @@ class AccountModel: ObservableObject { @Published var registrationStateAssociatedUIColor: Color = .clear @Published var isRegistrered: Bool = false @Published var notificationsCount: Int = 0 + @Published var showMwi: Bool = false + @Published var voicemailCount: Int = 0 @Published var isDefaultAccount: Bool = false @Published var displayName: String = "" @Published var address: String = "" @@ -51,23 +53,46 @@ class AccountModel: ObservableObject { init(account: Account, core: Core) { self.account = account - - self.computeNotificationsCount() - accountDelegate = AccountDelegateStub(onRegistrationStateChanged: { (_: Account, _: RegistrationState, _: String) in - self.update() - }) + self.computeNotificationsCount() + + accountDelegate = AccountDelegateStub( + onRegistrationStateChanged: { (_: Account, _: RegistrationState, _: String) in + self.update() + }, onMessageWaitingIndicationChanged: { (account: Account, mwi: MessageWaitingIndication) in + Log.info("\(AccountModel.TAG) Account \(account.params?.identityAddress?.asStringUriOnly() ?? "Error") has received a MWI NOTIFY. \(mwi.hasMessageWaiting() ? "Message(s) are waiting." : "No message is waiting.")") + let showMwiTmp = mwi.hasMessageWaiting() + var voicemailCountTmp = 0 + for summary in mwi.summaries { + let contextClass = summary.contextClass + let nbNew = summary.nbNew + let nbNewUrgent = summary.nbNewUrgent + let nbOld = summary.nbOld + let nbOldUrgent = summary.nbOldUrgent + Log.info("\(AccountModel.TAG) [MWI] \(contextClass): new \(nbNew) urgent \(nbNewUrgent), old \(nbOld) urgent \(nbOldUrgent)") + + voicemailCountTmp = Int(nbNew) + } + + DispatchQueue.main.async { + self.showMwi = showMwiTmp + self.voicemailCount = voicemailCountTmp + } + } + ) account.addDelegate(delegate: accountDelegate!) - coreDelegate = CoreDelegateStub(onCallStateChanged: { (_: Core, _: Call, _: Call.State, _: String) in - self.computeNotificationsCount() - }, onMessagesReceived: { (_: Core, _: ChatRoom, _: [ChatMessage]) in - self.computeNotificationsCount() - }, onChatRoomRead: { (_: Core, _: ChatRoom) in - self.computeNotificationsCount() - }, onMessageRetracted: { (_: Core, _: ChatRoom, _: ChatMessage) in - self.computeNotificationsCount() - }) + coreDelegate = CoreDelegateStub( + onCallStateChanged: { (_: Core, _: Call, _: Call.State, _: String) in + self.computeNotificationsCount() + }, onMessagesReceived: { (_: Core, _: ChatRoom, _: [ChatMessage]) in + self.computeNotificationsCount() + }, onChatRoomRead: { (_: Core, _: ChatRoom) in + self.computeNotificationsCount() + }, onMessageRetracted: { (_: Core, _: ChatRoom, _: ChatMessage) in + self.computeNotificationsCount() + } + ) core.addDelegate(delegate: coreDelegate!) CoreContext.shared.doOnCoreQueue { _ in @@ -311,6 +336,15 @@ class AccountModel: ObservableObject { self.isDefaultAccount = true } + + func callVoicemailUri() { + CoreContext.shared.doOnCoreQueue { core in + if let voicemail = self.account.params?.voicemailAddress { + Log.info("\(AccountModel.TAG) Calling voicemail address \(voicemail.asStringUriOnly())") + TelecomManager.shared.doCallOrJoinConf(address: voicemail) + } + } + } } class AccountDeviceModel: ObservableObject { diff --git a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift index 87e4bd06b..36f333cbd 100644 --- a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift +++ b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift @@ -40,6 +40,7 @@ class SharedMainViewModel: ObservableObject { @Published var dialPlansShortLabelList: [String] = [] @Published var fileUrlsToShare: [String] = [] + @Published var waitingMessageCount: Int = 0 @Published var operationInProgress = false