Start new call view

This commit is contained in:
Benoit Martins 2023-12-01 16:36:11 +01:00
parent 8b14538fcd
commit e47a04c5d9
20 changed files with 876 additions and 127 deletions

View file

@ -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 = "<group>"; };
D72343352AD037AF009AA24E /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
D726E4382B16440C0083C415 /* ContactAvatarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactAvatarModel.swift; sourceTree = "<group>"; };
D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallFragment.swift; sourceTree = "<group>"; };
D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartCallViewModel.swift; sourceTree = "<group>"; };
D72992382ADD7F68003AF125 /* HistoryContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryContactFragment.swift; sourceTree = "<group>"; };
D732A9082AFD235500DB42BA /* ShareSheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareSheetController.swift; sourceTree = "<group>"; };
D732A90A2B0376F500DB42BA /* linphonerc-default */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "linphonerc-default"; sourceTree = "<group>"; };
@ -129,6 +134,7 @@
D78290BA2ADD40B2004AA85C /* ContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactViewModel.swift; sourceTree = "<group>"; };
D783C77A2B1089B200622CC2 /* assistant_linphone_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_linphone_default_values; sourceTree = "<group>"; };
D783C77B2B1089B200622CC2 /* assistant_third_party_default_values */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = assistant_third_party_default_values; sourceTree = "<group>"; };
D79622332B1DFE600037EACD /* DialerBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialerBottomSheet.swift; sourceTree = "<group>"; };
D796F1FF2B0BB61A0041115F /* ToastViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = "<group>"; };
D7A03FBC2ACC2DB60081A588 /* ContactsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsView.swift; sourceTree = "<group>"; };
D7A03FBF2ACC2E390081A588 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
@ -295,6 +301,7 @@
children = (
D72250622ADE9615008FB426 /* HistoryViewModel.swift */,
D732A9142B04C7FE00DB42BA /* HistoryListViewModel.swift */,
D726E43E2B19E56F0083C415 /* StartCallViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
@ -314,6 +321,8 @@
D732A90E2B04C3B400DB42BA /* HistoryFragment.swift */,
D732A9122B04C7A300DB42BA /* HistoryListFragment.swift */,
D732A91A2B061BD900DB42BA /* HistoryListBottomSheet.swift */,
D726E43C2B19E4FE0083C415 /* StartCallFragment.swift */,
D79622332B1DFE600037EACD /* DialerBottomSheet.swift */,
);
path = Fragments;
sourceTree = "<group>";
@ -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 */,

View file

@ -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
}
}

View file

@ -0,0 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 19.5C10.9 19.5 10 20.4 10 21.5C10 22.6 10.9 23.5 12 23.5C13.1 23.5 14 22.6 14 21.5C14 20.4 13.1 19.5 12 19.5ZM6 1.5C4.9 1.5 4 2.4 4 3.5C4 4.6 4.9 5.5 6 5.5C7.1 5.5 8 4.6 8 3.5C8 2.4 7.1 1.5 6 1.5ZM6 7.5C4.9 7.5 4 8.4 4 9.5C4 10.6 4.9 11.5 6 11.5C7.1 11.5 8 10.6 8 9.5C8 8.4 7.1 7.5 6 7.5ZM6 13.5C4.9 13.5 4 14.4 4 15.5C4 16.6 4.9 17.5 6 17.5C7.1 17.5 8 16.6 8 15.5C8 14.4 7.1 13.5 6 13.5ZM18 5.5C19.1 5.5 20 4.6 20 3.5C20 2.4 19.1 1.5 18 1.5C16.9 1.5 16 2.4 16 3.5C16 4.6 16.9 5.5 18 5.5ZM12 13.5C10.9 13.5 10 14.4 10 15.5C10 16.6 10.9 17.5 12 17.5C13.1 17.5 14 16.6 14 15.5C14 14.4 13.1 13.5 12 13.5ZM18 13.5C16.9 13.5 16 14.4 16 15.5C16 16.6 16.9 17.5 18 17.5C19.1 17.5 20 16.6 20 15.5C20 14.4 19.1 13.5 18 13.5ZM18 7.5C16.9 7.5 16 8.4 16 9.5C16 10.6 16.9 11.5 18 11.5C19.1 11.5 20 10.6 20 9.5C20 8.4 19.1 7.5 18 7.5ZM12 7.5C10.9 7.5 10 8.4 10 9.5C10 10.6 10.9 11.5 12 11.5C13.1 11.5 14 10.6 14 9.5C14 8.4 13.1 7.5 12 7.5ZM12 1.5C10.9 1.5 10 2.4 10 3.5C10 4.6 10.9 5.5 12 5.5C13.1 5.5 14 4.6 14 3.5C14 2.4 13.1 1.5 12 1.5Z" fill="#4E6074"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -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)

View file

@ -87,16 +87,19 @@ 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
}
}
try? self.mCore.start()

View file

@ -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 {
@ -41,13 +42,17 @@ struct LinphoneApp: App {
&& 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()
}
}
}

View file

@ -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" : {

View file

@ -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()

View file

@ -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)
}

View file

@ -30,8 +30,6 @@ struct ContactsListFragment: View {
@Binding var showingSheet: Bool
var body: some View {
VStack {
List {
ForEach(0..<contactsManager.lastSearch.count, id: \.self) { index in
Button {
} label: {
@ -98,26 +96,6 @@ struct ContactsListFragment: View {
.listRowSeparator(.hidden)
}
}
.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)
)
}
}
}
#Preview {

