Init outgoing call

This commit is contained in:
Benoit Martins 2023-12-12 16:26:52 +01:00
parent 035149bd47
commit f7f9ee32b6
21 changed files with 634 additions and 154 deletions

View file

@ -68,6 +68,9 @@
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 */; };
D7B5678E2B28888F00DE63EB /* CallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B5678D2B28888F00DE63EB /* CallView.swift */; };
D7B99E992B29B39000BE7BF2 /* CallViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */; };
D7B99E9B2B29F7C300BE7BF2 /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */; };
D7C365082AEFAB7F00FE6142 /* ContactListBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */; };
D7C3650A2AF001C300FE6142 /* EditContactFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C365092AF001C300FE6142 /* EditContactFragment.swift */; };
D7C3650C2AF0084000FE6142 /* EditContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */; };
@ -157,6 +160,9 @@
D7A2EDDA2AC19EEC005D90FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
D7ADF5FF2AFE356400212231 /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = "<group>"; };
D7B5066C2AEFA9B900CEB4E9 /* ContactInnerFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactInnerFragment.swift; sourceTree = "<group>"; };
D7B5678D2B28888F00DE63EB /* CallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallView.swift; sourceTree = "<group>"; };
D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallViewModel.swift; sourceTree = "<group>"; };
D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = "<group>"; };
D7C365072AEFAB7F00FE6142 /* ContactListBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListBottomSheet.swift; sourceTree = "<group>"; };
D7C365092AF001C300FE6142 /* EditContactFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactFragment.swift; sourceTree = "<group>"; };
D7C3650B2AF0084000FE6142 /* EditContactViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditContactViewModel.swift; sourceTree = "<group>"; };
@ -232,6 +238,7 @@
D74C9D002ACB098C0021626A /* PermissionManager.swift */,
D7C3650D2AF15BF200FE6142 /* PhotoPicker.swift */,
D732A9082AFD235500DB42BA /* ShareSheetController.swift */,
D7B99E9A2B29F7C200BE7BF2 /* ActivityIndicator.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -286,6 +293,7 @@
isa = PBXGroup;
children = (
D719ABCA2ABC761800B41C10 /* Assistant */,
D7B5678C2B28883700DE63EB /* Call */,
D719ABC62ABC6F0200B41C10 /* Main */,
D7702EF02AC7200600557C00 /* Welcome */,
);
@ -473,6 +481,23 @@
path = Ressources;
sourceTree = "<group>";
};
D7B5678C2B28883700DE63EB /* Call */ = {
isa = PBXGroup;
children = (
D7B99E972B29B37F00BE7BF2 /* ViewModel */,
D7B5678D2B28888F00DE63EB /* CallView.swift */,
);
path = Call;
sourceTree = "<group>";
};
D7B99E972B29B37F00BE7BF2 /* ViewModel */ = {
isa = PBXGroup;
children = (
D7B99E982B29B39000BE7BF2 /* CallViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
};
D7D24D0C2AC1B4C700C6F35B /* Fonts */ = {
isa = PBXGroup;
children = (
@ -611,6 +636,7 @@
D719ABB92ABC67BF00B41C10 /* ContentView.swift in Sources */,
D71FCA832AE14D6E00D2E43E /* ContactFragment.swift in Sources */,
D7C3650C2AF0084000FE6142 /* EditContactViewModel.swift in Sources */,
D7B5678E2B28888F00DE63EB /* CallView.swift in Sources */,
D750D3392AD3E6EE00EC99C5 /* PopupLoadingView.swift in Sources */,
D7E6D0492AE933AD00A57AAF /* FavoriteContactsListFragment.swift in Sources */,
662B69DB2B25DE25007118BF /* ProviderDelegate.swift in Sources */,
@ -633,6 +659,7 @@
D7C48DF42AFA66F900D938CB /* EditContactController.swift in Sources */,
66C491FF2B24D4AC00CEA16D /* FileUtils.swift in Sources */,
D74C9D012ACB098C0021626A /* PermissionManager.swift in Sources */,
D7B99E992B29B39000BE7BF2 /* CallViewModel.swift in Sources */,
D7702EF22AC7205000557C00 /* WelcomeView.swift in Sources */,
D732A9152B04C7FE00DB42BA /* HistoryListViewModel.swift in Sources */,
D71FCA7F2AE1397200D2E43E /* ContactsListViewModel.swift in Sources */,
@ -660,6 +687,7 @@
D72343342ACEFFC3009AA24E /* QRScanner.swift in Sources */,
66C491FB2B24D32600CEA16D /* CoreExtension.swift in Sources */,
D726E43D2B19E4FE0083C415 /* StartCallFragment.swift in Sources */,
D7B99E9B2B29F7C300BE7BF2 /* ActivityIndicator.swift in Sources */,
D72343302ACEFEF8009AA24E /* QrCodeScannerFragment.swift in Sources */,
D726E43F2B19E56F0083C415 /* StartCallViewModel.swift in Sources */,
D7D1698C2AE66FA500109A5C /* MagicSearchSingleton.swift in Sources */,

View file

@ -313,6 +313,9 @@
},
"I understand" : {
},
"Incoming call" : {
},
"Incoming Call" : {
@ -352,6 +355,9 @@
},
"Message" : {
},
"Missed call" : {
},
"My Profile" : {
@ -385,6 +391,9 @@
},
"Other actions" : {
},
"Outgoing call" : {
},
"Outgoing Call" : {

View file

@ -24,6 +24,7 @@ import UIKit
import linphonesw
import AVFoundation
import os
import SwiftUI
class CallInfo {
var callId: String = ""
@ -207,6 +208,12 @@ extension ProviderDelegate: CXProviderDelegate {
let uuid = action.callUUID
let callInfo = callInfos[uuid]
let callId = callInfo?.callId ?? ""
DispatchQueue.main.async {
withAnimation {
TelecomManager.shared.callInProgress = true
}
}
CoreContext.shared.doOnCoreQueue { core in
Log.info("CallKit: answer call with call-id: \(String(describing: callId)) and UUID: \(uuid.description).")
@ -225,7 +232,11 @@ extension ProviderDelegate: CXProviderDelegate {
}
TelecomManager.shared.callkitAudioSessionActivated = false
core.configureAudioSession()
TelecomManager.shared.acceptCall(core: core, call: call!, hasVideo: call!.params?.videoEnabled ?? false)
if call != nil {
TelecomManager.shared.acceptCall(core: core, call: call!, hasVideo: call!.params?.videoEnabled ?? false)
}
action.fulfill()
}
}

View file

@ -24,6 +24,7 @@ import UserNotifications
import os
import CallKit
import AVFoundation
import SwiftUI
class CallAppData: NSObject {
var batteryWarningShown = false
@ -32,13 +33,16 @@ class CallAppData: NSObject {
}
class TelecomManager {
class TelecomManager: ObservableObject {
static let shared = TelecomManager()
static var uuidReplacedCall: String?
let providerDelegate: ProviderDelegate // to support callkit
let callController: CXCallController // to support callkit
@Published var callInProgress: Bool = false
@Published var callStarted: Bool = false
var actionToFulFill: CXCallAction?
var callkitAudioSessionActivated: Bool?
var nextCallIsTransfer: Bool = false
@ -78,7 +82,17 @@ class TelecomManager {
sCall.userData = UnsafeMutableRawPointer(Unmanaged.passRetained(appData!).toOpaque())
}
}
func doCallWithCore(addr: Address) {
CoreContext.shared.doOnCoreQueue { core in
do {
try self.doCall(core: core, addr: addr, isSas: false, isVideo: false)
} catch {
}
}
}
func doCall(core: Core, addr: Address, isSas: Bool, isVideo: Bool, isConference: Bool = false) throws {
// let displayName = FastAddressBook.displayName(for: addr.getCobject)
@ -135,6 +149,13 @@ class TelecomManager {
/* will be used later to notify user if video was not activated because of the linphone core*/
}
}
DispatchQueue.main.async {
self.callStarted = true
withAnimation {
self.callInProgress = true
}
}
}
}
@ -171,6 +192,10 @@ class TelecomManager {
}
try call.acceptWithParams(params: callParams)
DispatchQueue.main.async {
self.callStarted = true
}
} catch {
Log.error("accept call failed \(error)")
}
@ -195,7 +220,18 @@ class TelecomManager {
}
func incomingDisplayName(call: Call) -> String {
// TODO
if call.remoteAddress != nil {
let friend = ContactsManager.shared.getFriendWithAddress(address: call.remoteAddress!)
if friend != nil && friend!.address != nil && friend!.address!.displayName != nil {
return friend!.address!.displayName!
} else {
if call.remoteAddress!.displayName != nil {
return call.remoteAddress!.displayName!
} else if call.remoteAddress!.username != nil {
return call.remoteAddress!.username!
}
}
}
return "IncomingDisplayName"
}
@ -361,6 +397,13 @@ class TelecomManager {
}
case .End,
.Error:
DispatchQueue.main.async {
withAnimation {
self.callInProgress = false
self.callStarted = false
}
}
var displayName = "Unknown"
if call.dir == .Incoming {
displayName = incomingDisplayName(call: call)

View file

@ -0,0 +1,276 @@
/*
* Copyright (c) 2010-2020 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
struct CallView: View {
@ObservedObject private var coreContext = CoreContext.shared
@ObservedObject private var telecomManager = TelecomManager.shared
@ObservedObject private var contactsManager = ContactsManager.shared
@ObservedObject var callViewModel: CallViewModel
@State var startDate = Date.now
@State var timeElapsed: Int = 0
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Rectangle()
.foregroundColor(Color.orangeMain500)
.edgesIgnoringSafeArea(.top)
.frame(height: 0)
HStack {
if callViewModel.direction == .Outgoing {
Image("outgoing-call")
.resizable()
.frame(width: 15, height: 15)
.padding(.horizontal)
Text("Outgoing call")
.foregroundStyle(.white)
} else {
Image("incoming-call")
.resizable()
.frame(width: 15, height: 15)
.padding(.horizontal)
Text("Incoming call")
.foregroundStyle(.white)
}
Spacer()
}
.frame(height: 40)
ZStack {
VStack {
Spacer()
if callViewModel.remoteAddress != nil {
let addressFriend = contactsManager.getFriendWithAddress(address: callViewModel.remoteAddress!)
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 {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 100, hidePresence: true)
}
} else {
if callViewModel.remoteAddress!.displayName != nil {
Image(uiImage: contactsManager.textToImage(
firstName: callViewModel.remoteAddress!.displayName!,
lastName: callViewModel.remoteAddress!.displayName!.components(separatedBy: " ").count > 1
? callViewModel.remoteAddress!.displayName!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
} else {
Image(uiImage: contactsManager.textToImage(
firstName: callViewModel.remoteAddress!.username ?? "Username Error",
lastName: callViewModel.remoteAddress!.username!.components(separatedBy: " ").count > 1
? callViewModel.remoteAddress!.username!.components(separatedBy: " ")[1]
: ""))
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
}
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
}
Text(callViewModel.displayName)
.padding(.top)
.foregroundStyle(.white)
Text(callViewModel.remoteAddressString)
.foregroundStyle(.white)
Spacer()
}
if !telecomManager.callStarted {
VStack {
ActivityIndicator()
.frame(width: 20, height: 20)
.padding(.top, 100)
Text(counterToMinutes())
.onReceive(timer) { firedDate in
timeElapsed = Int(firedDate.timeIntervalSince(startDate))
}
.padding(.top)
.foregroundStyle(.white)
Spacer()
}
.background(.clear)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray600)
.cornerRadius(20)
.padding(.horizontal, 4)
if telecomManager.callStarted {
HStack(spacing: 12) {
Button {
terminateCall()
} label: {
Image("phone-disconnect")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
.frame(width: 90, height: 60)
.background(Color.redDanger500)
.cornerRadius(40)
Spacer()
Button {
} label: {
Image("video-camera")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
Button {
} label: {
Image("microphone")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
Button {
} label: {
Image("speaker-high")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
.frame(width: 60, height: 60)
.background(Color.gray500)
.cornerRadius(40)
}
.padding(.horizontal, 25)
.padding(.top, 20)
} else {
HStack(spacing: 12) {
Spacer()
Button {
terminateCall()
} label: {
Image("phone-disconnect")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
.frame(width: 90, height: 60)
.background(Color.redDanger500)
.cornerRadius(40)
Button {
//telecomManager.callStarted.toggle()
} label: {
Image("phone")
.renderingMode(.template)
.resizable()
.foregroundStyle(.white)
.frame(width: 32, height: 32)
}
.frame(width: 90, height: 60)
.background(Color.greenSuccess500)
.cornerRadius(40)
Spacer()
}
.padding(.horizontal, 25)
.padding(.top, 20)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.gray900)
}
func terminateCall() {
coreContext.doOnCoreQueue { core in
do {
// Terminates the call, whether it is ringing or running
try core.currentCall?.terminate()
} catch { NSLog(error.localizedDescription) }
}
timer.upstream.connect().cancel()
}
func counterToMinutes() -> String {
let currentTime = timeElapsed
let seconds = currentTime % 60
let minutes = String(format: "%02d", Int(currentTime / 60))
let hours = String(format: "%02d", Int(currentTime / 3600))
if Int(currentTime / 3600) > 0 {
return "\(hours):\(minutes):\(seconds < 10 ? "0" : "")\(seconds)"
} else {
return "\(minutes):\(seconds < 10 ? "0" : "")\(seconds)"
}
}
}
#Preview {
CallView(callViewModel: CallViewModel())
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2010-2020 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 CallViewModel: ObservableObject {
var coreContext = CoreContext.shared
var telecomManager = TelecomManager.shared
@Published var displayName: String = "Example Linphone"
@Published var direction: Call.Dir = .Outgoing
@Published var remoteAddressString: String = "example.linphone@sip.linphone.org"
@Published var remoteAddress: Address?
@Published var avatarModel: ContactAvatarModel?
init() {
coreContext.doOnCoreQueue { core in
if core.currentCall != nil && core.currentCall!.remoteAddress != nil {
DispatchQueue.main.async {
self.direction = .Incoming
self.remoteAddressString = String(core.currentCall!.remoteAddress!.asStringUriOnly().dropFirst(4))
self.remoteAddress = core.currentCall!.remoteAddress!
let friend = ContactsManager.shared.getFriendWithAddress(address: core.currentCall!.remoteAddress!)
if friend != nil && friend!.address != nil && friend!.address!.displayName != nil {
self.displayName = friend!.address!.displayName!
} else {
if core.currentCall!.remoteAddress!.displayName != nil {
self.displayName = core.currentCall!.remoteAddress!.displayName!
} else if core.currentCall!.remoteAddress!.username != nil {
self.displayName = core.currentCall!.remoteAddress!.username!
}
}
}
}
}
}
}

View file

@ -22,6 +22,7 @@ import SwiftUI
struct ContactInnerActionsFragment: View {
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject private var telecomManager = TelecomManager.shared
@ObservedObject var contactViewModel: ContactViewModel
@ObservedObject var editContactViewModel: EditContactViewModel
@ -62,8 +63,7 @@ struct ContactInnerActionsFragment: View {
VStack(spacing: 0) {
if contactViewModel.indexDisplayedFriend != nil && contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend != nil {
ForEach(0..<contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses.count, id: \.self) { index in
Button {
} label: {
HStack {
HStack {
VStack {
Text("SIP address :")
@ -82,30 +82,22 @@ struct ContactInnerActionsFragment: View {
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.onTapGesture {
withAnimation {
}
}
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.stringToCopy = contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index].asStringUriOnly()
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
}
}
)
.background(.white)
.onTapGesture {
withAnimation {
telecomManager.doCallWithCore(
addr: contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index]
)
}
}
.onLongPressGesture(minimumDuration: 0.2) {
contactViewModel.stringToCopy = contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses[index].asStringUriOnly()
showingSheet.toggle()
}
if !contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.isEmpty
|| index < contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.addresses.count - 1 {
@ -117,8 +109,7 @@ struct ContactInnerActionsFragment: View {
}
ForEach(0..<contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.count, id: \.self) { index in
Button {
} label: {
HStack {
HStack {
VStack {
if contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].label != nil
@ -138,37 +129,16 @@ struct ContactInnerActionsFragment: View {
.fixedSize(horizontal: false, vertical: true)
}
Spacer()
Image("phone")
.renderingMode(.template)
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.onTapGesture {
withAnimation {
}
}
}
.padding(.vertical, 15)
.padding(.horizontal, 20)
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.stringToCopy =
contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].phoneNumber
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
}
}
)
.background(.white)
.onLongPressGesture(minimumDuration: 0.2) {
contactViewModel.stringToCopy =
contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbersWithLabel[index].phoneNumber
showingSheet.toggle()
}
if index < contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.phoneNumbers.count - 1 {
VStack {

View file

@ -25,6 +25,7 @@ struct ContactInnerFragment: View {
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject private var telecomManager = TelecomManager.shared
@ObservedObject var contactAvatarModel: ContactAvatarModel
@ObservedObject var contactViewModel: ContactViewModel
@ -150,6 +151,7 @@ struct ContactInnerFragment: View {
Spacer()
Button(action: {
telecomManager.doCallWithCore(addr: contactsManager.lastSearch[contactViewModel.indexDisplayedFriend!].friend!.address!)
}, label: {
VStack {
HStack(alignment: .center) {
@ -158,11 +160,6 @@ struct ContactInnerFragment: View {
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.onTapGesture {
withAnimation {
}
}
}
.padding(16)
.background(Color.grayMain2c200)

View file

@ -75,7 +75,7 @@ struct ContactsInnerFragment: View {
VStack {
List {
ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet)}
ContactsListFragment(contactViewModel: contactViewModel, contactsListViewModel: ContactsListViewModel(), showingSheet: $showingSheet, startCallFunc: {addr in })}
.listStyle(.plain)
.overlay(
VStack {

View file

@ -29,10 +29,11 @@ struct ContactsListFragment: View {
@Binding var showingSheet: Bool
var startCallFunc: (_ addr: Address) -> Void
var body: some View {
ForEach(0..<contactsManager.lastSearch.count, id: \.self) { index in
Button {
} label: {
HStack {
HStack {
if index == 0
|| contactsManager.lastSearch[index].friend?.name!.lowercased().folding(
@ -77,21 +78,19 @@ struct ContactsListFragment: View {
.foregroundStyle(Color.orangeMain500)
}
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.selectedFriend = contactsManager.lastSearch[index].friend
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
contactViewModel.indexDisplayedFriend = index
}
}
)
.background(.white)
.onTapGesture {
withAnimation {
contactViewModel.indexDisplayedFriend = index
}
if contactsManager.lastSearch[index].friend != nil && contactsManager.lastSearch[index].friend!.address != nil {
startCallFunc(contactsManager.lastSearch[index].friend!.address!)
}
}
.onLongPressGesture(minimumDuration: 0.2) {
contactViewModel.selectedFriend = contactsManager.lastSearch[index].friend
showingSheet.toggle()
}
.buttonStyle(.borderless)
.listRowSeparator(.hidden)
}
@ -99,5 +98,5 @@ struct ContactsListFragment: View {
}
#Preview {
ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false))
ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false), startCallFunc: {addr in })
}

View file

@ -34,8 +34,7 @@ struct FavoriteContactsListFragment: View {
HStack {
ForEach(0..<contactsManager.lastSearch.count, id: \.self) { index in
if contactsManager.lastSearch[index].friend != nil && contactsManager.lastSearch[index].friend!.starred == true {
Button {
} label: {
VStack {
VStack {
if contactsManager.lastSearch[index].friend!.photo != nil
&& !contactsManager.lastSearch[index].friend!.photo!.isEmpty {
@ -51,21 +50,16 @@ struct FavoriteContactsListFragment: View {
.frame( maxWidth: .infinity, alignment: .center)
}
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
contactViewModel.selectedFriend = contactsManager.lastSearch[index].friend
showingSheet.toggle()
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
contactViewModel.indexDisplayedFriend = index
}
}
)
.background(.white)
.onTapGesture {
withAnimation {
contactViewModel.indexDisplayedFriend = index
}
}
.onLongPressGesture(minimumDuration: 0.2) {
contactViewModel.selectedFriend = contactsManager.lastSearch[index].friend
showingSheet.toggle()
}
.frame(minWidth: 70, maxWidth: 70)
}
}

View file

@ -28,6 +28,7 @@ struct ContentView: View {
@ObservedObject private var coreContext = CoreContext.shared
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
@ObservedObject private var telecomManager = TelecomManager.shared
@ObservedObject var contactsManager = ContactsManager.shared
var magicSearch = MagicSearchSingleton.shared
@ -620,6 +621,12 @@ struct ContentView: View {
}
}
if telecomManager.callInProgress {
CallView(callViewModel: CallViewModel())
.zIndex(3)
.transition(.scale.combined(with: .move(edge: .top)))
}
//if sharedMainViewModel.displayToast {
ToastView()
.zIndex(3)

View file

@ -44,7 +44,7 @@ struct SideMenu: View {
HStack {
List {
Text("My Profile").onTapGesture {
print("My Profile")
print("My Profile")
}
Text("Send logs").onTapGesture {
sendLogs()

View file

@ -19,6 +19,7 @@
import SwiftUI
import UniformTypeIdentifiers
import linphonesw
struct DialerBottomSheet: View {
@ -29,6 +30,7 @@ struct DialerBottomSheet: View {
@ObservedObject private var magicSearch = MagicSearchSingleton.shared
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject private var telecomManager = TelecomManager.shared
@ObservedObject var startCallViewModel: StartCallViewModel
@ -229,18 +231,12 @@ struct DialerBottomSheet: View {
.clipShape(Circle())
}
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
startCallViewModel.searchField += "+"
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
startCallViewModel.searchField += "0"
}
)
.onTapGesture {
startCallViewModel.searchField += "0"
}
.onLongPressGesture(minimumDuration: 0.2) {
startCallViewModel.searchField += "+"
}
Spacer()
@ -270,6 +266,14 @@ struct DialerBottomSheet: View {
Spacer()
Button {
if !startCallViewModel.searchField.isEmpty {
do {
let address = try Factory.Instance.createAddress(addr: String("sip:" + startCallViewModel.searchField + "@" + startCallViewModel.domain))
telecomManager.doCallWithCore(addr: address)
} catch {
}
}
} label: {
Image("phone")
.renderingMode(.template)

View file

@ -24,8 +24,9 @@ struct HistoryContactFragment: View {
@State private var orientation = UIDevice.current.orientation
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject private var telecomManager = TelecomManager.shared
@ObservedObject private var sharedMainViewModel = SharedMainViewModel.shared
@ObservedObject var contactAvatarModel: ContactAvatarModel
@ObservedObject var historyViewModel: HistoryViewModel
@ -372,6 +373,15 @@ struct HistoryContactFragment: View {
Spacer()
Button(action: {
if historyViewModel.displayedCall!.dir == .Outgoing && historyViewModel.displayedCall!.toAddress != nil {
telecomManager.doCallWithCore(
addr: historyViewModel.displayedCall!.toAddress!
)
} else if historyViewModel.displayedCall!.dir == .Incoming && historyViewModel.displayedCall!.fromAddress != nil {
telecomManager.doCallWithCore(
addr: historyViewModel.displayedCall!.fromAddress!
)
}
}, label: {
VStack {
HStack(alignment: .center) {
@ -380,11 +390,6 @@ struct HistoryContactFragment: View {
.resizable()
.foregroundStyle(Color.grayMain2c600)
.frame(width: 25, height: 25)
.onTapGesture {
withAnimation {
}
}
}
.padding(16)
.background(Color.grayMain2c200)

View file

@ -23,6 +23,7 @@ import linphonesw
struct HistoryListFragment: View {
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject private var telecomManager = TelecomManager.shared
@ObservedObject var historyListViewModel: HistoryListViewModel
@ObservedObject var historyViewModel: HistoryViewModel
@ -33,8 +34,7 @@ struct HistoryListFragment: View {
VStack {
List {
ForEach(0..<historyListViewModel.callLogs.count, id: \.self) { index in
Button {
} label: {
HStack {
HStack {
let fromAddressFriend = contactsManager.getFriendWithAddress(address: historyListViewModel.callLogs[index].fromAddress!)
let toAddressFriend = contactsManager.getFriendWithAddress(address: historyListViewModel.callLogs[index].toAddress!)
@ -148,26 +148,38 @@ struct HistoryListFragment: View {
.resizable()
.frame(width: 25, height: 25)
.padding(.trailing, 5)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
withAnimation {
if historyListViewModel.callLogs[index].dir == .Outgoing && historyListViewModel.callLogs[index].toAddress != nil {
telecomManager.doCallWithCore(
addr: historyListViewModel.callLogs[index].toAddress!
)
} else if historyListViewModel.callLogs[index].fromAddress != nil {
telecomManager.doCallWithCore(
addr: historyListViewModel.callLogs[index].fromAddress!
)
}
historyViewModel.displayedCall = nil
}
}
)
}
}
.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.displayedCall = historyListViewModel.callLogs[index]
}
}
)
.background(.white)
.onTapGesture {
withAnimation {
historyViewModel.displayedCall = historyListViewModel.callLogs[index]
}
}
.onLongPressGesture(minimumDuration: 0.2) {
historyViewModel.selectedCall = historyListViewModel.callLogs[index]
showingSheet.toggle()
}
}
}
.listStyle(.plain)

View file

@ -24,6 +24,7 @@ struct StartCallFragment: View {
@ObservedObject var contactsManager = ContactsManager.shared
@ObservedObject var magicSearch = MagicSearchSingleton.shared
@ObservedObject private var telecomManager = TelecomManager.shared
@ObservedObject var startCallViewModel: StartCallViewModel
@ -154,7 +155,21 @@ struct StartCallFragment: View {
.padding(.horizontal, 16)
}
ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false))
ContactsListFragment(contactViewModel: ContactViewModel(), contactsListViewModel: ContactsListViewModel(), showingSheet: .constant(false), startCallFunc: { addr in
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()
telecomManager.doCallWithCore(addr: addr)
}
})
.padding(.horizontal, 16)
HStack(alignment: .center) {
@ -220,20 +235,27 @@ struct StartCallFragment: View {
.foregroundStyle(Color.orangeMain500)
}
}
.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()
if contactsManager.lastSearchSuggestions[index].address != nil {
telecomManager.doCallWithCore(
addr: contactsManager.lastSearchSuggestions[index].address!
)
}
}
}
.padding(.horizontal)
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
}
)
.buttonStyle(.borderless)
.listRowSeparator(.hidden)
}

View file

@ -47,17 +47,19 @@ class HistoryListViewModel: ObservableObject {
}
}
core.publisher?.onCallLogUpdated?.postOnMainQueue { (_: (_: Core, _: CallLog)) in
core.publisher?.onCallLogUpdated?.postOnCoreQueue { (_: (_: Core, _: CallLog)) in
print("publisherpublisher onCallLogUpdated")
let account = core.defaultAccount
let logs = account != nil ? account!.callLogs : core.callLogs
self.callLogs.removeAll()
self.callLogsTmp.removeAll()
logs.forEach { log in
self.callLogs.append(log)
self.callLogsTmp.append(log)
DispatchQueue.main.async {
self.callLogs.removeAll()
self.callLogsTmp.removeAll()
logs.forEach { log in
self.callLogs.append(log)
self.callLogsTmp.append(log)
}
}
}
}

View file

@ -22,6 +22,12 @@ import linphonesw
class StartCallViewModel: ObservableObject {
@Published var searchField: String = ""
var domain: String = ""
init() {}
init() {
CoreContext.shared.doOnCoreQueue { core in
self.domain = core.defaultAccount?.params?.domain ?? ""
}
}
}

View file

@ -0,0 +1,33 @@
//
// ActivityIndicator.swift
// Linphone
//
// Created by Martins Benoît on 13/12/2023.
//
import SwiftUI
struct ActivityIndicator: View {
let style = StrokeStyle(lineWidth: 3, lineCap: .round)
@State var animate = false
let color1 = Color.white
let color2 = Color.white.opacity(0.5)
var body: some View {
ZStack {
Circle()
.trim(from: 0, to: 0.7)
.stroke(
AngularGradient(gradient: .init(colors: [color1, color2]), center: .center), style: style)
.rotationEffect(Angle(degrees: animate ? 360: 0))
.animation(Animation.linear(duration: 0.7).repeatForever(autoreverses: false), value: UUID())
}.onAppear {
self.animate.toggle()
}
}
}
#Preview {
ActivityIndicator()
}

View file

@ -24,6 +24,13 @@ struct Avatar: View {
@ObservedObject var contactAvatarModel: ContactAvatarModel
let avatarSize: CGFloat
let hidePresence: Bool
init(contactAvatarModel: ContactAvatarModel, avatarSize: CGFloat, hidePresence: Bool = false) {
self.contactAvatarModel = contactAvatarModel
self.avatarSize = avatarSize
self.hidePresence = hidePresence
}
var body: some View {
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: contactAvatarModel.friend!.photo!)) { image in
@ -42,7 +49,7 @@ struct Avatar: View {
Spacer()
VStack {
Spacer()
if contactAvatarModel.presenceStatus == .Online || contactAvatarModel.presenceStatus == .Busy {
if !hidePresence && (contactAvatarModel.presenceStatus == .Online || contactAvatarModel.presenceStatus == .Busy) {
Image(contactAvatarModel.presenceStatus == .Online ? "presence-online" : "presence-busy")
.resizable()
.frame(width: avatarSize/4, height: avatarSize/4)