This commit is contained in:
Benoit Martins 2023-12-21 14:06:52 +01:00
parent d89e616f37
commit cc6d599ec5
8 changed files with 479 additions and 263 deletions

View file

@ -47,7 +47,6 @@ final class ContactsManager: ObservableObject {
if core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off {
print("\(#function) - Core is being stopped or already destroyed, abort")
} else {
do {
self.friendList = try core.getFriendListByName(name: self.nativeAddressBookFriendList) ?? core.createFriendList()
} catch let error {
@ -81,6 +80,7 @@ final class ContactsManager: ObservableObject {
linphoneFriendList.displayName = self.linphoneAddressBookFriendList
core.addFriendList(list: linphoneFriendList)
}
linphoneFriendList.subscriptionsEnabled = true
}
}

View file

@ -86,12 +86,9 @@ final class CoreContext: ObservableObject {
}
self.mCore.autoIterateEnabled = false
self.mCore.friendsDatabasePath = "\(configDir)/friends.db"
self.mCore.callkitEnabled = true
self.mCore.pushNotificationEnabled = true
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

View file

@ -83,13 +83,13 @@ class TelecomManager: ObservableObject {
}
}
func startCall(core: Core, addr: Address?, isSas: Bool, isVideo: Bool, isConference: Bool = false) throws {
func startCallCallKit(core: Core, addr: Address?, isSas: Bool, isVideo: Bool, isConference: Bool = false) throws {
if addr == nil {
Log.info("Can not start a call with null address!")
return
}
if TelecomManager.callKitEnabled(core: core) && !nextCallIsTransfer != true {
if TelecomManager.callKitEnabled(core: core) {//&& !nextCallIsTransfer != true {
let uuid = UUID()
let name = "outgoingTODO" // FastAddressBook.displayName(for: addr) ?? "unknow"
let handle = CXHandle(type: .generic, value: addr?.asStringUriOnly() ?? "")
@ -110,7 +110,7 @@ class TelecomManager: ObservableObject {
func startCall(core: Core, addr: String, isSas: Bool = false, isVideo: Bool, isConference: Bool = false) {
do {
let address = try Factory.Instance.createAddress(addr: addr)
try startCall(core: core, addr: address, isSas: isSas, isVideo: isVideo, isConference: isConference)
try startCallCallKit(core: core, addr: address, isSas: isSas, isVideo: isVideo, isConference: isConference)
} catch {
Log.error("[TelecomManager] unable to create address for a new outgoing call : \(addr) \(error) ")
}
@ -118,11 +118,11 @@ class TelecomManager: ObservableObject {
func doCallWithCore(addr: Address) {
CoreContext.shared.doOnCoreQueue { core in
do {
try self.startCall(core: core, addr: addr, isSas: false, isVideo: false)
} catch {
Log.error("[TelecomManager] unable to create address for a new outgoing call : \(addr.asStringUriOnly()) \(error) ")
}
do {
try self.startCallCallKit(core: core, addr: addr, isSas: false, isVideo: false, isConference: false)
} catch {
Log.error("[TelecomManager] unable to create address for a new outgoing call : \(addr) \(error) ")
}
}
}
@ -329,6 +329,14 @@ class TelecomManager: ObservableObject {
let addr = call.remoteAddress
let displayName = incomingDisplayName(call: call)
#if targetEnvironment(simulator)
DispatchQueue.main.async {
withAnimation {
TelecomManager.shared.callInProgress = true
}
}
#endif
if call.replacedCall != nil {
endCallKitReplacedCall = false

View file

@ -18,6 +18,7 @@
*/
import SwiftUI
import CallKit
struct CallView: View {
@ -29,10 +30,213 @@ struct CallView: View {
@State var startDate = Date.now
@State var timeElapsed: Int = 0
@State var micMutted: Bool = false
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
if #available(iOS 16.4, *) {
innerView()
.background(.black)
.sheet(isPresented: .constant(true)) {
VStack {
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 {
muteCall()
} label: {
Image(micMutted ? "microphone-slash" : "microphone")
.renderingMode(.template)
.resizable()
.foregroundStyle(micMutted ? .black : .white)
.frame(width: 32, height: 32)
}
.frame(width: 60, height: 60)
.background(micMutted ? .white : 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)
HStack(spacing: 12) {
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)
Spacer()
Button {
muteCall()
} label: {
Image(micMutted ? "microphone-slash" : "microphone")
.renderingMode(.template)
.resizable()
.foregroundStyle(micMutted ? .black : .white)
.frame(width: 32, height: 32)
}
.frame(width: 60, height: 60)
.background(micMutted ? .white : Color.gray500)
.cornerRadius(40)
Spacer()
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)
Spacer()
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)
HStack(spacing: 12) {
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)
Spacer()
Button {
muteCall()
} label: {
Image(micMutted ? "microphone-slash" : "microphone")
.renderingMode(.template)
.resizable()
.foregroundStyle(micMutted ? .black : .white)
.frame(width: 32, height: 32)
}
.frame(width: 60, height: 60)
.background(micMutted ? .white : Color.gray500)
.cornerRadius(40)
Spacer()
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)
Spacer()
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)
}
.frame(maxHeight: .infinity, alignment: .top)
.background(.black)
.presentationDetents([.fraction(0.1), .medium])
.interactiveDismissDisabled()
.presentationBackgroundInteraction(.enabled)
}
}
}
@ViewBuilder
func innerView() -> some View {
VStack {
Rectangle()
.foregroundColor(Color.orangeMain500)
@ -40,222 +244,200 @@ struct CallView: View {
.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()
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)
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))
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)
}
}
.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) {
HStack {
}
.frame(width: 60, height: 60)
}
.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 {
acceptCall()
} 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() {
withAnimation {
telecomManager.callInProgress = false
telecomManager.callStarted = false
}
coreContext.doOnCoreQueue { core in
do {
// Terminates the call, whether it is ringing or running
try core.currentCall?.terminate()
} catch { NSLog(error.localizedDescription) }
if core.currentCall != nil {
telecomManager.terminateCall(call: core.currentCall!)
}
}
timer.upstream.connect().cancel()
}
func acceptCall() {
withAnimation {
telecomManager.callInProgress = true
telecomManager.callStarted = true
}
coreContext.doOnCoreQueue { core in
if core.currentCall != nil {
telecomManager.acceptCall(core: core, call: core.currentCall!, hasVideo: false)
}
}
timer.upstream.connect().cancel()
}
func muteCall() {
coreContext.doOnCoreQueue { core in
if core.currentCall != nil {
micMutted = !micMutted
core.currentCall!.microphoneMuted = micMutted
}
}
}
func counterToMinutes() -> String {
let currentTime = timeElapsed

View file

@ -446,19 +446,21 @@ struct ContentView: View {
})
: ContactAvatarModel(friend: nil, withPresence: false)
HistoryContactFragment(
contactAvatarModel: contactAvatarModel!,
historyViewModel: historyViewModel,
historyListViewModel: historyListViewModel,
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
isShowDeleteAllHistoryPopup: $isShowDeleteAllHistoryPopup,
isShowEditContactFragment: $isShowEditContactFragment,
indexPage: $index
)
.frame(maxWidth: .infinity)
.background(Color.gray100)
.ignoresSafeArea(.keyboard)
if contactAvatarModel != nil {
HistoryContactFragment(
contactAvatarModel: contactAvatarModel!,
historyViewModel: historyViewModel,
historyListViewModel: historyListViewModel,
contactViewModel: contactViewModel,
editContactViewModel: editContactViewModel,
isShowDeleteAllHistoryPopup: $isShowDeleteAllHistoryPopup,
isShowEditContactFragment: $isShowEditContactFragment,
indexPage: $index
)
.frame(maxWidth: .infinity)
.background(Color.gray100)
.ignoresSafeArea(.keyboard)
}
}
}
.onAppear {
@ -656,6 +658,9 @@ struct ContentView: View {
coreContext.onForeground()
if !isShowStartCallFragment {
contactsManager.fetchContacts()
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
historyListViewModel.computeCallLogsList()
}
}
print("Active")
} else if newPhase == .inactive {

View file

@ -231,12 +231,18 @@ struct DialerBottomSheet: View {
.clipShape(Circle())
}
}
.onTapGesture {
startCallViewModel.searchField += "0"
}
.onLongPressGesture(minimumDuration: 0.2) {
startCallViewModel.searchField += "+"
}
.simultaneousGesture(
LongPressGesture()
.onEnded { _ in
startCallViewModel.searchField += "+"
}
)
.highPriorityGesture(
TapGesture()
.onEnded { _ in
startCallViewModel.searchField += "0"
}
)
Spacer()