View file

@ -491,8 +491,8 @@ struct EditContactFragment: View {
?? ContactsManager.shared.textToImage(
firstName: editContactViewModel.firstName, lastName: editContactViewModel.lastName),
name: editContactViewModel.firstName
+ editContactViewModel.lastName
+ String(Int.random(in: 1...1000))
+ editContactViewModel.lastName,
prefix: String(Int.random(in: 1...1000))
+ ((selectedImage == nil) ? "-default" : ""),
contact: newContact, linphoneFriend: true, existingFriend: editContactViewModel.selectedEditFriend)
}

View file

@ -24,6 +24,7 @@ import linphonesw
struct ContentView: View {
@Environment(\.scenePhase) var scenePhase
private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom }
@ObservedObject private var coreContext = CoreContext.shared
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
@ -35,6 +36,7 @@ struct ContentView: View {
@ObservedObject var editContactViewModel: EditContactViewModel
@ObservedObject var historyViewModel: HistoryViewModel
@ObservedObject var historyListViewModel: HistoryListViewModel
@ObservedObject var startCallViewModel: StartCallViewModel
@State var index = 0
@State private var orientation = UIDevice.current.orientation
@ -43,10 +45,12 @@ struct ContentView: View {
@State private var searchIsActive = false
@State private var text = ""
@FocusState private var focusedField: Bool
@State private var showingDialer = false
@State var isMenuOpen = false
@State var isShowDeleteContactPopup = false
@State var isShowDeleteAllHistoryPopup = false
@State var isShowEditContactFragment = false
@State var isShowStartCallFragment = false
@State var isShowDismissPopup = false
var body: some View {
@ -322,6 +326,7 @@ struct ContentView: View {
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
index: $index,
isShowStartCallFragment: $isShowStartCallFragment,
isShowEditContactFragment: $isShowEditContactFragment
)
}
@ -502,6 +507,22 @@ struct ContentView: View {
}
}
if isShowStartCallFragment {
StartCallFragment(
startCallViewModel: startCallViewModel,
isShowStartCallFragment: $isShowStartCallFragment,
showingDialer: $showingDialer
)
.zIndex(3)
.transition(.move(edge: .bottom))
.halfSheet(showSheet: $showingDialer) {
DialerBottomSheet(
startCallViewModel: startCallViewModel,
showingDialer: $showingDialer
)
} onDismiss: {}
}
if isShowDeleteContactPopup {
PopupView(isShowPopup: $isShowDeleteContactPopup,
title: Text(
@ -626,7 +647,9 @@ struct ContentView: View {
.onChange(of: scenePhase) { newPhase in
if newPhase == .active {
coreContext.onForeground()
if !isShowStartCallFragment {
contactsManager.fetchContacts()
}
print("Active")
} else if newPhase == .inactive {
print("Inactive")
@ -649,7 +672,8 @@ struct ContentView: View {
contactViewModel: ContactViewModel(),
editContactViewModel: EditContactViewModel(),
historyViewModel: HistoryViewModel(),
historyListViewModel: HistoryListViewModel()
historyListViewModel: HistoryListViewModel(),
startCallViewModel: StartCallViewModel()
)
}
// swiftlint:enable type_body_length

View file

@ -0,0 +1,320 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
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)
)
}

View file

@ -49,7 +49,9 @@ struct HistoryListFragment: View {
: ContactAvatarModel(friend: nil, withPresence: false)
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
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 {

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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..<contactsManager.lastSearchSuggestions.count, id: \.self) { index in
Button {
} label: {
HStack {
if index < contactsManager.lastSearchSuggestions.count
&& contactsManager.lastSearchSuggestions[index].address != nil
&& contactsManager.lastSearchSuggestions[index].address!.username != nil {
Image(uiImage: contactsManager.textToImage(
firstName: contactsManager.lastSearchSuggestions[index].address!.username!,
lastName: ""))
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text(contactsManager.lastSearchSuggestions[index].address?.username ?? "")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
Text("Username error")
.default_text_style(styleSize: 16)
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundStyle(Color.orangeMain500)
}
}
.padding(.horizontal)
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
}
)
.buttonStyle(.borderless)
.listRowSeparator(.hidden)
}
}
}
#Preview {
StartCallFragment(startCallViewModel: StartCallViewModel(), isShowStartCallFragment: .constant(true), showingDialer: .constant(false))
}

View file

@ -18,6 +18,7 @@
*/
import SwiftUI
import linphonesw
struct HistoryView: View {
@ -27,6 +28,7 @@ struct HistoryView: View {
@ObservedObject var editContactViewModel: EditContactViewModel
@Binding var index: Int
@Binding var isShowStartCallFragment: Bool
@Binding var isShowEditContactFragment: Bool
var body: some View {
@ -42,6 +44,10 @@ struct HistoryView: View {
)
Button {
withAnimation {
MagicSearchSingleton.shared.searchForSuggestions()
isShowStartCallFragment.toggle()
}
} label: {
Image("phone-plus")
.padding()

View file

@ -0,0 +1,27 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
import linphonesw
class StartCallViewModel: ObservableObject {
@Published var searchField: String = ""
init() {}
}

View file

@ -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))

View file

@ -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,12 +49,28 @@ 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)
}
}

View file

@ -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 {