From 5cf66f28530b71539d47393b38aff1966cefd178 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Fri, 10 Nov 2023 15:15:43 +0100 Subject: [PATCH 01/14] Add presence icon for Avatar --- Linphone.xcodeproj/project.pbxproj | 20 +++ .../presence-busy.imageset/Contents.json | 21 ++++ .../presence-busy.imageset/presence-busy.svg | 5 + .../presence-online.imageset/Contents.json | 21 ++++ .../presence-online.svg | 5 + Linphone/Core/CoreContext.swift | 25 +++- Linphone/Linphone.entitlements | 14 ++- Linphone/LinphoneApp.swift | 2 +- Linphone/Ressources/linphonerc-default | 39 ++++++ Linphone/Ressources/linphonerc-factory | 65 ++++++++++ .../Fragments/ContactInnerFragment.swift | 25 +--- .../Fragments/ContactsListFragment.swift | 23 +--- .../Fragments/EditContactFragment.swift | 23 +--- .../FavoriteContactsListFragment.swift | 24 +--- Linphone/UI/Main/ContentView.swift | 2 + Linphone/Utils/Avatar.swift | 118 ++++++++++++++++++ 16 files changed, 343 insertions(+), 89 deletions(-) create mode 100644 Linphone/Assets.xcassets/presence-busy.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/presence-busy.imageset/presence-busy.svg create mode 100644 Linphone/Assets.xcassets/presence-online.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/presence-online.imageset/presence-online.svg create mode 100644 Linphone/Ressources/linphonerc-default create mode 100644 Linphone/Ressources/linphonerc-factory create mode 100644 Linphone/Utils/Avatar.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 64f20df16..ee1e35284 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -29,6 +29,8 @@ D72343362AD037AF009AA24E /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72343352AD037AF009AA24E /* ToastView.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 */; }; + D732A90D2B0376F500DB42BA /* linphonerc-factory in Resources */ = {isa = PBXBuildFile; fileRef = D732A90B2B0376F500DB42BA /* linphonerc-factory */; }; D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */; }; D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */; }; D74C9CF82ACACECE0021626A /* WelcomePage1Fragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */; }; @@ -45,6 +47,7 @@ D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBF2ACC2E390081A588 /* HistoryView.swift */; }; D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FC52ACC458A0081A588 /* SplashScreen.swift */; }; D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A2EDD52AC18115005D90FC /* SharedMainViewModel.swift */; }; + D7ADF6002AFE356400212231 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADF5FF2AFE356400212231 /* Avatar.swift */; }; D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */; }; D7C365082AEFAB7F00FE6142 /* ContactListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */; }; D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365092AF001C300FE6142 /* EditContactFragment.swift */; }; @@ -95,6 +98,8 @@ D72343352AD037AF009AA24E /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.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 = ""; }; + D732A90B2B0376F500DB42BA /* linphonerc-factory */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-factory"; sourceTree = ""; }; D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = ""; }; D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = ""; }; D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePage1Fragment.swift; sourceTree = ""; }; @@ -112,6 +117,7 @@ D7A03FC52ACC458A0081A588 /* SplashScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = ""; }; D7A2EDD52AC18115005D90FC /* SharedMainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedMainViewModel.swift; sourceTree = ""; }; D7A2EDDA2AC19EEC005D90FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + D7ADF5FF2AFE356400212231 /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = ""; }; D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerFragment.swift; sourceTree = ""; }; D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListBottomSheet.swift; sourceTree = ""; }; D7C365092AF001C300FE6142 /* EditContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactFragment.swift; sourceTree = ""; }; @@ -165,6 +171,7 @@ D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */, D7C48DF32AFA66F900D938CB /* EditContactController.swift */, D732A9082AFD235500DB42BA /* ShareSheetController.swift */, + D7ADF5FF2AFE356400212231 /* Avatar.swift */, ); path = Utils; sourceTree = ""; @@ -201,6 +208,7 @@ D70C93DD2AC2D0F60063CA3B /* Localizable.xcstrings */, D719ABBD2ABC67BF00B41C10 /* Preview Content */, D7D24D0C2AC1B4C700C6F35B /* Fonts */, + D7ADF6012AFE5C7C00212231 /* Ressources */, ); path = Linphone; sourceTree = ""; @@ -375,6 +383,15 @@ path = Viewmodel; sourceTree = ""; }; + D7ADF6012AFE5C7C00212231 /* Ressources */ = { + isa = PBXGroup; + children = ( + D732A90A2B0376F500DB42BA /* linphonerc-default */, + D732A90B2B0376F500DB42BA /* linphonerc-factory */, + ); + path = Ressources; + sourceTree = ""; + }; D7D24D0C2AC1B4C700C6F35B /* Fonts */ = { isa = PBXGroup; children = ( @@ -469,6 +486,8 @@ D719ABBF2ABC67BF00B41C10 /* Preview Assets.xcassets in Resources */, D719ABBB2ABC67BF00B41C10 /* Assets.xcassets in Resources */, D7D24D132AC1B4E800C6F35B /* NotoSans-Medium.ttf in Resources */, + D732A90C2B0376F500DB42BA /* linphonerc-default in Resources */, + D732A90D2B0376F500DB42BA /* linphonerc-factory in Resources */, D70C93DE2AC2D0F60063CA3B /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -503,6 +522,7 @@ buildActionMask = 2147483647; files = ( D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */, + D7ADF6002AFE356400212231 /* Avatar.swift in Sources */, D71707202AC5989C0037746F /* TextExtension.swift in Sources */, D719ABB92ABC67BF00B41C10 /* ContentView.swift in Sources */, D71FCA832AE14D6E00D2E43E /* ContactFragment.swift in Sources */, diff --git a/Linphone/Assets.xcassets/presence-busy.imageset/Contents.json b/Linphone/Assets.xcassets/presence-busy.imageset/Contents.json new file mode 100644 index 000000000..227036d8d --- /dev/null +++ b/Linphone/Assets.xcassets/presence-busy.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "presence-busy.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/presence-busy.imageset/presence-busy.svg b/Linphone/Assets.xcassets/presence-busy.imageset/presence-busy.svg new file mode 100644 index 000000000..0f24966ca --- /dev/null +++ b/Linphone/Assets.xcassets/presence-busy.imageset/presence-busy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Linphone/Assets.xcassets/presence-online.imageset/Contents.json b/Linphone/Assets.xcassets/presence-online.imageset/Contents.json new file mode 100644 index 000000000..606200f2a --- /dev/null +++ b/Linphone/Assets.xcassets/presence-online.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "presence-online.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/presence-online.imageset/presence-online.svg b/Linphone/Assets.xcassets/presence-online.imageset/presence-online.svg new file mode 100644 index 000000000..ae3b6ed5e --- /dev/null +++ b/Linphone/Assets.xcassets/presence-online.imageset/presence-online.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 350fb6a0d..a18811e17 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -30,6 +30,7 @@ final class CoreContext: ObservableObject { @Published var loggingInProgress: Bool = false @Published var toastMessage: String = "" @Published var defaultAccount: Account? + @Published var coreIsStarted: Bool = false private var mCore: Core! private var mIteratePublisher: AnyCancellable? @@ -53,17 +54,39 @@ final class CoreContext: ObservableObject { coreQueue.async { let configDir = Factory.Instance.getConfigDir(context: nil) - try? self.mCore = Factory.Instance.createCore(configPath: "\(configDir)/MyConfig", factoryConfigPath: "", systemContext: nil) + + let url = NSURL(fileURLWithPath: configDir) + if let pathComponent = url.appendingPathComponent("linphonerc") { + let filePath = pathComponent.path + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: filePath) { + let path = Bundle.main.path(forResource: "linphonerc-default", ofType: nil) + if path != nil { + try? FileManager.default.copyItem(at: NSURL(fileURLWithPath: path!) as URL, to: pathComponent) + } + } + } + + let config: Config! = Config.newForSharedCore( + appGroupId: "group.org.linphone.phone.msgNotification", + configFilename: "linphonerc", + factoryConfigFilename: Bundle.main.path(forResource: "linphonerc-factory", ofType: nil) + ) + + self.mCore = try? Factory.Instance.createCoreWithConfig(config: config, systemContext: nil) + self.mCore.autoIterateEnabled = false self.mCore.friendsDatabasePath = "\(configDir)/friends.db" self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in if cbVal.state == GlobalState.On { self.defaultAccount = self.mCore.defaultAccount + self.coreIsStarted = true } else if cbVal.state == GlobalState.Off { self.defaultAccount = nil } } + try? self.mCore.start() // Create a Core listener to listen for the callback we need diff --git a/Linphone/Linphone.entitlements b/Linphone/Linphone.entitlements index f2ef3ae02..500e2cecd 100644 --- a/Linphone/Linphone.entitlements +++ b/Linphone/Linphone.entitlements @@ -2,9 +2,15 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.belledonne-communications.linphone + group.org.linphone.phone.msgNotification + group.org.linphone.phone.linphoneExtension + + com.apple.security.files.user-selected.read-only + diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 85f14b81c..9308e26fe 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -29,7 +29,7 @@ struct LinphoneApp: App { var body: some Scene { WindowGroup { - if isActive { + if isActive && coreContext.coreIsStarted { if !sharedMainViewModel.welcomeViewDisplayed { WelcomeView(sharedMainViewModel: sharedMainViewModel) } else if coreContext.defaultAccount == nil || sharedMainViewModel.displayProfileMode { diff --git a/Linphone/Ressources/linphonerc-default b/Linphone/Ressources/linphonerc-default new file mode 100644 index 000000000..ea5356429 --- /dev/null +++ b/Linphone/Ressources/linphonerc-default @@ -0,0 +1,39 @@ + +## Start of default rc + +[sip] +contact="Linphone iPhone" +use_info=0 +use_ipv6=1 +keepalive_period=30000 +sip_port=-1 +sip_tcp_port=-1 +sip_tls_port=-1 +media_encryption=none +update_presence_model_timestamp_before_publish_expires_refresh=1 + +[net] +#Because dynamic bitrate adaption can increase bitrate, we must allow "no limit" +download_bw=0 +upload_bw=0 + +[video] +size=vga + +[app] +tunnel=disabled +auto_start=1 +record_aware=1 + +[tunnel] +host= +port=443 + +[misc] +log_collection_upload_server_url=https://www.linphone.org:444/lft.php +file_transfer_server_url=https://www.linphone.org:444/lft.php +version_check_url_root=https://www.linphone.org/releases +max_calls=10 +conference_layout=1 + +## End of default rc diff --git a/Linphone/Ressources/linphonerc-factory b/Linphone/Ressources/linphonerc-factory new file mode 100644 index 000000000..85b543074 --- /dev/null +++ b/Linphone/Ressources/linphonerc-factory @@ -0,0 +1,65 @@ + +## Start of factory rc + +# This file shall not contain path referencing package name, in order to be portable when app is renamed. +# Paths to resources must be set from LinphoneManager, after creating LinphoneCore. + +[net] +mtu=1300 +force_ice_disablement=0 + +[rtp] +accept_any_encryption=1 + +[sip] +guess_hostname=1 +register_only_when_network_is_up=1 +auto_net_state_mon=1 +auto_answer_replacing_calls=1 +ping_with_options=0 +use_cpim=1 +zrtp_key_agreements_suites=MS_ZRTP_KEY_AGREEMENT_K255_KYB512 +chat_messages_aggregation_delay=1000 +chat_messages_aggregation=1 +update_presence_model_timestamp_before_publish_expires_refresh=1 +rls_uri=sips:rls@sip.linphone.org + +[sound] +#remove this property for any application that is not Linphone public version itself +ec_calibrator_cool_tones=1 + +[video] +displaytype=MSAndroidTextureDisplay +auto_resize_preview_to_keep_ratio=1 +max_conference_size=vga + +[misc] +enable_basic_to_client_group_chat_room_migration=0 +enable_simple_group_chat_message_state=0 +aggregate_imdn=1 +notify_each_friend_individually_when_presence_received=0 +store_friends=0 + +[app] +activation_code_length=4 +prefer_basic_chat_room=1 +record_aware=1 + +[account_creator] +backend=1 +# 1 means FlexiAPI, 0 is XMLRPC +url=https://subscribe.linphone.org/api/ +# replace above URL by https://staging-subscribe.linphone.org/api/ for testing + +[lime] +lime_update_threshold=86400 + +[alerts] +alerts_enabled=1 + +[assistant] +algorithm=SHA-256 +password_min_length=6 +username_regex=^[a-z0-9+_.\-]*$ + +## End of factory rc diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift index ac6a8e5cd..dfefb33b6 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift @@ -113,28 +113,9 @@ struct ContactInnerFragment: View { && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.photo != nil && !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.photo!.isEmpty { - AsyncImage( - url: ContactsManager.shared.getImagePath( - friendPhotoPath: magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.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(friend: magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!, avatarSize: 100) + } else if contactViewModel.indexDisplayedFriend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil { Image("profil-picture-default") .resizable() diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift index 7b274e646..67562f06f 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift @@ -64,26 +64,9 @@ struct ContactsListFragment: View { } if magicSearch.lastSearch[index].friend!.photo != nil && !magicSearch.lastSearch[index].friend!.photo!.isEmpty { - AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: magicSearch.lastSearch[index].friend!.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(friend: magicSearch.lastSearch[index].friend!, avatarSize: 45) + } else { Image("profil-picture-default") .resizable() diff --git a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift index e7de05556..9e1b2b09c 100644 --- a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift @@ -129,26 +129,9 @@ struct EditContactFragment: View { if editContactViewModel.selectedEditFriend != nil && editContactViewModel.selectedEditFriend!.photo != nil && !editContactViewModel.selectedEditFriend!.photo!.isEmpty && selectedImage == nil && !removedImage { - AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: editContactViewModel.selectedEditFriend!.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(friend: editContactViewModel.selectedEditFriend!, avatarSize: 100) + } else if selectedImage == nil { Image("profil-picture-default") .resizable() diff --git a/Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift index 0eb2fa566..1942ece86 100644 --- a/Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift @@ -39,27 +39,9 @@ struct FavoriteContactsListFragment: View { VStack { if magicSearch.lastSearch[index].friend!.photo != nil && !magicSearch.lastSearch[index].friend!.photo!.isEmpty { - AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: magicSearch.lastSearch[index].friend!.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(friend: magicSearch.lastSearch[index].friend!, avatarSize: 45) + } else { Image("profil-picture-default") .resizable() diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index d9a8307c1..f3dfce4c8 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -136,6 +136,7 @@ struct ContentView: View { Menu { Button { isMenuOpen = false + contactViewModel.indexDisplayedFriend = nil magicSearch.allContact = true magicSearch.searchForContacts( sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) @@ -153,6 +154,7 @@ struct ContentView: View { Button { isMenuOpen = false + contactViewModel.indexDisplayedFriend = nil magicSearch.allContact = false magicSearch.searchForContacts( sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) diff --git a/Linphone/Utils/Avatar.swift b/Linphone/Utils/Avatar.swift new file mode 100644 index 000000000..72847b760 --- /dev/null +++ b/Linphone/Utils/Avatar.swift @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * 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 SwiftUI +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 && (friend.consolidatedPresence != ConsolidatedPresence.DoNotDisturb || friend.consolidatedPresence != ConsolidatedPresence.Offline) { + 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) + } + case .failure: + Image("profil-picture-default") + .resizable() + .frame(width: avatarSize, height: avatarSize) + .clipShape(Circle()) + @unknown default: + EmptyView() + } + } + .onAppear { + addDelegate() + } + .onDisappear { + removeAllDelegate() + } + } + + func addDelegate() { + if friend.address != nil { + print("Presence received for first \(friend.name!) \(friend.consolidatedPresence)") + //self.presenceImage = friend.consolidatedPresence == ConsolidatedPresence.Online ? "presence-online" : "presence-busy" + } + + let newFriendDelegate = FriendDelegateStub( + onPresenceReceived: { (linphoneFriend: Friend) -> Void in + print("Presence received for second \(linphoneFriend.name) \(linphoneFriend.consolidatedPresence)") + + /* + if linphoneFriend.address != nil && friend.address != nil + && linphoneFriend.address!.asStringUriOnly() == friend.address!.asStringUriOnly() { + let presenceModel = linphoneFriend.getPresenceModelForUriOrTel(uriOrTel: (linphoneFriend.address!.asStringUriOnly())) + if presenceModel != nil { + presenceImage = presenceModel!.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 + } + } +} From ce9f6c454c9f4cafdc69364cbe811a3cf14bc334 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Wed, 15 Nov 2023 16:18:06 +0100 Subject: [PATCH 02/14] Add history call list --- Linphone.xcodeproj/project.pbxproj | 16 ++ .../trash-simple-red.imageset/Contents.json | 21 ++ .../trash-simple-red.svg | 3 + Linphone/Contacts/ContactsManager.swift | 14 +- Linphone/LinphoneApp.swift | 9 +- Linphone/Localizable.xcstrings | 20 +- .../ContactInnerActionsFragment.swift | 24 +- .../Contacts/Fragments/ContactsFragment.swift | 4 +- .../Fragments/ContactsInnerFragment.swift | 15 +- Linphone/UI/Main/ContentView.swift | 189 +++++++++----- .../History/Fragments/HistoryFragment.swift | 92 +++++++ .../Fragments/HistoryListBottomSheet.swift | 240 ++++++++++++++++++ .../Fragments/HistoryListFragment.swift | 232 +++++++++++++++++ Linphone/UI/Main/History/HistoryView.swift | 50 +++- .../ViewModel/HistoryListViewModel.swift | 142 +++++++++++ .../History/ViewModel/HistoryViewModel.swift | 5 +- Linphone/Utils/EditContactController.swift | 2 +- 17 files changed, 978 insertions(+), 100 deletions(-) create mode 100644 Linphone/Assets.xcassets/trash-simple-red.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/trash-simple-red.imageset/trash-simple-red.svg create mode 100644 Linphone/UI/Main/History/Fragments/HistoryFragment.swift create mode 100644 Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift create mode 100644 Linphone/UI/Main/History/Fragments/HistoryListFragment.swift create mode 100644 Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 64f20df16..1279fcc32 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -29,6 +29,10 @@ D72343362AD037AF009AA24E /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72343352AD037AF009AA24E /* ToastView.swift */; }; D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */; }; D732A9092AFD235500DB42BA /* ShareSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9082AFD235500DB42BA /* ShareSheetController.swift */; }; + D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */; }; + D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */; }; + D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */; }; + D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */; }; D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */; }; D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */; }; D74C9CF82ACACECE0021626A /* WelcomePage1Fragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */; }; @@ -95,6 +99,10 @@ D72343352AD037AF009AA24E /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.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 = ""; }; + D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryFragment.swift; sourceTree = ""; }; + D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListFragment.swift; sourceTree = ""; }; + D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListViewModel.swift; sourceTree = ""; }; + D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListBottomSheet.swift; sourceTree = ""; }; D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = ""; }; D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = ""; }; D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePage1Fragment.swift; sourceTree = ""; }; @@ -267,6 +275,7 @@ isa = PBXGroup; children = ( D72250622ADE9615008FB426 /* HistoryViewModel.swift */, + D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -275,6 +284,9 @@ isa = PBXGroup; children = ( D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */, + D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */, + D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */, + D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */, ); path = Fragments; sourceTree = ""; @@ -510,12 +522,14 @@ D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */, D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */, D706BA822ADD72D100278F45 /* DeviceRotationViewModifier.swift in Sources */, + D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */, D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */, D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */, D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */, D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */, D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */, D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */, + D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */, D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */, D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */, D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */, @@ -523,9 +537,11 @@ D7C48DF42AFA66F900D938CB /* EditContactController.swift in Sources */, D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */, D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */, + D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */, D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */, D71FCA812AE14CFC00D2E43E /* ContactsListFragment.swift in Sources */, D719ABB72ABC67BF00B41C10 /* LinphoneApp.swift in Sources */, + D732A91B2B061BD900DB42BA /* HistoryListBottomSheet.swift in Sources */, D72250632ADE9615008FB426 /* HistoryViewModel.swift in Sources */, D7E6D0512AEBDBD500A57AAF /* ContactsListBottomSheet.swift in Sources */, D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */, diff --git a/Linphone/Assets.xcassets/trash-simple-red.imageset/Contents.json b/Linphone/Assets.xcassets/trash-simple-red.imageset/Contents.json new file mode 100644 index 000000000..02ba54e4e --- /dev/null +++ b/Linphone/Assets.xcassets/trash-simple-red.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "trash-simple-red.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-simple-red.imageset/trash-simple-red.svg b/Linphone/Assets.xcassets/trash-simple-red.imageset/trash-simple-red.svg new file mode 100644 index 000000000..ea7d36f6a --- /dev/null +++ b/Linphone/Assets.xcassets/trash-simple-red.imageset/trash-simple-red.svg @@ -0,0 +1,3 @@ + + + diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index fa9843527..9db3527c0 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -271,7 +271,7 @@ final class ContactsManager { } } - func getFriend(contact: Contact) -> Friend? { + func getFriendWithContact(contact: Contact) -> Friend? { if friendList != nil { let friend = friendList!.friends.first(where: {$0.nativeUri == contact.identifier}) return friend @@ -279,6 +279,18 @@ final class ContactsManager { return nil } } + + func getFriendWithAddress(address: Address) -> Friend? { + if friendList != nil { + var friend = friendList!.friends.first(where: {$0.addresses.contains(where: {$0.asStringUriOnly() == address.asStringUriOnly()})}) + if friend == nil { + friend = linphoneFriendList!.friends.first(where: {$0.addresses.contains(where: {$0.asStringUriOnly() == address.asStringUriOnly()})}) + } + return friend + } else { + return nil + } + } } struct PhoneNumber { diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 85f14b81c..35b3a538e 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -36,8 +36,13 @@ struct LinphoneApp: App { AssistantView(sharedMainViewModel: sharedMainViewModel) .toast(isShowing: $coreContext.toastMessage) } else if coreContext.defaultAccount != nil { - ContentView(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), historyViewModel: HistoryViewModel()) - .toast(isShowing: $coreContext.toastMessage) + ContentView( + contactViewModel: ContactViewModel(), + editContactViewModel: EditContactViewModel(), + historyViewModel: HistoryViewModel(), + historyListViewModel: HistoryListViewModel() + ) + .toast(isShowing: $coreContext.toastMessage) } } else { SplashScreen(isActive: $isActive) diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 0062a8ce1..695ac7218 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -104,9 +104,15 @@ }, "Add a picture" : { + }, + "Add the contact" : { + }, "Add to favourites" : { + }, + "All calls will be removed from the history." : { + }, "All contacts" : { @@ -172,6 +178,9 @@ }, "Copy number" : { + }, + "Copy SIP address" : { + }, "D'accord" : { @@ -187,6 +196,9 @@ }, "Delete %@?" : { + }, + "Delete all history" : { + }, "Delete this contact" : { @@ -199,6 +211,9 @@ }, "Display Name" : { + }, + "Do you really want to delete all calls history?" : { + }, "Domain" : { @@ -293,7 +308,7 @@ "Next" : { }, - "No calls for the moment..." : { + "No call for the moment..." : { }, "No contacts for the moment..." : { @@ -372,6 +387,9 @@ }, "See all" : { + }, + "See contact" : { + }, "See Linphone contact" : { diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift index ff8e3e2e7..8714c25e3 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerActionsFragment.swift @@ -1,9 +1,21 @@ -// -// ContactInnerActionsFragment.swift -// Linphone -// -// Created by Benoît Martins on 09/11/2023. -// +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * 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 SwiftUI diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift index ef48b5e84..8382da399 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsFragment.swift @@ -42,10 +42,10 @@ struct ContactsFragment: View { showingSheet: $showingSheet, showShareSheet: $showShareSheet ) - .presentationDetents([.fraction(0.2)]) + .presentationDetents([.fraction(0.2)]) } .sheet(isPresented: $showShareSheet) { - ShareSheet(friendToShare: contactViewModel.selectedFriendToShare!) + ShareSheet(friendToShare: contactViewModel.selectedFriendToShare!) .presentationDetents([.medium]) .edgesIgnoringSafeArea(.bottom) } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift index 8e09e519f..4f779b294 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift @@ -20,10 +20,7 @@ import SwiftUI import linphonesw -struct ContactsInnerFragment: View { - - @Environment(\.scenePhase) var scenePhase - +struct ContactsInnerFragment: View { @ObservedObject var magicSearch = MagicSearchSingleton.shared @ObservedObject var contactViewModel: ContactViewModel @@ -76,16 +73,6 @@ struct ContactsInnerFragment: View { ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet) } .navigationBarHidden(true) - .onChange(of: scenePhase) { newPhase in - if newPhase == .active { - ContactsManager.shared.fetchContacts() - print("Active") - } else if newPhase == .inactive { - print("Inactive") - } else if newPhase == .background { - print("Background") - } - } } } diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index d9a8307c1..6c677de8a 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -17,18 +17,23 @@ * along with this program. If not, see . */ +// swiftlint:disable type_body_length import SwiftUI import linphonesw struct ContentView: View { + @Environment(\.scenePhase) var scenePhase + + @ObservedObject private var coreContext = CoreContext.shared + var contactManager = ContactsManager.shared var magicSearch = MagicSearchSingleton.shared @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var editContactViewModel: EditContactViewModel @ObservedObject var historyViewModel: HistoryViewModel - @ObservedObject private var coreContext = CoreContext.shared + @ObservedObject var historyListViewModel: HistoryListViewModel @State var index = 0 @State private var orientation = UIDevice.current.orientation @@ -38,7 +43,8 @@ struct ContentView: View { @State private var text = "" @FocusState private var focusedField: Bool @State var isMenuOpen = false - @State var isShowDeletePopup = false + @State var isShowDeleteContactPopup = false + @State var isShowDeleteAllHistoryPopup = false @State var isShowEditContactFragment = false @State var isShowDismissPopup = false @@ -134,34 +140,50 @@ struct ContentView: View { } Menu { - Button { - isMenuOpen = false - magicSearch.allContact = true - magicSearch.searchForContacts( - sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) - } label: { - HStack { - Text("See all") - Spacer() - if magicSearch.allContact { - Image("green-check") - .resizable() - .frame(width: 25, height: 25, alignment: .leading) + if index == 0 { + Button { + isMenuOpen = false + magicSearch.allContact = true + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } label: { + HStack { + Text("See all") + Spacer() + if magicSearch.allContact { + Image("green-check") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + } } } - } - - Button { - isMenuOpen = false - magicSearch.allContact = false - magicSearch.searchForContacts( - sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) - } label: { - HStack { - Text("See Linphone contact") - Spacer() - if !magicSearch.allContact { - Image("green-check") + + Button { + isMenuOpen = false + magicSearch.allContact = false + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } label: { + HStack { + Text("See Linphone contact") + Spacer() + if !magicSearch.allContact { + Image("green-check") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + } + } + } + } else { + Button(role: .destructive) { + isMenuOpen = false + isShowDeleteAllHistoryPopup.toggle() + //historyListViewModel.removeCallLogs() + } label: { + HStack { + Text("Delete all history") + Spacer() + Image("trash-simple-red") .resizable() .frame(width: 25, height: 25, alignment: .leading) } @@ -193,9 +215,14 @@ struct ContentView: View { } text = "" - magicSearch.currentFilter = "" - magicSearch.searchForContacts( - sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + + if index == 0 { + magicSearch.currentFilter = "" + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } else { + historyListViewModel.resetFilterCallLogs() + } } label: { Image("caret-left") .renderingMode(.template) @@ -226,9 +253,13 @@ struct ContentView: View { self.focusedField = true } .onChange(of: text) { newValue in - magicSearch.currentFilter = newValue - magicSearch.searchForContacts( - sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + if index == 0 { + magicSearch.currentFilter = newValue + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } else { + historyListViewModel.filterCallLogs(filter: text) + } } } else { TextEditor(text: Binding( @@ -281,10 +312,17 @@ struct ContentView: View { historyViewModel: historyViewModel, editContactViewModel: editContactViewModel, isShowEditContactFragment: $isShowEditContactFragment, - isShowDeletePopup: $isShowDeletePopup + isShowDeletePopup: $isShowDeleteContactPopup ) } else if self.index == 1 { - HistoryView() + HistoryView( + historyListViewModel: historyListViewModel, + historyViewModel: historyViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + index: $index, + isShowEditContactFragment: $isShowEditContactFragment + ) } } .frame(maxWidth: @@ -367,7 +405,7 @@ struct ContentView: View { } } - if contactViewModel.indexDisplayedFriend != nil || !historyViewModel.historyTitle.isEmpty { + if contactViewModel.indexDisplayedFriend != nil || historyViewModel.indexDisplayedCall != nil { HStack(spacing: 0) { Spacer() .frame(maxWidth: @@ -381,12 +419,12 @@ struct ContentView: View { ContactFragment( contactViewModel: contactViewModel, editContactViewModel: editContactViewModel, - isShowDeletePopup: $isShowDeletePopup, + isShowDeletePopup: $isShowDeleteContactPopup, isShowDismissPopup: $isShowDismissPopup ) - .frame(maxWidth: .infinity) - .background(Color.gray100) - .ignoresSafeArea(.keyboard) + .frame(maxWidth: .infinity) + .background(Color.gray100) + .ignoresSafeArea(.keyboard) } else if self.index == 1 { HistoryContactFragment() .frame(maxWidth: .infinity) @@ -433,25 +471,25 @@ struct ContentView: View { isShowEditContactFragment: $isShowEditContactFragment, isShowDismissPopup: $isShowDismissPopup ) - .zIndex(3) - .transition(.move(edge: .bottom)) - .onAppear { - contactViewModel.indexDisplayedFriend = nil - } + .zIndex(3) + .transition(.move(edge: .bottom)) + .onAppear { + contactViewModel.indexDisplayedFriend = nil + } } - if isShowDeletePopup { - PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDeletePopup, + if isShowDeleteContactPopup { + PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDeleteContactPopup, title: Text( - contactViewModel.selectedFriend != nil - ? "Delete \(contactViewModel.selectedFriend!.name!)?" - : (contactViewModel.indexDisplayedFriend != nil - ? "Delete \(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.name!)?" - : "Error Name")), + contactViewModel.selectedFriend != nil + ? "Delete \(contactViewModel.selectedFriend!.name!)?" + : (contactViewModel.indexDisplayedFriend != nil + ? "Delete \(magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.name!)?" + : "Error Name")), content: Text("This contact will be deleted definitively."), titleFirstButton: Text("Cancel"), actionFirstButton: { - self.isShowDeletePopup.toggle()}, + self.isShowDeleteContactPopup.toggle()}, titleSecondButton: Text("Ok"), actionSecondButton: { if contactViewModel.selectedFriendToDelete != nil { @@ -470,18 +508,37 @@ struct ContentView: View { } magicSearch.searchForContacts( sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) - self.isShowDeletePopup.toggle() + self.isShowDeleteContactPopup.toggle() }) .background(.black.opacity(0.65)) .zIndex(3) .onTapGesture { - self.isShowDeletePopup.toggle() + self.isShowDeleteContactPopup.toggle() } .onAppear { contactViewModel.selectedFriendToDelete = contactViewModel.selectedFriend } } + if isShowDeleteAllHistoryPopup { + PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDeleteContactPopup, + title: Text("Do you really want to delete all calls history?"), + content: Text("All calls will be removed from the history."), + titleFirstButton: Text("Cancel"), + actionFirstButton: { + self.isShowDeleteAllHistoryPopup.toggle()}, + titleSecondButton: Text("Ok"), + actionSecondButton: { + historyListViewModel.removeCallLogs() + self.isShowDeleteAllHistoryPopup.toggle() + }) + .background(.black.opacity(0.65)) + .zIndex(3) + .onTapGesture { + self.isShowDeleteAllHistoryPopup.toggle() + } + } + if isShowDismissPopup { PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDismissPopup, title: Text("Don’t save modifications?"), @@ -524,13 +581,23 @@ struct ContentView: View { } } .onRotate { newOrientation in - if (contactViewModel.indexDisplayedFriend != nil || !historyViewModel.historyTitle.isEmpty) && searchIsActive { + if (contactViewModel.indexDisplayedFriend != nil || historyViewModel.indexDisplayedCall != nil) && searchIsActive { self.focusedField = false } else if searchIsActive { self.focusedField = true } orientation = newOrientation } + .onChange(of: scenePhase) { newPhase in + if newPhase == .active { + ContactsManager.shared.fetchContacts() + print("Active") + } else if newPhase == .inactive { + print("Inactive") + } else if newPhase == .background { + print("Background") + } + } } func openMenu() { @@ -541,5 +608,11 @@ struct ContentView: View { } #Preview { - ContentView(contactViewModel: ContactViewModel(), editContactViewModel: EditContactViewModel(), historyViewModel: HistoryViewModel()) + ContentView( + contactViewModel: ContactViewModel(), + editContactViewModel: EditContactViewModel(), + historyViewModel: HistoryViewModel(), + historyListViewModel: HistoryListViewModel() + ) } +// swiftlint:enable type_body_length diff --git a/Linphone/UI/Main/History/Fragments/HistoryFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryFragment.swift new file mode 100644 index 000000000..36a39e7b6 --- /dev/null +++ b/Linphone/UI/Main/History/Fragments/HistoryFragment.swift @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * 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 SwiftUI + +struct HistoryFragment: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @ObservedObject var historyListViewModel: HistoryListViewModel + @ObservedObject var historyViewModel: HistoryViewModel + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var editContactViewModel: EditContactViewModel + + @State private var showingSheet = false + @Binding var index: Int + @Binding var isShowEditContactFragment: Bool + + var body: some View { + ZStack { + if #available(iOS 16.0, *) { + if idiom != .pad { + HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet) + .sheet(isPresented: $showingSheet) { + HistoryListBottomSheet( + historyViewModel: historyViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + historyListViewModel: historyListViewModel, + showingSheet: $showingSheet, + index: $index, + isShowEditContactFragment: $isShowEditContactFragment + ) + .presentationDetents([.fraction(0.2)]) + } + } else { + HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet) + .halfSheet(showSheet: $showingSheet) { + HistoryListBottomSheet( + historyViewModel: historyViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + historyListViewModel: historyListViewModel, + showingSheet: $showingSheet, + index: $index, + isShowEditContactFragment: $isShowEditContactFragment + ) + } onDismiss: {} + } + } else { + HistoryListFragment(historyListViewModel: historyListViewModel, historyViewModel: historyViewModel, showingSheet: $showingSheet) + .halfSheet(showSheet: $showingSheet) { + HistoryListBottomSheet( + historyViewModel: historyViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + historyListViewModel: historyListViewModel, + showingSheet: $showingSheet, + index: $index, + isShowEditContactFragment: $isShowEditContactFragment + ) + } onDismiss: {} + } + } + } +} + +#Preview { + HistoryFragment( + historyListViewModel: HistoryListViewModel(), + historyViewModel: HistoryViewModel(), + contactViewModel: ContactViewModel(), + editContactViewModel: EditContactViewModel(), + index: .constant(1), + isShowEditContactFragment: .constant(false) + ) +} diff --git a/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift b/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift new file mode 100644 index 000000000..a9c9092c1 --- /dev/null +++ b/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * 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 SwiftUI +import UniformTypeIdentifiers + +struct HistoryListBottomSheet: View { + + @Environment(\.dismiss) var dismiss + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @ObservedObject var historyViewModel: HistoryViewModel + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var editContactViewModel: EditContactViewModel + @ObservedObject var historyListViewModel: HistoryListViewModel + + @State private var orientation = UIDevice.current.orientation + + @Binding var showingSheet: Bool + @Binding var index: Int + @Binding var isShowEditContactFragment: Bool + + var body: some View { + VStack(alignment: .leading) { + if idiom != .pad && (orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Spacer() + HStack { + Spacer() + Button("Close") { + if #available(iOS 16.0, *) { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } + } + .padding(.trailing) + } + + Spacer() + Button { + + if #available(iOS 16.0, *) { + if idiom != .pad { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } else { + showingSheet.toggle() + dismiss() + } + + index = 0 + + if ContactsManager.shared.getFriendWithAddress( + address: historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing + ? historyViewModel.selectedCall!.toAddress! + : historyViewModel.selectedCall!.fromAddress! + ) != nil { + let addressCall = historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing + ? historyViewModel.selectedCall!.toAddress! + : historyViewModel.selectedCall!.fromAddress! + + let friendIndex = MagicSearchSingleton.shared.lastSearch.firstIndex(where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressCall.asStringUriOnly()})}) + if friendIndex != nil { + withAnimation { + contactViewModel.indexDisplayedFriend = friendIndex + } + } + } else { + let addressCall = historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing + ? historyViewModel.selectedCall!.toAddress! + : historyViewModel.selectedCall!.fromAddress! + + withAnimation { + isShowEditContactFragment.toggle() + editContactViewModel.sipAddresses.removeAll() + editContactViewModel.sipAddresses.append(String(addressCall.asStringUriOnly().dropFirst(4))) + editContactViewModel.sipAddresses.append("") + } + } + } label: { + HStack { + if ContactsManager.shared.getFriendWithAddress( + address: historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing + ? historyViewModel.selectedCall!.toAddress! + : historyViewModel.selectedCall!.fromAddress! + ) != nil { + Image("user-circle") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + Text("See contact") + .default_text_style(styleSize: 16) + Spacer() + } else { + Image("plus-circle") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + Text("Add the contact") + .default_text_style(styleSize: 16) + Spacer() + } + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + VStack { + Divider() + } + .frame(maxWidth: .infinity) + + Button { + if historyViewModel.selectedCall != nil && historyViewModel.selectedCall!.dir == .Outgoing { + UIPasteboard.general.setValue( + historyViewModel.selectedCall!.toAddress!.asStringUriOnly().dropFirst(4), + forPasteboardType: UTType.plainText.identifier + ) + } else { + UIPasteboard.general.setValue( + historyViewModel.selectedCall!.fromAddress!.asStringUriOnly().dropFirst(4), + forPasteboardType: UTType.plainText.identifier + ) + } + + if #available(iOS 16.0, *) { + if idiom != .pad { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } else { + showingSheet.toggle() + dismiss() + } + } label: { + HStack { + Image("copy") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25, alignment: .leading) + Text("Copy SIP address") + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + VStack { + Divider() + } + .frame(maxWidth: .infinity) + + Button { + CoreContext.shared.doOnCoreQueue { core in + if historyViewModel.selectedCall != nil { + core.removeCallLog(callLog: historyViewModel.selectedCall!) + historyListViewModel.removeCallLog(callLog: historyViewModel.selectedCall!) + } + } + + if #available(iOS 16.0, *) { + if idiom != .pad { + showingSheet.toggle() + } else { + showingSheet.toggle() + dismiss() + } + } else { + showingSheet.toggle() + dismiss() + } + } label: { + HStack { + Image("trash-simple") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.redDanger500) + .frame(width: 25, height: 25, alignment: .leading) + Text("Delete") + .foregroundStyle(Color.redDanger500) + .default_text_style(styleSize: 16) + Spacer() + } + .frame(maxHeight: .infinity) + } + .padding(.horizontal, 30) + .background(Color.gray100) + + } + .background(Color.gray100) + .frame(maxWidth: .infinity) + .onRotate { newOrientation in + orientation = newOrientation + } + } +} + +#Preview { + HistoryListBottomSheet( + historyViewModel: HistoryViewModel(), + contactViewModel: ContactViewModel(), + editContactViewModel: EditContactViewModel(), + historyListViewModel: HistoryListViewModel(), + showingSheet: .constant(false), + index: .constant(1), + isShowEditContactFragment: .constant(false) + ) +} diff --git a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift new file mode 100644 index 000000000..f370c3352 --- /dev/null +++ b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * 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 SwiftUI +import linphonesw + +struct HistoryListFragment: View { + + @ObservedObject var historyListViewModel: HistoryListViewModel + @ObservedObject var historyViewModel: HistoryViewModel + + @Binding var showingSheet: Bool + + var body: some View { + VStack { + List { + ForEach(0.. 1 + ? historyListViewModel.callLogs[index].toAddress!.displayName!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 45, height: 45) + .clipShape(Circle()) + + } else { + Image(uiImage: ContactsManager.shared.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] + : "")) + .resizable() + .frame(width: 45, height: 45) + .clipShape(Circle()) + } + + } else if historyListViewModel.callLogs[index].fromAddress != nil { + if historyListViewModel.callLogs[index].fromAddress!.displayName != nil { + Image(uiImage: ContactsManager.shared.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] + : "")) + .resizable() + .frame(width: 45, height: 45) + .clipShape(Circle()) + } else { + Image(uiImage: ContactsManager.shared.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] + : "")) + .resizable() + .frame(width: 45, height: 45) + .clipShape(Circle()) + } + } + } + + VStack(spacing: 0) { + Spacer() + + let fromAddressFriend = ContactsManager.shared.getFriendWithAddress(address: historyListViewModel.callLogs[index].fromAddress!) + let toAddressFriend = ContactsManager.shared.getFriendWithAddress(address: historyListViewModel.callLogs[index].toAddress!) + + if historyListViewModel.callLogs[index].dir == .Incoming && fromAddressFriend != nil { + Text(fromAddressFriend!.name!) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else if historyListViewModel.callLogs[index].dir == .Outgoing && toAddressFriend != nil { + Text(toAddressFriend!.name!) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else { + if historyListViewModel.callLogs[index].dir == .Outgoing && historyListViewModel.callLogs[index].toAddress != nil { + Text(historyListViewModel.callLogs[index].toAddress!.displayName != nil + ? historyListViewModel.callLogs[index].toAddress!.displayName! + : historyListViewModel.callLogs[index].toAddress!.username!) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } else if historyListViewModel.callLogs[index].fromAddress != nil { + Text(historyListViewModel.callLogs[index].fromAddress!.displayName != nil + ? historyListViewModel.callLogs[index].fromAddress!.displayName! + : historyListViewModel.callLogs[index].fromAddress!.username!) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity, alignment: .leading) + .lineLimit(1) + } + } + HStack { + Image(historyListViewModel.getCallIconResId(callStatus: historyListViewModel.callLogs[index].status, callDir: historyListViewModel.callLogs[index].dir)) + .resizable() + .frame( + width: historyListViewModel.getCallIconResId(callStatus: historyListViewModel.callLogs[index].status, callDir: historyListViewModel.callLogs[index].dir).contains("rejected") ? 12 : 8, + height: historyListViewModel.getCallIconResId(callStatus: historyListViewModel.callLogs[index].status, callDir: historyListViewModel.callLogs[index].dir).contains("rejected") ? 6 : 8) + + Text(historyListViewModel.getCallTime(startDate: historyListViewModel.callLogs[index].startDate)) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer() + } + + Spacer() + } + + Image("phone") + .resizable() + .frame(width: 25, height: 25) + .padding(.trailing, 5) + } + } + .buttonStyle(.borderless) + .listRowInsets(EdgeInsets(top: 5, leading: 20, bottom: 5, trailing: 20)) + .listRowSeparator(.hidden) + .simultaneousGesture( + LongPressGesture() + .onEnded { _ in + historyViewModel.selectedCall = historyListViewModel.callLogs[index] + showingSheet.toggle() + } + ) + .highPriorityGesture( + TapGesture() + .onEnded { _ in + withAnimation { + //historyViewModel.indexDisplayedCall = index + } + } + ) + } + } + .listStyle(.plain) + .overlay( + VStack { + if historyListViewModel.callLogs.isEmpty { + Spacer() + Image("illus-belledonne") + .resizable() + .scaledToFit() + .clipped() + .padding(.all) + Text("No call for the moment...") + .default_text_style_800(styleSize: 16) + Spacer() + Spacer() + } + } + .padding(.all) + ) + } + } +} + +#Preview { + HistoryListFragment(historyListViewModel: HistoryListViewModel(), historyViewModel: HistoryViewModel(), showingSheet: .constant(false)) +} diff --git a/Linphone/UI/Main/History/HistoryView.swift b/Linphone/UI/Main/History/HistoryView.swift index 25d6b0b1e..a308d4688 100644 --- a/Linphone/UI/Main/History/HistoryView.swift +++ b/Linphone/UI/Main/History/HistoryView.swift @@ -21,22 +21,37 @@ import SwiftUI struct HistoryView: View { + @ObservedObject var historyListViewModel: HistoryListViewModel + @ObservedObject var historyViewModel: HistoryViewModel + @ObservedObject var contactViewModel: ContactViewModel + @ObservedObject var editContactViewModel: EditContactViewModel + + @Binding var index: Int + @Binding var isShowEditContactFragment: Bool + var body: some View { NavigationView { - VStack(spacing: 0) { - VStack { - Spacer() - Image("illus-belledonne") - .resizable() - .scaledToFit() - .clipped() - .padding(.all) - Text("No calls for the moment...") - .default_text_style_800(styleSize: 16) - Spacer() - Spacer() + ZStack(alignment: .bottomTrailing) { + HistoryFragment( + historyListViewModel: historyListViewModel, + historyViewModel: historyViewModel, + contactViewModel: contactViewModel, + editContactViewModel: editContactViewModel, + index: $index, + isShowEditContactFragment: $isShowEditContactFragment + ) + + Button { + + } label: { + Image("phone-plus") + .padding() + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } - .padding(.all) + .padding() } } .navigationViewStyle(.stack) @@ -44,5 +59,12 @@ struct HistoryView: View { } #Preview { - HistoryView() + HistoryFragment( + historyListViewModel: HistoryListViewModel(), + historyViewModel: HistoryViewModel(), + contactViewModel: ContactViewModel(), + editContactViewModel: EditContactViewModel(), + index: .constant(1), + isShowEditContactFragment: .constant(false) + ) } diff --git a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift new file mode 100644 index 000000000..94d57a8d6 --- /dev/null +++ b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * 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 linphonesw + +class HistoryListViewModel: ObservableObject { + + private var coreContext = CoreContext.shared + + @Published var callLogs: [CallLog] = [] + var callLogsTmp: [CallLog] = [] + + init() { + computeCallLogsList() + } + + func computeCallLogsList() { + coreContext.doOnCoreQueue { core in + let account = core.defaultAccount + let logs = account?.callLogs != nil ? account!.callLogs : core.callLogs + + self.callLogs.removeAll() + self.callLogsTmp.removeAll() + + DispatchQueue.main.async { + logs.forEach { log in + self.callLogs.append(log) + self.callLogsTmp.append(log) + } + } + } + } + + func getCallIconResId(callStatus: Call.Status, callDir: Call.Dir) -> String { + switch callStatus { + case Call.Status.Missed: + if callDir == .Outgoing { + "outgoing-call-missed" + } else { + "incoming-call-missed" + } + + case Call.Status.Success: + if callDir == .Outgoing { + "outgoing-call" + } else { + "incoming-call" + } + + default: + if callDir == .Outgoing { + "outgoing-call-rejected" + } else { + "incoming-call-rejected" + } + } + } + + 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 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 "Yesterday " + 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 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 formatter.string(from: myNSDate) + } + } + + func filterCallLogs(filter: String) { + callLogs.removeAll() + callLogsTmp.forEach { callLog in + if callLog.dir == .Outgoing && callLog.toAddress != nil { + if callLog.toAddress!.username != nil && callLog.toAddress!.username!.contains(filter) { + callLogs.append(callLog) + } else if callLog.toAddress!.displayName != nil && callLog.toAddress!.displayName!.contains(filter) { + callLogs.append(callLog) + } + } else if callLog.fromAddress != nil { + if callLog.fromAddress!.username != nil && callLog.fromAddress!.username!.contains(filter) { + callLogs.append(callLog) + } else if callLog.fromAddress!.displayName != nil && callLog.fromAddress!.displayName!.contains(filter) { + callLogs.append(callLog) + } + } + } + } + + func resetFilterCallLogs() { + callLogs = callLogsTmp + } + + func removeCallLogs() { + coreContext.doOnCoreQueue { core in + let account = core.defaultAccount + if account != nil { + account!.clearCallLogs() + } else { + core.clearCallLogs() + } + self.callLogs.removeAll() + self.callLogsTmp.removeAll() + } + } + + func removeCallLog(callLog: CallLog) { + let index = self.callLogs.firstIndex(where: {$0.callId == callLog.callId}) + self.callLogs.remove(at: index!) + + let indexTmp = self.callLogsTmp.firstIndex(where: {$0.callId == callLog.callId}) + self.callLogsTmp.remove(at: index!) + } +} diff --git a/Linphone/UI/Main/History/ViewModel/HistoryViewModel.swift b/Linphone/UI/Main/History/ViewModel/HistoryViewModel.swift index 3222b6a0f..850a2f2ab 100644 --- a/Linphone/UI/Main/History/ViewModel/HistoryViewModel.swift +++ b/Linphone/UI/Main/History/ViewModel/HistoryViewModel.swift @@ -18,10 +18,13 @@ */ import Foundation +import linphonesw class HistoryViewModel: ObservableObject { - @Published var historyTitle: String = "" + @Published var indexDisplayedCall: Int? + + var selectedCall: CallLog? init() {} } diff --git a/Linphone/Utils/EditContactController.swift b/Linphone/Utils/EditContactController.swift index b3a9d250e..b29718a4e 100644 --- a/Linphone/Utils/EditContactController.swift +++ b/Linphone/Utils/EditContactController.swift @@ -53,7 +53,7 @@ struct EditContactView: UIViewControllerRepresentable { name: cnc.givenName + cnc.familyName + String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""), contact: newContact, linphoneFriend: false, - existingFriend: ContactsManager.shared.getFriend(contact: newContact)) + existingFriend: ContactsManager.shared.getFriendWithContact(contact: newContact)) MagicSearchSingleton.shared.searchForContacts(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) } From 375c8b0ce6f0f1d93a55251e6a845c6079b496ed Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 16 Nov 2023 16:59:10 +0100 Subject: [PATCH 03/14] Add history call detail --- Linphone/Localizable.xcstrings | 6 +- Linphone/UI/Main/ContentView.swift | 9 +- .../Fragments/HistoryContactFragment.swift | 409 +++++++++++++++++- .../Fragments/HistoryListFragment.swift | 40 +- .../History/ViewModel/HistoryViewModel.swift | 2 +- 5 files changed, 418 insertions(+), 48 deletions(-) diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 695ac7218..dda26ce71 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -145,6 +145,9 @@ }, "Block the number" : { + }, + "Call history" : { + }, "Calls" : { @@ -253,9 +256,6 @@ }, "First name*" : { - }, - "History Contact fragment" : { - }, "I prefere create an account" : { diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 6c677de8a..62f76caca 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -61,6 +61,7 @@ struct ContentView: View { Spacer() Button(action: { self.index = 0 + historyViewModel.displayedCall = nil }, label: { VStack { Image("address-book") @@ -178,7 +179,6 @@ struct ContentView: View { Button(role: .destructive) { isMenuOpen = false isShowDeleteAllHistoryPopup.toggle() - //historyListViewModel.removeCallLogs() } label: { HStack { Text("Delete all history") @@ -353,6 +353,7 @@ struct ContentView: View { Spacer() Button(action: { self.index = 0 + historyViewModel.displayedCall = nil }, label: { VStack { Image("address-book") @@ -405,7 +406,7 @@ struct ContentView: View { } } - if contactViewModel.indexDisplayedFriend != nil || historyViewModel.indexDisplayedCall != nil { + if contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil { HStack(spacing: 0) { Spacer() .frame(maxWidth: @@ -426,7 +427,7 @@ struct ContentView: View { .background(Color.gray100) .ignoresSafeArea(.keyboard) } else if self.index == 1 { - HistoryContactFragment() + HistoryContactFragment(historyViewModel: historyViewModel, isShowDeleteAllHistoryPopup: $isShowDeleteAllHistoryPopup) .frame(maxWidth: .infinity) .background(Color.gray100) .ignoresSafeArea(.keyboard) @@ -581,7 +582,7 @@ struct ContentView: View { } } .onRotate { newOrientation in - if (contactViewModel.indexDisplayedFriend != nil || historyViewModel.indexDisplayedCall != nil) && searchIsActive { + if (contactViewModel.indexDisplayedFriend != nil || historyViewModel.displayedCall != nil) && searchIsActive { self.focusedField = false } else if searchIsActive { self.focusedField = true diff --git a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift index ef7dc3419..97489fb0b 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift @@ -20,15 +20,410 @@ import SwiftUI struct HistoryContactFragment: View { - var body: some View { - VStack { - Spacer() - Text("History Contact fragment") - Spacer() + + @State private var orientation = UIDevice.current.orientation + + @ObservedObject var sharedMainViewModel = SharedMainViewModel() + @ObservedObject var historyViewModel: HistoryViewModel + + @State var isMenuOpen = false + + @Binding var isShowDeleteAllHistoryPopup: Bool + + var body: some View { + NavigationView { + VStack(spacing: 1) { + Rectangle() + .foregroundColor(Color.orangeMain500) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + + HStack { + if !(orientation == .landscapeLeft || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.top, 2) + .onTapGesture { + withAnimation { + historyViewModel.displayedCall = nil + } + } + } + + Text("Call history") + .default_text_style_orange_800(styleSize: 20) + + Spacer() + + Menu { + Button { + isMenuOpen = false + } label: { + HStack { + Text("See all") + Spacer() + Image("green-check") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + } + } + + Button { + isMenuOpen = false + } label: { + HStack { + Text("See Linphone contact") + Spacer() + Image("green-check") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + } + } + + Button(role: .destructive) { + isMenuOpen = false + } label: { + HStack { + Text("Delete all history") + Spacer() + Image("trash-simple-red") + .resizable() + .frame(width: 25, height: 25, alignment: .leading) + } + } + } label: { + Image("dots-three-vertical") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + } + .padding(.leading) + .onTapGesture { + isMenuOpen = true + } + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + ScrollView { + VStack(spacing: 0) { + 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 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() + } + } + } 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( + firstName: historyViewModel.displayedCall!.toAddress!.displayName!, + lastName: historyViewModel.displayedCall!.toAddress!.displayName!.components(separatedBy: " ").count > 1 + ? historyViewModel.displayedCall!.toAddress!.displayName!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 100, height: 100) + .clipShape(Circle()) + + Text(historyViewModel.displayedCall!.toAddress!.displayName!) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + Text("") + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + } else { + Image(uiImage: ContactsManager.shared.textToImage( + firstName: historyViewModel.displayedCall!.toAddress!.username ?? "Username Error", + lastName: historyViewModel.displayedCall!.toAddress!.username!.components(separatedBy: " ").count > 1 + ? historyViewModel.displayedCall!.toAddress!.username!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 100, height: 100) + .clipShape(Circle()) + + Text(historyViewModel.displayedCall!.toAddress!.username ?? "Username Error") + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + Text("") + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + } + + } else if historyViewModel.displayedCall!.fromAddress != nil { + if historyViewModel.displayedCall!.fromAddress!.displayName != nil { + Image(uiImage: ContactsManager.shared.textToImage( + firstName: historyViewModel.displayedCall!.fromAddress!.displayName!, + lastName: historyViewModel.displayedCall!.fromAddress!.displayName!.components(separatedBy: " ").count > 1 + ? historyViewModel.displayedCall!.fromAddress!.displayName!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 100, height: 100) + .clipShape(Circle()) + + Text(historyViewModel.displayedCall!.fromAddress!.displayName!) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + Text("") + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + } else { + Image(uiImage: ContactsManager.shared.textToImage( + firstName: historyViewModel.displayedCall!.fromAddress!.username ?? "Username Error", + lastName: historyViewModel.displayedCall!.fromAddress!.username!.components(separatedBy: " ").count > 1 + ? historyViewModel.displayedCall!.fromAddress!.username!.components(separatedBy: " ")[1] + : "")) + .resizable() + .frame(width: 100, height: 100) + .clipShape(Circle()) + + Text(historyViewModel.displayedCall!.fromAddress!.username ?? "Username Error") + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + Text("") + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + } + } + } + if historyViewModel.displayedCall != nil + && addressFriend != nil + && addressFriend!.name != nil { + Text((addressFriend!.name)!) + .foregroundStyle(Color.grayMain2c700) + .multilineTextAlignment(.center) + .default_text_style(styleSize: 14) + .frame(maxWidth: .infinity) + .padding(.top, 10) + + Text("En ligne") + .foregroundStyle(Color.greenSuccess500) + .multilineTextAlignment(.center) + .default_text_style_300(styleSize: 12) + .frame(maxWidth: .infinity) + .frame(height: 20) + } + } + .frame(minHeight: 150) + .frame(maxWidth: .infinity) + .padding(.top, 10) + .background(Color.gray100) + + HStack { + Spacer() + + Button(action: { + }, label: { + VStack { + HStack(alignment: .center) { + Image("phone") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + .onTapGesture { + withAnimation { + + } + } + } + .padding(16) + .background(Color.grayMain2c200) + .cornerRadius(40) + + Text("Appel") + .default_text_style(styleSize: 14) + } + }) + + Spacer() + + Button(action: { + + }, label: { + VStack { + HStack(alignment: .center) { + Image("chat-teardrop-text") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + .onTapGesture { + withAnimation { + + } + } + } + .padding(16) + .background(Color.grayMain2c200) + .cornerRadius(40) + + Text("Message") + .default_text_style(styleSize: 14) + } + }) + + Spacer() + + Button(action: { + + }, label: { + VStack { + HStack(alignment: .center) { + Image("video-camera") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c600) + .frame(width: 25, height: 25) + .onTapGesture { + withAnimation { + + } + } + } + .padding(16) + .background(Color.grayMain2c200) + .cornerRadius(40) + + Text("Video Call") + .default_text_style(styleSize: 14) + } + }) + + Spacer() + } + .padding(.top, 20) + .frame(maxWidth: .infinity) + .background(Color.gray100) + + 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 addressFriend = historyViewModel.displayedCall != nil ? (historyViewModel.displayedCall!.dir == .Incoming ? fromAddressFriend : toAddressFriend) : nil + + if historyViewModel.displayedCall != nil && addressFriend != nil && addressFriend != nil { + ForEach(0.. Date: Mon, 20 Nov 2023 13:29:22 +0100 Subject: [PATCH 04/14] Converting SharedMainViewModel class into singleton --- Linphone/Core/CoreContext.swift | 8 +++--- Linphone/LinphoneApp.swift | 10 ++++---- Linphone/Localizable.xcstrings | 6 +++++ Linphone/UI/Assistant/AssistantView.swift | 8 +++--- .../Assistant/Fragments/LoginFragment.swift | 10 ++++---- .../Fragments/PermissionsFragment.swift | 4 +-- .../Fragments/ProfileModeFragment.swift | 6 ++--- .../Fragments/QrCodeScannerFragment.swift | 3 ++- .../ThirdPartySipAccountLoginFragment.swift | 4 +-- .../ThirdPartySipAccountWarningFragment.swift | 6 ++--- .../UI/Assistant/Viewmodel/QRScanner.swift | 5 ++-- .../Fragments/ContactInnerFragment.swift | 2 +- .../Fragments/ContactListBottomSheet.swift | 4 +++ .../Fragments/EditContactFragment.swift | 2 +- Linphone/UI/Main/ContentView.swift | 6 ++--- .../UI/Main/Fragments/PopupLoadingView.swift | 4 +-- Linphone/UI/Main/Fragments/PopupView.swift | 4 +-- Linphone/UI/Main/Fragments/ToastView.swift | 25 +++++++++++++++---- .../Fragments/HistoryContactFragment.swift | 4 ++- .../Fragments/HistoryListBottomSheet.swift | 5 ++++ Linphone/UI/Main/History/HistoryView.swift | 2 +- .../ViewModel/HistoryListViewModel.swift | 11 ++++++-- .../Main/Viewmodel/SharedMainViewModel.swift | 6 ++++- Linphone/UI/Welcome/WelcomeView.swift | 8 +++--- 24 files changed, 99 insertions(+), 54 deletions(-) diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 350fb6a0d..386b9f07b 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -24,11 +24,11 @@ import Combine final class CoreContext: ObservableObject { static let shared = CoreContext() + private var sharedMainViewModel = SharedMainViewModel.shared var coreVersion: String = Core.getVersion @Published var loggedIn: Bool = false @Published var loggingInProgress: Bool = false - @Published var toastMessage: String = "" @Published var defaultAccount: Account? private var mCore: Core! @@ -71,9 +71,9 @@ final class CoreContext: ObservableObject { self.mCore.publisher?.onConfiguringStatus?.postOnMainQueue { (cbVal: (core: Core, status: Config.ConfiguringState, message: String)) in NSLog("New configuration state is \(cbVal.status) = \(cbVal.message)\n") if cbVal.status == Config.ConfiguringState.Successful { - self.toastMessage = "Successful" + self.sharedMainViewModel.toastMessage = "Successful" } else { - self.toastMessage = "Failed" + self.sharedMainViewModel.toastMessage = "Failed" } } @@ -90,7 +90,7 @@ final class CoreContext: ObservableObject { } else if cbVal.state == .Progress { self.loggingInProgress = true } else { - self.toastMessage = "Registration failed" + self.sharedMainViewModel.toastMessage = "Registration failed" self.loggingInProgress = false self.loggedIn = false } diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 35b3a538e..52b399973 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -23,7 +23,7 @@ import SwiftUI struct LinphoneApp: App { @ObservedObject private var coreContext = CoreContext.shared - @ObservedObject private var sharedMainViewModel = SharedMainViewModel() + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @State private var isActive = false @@ -31,10 +31,10 @@ struct LinphoneApp: App { WindowGroup { if isActive { if !sharedMainViewModel.welcomeViewDisplayed { - WelcomeView(sharedMainViewModel: sharedMainViewModel) + WelcomeView() } else if coreContext.defaultAccount == nil || sharedMainViewModel.displayProfileMode { - AssistantView(sharedMainViewModel: sharedMainViewModel) - .toast(isShowing: $coreContext.toastMessage) + AssistantView() + .toast(isShowing: $sharedMainViewModel.toastMessage) } else if coreContext.defaultAccount != nil { ContentView( contactViewModel: ContactViewModel(), @@ -42,7 +42,7 @@ struct LinphoneApp: App { historyViewModel: HistoryViewModel(), historyListViewModel: HistoryListViewModel() ) - .toast(isShowing: $coreContext.toastMessage) + .toast(isShowing: $sharedMainViewModel.toastMessage) } } else { SplashScreen(isActive: $isActive) diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 7965caa6a..8a1a3da17 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -262,6 +262,9 @@ }, "First name*" : { + }, + "History has been deleted" : { + }, "I prefere create an account" : { @@ -414,6 +417,9 @@ }, "SIP address :" : { + }, + "SIP address copied into clipboard" : { + }, "sip.linphone.org" : { diff --git a/Linphone/UI/Assistant/AssistantView.swift b/Linphone/UI/Assistant/AssistantView.swift index 1819486d8..6c4418bcf 100644 --- a/Linphone/UI/Assistant/AssistantView.swift +++ b/Linphone/UI/Assistant/AssistantView.swift @@ -21,18 +21,18 @@ import SwiftUI struct AssistantView: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject private var coreContext = CoreContext.shared var body: some View { if sharedMainViewModel.displayProfileMode && coreContext.loggedIn { - ProfileModeFragment(sharedMainViewModel: sharedMainViewModel) + ProfileModeFragment() } else { - LoginFragment(accountLoginViewModel: AccountLoginViewModel(), sharedMainViewModel: sharedMainViewModel) + LoginFragment(accountLoginViewModel: AccountLoginViewModel()) } } } #Preview { - LoginFragment(accountLoginViewModel: AccountLoginViewModel(), sharedMainViewModel: SharedMainViewModel()) + LoginFragment(accountLoginViewModel: AccountLoginViewModel()) } diff --git a/Linphone/UI/Assistant/Fragments/LoginFragment.swift b/Linphone/UI/Assistant/Fragments/LoginFragment.swift index 61677f584..8ae3e46e1 100644 --- a/Linphone/UI/Assistant/Fragments/LoginFragment.swift +++ b/Linphone/UI/Assistant/Fragments/LoginFragment.swift @@ -22,8 +22,8 @@ import SwiftUI struct LoginFragment: View { @ObservedObject private var coreContext = CoreContext.shared + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject var accountLoginViewModel: AccountLoginViewModel - @ObservedObject var sharedMainViewModel: SharedMainViewModel @State private var isSecured: Bool = true @@ -183,7 +183,7 @@ struct LoginFragment: View { .padding(.bottom) NavigationLink(isActive: $isLinkSIPActive, destination: { - ThirdPartySipAccountWarningFragment(sharedMainViewModel: sharedMainViewModel, accountLoginViewModel: accountLoginViewModel) + ThirdPartySipAccountWarningFragment(accountLoginViewModel: accountLoginViewModel) }, label: { Text("Use SIP Account") .default_text_style_orange_600(styleSize: 20) @@ -268,7 +268,7 @@ struct LoginFragment: View { let contentPopup3 = Text(" et ") let contentPopup4 = Text("[nos conditions d’utilisation](https://linphone.org/general-terms)").underline() let contentPopup5 = Text(".") - PopupView(sharedMainViewModel: sharedMainViewModel, isShowPopup: $isShowPopup, + PopupView(isShowPopup: $isShowPopup, title: Text("Conditions de service"), content: contentPopup1 + contentPopup2 + contentPopup3 + contentPopup4 + contentPopup5, titleFirstButton: Text("Deny all"), @@ -283,7 +283,7 @@ struct LoginFragment: View { } if coreContext.loggingInProgress { - PopupLoadingView(sharedMainViewModel: sharedMainViewModel) + PopupLoadingView() .background(.black.opacity(0.65)) } } @@ -306,5 +306,5 @@ struct LoginFragment: View { } #Preview { - LoginFragment(accountLoginViewModel: AccountLoginViewModel(), sharedMainViewModel: SharedMainViewModel()) + LoginFragment(accountLoginViewModel: AccountLoginViewModel()) } diff --git a/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift b/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift index efb732cd4..82f709bac 100644 --- a/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift +++ b/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift @@ -21,7 +21,7 @@ import SwiftUI struct PermissionsFragment: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared var permissionManager = PermissionManager.shared @@ -204,5 +204,5 @@ struct PermissionsFragment: View { } #Preview { - PermissionsFragment(sharedMainViewModel: SharedMainViewModel()) + PermissionsFragment() } diff --git a/Linphone/UI/Assistant/Fragments/ProfileModeFragment.swift b/Linphone/UI/Assistant/Fragments/ProfileModeFragment.swift index 0eec8a5e0..fbe34e463 100644 --- a/Linphone/UI/Assistant/Fragments/ProfileModeFragment.swift +++ b/Linphone/UI/Assistant/Fragments/ProfileModeFragment.swift @@ -21,7 +21,7 @@ import SwiftUI struct ProfileModeFragment: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @State var options: Int = 1 @State private var isShowPopup = false @@ -142,7 +142,7 @@ struct ProfileModeFragment: View { } if self.isShowPopup { - PopupView(sharedMainViewModel: sharedMainViewModel, isShowPopup: $isShowPopup, + PopupView(isShowPopup: $isShowPopup, title: Text(isShowPopupForDefault ? "Default mode" : "Interoperable mode"), content: Text( isShowPopupForDefault @@ -167,5 +167,5 @@ struct ProfileModeFragment: View { } #Preview { - ProfileModeFragment(sharedMainViewModel: SharedMainViewModel()) + ProfileModeFragment() } diff --git a/Linphone/UI/Assistant/Fragments/QrCodeScannerFragment.swift b/Linphone/UI/Assistant/Fragments/QrCodeScannerFragment.swift index d5c9ad36e..31045592c 100644 --- a/Linphone/UI/Assistant/Fragments/QrCodeScannerFragment.swift +++ b/Linphone/UI/Assistant/Fragments/QrCodeScannerFragment.swift @@ -22,6 +22,7 @@ import SwiftUI struct QrCodeScannerFragment: View { @ObservedObject private var coreContext = CoreContext.shared + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @Environment(\.dismiss) var dismiss @@ -54,7 +55,7 @@ struct QrCodeScannerFragment: View { .edgesIgnoringSafeArea(.all) .navigationBarHidden(true) - if coreContext.toastMessage == "Successful" { + if sharedMainViewModel.toastMessage == "Successful" { ZStack { }.onAppear { diff --git a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift index b8be41481..6bb7e3bd0 100644 --- a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift +++ b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountLoginFragment.swift @@ -21,7 +21,7 @@ import SwiftUI struct ThirdPartySipAccountLoginFragment: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject private var coreContext = CoreContext.shared @ObservedObject var accountLoginViewModel: AccountLoginViewModel @@ -233,5 +233,5 @@ struct ThirdPartySipAccountLoginFragment: View { } #Preview { - ThirdPartySipAccountLoginFragment(sharedMainViewModel: SharedMainViewModel(), accountLoginViewModel: AccountLoginViewModel()) + ThirdPartySipAccountLoginFragment(accountLoginViewModel: AccountLoginViewModel()) } diff --git a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountWarningFragment.swift b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountWarningFragment.swift index 3d85dcd02..a3aa14ad5 100644 --- a/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountWarningFragment.swift +++ b/Linphone/UI/Assistant/Fragments/ThirdPartySipAccountWarningFragment.swift @@ -21,7 +21,7 @@ import SwiftUI struct ThirdPartySipAccountWarningFragment: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject private var coreContext = CoreContext.shared @ObservedObject var accountLoginViewModel: AccountLoginViewModel @@ -152,7 +152,7 @@ struct ThirdPartySipAccountWarningFragment: View { .padding(.horizontal) NavigationLink(destination: { - ThirdPartySipAccountLoginFragment(sharedMainViewModel: sharedMainViewModel, accountLoginViewModel: accountLoginViewModel) + ThirdPartySipAccountLoginFragment(accountLoginViewModel: accountLoginViewModel) }, label: { Text("I understand") .default_text_style_white_600(styleSize: 20) @@ -178,5 +178,5 @@ struct ThirdPartySipAccountWarningFragment: View { } #Preview { - ThirdPartySipAccountWarningFragment(sharedMainViewModel: SharedMainViewModel(), accountLoginViewModel: AccountLoginViewModel()) + ThirdPartySipAccountWarningFragment(accountLoginViewModel: AccountLoginViewModel()) } diff --git a/Linphone/UI/Assistant/Viewmodel/QRScanner.swift b/Linphone/UI/Assistant/Viewmodel/QRScanner.swift index 5d546ef96..a7f3d2eeb 100644 --- a/Linphone/UI/Assistant/Viewmodel/QRScanner.swift +++ b/Linphone/UI/Assistant/Viewmodel/QRScanner.swift @@ -43,6 +43,7 @@ struct QRScanner: UIViewControllerRepresentable { class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { private var coreContext = CoreContext.shared + private var sharedMainViewModel = SharedMainViewModel.shared @Binding var scanResult: String private var lastResult: String = "" @@ -76,10 +77,10 @@ class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { try? core.start() } } else { - coreContext.toastMessage = "Invalide URI" + sharedMainViewModel.toastMessage = "Invalide URI" } } else { - coreContext.toastMessage = "Invalide URI" + sharedMainViewModel.toastMessage = "Invalide URI" } } } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift index ac6a8e5cd..84f5b996b 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift @@ -23,7 +23,7 @@ import ContactsUI struct ContactInnerFragment: View { - @ObservedObject private var sharedMainViewModel = SharedMainViewModel() + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject var magicSearch = MagicSearchSingleton.shared diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactListBottomSheet.swift b/Linphone/UI/Main/Contacts/Fragments/ContactListBottomSheet.swift index bbf9ac0cc..a35158ec4 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactListBottomSheet.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactListBottomSheet.swift @@ -25,6 +25,7 @@ struct ContactListBottomSheet: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @ObservedObject var magicSearch = MagicSearchSingleton.shared + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject var contactViewModel: ContactViewModel @@ -68,6 +69,9 @@ struct ContactListBottomSheet: View { showingSheet.toggle() dismiss() } + + sharedMainViewModel.toastMessage = "Success_copied_into_clipboard" + } label: { HStack { Image("copy") diff --git a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift index e7de05556..e82feb785 100644 --- a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift @@ -23,7 +23,7 @@ import linphonesw struct EditContactFragment: View { @ObservedObject var editContactViewModel: EditContactViewModel - @ObservedObject private var sharedMainViewModel = SharedMainViewModel() + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @Environment(\.dismiss) var dismiss diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index ad1d37600..8b7ea36b9 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -488,7 +488,7 @@ struct ContentView: View { } if isShowDeleteContactPopup { - PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDeleteContactPopup, + PopupView(isShowPopup: $isShowDeleteContactPopup, title: Text( contactViewModel.selectedFriend != nil ? "Delete \(contactViewModel.selectedFriend!.name!)?" @@ -530,7 +530,7 @@ struct ContentView: View { } if isShowDeleteAllHistoryPopup { - PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDeleteContactPopup, + PopupView(isShowPopup: $isShowDeleteContactPopup, title: Text("Do you really want to delete all calls history?"), content: Text("All calls will be removed from the history."), titleFirstButton: Text("Cancel"), @@ -552,7 +552,7 @@ struct ContentView: View { } if isShowDismissPopup { - PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: $isShowDismissPopup, + PopupView(isShowPopup: $isShowDismissPopup, title: Text("Don’t save modifications?"), content: Text("All modifications will be canceled."), titleFirstButton: Text("Cancel"), diff --git a/Linphone/UI/Main/Fragments/PopupLoadingView.swift b/Linphone/UI/Main/Fragments/PopupLoadingView.swift index 99f4f063e..0e1eb2aa6 100644 --- a/Linphone/UI/Main/Fragments/PopupLoadingView.swift +++ b/Linphone/UI/Main/Fragments/PopupLoadingView.swift @@ -21,7 +21,7 @@ import SwiftUI struct PopupLoadingView: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared var body: some View { GeometryReader { geometry in @@ -54,6 +54,6 @@ struct PopupLoadingView: View { } #Preview { - PopupLoadingView(sharedMainViewModel: SharedMainViewModel()) + PopupLoadingView() .background(.black.opacity(0.65)) } diff --git a/Linphone/UI/Main/Fragments/PopupView.swift b/Linphone/UI/Main/Fragments/PopupView.swift index dbfd53afa..ad4bb3dfd 100644 --- a/Linphone/UI/Main/Fragments/PopupView.swift +++ b/Linphone/UI/Main/Fragments/PopupView.swift @@ -22,7 +22,7 @@ import Photos struct PopupView: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared var permissionManager = PermissionManager.shared @@ -100,7 +100,7 @@ struct PopupView: View { } #Preview { - PopupView(sharedMainViewModel: SharedMainViewModel(), isShowPopup: .constant(true), + PopupView(isShowPopup: .constant(true), title: Text("Title"), content: Text("Content"), titleFirstButton: Text("Deny all"), diff --git a/Linphone/UI/Main/Fragments/ToastView.swift b/Linphone/UI/Main/Fragments/ToastView.swift index b24ba953e..4caf6acab 100644 --- a/Linphone/UI/Main/Fragments/ToastView.swift +++ b/Linphone/UI/Main/Fragments/ToastView.swift @@ -21,7 +21,7 @@ import SwiftUI struct ToastView: ViewModifier { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @Binding var isShowing: String @@ -29,6 +29,7 @@ struct ToastView: ViewModifier { ZStack { content toastView + .padding(.top, 60) } } @@ -36,11 +37,11 @@ struct ToastView: ViewModifier { VStack { if !isShowing.isEmpty { HStack { - Image(isShowing == "Successful" ? "smiley" : "warning-circle") + Image(isShowing.contains("Success") ? "check" : "warning-circle") .resizable() .renderingMode(.template) .frame(width: 25, height: 25, alignment: .leading) - .foregroundStyle(isShowing == "Successful" ? Color.greenSuccess500 : Color.redDanger500) + .foregroundStyle(isShowing.contains("Success") ? Color.greenSuccess500 : Color.redDanger500) switch isShowing { case "Successful": @@ -50,6 +51,20 @@ struct ToastView: ViewModifier { .default_text_style(styleSize: 15) .padding(8) + case "Success_remove_call_logs": + Text("History has been deleted") + .multilineTextAlignment(.center) + .foregroundStyle(Color.greenSuccess500) + .default_text_style(styleSize: 15) + .padding(8) + + case "Success_copied_into_clipboard": + Text("SIP address copied into clipboard") + .multilineTextAlignment(.center) + .foregroundStyle(Color.greenSuccess500) + .default_text_style(styleSize: 15) + .padding(8) + case "Failed": Text("Invalid QR code!") .multilineTextAlignment(.center) @@ -85,7 +100,7 @@ struct ToastView: ViewModifier { .overlay( RoundedRectangle(cornerRadius: 50) .inset(by: 0.5) - .stroke(isShowing == "Successful" ? Color.greenSuccess500 : Color.redDanger500, lineWidth: 1) + .stroke(isShowing.contains("Success") ? Color.greenSuccess500 : Color.redDanger500, lineWidth: 1) ) .onTapGesture { isShowing = "" @@ -108,6 +123,6 @@ struct ToastView: ViewModifier { extension View { func toast(isShowing: Binding) -> some View { - self.modifier(ToastView(sharedMainViewModel: SharedMainViewModel(), isShowing: isShowing)) + self.modifier(ToastView(isShowing: isShowing)) } } diff --git a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift index 8062d5913..cb4916f6f 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift @@ -24,7 +24,7 @@ struct HistoryContactFragment: View { @State private var orientation = UIDevice.current.orientation - @ObservedObject var sharedMainViewModel = SharedMainViewModel() + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @ObservedObject var historyViewModel: HistoryViewModel @ObservedObject var historyListViewModel: HistoryListViewModel @ObservedObject var contactViewModel: ContactViewModel @@ -131,6 +131,8 @@ struct HistoryContactFragment: View { forPasteboardType: UTType.plainText.identifier ) } + + sharedMainViewModel.toastMessage = "Success_copied_into_clipboard" } label: { HStack { Text("Copy SIP address") diff --git a/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift b/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift index a9c9092c1..233c18f57 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift @@ -26,6 +26,8 @@ struct HistoryListBottomSheet: View { private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + @ObservedObject var historyViewModel: HistoryViewModel @ObservedObject var contactViewModel: ContactViewModel @ObservedObject var editContactViewModel: EditContactViewModel @@ -161,6 +163,9 @@ struct HistoryListBottomSheet: View { showingSheet.toggle() dismiss() } + + sharedMainViewModel.toastMessage = "Success_copied_into_clipboard" + } label: { HStack { Image("copy") diff --git a/Linphone/UI/Main/History/HistoryView.swift b/Linphone/UI/Main/History/HistoryView.swift index a308d4688..8b4977b97 100644 --- a/Linphone/UI/Main/History/HistoryView.swift +++ b/Linphone/UI/Main/History/HistoryView.swift @@ -42,7 +42,7 @@ struct HistoryView: View { ) Button { - + SharedMainViewModel.shared.toastMessage = "Success_remove_call_logs" } label: { Image("phone-plus") .padding() diff --git a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift index 8321523f6..4758564a2 100644 --- a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift +++ b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift @@ -155,8 +155,11 @@ class HistoryListViewModel: ObservableObject { } else { core.clearCallLogs() } - self.callLogs.removeAll() - self.callLogsTmp.removeAll() + + DispatchQueue.main.async { + self.callLogs.removeAll() + self.callLogsTmp.removeAll() + } } } else { removeCallLogsWithAddress() @@ -167,6 +170,10 @@ class HistoryListViewModel: ObservableObject { func removeCallLogsWithAddress() { self.callLogs.filter { $0.toAddress!.asStringUriOnly() == callLogsAddressToDelete || $0.fromAddress!.asStringUriOnly() == callLogsAddressToDelete }.forEach { callLog in removeCallLog(callLog: callLog) + + coreContext.doOnCoreQueue { core in + core.removeCallLog(callLog: callLog) + } } } diff --git a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift index 6adcc0dcc..189962e19 100644 --- a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift +++ b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift @@ -21,6 +21,10 @@ import linphonesw class SharedMainViewModel: ObservableObject { + static let shared = SharedMainViewModel() + + @Published var toastMessage: String = "" + @Published var welcomeViewDisplayed = false @Published var generalTermsAccepted = false @Published var displayProfileMode = false @@ -31,7 +35,7 @@ class SharedMainViewModel: ObservableObject { var maxWidth = 400.0 - init() { + private init() { let preferences = UserDefaults.standard if preferences.object(forKey: welcomeViewKey) == nil { diff --git a/Linphone/UI/Welcome/WelcomeView.swift b/Linphone/UI/Welcome/WelcomeView.swift index 5a4d6e700..6e19048a4 100644 --- a/Linphone/UI/Welcome/WelcomeView.swift +++ b/Linphone/UI/Welcome/WelcomeView.swift @@ -21,7 +21,7 @@ import SwiftUI struct WelcomeView: View { - @ObservedObject var sharedMainViewModel: SharedMainViewModel + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared @State private var index = 0 @@ -39,7 +39,7 @@ struct WelcomeView: View { VStack(alignment: .trailing) { NavigationLink(destination: { - PermissionsFragment(sharedMainViewModel: sharedMainViewModel) + PermissionsFragment() }, label: { Text("Skip") .underline() @@ -96,7 +96,7 @@ struct WelcomeView: View { if index == 2 { NavigationLink(destination: { - PermissionsFragment(sharedMainViewModel: sharedMainViewModel) + PermissionsFragment() }, label: { Text("Start") .default_text_style_white_600(styleSize: 20) @@ -158,5 +158,5 @@ struct WelcomeView: View { } #Preview { - WelcomeView(sharedMainViewModel: SharedMainViewModel()) + WelcomeView() } From 0142a9146bfdec4595ce154031001f413beaafdf Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Mon, 20 Nov 2023 17:00:19 +0100 Subject: [PATCH 05/14] Display toast when user copy adress or delete call logs in History views --- Linphone.xcodeproj/project.pbxproj | 4 ++ Linphone/Core/CoreContext.swift | 9 ++-- Linphone/LinphoneApp.swift | 2 - .../Fragments/QrCodeScannerFragment.swift | 4 +- .../UI/Assistant/Viewmodel/QRScanner.swift | 6 ++- .../Fragments/ContactListBottomSheet.swift | 3 +- Linphone/UI/Main/ContentView.swift | 9 ++++ Linphone/UI/Main/Fragments/ToastView.swift | 51 ++++++++----------- .../Fragments/HistoryContactFragment.swift | 4 +- .../Fragments/HistoryListBottomSheet.swift | 3 +- Linphone/UI/Main/History/HistoryView.swift | 1 - .../Main/Viewmodel/SharedMainViewModel.swift | 2 - .../UI/Main/Viewmodel/ToastViewModel.swift | 19 +++++++ 13 files changed, 73 insertions(+), 44 deletions(-) create mode 100644 Linphone/UI/Main/Viewmodel/ToastViewModel.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 71f3fce2e..64e88979f 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D777DBB22AE12C5900565A99 /* ContactsManager.swift */; }; D78290B82ADD3910004AA85C /* ContactsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78290B72ADD3910004AA85C /* ContactsFragment.swift */; }; D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */; }; + D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */; }; D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */; }; D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBF2ACC2E390081A588 /* HistoryView.swift */; }; D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FC52ACC458A0081A588 /* SplashScreen.swift */; }; @@ -117,6 +118,7 @@ D777DBB22AE12C5900565A99 /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = ""; }; D78290B72ADD3910004AA85C /* ContactsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsFragment.swift; sourceTree = ""; }; D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactViewModel.swift; sourceTree = ""; }; + D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = ""; }; D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = ""; }; D7A03FBF2ACC2E390081A588 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; D7A03FC52ACC458A0081A588 /* SplashScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = ""; }; @@ -386,6 +388,7 @@ isa = PBXGroup; children = ( D7A2EDD52AC18115005D90FC /* SharedMainViewModel.swift */, + D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */, ); path = Viewmodel; sourceTree = ""; @@ -529,6 +532,7 @@ D719ABC92ABC6FD700B41C10 /* CoreContext.swift in Sources */, D7EAACCF2AD6ED8000AA6A8A /* PermissionsFragment.swift in Sources */, D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */, + D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */, D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */, D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */, D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */, diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 386b9f07b..4a76095bc 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -71,9 +71,11 @@ final class CoreContext: ObservableObject { self.mCore.publisher?.onConfiguringStatus?.postOnMainQueue { (cbVal: (core: Core, status: Config.ConfiguringState, message: String)) in NSLog("New configuration state is \(cbVal.status) = \(cbVal.message)\n") if cbVal.status == Config.ConfiguringState.Successful { - self.sharedMainViewModel.toastMessage = "Successful" + ToastViewModel.shared.toastMessage = "Successful" + ToastViewModel.shared.displayToast.toggle() } else { - self.sharedMainViewModel.toastMessage = "Failed" + ToastViewModel.shared.toastMessage = "Failed" + ToastViewModel.shared.displayToast.toggle() } } @@ -90,7 +92,8 @@ final class CoreContext: ObservableObject { } else if cbVal.state == .Progress { self.loggingInProgress = true } else { - self.sharedMainViewModel.toastMessage = "Registration failed" + ToastViewModel.shared.toastMessage = "Registration failed" + ToastViewModel.shared.displayToast.toggle() self.loggingInProgress = false self.loggedIn = false } diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 52b399973..6371e5bee 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -34,7 +34,6 @@ struct LinphoneApp: App { WelcomeView() } else if coreContext.defaultAccount == nil || sharedMainViewModel.displayProfileMode { AssistantView() - .toast(isShowing: $sharedMainViewModel.toastMessage) } else if coreContext.defaultAccount != nil { ContentView( contactViewModel: ContactViewModel(), @@ -42,7 +41,6 @@ struct LinphoneApp: App { historyViewModel: HistoryViewModel(), historyListViewModel: HistoryListViewModel() ) - .toast(isShowing: $sharedMainViewModel.toastMessage) } } else { SplashScreen(isActive: $isActive) diff --git a/Linphone/UI/Assistant/Fragments/QrCodeScannerFragment.swift b/Linphone/UI/Assistant/Fragments/QrCodeScannerFragment.swift index 31045592c..a12a98419 100644 --- a/Linphone/UI/Assistant/Fragments/QrCodeScannerFragment.swift +++ b/Linphone/UI/Assistant/Fragments/QrCodeScannerFragment.swift @@ -55,13 +55,15 @@ struct QrCodeScannerFragment: View { .edgesIgnoringSafeArea(.all) .navigationBarHidden(true) - if sharedMainViewModel.toastMessage == "Successful" { + /* + if $isShowToast { ZStack { }.onAppear { dismiss() } } + */ } } diff --git a/Linphone/UI/Assistant/Viewmodel/QRScanner.swift b/Linphone/UI/Assistant/Viewmodel/QRScanner.swift index a7f3d2eeb..014cf08f3 100644 --- a/Linphone/UI/Assistant/Viewmodel/QRScanner.swift +++ b/Linphone/UI/Assistant/Viewmodel/QRScanner.swift @@ -77,10 +77,12 @@ class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { try? core.start() } } else { - sharedMainViewModel.toastMessage = "Invalide URI" + ToastViewModel.shared.toastMessage = "Invalide URI" + ToastViewModel.shared.displayToast.toggle() } } else { - sharedMainViewModel.toastMessage = "Invalide URI" + ToastViewModel.shared.toastMessage = "Invalide URI" + ToastViewModel.shared.displayToast.toggle() } } } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactListBottomSheet.swift b/Linphone/UI/Main/Contacts/Fragments/ContactListBottomSheet.swift index a35158ec4..0a883e3de 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactListBottomSheet.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactListBottomSheet.swift @@ -70,7 +70,8 @@ struct ContactListBottomSheet: View { dismiss() } - sharedMainViewModel.toastMessage = "Success_copied_into_clipboard" + ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard" + ToastViewModel.shared.displayToast.toggle() } label: { HStack { diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 8b7ea36b9..cdffd8cd0 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -26,6 +26,7 @@ struct ContentView: View { @Environment(\.scenePhase) var scenePhase @ObservedObject private var coreContext = CoreContext.shared + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared var contactManager = ContactsManager.shared var magicSearch = MagicSearchSingleton.shared @@ -543,6 +544,9 @@ struct ContentView: View { historyListViewModel.removeCallLogs() self.isShowDeleteAllHistoryPopup.toggle() historyViewModel.displayedCall = nil + + ToastViewModel.shared.toastMessage = "Success_remove_call_logs" + ToastViewModel.shared.displayToast.toggle() }) .background(.black.opacity(0.65)) .zIndex(3) @@ -580,6 +584,11 @@ struct ContentView: View { self.isShowDismissPopup.toggle() } } + + //if sharedMainViewModel.displayToast { + ToastView() + .zIndex(3) + //} } } .overlay { diff --git a/Linphone/UI/Main/Fragments/ToastView.swift b/Linphone/UI/Main/Fragments/ToastView.swift index 4caf6acab..40d5e7e16 100644 --- a/Linphone/UI/Main/Fragments/ToastView.swift +++ b/Linphone/UI/Main/Fragments/ToastView.swift @@ -19,31 +19,21 @@ import SwiftUI -struct ToastView: ViewModifier { +struct ToastView: View { - @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + @ObservedObject private var toastViewModel = ToastViewModel.shared - @Binding var isShowing: String - - func body(content: Content) -> some View { - ZStack { - content - toastView - .padding(.top, 60) - } - } - - private var toastView: some View { + var body: some View { VStack { - if !isShowing.isEmpty { + if toastViewModel.displayToast { HStack { - Image(isShowing.contains("Success") ? "check" : "warning-circle") + Image(toastViewModel.toastMessage.contains("Success") ? "check" : "warning-circle") .resizable() .renderingMode(.template) .frame(width: 25, height: 25, alignment: .leading) - .foregroundStyle(isShowing.contains("Success") ? Color.greenSuccess500 : Color.redDanger500) + .foregroundStyle(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500) - switch isShowing { + switch toastViewModel.toastMessage { case "Successful": Text("QR code validated!") .multilineTextAlignment(.center) @@ -100,29 +90,30 @@ struct ToastView: ViewModifier { .overlay( RoundedRectangle(cornerRadius: 50) .inset(by: 0.5) - .stroke(isShowing.contains("Success") ? Color.greenSuccess500 : Color.redDanger500, lineWidth: 1) + .stroke(toastViewModel.toastMessage.contains("Success") ? Color.greenSuccess500 : Color.redDanger500, lineWidth: 1) ) .onTapGesture { - isShowing = "" + withAnimation { + toastViewModel.toastMessage = "" + toastViewModel.displayToast = false + } } .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - isShowing = "" + withAnimation { + toastViewModel.toastMessage = "" + toastViewModel.displayToast = false + } } } + + Spacer() } - Spacer() } - .frame(maxWidth: sharedMainViewModel.maxWidth) + .frame(maxWidth: SharedMainViewModel.shared.maxWidth) .padding(.horizontal, 16) .padding(.bottom, 18) - .animation(.linear(duration: 0.3), value: isShowing) - .transition(.opacity) - } -} - -extension View { - func toast(isShowing: Binding) -> some View { - self.modifier(ToastView(isShowing: isShowing)) + .transition(.move(edge: .top)) + .padding(.top, 60) } } diff --git a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift index cb4916f6f..07cc6a2a5 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift @@ -132,7 +132,9 @@ struct HistoryContactFragment: View { ) } - sharedMainViewModel.toastMessage = "Success_copied_into_clipboard" + ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard" + ToastViewModel.shared.displayToast.toggle() + } label: { HStack { Text("Copy SIP address") diff --git a/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift b/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift index 233c18f57..e5a58b79d 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryListBottomSheet.swift @@ -164,7 +164,8 @@ struct HistoryListBottomSheet: View { dismiss() } - sharedMainViewModel.toastMessage = "Success_copied_into_clipboard" + ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard" + ToastViewModel.shared.displayToast.toggle() } label: { HStack { diff --git a/Linphone/UI/Main/History/HistoryView.swift b/Linphone/UI/Main/History/HistoryView.swift index 8b4977b97..13c857b14 100644 --- a/Linphone/UI/Main/History/HistoryView.swift +++ b/Linphone/UI/Main/History/HistoryView.swift @@ -42,7 +42,6 @@ struct HistoryView: View { ) Button { - SharedMainViewModel.shared.toastMessage = "Success_remove_call_logs" } label: { Image("phone-plus") .padding() diff --git a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift index 189962e19..abb48824b 100644 --- a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift +++ b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift @@ -23,8 +23,6 @@ class SharedMainViewModel: ObservableObject { static let shared = SharedMainViewModel() - @Published var toastMessage: String = "" - @Published var welcomeViewDisplayed = false @Published var generalTermsAccepted = false @Published var displayProfileMode = false diff --git a/Linphone/UI/Main/Viewmodel/ToastViewModel.swift b/Linphone/UI/Main/Viewmodel/ToastViewModel.swift new file mode 100644 index 000000000..6fb3287a9 --- /dev/null +++ b/Linphone/UI/Main/Viewmodel/ToastViewModel.swift @@ -0,0 +1,19 @@ +// +// ToastViewModel.swift +// Linphone +// +// Created by Benoît Martins on 20/11/2023. +// + +import Foundation + +class ToastViewModel: ObservableObject { + + static let shared = ToastViewModel() + + var toastMessage: String = "" + @Published var displayToast = false + + private init() { + } +} From d7da763dae5cf2700522b60d84a436327d336ac1 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 21 Nov 2023 14:16:06 +0100 Subject: [PATCH 06/14] Refresh calllogs list when receive callback --- Linphone/LinphoneApp.swift | 25 ++++++++++--- .../Fragments/HistoryContactFragment.swift | 22 ++++++------ .../ViewModel/HistoryListViewModel.swift | 36 +++++++++++++++++++ 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 6371e5bee..86403a04f 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -25,6 +25,11 @@ struct LinphoneApp: App { @ObservedObject private var coreContext = CoreContext.shared @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + @State private var contactViewModel: ContactViewModel? + @State private var editContactViewModel: EditContactViewModel? + @State private var historyViewModel: HistoryViewModel? + @State private var historyListViewModel: HistoryListViewModel? + @State private var isActive = false var body: some Scene { @@ -34,16 +39,26 @@ struct LinphoneApp: App { WelcomeView() } else if coreContext.defaultAccount == nil || sharedMainViewModel.displayProfileMode { AssistantView() - } else if coreContext.defaultAccount != nil { + } else if coreContext.defaultAccount != nil + && contactViewModel != nil + && editContactViewModel != nil + && historyViewModel != nil + && historyListViewModel != nil { ContentView( - contactViewModel: ContactViewModel(), - editContactViewModel: EditContactViewModel(), - historyViewModel: HistoryViewModel(), - historyListViewModel: HistoryListViewModel() + contactViewModel: contactViewModel!, + editContactViewModel: editContactViewModel!, + historyViewModel: historyViewModel!, + historyListViewModel: historyListViewModel! ) } } else { SplashScreen(isActive: $isActive) + .onDisappear { + contactViewModel = ContactViewModel() + editContactViewModel = EditContactViewModel() + historyViewModel = HistoryViewModel() + historyListViewModel = HistoryListViewModel() + } } } } diff --git a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift index 07cc6a2a5..139245706 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryContactFragment.swift @@ -73,8 +73,6 @@ struct HistoryContactFragment: View { Button { isMenuOpen = false - indexPage = 0 - if ContactsManager.shared.getFriendWithAddress( address: historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing ? historyViewModel.displayedCall!.toAddress! @@ -87,11 +85,13 @@ struct HistoryContactFragment: View { let friendIndex = MagicSearchSingleton.shared.lastSearch.firstIndex( where: {$0.friend!.addresses.contains(where: {$0.asStringUriOnly() == addressCall.asStringUriOnly()})}) if friendIndex != nil { - - withAnimation { + + withAnimation { historyViewModel.displayedCall = nil - contactViewModel.indexDisplayedFriend = friendIndex - } + indexPage = 0 + + contactViewModel.indexDisplayedFriend = friendIndex + } } } else { let addressCall = historyViewModel.displayedCall != nil && historyViewModel.displayedCall!.dir == .Outgoing @@ -100,10 +100,12 @@ struct HistoryContactFragment: View { withAnimation { historyViewModel.displayedCall = nil - isShowEditContactFragment.toggle() - editContactViewModel.sipAddresses.removeAll() - editContactViewModel.sipAddresses.append(String(addressCall.asStringUriOnly().dropFirst(4))) - editContactViewModel.sipAddresses.append("") + indexPage = 0 + + isShowEditContactFragment.toggle() + editContactViewModel.sipAddresses.removeAll() + editContactViewModel.sipAddresses.append(String(addressCall.asStringUriOnly().dropFirst(4))) + editContactViewModel.sipAddresses.append("") } } diff --git a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift index 4758564a2..3251d2511 100644 --- a/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift +++ b/Linphone/UI/Main/History/ViewModel/HistoryListViewModel.swift @@ -26,9 +26,12 @@ class HistoryListViewModel: ObservableObject { @Published var callLogs: [CallLog] = [] var callLogsTmp: [CallLog] = [] + @Published private var coreDelegate: CoreDelegate? + var callLogsAddressToDelete = "" init() { + removeAllDelegate() computeCallLogsList() } @@ -46,6 +49,29 @@ class HistoryListViewModel: ObservableObject { self.callLogsTmp.append(log) } } + + DispatchQueue.main.async { + self.coreDelegate = CoreDelegateStub( + onCallLogUpdated: { (_: Core, _: CallLog) -> Void in + DispatchQueue.main.async { + let account = core.defaultAccount + let logs = account?.callLogs != nil ? account!.callLogs : core.callLogs + + self.callLogs.removeAll() + self.callLogsTmp.removeAll() + + logs.forEach { log in + self.callLogs.append(log) + self.callLogsTmp.append(log) + } + } + } + ) + if self.coreDelegate != nil { + core.addDelegate(delegate: self.coreDelegate!) + } + } + } } @@ -184,4 +210,14 @@ class HistoryListViewModel: ObservableObject { let indexTmp = self.callLogsTmp.firstIndex(where: {$0.callId == callLog.callId}) self.callLogsTmp.remove(at: indexTmp!) } + + func removeAllDelegate() { + coreContext.doOnCoreQueue { core in + if self.coreDelegate != nil { + core.removeDelegate(delegate: self.coreDelegate!) + self.coreDelegate = nil + } + } + } + } From 84da8a367b7a63b4de276c1c9137a5ecca9c222c Mon Sep 17 00:00:00 2001 From: "benoit.martins" Date: Tue, 21 Nov 2023 17:28:36 +0100 Subject: [PATCH 07/14] Fix Presence --- Linphone/Contacts/ContactsManager.swift | 5 +- Linphone/Utils/Avatar.swift | 105 ++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 Linphone/Utils/Avatar.swift diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index 9db3527c0..74010a791 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -188,7 +188,7 @@ final class ContactsManager { self.coreContext.doOnCoreQueue { core in do { let friend = try existingFriend ?? core.createFriend() - + friend.edit() friend.nativeUri = contact.identifier try friend.setName(newValue: contact.firstName + " " + contact.lastName) @@ -274,6 +274,9 @@ final class ContactsManager { func getFriendWithContact(contact: Contact) -> Friend? { if friendList != nil { let friend = friendList!.friends.first(where: {$0.nativeUri == contact.identifier}) + if friend == nil && friendList != nil { + return linphoneFriendList!.friends.first(where: {$0.nativeUri == contact.identifier}) + } return friend } else { return nil diff --git a/Linphone/Utils/Avatar.swift b/Linphone/Utils/Avatar.swift new file mode 100644 index 000000000..16d3f9fba --- /dev/null +++ b/Linphone/Utils/Avatar.swift @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * 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 SwiftUI +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() + } + .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 + } + } +} From a209349f9501de459579502ba8ec460cde6d9d57 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Wed, 22 Nov 2023 18:23:16 +0100 Subject: [PATCH 08/14] Fix rebase --- Linphone.xcodeproj/project.pbxproj | 20 ++++++ Linphone/.DS_Store | Bin 6148 -> 8196 bytes .../presence-busy.imageset/Contents.json | 21 ++++++ .../presence-busy.imageset/presence-busy.svg | 5 ++ .../presence-online.imageset/Contents.json | 21 ++++++ .../presence-online.svg | 5 ++ Linphone/Contacts/ContactsManager.swift | 2 +- Linphone/Core/CoreContext.swift | 31 ++++++++- Linphone/Linphone.entitlements | 14 ++-- Linphone/LinphoneApp.swift | 4 +- Linphone/Ressources/linphonerc-default | 39 +++++++++++ Linphone/Ressources/linphonerc-factory | 65 ++++++++++++++++++ Linphone/SplashScreen.swift | 8 --- .../Fragments/ContactInnerFragment.swift | 23 +------ .../Fragments/ContactsListFragment.swift | 21 +----- .../Fragments/EditContactFragment.swift | 21 +----- .../FavoriteContactsListFragment.swift | 22 +----- Linphone/Utils/Avatar.swift | 2 + 18 files changed, 224 insertions(+), 100 deletions(-) create mode 100644 Linphone/Assets.xcassets/presence-busy.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/presence-busy.imageset/presence-busy.svg create mode 100644 Linphone/Assets.xcassets/presence-online.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/presence-online.imageset/presence-online.svg create mode 100644 Linphone/Ressources/linphonerc-default create mode 100644 Linphone/Ressources/linphonerc-factory diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 64e88979f..cb827f198 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -29,6 +29,8 @@ D72343362AD037AF009AA24E /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72343352AD037AF009AA24E /* ToastView.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 */; }; + D732A90D2B0376F500DB42BA /* linphonerc-factory in Resources */ = {isa = PBXBuildFile; fileRef = D732A90B2B0376F500DB42BA /* linphonerc-factory */; }; D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */; }; D732A9132B04C7A300DB42BA /* HistoryListFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */; }; D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */; }; @@ -51,6 +53,7 @@ D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBF2ACC2E390081A588 /* HistoryView.swift */; }; D7A03FC62ACC458A0081A588 /* SplashScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FC52ACC458A0081A588 /* SplashScreen.swift */; }; D7A2EDD62AC18115005D90FC /* SharedMainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A2EDD52AC18115005D90FC /* SharedMainViewModel.swift */; }; + D7ADF6002AFE356400212231 /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADF5FF2AFE356400212231 /* Avatar.swift */; }; D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */; }; D7C365082AEFAB7F00FE6142 /* ContactListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */; }; D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365092AF001C300FE6142 /* EditContactFragment.swift */; }; @@ -101,6 +104,8 @@ D72343352AD037AF009AA24E /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.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 = ""; }; + D732A90B2B0376F500DB42BA /* linphonerc-factory */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-factory"; sourceTree = ""; }; D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryFragment.swift; sourceTree = ""; }; D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListFragment.swift; sourceTree = ""; }; D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListViewModel.swift; sourceTree = ""; }; @@ -124,6 +129,7 @@ D7A03FC52ACC458A0081A588 /* SplashScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SplashScreen.swift; sourceTree = ""; }; D7A2EDD52AC18115005D90FC /* SharedMainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedMainViewModel.swift; sourceTree = ""; }; D7A2EDDA2AC19EEC005D90FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + D7ADF5FF2AFE356400212231 /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = ""; }; D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerFragment.swift; sourceTree = ""; }; D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListBottomSheet.swift; sourceTree = ""; }; D7C365092AF001C300FE6142 /* EditContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactFragment.swift; sourceTree = ""; }; @@ -178,6 +184,7 @@ D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */, D7C48DF32AFA66F900D938CB /* EditContactController.swift */, D732A9082AFD235500DB42BA /* ShareSheetController.swift */, + D7ADF5FF2AFE356400212231 /* Avatar.swift */, ); path = Utils; sourceTree = ""; @@ -214,6 +221,7 @@ D70C93DD2AC2D0F60063CA3B /* Localizable.xcstrings */, D719ABBD2ABC67BF00B41C10 /* Preview Content */, D7D24D0C2AC1B4C700C6F35B /* Fonts */, + D7ADF6012AFE5C7C00212231 /* Ressources */, ); path = Linphone; sourceTree = ""; @@ -393,6 +401,15 @@ path = Viewmodel; sourceTree = ""; }; + D7ADF6012AFE5C7C00212231 /* Ressources */ = { + isa = PBXGroup; + children = ( + D732A90A2B0376F500DB42BA /* linphonerc-default */, + D732A90B2B0376F500DB42BA /* linphonerc-factory */, + ); + path = Ressources; + sourceTree = ""; + }; D7D24D0C2AC1B4C700C6F35B /* Fonts */ = { isa = PBXGroup; children = ( @@ -487,6 +504,8 @@ D719ABBF2ABC67BF00B41C10 /* Preview Assets.xcassets in Resources */, D719ABBB2ABC67BF00B41C10 /* Assets.xcassets in Resources */, D7D24D132AC1B4E800C6F35B /* NotoSans-Medium.ttf in Resources */, + D732A90C2B0376F500DB42BA /* linphonerc-default in Resources */, + D732A90D2B0376F500DB42BA /* linphonerc-factory in Resources */, D70C93DE2AC2D0F60063CA3B /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -521,6 +540,7 @@ buildActionMask = 2147483647; files = ( D7C3650E2AF15BF200FE6142 /* PhotoPicker.swift in Sources */, + D7ADF6002AFE356400212231 /* Avatar.swift in Sources */, D71707202AC5989C0037746F /* TextExtension.swift in Sources */, D719ABB92ABC67BF00B41C10 /* ContentView.swift in Sources */, D71FCA832AE14D6E00D2E43E /* ContactFragment.swift in Sources */, diff --git a/Linphone/.DS_Store b/Linphone/.DS_Store index ee98363d48998eeedc9d54f56ee7d99d34976794..06ce575e56dfde609b73451038cc47f059aa7fea 100644 GIT binary patch delta 651 zcmcIiO-lkn7=E{PcLx>B$nYSjcUdwC>`-b_un;6kmpX)QX2P;tenfN#1Mjg65p?j< zSt9lr^e>`L{egZ!1c5zkAEmYY92KP;aNS=H-;pjeA=&^#hY6Q)hP8~S0*zCg zLV@ZyH>o;qknUV9{wisjm$txYpj*@eUUzq+nFST--goAHf8cfkhZp1vCB2l0rIL&M zMb0cwR6Z%zO~W#)P!TX@s5q#%g#3qybvUS?oBUCV3SB262ZTk5woytArHvc=fW+UE zARa=(PFqPO(}~zZnm@}~-|$^Td9&L$K@k+Run*Ik``Mi!dlz|NR^yv~#$eF>BZ%+$ MuXs;B|1TrgU%(`xY5)KL delta 120 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{Mvv5r;6q~50$jG%ZU^g=(*JK_6$IZ0@0*sSi z3-(MtC}g*}T!e*jV#%Gw>>M0|%s?GLAixbITtS*Q7Jg@*%rD~!GKzr-Vl2oehRyLj GbC>~z@fK46 diff --git a/Linphone/Assets.xcassets/presence-busy.imageset/Contents.json b/Linphone/Assets.xcassets/presence-busy.imageset/Contents.json new file mode 100644 index 000000000..227036d8d --- /dev/null +++ b/Linphone/Assets.xcassets/presence-busy.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "presence-busy.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/presence-busy.imageset/presence-busy.svg b/Linphone/Assets.xcassets/presence-busy.imageset/presence-busy.svg new file mode 100644 index 000000000..0f24966ca --- /dev/null +++ b/Linphone/Assets.xcassets/presence-busy.imageset/presence-busy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Linphone/Assets.xcassets/presence-online.imageset/Contents.json b/Linphone/Assets.xcassets/presence-online.imageset/Contents.json new file mode 100644 index 000000000..606200f2a --- /dev/null +++ b/Linphone/Assets.xcassets/presence-online.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "presence-online.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/presence-online.imageset/presence-online.svg b/Linphone/Assets.xcassets/presence-online.imageset/presence-online.svg new file mode 100644 index 000000000..ae3b6ed5e --- /dev/null +++ b/Linphone/Assets.xcassets/presence-online.imageset/presence-online.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index 74010a791..717bc923e 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -173,7 +173,7 @@ final class ContactsManager { self.saveFriend(result: result, contact: contact, existingFriend: existingFriend) { resultFriend in if resultFriend != nil { if linphoneFriend && existingFriend == nil { - _ = self.linphoneFriendList?.addLocalFriend(linphoneFriend: resultFriend!) + _ = self.linphoneFriendList?.addFriend(linphoneFriend: resultFriend!) self.linphoneFriendList?.updateSubscriptions() } else if existingFriend == nil { _ = self.friendList?.addLocalFriend(linphoneFriend: resultFriend!) diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 4a76095bc..5d025600b 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -30,11 +30,18 @@ final class CoreContext: ObservableObject { @Published var loggedIn: Bool = false @Published var loggingInProgress: Bool = false @Published var defaultAccount: Account? + @Published var coreIsStarted: Bool = false private var mCore: Core! private var mIteratePublisher: AnyCancellable? - private init() {} + private init() { + do { + try initialiseCore() + } catch { + + } + } func doOnCoreQueue(synchronous: Bool = false, lambda: @escaping (Core) -> Void) { if synchronous { @@ -53,7 +60,26 @@ final class CoreContext: ObservableObject { coreQueue.async { let configDir = Factory.Instance.getConfigDir(context: nil) - try? self.mCore = Factory.Instance.createCore(configPath: "\(configDir)/MyConfig", factoryConfigPath: "", systemContext: nil) + let url = NSURL(fileURLWithPath: configDir) + if let pathComponent = url.appendingPathComponent("linphonerc") { + let filePath = pathComponent.path + let fileManager = FileManager.default + if !fileManager.fileExists(atPath: filePath) { + let path = Bundle.main.path(forResource: "linphonerc-default", ofType: nil) + if path != nil { + try? FileManager.default.copyItem(at: NSURL(fileURLWithPath: path!) as URL, to: pathComponent) + } + } + } + + let config: Config! = Config.newForSharedCore( + appGroupId: "group.org.linphone.phone.msgNotification", + configFilename: "linphonerc", + factoryConfigFilename: Bundle.main.path(forResource: "linphonerc-factory", ofType: nil) + ) + + self.mCore = try? Factory.Instance.createCoreWithConfig(config: config, systemContext: nil) + self.mCore.autoIterateEnabled = false self.mCore.friendsDatabasePath = "\(configDir)/friends.db" @@ -89,6 +115,7 @@ final class CoreContext: ObservableObject { if cbVal.state == .Ok { self.loggingInProgress = false self.loggedIn = true + self.coreIsStarted = true } else if cbVal.state == .Progress { self.loggingInProgress = true } else { diff --git a/Linphone/Linphone.entitlements b/Linphone/Linphone.entitlements index f2ef3ae02..7fe270b49 100644 --- a/Linphone/Linphone.entitlements +++ b/Linphone/Linphone.entitlements @@ -2,9 +2,15 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + + com.apple.security.application-groups + + group.belledonne-communications.linphone + group.org.linphone.phone.linphoneExtension + group.org.linphone.phone.msgNotification + + com.apple.security.files.user-selected.read-only + diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 86403a04f..1075bea5b 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -30,11 +30,11 @@ struct LinphoneApp: App { @State private var historyViewModel: HistoryViewModel? @State private var historyListViewModel: HistoryListViewModel? - @State private var isActive = false + @State private var isActive = true var body: some Scene { WindowGroup { - if isActive { + if isActive && coreContext.coreIsStarted { if !sharedMainViewModel.welcomeViewDisplayed { WelcomeView() } else if coreContext.defaultAccount == nil || sharedMainViewModel.displayProfileMode { diff --git a/Linphone/Ressources/linphonerc-default b/Linphone/Ressources/linphonerc-default new file mode 100644 index 000000000..ea5356429 --- /dev/null +++ b/Linphone/Ressources/linphonerc-default @@ -0,0 +1,39 @@ + +## Start of default rc + +[sip] +contact="Linphone iPhone" +use_info=0 +use_ipv6=1 +keepalive_period=30000 +sip_port=-1 +sip_tcp_port=-1 +sip_tls_port=-1 +media_encryption=none +update_presence_model_timestamp_before_publish_expires_refresh=1 + +[net] +#Because dynamic bitrate adaption can increase bitrate, we must allow "no limit" +download_bw=0 +upload_bw=0 + +[video] +size=vga + +[app] +tunnel=disabled +auto_start=1 +record_aware=1 + +[tunnel] +host= +port=443 + +[misc] +log_collection_upload_server_url=https://www.linphone.org:444/lft.php +file_transfer_server_url=https://www.linphone.org:444/lft.php +version_check_url_root=https://www.linphone.org/releases +max_calls=10 +conference_layout=1 + +## End of default rc diff --git a/Linphone/Ressources/linphonerc-factory b/Linphone/Ressources/linphonerc-factory new file mode 100644 index 000000000..85b543074 --- /dev/null +++ b/Linphone/Ressources/linphonerc-factory @@ -0,0 +1,65 @@ + +## Start of factory rc + +# This file shall not contain path referencing package name, in order to be portable when app is renamed. +# Paths to resources must be set from LinphoneManager, after creating LinphoneCore. + +[net] +mtu=1300 +force_ice_disablement=0 + +[rtp] +accept_any_encryption=1 + +[sip] +guess_hostname=1 +register_only_when_network_is_up=1 +auto_net_state_mon=1 +auto_answer_replacing_calls=1 +ping_with_options=0 +use_cpim=1 +zrtp_key_agreements_suites=MS_ZRTP_KEY_AGREEMENT_K255_KYB512 +chat_messages_aggregation_delay=1000 +chat_messages_aggregation=1 +update_presence_model_timestamp_before_publish_expires_refresh=1 +rls_uri=sips:rls@sip.linphone.org + +[sound] +#remove this property for any application that is not Linphone public version itself +ec_calibrator_cool_tones=1 + +[video] +displaytype=MSAndroidTextureDisplay +auto_resize_preview_to_keep_ratio=1 +max_conference_size=vga + +[misc] +enable_basic_to_client_group_chat_room_migration=0 +enable_simple_group_chat_message_state=0 +aggregate_imdn=1 +notify_each_friend_individually_when_presence_received=0 +store_friends=0 + +[app] +activation_code_length=4 +prefer_basic_chat_room=1 +record_aware=1 + +[account_creator] +backend=1 +# 1 means FlexiAPI, 0 is XMLRPC +url=https://subscribe.linphone.org/api/ +# replace above URL by https://staging-subscribe.linphone.org/api/ for testing + +[lime] +lime_update_threshold=86400 + +[alerts] +alerts_enabled=1 + +[assistant] +algorithm=SHA-256 +password_min_length=6 +username_regex=^[a-z0-9+_.\-]*$ + +## End of factory rc diff --git a/Linphone/SplashScreen.swift b/Linphone/SplashScreen.swift index 350fc93ad..e8c11ef7c 100644 --- a/Linphone/SplashScreen.swift +++ b/Linphone/SplashScreen.swift @@ -38,14 +38,6 @@ struct SplashScreen: View { } .ignoresSafeArea(.all) - .onAppear { - Task { - try coreContext.initialiseCore() - withAnimation { - self.isActive = true - } - } - } } } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift index 84f5b996b..426205a74 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactInnerFragment.swift @@ -113,28 +113,7 @@ struct ContactInnerFragment: View { && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.photo != nil && !magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.photo!.isEmpty { - AsyncImage( - url: ContactsManager.shared.getImagePath( - friendPhotoPath: magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.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(friend: magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend!, avatarSize: 100) } else if contactViewModel.indexDisplayedFriend != nil && magicSearch.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil { Image("profil-picture-default") .resizable() diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift index 7b274e646..32e4f2ebe 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift @@ -64,26 +64,7 @@ struct ContactsListFragment: View { } if magicSearch.lastSearch[index].friend!.photo != nil && !magicSearch.lastSearch[index].friend!.photo!.isEmpty { - AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: magicSearch.lastSearch[index].friend!.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(friend: magicSearch.lastSearch[index].friend!, avatarSize: 45) } else { Image("profil-picture-default") .resizable() diff --git a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift index e82feb785..8209eb469 100644 --- a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift @@ -129,26 +129,7 @@ struct EditContactFragment: View { if editContactViewModel.selectedEditFriend != nil && editContactViewModel.selectedEditFriend!.photo != nil && !editContactViewModel.selectedEditFriend!.photo!.isEmpty && selectedImage == nil && !removedImage { - AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: editContactViewModel.selectedEditFriend!.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(friend: editContactViewModel.selectedEditFriend!, avatarSize: 100) } else if selectedImage == nil { Image("profil-picture-default") .resizable() diff --git a/Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift index 0eb2fa566..3c9a61bf1 100644 --- a/Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/FavoriteContactsListFragment.swift @@ -39,27 +39,7 @@ struct FavoriteContactsListFragment: View { VStack { if magicSearch.lastSearch[index].friend!.photo != nil && !magicSearch.lastSearch[index].friend!.photo!.isEmpty { - AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: magicSearch.lastSearch[index].friend!.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(friend: magicSearch.lastSearch[index].friend!, avatarSize: 45) } else { Image("profil-picture-default") .resizable() diff --git a/Linphone/Utils/Avatar.swift b/Linphone/Utils/Avatar.swift index 16d3f9fba..5597ba38a 100644 --- a/Linphone/Utils/Avatar.swift +++ b/Linphone/Utils/Avatar.swift @@ -83,8 +83,10 @@ struct Avatar: View { } func addDelegate() { + print("onPresenceReceivedonPresenceReceived \(friend.name) \(friend.consolidatedPresence)") let newFriendDelegate = FriendDelegateStub( onPresenceReceived: { (linphoneFriend: Friend) -> Void in + print("onPresenceReceivedonPresenceReceived delegate \(friend.name) \(friend.consolidatedPresence) \(linphoneFriend.consolidatedPresence)") self.presenceImage = linphoneFriend.consolidatedPresence == ConsolidatedPresence.Online ? "presence-online" : "presence-busy" } ) From b576785399bb86c5340ef8b1378af6569457074d Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Thu, 23 Nov 2023 17:21:49 +0100 Subject: [PATCH 09/14] Send and clear logs --- Linphone/Contacts/ContactsManager.swift | 3 ++ Linphone/Core/CoreContext.swift | 28 ++++++++---- Linphone/LinphoneApp.swift | 6 +-- Linphone/Localizable.xcstrings | 9 ++-- Linphone/Ressources/linphonerc-default | 1 + Linphone/SplashScreen.swift | 5 +-- Linphone/UI/Main/Fragments/SideMenu.swift | 53 ++++++++++++++++++++++- 7 files changed, 83 insertions(+), 22 deletions(-) diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index 717bc923e..44149bb49 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -67,6 +67,9 @@ final class ContactsManager { do { self.linphoneFriendList = try core.getFriendListByName(name: self.linphoneAddressBookFriendList) ?? core.createFriendList() + + //self.linphoneFriendList?.updateSubscriptions() + print("friendListfriendListfriendListfriendList \(self.linphoneFriendList!.rlsAddress)") } catch let error { print("\(#function) - Failed to enumerate contacts: \(error)") } diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 5d025600b..ecb07e916 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -60,6 +60,10 @@ final class CoreContext: ObservableObject { coreQueue.async { let configDir = Factory.Instance.getConfigDir(context: nil) + + Factory.Instance.logCollectionPath = configDir + Factory.Instance.enableLogCollection(state: LogCollectionState.Enabled) + let url = NSURL(fileURLWithPath: configDir) if let pathComponent = url.appendingPathComponent("linphonerc") { let filePath = pathComponent.path @@ -71,25 +75,32 @@ final class CoreContext: ObservableObject { } } } - - let config: Config! = Config.newForSharedCore( - appGroupId: "group.org.linphone.phone.msgNotification", - configFilename: "linphonerc", - factoryConfigFilename: Bundle.main.path(forResource: "linphonerc-factory", ofType: nil) - ) - - self.mCore = try? Factory.Instance.createCoreWithConfig(config: config, systemContext: nil) + + let config = try? Factory.Instance.createConfigWithFactory( + path: "\(configDir)/linphonerc", + factoryPath: Bundle.main.path(forResource: "linphonerc-factory", ofType: nil) + ) + if config != nil { + self.mCore = try? Factory.Instance.createCoreWithConfig(config: config!, systemContext: nil) + } self.mCore.autoIterateEnabled = false self.mCore.friendsDatabasePath = "\(configDir)/friends.db" + //self.mCore.logCollectionUploadServerUrl = "https://www.linphone.org:444/lft.php" + //self.mCore.friendListSubscriptionEnabled = true + + print("configDirconfigDir \(configDir)") + self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in if cbVal.state == GlobalState.On { self.defaultAccount = self.mCore.defaultAccount } else if cbVal.state == GlobalState.Off { self.defaultAccount = nil } + self.coreIsStarted = true } + try? self.mCore.start() // Create a Core listener to listen for the callback we need @@ -115,7 +126,6 @@ final class CoreContext: ObservableObject { if cbVal.state == .Ok { self.loggingInProgress = false self.loggedIn = true - self.coreIsStarted = true } else if cbVal.state == .Progress { self.loggingInProgress = true } else { diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 1075bea5b..159abb97d 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -30,11 +30,9 @@ struct LinphoneApp: App { @State private var historyViewModel: HistoryViewModel? @State private var historyListViewModel: HistoryListViewModel? - @State private var isActive = true - var body: some Scene { WindowGroup { - if isActive && coreContext.coreIsStarted { + if coreContext.coreIsStarted { if !sharedMainViewModel.welcomeViewDisplayed { WelcomeView() } else if coreContext.defaultAccount == nil || sharedMainViewModel.displayProfileMode { @@ -52,7 +50,7 @@ struct LinphoneApp: App { ) } } else { - SplashScreen(isActive: $isActive) + SplashScreen() .onDisappear { contactViewModel = ContactViewModel() editContactViewModel = EditContactViewModel() diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 8a1a3da17..d95a2a884 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -163,6 +163,9 @@ }, "Chiffrement de bout en bout de tous vos échanges, grâce au mode default vos communications sont à l’abri des regards." : { + }, + "Clear logs" : { + }, "Close" : { @@ -375,9 +378,6 @@ }, "Plus tard" : { - }, - "Posts" : { - }, "Pour vous permettre de vous profitez pleinement de Linphone nous avons besoin des autorisations suivantes :" : { @@ -408,6 +408,9 @@ }, "See Linphone contact" : { + }, + "Send logs" : { + }, "Share" : { diff --git a/Linphone/Ressources/linphonerc-default b/Linphone/Ressources/linphonerc-default index ea5356429..46ca19ac0 100644 --- a/Linphone/Ressources/linphonerc-default +++ b/Linphone/Ressources/linphonerc-default @@ -16,6 +16,7 @@ update_presence_model_timestamp_before_publish_expires_refresh=1 #Because dynamic bitrate adaption can increase bitrate, we must allow "no limit" download_bw=0 upload_bw=0 +friendlist_subscription_enabled=1 [video] size=vga diff --git a/Linphone/SplashScreen.swift b/Linphone/SplashScreen.swift index e8c11ef7c..e15c4aaac 100644 --- a/Linphone/SplashScreen.swift +++ b/Linphone/SplashScreen.swift @@ -21,9 +21,6 @@ import SwiftUI struct SplashScreen: View { - @ObservedObject private var coreContext = CoreContext.shared - @Binding var isActive: Bool - var body: some View { GeometryReader { _ in VStack { @@ -42,5 +39,5 @@ struct SplashScreen: View { } #Preview { - SplashScreen(isActive: .constant(true)) + SplashScreen() } diff --git a/Linphone/UI/Main/Fragments/SideMenu.swift b/Linphone/UI/Main/Fragments/SideMenu.swift index 317490553..8da7a0086 100644 --- a/Linphone/UI/Main/Fragments/SideMenu.swift +++ b/Linphone/UI/Main/Fragments/SideMenu.swift @@ -18,8 +18,15 @@ */ import SwiftUI +import linphonesw +import UniformTypeIdentifiers struct SideMenu: View { + + @ObservedObject private var coreContext = CoreContext.shared + + @State private var coreDelegate: CoreDelegate? + let width: CGFloat let isOpen: Bool let menuClose: () -> Void @@ -41,9 +48,13 @@ struct SideMenu: View { Text("My Profile").onTapGesture { print("My Profile") } - Text("Posts").onTapGesture { - print("Posts") + Text("Send logs").onTapGesture { + sendLogs() } + Text("Clear logs").onTapGesture { + print("Clear logs") + Core.resetLogCollection() + } Text("Logout").onTapGesture { print("Logout") } @@ -60,4 +71,42 @@ struct SideMenu: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) } + + func sendLogs() { + coreContext.doOnCoreQueue { core in + core.uploadLogCollection() + + let newCoreDelegate = CoreDelegateStub( + onLogCollectionUploadStateChanged: { core, logCollectionUploadState, logString in + print("newCoreDelegatenewCoreDelegate \(logString)") + + if logString.starts(with: "https") { + UIPasteboard.general.setValue( + logString, + forPasteboardType: UTType.plainText.identifier + ) + + removeAllDelegate() + + ToastViewModel.shared.toastMessage = "Success_copied_into_clipboard" + ToastViewModel.shared.displayToast.toggle() + } + } + ) + + coreDelegate = newCoreDelegate + if coreDelegate != nil { + core.addDelegate(delegate: coreDelegate!) + } + } + } + + func removeAllDelegate() { + coreContext.doOnCoreQueue { core in + if coreDelegate != nil { + core.removeDelegate(delegate: coreDelegate!) + coreDelegate = nil + } + } + } } From 07b2c1e04ec5edc02d8455be6eb9b37fee21b5a4 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Fri, 24 Nov 2023 16:48:04 +0100 Subject: [PATCH 10/14] Add assistant config files (linphone and third party) Fix presence --- Linphone.xcodeproj/project.pbxproj | 8 +++++ Linphone/Contacts/ContactsManager.swift | 7 ++-- Linphone/Core/CoreContext.swift | 5 +-- .../assistant_linphone_default_values | 36 +++++++++++++++++++ .../assistant_third_party_default_values | 25 +++++++++++++ Linphone/Ressources/linphonerc-default | 1 - .../Viewmodel/AccountLoginViewModel.swift | 11 ++++++ Linphone/UI/Main/Fragments/SideMenu.swift | 1 - Linphone/Utils/Avatar.swift | 2 -- 9 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 Linphone/Ressources/assistant_linphone_default_values create mode 100644 Linphone/Ressources/assistant_third_party_default_values diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index cb827f198..f9374e42c 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -48,6 +48,8 @@ D777DBB32AE12C5900565A99 /* ContactsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D777DBB22AE12C5900565A99 /* ContactsManager.swift */; }; D78290B82ADD3910004AA85C /* ContactsFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78290B72ADD3910004AA85C /* ContactsFragment.swift */; }; D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */; }; + D783C77C2B1089B200622CC2 /* assistant_linphone_default_values in Resources */ = {isa = PBXBuildFile; fileRef = D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */; }; + D783C77D2B1089B200622CC2 /* assistant_third_party_default_values in Resources */ = {isa = PBXBuildFile; fileRef = D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */; }; D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */; }; D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */; }; D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBF2ACC2E390081A588 /* HistoryView.swift */; }; @@ -123,6 +125,8 @@ D777DBB22AE12C5900565A99 /* ContactsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsManager.swift; sourceTree = ""; }; D78290B72ADD3910004AA85C /* ContactsFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsFragment.swift; sourceTree = ""; }; D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactViewModel.swift; sourceTree = ""; }; + D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_linphone_default_values; sourceTree = ""; }; + D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_third_party_default_values; sourceTree = ""; }; D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = ""; }; D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = ""; }; D7A03FBF2ACC2E390081A588 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; @@ -404,6 +408,8 @@ D7ADF6012AFE5C7C00212231 /* Ressources */ = { isa = PBXGroup; children = ( + D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */, + D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */, D732A90A2B0376F500DB42BA /* linphonerc-default */, D732A90B2B0376F500DB42BA /* linphonerc-factory */, ); @@ -499,6 +505,7 @@ D7D24D142AC1B4E800C6F35B /* NotoSans-Regular.ttf in Resources */, D7D24D182AC1B4E800C6F35B /* NotoSans-ExtraBold.ttf in Resources */, D7D24D152AC1B4E800C6F35B /* NotoSans-Light.ttf in Resources */, + D783C77D2B1089B200622CC2 /* assistant_third_party_default_values in Resources */, D7D24D162AC1B4E800C6F35B /* NotoSans-SemiBold.ttf in Resources */, D7D24D172AC1B4E800C6F35B /* NotoSans-Bold.ttf in Resources */, D719ABBF2ABC67BF00B41C10 /* Preview Assets.xcassets in Resources */, @@ -506,6 +513,7 @@ D7D24D132AC1B4E800C6F35B /* NotoSans-Medium.ttf in Resources */, D732A90C2B0376F500DB42BA /* linphonerc-default in Resources */, D732A90D2B0376F500DB42BA /* linphonerc-factory in Resources */, + D783C77C2B1089B200622CC2 /* assistant_linphone_default_values in Resources */, D70C93DE2AC2D0F60063CA3B /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index 44149bb49..eafa75152 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -67,9 +67,6 @@ final class ContactsManager { do { self.linphoneFriendList = try core.getFriendListByName(name: self.linphoneAddressBookFriendList) ?? core.createFriendList() - - //self.linphoneFriendList?.updateSubscriptions() - print("friendListfriendListfriendListfriendList \(self.linphoneFriendList!.rlsAddress)") } catch let error { print("\(#function) - Failed to enumerate contacts: \(error)") } @@ -239,7 +236,11 @@ final class ContactsManager { friend.organization = contact.organizationName friend.jobTitle = contact.jobTitle + try friend.setSubscribesenabled(newValue: false) + try friend.setIncsubscribepolicy(newValue: .SPDeny) + friend.done() + completion(friend) } catch let error { print("Failed to enumerate contact", error) diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index ecb07e916..9f8e1d5bf 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -87,10 +87,7 @@ final class CoreContext: ObservableObject { self.mCore.autoIterateEnabled = false self.mCore.friendsDatabasePath = "\(configDir)/friends.db" - //self.mCore.logCollectionUploadServerUrl = "https://www.linphone.org:444/lft.php" - //self.mCore.friendListSubscriptionEnabled = true - - print("configDirconfigDir \(configDir)") + self.mCore.friendListSubscriptionEnabled = true self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in if cbVal.state == GlobalState.On { diff --git a/Linphone/Ressources/assistant_linphone_default_values b/Linphone/Ressources/assistant_linphone_default_values new file mode 100644 index 000000000..f3723d7a9 --- /dev/null +++ b/Linphone/Ressources/assistant_linphone_default_values @@ -0,0 +1,36 @@ + + +
+ 1 + 0 + 1 + 120 + sip:voip-metrics@sip.linphone.org;transport=tls + 1 + 180 + 31536000 + sip:?@sip.linphone.org + <sip:sip.linphone.org;transport=tls> + <sip:sip.linphone.org;transport=tls> + 1 + nat_policy_default_values + sip.linphone.org + sip:conference-factory@sip.linphone.org + sip:videoconference-factory@sip.linphone.org + 1 + 1 + 1 + https://lime.linphone.org/lime-server/lime-server.php +
+
+ stun.linphone.org + stun,ice +
+
+ zrtp + 1 +
+
+ 1 +
+
diff --git a/Linphone/Ressources/assistant_third_party_default_values b/Linphone/Ressources/assistant_third_party_default_values new file mode 100644 index 000000000..78927cf4e --- /dev/null +++ b/Linphone/Ressources/assistant_third_party_default_values @@ -0,0 +1,25 @@ + + +
+ 0 + 0 + 0 + -1 + + 0 + 0 + 3600 + + + + 1 + + + + + 0 + 0 + 0 + +
+
diff --git a/Linphone/Ressources/linphonerc-default b/Linphone/Ressources/linphonerc-default index 46ca19ac0..ea5356429 100644 --- a/Linphone/Ressources/linphonerc-default +++ b/Linphone/Ressources/linphonerc-default @@ -16,7 +16,6 @@ update_presence_model_timestamp_before_publish_expires_refresh=1 #Because dynamic bitrate adaption can increase bitrate, we must allow "no limit" download_bw=0 upload_bw=0 -friendlist_subscription_enabled=1 [video] size=vga diff --git a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift index 7462e16dd..e9150c82c 100644 --- a/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift +++ b/Linphone/UI/Assistant/Viewmodel/AccountLoginViewModel.swift @@ -35,6 +35,17 @@ class AccountLoginViewModel: ObservableObject { func login() { coreContext.doOnCoreQueue { core in do { + + if self.domain != "sip.linphone.org" { + if let assistantLinphone = Bundle.main.path(forResource: "assistant_third_party_default_values", ofType: nil) { + core.loadConfigFromXml(xmlUri: assistantLinphone) + } + } else { + if let assistantLinphone = Bundle.main.path(forResource: "assistant_linphone_default_values", ofType: nil) { + core.loadConfigFromXml(xmlUri: assistantLinphone) + } + } + // Get the transport protocol to use. // TLS is strongly recommended // Only use UDP if you don't have the choice diff --git a/Linphone/UI/Main/Fragments/SideMenu.swift b/Linphone/UI/Main/Fragments/SideMenu.swift index 8da7a0086..60f3c9606 100644 --- a/Linphone/UI/Main/Fragments/SideMenu.swift +++ b/Linphone/UI/Main/Fragments/SideMenu.swift @@ -78,7 +78,6 @@ struct SideMenu: View { let newCoreDelegate = CoreDelegateStub( onLogCollectionUploadStateChanged: { core, logCollectionUploadState, logString in - print("newCoreDelegatenewCoreDelegate \(logString)") if logString.starts(with: "https") { UIPasteboard.general.setValue( diff --git a/Linphone/Utils/Avatar.swift b/Linphone/Utils/Avatar.swift index 5597ba38a..16d3f9fba 100644 --- a/Linphone/Utils/Avatar.swift +++ b/Linphone/Utils/Avatar.swift @@ -83,10 +83,8 @@ struct Avatar: View { } func addDelegate() { - print("onPresenceReceivedonPresenceReceived \(friend.name) \(friend.consolidatedPresence)") let newFriendDelegate = FriendDelegateStub( onPresenceReceived: { (linphoneFriend: Friend) -> Void in - print("onPresenceReceivedonPresenceReceived delegate \(friend.name) \(friend.consolidatedPresence) \(linphoneFriend.consolidatedPresence)") self.presenceImage = linphoneFriend.consolidatedPresence == ConsolidatedPresence.Online ? "presence-online" : "presence-busy" } ) From 7b476904cb9b9e13b1f39e8704be99ae6e3e27ea Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 28 Nov 2023 17:28:46 +0100 Subject: [PATCH 11/14] Fix presence --- Linphone.xcodeproj/project.pbxproj | 12 ++ Linphone/Contacts/ContactsManager.swift | 9 +- Linphone/Localizable.xcstrings | 3 - .../Contacts/Fragments/ContactFragment.swift | 10 +- .../ContactInnerActionsFragment.swift | 84 ++++++------ .../Fragments/ContactInnerFragment.swift | 42 +++--- .../Fragments/ContactListBottomSheet.swift | 1 - .../Fragments/ContactsInnerFragment.swift | 8 +- .../Fragments/ContactsListBottomSheet.swift | 4 +- .../Fragments/ContactsListFragment.swift | 22 ++-- .../Fragments/EditContactFragment.swift | 27 +++- .../FavoriteContactsListFragment.swift | 20 +-- .../Contacts/Model/ContactAvatarModel.swift | 121 ++++++++++++++++++ .../Contacts/ViewModel/ContactViewModel.swift | 6 +- .../ViewModel/ContactsListViewModel.swift | 2 +- Linphone/UI/Main/ContentView.swift | 34 +++-- .../Fragments/HistoryContactFragment.swift | 63 ++++----- .../Fragments/HistoryListBottomSheet.swift | 7 +- .../Fragments/HistoryListFragment.swift | 49 +++---- .../ViewModel/HistoryListViewModel.swift | 5 +- .../UI/Main/Viewmodel/ToastViewModel.swift | 24 +++- Linphone/Utils/Avatar.swift | 115 ++++++----------- Linphone/Utils/IntExtension.swift | 12 +- Linphone/Utils/MagicSearchSingleton.swift | 45 ++++++- 24 files changed, 445 insertions(+), 280 deletions(-) create mode 100644 Linphone/UI/Main/Contacts/Model/ContactAvatarModel.swift 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) + } + } } From 8b14538fcdc8b7595bfee9ea6a7731a1cde73790 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Fri, 1 Dec 2023 10:20:31 +0100 Subject: [PATCH 12/14] Send own presence --- Linphone/Core/CoreContext.swift | 27 +++++++++++++++++++ .../Fragments/EditContactFragment.swift | 1 - Linphone/UI/Main/ContentView.swift | 2 ++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index 9f8e1d5bf..b6699bb8c 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -123,6 +123,9 @@ final class CoreContext: ObservableObject { if cbVal.state == .Ok { self.loggingInProgress = false self.loggedIn = true + if self.mCore.consolidatedPresence != ConsolidatedPresence.Online { + self.onForeground() + } } else if cbVal.state == .Progress { self.loggingInProgress = true } else { @@ -154,6 +157,30 @@ final class CoreContext: ObservableObject { } } + + func onForeground() { + coreQueue.async { + // We can't rely on defaultAccount?.params?.isPublishEnabled + // as it will be modified by the SDK when changing the presence status + if self.mCore.config!.getBool(section: "app", key: "publish_presence", defaultValue: true) { + NSLog("App is in foreground, PUBLISHING presence as Online") + self.mCore.consolidatedPresence = ConsolidatedPresence.Online + } + } + } + + func onBackground() { + coreQueue.async { + // We can't rely on defaultAccount?.params?.isPublishEnabled + // as it will be modified by the SDK when changing the presence status + if self.mCore.config!.getBool(section: "app", key: "publish_presence", defaultValue: true) { + NSLog("App is in background, un-PUBLISHING presence info") + // We don't use ConsolidatedPresence.Busy but Offline to do an unsubscribe, + // Flexisip will handle the Busy status depending on other devices + self.mCore.consolidatedPresence = ConsolidatedPresence.Offline + } + } + } } // swiftlint:enable large_tuple diff --git a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift index abb0fda21..2e5f702f1 100644 --- a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift @@ -504,7 +504,6 @@ struct EditContactFragment: View { let result = ContactsManager.shared.lastSearch.firstIndex(where: { $0.friend!.name == newContact.firstName + " " + newContact.lastName }) - print("getFriendIndexWithFriendgetFriendIndexWithFriend \(newContact.firstName) \(newContact.lastName) \(result)") contactViewModel.indexDisplayedFriend = result } } diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index 186b5fd3f..9096f3117 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -625,11 +625,13 @@ struct ContentView: View { } .onChange(of: scenePhase) { newPhase in if newPhase == .active { + coreContext.onForeground() contactsManager.fetchContacts() print("Active") } else if newPhase == .inactive { print("Inactive") } else if newPhase == .background { + coreContext.onBackground() print("Background") } } From e47a04c5d9e0065bfdc7d3823d60dffcb2b02c01 Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Fri, 1 Dec 2023 16:36:11 +0100 Subject: [PATCH 13/14] Start new call view --- Linphone.xcodeproj/project.pbxproj | 12 + .../dialer.imageset/Contents.json | 21 ++ .../dialer.imageset/dialer.svg | 3 + Linphone/Contacts/ContactsManager.swift | 13 +- Linphone/Core/CoreContext.swift | 5 +- Linphone/LinphoneApp.swift | 12 +- Linphone/Localizable.xcstrings | 51 +++ .../Fragments/PermissionsFragment.swift | 5 +- .../Fragments/ContactsInnerFragment.swift | 24 +- .../Fragments/ContactsListFragment.swift | 174 +++++----- .../Fragments/EditContactFragment.swift | 4 +- Linphone/UI/Main/ContentView.swift | 32 +- .../History/Fragments/DialerBottomSheet.swift | 320 ++++++++++++++++++ .../Fragments/HistoryListFragment.swift | 4 +- .../History/Fragments/StartCallFragment.swift | 245 ++++++++++++++ Linphone/UI/Main/History/HistoryView.swift | 6 + .../ViewModel/StartCallViewModel.swift | 27 ++ Linphone/Utils/EditContactController.swift | 3 +- Linphone/Utils/MagicSearchSingleton.swift | 35 +- Linphone/Utils/PermissionManager.swift | 7 + 20 files changed, 876 insertions(+), 127 deletions(-) create mode 100644 Linphone/Assets.xcassets/dialer.imageset/Contents.json create mode 100644 Linphone/Assets.xcassets/dialer.imageset/dialer.svg create mode 100644 Linphone/UI/Main/History/Fragments/DialerBottomSheet.swift create mode 100644 Linphone/UI/Main/History/Fragments/StartCallFragment.swift create mode 100644 Linphone/UI/Main/History/ViewModel/StartCallViewModel.swift diff --git a/Linphone.xcodeproj/project.pbxproj b/Linphone.xcodeproj/project.pbxproj index 60a8812a8..3636ae410 100644 --- a/Linphone.xcodeproj/project.pbxproj +++ b/Linphone.xcodeproj/project.pbxproj @@ -28,6 +28,8 @@ 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 */; }; + D726E43D2B19E4FE0083C415 /* StartCallFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */; }; + D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D726E43E2B19E56F0083C415 /* StartCallViewModel.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 */; }; @@ -51,6 +53,7 @@ D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */; }; D783C77C2B1089B200622CC2 /* assistant_linphone_default_values in Resources */ = {isa = PBXBuildFile; fileRef = D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */; }; D783C77D2B1089B200622CC2 /* assistant_third_party_default_values in Resources */ = {isa = PBXBuildFile; fileRef = D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */; }; + D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D79622332B1DFE600037EACD /* DialerBottomSheet.swift */; }; D796F2002B0BB61A0041115F /* ToastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */; }; D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */; }; D7A03FC02ACC2E390081A588 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A03FBF2ACC2E390081A588 /* HistoryView.swift */; }; @@ -106,6 +109,8 @@ 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 = ""; }; + D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallFragment.swift; sourceTree = ""; }; + D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallViewModel.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 = ""; }; @@ -129,6 +134,7 @@ D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactViewModel.swift; sourceTree = ""; }; D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_linphone_default_values; sourceTree = ""; }; D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_third_party_default_values; sourceTree = ""; }; + D79622332B1DFE600037EACD /* DialerBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialerBottomSheet.swift; sourceTree = ""; }; D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = ""; }; D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = ""; }; D7A03FBF2ACC2E390081A588 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; @@ -295,6 +301,7 @@ children = ( D72250622ADE9615008FB426 /* HistoryViewModel.swift */, D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */, + D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -314,6 +321,8 @@ D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */, D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */, D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */, + D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */, + D79622332B1DFE600037EACD /* DialerBottomSheet.swift */, ); path = Fragments; sourceTree = ""; @@ -576,6 +585,7 @@ D7A03FBD2ACC2DB60081A588 /* ContactsView.swift in Sources */, D719ABCF2ABC779A00B41C10 /* AccountLoginViewModel.swift in Sources */, D732A90F2B04C3B400DB42BA /* HistoryFragment.swift in Sources */, + D79622342B1DFE600037EACD /* DialerBottomSheet.swift in Sources */, D78290BB2ADD40B2004AA85C /* ContactViewModel.swift in Sources */, D72992392ADD7F68003AF125 /* HistoryContactFragment.swift in Sources */, D7B5066D2AEFA9B900CEB4E9 /* ContactInnerFragment.swift in Sources */, @@ -605,7 +615,9 @@ D7C48DF62AFCDF4700D938CB /* ContactInnerActionsFragment.swift in Sources */, D72343322ACEFF58009AA24E /* QRScannerController.swift in Sources */, D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */, + D726E43D2B19E4FE0083C415 /* StartCallFragment.swift in Sources */, D72343302ACEFEF8009AA24E /* QrCodeScannerFragment.swift in Sources */, + D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */, D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */, D72250692ADFBF2D008FB426 /* SideMenu.swift in Sources */, D717071E2AC5922E0037746F /* ColorExtension.swift in Sources */, diff --git a/Linphone/Assets.xcassets/dialer.imageset/Contents.json b/Linphone/Assets.xcassets/dialer.imageset/Contents.json new file mode 100644 index 000000000..117f088cd --- /dev/null +++ b/Linphone/Assets.xcassets/dialer.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "dialer.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Linphone/Assets.xcassets/dialer.imageset/dialer.svg b/Linphone/Assets.xcassets/dialer.imageset/dialer.svg new file mode 100644 index 000000000..71705dfdf --- /dev/null +++ b/Linphone/Assets.xcassets/dialer.imageset/dialer.svg @@ -0,0 +1,3 @@ + + + diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index 6b80d8131..139e62338 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -35,6 +35,7 @@ final class ContactsManager: ObservableObject { var linphoneFriendList: FriendList? @Published var lastSearch: [SearchResult] = [] + @Published var lastSearchSuggestions: [SearchResult] = [] @Published var avatarListModel: [ContactAvatarModel] = [] private init() { @@ -121,7 +122,8 @@ final class ContactsManager: ObservableObject { && contact.phoneNumbers.first?.value.stringValue != nil ? contact.phoneNumbers.first!.value.stringValue : contact.givenName, lastName: contact.familyName), - name: contact.givenName + contact.familyName + String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""), + name: contact.givenName + contact.familyName, + prefix: String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""), contact: newContact, linphoneFriend: false, existingFriend: nil) } }) @@ -167,12 +169,12 @@ final class ContactsManager: ObservableObject { return IBImgViewUserProfile } - func saveImage(image: UIImage, name: String, contact: Contact, linphoneFriend: Bool, existingFriend: Friend?) { + func saveImage(image: UIImage, name: String, prefix: String, contact: Contact, linphoneFriend: Bool, existingFriend: Friend?) { guard let data = image.jpegData(compressionQuality: 1) ?? image.pngData() else { return } - awaitDataWrite(data: data, name: name) { _, result in + awaitDataWrite(data: data, name: name, prefix: prefix) { _, result in self.saveFriend(result: result, contact: contact, existingFriend: existingFriend) { resultFriend in if resultFriend != nil { if linphoneFriend && existingFriend == nil { @@ -260,15 +262,16 @@ final class ContactsManager: ObservableObject { return imagePath } - func awaitDataWrite(data: Data, name: String, completion: @escaping ((), String) -> Void) { + func awaitDataWrite(data: Data, name: String, prefix: String,completion: @escaping ((), String) -> Void) { let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first if directory != nil { DispatchQueue.main.async { do { - let urlName = URL(string: name) + let urlName = URL(string: name + prefix) let imagePath = urlName != nil ? urlName!.absoluteString.replacingOccurrences(of: "%", with: "") : String(Int.random(in: 1...1000)) let decodedData: () = try data.write(to: directory!.appendingPathComponent(imagePath + ".png")) + completion(decodedData, imagePath + ".png") } catch { print("Error: ", error) diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index b6699bb8c..ce88facec 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -87,15 +87,18 @@ final class CoreContext: ObservableObject { self.mCore.autoIterateEnabled = false self.mCore.friendsDatabasePath = "\(configDir)/friends.db" + print("configDirconfigDirconfigDir \(configDir)") + self.mCore.friendListSubscriptionEnabled = true self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in if cbVal.state == GlobalState.On { self.defaultAccount = self.mCore.defaultAccount + self.coreIsStarted = true } else if cbVal.state == GlobalState.Off { self.defaultAccount = nil + self.coreIsStarted = true } - self.coreIsStarted = true } try? self.mCore.start() diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 159abb97d..61e22bdf3 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -29,6 +29,7 @@ struct LinphoneApp: App { @State private var editContactViewModel: EditContactViewModel? @State private var historyViewModel: HistoryViewModel? @State private var historyListViewModel: HistoryListViewModel? + @State private var startCallViewModel: StartCallViewModel? var body: some Scene { WindowGroup { @@ -37,17 +38,21 @@ struct LinphoneApp: App { WelcomeView() } else if coreContext.defaultAccount == nil || sharedMainViewModel.displayProfileMode { AssistantView() - } else if coreContext.defaultAccount != nil + } else if coreContext.defaultAccount != nil && contactViewModel != nil && editContactViewModel != nil && historyViewModel != nil - && historyListViewModel != nil { + && historyListViewModel != nil + && startCallViewModel != nil { ContentView( contactViewModel: contactViewModel!, editContactViewModel: editContactViewModel!, historyViewModel: historyViewModel!, - historyListViewModel: historyListViewModel! + historyListViewModel: historyListViewModel!, + startCallViewModel: startCallViewModel! ) + } else { + SplashScreen() } } else { SplashScreen() @@ -56,6 +61,7 @@ struct LinphoneApp: App { editContactViewModel = EditContactViewModel() historyViewModel = HistoryViewModel() historyListViewModel = HistoryListViewModel() + startCallViewModel = StartCallViewModel() } } } diff --git a/Linphone/Localizable.xcstrings b/Linphone/Localizable.xcstrings index 394b1e7f0..569f38f71 100644 --- a/Linphone/Localizable.xcstrings +++ b/Linphone/Localizable.xcstrings @@ -27,6 +27,9 @@ }, "[notre politique de confidentialité](https://linphone.org/privacy-policy)" : { + }, + "*" : { + }, "**Camera** : Pour capturer votre vidéo lors des appels vidéo et conférence." : { @@ -45,6 +48,9 @@ }, "**Notifications** : Pour vous informé quand vous recevez un message ou un appel." : { + }, + "#" : { + }, "%lld Book (Example)" : { "extractionState" : "manual", @@ -98,6 +104,39 @@ } } } + }, + "+" : { + + }, + "0" : { + + }, + "1" : { + + }, + "2" : { + + }, + "3" : { + + }, + "4" : { + + }, + "5" : { + + }, + "6" : { + + }, + "7" : { + + }, + "8" : { + + }, + "9" : { + }, "Accept all" : { @@ -313,6 +352,9 @@ }, "My Profile" : { + }, + "New call" : { + }, "New contact" : { @@ -393,6 +435,9 @@ }, "Scan QR code" : { + }, + "Search contact or history call" : { + }, "Sécurisé" : { @@ -429,6 +474,9 @@ }, "Start" : { + }, + "Suggestions" : { + }, "TCP" : { @@ -479,6 +527,9 @@ } } } + }, + "Username error" : { + }, "Video Call" : { diff --git a/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift b/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift index 82f709bac..fb7eed70a 100644 --- a/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift +++ b/Linphone/UI/Assistant/Fragments/PermissionsFragment.swift @@ -172,8 +172,7 @@ struct PermissionsFragment: View { .padding(.horizontal) Button { - permissionManager.contactsRequestPermission() - permissionManager.cameraRequestPermission() + permissionManager.getPermissions() } label: { Text("D'accord") .default_text_style_white_600(styleSize: 20) @@ -193,7 +192,7 @@ struct PermissionsFragment: View { } .navigationViewStyle(StackNavigationViewStyle()) .navigationBarHidden(true) - .onReceive(permissionManager.$cameraPermissionGranted, perform: { (granted) in + .onReceive(permissionManager.$contactsPermissionGranted, perform: { (granted) in if granted { withAnimation { sharedMainViewModel.changeWelcomeView() diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift index d1cb1215f..0168bb88f 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsInnerFragment.swift @@ -72,7 +72,29 @@ struct ContactsInnerFragment: View { .padding(.top, 10) .padding(.horizontal, 16) } - ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet) + + VStack { + List { + ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet)} + .listStyle(.plain) + .overlay( + VStack { + if contactsManager.lastSearch.isEmpty { + Spacer() + Image("illus-belledonne") + .resizable() + .scaledToFit() + .clipped() + .padding(.all) + Text("No contacts for the moment...") + .default_text_style_800(styleSize: 16) + Spacer() + Spacer() + } + } + .padding(.all) + ) + } } .navigationBarHidden(true) } diff --git a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift index 0857c53aa..f48664a24 100644 --- a/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/ContactsListFragment.swift @@ -21,105 +21,83 @@ import SwiftUI import linphonesw struct ContactsListFragment: View { - - @ObservedObject var contactsManager = ContactsManager.shared - - @ObservedObject var contactViewModel: ContactViewModel - @ObservedObject var contactsListViewModel: ContactsListViewModel - - @Binding var showingSheet: Bool - - var body: some View { - VStack { - List { - ForEach(0... + */ + +import SwiftUI +import UniformTypeIdentifiers + +struct DialerBottomSheet: View { + + @Environment(\.dismiss) var dismiss + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + + @ObservedObject private var magicSearch = MagicSearchSingleton.shared + @ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared + @ObservedObject var contactsManager = ContactsManager.shared + + @ObservedObject var startCallViewModel: StartCallViewModel + + @State private var orientation = UIDevice.current.orientation + + @Binding var showingDialer: Bool + + var body: some View { + VStack(alignment: .center, spacing: 0) { + VStack(alignment: .center, spacing: 0) { + if idiom != .pad && (orientation == .landscapeLeft + || orientation == .landscapeRight + || UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height) { + Spacer() + HStack { + Spacer() + Button("Close") { + showingDialer.toggle() + dismiss() + } + } + .padding(.trailing) + } else { + Capsule() + .fill(Color.grayMain2c300) + .frame(width: 75, height: 5) + .padding(15) + } + + Spacer() + + HStack { + Button { + startCallViewModel.searchField += "1" + } label: { + Text("1") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "2" + } label: { + Text("2") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "3" + } label: { + Text("3") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + } + .padding(.horizontal, 60) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + HStack { + Button { + startCallViewModel.searchField += "4" + } label: { + Text("4") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "5" + } label: { + Text("5") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "6" + } label: { + Text("6") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + } + .padding(.horizontal, 60) + .padding(.top, 10) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + HStack { + Button { + startCallViewModel.searchField += "7" + } label: { + Text("7") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "8" + } label: { + Text("8") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + startCallViewModel.searchField += "9" + } label: { + Text("9") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + } + .padding(.horizontal, 60) + .padding(.top, 10) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + HStack { + Button { + startCallViewModel.searchField += "*" + } label: { + Text("*") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + + Spacer() + + Button { + } label: { + ZStack { + Text("0") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 75) + .padding(.top, -15) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + Text("+") + .default_text_style(styleSize: 20) + .multilineTextAlignment(.center) + .frame(width: 60, height: 85) + .padding(.bottom, -25) + .background(.clear) + .clipShape(Circle()) + } + } + .simultaneousGesture( + LongPressGesture() + .onEnded { _ in + startCallViewModel.searchField += "+" + } + ) + .highPriorityGesture( + TapGesture() + .onEnded { _ in + startCallViewModel.searchField += "0" + } + ) + + Spacer() + + Button { + startCallViewModel.searchField += "#" + } label: { + Text("#") + .default_text_style(styleSize: 32) + .multilineTextAlignment(.center) + .frame(width: 60, height: 60) + .background(.white) + .clipShape(Circle()) + .shadow(color: .black.opacity(0.2), radius: 4) + } + } + .padding(.horizontal, 60) + .padding(.top, 10) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + HStack { + + HStack { + + } + .frame(width: 60, height: 60) + + Spacer() + + Button { + } label: { + Image("phone") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 32, height: 32) + + } + .frame(width: 90, height: 60) + .background(Color.greenSuccess500) + .cornerRadius(40) + .shadow(color: .black.opacity(0.2), radius: 4) + + Spacer() + + Button { + startCallViewModel.searchField = String(startCallViewModel.searchField.dropLast()) + } label: { + Image("backspace-fill") + .resizable() + .frame(width: 32, height: 32) + + } + .frame(width: 60, height: 60) + } + .padding(.horizontal, 60) + .padding(.top, 20) + .frame(maxWidth: sharedMainViewModel.maxWidth) + + Spacer() + } + .frame(maxWidth: .infinity) + .frame(maxHeight: .infinity) + } + .background(Color.gray100) + .frame(maxWidth: .infinity) + .frame(maxHeight: .infinity) + .onRotate { newOrientation in + orientation = newOrientation + } + } +} + +#Preview { + DialerBottomSheet( + startCallViewModel: StartCallViewModel(), showingDialer: .constant(false) + ) +} diff --git a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift index 50fd2513a..a2fcac515 100644 --- a/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift +++ b/Linphone/UI/Main/History/Fragments/HistoryListFragment.swift @@ -49,7 +49,9 @@ struct HistoryListFragment: View { : ContactAvatarModel(friend: nil, withPresence: false) if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty { - Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 45) + if contactAvatarModel != nil { + Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 45) + } } else { if historyListViewModel.callLogs[index].dir == .Outgoing && historyListViewModel.callLogs[index].toAddress != nil { if historyListViewModel.callLogs[index].toAddress!.displayName != nil { diff --git a/Linphone/UI/Main/History/Fragments/StartCallFragment.swift b/Linphone/UI/Main/History/Fragments/StartCallFragment.swift new file mode 100644 index 000000000..b03dde24e --- /dev/null +++ b/Linphone/UI/Main/History/Fragments/StartCallFragment.swift @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * 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 SwiftUI +import linphonesw + +struct StartCallFragment: View { + + @ObservedObject var contactsManager = ContactsManager.shared + @ObservedObject var magicSearch = MagicSearchSingleton.shared + + @ObservedObject var startCallViewModel: StartCallViewModel + + @Binding var isShowStartCallFragment: Bool + @Binding var showingDialer: Bool + + @FocusState var isSearchFieldFocused: Bool + @State private var hasTimeElapsed = false + @State private var delayedColor = Color.white + + var body: some View { + ZStack { + VStack(spacing: 1) { + + Rectangle() + .foregroundColor(delayedColor) + .edgesIgnoringSafeArea(.top) + .frame(height: 0) + .task(delayColor) + + HStack { + Image("caret-left") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.orangeMain500) + .frame(width: 25, height: 25, alignment: .leading) + .padding(.top, 2) + .onTapGesture { + DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { + magicSearch.searchForContacts( + sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + } + + startCallViewModel.searchField = "" + magicSearch.currentFilterSuggestions = "" + delayColorDismiss() + withAnimation { + isShowStartCallFragment.toggle() + } + } + + Text("New call") + .multilineTextAlignment(.leading) + .default_text_style_orange_800(styleSize: 16) + + Spacer() + + } + .frame(maxWidth: .infinity) + .frame(height: 50) + .padding(.horizontal) + .padding(.bottom, 4) + .background(.white) + + VStack(spacing: 0) { + ZStack(alignment: .trailing) { + TextField("Search contact or history call", text: $startCallViewModel.searchField) + .default_text_style(styleSize: 15) + .frame(height: 25) + .focused($isSearchFieldFocused) + .padding(.horizontal, 30) + .onChange(of: startCallViewModel.searchField) { newValue in + magicSearch.currentFilterSuggestions = newValue + magicSearch.searchForSuggestions() + } + + HStack { + Button(action: { + }, label: { + Image("magnifying-glass") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25) + }) + + Spacer() + + if startCallViewModel.searchField.isEmpty { + Button(action: { + isSearchFieldFocused = false + + DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) { + showingDialer.toggle() + } + }, label: { + Image(!showingDialer ? "dialer" : "keyboard") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25) + }) + } else { + Button(action: { + startCallViewModel.searchField = "" + magicSearch.currentFilterSuggestions = "" + magicSearch.searchForSuggestions() + }, label: { + Image("x") + .renderingMode(.template) + .resizable() + .foregroundStyle(Color.grayMain2c500) + .frame(width: 25, height: 25) + }) + } + } + } + .padding(.horizontal, 15) + .padding(.vertical, 10) + .cornerRadius(60) + .overlay( + RoundedRectangle(cornerRadius: 60) + .inset(by: 0.5) + .stroke(isSearchFieldFocused ? Color.orangeMain500 : Color.gray200, lineWidth: 1) + ) + .padding(.vertical) + .padding(.horizontal) + + ScrollView { + if !ContactsManager.shared.lastSearch.isEmpty { + HStack(alignment: .center) { + Text("All contacts") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + } + + ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false)) + .padding(.horizontal, 16) + + HStack(alignment: .center) { + Text("Suggestions") + .default_text_style_800(styleSize: 16) + + Spacer() + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + + suggestionsList + } + } + .frame(maxWidth: .infinity) + } + .background(.white) + } + .navigationBarHidden(true) + } + + @Sendable private func delayColor() async { + try? await Task.sleep(nanoseconds: 250_000_000) + delayedColor = Color.orangeMain500 + } + + func delayColorDismiss() { + Task { + try? await Task.sleep(nanoseconds: 80_000_000) + delayedColor = .white + } + } + + var suggestionsList: some View { + ForEach(0... + */ + +import linphonesw + +class StartCallViewModel: ObservableObject { + + @Published var searchField: String = "" + + init() {} +} diff --git a/Linphone/Utils/EditContactController.swift b/Linphone/Utils/EditContactController.swift index b29718a4e..df8e0c67f 100644 --- a/Linphone/Utils/EditContactController.swift +++ b/Linphone/Utils/EditContactController.swift @@ -50,7 +50,8 @@ struct EditContactView: UIViewControllerRepresentable { && cnc.phoneNumbers.first?.value.stringValue != nil ? cnc.phoneNumbers.first!.value.stringValue : cnc.givenName, lastName: cnc.familyName), - name: cnc.givenName + cnc.familyName + String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""), + name: cnc.givenName + cnc.familyName, + prefix: String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""), contact: newContact, linphoneFriend: false, existingFriend: ContactsManager.shared.getFriendWithContact(contact: newContact)) diff --git a/Linphone/Utils/MagicSearchSingleton.swift b/Linphone/Utils/MagicSearchSingleton.swift index 488eb5d4b..0cd349bb8 100644 --- a/Linphone/Utils/MagicSearchSingleton.swift +++ b/Linphone/Utils/MagicSearchSingleton.swift @@ -30,6 +30,9 @@ final class MagicSearchSingleton: ObservableObject { var currentFilter: String = "" var previousFilter: String? + var currentFilterSuggestions: String = "" + var previousFilterSuggestions: String? + var needUpdateLastSearchContacts = false private var limitSearchToLinphoneAccounts = true @@ -46,11 +49,27 @@ final class MagicSearchSingleton: ObservableObject { self.magicSearch.publisher?.onSearchResultsReceived?.postOnMainQueue { (magicSearch: MagicSearch) in self.needUpdateLastSearchContacts = true - self.contactsManager.lastSearch = magicSearch.lastSearch.sorted(by: { + + var lastSearchFriend: [SearchResult] = [] + var lastSearchSuggestions: [SearchResult] = [] + + magicSearch.lastSearch.forEach { searchResult in + if searchResult.friend != nil { + lastSearchFriend.append(searchResult) + } else { + lastSearchSuggestions.append(searchResult) + } + } + + self.contactsManager.lastSearch = lastSearchFriend.sorted(by: { $0.friend!.name!.lowercased().folding(options: .diacriticInsensitive, locale: .current) < $1.friend!.name!.lowercased().folding(options: .diacriticInsensitive, locale: .current) }) + + self.contactsManager.lastSearchSuggestions = lastSearchSuggestions.sorted(by: { + $0.address!.asStringUriOnly() < $1.address!.asStringUriOnly() + }) self.contactsManager.avatarListModel.forEach { contactAvatarModel in contactAvatarModel.removeAllDelegate() @@ -91,26 +110,26 @@ final class MagicSearchSingleton: ObservableObject { } } - func searchForContactsWithResult(sourceFlags: Int) { + func searchForSuggestions() { coreContext.doOnCoreQueue { _ in var needResetCache = false DispatchQueue.main.sync { - if let oldFilter = self.previousFilter { - if oldFilter.count > self.currentFilter.count || oldFilter != self.currentFilter { + if let oldFilter = self.previousFilterSuggestions { + if oldFilter.count > self.currentFilterSuggestions.count || oldFilter != self.currentFilterSuggestions { needResetCache = true } } - self.previousFilter = self.currentFilter + self.previousFilterSuggestions = self.currentFilterSuggestions } if needResetCache { self.magicSearch.resetSearchCache() } self.magicSearch.getContactsListAsync( - filter: self.currentFilter, - domain: self.allContact ? "" : self.domainDefaultAccount, - sourceFlags: sourceFlags, + filter: self.currentFilterSuggestions, + domain: self.domainDefaultAccount, + sourceFlags: MagicSearch.Source.All.rawValue, aggregation: MagicSearch.Aggregation.Friend) } } diff --git a/Linphone/Utils/PermissionManager.swift b/Linphone/Utils/PermissionManager.swift index f1d741f7e..e19833012 100644 --- a/Linphone/Utils/PermissionManager.swift +++ b/Linphone/Utils/PermissionManager.swift @@ -31,6 +31,13 @@ class PermissionManager: ObservableObject { private init() {} + + func getPermissions(){ + photoLibraryRequestPermission() + cameraRequestPermission() + contactsRequestPermission() + } + func photoLibraryRequestPermission() { PHPhotoLibrary.requestAuthorization(for: .readWrite, handler: {status in DispatchQueue.main.async { From 77951adaa1bea61f7f0c55b78806c7e1899936eb Mon Sep 17 00:00:00 2001 From: Benoit Martins Date: Tue, 5 Dec 2023 16:59:46 +0100 Subject: [PATCH 14/14] Fix contact image --- Linphone/Contacts/ContactsManager.swift | 5 +++-- Linphone/Core/CoreContext.swift | 2 -- .../UI/Main/Contacts/Fragments/EditContactFragment.swift | 3 +-- Linphone/Utils/EditContactController.swift | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index 139e62338..0b00df121 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -123,7 +123,7 @@ final class ContactsManager: ObservableObject { ? contact.phoneNumbers.first!.value.stringValue : contact.givenName, lastName: contact.familyName), name: contact.givenName + contact.familyName, - prefix: String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""), + prefix: ((imageThumbnail == nil) ? "-default" : ""), contact: newContact, linphoneFriend: false, existingFriend: nil) } }) @@ -269,7 +269,8 @@ final class ContactsManager: ObservableObject { DispatchQueue.main.async { do { let urlName = URL(string: name + prefix) - let imagePath = urlName != nil ? urlName!.absoluteString.replacingOccurrences(of: "%", with: "") : String(Int.random(in: 1...1000)) + let imagePath = urlName != nil ? urlName!.absoluteString.replacingOccurrences(of: "%", with: "") : "ImageError" + let decodedData: () = try data.write(to: directory!.appendingPathComponent(imagePath + ".png")) completion(decodedData, imagePath + ".png") diff --git a/Linphone/Core/CoreContext.swift b/Linphone/Core/CoreContext.swift index ce88facec..7d9bb5e6e 100644 --- a/Linphone/Core/CoreContext.swift +++ b/Linphone/Core/CoreContext.swift @@ -87,8 +87,6 @@ final class CoreContext: ObservableObject { self.mCore.autoIterateEnabled = false self.mCore.friendsDatabasePath = "\(configDir)/friends.db" - print("configDirconfigDirconfigDir \(configDir)") - self.mCore.friendListSubscriptionEnabled = true self.mCore.publisher?.onGlobalStateChanged?.postOnMainQueue { (cbVal: (core: Core, state: GlobalState, message: String)) in diff --git a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift index 0798aa34d..1f1c86112 100644 --- a/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift +++ b/Linphone/UI/Main/Contacts/Fragments/EditContactFragment.swift @@ -492,8 +492,7 @@ struct EditContactFragment: View { firstName: editContactViewModel.firstName, lastName: editContactViewModel.lastName), name: editContactViewModel.firstName + editContactViewModel.lastName, - prefix: String(Int.random(in: 1...1000)) - + ((selectedImage == nil) ? "-default" : ""), + prefix: ((selectedImage == nil) ? "-default" : ""), contact: newContact, linphoneFriend: true, existingFriend: editContactViewModel.selectedEditFriend) } diff --git a/Linphone/Utils/EditContactController.swift b/Linphone/Utils/EditContactController.swift index df8e0c67f..4734b5562 100644 --- a/Linphone/Utils/EditContactController.swift +++ b/Linphone/Utils/EditContactController.swift @@ -51,7 +51,7 @@ struct EditContactView: UIViewControllerRepresentable { ? cnc.phoneNumbers.first!.value.stringValue : cnc.givenName, lastName: cnc.familyName), name: cnc.givenName + cnc.familyName, - prefix: String(Int.random(in: 1...1000)) + ((imageThumbnail == nil) ? "-default" : ""), + prefix: ((imageThumbnail == nil) ? "-default" : ""), contact: newContact, linphoneFriend: false, existingFriend: ContactsManager.shared.getFriendWithContact(contact: newContact))