View file

@ -51,6 +51,11 @@ struct HistoryListFragment: View {
if addressFriend != nil && addressFriend!.photo != nil && !addressFriend!.photo!.isEmpty {
if contactAvatarModel != nil {
Avatar(contactAvatarModel: contactAvatarModel!, avatarSize: 45)
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
}
} else {
if historyListViewModel.callLogs[index].dir == .Outgoing && historyListViewModel.callLogs[index].toAddress != nil {
@ -95,7 +100,12 @@ struct HistoryListFragment: View {
.frame(width: 45, height: 45)
.clipShape(Circle())
}
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: 45, height: 45)
.clipShape(Circle())
}
}
VStack(spacing: 0) {

View file

@ -23,6 +23,7 @@ import linphonesw
struct Avatar: View {
@ObservedObject var contactAvatarModel: ContactAvatarModel
let avatarSize: CGFloat
let hidePresence: Bool
@ -33,41 +34,48 @@ struct Avatar: View {
}
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)
if contactAvatarModel.friend != nil {
AsyncImage(url: ContactsManager.shared.getImagePath(friendPhotoPath: contactAvatarModel.friend!.photo!)) { image in
switch image {
case .empty:
ProgressView()
.frame(width: avatarSize, height: avatarSize)
.clipShape(Circle())
HStack {
Spacer()
VStack {
case .success(let image):
ZStack {
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: avatarSize, height: avatarSize)
.clipShape(Circle())
HStack {
Spacer()
if !hidePresence && (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)
VStack {
Spacer()
if !hidePresence && (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)
}
.frame(width: avatarSize, height: avatarSize)
case .failure:
Image("profil-picture-default")
.resizable()
.frame(width: avatarSize, height: avatarSize)
.clipShape(Circle())
@unknown default:
EmptyView()
}
case .failure:
Image("profil-picture-default")
.resizable()
.frame(width: avatarSize, height: avatarSize)
.clipShape(Circle())
@unknown default:
EmptyView()
}
} else {
Image("profil-picture-default")
.resizable()
.frame(width: avatarSize, height: avatarSize)
.clipShape(Circle())
}
}
}