diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index f9374e42c..60a8812a8 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ D72343322ACEFF58009AA24E /* QRScannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72343312ACEFF58009AA24E /* QRScannerController.swift */; }; D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72343332ACEFFC3009AA24E /* QRScanner.swift */; }; D72343362AD037AF009AA24E /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72343352AD037AF009AA24E /* ToastView.swift */; }; + D726E4392B16440C0083C415 /* ContactAvatarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D726E4382B16440C0083C415 /* ContactAvatarModel.swift */; }; D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */; }; D732A9092AFD235500DB42BA /* ShareSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9082AFD235500DB42BA /* ShareSheetController.swift */; }; D732A90C2B0376F500DB42BA /* linphonerc-default in Resources */ = {isa = PBXBuildFile; fileRef = D732A90A2B0376F500DB42BA /* linphonerc-default */; }; @@ -104,6 +105,7 @@ D72343312ACEFF58009AA24E /* QRScannerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerController.swift; sourceTree = ""; }; D72343332ACEFFC3009AA24E /* QRScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScanner.swift; sourceTree = ""; }; D72343352AD037AF009AA24E /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; + D726E4382B16440C0083C415 /* ContactAvatarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactAvatarModel.swift; sourceTree = ""; }; D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryContactFragment.swift; sourceTree = ""; }; D732A9082AFD235500DB42BA /* ShareSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetController.swift; sourceTree = ""; }; D732A90A2B0376F500DB42BA /* linphonerc-default */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-default"; sourceTree = ""; }; @@ -297,6 +299,14 @@ path = ViewModel; sourceTree = ""; }; + D726E4372B1643FF0083C415 /* Model */ = { + isa = PBXGroup; + children = ( + D726E4382B16440C0083C415 /* ContactAvatarModel.swift */, + ); + path = Model; + sourceTree = ""; + }; D72992372ADD7F1C003AF125 /* Fragments */ = { isa = PBXGroup; children = ( @@ -380,6 +390,7 @@ isa = PBXGroup; children = ( D78290B62ADD38F9004AA85C /* Fragments */, + D726E4372B1643FF0083C415 /* Model */, D78290B92ADD409D004AA85C /* ViewModel */, D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */, ); @@ -578,6 +589,7 @@ D719ABB72ABC67BF00B41C10 /* LinphoneApp.swift in Sources */, D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */, D72250632ADE9615008FB426 /* HistoryViewModel.swift in Sources */, + D726E4392B16440C0083C415 /* ContactAvatarModel.swift in Sources */, D76005F62B0798B00054B79A /* IntExtension.swift in Sources */, D7E6D0512AEBDBD500A57AAF /* ContactsListBottomSheet.swift in Sources */, D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */, diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index eafa75152..6b80d8131 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -22,12 +22,11 @@ import Contacts import SwiftUI import ContactsUI -final class ContactsManager { +final class ContactsManager: ObservableObject { static let shared = ContactsManager() private var coreContext = CoreContext.shared - private var magicSearch = MagicSearchSingleton.shared private let nativeAddressBookFriendList = "Native address-book" let linphoneAddressBookFriendList = "Linphone address-book" @@ -35,6 +34,9 @@ final class ContactsManager { var friendList: FriendList? var linphoneFriendList: FriendList? + @Published var lastSearch: [SearchResult] = [] + @Published var avatarListModel: [ContactAvatarModel] = [] + private init() { fetchContacts() } @@ -132,7 +134,8 @@ final class ContactsManager { print("\(#function) - access denied") } } - self.magicSearch.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + + MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) } } diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index d95a2a884..394b1e7f0 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -247,9 +247,6 @@ }, "En continuant, vous acceptez ces conditions, " : { - }, - "En ligne" : { - }, "Error" : { diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift index f1f427e2f..e29473d97 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactFragment.swift @@ -34,9 +34,11 @@ struct ContactFragment: View { @State private var showShareSheet = false var body: some View { + let indexDisplayed = contactViewModel.indexDisplayedFriend != nil ? contactViewModel.indexDisplayedFriend! : 0 if #available(iOS 16.0, *) { if idiom != .pad { ContactInnerFragment( + contactAvatarModel: ContactsManager.shared.avatarListModel[indexDisplayed], contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, cnContact: CNContact(), @@ -50,12 +52,13 @@ struct ContactFragment: View { .presentationDetents([.fraction(0.2)]) } .sheet(isPresented: $showShareSheet) { - ShareSheet(friendToShare: MagicSearchSingleton.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) + ShareSheet(friendToShare: ContactsManager.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) .presentationDetents([.medium]) .edgesIgnoringSafeArea(.bottom) } } else { ContactInnerFragment( + contactAvatarModel: ContactsManager.shared.avatarListModel[indexDisplayed], contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, cnContact: CNContact(), @@ -68,12 +71,13 @@ struct ContactFragment: View { ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet) } onDismiss: {} .sheet(isPresented: $showShareSheet) { - ShareSheet(friendToShare: MagicSearchSingleton.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) + ShareSheet(friendToShare: ContactsManager.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) .edgesIgnoringSafeArea(.bottom) } } } else { ContactInnerFragment( + contactAvatarModel: ContactsManager.shared.avatarListModel[indexDisplayed], contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, cnContact: CNContact(), @@ -86,7 +90,7 @@ struct ContactFragment: View { ContactListBottomSheet(contactViewModel: contactViewModel, showingSheet: $showingSheet) } onDismiss: {} .sheet(isPresented: $showShareSheet) { - ShareSheet(friendToShare: MagicSearchSingleton.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) + ShareSheet(friendToShare: ContactsManager.shared.lastSearch[contactViewModel.indexDisplayedFriend!].friend!) .edgesIgnoringSafeArea(.bottom) } } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift index 8714c25e3..99333cd53 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift @@ -21,7 +21,8 @@ import SwiftUI struct ContactInnerActionsFragment: View { - @ObservedObject var magicSearch = MagicSearchSingleton.shared + @ObservedObject var contactsManager = ContactsManager.shared + @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var editContactViewModel: EditContactViewModel @@ -59,8 +60,8 @@ struct ContactInnerActionsFragment: View { if informationIsOpen { VStack(spacing: 0) { - if contactViewModel.indexDisplayedFriend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil { - ForEach(0... + */ + +import Foundation +import linphonesw + +class ContactAvatarModel: ObservableObject { + + let friend: Friend? + + let withPresence: Bool? + + @Published var lastPresenceInfo: String + + @Published var presenceStatus: ConsolidatedPresence + + private var friendDelegate: FriendDelegate? + + init(friend: Friend?, withPresence: Bool?) { + self.friend = friend + self.withPresence = withPresence + if friend != nil && + withPresence == true { + self.lastPresenceInfo = "" + + self.presenceStatus = friend!.consolidatedPresence + + if friend!.consolidatedPresence == .Online || friend!.consolidatedPresence == .Busy { + if friend!.consolidatedPresence == .Online || friend!.presenceModel!.latestActivityTimestamp != -1 { + self.lastPresenceInfo = friend!.consolidatedPresence == .Online ? "Online" : getCallTime(startDate: friend!.presenceModel!.latestActivityTimestamp) + } else { + self.lastPresenceInfo = "Away" + } + } else { + self.lastPresenceInfo = "" + } + + if self.friendDelegate != nil { + self.friend!.removeDelegate(delegate: self.friendDelegate!) + self.friendDelegate = nil + } + + addDelegate() + } else { + self.lastPresenceInfo = "" + self.presenceStatus = .Offline + } + } + + func addDelegate() { + let newFriendDelegate = FriendDelegateStub( + onPresenceReceived: { (linphoneFriend: Friend) -> Void in + DispatchQueue.main.sync { + self.presenceStatus = linphoneFriend.consolidatedPresence + if linphoneFriend.consolidatedPresence == .Online || linphoneFriend.consolidatedPresence == .Busy { + if linphoneFriend.consolidatedPresence == .Online || linphoneFriend.presenceModel!.latestActivityTimestamp != -1 { + self.lastPresenceInfo = linphoneFriend.consolidatedPresence == .Online ? "Online" : self.getCallTime(startDate: linphoneFriend.presenceModel!.latestActivityTimestamp) + } else { + self.lastPresenceInfo = "Away" + } + } else { + self.lastPresenceInfo = "" + } + } + } + ) + + friendDelegate = newFriendDelegate + if friendDelegate != nil { + friend!.addDelegate(delegate: friendDelegate!) + } + } + + func removeAllDelegate() { + if friendDelegate != nil { + presenceStatus = .Offline + friend!.removeDelegate(delegate: friendDelegate!) + friendDelegate = nil + } + } + + func getCallTime(startDate: time_t) -> String { + let timeInterval = TimeInterval(startDate) + + let myNSDate = Date(timeIntervalSince1970: timeInterval) + + if Calendar.current.isDateInToday(myNSDate) { + let formatter = DateFormatter() + formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "HH:mm" : "h:mm a" + return "Online today at " + formatter.string(from: myNSDate) + } else if Calendar.current.isDateInYesterday(myNSDate) { + let formatter = DateFormatter() + formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "HH:mm" : "h:mm a" + return "Online yesterday at " + formatter.string(from: myNSDate) + } else if Calendar.current.isDate(myNSDate, equalTo: .now, toGranularity: .year) { + let formatter = DateFormatter() + formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "dd/MM | HH:mm" : "MM/dd | h:mm a" + return "Online on " + formatter.string(from: myNSDate) + } else { + let formatter = DateFormatter() + formatter.dateFormat = Locale.current.identifier == "fr_FR" ? "dd/MM/yy | HH:mm" : "MM/dd/yy | h:mm a" + return "Online on " + formatter.string(from: myNSDate) + } + } +} diff --git a/Linphone/UI/Main/Contacts/ViewModel/ContactViewModel.swift b/Linphone/UI/Main/Contacts/ViewModel/ContactViewModel.swift index a3f8443c7..1c17f540c 100644 --- a/Linphone/UI/Main/Contacts/ViewModel/ContactViewModel.swift +++ b/Linphone/UI/Main/Contacts/ViewModel/ContactViewModel.swift @@ -29,9 +29,5 @@ class ContactViewModel: ObservableObject { var selectedFriendToShare: Friend? var selectedFriendToDelete: Friend? - private var magicSearch = MagicSearchSingleton.shared - - init() { - magicSearch.searchForContacts( - sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue)} + init() {} } diff --git a/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift b/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift index a126f5975..3c59661d2 100644 --- a/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift +++ b/Linphone/UI/Main/Contacts/ViewModel/ContactsListViewModel.swift @@ -20,6 +20,6 @@ import linphonesw class ContactsListViewModel: ObservableObject { - + init() {} } diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index cdffd8cd0..186b5fd3f 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -28,7 +28,7 @@ struct ContentView: View { @ObservedObject private var coreContext = CoreContext.shared @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared - var contactManager = ContactsManager.shared + @ObservedObject var contactsManager = ContactsManager.shared var magicSearch = MagicSearchSingleton.shared @ObservedObject var contactViewModel: ContactViewModel @@ -146,7 +146,7 @@ struct ContentView: View { Button { isMenuOpen = false magicSearch.allContact = true - magicSearch.searchForContacts( + MagicSearchSingleton.shared.searchForContacts( sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) } label: { HStack { @@ -163,7 +163,7 @@ struct ContentView: View { Button { isMenuOpen = false magicSearch.allContact = false - magicSearch.searchForContacts( + MagicSearchSingleton.shared.searchForContacts( sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) } label: { HStack { @@ -219,7 +219,7 @@ struct ContentView: View { if index == 0 { magicSearch.currentFilter = "" - magicSearch.searchForContacts( + MagicSearchSingleton.shared.searchForContacts( sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) } else { historyListViewModel.resetFilterCallLogs() @@ -256,7 +256,7 @@ struct ContentView: View { .onChange(of: text) { newValue in if index == 0 { magicSearch.currentFilter = newValue - magicSearch.searchForContacts( + MagicSearchSingleton.shared.searchForContacts( sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) } else { historyListViewModel.filterCallLogs(filter: text) @@ -284,7 +284,7 @@ struct ContentView: View { } .onChange(of: text) { newValue in magicSearch.currentFilter = newValue - magicSearch.searchForContacts( + MagicSearchSingleton.shared.searchForContacts( sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) } } @@ -428,7 +428,20 @@ struct ContentView: View { .background(Color.gray100) .ignoresSafeArea(.keyboard) } else if self.index == 1 { + let fromAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil + let toAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil + let addressFriend = historyViewModel.displayedCall != nil ? (historyViewModel.displayedCall!.dir == .Incoming ? fromAddressFriend : toAddressFriend) : nil + + let contactAvatarModel = addressFriend != nil + ? ContactsManager.shared.avatarListModel.first(where: { + ($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy) + && $0.friend!.name == addressFriend!.name + && $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly() + }) + : ContactAvatarModel(friend: nil, withPresence: false) + HistoryContactFragment( + contactAvatarModel: contactAvatarModel!, historyViewModel: historyViewModel, historyListViewModel: historyListViewModel, contactViewModel: contactViewModel, @@ -478,6 +491,7 @@ struct ContentView: View { if isShowEditContactFragment { EditContactFragment( editContactViewModel: editContactViewModel, + contactViewModel: contactViewModel, isShowEditContactFragment: $isShowEditContactFragment, isShowDismissPopup: $isShowDismissPopup ) @@ -494,7 +508,7 @@ struct ContentView: View { contactViewModel.selectedFriend != nil ? "Delete \(contactViewModel.selectedFriend!.name!)?" : (contactViewModel.indexDisplayedFriend != nil - ? "Delete \(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.name!)?" + ? "Delete \(contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.name!)?" : "Error Name")), content: Text("This contact will be deleted definitively."), titleFirstButton: Text("Cancel"), @@ -514,9 +528,9 @@ struct ContentView: View { withAnimation { contactViewModel.indexDisplayedFriend = nil } - magicSearch.lastSearch[tmpIndex!].friend!.remove() + contactsManager.lastSearch[tmpIndex!].friend!.remove() } - magicSearch.searchForContacts( + MagicSearchSingleton.shared.searchForContacts( sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) self.isShowDeleteContactPopup.toggle() }) @@ -611,7 +625,7 @@ struct ContentView: View { } .onChange(of: scenePhase) { newPhase in if newPhase == .active { - ContactsManager.shared.fetchContacts() + contactsManager.fetchContacts() print("Active") } else if newPhase == .inactive { print("Inactive") diff --git a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift index 139245706..dbb5c4a4a 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift @@ -25,6 +25,9 @@ struct HistoryContactFragment: View { @State private var orientation = UIDevice.current.orientation @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + @ObservedObject var contactsManager = ContactsManager.shared + + @ObservedObject var contactAvatarModel: ContactAvatarModel @ObservedObject var historyViewModel: HistoryViewModel @ObservedObject var historyListViewModel: HistoryListViewModel @ObservedObject var contactViewModel: ContactViewModel @@ -66,14 +69,14 @@ struct HistoryContactFragment: View { Spacer() Menu { - let fromAddressFriend = historyViewModel.displayedCall != nil ? ContactsManager.shared.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil - let toAddressFriend = historyViewModel.displayedCall != nil ? ContactsManager.shared.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil + let fromAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil + let toAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil let addressFriend = historyViewModel.displayedCall != nil ? (historyViewModel.displayedCall!.dir == .Incoming ? fromAddressFriend : toAddressFriend) : nil Button { isMenuOpen = false - if ContactsManager.shared.getFriendWithAddress( + if contactsManager.getFriendWithAddress( address: historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing ? historyViewModel.displayedCall!.toAddress! : historyViewModel.displayedCall!.fromAddress! @@ -82,7 +85,7 @@ struct HistoryContactFragment: View { ? historyViewModel.displayedCall!.toAddress! : historyViewModel.displayedCall!.fromAddress! - let friendIndex = MagicSearchSingleton.shared.lastSearch.firstIndex( + let friendIndex = contactsManager.lastSearch.firstIndex( where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressCall.asStringUriOnly()})}) if friendIndex != nil { @@ -190,40 +193,19 @@ struct HistoryContactFragment: View { VStack(spacing: 0) { VStack(spacing: 0) { - let fromAddressFriend = historyViewModel.displayedCall != nil ? ContactsManager.shared.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil - let toAddressFriend = historyViewModel.displayedCall != nil ? ContactsManager.shared.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil + let fromAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.fromAddress!) : nil + let toAddressFriend = historyViewModel.displayedCall != nil ? contactsManager.getFriendWithAddress(address: historyViewModel.displayedCall!.toAddress!) : nil let addressFriend = historyViewModel.displayedCall != nil ? (historyViewModel.displayedCall!.dir == .Incoming ? fromAddressFriend : toAddressFriend) : nil if historyViewModel.displayedCall != nil && addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty { - AsyncImage( - url: ContactsManager.shared.getImagePath( - friendPhotoPath: addressFriend!.photo!)) { image in - switch image { - case .empty: - ProgressView() - .frame(width: 100, height: 100) - case .success(let image): - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 100, height: 100) - .clipShape(Circle()) - case .failure: - Image("profil-picture-default") - .resizable() - .frame(width: 100, height: 100) - .clipShape(Circle()) - @unknown default: - EmptyView() - } - } + Avatar(contactAvatarModel: contactAvatarModel, avatarSize: 100) } else if historyViewModel.displayedCall != nil { if historyViewModel.displayedCall!.dir == .Outgoing && historyViewModel.displayedCall!.toAddress != nil { if historyViewModel.displayedCall!.toAddress!.displayName != nil { - Image(uiImage: ContactsManager.shared.textToImage( + Image(uiImage: contactsManager.textToImage( firstName: historyViewModel.displayedCall!.toAddress!.displayName!, lastName: historyViewModel.displayedCall!.toAddress!.displayName!.components(separatedBy: " ").count > 1 ? historyViewModel.displayedCall!.toAddress!.displayName!.components(separatedBy: " ")[1] @@ -252,7 +234,7 @@ struct HistoryContactFragment: View { .frame(maxWidth: .infinity) .frame(height: 20) } else { - Image(uiImage: ContactsManager.shared.textToImage( + Image(uiImage: contactsManager.textToImage( firstName: historyViewModel.displayedCall!.toAddress!.username ?? "Username Error", lastName: historyViewModel.displayedCall!.toAddress!.username!.components(separatedBy: " ").count > 1 ? historyViewModel.displayedCall!.toAddress!.username!.components(separatedBy: " ")[1] @@ -284,7 +266,7 @@ struct HistoryContactFragment: View { } else if historyViewModel.displayedCall!.fromAddress != nil { if historyViewModel.displayedCall!.fromAddress!.displayName != nil { - Image(uiImage: ContactsManager.shared.textToImage( + Image(uiImage: contactsManager.textToImage( firstName: historyViewModel.displayedCall!.fromAddress!.displayName!, lastName: historyViewModel.displayedCall!.fromAddress!.displayName!.components(separatedBy: " ").count > 1 ? historyViewModel.displayedCall!.fromAddress!.displayName!.components(separatedBy: " ")[1] @@ -313,7 +295,7 @@ struct HistoryContactFragment: View { .frame(maxWidth: .infinity) .frame(height: 20) } else { - Image(uiImage: ContactsManager.shared.textToImage( + Image(uiImage: contactsManager.textToImage( firstName: historyViewModel.displayedCall!.fromAddress!.username ?? "Username Error", lastName: historyViewModel.displayedCall!.fromAddress!.username!.components(separatedBy: " ").count > 1 ? historyViewModel.displayedCall!.fromAddress!.username!.components(separatedBy: " ")[1] @@ -370,13 +352,15 @@ struct HistoryContactFragment: View { .padding(.top, 5) } - Text("En ligne") - .foregroundStyle(Color.greenSuccess500) + Text(contactAvatarModel.lastPresenceInfo) + .foregroundStyle(contactAvatarModel.lastPresenceInfo == "Online" + ? Color.greenSuccess500 + : Color.orangeWarning600) .multilineTextAlignment(.center) .default_text_style_300(styleSize: 12) .frame(maxWidth: .infinity) .frame(height: 20) - .padding(.top, 5) + .padding(.top, 5) } } .frame(minHeight: 150) @@ -508,7 +492,11 @@ struct HistoryContactFragment: View { .frame(maxWidth: .infinity, alignment: .leading) Text(historyListViewModel.getCallTime(startDate: callLogsFilter[index].startDate)) - .foregroundStyle(callLogsFilter[index].status != .Success ? Color.redDanger500 : Color.grayMain2c600) + .foregroundStyle( + callLogsFilter[index].status != .Success + ? Color.redDanger500 + : Color.grayMain2c600 + ) .default_text_style_300(styleSize: 12) .frame(maxWidth: .infinity, alignment: .leading) } @@ -546,7 +534,8 @@ struct HistoryContactFragment: View { #Preview { HistoryContactFragment( - historyViewModel: HistoryViewModel(), + contactAvatarModel: ContactAvatarModel(friend: nil, withPresence: false), + historyViewModel: HistoryViewModel(), historyListViewModel: HistoryListViewModel(), contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), diff --git a/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift b/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift index e5a58b79d..fb8dfd731 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift @@ -27,6 +27,7 @@ struct HistoryListBottomSheet: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + @ObservedObject var contactsManager = ContactsManager.shared @ObservedObject var historyViewModel: HistoryViewModel @ObservedObject var contactViewModel: ContactViewModel @@ -76,7 +77,7 @@ struct HistoryListBottomSheet: View { index = 0 - if ContactsManager.shared.getFriendWithAddress( + if contactsManager.getFriendWithAddress( address: historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing ? historyViewModel.selectedCall!.toAddress! : historyViewModel.selectedCall!.fromAddress! @@ -85,7 +86,7 @@ struct HistoryListBottomSheet: View { ? historyViewModel.selectedCall!.toAddress! : historyViewModel.selectedCall!.fromAddress! - let friendIndex = MagicSearchSingleton.shared.lastSearch.firstIndex(where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressCall.asStringUriOnly()})}) + let friendIndex = contactsManager.lastSearch.firstIndex(where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressCall.asStringUriOnly()})}) if friendIndex != nil { withAnimation { contactViewModel.indexDisplayedFriend = friendIndex @@ -105,7 +106,7 @@ struct HistoryListBottomSheet: View { } } label: { HStack { - if ContactsManager.shared.getFriendWithAddress( + if contactsManager.getFriendWithAddress( address: historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing ? historyViewModel.selectedCall!.toAddress! : historyViewModel.selectedCall!.fromAddress! diff --git a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift index 677623806..50fd2513a 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift @@ -22,6 +22,8 @@ import linphonesw struct HistoryListFragment: View { + @ObservedObject var contactsManager = ContactsManager.shared + @ObservedObject var historyListViewModel: HistoryListViewModel @ObservedObject var historyViewModel: HistoryViewModel @@ -34,37 +36,24 @@ struct HistoryListFragment: View { Button { } label: { HStack { - let fromAddressFriend = ContactsManager.shared.getFriendWithAddress(address: historyListViewModel.callLogs[index].fromAddress!) - let toAddressFriend = ContactsManager.shared.getFriendWithAddress(address: historyListViewModel.callLogs[index].toAddress!) + let fromAddressFriend = contactsManager.getFriendWithAddress(address: historyListViewModel.callLogs[index].fromAddress!) + let toAddressFriend = contactsManager.getFriendWithAddress(address: historyListViewModel.callLogs[index].toAddress!) let addressFriend = historyListViewModel.callLogs[index].dir == .Incoming ? fromAddressFriend : toAddressFriend + let contactAvatarModel = addressFriend != nil + ? ContactsManager.shared.avatarListModel.first(where: { + ($0.friend!.consolidatedPresence == .Online || $0.friend!.consolidatedPresence == .Busy) + && $0.friend!.name == addressFriend!.name + && $0.friend!.address!.asStringUriOnly() == addressFriend!.address!.asStringUriOnly() + }) + : ContactAvatarModel(friend: nil, withPresence: false) + if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty { - AsyncImage(url: - ContactsManager.shared.getImagePath( - friendPhotoPath: addressFriend!.photo!)) { image in - switch image { - case .empty: - ProgressView() - .frame(width: 45, height: 45) - case .success(let image): - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 45, height: 45) - .clipShape(Circle()) - case .failure: - Image("profil-picture-default") - .resizable() - .frame(width: 45, height: 45) - .clipShape(Circle()) - @unknown default: - EmptyView() - } - } + Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 45) } else { if historyListViewModel.callLogs[index].dir == .Outgoing && historyListViewModel.callLogs[index].toAddress != nil { if historyListViewModel.callLogs[index].toAddress!.displayName != nil { - Image(uiImage: ContactsManager.shared.textToImage( + Image(uiImage: contactsManager.textToImage( firstName: historyListViewModel.callLogs[index].toAddress!.displayName!, lastName: historyListViewModel.callLogs[index].toAddress!.displayName!.components(separatedBy: " ").count > 1 ? historyListViewModel.callLogs[index].toAddress!.displayName!.components(separatedBy: " ")[1] @@ -74,7 +63,7 @@ struct HistoryListFragment: View { .clipShape(Circle()) } else { - Image(uiImage: ContactsManager.shared.textToImage( + Image(uiImage: contactsManager.textToImage( firstName: historyListViewModel.callLogs[index].toAddress!.username ?? "Username Error", lastName: historyListViewModel.callLogs[index].toAddress!.username!.components(separatedBy: " ").count > 1 ? historyListViewModel.callLogs[index].toAddress!.username!.components(separatedBy: " ")[1] @@ -86,7 +75,7 @@ struct HistoryListFragment: View { } else if historyListViewModel.callLogs[index].fromAddress != nil { if historyListViewModel.callLogs[index].fromAddress!.displayName != nil { - Image(uiImage: ContactsManager.shared.textToImage( + Image(uiImage: contactsManager.textToImage( firstName: historyListViewModel.callLogs[index].fromAddress!.displayName!, lastName: historyListViewModel.callLogs[index].fromAddress!.displayName!.components(separatedBy: " ").count > 1 ? historyListViewModel.callLogs[index].fromAddress!.displayName!.components(separatedBy: " ")[1] @@ -95,7 +84,7 @@ struct HistoryListFragment: View { .frame(width: 45, height: 45) .clipShape(Circle()) } else { - Image(uiImage: ContactsManager.shared.textToImage( + Image(uiImage: contactsManager.textToImage( firstName: historyListViewModel.callLogs[index].fromAddress!.username ?? "Username Error", lastName: historyListViewModel.callLogs[index].fromAddress!.username!.components(separatedBy: " ").count > 1 ? historyListViewModel.callLogs[index].fromAddress!.username!.components(separatedBy: " ")[1] @@ -110,8 +99,8 @@ struct HistoryListFragment: View { VStack(spacing: 0) { Spacer() - let fromAddressFriend = ContactsManager.shared.getFriendWithAddress(address: historyListViewModel.callLogs[index].fromAddress!) - let toAddressFriend = ContactsManager.shared.getFriendWithAddress(address: historyListViewModel.callLogs[index].toAddress!) + let fromAddressFriend = contactsManager.getFriendWithAddress(address: historyListViewModel.callLogs[index].fromAddress!) + let toAddressFriend = contactsManager.getFriendWithAddress(address: historyListViewModel.callLogs[index].toAddress!) let addressFriend = historyListViewModel.callLogs[index].dir == .Incoming ? fromAddressFriend : toAddressFriend if addressFriend != nil { diff --git a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift index 3251d2511..19e7c9668 100644 --- a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift +++ b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift @@ -53,9 +53,9 @@ class HistoryListViewModel: ObservableObject { DispatchQueue.main.async { self.coreDelegate = CoreDelegateStub( onCallLogUpdated: { (_: Core, _: CallLog) -> Void in - DispatchQueue.main.async { + DispatchQueue.main.sync { let account = core.defaultAccount - let logs = account?.callLogs != nil ? account!.callLogs : core.callLogs + let logs = account != nil ? account!.callLogs : core.callLogs self.callLogs.removeAll() self.callLogsTmp.removeAll() @@ -71,7 +71,6 @@ class HistoryListViewModel: ObservableObject { core.addDelegate(delegate: self.coreDelegate!) } } - } } diff --git a/Linphone/UI/Main/Viewmodel/ToastViewModel.swift b/Linphone/UI/Main/Viewmodel/ToastViewModel.swift index 6fb3287a9..b046c5a06 100644 --- a/Linphone/UI/Main/Viewmodel/ToastViewModel.swift +++ b/Linphone/UI/Main/Viewmodel/ToastViewModel.swift @@ -1,9 +1,21 @@ -// -// ToastViewModel.swift -// Linphone -// -// Created by BenoƮt Martins on 20/11/2023. -// +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of Linphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ import Foundation diff --git a/Linphone/Utils/Avatar.swift b/Linphone/Utils/Avatar.swift index 16d3f9fba..609b13885 100644 --- a/Linphone/Utils/Avatar.swift +++ b/Linphone/Utils/Avatar.swift @@ -22,84 +22,45 @@ import linphonesw struct Avatar: View { - var friend: Friend - let avatarSize: CGFloat - - @State private var friendDelegate: FriendDelegate? - @State private var presenceImage = "" - - var body: some View { - AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: friend.photo!)) { image in - switch image { - case .empty: - ProgressView() - .frame(width: avatarSize, height: avatarSize) - case .success(let image): - ZStack { - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: avatarSize, height: avatarSize) - .clipShape(Circle()) - HStack { - Spacer() - VStack { - Spacer() - if !friend.addresses.isEmpty { - if presenceImage.isEmpty - && (friend.consolidatedPresence == ConsolidatedPresence.Online || friend.consolidatedPresence == ConsolidatedPresence.Busy) { - Image(friend.consolidatedPresence == ConsolidatedPresence.Online ? "presence-online" : "presence-busy") - .resizable() - .frame(width: avatarSize/4, height: avatarSize/4) - .padding(.trailing, avatarSize == 45 ? 1 : 3) - .padding(.bottom, avatarSize == 45 ? 1 : 3) - } else if !presenceImage.isEmpty { - Image(presenceImage) - .resizable() - .frame(width: avatarSize/4, height: avatarSize/4) - .padding(.trailing, avatarSize == 45 ? 1 : 3) - .padding(.bottom, avatarSize == 45 ? 1 : 3) - } - } - } - } - .frame(width: avatarSize, height: avatarSize) - } - .onAppear { - addDelegate() + @ObservedObject var contactAvatarModel: ContactAvatarModel + let avatarSize: CGFloat + + var body: some View { + AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: contactAvatarModel.friend!.photo!)) { image in + switch image { + case .empty: + ProgressView() + .frame(width: avatarSize, height: avatarSize) + case .success(let image): + ZStack { + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + HStack { + Spacer() + VStack { + Spacer() + if contactAvatarModel.presenceStatus == .Online || contactAvatarModel.presenceStatus == .Busy { + Image(contactAvatarModel.presenceStatus == .Online ? "presence-online" : "presence-busy") + .resizable() + .frame(width: avatarSize/4, height: avatarSize/4) + .padding(.trailing, avatarSize == 45 ? 1 : 3) + .padding(.bottom, avatarSize == 45 ? 1 : 3) + } + } + } + .frame(width: avatarSize, height: avatarSize) } - .onDisappear { - removeAllDelegate() - } - case .failure: - Image("profil-picture-default") - .resizable() - .frame(width: avatarSize, height: avatarSize) - .clipShape(Circle()) - @unknown default: - EmptyView() - } - } - } - - func addDelegate() { - let newFriendDelegate = FriendDelegateStub( - onPresenceReceived: { (linphoneFriend: Friend) -> Void in - self.presenceImage = linphoneFriend.consolidatedPresence == ConsolidatedPresence.Online ? "presence-online" : "presence-busy" - } - ) - - friendDelegate = newFriendDelegate - if friendDelegate != nil { - friend.addDelegate(delegate: friendDelegate!) - } - } - - func removeAllDelegate() { - if friendDelegate != nil { - presenceImage = "" - friend.removeDelegate(delegate: friendDelegate!) - friendDelegate = nil + case .failure: + Image("profil-picture-default") + .resizable() + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + @unknown default: + EmptyView() + } } } } diff --git a/Linphone/Utils/IntExtension.swift b/Linphone/Utils/IntExtension.swift index fca8ba9d1..0f457700a 100644 --- a/Linphone/Utils/IntExtension.swift +++ b/Linphone/Utils/IntExtension.swift @@ -28,7 +28,7 @@ extension Int { public func convertDurationToString() -> String { var duration = "" let (hour, minute, second) = self.hmsFrom() - if (hour > 0) { + if hour > 0 { duration = self.getHour(hour: hour) } return "\(duration)\(self.getMinute(minute: minute))\(self.getSecond(second: second))" @@ -36,18 +36,18 @@ extension Int { private func getHour(hour: Int) -> String { var duration = "\(hour):" - if (hour < 10) { + if hour < 10 { duration = "0\(hour):" } return duration } private func getMinute(minute: Int) -> String { - if (minute == 0) { + if minute == 0 { return "00:" } - if (minute < 10) { + if minute < 10 { return "0\(minute):" } @@ -55,11 +55,11 @@ extension Int { } private func getSecond(second: Int) -> String { - if (second == 0){ + if second == 0 { return "00" } - if (second < 10) { + if second < 10 { return "0\(second)" } return "\(second)" diff --git a/Linphone/Utils/MagicSearchSingleton.swift b/Linphone/Utils/MagicSearchSingleton.swift index 7ce369b0a..488eb5d4b 100644 --- a/Linphone/Utils/MagicSearchSingleton.swift +++ b/Linphone/Utils/MagicSearchSingleton.swift @@ -23,6 +23,7 @@ final class MagicSearchSingleton: ObservableObject { static let shared = MagicSearchSingleton() private var coreContext = CoreContext.shared + private var contactsManager = ContactsManager.shared private var magicSearch: MagicSearch! @@ -31,8 +32,6 @@ final class MagicSearchSingleton: ObservableObject { var needUpdateLastSearchContacts = false - @Published var lastSearch: [SearchResult] = [] - private var limitSearchToLinphoneAccounts = true @Published var allContact = false @@ -47,7 +46,23 @@ final class MagicSearchSingleton: ObservableObject { self.magicSearch.publisher?.onSearchResultsReceived?.postOnMainQueue { (magicSearch: MagicSearch) in self.needUpdateLastSearchContacts = true - self.lastSearch = magicSearch.lastSearch + self.contactsManager.lastSearch = magicSearch.lastSearch.sorted(by: { + $0.friend!.name!.lowercased().folding(options: .diacriticInsensitive, locale: .current) + < + $1.friend!.name!.lowercased().folding(options: .diacriticInsensitive, locale: .current) + }) + + self.contactsManager.avatarListModel.forEach { contactAvatarModel in + contactAvatarModel.removeAllDelegate() + } + + self.contactsManager.avatarListModel.removeAll() + + self.contactsManager.lastSearch.forEach { searchResult in + if searchResult.friend != nil { + self.contactsManager.avatarListModel.append(ContactAvatarModel(friend: searchResult.friend!, withPresence: true)) + } + } } } } @@ -75,4 +90,28 @@ final class MagicSearchSingleton: ObservableObject { aggregation: MagicSearch.Aggregation.Friend) } } + + func searchForContactsWithResult(sourceFlags: Int) { + coreContext.doOnCoreQueue { _ in + var needResetCache = false + + DispatchQueue.main.sync { + if let oldFilter = self.previousFilter { + if oldFilter.count > self.currentFilter.count || oldFilter != self.currentFilter { + needResetCache = true + } + } + self.previousFilter = self.currentFilter + } + if needResetCache { + self.magicSearch.resetSearchCache() + } + + self.magicSearch.getContactsListAsync( + filter: self.currentFilter, + domain: self.allContact ? "" : self.domainDefaultAccount, + sourceFlags: sourceFlags, + aggregation: MagicSearch.Aggregation.Friend) + } + } }