diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c9a545775..3fbfa5902 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,13 +7,13 @@ variables:
job-ios:
stage: build
- tags: [ "macosx-xcode12" ]
+ tags: [ "macmini-m1-xcode13" ]
script:
- pod install --repo-update
- pwd
- - xcodebuild archive -scheme $archive_scheme -archivePath ./$archive_path -configuration Release -workspace ./linphone.xcworkspace -UseModernBuildSystem=NO
- - xcodebuild -exportArchive -archivePath ./$archive_path -exportPath ./$export_path -exportOptionsPlist ./$export_options_plist -allowProvisioningUpdates -UseModernBuildSystem=NO
+ - xcodebuild archive -scheme $archive_scheme -archivePath ./$archive_path -configuration Release -workspace ./linphone.xcworkspace -UseModernBuildSystem=YES -destination 'generic/platform=iOS'
+ - xcodebuild -exportArchive -archivePath ./$archive_path -exportPath ./$export_path -exportOptionsPlist ./$export_options_plist -allowProvisioningUpdates -UseModernBuildSystem=YES -destination 'generic/platform=iOS'
artifacts:
diff --git a/Classes/AudioHelper.m b/Classes/AudioHelper.m
deleted file mode 100644
index 7298e65b3..000000000
--- a/Classes/AudioHelper.m
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * 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 .
- */
-
-#import "AudioHelper.h"
-
-@implementation AudioHelper
-
-+ (NSArray *)bluetoothRoutes {
- return @[AVAudioSessionPortBluetoothHFP, AVAudioSessionPortCarAudio, AVAudioSessionPortBluetoothA2DP, AVAudioSessionPortBluetoothLE ];
-}
-
-+ (AVAudioSessionPortDescription *)bluetoothAudioDevice {
- return [AudioHelper audioDeviceFromTypes:[AudioHelper bluetoothRoutes]];
-}
-
-+ (AVAudioSessionPortDescription *)builtinAudioDevice {
- NSArray *builtinRoutes = @[ AVAudioSessionPortBuiltInMic ];
- return [AudioHelper audioDeviceFromTypes:builtinRoutes];
-}
-
-+ (AVAudioSessionPortDescription *)speakerAudioDevice {
- NSArray *builtinRoutes = @[ AVAudioSessionPortBuiltInSpeaker ];
- return [AudioHelper audioDeviceFromTypes:builtinRoutes];
-}
-
-+ (AVAudioSessionPortDescription *)audioDeviceFromTypes:(NSArray *)types {
- NSArray *routes = [[AVAudioSession sharedInstance] availableInputs];
- for (AVAudioSessionPortDescription *route in routes) {
- if ([types containsObject:route.portType]) {
- return route;
- }
- }
- return nil;
-}
-
-@end
diff --git a/Classes/Base.lproj/CallIncomingView.xib b/Classes/Base.lproj/CallIncomingView.xib
deleted file mode 100644
index ae5bd9336..000000000
--- a/Classes/Base.lproj/CallIncomingView.xib
+++ /dev/null
@@ -1,317 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Classes/Base.lproj/CallOutgoingView.xib b/Classes/Base.lproj/CallOutgoingView.xib
deleted file mode 100644
index 890553e00..000000000
--- a/Classes/Base.lproj/CallOutgoingView.xib
+++ /dev/null
@@ -1,623 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Classes/Base.lproj/CallView.xib b/Classes/Base.lproj/CallView.xib
deleted file mode 100644
index 57a17a2c7..000000000
--- a/Classes/Base.lproj/CallView.xib
+++ /dev/null
@@ -1,1857 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T
-S2V5ZWRBcmNoaXZlctEICVRyb290gAGvEBELDBkaHxQkKSoxNDdBSUpOUVUkbnVsbNYNDg8QERITFBUW
-FxhWTlNTaXplXk5TUmVzaXppbmdNb2RlViRjbGFzc1xOU0ltYWdlRmxhZ3NWTlNSZXBzV05TQ29sb3KA
-AhAAgBASIMAAAIADgAtYezMzLCAzM33SGw8cHlpOUy5vYmplY3RzoR2ABIAK0hsPICOiISKABYAGgAnT
-DyUmJygUXxAUTlNUSUZGUmVwcmVzZW50YXRpb25fEBlOU0ludGVybmFsTGF5b3V0RGlyZWN0aW9ugAiA
-B08RGbpNTQAqAAARDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAABAwMDFQYGBiYGBgYiAgICDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAIGBgYhCQkJNgkJCTEJCQkzCQkJMwMDAxQAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDAxUKCgo2BQUFIAEBAQgCAgIMCAgIKwkJCTEBAQEHAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAEAwMDFAAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAYGBiMICAgwAAAABQAAAAAAAAAAAgIC
-EQoKCjYCAgISAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAACAgIOCgoKNQcHBy4DAwMSAAAAAQAAAAAAAAAAAAAAAAYGBiEICAgxAQEB
-BwAAAAAAAAAAAwMDFAoKCjYCAgIQAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMPCQkJMgkJCTUJCQk0BAQEGQAAAAAAAAAAAAAA
-AAUFBRsKCgo3BQUFHwAAAAAAAAAEBwcHLAkJCTMCAgIMAAAAAAAAAAABAQEHCAgIKwkJCS0GBgYiAgIC
-EQAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQUFBRoJCQkyCQkJNAkJCTMKCgo1BAQE
-FwAAAAAAAAAABAQEGQkJCTMICAgxAwMDFwAAAAAAAAADBQUFIgkJCTQHBwcuAgICDQAAAAAAAAAFBQUF
-IQcHBykJCQkyCgoKNgcHBygBAQEKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBgYGHgoKCjYHBwcnBwcH
-KAoKCjYHBwcnAAAAAgAAAAADAwMUCgoKNgcHBycBAQEJAAAAAAAAAAAAAAAAAAAAAQICAhIJCQkwCAgI
-LwEBAQcAAAAAAAAAAAAAAAEAAAAGBQUFGwkJCTMICAgvAgICCgAAAAAAAAAAAAAAAAAAAAADAwMWCgoK
-NgYGBiEAAAACAwMDEwgICC4CAgIKAAAAAAAAAAIHBwcsBwcHLAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAACAgIQCgoKNAUFBR0AAAAAAAAAAAAAAAAAAAAAAAAAAAMDAw8JCQkyCAgIKgAAAAIAAAAAAAAA
-AAAAAAQHBwcuBwcHLAAAAAQAAAAAAAAAAAAAAAIAAAAAAAAAAAICAgkJCQk0BQUFHAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAABCQkJLQcHBygAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQaCgoK
-NQUFBSEFBQUfAwMDEgMDAxIKCgo2BAQEFgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQgJCQkzCAgI
-LwUFBSIFBQUaAwMDFQMDAxQDAwMUBAQEFgUFBRwHBwclCQkJNQcHBycAAAAAAAAAAAAAAAAAAAAAAAAA
-AQUFBR8ICAgwCQkJMwkJCTQKCgo3BQUFHwYGBiEJCQk0AQEBCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAEEBAQYBwcHKQgICDAJCQkzCQkJNQkJCTUJCQk1CQkJNAkJCTMICAgvBgYGJAICAg4AAAAAAAAA
-AAAAAAAAAAAAAAAAAQUFBR8JCQk0CQkJNAkJCTQHBwcqAAAABAYGBiUICAgvAAAABgAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUCAgILAwMDDwICAhEDAwMPAwMDDgEBAQkAAAADAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMTCQkJLQkJCTIDAwMOAAAAAAEBAQcCAgIMAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAAAAQEBBgICAgwAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBBwUFBSAHBwcsCAgIKwUFBRwAAAADAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAICAgoGBgYjBwcHLAcHByoEBAQYAAAAAQAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCAgIKgoKCjUICAgoCAgIKwkJCTYGBgYjAAAA
-AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBCQgICC8JCQkzBwcHKAcHBywKCgo2BQUF
-HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQXCgoKNgQEBBcAAAAAAAAA
-AgUFBR8JCQk1AwMDDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQUFHwkJCTQCAgIRAAAA
-AAAAAAMGBgYmCQkJMgEBAQkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBQUgCQkJ
-MgAAAAQAAAAAAAAAAAICAgwJCQk1BAQEFwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwcH
-KQgICCsAAAABAAAAAAAAAAADAwMUCQkJNQICAhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAEBAQYCQkJNQICAhAAAAAAAAAAAAQEBBgJCQk1AgICEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAABQUFIQkJCTEBAQEKAAAAAAAAAAEFBQUgCQkJMwEBAQoAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAMGBgYeCgoKNwYGBiYAAAAAAAAAAggICC4JCQk1BAQEGAAAAAEAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAFBgYGJQoKCjgFBQUfAAAAAAEBAQgJCQkyCAgIMgICAhEAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwYGBiMJCQk2BwcHJwICAg0AAAAAAAAAAAMDAxIICAgrCQkJ
-NgUFBR0AAAABAAAAAAAAAAAAAAAAAAAAAAAAAAYHBwcqCQkJNQYGBiMBAQEKAAAAAAAAAAEDAwMXCAgI
-LgkJCTQEBAQWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQUFGgoKCjYFBQUdAAAAAQAAAAAAAAAAAAAA
-AAAAAAAAAAAEBgYGJAoKCjUCAgIRAAAAAAAAAAAAAAAAAAAAAAYGBiEKCgo1BAQEFwAAAAAAAAAAAAAA
-AAAAAAAAAAAAAQEBCAgICCoJCQkzAgICCwAAAAAAAAAAAAAAAAAAAAAAAAAECQkJLQcHBykAAAABAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQgICC8HBwcnAAAAAAAAAAAAAAAAAQEBCAkJCTIFBQUiAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQoJCQk0BQUFHgAAAAAAAAAAAAAAAAAAAAABAQEICQkJ
-MQYGBiMAAAAEAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAQEBBggICCoHBwctAAAAAgAAAAAAAAAAAgIC
-DQoKCjUFBQUcAAAAAwAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgEBAQoICAgvBgYGJgAAAAAAAAAAAAAA
-AAAAAAAAAAAEBwcHLAkJCTUICAgxCAgIKQYGBiQGBgYjBgYGIwYGBiQICAgqCQkJMgkJCTUHBwcmAAAA
-AAAAAAAAAAAAAQEBCQgICDAJCQk1CAgILwcHBygGBgYjBgYGIwYGBiMGBgYlBwcHLAkJCTMJCQk2BgYG
-HgAAAAAAAAAAAAAAAAAAAAAAAAAAAQEBCQMDAxYFBQUiCAgIKggICDAICAgxCAgIMAgICC8HBwcpBQUF
-IAMDAxQBAQEGAAAAAAAAAAAAAAAAAAAAAAICAgwEBAQYBgYGIwgICCsICAgwCAgIMAgICDAICAguBwcH
-KAUFBR4DAwMSAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAADAAAA
-AgAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAA
-AwAAAAIAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAEBAQkDAwMPAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIFBQUfBwcHLQkJCTIJCQk1AwMDDwAAAAAAAAAAAAAA
-AAAAAAAEBAQXBQUFHwAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMICAguCQkJNAkJCTQHBwctAQEB
-CAAAAAIAAAAFAgICDQYGBiQKCgo2BwcHJwAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGBgYlCQkJ
-NQkJCTQJCQk0CQkJLQgICCoICAguCQkJNQkJCTMEBAQfAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAFBQUcCgoKNgUFBR8FBQUaBwcHJgcHBykHBwcmBQUFGwICAgoAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAABAQELBgYGIQAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAQAAAwAAAAEA
-IQAAAQEAAwAAAAEAIQAAAQIAAwAAAAQAABHSAQMAAwAAAAEAAQAAAQYAAwAAAAEAAgAAAQoAAwAAAAEA
-AQAAAREABAAAAAEAAAAIARIAAwAAAAEAAQAAARUAAwAAAAEABAAAARYAAwAAAAEAIQAAARcABAAAAAEA
-ABEEARwAAwAAAAEAAQAAASgAAwAAAAEAAgAAAVIAAwAAAAEAAQAAAVMAAwAAAAQAABHah3MABwAAB9gA
-ABHiAAAAAAAIAAgACAAIAAEAAQABAAEAAAfYYXBwbAIgAABtbnRyUkdCIFhZWiAH2QACABkACwAaAAth
-Y3NwQVBQTAAAAABhcHBsAAAAAAAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLWFwcGwAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtkZXNjAAABCAAAAG9kc2NtAAABeAAABZxj
-cHJ0AAAHFAAAADh3dHB0AAAHTAAAABRyWFlaAAAHYAAAABRnWFlaAAAHdAAAABRiWFlaAAAHiAAAABRy
-VFJDAAAHnAAAAA5jaGFkAAAHrAAAACxiVFJDAAAHnAAAAA5nVFJDAAAHnAAAAA5kZXNjAAAAAAAAABRH
-ZW5lcmljIFJHQiBQcm9maWxlAAAAAAAAAAAAAAAUR2VuZXJpYyBSR0IgUHJvZmlsZQAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbWx1YwAAAAAAAAAfAAAADHNrU0sA
-AAAoAAABhGRhREsAAAAuAAABrGNhRVMAAAAkAAAB2nZpVk4AAAAkAAAB/nB0QlIAAAAmAAACInVrVUEA
-AAAqAAACSGZyRlUAAAAoAAACcmh1SFUAAAAoAAACmnpoVFcAAAAWAAACwm5iTk8AAAAmAAAC2GNzQ1oA
-AAAiAAAC/mhlSUwAAAAeAAADIGl0SVQAAAAoAAADPnJvUk8AAAAkAAADZmRlREUAAAAsAAADimtvS1IA
-AAAWAAADtnN2U0UAAAAmAAAC2HpoQ04AAAAWAAADzGphSlAAAAAaAAAD4mVsR1IAAAAiAAAD/HB0UE8A
-AAAmAAAEHm5sTkwAAAAoAAAERGVzRVMAAAAmAAAEHnRoVEgAAAAkAAAEbHRyVFIAAAAiAAAEkGZpRkkA
-AAAoAAAEsmhySFIAAAAoAAAE2nBsUEwAAAAsAAAFAnJ1UlUAAAAiAAAFLmFyRUcAAAAmAAAFUGVuVVMA
-AAAmAAAFdgBWAWEAZQBvAGIAZQBjAG4A/QAgAFIARwBCACAAcAByAG8AZgBpAGwARwBlAG4AZQByAGUA
-bAAgAFIARwBCAC0AYgBlAHMAawByAGkAdgBlAGwAcwBlAFAAZQByAGYAaQBsACAAUgBHAEIAIABnAGUA
-bgDoAHIAaQBjAEMepQB1ACAAaADsAG4AaAAgAFIARwBCACAAQwBoAHUAbgBnAFAAZQByAGYAaQBsACAA
-UgBHAEIAIABHAGUAbgDpAHIAaQBjAG8EFwQwBDMEMAQ7BEwEPQQ4BDkAIAQ/BEAEPgREBDAEOQQ7ACAA
-UgBHAEIAUAByAG8AZgBpAGwAIABnAOkAbgDpAHIAaQBxAHUAZQAgAFIAVgBCAMEAbAB0AGEAbADhAG4A
-bwBzACAAUgBHAEIAIABwAHIAbwBmAGkAbJAadSgAIABSAEcAQgAggnJfaWPPj/AARwBlAG4AZQByAGkA
-cwBrACAAUgBHAEIALQBwAHIAbwBmAGkAbABPAGIAZQBjAG4A/QAgAFIARwBCACAAcAByAG8AZgBpAGwF
-5AXoBdUF5AXZBdwAIABSAEcAQgAgBdsF3AXcBdkAUAByAG8AZgBpAGwAbwAgAFIARwBCACAAZwBlAG4A
-ZQByAGkAYwBvAFAAcgBvAGYAaQBsACAAUgBHAEIAIABnAGUAbgBlAHIAaQBjAEEAbABsAGcAZQBtAGUA
-aQBuAGUAcwAgAFIARwBCAC0AUAByAG8AZgBpAGzHfLwYACAAUgBHAEIAINUEuFzTDMd8Zm6QGgAgAFIA
-RwBCACBjz4/wZYdO9k4AgiwAIABSAEcAQgAgMNcw7TDVMKEwpDDrA5MDtQO9A7kDugPMACADwAPBA78D
-xgOvA7sAIABSAEcAQgBQAGUAcgBmAGkAbAAgAFIARwBCACAAZwBlAG4A6QByAGkAYwBvAEEAbABnAGUA
-bQBlAGUAbgAgAFIARwBCAC0AcAByAG8AZgBpAGUAbA5CDhsOIw5EDh8OJQ5MACAAUgBHAEIAIA4XDjEO
-SA4nDkQOGwBHAGUAbgBlAGwAIABSAEcAQgAgAFAAcgBvAGYAaQBsAGkAWQBsAGUAaQBuAGUAbgAgAFIA
-RwBCAC0AcAByAG8AZgBpAGkAbABpAEcAZQBuAGUAcgBpAQ0AawBpACAAUgBHAEIAIABwAHIAbwBmAGkA
-bABVAG4AaQB3AGUAcgBzAGEAbABuAHkAIABwAHIAbwBmAGkAbAAgAFIARwBCBB4EMQRJBDgEOQAgBD8E
-QAQ+BEQEOAQ7BEwAIABSAEcAQgZFBkQGQQAgBioGOQYxBkoGQQAgAFIARwBCACAGJwZEBjkGJwZFAEcA
-ZQBuAGUAcgBpAGMAIABSAEcAQgAgAFAAcgBvAGYAaQBsAGV0ZXh0AAAAAENvcHlyaWdodCAyMDA3IEFw
-cGxlIEluYy4sIGFsbCByaWdodHMgcmVzZXJ2ZWQuAFhZWiAAAAAAAADzUgABAAAAARbPWFlaIAAAAAAA
-AHRNAAA97gAAA9BYWVogAAAAAAAAWnUAAKxzAAAXNFhZWiAAAAAAAAAoGgAAFZ8AALg2Y3VydgAAAAAA
-AAABAc0AAHNmMzIAAAAAAAEMQgAABd7///MmAAAHkgAA/ZH///ui///9owAAA9wAAMBs0issLS5aJGNs
-YXNzbmFtZVgkY2xhc3Nlc18QEE5TQml0bWFwSW1hZ2VSZXCjLS8wWk5TSW1hZ2VSZXBYTlNPYmplY3TS
-KywyM1dOU0FycmF5ojIw0issNTZeTlNNdXRhYmxlQXJyYXmjNTIw1Tg5OjsPPD0+P0BXTlNXaGl0ZVxO
-U0NvbXBvbmVudHNcTlNDb2xvclNwYWNlXxASTlNDdXN0b21Db2xvclNwYWNlRDAgMABDMCAwEAOADIAP
-1EJDRA9FRkdIVE5TSURVTlNJQ0NXTlNNb2RlbBAJgA0QAIAOTxERnAAAEZxhcHBsAgAAAG1udHJHUkFZ
-WFlaIAfcAAgAFwAPAC4AD2Fjc3BBUFBMAAAAAG5vbmUAAAAAAAAAAAAAAAAAAAAAAAD21gABAAAAANMt
-YXBwbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABWRlc2MAAADA
-AAAAeWRzY20AAAE8AAAIGmNwcnQAAAlYAAAAI3d0cHQAAAl8AAAAFGtUUkMAAAmQAAAIDGRlc2MAAAAA
-AAAAH0dlbmVyaWMgR3JheSBHYW1tYSAyLjIgUHJvZmlsZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABtbHVj
-AAAAAAAAAB8AAAAMc2tTSwAAAC4AAAGEZGFESwAAADoAAAGyY2FFUwAAADgAAAHsdmlWTgAAAEAAAAIk
-cHRCUgAAAEoAAAJkdWtVQQAAACwAAAKuZnJGVQAAAD4AAALaaHVIVQAAADQAAAMYemhUVwAAABoAAANM
-a29LUgAAACIAAANmbmJOTwAAADoAAAOIY3NDWgAAACgAAAPCaGVJTAAAACQAAAPqcm9STwAAACoAAAQO
-ZGVERQAAAE4AAAQ4aXRJVAAAAE4AAASGc3ZTRQAAADgAAATUemhDTgAAABoAAAUMamFKUAAAACYAAAUm
-ZWxHUgAAACoAAAVMcHRQTwAAAFIAAAV2bmxOTAAAAEAAAAXIZXNFUwAAAEwAAAYIdGhUSAAAADIAAAZU
-dHJUUgAAACQAAAaGZmlGSQAAAEYAAAaqaHJIUgAAAD4AAAbwcGxQTAAAAEoAAAcuYXJFRwAAACwAAAd4
-cnVSVQAAADoAAAekZW5VUwAAADwAAAfeAFYBYQBlAG8AYgBlAGMAbgDhACAAcwBpAHYA4QAgAGcAYQBt
-AGEAIAAyACwAMgBHAGUAbgBlAHIAaQBzAGsAIABnAHIA5QAgADIALAAyACAAZwBhAG0AbQBhAC0AcABy
-AG8AZgBpAGwARwBhAG0AbQBhACAAZABlACAAZwByAGkAcwBvAHMAIABnAGUAbgDoAHIAaQBjAGEAIAAy
-AC4AMgBDHqUAdQAgAGgA7ABuAGgAIABNAOAAdQAgAHgA4QBtACAAQwBoAHUAbgBnACAARwBhAG0AbQBh
-ACAAMgAuADIAUABlAHIAZgBpAGwAIABHAGUAbgDpAHIAaQBjAG8AIABkAGEAIABHAGEAbQBhACAAZABl
-ACAAQwBpAG4AegBhAHMAIAAyACwAMgQXBDAEMwQwBDsETAQ9BDAAIABHAHIAYQB5AC0EMwQwBDwEMAAg
-ADIALgAyAFAAcgBvAGYAaQBsACAAZwDpAG4A6QByAGkAcQB1AGUAIABnAHIAaQBzACAAZwBhAG0AbQBh
-ACAAMgAsADIAwQBsAHQAYQBsAOEAbgBvAHMAIABzAHoA/AByAGsAZQAgAGcAYQBtAG0AYQAgADIALgAy
-kBp1KHBwlo5RSV6mADIALgAygnJfaWPPj/DHfLwYACDWjMDJACCsELnIACAAMgAuADIAINUEuFzTDMd8
-AEcAZQBuAGUAcgBpAHMAawAgAGcAcgDlACAAZwBhAG0AbQBhACAAMgAsADIALQBwAHIAbwBmAGkAbABP
-AGIAZQBjAG4A4QAgAWEAZQBkAOEAIABnAGEAbQBhACAAMgAuADIF0gXQBd4F1AAgBdAF5AXVBegAIAXb
-BdwF3AXZACAAMgAuADIARwBhAG0AYQAgAGcAcgBpACAAZwBlAG4AZQByAGkAYwEDACAAMgAsADIAQQBs
-AGwAZwBlAG0AZQBpAG4AZQBzACAARwByAGEAdQBzAHQAdQBmAGUAbgAtAFAAcgBvAGYAaQBsACAARwBh
-AG0AbQBhACAAMgAsADIAUAByAG8AZgBpAGwAbwAgAGcAcgBpAGcAaQBvACAAZwBlAG4AZQByAGkAYwBv
-ACAAZABlAGwAbABhACAAZwBhAG0AbQBhACAAMgAsADIARwBlAG4AZQByAGkAcwBrACAAZwByAOUAIAAy
-ACwAMgAgAGcAYQBtAG0AYQBwAHIAbwBmAGkAbGZukBpwcF6mfPtlcAAyAC4AMmPPj/Blh072TgCCLDCw
-MOwwpDCsMPMw3gAgADIALgAyACAw1zDtMNUwoTCkMOsDkwO1A70DuQO6A8wAIAOTA7oDwQO5ACADkwOs
-A7wDvAOxACAAMgAuADIAUABlAHIAZgBpAGwAIABnAGUAbgDpAHIAaQBjAG8AIABkAGUAIABjAGkAbgB6
-AGUAbgB0AG8AcwAgAGQAYQAgAEcAYQBtAG0AYQAgADIALAAyAEEAbABnAGUAbQBlAGUAbgAgAGcAcgBp
-AGoAcwAgAGcAYQBtAG0AYQAgADIALAAyAC0AcAByAG8AZgBpAGUAbABQAGUAcgBmAGkAbAAgAGcAZQBu
-AOkAcgBpAGMAbwAgAGQAZQAgAGcAYQBtAG0AYQAgAGQAZQAgAGcAcgBpAHMAZQBzACAAMgAsADIOIw4x
-DgcOKg41DkEOAQ4hDiEOMg5ADgEOIw4iDkwOFw4xDkgOJw5EDhsAIAAyAC4AMgBHAGUAbgBlAGwAIABH
-AHIAaQAgAEcAYQBtAGEAIAAyACwAMgBZAGwAZQBpAG4AZQBuACAAaABhAHIAbQBhAGEAbgAgAGcAYQBt
-AG0AYQAgADIALAAyACAALQBwAHIAbwBmAGkAaQBsAGkARwBlAG4AZQByAGkBDQBrAGkAIABHAHIAYQB5
-ACAARwBhAG0AbQBhACAAMgAuADIAIABwAHIAbwBmAGkAbABVAG4AaQB3AGUAcgBzAGEAbABuAHkAIABw
-AHIAbwBmAGkAbAAgAHMAegBhAHIAbwFbAGMAaQAgAGcAYQBtAG0AYQAgADIALAAyBjoGJwZFBicAIAAy
-AC4AMgAgBkQGSAZGACAGMQZFBicGLwZKACAGOQYnBkUEHgQxBEkEMARPACAEQQQ1BEAEMARPACAEMwQw
-BDwEPAQwACAAMgAsADIALQQ/BEAEPgREBDgEOwRMAEcAZQBuAGUAcgBpAGMAIABHAHIAYQB5ACAARwBh
-AG0AbQBhACAAMgAuADIAIABQAHIAbwBmAGkAbABlAAB0ZXh0AAAAAENvcHlyaWdodCBBcHBsZSBJbmMu
-LCAyMDEyAABYWVogAAAAAAAA81EAAQAAAAEWzGN1cnYAAAAAAAAEAAAAAAUACgAPABQAGQAeACMAKAAt
-ADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCGAIsAkACVAJoAnwCkAKkArgCyALcAvADB
-AMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwElASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1
-AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gIDAgwCFAIdAiYCLwI4AkECSwJUAl0CZwJx
-AnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMtAzgDQwNPA1oDZgNyA34DigOWA6IDrgO6
-A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSoBLYExATTBOEE8AT+BQ0FHAUrBToFSQVY
-BWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7BowGnQavBsAG0QbjBvUHBwcZBysHPQdP
-B2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiqCL4I0gjnCPsJEAklCToJTwlkCXkJjwmk
-CboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsLIgs5C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxc
-DHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4uDkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96
-D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGMEaoRyRHoEgcSJhJFEmQShBKjEsMS4xMD
-EyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVWFXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6
-Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZaxmRGbcZ3RoEGioaURp3Gp4axRrsGxQbOxtj
-G4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5AHmoelB6+HukfEx8+H2kflB+/H+ogFSBB
-IGwgmCDEIPAhHCFIIXUhoSHOIfsiJyJVIoIiryLdIwojOCNmI5QjwiPwJB8kTSR8JKsk2iUJJTglaCWX
-Jccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkGKTgpaymdKdAqAio1KmgqmyrPKwIrNitp
-K50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8kL1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6
-MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXCNf02NzZyNq426TckN2A3nDfXOBQ4UDiM
-OMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzjPSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/i
-QCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SKRM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fA
-SAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAn
-UHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1ka
-WWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69Xw9fYV+zYAVgV2CqYPxhT2GiYfViSWKc
-YvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iWaOxpQ2maafFqSGqfavdrT2una/9sV2yv
-bQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMBc11zuHQUdHB0zHUodYV14XY+dpt2+HdW
-d7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4BfmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKS
-gvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZif6KZIrKizCLlov8jGOMyo0xjZiN/45m
-js6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrV
-m0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKWowajdqPmpFakx6U4pammGqaLpv2nbqfg
-qFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxavi7AAsHWw6rFgsdayS7LCszizrrQltJy1E7WK
-tgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8IbybvRW9j74KvoS+/796v/XAcMDswWfB48JfwtvDWMPU
-xFHEzsVLxcjGRsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1zTXNtc42zrbPN8+40DnQutE80b7SP9LB
-00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF3IrdEN2W3hzeot8p36/gNuC94UThzOJT
-4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv77IbtEe2c7ijutO9A78zwWPDl8XLx//KM
-8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY/Sn9uv5L/tz/bf//0issS0xcTlNDb2xv
-clNwYWNlok0wXE5TQ29sb3JTcGFjZdIrLE9QV05TQ29sb3KiTzDSKyxSU1dOU0ltYWdlolIwAAgAEQAa
-ACQAKQAyADcASQBMAFEAUwBnAG0AegCBAJAAlwCkAKsAswC1ALcAuQC+AMAAwgDLANAA2wDdAN8A4QDm
-AOkA6wDtAO8A9gENASkBKwEtGusa8Br7GwQbFxsbGyYbLxs0GzwbPxtEG1MbVxtiG2obdxuEG5kbnhui
-G6QbphuoG7Ebthu8G8QbxhvIG8obzC1sLXEtfi2BLY4tky2bLZ4toy2rAAAAAAAAAgEAAAAAAAAAVAAA
-AAAAAAAAAAAAAAAALa4
-
-
-
-
-
-YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T
-S2V5ZWRBcmNoaXZlctEICVRyb290gAGvEBELDBkaHxQkKSoxNDdBSUpOUVUkbnVsbNYNDg8QERITFBUW
-FxhWTlNTaXplXk5TUmVzaXppbmdNb2RlViRjbGFzc1xOU0ltYWdlRmxhZ3NWTlNSZXBzV05TQ29sb3KA
-AhAAgBASIMAAAIADgAtYezQwLCAzNn3SGw8cHlpOUy5vYmplY3RzoR2ABIAK0hsPICOiISKABYAGgAnT
-DyUmJygUXxAUTlNUSUZGUmVwcmVzZW50YXRpb25fEBlOU0ludGVybmFsTGF5b3V0RGlyZWN0aW9ugAiA
-B08RHzZNTQAqAAAWiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAABhgAABkYAAAZBQAABgAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAOwABPJsABJ7IAAXMyAAFzJ0ABKA/AAFBAQAAAgAAAAAAAAAAAAAAAAAAAAACAgICAQEB
-AQAAAAAKCgoKBAQEBAAAAAAAAAAACQkJCQkJCQkAAAAAAAAAAAAAAAAAAAAAAAAAAAkJCQkHBwcHAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASQACS9gABtz7AAf/+wAH
-//sAB//7AAf/2wAG31AAAlIAAAAAAAAAAAAAAAAAAAAAW1tbW6KioqKZmZmZ29vb22BgYGIICAgIh4eH
-icnJycnJycnKenp6fAcHBwcAAAAAEhISEpOTk5XS0tLTy8vLzHh4eHoHBwcHAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAFgAAF7wABcD7AAf/+wAH//sAB//7AAf/+wAH//sAB//AAAXEGgAA
-GwAAAAAAAAAAAAAAAHBwcHD/////gYGBgygoKCoNDQ0NiIiIiq6urq8XFxcXFxcXGbm5ublycnJyAAAA
-AJiYmJnBwcHBKCgoKC0tLS/T09PUV1dXVwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AEQAAUbrAAbv+wAH//sAB//7AAf/+wAH//sAB//7AAf/7AAG8EoAAkwAAAAAAAAAAAAAAAB0dHR00NDQ
-0AAAAAAAAAAABwcHB9TU1NWRkZGTY2NjY2NjY2Ojo6OjyMjIyBYWFhbd3d3dOzs7PQAAAAAAAAAAFxcX
-GRUVFRUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSAAJU8gAG9vsAB//7AAf/+wAH
-//sAB//7AAf/+wAH//QABvhcAAJeAAAAAAAAAAAAAAAAdnZ2dre3t7cAAAAAAAAAAA4ODg7g4ODhoaGh
-oYyMjIyMjIyMhoaGhlJSUlQcHBwc4eHh4ioqKioAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAOAABOeQABuj7AAf/+wAH//sAB//7AAf/+wAH//sAB//mAAbqPgAB
-PwAAAAAAAAAAAAAAAHZ2dna4uLi4AAAAAAAAAAAEBAQEwsLCw2JiYmIAAAAAAAAAADMzMzNeXl5eCQkJ
-Cc3Nzc5paWlpAAAAAAAAAABxcXFzWVlZWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAsAAAyiAASl+wAH//sAB//7AAf/+wAH//sAB//7AAf/pgAEqQ8AABAAAAAAAAAAAAAAAAB4eHh4vLy8
-vAAAAAAAAAAAAAAAAEhISEjX19fYmZmZmouLi43a2trbYmJiZAAAAABRUVFT39/f35ubm5yenp6f3Nzc
-3Tk5OTkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKgABK7QABbf1AAb5+wAH
-//sAB//2AAb6tgAFuSwAAS0AAAAAAAAAAAAAAAAAAAAAKioqKkFBQUMAAAAAAAAAAAAAAAAAAAAAJiYm
-KG5ubnBxcXFzMjIyNAAAAAAAAAAAAAAAADAwMDB0dHR0bGxsbiMjIyMAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZAAAaYwACZZYABJmVAASYZQACZxwAAB0AAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAFAAAGBAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAQAQAAAwAAAAEAKAAAAQEAAwAAAAEAJAAAAQIAAwAAAAQAABdOAQMAAwAAAAEA
-AQAAAQYAAwAAAAEAAgAAAQoAAwAAAAEAAQAAAREABAAAAAEAAAAIARIAAwAAAAEAAQAAARUAAwAAAAEA
-BAAAARYAAwAAAAEAJAAAARcABAAAAAEAABaAARwAAwAAAAEAAQAAASgAAwAAAAEAAgAAAVIAAwAAAAEA
-AQAAAVMAAwAAAAQAABdWh3MABwAAB9gAABdeAAAAAAAIAAgACAAIAAEAAQABAAEAAAfYYXBwbAIgAABt
-bnRyUkdCIFhZWiAH2QACABkACwAaAAthY3NwQVBQTAAAAABhcHBsAAAAAAAAAAAAAAAAAAAAAAAA9tYA
-AQAAAADTLWFwcGwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtk
-ZXNjAAABCAAAAG9kc2NtAAABeAAABZxjcHJ0AAAHFAAAADh3dHB0AAAHTAAAABRyWFlaAAAHYAAAABRn
-WFlaAAAHdAAAABRiWFlaAAAHiAAAABRyVFJDAAAHnAAAAA5jaGFkAAAHrAAAACxiVFJDAAAHnAAAAA5n
-VFJDAAAHnAAAAA5kZXNjAAAAAAAAABRHZW5lcmljIFJHQiBQcm9maWxlAAAAAAAAAAAAAAAUR2VuZXJp
-YyBSR0IgUHJvZmlsZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAbWx1YwAAAAAAAAAfAAAADHNrU0sAAAAoAAABhGRhREsAAAAuAAABrGNhRVMAAAAkAAAB2nZpVk4A
-AAAkAAAB/nB0QlIAAAAmAAACInVrVUEAAAAqAAACSGZyRlUAAAAoAAACcmh1SFUAAAAoAAACmnpoVFcA
-AAAWAAACwm5iTk8AAAAmAAAC2GNzQ1oAAAAiAAAC/mhlSUwAAAAeAAADIGl0SVQAAAAoAAADPnJvUk8A
-AAAkAAADZmRlREUAAAAsAAADimtvS1IAAAAWAAADtnN2U0UAAAAmAAAC2HpoQ04AAAAWAAADzGphSlAA
-AAAaAAAD4mVsR1IAAAAiAAAD/HB0UE8AAAAmAAAEHm5sTkwAAAAoAAAERGVzRVMAAAAmAAAEHnRoVEgA
-AAAkAAAEbHRyVFIAAAAiAAAEkGZpRkkAAAAoAAAEsmhySFIAAAAoAAAE2nBsUEwAAAAsAAAFAnJ1UlUA
-AAAiAAAFLmFyRUcAAAAmAAAFUGVuVVMAAAAmAAAFdgBWAWEAZQBvAGIAZQBjAG4A/QAgAFIARwBCACAA
-cAByAG8AZgBpAGwARwBlAG4AZQByAGUAbAAgAFIARwBCAC0AYgBlAHMAawByAGkAdgBlAGwAcwBlAFAA
-ZQByAGYAaQBsACAAUgBHAEIAIABnAGUAbgDoAHIAaQBjAEMepQB1ACAAaADsAG4AaAAgAFIARwBCACAA
-QwBoAHUAbgBnAFAAZQByAGYAaQBsACAAUgBHAEIAIABHAGUAbgDpAHIAaQBjAG8EFwQwBDMEMAQ7BEwE
-PQQ4BDkAIAQ/BEAEPgREBDAEOQQ7ACAAUgBHAEIAUAByAG8AZgBpAGwAIABnAOkAbgDpAHIAaQBxAHUA
-ZQAgAFIAVgBCAMEAbAB0AGEAbADhAG4AbwBzACAAUgBHAEIAIABwAHIAbwBmAGkAbJAadSgAIABSAEcA
-QgAggnJfaWPPj/AARwBlAG4AZQByAGkAcwBrACAAUgBHAEIALQBwAHIAbwBmAGkAbABPAGIAZQBjAG4A
-/QAgAFIARwBCACAAcAByAG8AZgBpAGwF5AXoBdUF5AXZBdwAIABSAEcAQgAgBdsF3AXcBdkAUAByAG8A
-ZgBpAGwAbwAgAFIARwBCACAAZwBlAG4AZQByAGkAYwBvAFAAcgBvAGYAaQBsACAAUgBHAEIAIABnAGUA
-bgBlAHIAaQBjAEEAbABsAGcAZQBtAGUAaQBuAGUAcwAgAFIARwBCAC0AUAByAG8AZgBpAGzHfLwYACAA
-UgBHAEIAINUEuFzTDMd8Zm6QGgAgAFIARwBCACBjz4/wZYdO9k4AgiwAIABSAEcAQgAgMNcw7TDVMKEw
-pDDrA5MDtQO9A7kDugPMACADwAPBA78DxgOvA7sAIABSAEcAQgBQAGUAcgBmAGkAbAAgAFIARwBCACAA
-ZwBlAG4A6QByAGkAYwBvAEEAbABnAGUAbQBlAGUAbgAgAFIARwBCAC0AcAByAG8AZgBpAGUAbA5CDhsO
-Iw5EDh8OJQ5MACAAUgBHAEIAIA4XDjEOSA4nDkQOGwBHAGUAbgBlAGwAIABSAEcAQgAgAFAAcgBvAGYA
-aQBsAGkAWQBsAGUAaQBuAGUAbgAgAFIARwBCAC0AcAByAG8AZgBpAGkAbABpAEcAZQBuAGUAcgBpAQ0A
-awBpACAAUgBHAEIAIABwAHIAbwBmAGkAbABVAG4AaQB3AGUAcgBzAGEAbABuAHkAIABwAHIAbwBmAGkA
-bAAgAFIARwBCBB4EMQRJBDgEOQAgBD8EQAQ+BEQEOAQ7BEwAIABSAEcAQgZFBkQGQQAgBioGOQYxBkoG
-QQAgAFIARwBCACAGJwZEBjkGJwZFAEcAZQBuAGUAcgBpAGMAIABSAEcAQgAgAFAAcgBvAGYAaQBsAGV0
-ZXh0AAAAAENvcHlyaWdodCAyMDA3IEFwcGxlIEluYy4sIGFsbCByaWdodHMgcmVzZXJ2ZWQuAFhZWiAA
-AAAAAADzUgABAAAAARbPWFlaIAAAAAAAAHRNAAA97gAAA9BYWVogAAAAAAAAWnUAAKxzAAAXNFhZWiAA
-AAAAAAAoGgAAFZ8AALg2Y3VydgAAAAAAAAABAc0AAHNmMzIAAAAAAAEMQgAABd7///MmAAAHkgAA/ZH/
-//ui///9owAAA9wAAMBs0issLS5aJGNsYXNzbmFtZVgkY2xhc3Nlc18QEE5TQml0bWFwSW1hZ2VSZXCj
-LS8wWk5TSW1hZ2VSZXBYTlNPYmplY3TSKywyM1dOU0FycmF5ojIw0issNTZeTlNNdXRhYmxlQXJyYXmj
-NTIw1Tg5OjsPPD0+P0BXTlNXaGl0ZVxOU0NvbXBvbmVudHNcTlNDb2xvclNwYWNlXxASTlNDdXN0b21D
-b2xvclNwYWNlRDAgMABDMCAwEAOADIAP1EJDRA9FRkdIVE5TSURVTlNJQ0NXTlNNb2RlbBAJgA0QAIAO
-TxERnAAAEZxhcHBsAgAAAG1udHJHUkFZWFlaIAfcAAgAFwAPAC4AD2Fjc3BBUFBMAAAAAG5vbmUAAAAA
-AAAAAAAAAAAAAAAAAAD21gABAAAAANMtYXBwbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAABWRlc2MAAADAAAAAeWRzY20AAAE8AAAIGmNwcnQAAAlYAAAAI3d0cHQAAAl8
-AAAAFGtUUkMAAAmQAAAIDGRlc2MAAAAAAAAAH0dlbmVyaWMgR3JheSBHYW1tYSAyLjIgUHJvZmlsZQAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAABtbHVjAAAAAAAAAB8AAAAMc2tTSwAAAC4AAAGEZGFESwAAADoAAAGy
-Y2FFUwAAADgAAAHsdmlWTgAAAEAAAAIkcHRCUgAAAEoAAAJkdWtVQQAAACwAAAKuZnJGVQAAAD4AAALa
-aHVIVQAAADQAAAMYemhUVwAAABoAAANMa29LUgAAACIAAANmbmJOTwAAADoAAAOIY3NDWgAAACgAAAPC
-aGVJTAAAACQAAAPqcm9STwAAACoAAAQOZGVERQAAAE4AAAQ4aXRJVAAAAE4AAASGc3ZTRQAAADgAAATU
-emhDTgAAABoAAAUMamFKUAAAACYAAAUmZWxHUgAAACoAAAVMcHRQTwAAAFIAAAV2bmxOTAAAAEAAAAXI
-ZXNFUwAAAEwAAAYIdGhUSAAAADIAAAZUdHJUUgAAACQAAAaGZmlGSQAAAEYAAAaqaHJIUgAAAD4AAAbw
-cGxQTAAAAEoAAAcuYXJFRwAAACwAAAd4cnVSVQAAADoAAAekZW5VUwAAADwAAAfeAFYBYQBlAG8AYgBl
-AGMAbgDhACAAcwBpAHYA4QAgAGcAYQBtAGEAIAAyACwAMgBHAGUAbgBlAHIAaQBzAGsAIABnAHIA5QAg
-ADIALAAyACAAZwBhAG0AbQBhAC0AcAByAG8AZgBpAGwARwBhAG0AbQBhACAAZABlACAAZwByAGkAcwBv
-AHMAIABnAGUAbgDoAHIAaQBjAGEAIAAyAC4AMgBDHqUAdQAgAGgA7ABuAGgAIABNAOAAdQAgAHgA4QBt
-ACAAQwBoAHUAbgBnACAARwBhAG0AbQBhACAAMgAuADIAUABlAHIAZgBpAGwAIABHAGUAbgDpAHIAaQBj
-AG8AIABkAGEAIABHAGEAbQBhACAAZABlACAAQwBpAG4AegBhAHMAIAAyACwAMgQXBDAEMwQwBDsETAQ9
-BDAAIABHAHIAYQB5AC0EMwQwBDwEMAAgADIALgAyAFAAcgBvAGYAaQBsACAAZwDpAG4A6QByAGkAcQB1
-AGUAIABnAHIAaQBzACAAZwBhAG0AbQBhACAAMgAsADIAwQBsAHQAYQBsAOEAbgBvAHMAIABzAHoA/ABy
-AGsAZQAgAGcAYQBtAG0AYQAgADIALgAykBp1KHBwlo5RSV6mADIALgAygnJfaWPPj/DHfLwYACDWjMDJ
-ACCsELnIACAAMgAuADIAINUEuFzTDMd8AEcAZQBuAGUAcgBpAHMAawAgAGcAcgDlACAAZwBhAG0AbQBh
-ACAAMgAsADIALQBwAHIAbwBmAGkAbABPAGIAZQBjAG4A4QAgAWEAZQBkAOEAIABnAGEAbQBhACAAMgAu
-ADIF0gXQBd4F1AAgBdAF5AXVBegAIAXbBdwF3AXZACAAMgAuADIARwBhAG0AYQAgAGcAcgBpACAAZwBl
-AG4AZQByAGkAYwEDACAAMgAsADIAQQBsAGwAZwBlAG0AZQBpAG4AZQBzACAARwByAGEAdQBzAHQAdQBm
-AGUAbgAtAFAAcgBvAGYAaQBsACAARwBhAG0AbQBhACAAMgAsADIAUAByAG8AZgBpAGwAbwAgAGcAcgBp
-AGcAaQBvACAAZwBlAG4AZQByAGkAYwBvACAAZABlAGwAbABhACAAZwBhAG0AbQBhACAAMgAsADIARwBl
-AG4AZQByAGkAcwBrACAAZwByAOUAIAAyACwAMgAgAGcAYQBtAG0AYQBwAHIAbwBmAGkAbGZukBpwcF6m
-fPtlcAAyAC4AMmPPj/Blh072TgCCLDCwMOwwpDCsMPMw3gAgADIALgAyACAw1zDtMNUwoTCkMOsDkwO1
-A70DuQO6A8wAIAOTA7oDwQO5ACADkwOsA7wDvAOxACAAMgAuADIAUABlAHIAZgBpAGwAIABnAGUAbgDp
-AHIAaQBjAG8AIABkAGUAIABjAGkAbgB6AGUAbgB0AG8AcwAgAGQAYQAgAEcAYQBtAG0AYQAgADIALAAy
-AEEAbABnAGUAbQBlAGUAbgAgAGcAcgBpAGoAcwAgAGcAYQBtAG0AYQAgADIALAAyAC0AcAByAG8AZgBp
-AGUAbABQAGUAcgBmAGkAbAAgAGcAZQBuAOkAcgBpAGMAbwAgAGQAZQAgAGcAYQBtAG0AYQAgAGQAZQAg
-AGcAcgBpAHMAZQBzACAAMgAsADIOIw4xDgcOKg41DkEOAQ4hDiEOMg5ADgEOIw4iDkwOFw4xDkgOJw5E
-DhsAIAAyAC4AMgBHAGUAbgBlAGwAIABHAHIAaQAgAEcAYQBtAGEAIAAyACwAMgBZAGwAZQBpAG4AZQBu
-ACAAaABhAHIAbQBhAGEAbgAgAGcAYQBtAG0AYQAgADIALAAyACAALQBwAHIAbwBmAGkAaQBsAGkARwBl
-AG4AZQByAGkBDQBrAGkAIABHAHIAYQB5ACAARwBhAG0AbQBhACAAMgAuADIAIABwAHIAbwBmAGkAbABV
-AG4AaQB3AGUAcgBzAGEAbABuAHkAIABwAHIAbwBmAGkAbAAgAHMAegBhAHIAbwFbAGMAaQAgAGcAYQBt
-AG0AYQAgADIALAAyBjoGJwZFBicAIAAyAC4AMgAgBkQGSAZGACAGMQZFBicGLwZKACAGOQYnBkUEHgQx
-BEkEMARPACAEQQQ1BEAEMARPACAEMwQwBDwEPAQwACAAMgAsADIALQQ/BEAEPgREBDgEOwRMAEcAZQBu
-AGUAcgBpAGMAIABHAHIAYQB5ACAARwBhAG0AbQBhACAAMgAuADIAIABQAHIAbwBmAGkAbABlAAB0ZXh0
-AAAAAENvcHlyaWdodCBBcHBsZSBJbmMuLCAyMDEyAABYWVogAAAAAAAA81EAAQAAAAEWzGN1cnYAAAAA
-AAAEAAAAAAUACgAPABQAGQAeACMAKAAtADIANwA7AEAARQBKAE8AVABZAF4AYwBoAG0AcgB3AHwAgQCG
-AIsAkACVAJoAnwCkAKkArgCyALcAvADBAMYAywDQANUA2wDgAOUA6wDwAPYA+wEBAQcBDQETARkBHwEl
-ASsBMgE4AT4BRQFMAVIBWQFgAWcBbgF1AXwBgwGLAZIBmgGhAakBsQG5AcEByQHRAdkB4QHpAfIB+gID
-AgwCFAIdAiYCLwI4AkECSwJUAl0CZwJxAnoChAKOApgCogKsArYCwQLLAtUC4ALrAvUDAAMLAxYDIQMt
-AzgDQwNPA1oDZgNyA34DigOWA6IDrgO6A8cD0wPgA+wD+QQGBBMEIAQtBDsESARVBGMEcQR+BIwEmgSo
-BLYExATTBOEE8AT+BQ0FHAUrBToFSQVYBWcFdwWGBZYFpgW1BcUF1QXlBfYGBgYWBicGNwZIBlkGagZ7
-BowGnQavBsAG0QbjBvUHBwcZBysHPQdPB2EHdAeGB5kHrAe/B9IH5Qf4CAsIHwgyCEYIWghuCIIIlgiq
-CL4I0gjnCPsJEAklCToJTwlkCXkJjwmkCboJzwnlCfsKEQonCj0KVApqCoEKmAquCsUK3ArzCwsLIgs5
-C1ELaQuAC5gLsAvIC+EL+QwSDCoMQwxcDHUMjgynDMAM2QzzDQ0NJg1ADVoNdA2ODakNww3eDfgOEw4u
-DkkOZA5/DpsOtg7SDu4PCQ8lD0EPXg96D5YPsw/PD+wQCRAmEEMQYRB+EJsQuRDXEPURExExEU8RbRGM
-EaoRyRHoEgcSJhJFEmQShBKjEsMS4xMDEyMTQxNjE4MTpBPFE+UUBhQnFEkUahSLFK0UzhTwFRIVNBVW
-FXgVmxW9FeAWAxYmFkkWbBaPFrIW1hb6Fx0XQRdlF4kXrhfSF/cYGxhAGGUYihivGNUY+hkgGUUZaxmR
-GbcZ3RoEGioaURp3Gp4axRrsGxQbOxtjG4obshvaHAIcKhxSHHscoxzMHPUdHh1HHXAdmR3DHeweFh5A
-HmoelB6+HukfEx8+H2kflB+/H+ogFSBBIGwgmCDEIPAhHCFIIXUhoSHOIfsiJyJVIoIiryLdIwojOCNm
-I5QjwiPwJB8kTSR8JKsk2iUJJTglaCWXJccl9yYnJlcmhya3JugnGCdJJ3onqyfcKA0oPyhxKKIo1CkG
-KTgpaymdKdAqAio1KmgqmyrPKwIrNitpK50r0SwFLDksbiyiLNctDC1BLXYtqy3hLhYuTC6CLrcu7i8k
-L1ovkS/HL/4wNTBsMKQw2zESMUoxgjG6MfIyKjJjMpsy1DMNM0YzfzO4M/E0KzRlNJ402DUTNU01hzXC
-Nf02NzZyNq426TckN2A3nDfXOBQ4UDiMOMg5BTlCOX85vDn5OjY6dDqyOu87LTtrO6o76DwnPGU8pDzj
-PSI9YT2hPeA+ID5gPqA+4D8hP2E/oj/iQCNAZECmQOdBKUFqQaxB7kIwQnJCtUL3QzpDfUPARANER0SK
-RM5FEkVVRZpF3kYiRmdGq0bwRzVHe0fASAVIS0iRSNdJHUljSalJ8Eo3Sn1KxEsMS1NLmkviTCpMcky6
-TQJNSk2TTdxOJU5uTrdPAE9JT5NP3VAnUHFQu1EGUVBRm1HmUjFSfFLHUxNTX1OqU/ZUQlSPVNtVKFV1
-VcJWD1ZcVqlW91dEV5JX4FgvWH1Yy1kaWWlZuFoHWlZaplr1W0VblVvlXDVchlzWXSddeF3JXhpebF69
-Xw9fYV+zYAVgV2CqYPxhT2GiYfViSWKcYvBjQ2OXY+tkQGSUZOllPWWSZedmPWaSZuhnPWeTZ+loP2iW
-aOxpQ2maafFqSGqfavdrT2una/9sV2yvbQhtYG25bhJua27Ebx5veG/RcCtwhnDgcTpxlXHwcktypnMB
-c11zuHQUdHB0zHUodYV14XY+dpt2+HdWd7N4EXhueMx5KnmJeed6RnqlewR7Y3vCfCF8gXzhfUF9oX4B
-fmJ+wn8jf4R/5YBHgKiBCoFrgc2CMIKSgvSDV4O6hB2EgITjhUeFq4YOhnKG14c7h5+IBIhpiM6JM4mZ
-if6KZIrKizCLlov8jGOMyo0xjZiN/45mjs6PNo+ekAaQbpDWkT+RqJIRknqS45NNk7aUIJSKlPSVX5XJ
-ljSWn5cKl3WX4JhMmLiZJJmQmfyaaJrVm0Kbr5wcnImc951kndKeQJ6unx2fi5/6oGmg2KFHobaiJqKW
-owajdqPmpFakx6U4pammGqaLpv2nbqfgqFKoxKk3qamqHKqPqwKrdavprFys0K1ErbiuLa6hrxavi7AA
-sHWw6rFgsdayS7LCszizrrQltJy1E7WKtgG2ebbwt2i34LhZuNG5SrnCuju6tbsuu6e8IbybvRW9j74K
-voS+/796v/XAcMDswWfB48JfwtvDWMPUxFHEzsVLxcjGRsbDx0HHv8g9yLzJOsm5yjjKt8s2y7bMNcy1
-zTXNtc42zrbPN8+40DnQutE80b7SP9LB00TTxtRJ1MvVTtXR1lXW2Ndc1+DYZNjo2WzZ8dp22vvbgNwF
-3IrdEN2W3hzeot8p36/gNuC94UThzOJT4tvjY+Pr5HPk/OWE5g3mlucf56noMui86Ubp0Opb6uXrcOv7
-7IbtEe2c7ijutO9A78zwWPDl8XLx//KM8xnzp/Q09ML1UPXe9m32+/eK+Bn4qPk4+cf6V/rn+3f8B/yY
-/Sn9uv5L/tz/bf//0issS0xcTlNDb2xvclNwYWNlok0wXE5TQ29sb3JTcGFjZdIrLE9QV05TQ29sb3Ki
-TzDSKyxSU1dOU0ltYWdlolIwAAgAEQAaACQAKQAyADcASQBMAFEAUwBnAG0AegCBAJAAlwCkAKsAswC1
-ALcAuQC+AMAAwgDLANAA2wDdAN8A4QDmAOkA6wDtAO8A9gENASkBKwEtIGcgbCB3IIAgkyCXIKIgqyCw
-ILgguyDAIM8g0yDeIOYg8yEAIRUhGiEeISAhIiEkIS0hMiE4IUAhQiFEIUYhSDLoMu0y+jL9MwozDzMX
-MxozHzMnAAAAAAAAAgEAAAAAAAAAVAAAAAAAAAAAAAAAAAAAMyo
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Classes/Base.lproj/CallView~ipad.xib b/Classes/Base.lproj/CallView~ipad.xib
deleted file mode 100644
index 684d42dc6..000000000
--- a/Classes/Base.lproj/CallView~ipad.xib
+++ /dev/null
@@ -1,1397 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Classes/Base.lproj/ChatConversationView.xib b/Classes/Base.lproj/ChatConversationView.xib
index 89b124cf6..ec065c6bf 100644
--- a/Classes/Base.lproj/ChatConversationView.xib
+++ b/Classes/Base.lproj/ChatConversationView.xib
@@ -1,9 +1,9 @@
-
+
-
+
@@ -21,7 +21,6 @@
-
@@ -185,22 +184,6 @@
-
diff --git a/Classes/CallConferenceTableView.h b/Classes/CallConferenceTableView.h
deleted file mode 100644
index 41698c257..000000000
--- a/Classes/CallConferenceTableView.h
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 .
- */
-
-#import
-
-@interface CallConferenceTableView : UITableViewController
-
-- (void)update;
-
-@end
diff --git a/Classes/CallConferenceTableView.m b/Classes/CallConferenceTableView.m
deleted file mode 100644
index 9e89b79e2..000000000
--- a/Classes/CallConferenceTableView.m
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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 .
- */
-
-#import "linphoneapp-Swift.h"
-#import "CallConferenceTableView.h"
-#import "UICallConferenceCell.h"
-#import "LinphoneManager.h"
-#import "Utils.h"
-#import "linphoneapp-Swift.h"
-
-@implementation CallConferenceTableView
-
-#pragma mark - UI change
-
-- (void)update {
- [self.tableView reloadData];
-}
-
-#pragma mark - UITableViewDataSource Functions
-
-- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
- NSString *kCellId = NSStringFromClass(UICallConferenceCell.class);
- UICallConferenceCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellId];
- if (cell == nil) {
- cell = [[UICallConferenceCell alloc] initWithIdentifier:kCellId];
- }
- LinphoneConference *c = [CallManager.instance getConference];
- if (c != nil) {
- [cell setParticipant:bctbx_list_nth_data(linphone_conference_get_participant_list(c),(int)indexPath.row)];
- }
- cell.contentView.userInteractionEnabled = NO;
- return cell;
-}
-
-- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
- return [CallManager.instance getConference] ? linphone_conference_get_participant_count([CallManager.instance getConference]) : 0;
-}
-
-- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
- return 1;
-}
-
-- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
- return 1e-5;
-}
-- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
- return 1e-5;
-}
-
-@end
diff --git a/Classes/CallIncomingView.h b/Classes/CallIncomingView.h
deleted file mode 100644
index 8031a7b2e..000000000
--- a/Classes/CallIncomingView.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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 .
- */
-
-#import
-
-#import "UICompositeView.h"
-#import "TPMultiLayoutViewController.h"
-#import "UIRoundedImageView.h"
-#include "LinphoneManager.h"
-
-@protocol IncomingCallViewDelegate
-
-- (void)incomingCallAccepted:(LinphoneCall *)call evenWithVideo:(BOOL)video;
-- (void)incomingCallDeclined:(LinphoneCall *)call;
-- (void)incomingCallAborted:(LinphoneCall *)call;
-
-@end
-
-@interface CallIncomingView : TPMultiLayoutViewController {
-}
-
-@property(nonatomic) Boolean earlyMedia;
-
-@property(weak, nonatomic) IBOutlet UILabel *nameLabel;
-@property(nonatomic, strong) IBOutlet UILabel *addressLabel;
-@property(nonatomic, strong) IBOutlet UIRoundedImageView *avatarImage;
-@property(nonatomic, assign) LinphoneCall *call;
-@property(nonatomic, strong) id delegate;
-@property(weak, nonatomic) IBOutlet UIView *tabVideoBar;
-@property(weak, nonatomic) IBOutlet UIView *tabBar;
-@property (weak, nonatomic) IBOutlet UIView *earlyMediaView;
-
-
-- (IBAction)onAcceptClick:(id)event;
-- (IBAction)onDeclineClick:(id)event;
-- (IBAction)onAcceptAudioOnlyClick:(id)sender;
-
-@end
diff --git a/Classes/CallIncomingView.m b/Classes/CallIncomingView.m
deleted file mode 100644
index ccf22052b..000000000
--- a/Classes/CallIncomingView.m
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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 .
- */
-
-#import "CallIncomingView.h"
-#import "LinphoneManager.h"
-#import "FastAddressBook.h"
-#import "PhoneMainView.h"
-#import "Utils.h"
-
-@implementation CallIncomingView
-
-#pragma mark - ViewController Functions
-
-- (void)viewWillAppear:(BOOL)animated {
- [super viewWillAppear:animated];
-
- [NSNotificationCenter.defaultCenter addObserver:self
- selector:@selector(callUpdateEvent:)
- name:kLinphoneCallUpdate
- object:nil];
-}
-
-- (void)viewWillDisappear:(BOOL)animated {
- [super viewWillDisappear:animated];
- [NSNotificationCenter.defaultCenter removeObserver:self name:kLinphoneCallUpdate object:nil];
- _call = NULL;
-}
-
-#pragma mark - UICompositeViewDelegate Functions
-
-static UICompositeViewDescription *compositeDescription = nil;
-
-+ (UICompositeViewDescription *)compositeViewDescription {
- if (compositeDescription == nil) {
- compositeDescription = [[UICompositeViewDescription alloc] init:self.class
- statusBar:StatusBarView.class
- tabBar:nil
- sideMenu:CallSideMenuView.class
- fullscreen:false
- isLeftFragment:YES
- fragmentWith:nil];
- compositeDescription.darkBackground = true;
- }
- return compositeDescription;
-}
-
-- (UICompositeViewDescription *)compositeViewDescription {
- return self.class.compositeViewDescription;
-}
-
-- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
- [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
- if (_earlyMedia && [LinphoneManager.instance lpConfigBoolForKey:@"pref_accept_early_media"] && linphone_core_get_calls_nb(LC) < 2) {
- _earlyMediaView.hidden = NO;
- linphone_core_set_native_video_window_id(LC, (__bridge void *)(_earlyMediaView));
- }
- if (_call) {
- [self update];
- }
-}
-
-#pragma mark - Event Functions
-
-- (void)callUpdateEvent:(NSNotification *)notif {
- LinphoneCall *acall = [[notif.userInfo objectForKey:@"call"] pointerValue];
- LinphoneCallState astate = [[notif.userInfo objectForKey:@"state"] intValue];
- [self callUpdate:acall state:astate];
-}
-
-- (void)callUpdate:(LinphoneCall *)acall state:(LinphoneCallState)astate {
- if (_call == acall && (astate == LinphoneCallEnd || astate == LinphoneCallError)) {
- [_delegate incomingCallAborted:_call];
- } else if ([LinphoneManager.instance lpConfigBoolForKey:@"auto_answer"]) {
- LinphoneCallState state = linphone_call_get_state(_call);
- if (state == LinphoneCallIncomingReceived) {
- LOGI(@"Auto answering call");
- [self onAcceptClick:nil];
- }
- }
-}
-
-#pragma mark -
-
-- (void)update {
- const LinphoneAddress *addr = linphone_call_get_remote_address(_call);
- [ContactDisplay setDisplayNameLabel:_nameLabel forAddress:addr withAddressLabel:_addressLabel];
- char *uri = linphone_address_as_string_uri_only(addr);
- ms_free(uri);
- [_avatarImage setImage:[FastAddressBook imageForAddress:addr] bordered:YES withRoundedRadius:YES];
-
- _tabBar.hidden = linphone_call_params_video_enabled(linphone_call_get_remote_params(_call));
- _tabVideoBar.hidden = !_tabBar.hidden;
-}
-
-#pragma mark - Property Functions
-static void hideSpinner(LinphoneCall *call, void *user_data) {
- CallIncomingView *thiz = (__bridge CallIncomingView *)user_data;
- thiz.earlyMedia = TRUE;
- thiz.earlyMediaView.hidden = NO;
- linphone_core_set_native_video_window_id(LC, (__bridge void *)(thiz.earlyMediaView));
-}
-
-- (void)setCall:(LinphoneCall *)call {
- _call = call;
- _earlyMedia = FALSE;
- if ([LinphoneManager.instance lpConfigBoolForKey:@"pref_accept_early_media"] && linphone_core_get_calls_nb(LC) < 2) {
- linphone_call_accept_early_media(_call);
- // linphone_call_params_get_used_video_codec return 0 if no video stream enabled
- if (linphone_call_params_get_used_video_codec(linphone_call_get_current_params(_call))) {
- linphone_call_set_next_video_frame_decoded_callback(call, hideSpinner, (__bridge void *)(self));
- }
- } else {
- _earlyMediaView.hidden = YES;
- }
-
- [self update];
- [self callUpdate:_call state:linphone_call_get_state(call)];
-}
-
-#pragma mark - Action Functions
-
-- (IBAction)onAcceptClick:(id)event {
- [_delegate incomingCallAccepted:_call evenWithVideo:YES];
-}
-
-- (IBAction)onDeclineClick:(id)event {
- [_delegate incomingCallDeclined:_call];
-}
-
-- (IBAction)onAcceptAudioOnlyClick:(id)sender {
- [_delegate incomingCallAccepted:_call evenWithVideo:NO];
-}
-
-@end
diff --git a/Classes/CallOutgoingView.h b/Classes/CallOutgoingView.h
deleted file mode 100644
index 1d4430efe..000000000
--- a/Classes/CallOutgoingView.h
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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 .
- */
-
-#import
-
-#import "UICompositeView.h"
-#import "TPMultiLayoutViewController.h"
-#include "linphone/linphonecore.h"
-#import "UIRoundedImageView.h"
-#import "UIDigitButton.h"
-
-
-@interface CallOutgoingView : TPMultiLayoutViewController {
-}
-@property(weak, nonatomic) IBOutlet UIRoundedImageView *avatarImage;
-@property(weak, nonatomic) IBOutlet UILabel *nameLabel;
-@property(weak, nonatomic) IBOutlet UISpeakerButton *speakerButton;
-@property(weak, nonatomic) IBOutlet UILabel *addressLabel;
-@property(weak, nonatomic) IBOutlet UIToggleButton *routesButton;
-@property(weak, nonatomic) IBOutlet UIView *routesView;
-@property(weak, nonatomic) IBOutlet UIBluetoothButton *routesBluetoothButton;
-@property(weak, nonatomic) IBOutlet UIButton *routesEarpieceButton;
-@property(weak, nonatomic) IBOutlet UISpeakerButton *routesSpeakerButton;
-@property(weak, nonatomic) IBOutlet UIMutedMicroButton *microButton;
-@property (weak, nonatomic) IBOutlet UIButton *declineButton;
-
-@property (weak, nonatomic) IBOutlet UIToggleButton *numpadButton;
-@property (strong, nonatomic) IBOutlet UIView *numpadView;
-@property(nonatomic, strong) IBOutlet UIDigitButton *oneButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *twoButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *threeButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *fourButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *fiveButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *sixButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *sevenButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *eightButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *nineButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *starButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *zeroButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *hashButton;
-@property (weak, nonatomic) IBOutlet UIButton *declineButton_earlyMedia;
-
-- (IBAction)onDeclineClick:(id)sender;
-
-@end
diff --git a/Classes/CallOutgoingView.m b/Classes/CallOutgoingView.m
deleted file mode 100644
index 462c1cbfa..000000000
--- a/Classes/CallOutgoingView.m
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
- * 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 .
- */
-
-#import "linphoneapp-Swift.h"
-#import "CallOutgoingView.h"
-#import "PhoneMainView.h"
-
-@implementation CallOutgoingView
-
-#pragma mark - UICompositeViewDelegate Functions
-
-static UICompositeViewDescription *compositeDescription = nil;
-
-+ (UICompositeViewDescription *)compositeViewDescription {
- if (compositeDescription == nil) {
- compositeDescription = [[UICompositeViewDescription alloc] init:self.class
- statusBar:StatusBarView.class
- tabBar:nil
- sideMenu:CallSideMenuView.class
- fullscreen:false
- isLeftFragment:NO
- fragmentWith:nil];
-
- compositeDescription.darkBackground = true;
- }
- return compositeDescription;
-}
-
-- (UICompositeViewDescription *)compositeViewDescription {
- return self.class.compositeViewDescription;
-}
-
-- (void)viewDidLoad {
- _routesEarpieceButton.enabled = !IPAD;
-
- [_zeroButton setDigit:'0'];
- [_zeroButton setDtmf:true];
- [_oneButton setDigit:'1'];
- [_oneButton setDtmf:true];
- [_twoButton setDigit:'2'];
- [_twoButton setDtmf:true];
- [_threeButton setDigit:'3'];
- [_threeButton setDtmf:true];
- [_fourButton setDigit:'4'];
- [_fourButton setDtmf:true];
- [_fiveButton setDigit:'5'];
- [_fiveButton setDtmf:true];
- [_sixButton setDigit:'6'];
- [_sixButton setDtmf:true];
- [_sevenButton setDigit:'7'];
- [_sevenButton setDtmf:true];
- [_eightButton setDigit:'8'];
- [_eightButton setDtmf:true];
- [_nineButton setDigit:'9'];
- [_nineButton setDtmf:true];
- [_starButton setDigit:'*'];
- [_starButton setDtmf:true];
- [_hashButton setDigit:'#'];
- [_hashButton setDtmf:true];
-
-}
-
-- (void)viewWillAppear:(BOOL)animated {
- [super viewWillAppear:animated];
-
- [NSNotificationCenter.defaultCenter addObserver:self
- selector:@selector(bluetoothAvailabilityUpdateEvent:)
- name:kLinphoneBluetoothAvailabilityUpdate
- object:nil];
-
- [NSNotificationCenter.defaultCenter addObserver:self
- selector:@selector(callUpdateEvent:)
- name:kLinphoneCallUpdate
- object:nil];
-
- LinphoneCall *call = linphone_core_get_current_call(LC);
- if (!call) {
- return;
- }
-
- const LinphoneAddress *addr = linphone_call_get_remote_address(call);
- [ContactDisplay setDisplayNameLabel:_nameLabel forAddress:addr withAddressLabel:_addressLabel];
- char *uri = linphone_address_as_string_uri_only(addr);
- ms_free(uri);
- [_avatarImage setImage:[FastAddressBook imageForAddress:addr] bordered:NO withRoundedRadius:YES];
-
- [self hideSpeaker: [CallManager.instance isBluetoothAvailable]];
-
- [_speakerButton update];
- [_microButton update];
- [_routesButton update];
- [self hidePad:TRUE animated:FALSE];
- [self callUpdate:call state:linphone_call_get_state(call) animated:true];
-
-}
-
-- (void)viewDidAppear:(BOOL)animated {
- [super viewDidAppear:animated];
- // if there is no call (for whatever reason), we must wait viewDidAppear method
- // before popping current view, because UICompositeView cannot handle view change
- // directly in viewWillAppear (this would lead to crash in deallocated memory - easily
- // reproductible on iPad mini).
- if (!linphone_core_get_current_call(LC)) {
- [PhoneMainView.instance popCurrentView];
- }
-}
-
-- (void)viewWillDisappear:(BOOL)animated {
- [super viewWillDisappear:animated];
- [NSNotificationCenter.defaultCenter removeObserver:self];
-}
-
-- (IBAction)onRoutesBluetoothClick:(id)sender {
- [self hideRoutes:TRUE animated:TRUE];
- [CallManager.instance changeRouteToBluetooth];
-}
-
-- (IBAction)onRoutesEarpieceClick:(id)sender {
- [self hideRoutes:TRUE animated:TRUE];
- [CallManager.instance changeRouteToDefault];
-}
-
-- (IBAction)onRoutesSpeakerClick:(id)sender {
- [self hideRoutes:TRUE animated:TRUE];
- [CallManager.instance changeRouteToSpeaker];
-}
-
-- (IBAction)onRoutesClick:(id)sender {
- if ([_routesView isHidden]) {
- [self hideRoutes:FALSE animated:ANIMATED];
- } else {
- [self hideRoutes:TRUE animated:ANIMATED];
- }
-}
-
-- (IBAction)onNumpadClick:(id)sender {
- if ([_numpadView isHidden]) {
- [self hidePad:FALSE animated:ANIMATED];
- } else {
- [self hidePad:TRUE animated:ANIMATED];
- }
-}
-
-- (IBAction)onDeclineClick:(id)sender {
- LinphoneCall *call = linphone_core_get_current_call(LC);
- if (call) {
- [CallManager.instance terminateCallWithCall:call];
- }
-}
-
-- (void)hidePad:(BOOL)hidden animated:(BOOL)animated {
- if (hidden) {
- [_numpadButton setOff];
- } else {
- [_numpadButton setOn];
- }
- if (hidden != _numpadView.hidden) {
- if (animated) {
- [self hideAnimation:hidden forView:_numpadView completion:nil];
- } else {
- [_numpadView setHidden:hidden];
- }
- }
-}
-
-- (void)hideRoutes:(BOOL)hidden animated:(BOOL)animated {
- if (hidden) {
- [_routesButton setOff];
- } else {
- [_routesButton setOn];
- }
-
- _routesBluetoothButton.selected = [CallManager.instance isBluetoothEnabled];
- _routesSpeakerButton.selected = [CallManager.instance isSpeakerEnabled];
- _routesEarpieceButton.selected = !_routesBluetoothButton.selected && !_routesSpeakerButton.selected;
-
- if (hidden != _routesView.hidden) {
- [_routesView setHidden:hidden];
- }
-}
-
-- (void)hideSpeaker:(BOOL)hidden {
- _speakerButton.hidden = hidden;
- _routesButton.hidden = !hidden;
-}
-
-#pragma mark - Event Functions
-
-- (void)bluetoothAvailabilityUpdateEvent:(NSNotification *)notif {
- bool available = [[notif.userInfo objectForKey:@"available"] intValue];
- dispatch_async(dispatch_get_main_queue(), ^{
- [self hideSpeaker:available];
- });
-}
-
-- (void)callUpdateEvent:(NSNotification *)notif {
- LinphoneCall *call = [[notif.userInfo objectForKey:@"call"] pointerValue];
- LinphoneCallState state = [[notif.userInfo objectForKey:@"state"] intValue];
- [self callUpdate:call state:state animated:TRUE];
-}
-
-- (void)callUpdate:(LinphoneCall *)call state:(LinphoneCallState)state animated:(BOOL)animated {
- _declineButton_earlyMedia.hidden = linphone_call_get_state(call) != LinphoneCallStateOutgoingEarlyMedia;
- _declineButton.hidden = !_declineButton_earlyMedia.hidden;
- _numpadButton.hidden = _declineButton_earlyMedia.hidden;
-}
-
-#pragma mark - Animation
-
-- (void)hideAnimation:(BOOL)hidden forView:(UIView *)target completion:(void (^)(BOOL finished))completion {
- if (hidden) {
- int original_y = target.frame.origin.y;
- CGRect newFrame = target.frame;
- newFrame.origin.y = self.view.frame.size.height;
- [UIView animateWithDuration:0.5
- delay:0.0
- options:UIViewAnimationOptionCurveEaseIn
- animations:^{
- target.frame = newFrame;
- }
- completion:^(BOOL finished) {
- CGRect originFrame = target.frame;
- originFrame.origin.y = original_y;
- target.hidden = YES;
- target.frame = originFrame;
- if (completion)
- completion(finished);
- }];
- } else {
- CGRect frame = target.frame;
- int original_y = frame.origin.y;
- frame.origin.y = self.view.frame.size.height;
- target.frame = frame;
- frame.origin.y = original_y;
- target.hidden = NO;
-
- [UIView animateWithDuration:0.5
- delay:0.0
- options:UIViewAnimationOptionCurveEaseOut
- animations:^{
- target.frame = frame;
- }
- completion:^(BOOL finished) {
- target.frame = frame; // in case application did not finish
- if (completion)
- completion(finished);
- }];
- }
-}
-
-
-@end
diff --git a/Classes/CallPausedTableView.h b/Classes/CallPausedTableView.h
deleted file mode 100644
index 622f19084..000000000
--- a/Classes/CallPausedTableView.h
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * 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 .
- */
-
-#import
-
-@interface CallPausedTableView : UITableViewController
-
-- (void)update;
-
-@end
diff --git a/Classes/CallPausedTableView.m b/Classes/CallPausedTableView.m
deleted file mode 100644
index 2dc9b6c29..000000000
--- a/Classes/CallPausedTableView.m
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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 .
- */
-
-#import "CallPausedTableView.h"
-#import "UICallPausedCell.h"
-#import "LinphoneManager.h"
-#import "Utils.h"
-
-@implementation CallPausedTableView
-
-#pragma mark - UI change
-
-- (void)update {
- [self.tableView reloadData];
- CGRect newOrigin = self.tableView.frame;
- newOrigin.size.height =
- [self tableView:self.tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]] *
- [self tableView:self.tableView numberOfRowsInSection:0];
- newOrigin.origin.y += self.tableView.frame.size.height - newOrigin.size.height;
- self.tableView.frame = newOrigin;
-}
-
-#pragma mark - UITableViewDataSource Functions
-- (LinphoneCall *)conferenceCallForRow:(NSInteger)row {
- const MSList *calls = linphone_core_get_calls(LC);
- int i = -1;
- while (calls) {
- if (linphone_call_get_state(calls->data) == LinphoneCallPaused) {
- i++;
- if (i == row)
- break;
- }
- calls = calls->next;
- }
- // we should reach this only when we are querying for conference
- return (calls ? calls->data : NULL);
-}
-
-#pragma mark - UITableViewDataSource Functions
-
-- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
- NSString *kCellId = NSStringFromClass(UICallPausedCell.class);
- UICallPausedCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellId];
- if (cell == nil) {
- cell = [[UICallPausedCell alloc] initWithIdentifier:kCellId];
- }
- [cell setCall:[self conferenceCallForRow:indexPath.row]];
- cell.contentView.userInteractionEnabled = false;
- return cell;
-}
-
-- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
- const MSList *calls = linphone_core_get_calls(LC);
- int count = 0;
- int conference_in_pause = 0;
- int call_in_conference = 0;
- while (calls) {
- LinphoneCall *call = calls->data;
- BOOL callInConference = linphone_call_params_get_local_conference_mode(linphone_call_get_current_params(call));
- if (linphone_call_get_state(call) == LinphoneCallPaused) {
- count++;
- }
- if(callInConference) {
- call_in_conference++;
- if (!linphone_core_is_in_conference(LC)) {
- conference_in_pause = 1;
- }
- }
- calls = calls->next;
- }
- if(call_in_conference == 1) {
- conference_in_pause = 0;
- }
- return count + conference_in_pause;
-}
-
-- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
- return 1;
-}
-
-- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
- return 1e-5;
-}
-- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
- return 1e-5;
-}
-
-@end
diff --git a/Classes/CallSideMenuView.h b/Classes/CallSideMenuView.h
deleted file mode 100644
index 2a97f78d5..000000000
--- a/Classes/CallSideMenuView.h
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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 .
- */
-
-#import
-
-#import "SideMenuTableView.h"
-#import "PhoneMainView.h"
-
-@interface CallSideMenuView : UIViewController
-
-@property(weak, nonatomic) IBOutlet UILabel *statsLabel;
-
-- (IBAction)onLateralSwipe:(id)sender;
-
-@end
diff --git a/Classes/CallSideMenuView.m b/Classes/CallSideMenuView.m
deleted file mode 100644
index 27ee4de48..000000000
--- a/Classes/CallSideMenuView.m
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * 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 .
- */
-
-#import "CallSideMenuView.h"
-#import "LinphoneManager.h"
-#import "PhoneMainView.h"
-
-@implementation CallSideMenuView {
- NSTimer *updateTimer;
-}
-
-#pragma mark - ViewController Functions
-
-- (void)viewWillAppear:(BOOL)animated {
- [super viewWillAppear:animated];
- if (updateTimer != nil) {
- [updateTimer invalidate];
- }
- updateTimer = [NSTimer scheduledTimerWithTimeInterval:1
- target:self
- selector:@selector(updateStats:)
- userInfo:nil
- repeats:YES];
-
- [self updateStats:nil];
-}
-
-- (void)viewWillDisappear:(BOOL)animated {
- [super viewWillDisappear:animated];
- if (updateTimer != nil) {
- [updateTimer invalidate];
- updateTimer = nil;
- }
-}
-
-- (IBAction)onLateralSwipe:(id)sender {
- [PhoneMainView.instance.mainViewController hideSideMenu:YES];
-}
-
-+ (NSString *)iceToString:(LinphoneIceState)state {
- switch (state) {
- case LinphoneIceStateNotActivated:
- return NSLocalizedString(@"Not activated", @"ICE has not been activated for this call");
- break;
- case LinphoneIceStateFailed:
- return NSLocalizedString(@"Failed", @"ICE processing has failed");
- break;
- case LinphoneIceStateInProgress:
- return NSLocalizedString(@"In progress", @"ICE process is in progress");
- break;
- case LinphoneIceStateHostConnection:
- return NSLocalizedString(@"Direct connection",
- @"ICE has established a direct connection to the remote host");
- break;
- case LinphoneIceStateReflexiveConnection:
- return NSLocalizedString(
- @"NAT(s) connection",
- @"ICE has established a connection to the remote host through one or several NATs");
- break;
- case LinphoneIceStateRelayConnection:
- return NSLocalizedString(@"Relay connection", @"ICE has established a connection through a relay");
- break;
- }
-}
-
-+ (NSString*)afinetToString:(int)remote_family {
- return (remote_family == LinphoneAddressFamilyUnspec) ? @"Unspecified":(remote_family == LinphoneAddressFamilyInet) ? @"IPv4" : @"IPv6";
-}
-
-+ (NSString *)mediaEncryptionToString:(LinphoneMediaEncryption)enc {
- switch (enc) {
- case LinphoneMediaEncryptionDTLS:
- return @"DTLS";
- case LinphoneMediaEncryptionSRTP:
- return @"SRTP";
- case LinphoneMediaEncryptionZRTP:
- return @"ZRTP";
- case LinphoneMediaEncryptionNone:
- break;
- }
- return NSLocalizedString(@"None", nil);
-}
-
-- (NSString *)updateStatsForCall:(LinphoneCall *)call stream:(LinphoneStreamType)stream {
- NSMutableString *result = [[NSMutableString alloc] init];
- const PayloadType *payload = NULL;
- const LinphoneCallStats *stats;
- const LinphoneCallParams *params = linphone_call_get_current_params(call);
- NSString *name;
-
- switch (stream) {
- case LinphoneStreamTypeAudio:
- name = @"Audio";
- payload = linphone_call_params_get_used_audio_codec(params);
- stats = linphone_call_get_audio_stats(call);
- break;
- case LinphoneStreamTypeText:
- name = @"Text";
- payload = linphone_call_params_get_used_text_codec(params);
- stats = linphone_call_get_text_stats(call);
- break;
- case LinphoneStreamTypeVideo:
- name = @"Video";
- payload = linphone_call_params_get_used_video_codec(params);
- stats = linphone_call_get_video_stats(call);
- break;
- case LinphoneStreamTypeUnknown:
- break;
- }
- if (payload == NULL) {
- return result;
- }
-
- [result appendString:@"\n"];
- [result appendString:name];
- [result appendString:@"\n"];
-
- [result appendString:[NSString stringWithFormat:@"Codec: %s/%iHz", payload->mime_type, payload->clock_rate]];
- if (stream == LinphoneStreamTypeAudio) {
- [result appendString:[NSString stringWithFormat:@"/%i channels", payload->channels]];
- }
- [result appendString:@"\n"];
- // Encoder & decoder descriptions
- const char *enc_desc = ms_factory_get_encoder(linphone_core_get_ms_factory(LC), payload->mime_type)->text;
- const char *dec_desc = ms_factory_get_decoder(linphone_core_get_ms_factory(LC), payload->mime_type)->text;
- if (strcmp(enc_desc, dec_desc) == 0) {
- [result appendString:[NSString stringWithFormat:@"Encoder/Decoder: %s", enc_desc]];
- [result appendString:@"\n"];
- } else {
- [result appendString:[NSString stringWithFormat:@"Encoder: %s", enc_desc]];
- [result appendString:@"\n"];
- [result appendString:[NSString stringWithFormat:@"Decoder: %s", dec_desc]];
- [result appendString:@"\n"];
- }
-
- if (stats != NULL) {
- [result appendString:[NSString stringWithFormat:@"Download bandwidth: %1.1f kbits/s",
- linphone_call_stats_get_download_bandwidth(stats)]];
- [result appendString:@"\n"];
- [result appendString:[NSString stringWithFormat:@"Upload bandwidth: %1.1f kbits/s",
- linphone_call_stats_get_upload_bandwidth(stats)]];
- [result appendString:@"\n"];
- if (stream == LinphoneStreamTypeVideo) {
- /*[result appendString:[NSString stringWithFormat:@"Estimated download bandwidth: %1.1f kbits/s",
- linphone_call_stats_get_estimated_download_bandwidth(stats)]];
- [result appendString:@"\n"];*/
- }
- [result
- appendString:[NSString stringWithFormat:@"ICE state: %@",
- [self.class iceToString:linphone_call_stats_get_ice_state(stats)]]];
- [result appendString:@"\n"];
- [result
- appendString:[NSString
- stringWithFormat:@"Afinet: %@",
- [self.class afinetToString:linphone_call_stats_get_ip_family_of_remote(
- stats)]]];
- [result appendString:@"\n"];
-
- // RTP stats section (packet loss count, etc)
- const rtp_stats_t rtp_stats = *linphone_call_stats_get_rtp_stats(stats);
- [result
- appendString:[NSString stringWithFormat:
- @"RTP packets: %llu total, %lld cum loss, %llu discarded, %llu OOT, %llu bad",
- rtp_stats.packet_recv, rtp_stats.cum_packet_loss, rtp_stats.discarded,
- rtp_stats.outoftime, rtp_stats.bad]];
- [result appendString:@"\n"];
- [result appendString:[NSString stringWithFormat:@"Sender loss rate: %.2f%%",
- linphone_call_stats_get_sender_loss_rate(stats)]];
- [result appendString:@"\n"];
- [result appendString:[NSString stringWithFormat:@"Receiver loss rate: %.2f%%",
- linphone_call_stats_get_receiver_loss_rate(stats)]];
- [result appendString:@"\n"];
-
- if (stream == LinphoneStreamTypeVideo) {
- const LinphoneVideoDefinition *recv_definition = linphone_call_params_get_received_video_definition(params);
- const LinphoneVideoDefinition *sent_definition = linphone_call_params_get_sent_video_definition(params);
- float sentFPS = linphone_call_params_get_sent_framerate(params);
- float recvFPS = linphone_call_params_get_received_framerate(params);
- [result appendString:[NSString stringWithFormat:@"Sent video resolution: %dx%d (%.1fFPS)", linphone_video_definition_get_width(sent_definition),
- linphone_video_definition_get_height(sent_definition), sentFPS]];
- [result appendString:@"\n"];
- [result appendString:[NSString stringWithFormat:@"Received video resolution: %dx%d (%.1fFPS)",
- linphone_video_definition_get_width(recv_definition),
- linphone_video_definition_get_height(recv_definition), recvFPS]];
- [result appendString:@"\n"];
- }
- }
- return result;
-}
-
-- (void)updateStats:(NSTimer *)timer {
- LinphoneCall *call = linphone_core_get_current_call(LC);
-
- if (!call) {
- _statsLabel.text = NSLocalizedString(@"No call in progress", nil);
- return;
- }
-
- NSMutableString *stats = [[NSMutableString alloc] init];
-
- LinphoneMediaEncryption enc = linphone_call_params_get_media_encryption(linphone_call_get_current_params(call));
- if (enc != LinphoneMediaEncryptionNone) {
- [stats appendString:[NSString
- stringWithFormat:@"Call encrypted using %@", [self.class mediaEncryptionToString:enc]]];
- }
-
- [stats appendString:[self updateStatsForCall:call stream:LinphoneStreamTypeAudio]];
- [stats appendString:[self updateStatsForCall:call stream:LinphoneStreamTypeVideo]];
- [stats appendString:[self updateStatsForCall:call stream:LinphoneStreamTypeText]];
-
- _statsLabel.text = stats;
-}
-
-@end
diff --git a/Classes/CallSideMenuView.xib b/Classes/CallSideMenuView.xib
deleted file mode 100644
index a0208f810..000000000
--- a/Classes/CallSideMenuView.xib
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Audio: upr
-Video: down
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Classes/CallView.h b/Classes/CallView.h
deleted file mode 100644
index fa188895d..000000000
--- a/Classes/CallView.h
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * 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 .
- */
-
-#import
-
-#import "VideoZoomHandler.h"
-#import "UICamSwitch.h"
-
-#import "UICompositeView.h"
-#import "CallPausedTableView.h"
-
-#import "UIMutedMicroButton.h"
-#import "UIPauseButton.h"
-#import "UISpeakerButton.h"
-#import "UIVideoButton.h"
-#import "UIHangUpButton.h"
-#import "UIDigitButton.h"
-#import "UIRoundedImageView.h"
-#import "UIBouncingView.h"
-
-@class VideoView;
-
-@interface CallView : TPMultiLayoutViewController {
- @private
- UITapGestureRecognizer *singleFingerTap;
- NSTimer *hideControlsTimer;
- NSTimer *videoDismissTimer;
- BOOL videoHidden;
- BOOL callRecording;
- VideoZoomHandler *videoZoomHandler;
-}
-
-@property(nonatomic, strong) IBOutlet CallPausedTableView *pausedCallsTable;
-
-@property(nonatomic, strong) IBOutlet UIView *videoGroup;
-@property(nonatomic, strong) IBOutlet UIView *videoView;
-@property(nonatomic, strong) IBOutlet UIView *videoPreview;
-@property(nonatomic, strong) IBOutlet UICamSwitch *videoCameraSwitch;
-@property(nonatomic, strong) IBOutlet UIActivityIndicatorView *videoWaitingForFirstImage;
-@property(weak, nonatomic) IBOutlet UIView *callView;
-
-@property(nonatomic, strong) IBOutlet UIPauseButton *callPauseButton;
-@property(nonatomic, strong) IBOutlet UIButton *optionsConferenceButton;
-@property(nonatomic, strong) IBOutlet UIVideoButton *videoButton;
-@property(nonatomic, strong) IBOutlet UIMutedMicroButton *microButton;
-@property(nonatomic, strong) IBOutlet UISpeakerButton *speakerButton;
-@property(nonatomic, strong) IBOutlet UIToggleButton *routesButton;
-@property(nonatomic, strong) IBOutlet UIToggleButton *optionsButton;
-@property(nonatomic, strong) IBOutlet UIHangUpButton *hangupButton;
-@property(nonatomic, strong) IBOutlet UIView *numpadView;
-@property(nonatomic, strong) IBOutlet UIView *routesView;
-@property(nonatomic, strong) IBOutlet UIView *optionsView;
-@property(nonatomic, strong) IBOutlet UIButton *routesEarpieceButton;
-@property(nonatomic, strong) IBOutlet UIButton *routesSpeakerButton;
-@property(nonatomic, strong) IBOutlet UIButton *routesBluetoothButton;
-@property(nonatomic, strong) IBOutlet UIButton *optionsAddButton;
-@property(nonatomic, strong) IBOutlet UIButton *optionsTransferButton;
-@property(nonatomic, strong) IBOutlet UIToggleButton *numpadButton;
-@property(weak, nonatomic) IBOutlet UIPauseButton *conferencePauseButton;
-@property(weak, nonatomic) IBOutlet UIBouncingView *chatNotificationView;
-@property(weak, nonatomic) IBOutlet UILabel *chatNotificationLabel;
-@property (weak, nonatomic) IBOutlet UIButton *recordButton;
-@property (weak, nonatomic) IBOutlet UIButton *recordButtonOnView;
-
-@property(weak, nonatomic) IBOutlet UIView *bottomBar;
-@property(nonatomic, strong) IBOutlet UIDigitButton *oneButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *twoButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *threeButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *fourButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *fiveButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *sixButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *sevenButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *eightButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *nineButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *starButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *zeroButton;
-@property(nonatomic, strong) IBOutlet UIDigitButton *hashButton;
-@property(weak, nonatomic) IBOutlet UIRoundedImageView *avatarImage;
-@property(weak, nonatomic) IBOutlet UILabel *nameLabel;
-@property(weak, nonatomic) IBOutlet UILabel *durationLabel;
-@property(weak, nonatomic) IBOutlet UIView *pausedByRemoteView;
-@property(weak, nonatomic) IBOutlet UIView *noActiveCallView;
-@property(weak, nonatomic) IBOutlet UIView *conferenceView;
-@property(strong, nonatomic) IBOutlet CallPausedTableView *conferenceCallsTable;
-@property (weak, nonatomic) IBOutlet UIView *waitView;
-@property (weak, nonatomic) IBOutlet UIView *infoView;
-
-- (IBAction)onRoutesClick:(id)sender;
-- (IBAction)onRoutesBluetoothClick:(id)sender;
-- (IBAction)onRoutesEarpieceClick:(id)sender;
-- (IBAction)onRoutesSpeakerClick:(id)sender;
-- (IBAction)onOptionsClick:(id)sender;
-- (IBAction)onOptionsTransferClick:(id)sender;
-- (IBAction)onOptionsAddClick:(id)sender;
-- (IBAction)onOptionsConferenceClick:(id)sender;
-- (IBAction)onNumpadClick:(id)sender;
-- (IBAction)onChatClick:(id)sender;
-- (IBAction)onRecordClick:(id)sender;
-- (IBAction)onRecordOnViewClick:(id)sender;
-
-@end
diff --git a/Classes/CallView.m b/Classes/CallView.m
deleted file mode 100644
index 9d9514ee1..000000000
--- a/Classes/CallView.m
+++ /dev/null
@@ -1,1007 +0,0 @@
-/*
- * 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 .
- */
-
-#import "linphoneapp-Swift.h"
-#import
-#import
-#import
-#import
-#import
-#import
-#import
-#import "UICallConferenceCell.h"
-
-#import "CallView.h"
-#import "CallSideMenuView.h"
-#import "LinphoneManager.h"
-#import "PhoneMainView.h"
-#import "Utils.h"
-
-#include "linphone/linphonecore.h"
-
-#import "linphoneapp-Swift.h"
-
-const NSInteger SECURE_BUTTON_TAG = 5;
-
-@implementation CallView {
- BOOL hiddenVolume;
-}
-
-#pragma mark - Lifecycle Functions
-
-- (id)init {
- self = [super initWithNibName:NSStringFromClass(self.class) bundle:[NSBundle mainBundle]];
- if (self != nil) {
- singleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleControls:)];
- videoZoomHandler = [[VideoZoomHandler alloc] init];
- videoHidden = TRUE;
- [self updateCallView];
- }
- return self;
-}
-
-#pragma mark - UICompositeViewDelegate Functions
-
-static UICompositeViewDescription *compositeDescription = nil;
-
-+ (UICompositeViewDescription *)compositeViewDescription {
- if (compositeDescription == nil) {
- compositeDescription = [[UICompositeViewDescription alloc] init:self.class
- statusBar:StatusBarView.class
- tabBar:nil
- sideMenu:CallSideMenuView.class
- fullscreen:false
- isLeftFragment:YES
- fragmentWith:nil];
- compositeDescription.darkBackground = true;
- }
- return compositeDescription;
-}
-
-- (UICompositeViewDescription *)compositeViewDescription {
- return self.class.compositeViewDescription;
-}
-
-#pragma mark - ViewController Functions
-
-- (void)viewDidLoad {
- [super viewDidLoad];
-
- _routesEarpieceButton.enabled = !IPAD;
-
-// TODO: fixme! video preview frame is too big compared to openGL preview
-// frame, so until this is fixed, temporary disabled it.
-#if 0
- _videoPreview.layer.borderColor = UIColor.whiteColor.CGColor;
- _videoPreview.layer.borderWidth = 1;
-#endif
- [singleFingerTap setNumberOfTapsRequired:1];
- [singleFingerTap setCancelsTouchesInView:FALSE];
- [self.videoView addGestureRecognizer:singleFingerTap];
-
- [videoZoomHandler setup:_videoGroup];
- _videoGroup.alpha = 0;
-
- [_videoCameraSwitch setPreview:_videoPreview];
-
- UIPanGestureRecognizer *dragndrop =
- [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(moveVideoPreview:)];
- dragndrop.minimumNumberOfTouches = 1;
- [_videoPreview addGestureRecognizer:dragndrop];
-
- [_zeroButton setDigit:'0'];
- [_zeroButton setDtmf:true];
- [_oneButton setDigit:'1'];
- [_oneButton setDtmf:true];
- [_twoButton setDigit:'2'];
- [_twoButton setDtmf:true];
- [_threeButton setDigit:'3'];
- [_threeButton setDtmf:true];
- [_fourButton setDigit:'4'];
- [_fourButton setDtmf:true];
- [_fiveButton setDigit:'5'];
- [_fiveButton setDtmf:true];
- [_sixButton setDigit:'6'];
- [_sixButton setDtmf:true];
- [_sevenButton setDigit:'7'];
- [_sevenButton setDtmf:true];
- [_eightButton setDigit:'8'];
- [_eightButton setDtmf:true];
- [_nineButton setDigit:'9'];
- [_nineButton setDtmf:true];
- [_starButton setDigit:'*'];
- [_starButton setDtmf:true];
- [_hashButton setDigit:'#'];
- [_hashButton setDtmf:true];
-}
-
-- (void)dealloc {
- [PhoneMainView.instance.view removeGestureRecognizer:singleFingerTap];
- // Remove all observer
- [NSNotificationCenter.defaultCenter removeObserver:self];
-}
-
-- (void)viewWillAppear:(BOOL)animated {
- [super viewWillAppear:animated];
- _waitView.hidden = TRUE;
- CallManager.instance.nextCallIsTransfer = FALSE;
-
- callRecording = FALSE;
- _recordButtonOnView.hidden = TRUE;
-
- // Update on show
- [self hideRoutes:TRUE animated:FALSE];
- [self hideOptions:TRUE animated:FALSE];
- [self hidePad:TRUE animated:FALSE];
- [self hideSpeaker: [CallManager.instance isBluetoothAvailable]];
- [self callDurationUpdate];
- [self onCurrentCallChange];
- // Set windows (warn memory leaks)
- linphone_core_set_native_video_window_id(LC, (__bridge void *)(_videoView));
- linphone_core_set_native_preview_window_id(LC, (__bridge void *)(_videoPreview));
-
- [self previewTouchLift];
- // Enable tap
- [singleFingerTap setEnabled:TRUE];
-
- [NSNotificationCenter.defaultCenter addObserver:self
- selector:@selector(messageReceived:)
- name:kLinphoneMessageReceived
- object:nil];
- [NSNotificationCenter.defaultCenter addObserver:self
- selector:@selector(bluetoothAvailabilityUpdateEvent:)
- name:kLinphoneBluetoothAvailabilityUpdate
- object:nil];
- [NSNotificationCenter.defaultCenter addObserver:self
- selector:@selector(callUpdateEvent:)
- name:kLinphoneCallUpdate
- object:nil];
-
- [NSNotificationCenter.defaultCenter addObserver:self
- selector:@selector(participantListChanged:)
- name:kLinphoneConfStateParticipantListChanged
- object:nil];
- [NSNotificationCenter.defaultCenter addObserver:self
- selector:@selector(confStateChanged:)
- name:kLinphoneConfStateChanged
- object:nil];
-
-
-
-
- [NSTimer scheduledTimerWithTimeInterval:1
- target:self
- selector:@selector(callDurationUpdate)
- userInfo:nil
- repeats:YES];
-
-}
-
-- (void)viewDidAppear:(BOOL)animated {
- [super viewDidAppear:animated];
-
- [[UIApplication sharedApplication] setIdleTimerDisabled:YES];
- [[UIDevice currentDevice] setProximityMonitoringEnabled:TRUE];
-
- [PhoneMainView.instance setVolumeHidden:TRUE];
- hiddenVolume = TRUE;
-
- // we must wait didAppear to reset fullscreen mode because we cannot change it in viewwillappear
- LinphoneCall *call = linphone_core_get_current_call(LC);
- LinphoneCallState state = (call != NULL) ? linphone_call_get_state(call) : 0;
- [self callUpdate:call state:state animated:FALSE];
-}
-
-- (void)viewWillDisappear:(BOOL)animated {
- [super viewWillDisappear:animated];
-[[UIDevice currentDevice] setProximityMonitoringEnabled:FALSE];
- [self disableVideoDisplay:TRUE animated:NO];
-
- if (hideControlsTimer != nil) {
- [hideControlsTimer invalidate];
- hideControlsTimer = nil;
- }
-
- if (hiddenVolume) {
- [PhoneMainView.instance setVolumeHidden:FALSE];
- hiddenVolume = FALSE;
- }
-
- if (videoDismissTimer) {
- [self dismissVideoActionSheet:videoDismissTimer];
- [videoDismissTimer invalidate];
- videoDismissTimer = nil;
- }
-
- // Remove observer
- [NSNotificationCenter.defaultCenter removeObserver:self];
-}
-
-- (void)viewDidDisappear:(BOOL)animated {
- [super viewDidDisappear:animated];
-
- [[UIApplication sharedApplication] setIdleTimerDisabled:false];
- [[UIDevice currentDevice] setProximityMonitoringEnabled:FALSE];
-
- [PhoneMainView.instance fullScreen:false];
- // Disable tap
- [singleFingerTap setEnabled:FALSE];
-
- if (linphone_core_get_calls_nb(LC) == 0) {
- // reseting speaker button because no more call
- _speakerButton.selected = FALSE;
- }
-
- NSString *address = [LinphoneManager.instance lpConfigStringForKey:@"sas_dialog_denied"];
- if (address) {
- UIConfirmationDialog *securityDialog = [UIConfirmationDialog ShowWithMessage:NSLocalizedString(@"Trust has been denied. Make a call to start the authentication process again.", nil)
- cancelMessage:NSLocalizedString(@"CANCEL", nil)
- confirmMessage:NSLocalizedString(@"CALL", nil)
- onCancelClick:^() {
- }
- onConfirmationClick:^() {
- LinphoneAddress *addr = linphone_address_new(address.UTF8String);
- [CallManager.instance startCallWithAddr:addr isSas:TRUE];
- linphone_address_unref(addr);
- } ];
- [securityDialog.securityImage setImage:[UIImage imageNamed:@"security_alert_indicator.png"]];
- securityDialog.securityImage.hidden = FALSE;
- [securityDialog setSpecialColor];
- [LinphoneManager.instance lpConfigSetString:nil forKey:@"sas_dialog_denied"];
- }
-}
-
-- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
- [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
- [self updateUnreadMessage:NO];
- [self previewTouchLift];
- [_recordButtonOnView setHidden:!callRecording];
- [self updateCallView];
- LinphoneCall *call = linphone_core_get_current_call(LC) ;
- if (call && linphone_call_get_state(call) == LinphoneCallStatePausedByRemote) {
- _pausedByRemoteView.hidden = NO;
- [self updateInfoView:TRUE];
- }
- _conferenceView.hidden = ![CallManager.instance isInConference];
- [self onCurrentCallChange];
-}
-
-#pragma mark - UI modification
-
-- (void)updateInfoView:(BOOL)pausedByRemote {
- CGRect infoFrame = _infoView.frame;
- if (pausedByRemote || !videoHidden) {
- infoFrame.origin.y = 0;
- } else {
- infoFrame.origin.y = (_avatarImage.frame.origin.y-66)/2;
- }
- _infoView.frame = infoFrame;
-}
-
-- (void)updateCallView {
- CGRect pauseFrame = _callPauseButton.frame;
- CGRect recordFrame = _recordButtonOnView.frame;
- if (videoHidden) {
- pauseFrame.origin.y = _bottomBar.frame.origin.y - pauseFrame.size.height - 60;
- } else {
- pauseFrame.origin.y = _videoCameraSwitch.frame.origin.y+_videoGroup.frame.origin.y;
- }
- recordFrame.origin.y = _bottomBar.frame.origin.y - pauseFrame.size.height - 60;
- _callPauseButton.frame = pauseFrame;
- _recordButtonOnView.frame = recordFrame;
- [self updateInfoView:FALSE];
-}
-
-- (void)hideSpinnerIndicator:(LinphoneCall *)call {
- _videoWaitingForFirstImage.hidden = TRUE;
-}
-
-static void hideSpinner(LinphoneCall *call, void *user_data) {
- CallView *thiz = (__bridge CallView *)user_data;
- [thiz hideSpinnerIndicator:call];
-}
-
-- (void)updateBottomBar:(LinphoneCall *)call state:(LinphoneCallState)state {
- [_speakerButton update];
- [_microButton update];
- [_callPauseButton update];
- [_conferencePauseButton update];
- [_videoButton update];
- [_hangupButton update];
-
- _optionsButton.enabled = (!call || !linphone_core_sound_resources_locked(LC));
- _optionsTransferButton.enabled = call && !linphone_core_sound_resources_locked(LC);
- // enable conference button if 2 calls are presents and at least one is not in the conference
- int confSize = linphone_core_get_conference_size(LC) - ([CallManager.instance isInConference] ? 1 : 0);
- _optionsConferenceButton.enabled = (linphone_core_get_calls_nb(LC) > 1) && (linphone_core_get_calls_nb(LC) != confSize) && !CallManager.instance.hasConferenceAsGuest;
-
- // Disable transfert in conference
- if (linphone_core_get_current_call(LC) == NULL) {
- [_optionsTransferButton setEnabled:FALSE];
- } else {
- [_optionsTransferButton setEnabled:TRUE];
- }
-
- switch (state) {
- case LinphoneCallEnd:
- case LinphoneCallError:
- case LinphoneCallIncoming:
- case LinphoneCallOutgoing:
- [self hidePad:TRUE animated:TRUE];
- [self hideOptions:TRUE animated:TRUE];
- [self hideRoutes:TRUE animated:TRUE];
- default:
- break;
- }
-}
-
-- (void)toggleControls:(id)sender {
- bool controlsHidden = (_bottomBar.alpha == 0.0);
- [self hideControls:!controlsHidden sender:sender];
-}
-
-- (void)timerHideControls:(id)sender {
- [self hideControls:TRUE sender:sender];
-}
-
-- (void)hideControls:(BOOL)hidden sender:(id)sender {
- if (videoHidden && hidden)
- return;
-
- if (hideControlsTimer) {
- [hideControlsTimer invalidate];
- hideControlsTimer = nil;
- }
-
- if ([[PhoneMainView.instance currentView] equal:CallView.compositeViewDescription]) {
- // show controls
- [UIView beginAnimations:nil context:nil];
- [UIView setAnimationDuration:0.35];
- _pausedCallsTable.tableView.alpha = _videoCameraSwitch.alpha = _callPauseButton.alpha = _routesView.alpha =
- _optionsView.alpha = _numpadView.alpha = _bottomBar.alpha = _conferenceView.alpha = (hidden ? 0 : 1);
- _infoView.alpha = (hidden ? 0 : .8f);
-
- if ([CallManager.instance inVideoConf]) {
- _videoCameraSwitch.frame = CGRectMake(_videoCameraSwitch.frame.origin.x, _bottomBar.frame.origin.y - 75, _videoCameraSwitch.frame.size.width,_videoCameraSwitch.frame.size.height);
- }
-
- if (CallManager.instance.isInConference) {
- _callPauseButton.hidden = true;
- _conferenceView.frame = CGRectMake(_conferenceView.frame.origin.x,_conferenceView.frame.origin.y,_conferenceView.frame.size.width,_conferenceCallsTable.tableView.frame.origin.y+[_conferenceCallsTable.tableView numberOfRowsInSection:0]*CONFERENCE_CELL_HEIGHT+10);
- }
-
-
- [UIView commitAnimations];
-
- [PhoneMainView.instance hideTabBar:hidden];
- [PhoneMainView.instance hideStatusBar:hidden];
-
- if (!hidden) {
- // hide controls in 5 sec
- hideControlsTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
- target:self
- selector:@selector(timerHideControls:)
- userInfo:nil
- repeats:NO];
- }
- }
-}
-
-- (void)disableVideoDisplay:(BOOL)disabled animated:(BOOL)animation {
- if (disabled == videoHidden && animation)
- return;
- videoHidden = disabled;
-
- if (!disabled) {
- [videoZoomHandler resetZoom];
- }
- if (animation) {
- [UIView beginAnimations:nil context:nil];
- [UIView setAnimationDuration:1.0];
- }
-
- [_videoGroup setAlpha:disabled ? 0 : 1];
-
- [self hideControls:!disabled sender:nil];
-
- if (animation) {
- [UIView commitAnimations];
- }
-
- // only show camera switch button if we have more than 1 camera
- _videoCameraSwitch.hidden = (disabled || !LinphoneManager.instance.frontCamId);
- _videoPreview.hidden = (disabled || !linphone_core_self_view_enabled(LC));
-
- if (hideControlsTimer != nil) {
- [hideControlsTimer invalidate];
- hideControlsTimer = nil;
- }
-
- if(![PhoneMainView.instance isIphoneXDevice]){
- [PhoneMainView.instance fullScreen:!disabled];
- }
- [PhoneMainView.instance hideTabBar:!disabled];
-
- if (!disabled) {
-#ifdef TEST_VIDEO_VIEW_CHANGE
- [NSTimer scheduledTimerWithTimeInterval:5.0
- target:self
- selector:@selector(_debugChangeVideoView)
- userInfo:nil
- repeats:YES];
-#endif
- // [self batteryLevelChanged:nil];
-
- [_videoWaitingForFirstImage setHidden:NO];
- [_videoWaitingForFirstImage startAnimating];
-
- if ([CallManager.instance inVideoConf])
- [self hideSpinnerIndicator:nil];
- else {
- LinphoneCall *call = linphone_core_get_current_call(LC);
- // linphone_call_params_get_used_video_codec return 0 if no video stream enabled
- if (call != NULL && linphone_call_params_get_used_video_codec(linphone_call_get_current_params(call))) {
- linphone_call_set_next_video_frame_decoded_callback(call, hideSpinner, (__bridge void *)(self));
- }
- }
- }
-
- if ([CallManager.instance isInConference]) {
- [_conferenceView removeFromSuperview];
- [_callView addSubview:_conferenceView];
- } else {
- [_conferenceView removeFromSuperview];
- [self.view addSubview:_conferenceView];
- [self.view sendSubviewToBack:_conferenceView];
- }
-}
-
-- (void)displayVideoCall:(BOOL)animated {
- [self disableVideoDisplay:FALSE animated:animated];
-}
-
-- (void)displayAudioCall:(BOOL)animated {
- [self disableVideoDisplay:TRUE animated:animated];
-}
-
-- (void)callDurationUpdate {
- int duration =
- linphone_core_get_current_call(LC) ? linphone_call_get_duration(linphone_core_get_current_call(LC)) : 0;
- _durationLabel.text = [LinphoneUtils durationToString:duration];
-
- [_pausedCallsTable update];
- [_conferenceCallsTable update];
-}
-
-- (void)onCurrentCallChange {
- LinphoneCall *call = linphone_core_get_current_call(LC);
-
- _noActiveCallView.hidden = (call || CallManager.instance.isInConference);
- _callView.hidden = !call && !CallManager.instance.isInConference;
- _conferenceView.hidden = !CallManager.instance.isInConference;
- _conferenceView.hidden = !CallManager.instance.isInConference;
- _callPauseButton.hidden = !call;
-
-
- [_callPauseButton setType:UIPauseButtonType_CurrentCall call:call];
- [_conferencePauseButton setType:UIPauseButtonType_Conference call:call];
-
- if (call) {
- const LinphoneAddress *addr = linphone_call_get_remote_address(call);
- [ContactDisplay setDisplayNameLabel:_nameLabel forAddress:addr];
- char *uri = linphone_address_as_string_uri_only(addr);
- ms_free(uri);
- [_avatarImage setImage:[FastAddressBook imageForAddress:addr] bordered:YES withRoundedRadius:YES];
- }
-}
-
-- (void)hidePad:(BOOL)hidden animated:(BOOL)animated {
- if (hidden) {
- [_numpadButton setOff];
- } else {
- [_numpadButton setOn];
- }
- if (hidden != _numpadView.hidden) {
- if (animated) {
- [self hideAnimation:hidden forView:_numpadView completion:nil];
- } else {
- [_numpadView setHidden:hidden];
- }
- }
-}
-
-- (void)hideRoutes:(BOOL)hidden animated:(BOOL)animated {
- if (hidden) {
- [_routesButton setOff];
- } else {
- [_routesButton setOn];
- }
-
- _routesBluetoothButton.selected = [CallManager.instance isBluetoothEnabled];
- _routesSpeakerButton.selected = [CallManager.instance isSpeakerEnabled];
- _routesEarpieceButton.selected = !_routesBluetoothButton.selected && !_routesSpeakerButton.selected;
-
- if (hidden != _routesView.hidden) {
- if (animated) {
- [self hideAnimation:hidden forView:_routesView completion:nil];
- } else {
- [_routesView setHidden:hidden];
- }
- }
-}
-
-- (void)hideOptions:(BOOL)hidden animated:(BOOL)animated {
- if (hidden) {
- [_optionsButton setOff];
- } else {
- [_optionsButton setOn];
- }
- if (hidden != _optionsView.hidden) {
- if (animated) {
- [self hideAnimation:hidden forView:_optionsView completion:nil];
- } else {
- [_optionsView setHidden:hidden];
- }
- }
-}
-
-- (void)hideSpeaker:(BOOL)hidden {
- _speakerButton.hidden = hidden;
- _routesButton.hidden = !hidden;
-}
-
-#pragma mark - Event Functions
-
-- (void)bluetoothAvailabilityUpdateEvent:(NSNotification *)notif {
- bool available = [[notif.userInfo objectForKey:@"available"] intValue];
- dispatch_async(dispatch_get_main_queue(), ^{
- [self hideSpeaker:available];
- });
-}
-
-- (void)callUpdateEvent:(NSNotification *)notif {
- LinphoneCall *call = [[notif.userInfo objectForKey:@"call"] pointerValue];
- LinphoneCallState state = [[notif.userInfo objectForKey:@"state"] intValue];
- [self callUpdate:call state:state animated:TRUE];
-}
-
-- (void)callUpdate:(LinphoneCall *)call state:(LinphoneCallState)state animated:(BOOL)animated {
- [self updateBottomBar:call state:state];
- if (hiddenVolume) {
- [PhoneMainView.instance setVolumeHidden:FALSE];
- hiddenVolume = FALSE;
- }
-
- // Update tables
- [_pausedCallsTable update];
- [_conferenceCallsTable update];
-
- [self onCurrentCallChange];
-
- LinphoneCall *currentCall = linphone_core_get_current_call(LC);
- BOOL shouldDisableVideo = currentCall ? !linphone_call_params_video_enabled(linphone_call_get_current_params(currentCall)): ![CallManager.instance inVideoConf];
-
-
- if (videoHidden != shouldDisableVideo) {
- if (!shouldDisableVideo) {
- [self displayVideoCall:animated];
- } else {
- [self displayAudioCall:animated];
- }
- }
-
- // Fake call update
- if (call == NULL) {
- return;
- }
-
- if (state != LinphoneCallPausedByRemote) {
- _pausedByRemoteView.hidden = YES;
- }
-
- switch (state) {
- case LinphoneCallIncomingReceived:
- case LinphoneCallOutgoingInit:
- case LinphoneCallConnected:
- case LinphoneCallStreamsRunning: {
- // check video, because video can be disabled because of the low bandwidth.
- if (!linphone_call_params_video_enabled(linphone_call_get_current_params(call))) {
- const LinphoneCallParams *param = linphone_call_get_current_params(call);
- CallAppData *data = [CallManager getAppDataWithCall:call];
- if (state == LinphoneCallStreamsRunning && data && data.videoRequested && linphone_call_params_low_bandwidth_enabled(param)) {
- // too bad video was not enabled because low bandwidth
- UIAlertController *errView = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Low bandwidth", nil)
- message:NSLocalizedString(@"Video cannot be activated because of low bandwidth "
- @"condition, only audio is available",
- nil)
- preferredStyle:UIAlertControllerStyleAlert];
-
- UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Continue", nil)
- style:UIAlertActionStyleDefault
- handler:^(UIAlertAction * action) {}];
-
- [errView addAction:defaultAction];
- [self presentViewController:errView animated:YES completion:nil];
- data.videoRequested = FALSE;
- [CallManager setAppDataWithCall:call appData:data];
- }
- }
- break;
- }
- case LinphoneCallUpdatedByRemote: {
- const LinphoneCallParams *current = linphone_call_get_current_params(call);
- const LinphoneCallParams *remote = linphone_call_get_remote_params(call);
-
- /* remote wants to add video */
- if ((linphone_core_video_display_enabled(LC) && !linphone_call_params_video_enabled(current) &&
- linphone_call_params_video_enabled(remote)) &&
- (!linphone_core_get_video_policy(LC)->automatically_accept ||
- (([UIApplication sharedApplication].applicationState != UIApplicationStateActive) &&
- floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max))) {
- linphone_core_defer_call_update(LC, call);
- [self displayAskToEnableVideoCall:call];
- } else if (linphone_call_params_video_enabled(current) && !linphone_call_params_video_enabled(remote) && ![CallManager.instance inVideoConf]) {
- [self displayAudioCall:animated];
- }
- break;
- }
- case LinphoneCallPausing:
- case LinphoneCallPaused:
- [self displayAudioCall:animated];
- break;
- case LinphoneCallPausedByRemote:
- if (![CallManager.instance inVideoConf]) {
- [self displayAudioCall:animated];
- if (call == linphone_core_get_current_call(LC)) {
- _pausedByRemoteView.hidden = NO;
- [self updateInfoView:TRUE];
- }
- }
- break;
- case LinphoneCallEnd:
- case LinphoneCallError:
- default:
- break;
- }
-}
-
-#pragma mark - ActionSheet Functions
-
-- (void)displayAskToEnableVideoCall:(LinphoneCall *)call {
-
- if (CallManager.instance.inVideoConf) { // we are hosting a video conf, so just accept people wanting to activate video.
- LinphoneCallParams *params = linphone_core_create_call_params(LC, call);
- linphone_call_params_enable_video(params, TRUE);
- linphone_call_accept_update(call, params);
- return;
- }
-
- if (linphone_core_get_video_policy(LC)->automatically_accept &&
- !([UIApplication sharedApplication].applicationState != UIApplicationStateActive))
- return;
-
- NSString *username = [FastAddressBook displayNameForAddress:linphone_call_get_remote_address(call)];
- NSString *title = [NSString stringWithFormat:NSLocalizedString(@"%@ would like to enable video", nil), username];
- if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive &&
- floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_9_x_Max) {
- UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
- content.title = NSLocalizedString(@"Video request", nil);
- content.body = title;
- content.categoryIdentifier = @"video_request";
- content.userInfo = @{
- @"CallId" : [NSString stringWithUTF8String:linphone_call_log_get_call_id(linphone_call_get_call_log(call))]
- };
-
- UNNotificationRequest *req =
- [UNNotificationRequest requestWithIdentifier:@"video_request" content:content trigger:NULL];
- [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:req
- withCompletionHandler:^(NSError *_Nullable error) {
- // Enable or disable features based on authorization.
- if (error) {
- LOGD(@"Error while adding notification request :");
- LOGD(error.description);
- }
- }];
- } else {
- UIConfirmationDialog *sheet = [UIConfirmationDialog ShowWithMessage:title
- cancelMessage:nil
- confirmMessage:NSLocalizedString(@"ACCEPT", nil)
- onCancelClick:^() {
- LOGI(@"User declined video proposal");
- if (call == linphone_core_get_current_call(LC)) {
- [CallManager.instance acceptVideoWithCall:call confirm:FALSE];
- [videoDismissTimer invalidate];
- videoDismissTimer = nil;
- }
- }
- onConfirmationClick:^() {
- LOGI(@"User accept video proposal");
- if (call == linphone_core_get_current_call(LC)) {
- [CallManager.instance acceptVideoWithCall:call confirm:TRUE];
- [videoDismissTimer invalidate];
- videoDismissTimer = nil;
- }
- }
- inController:self];
- videoDismissTimer = [NSTimer scheduledTimerWithTimeInterval:30
- target:self
- selector:@selector(dismissVideoActionSheet:)
- userInfo:sheet
- repeats:NO];
- }
-}
-
-- (void)dismissVideoActionSheet:(NSTimer *)timer {
- UIConfirmationDialog *sheet = (UIConfirmationDialog *)timer.userInfo;
- [sheet dismiss];
-}
-
-#pragma mark VideoPreviewMoving
-
-- (void)moveVideoPreview:(UIPanGestureRecognizer *)dragndrop {
- CGPoint center = [dragndrop locationInView:_videoPreview.superview];
- _videoPreview.center = center;
- if (dragndrop.state == UIGestureRecognizerStateEnded) {
- [self previewTouchLift];
- }
-}
-
-- (CGFloat)coerce:(CGFloat)value betweenMin:(CGFloat)min andMax:(CGFloat)max {
- return MAX(min, MIN(value, max));
-}
-
-- (void)previewTouchLift {
- CGRect previewFrame = _videoPreview.frame;
- previewFrame.origin.x = [self coerce:previewFrame.origin.x
- betweenMin:5
- andMax:(UIScreen.mainScreen.bounds.size.width - 5 - previewFrame.size.width)];
- previewFrame.origin.y = [self coerce:previewFrame.origin.y
- betweenMin:5
- andMax:(UIScreen.mainScreen.bounds.size.height - 5 - previewFrame.size.height)];
-
- if (!CGRectEqualToRect(previewFrame, _videoPreview.frame)) {
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- [UIView animateWithDuration:0.3
- animations:^{
- LOGD(@"Recentering preview to %@", NSStringFromCGRect(previewFrame));
- _videoPreview.frame = previewFrame;
- }];
- });
- }
-}
-
-#pragma mark - Action Functions
-
-- (IBAction)onNumpadClick:(id)sender {
- if ([_numpadView isHidden]) {
- [self hidePad:FALSE animated:ANIMATED];
- } else {
- [self hidePad:TRUE animated:ANIMATED];
- }
-}
-
-- (IBAction)onChatClick:(id)sender {
- const LinphoneCall *currentCall = linphone_core_get_current_call(LC);
- const LinphoneAddress *addr = currentCall ? linphone_call_get_remote_address(currentCall) : NULL;
- // TODO encrpted or unencrpted
- [LinphoneManager.instance lpConfigSetBool:TRUE forKey:@"create_chat"];
- [PhoneMainView.instance getOrCreateOneToOneChatRoom:addr waitView:_waitView isEncrypted:FALSE];
-}
-
-- (IBAction)onRecordClick:(id)sender {
- if (![_optionsView isHidden])
- [self hideOptions:TRUE animated:ANIMATED];
- if (callRecording) {
- [self onRecordOnViewClick:nil];
- } else {
- LOGD(@"Recording Starts");
-
- [_recordButton setImage:[UIImage imageNamed:@"rec_off_default.png"] forState:UIControlStateNormal];
- [_recordButtonOnView setHidden:FALSE];
-
- LinphoneCall *call = linphone_core_get_current_call(LC);
- linphone_call_start_recording(call);
-
- callRecording = TRUE;
- }
-}
-
-- (IBAction)onRecordOnViewClick:(id)sender {
- LOGD(@"Recording Stops");
- [_recordButton setImage:[UIImage imageNamed:@"rec_on_default.png"] forState:UIControlStateNormal];
- [_recordButtonOnView setHidden:TRUE];
-
- LinphoneCall *call = linphone_core_get_current_call(LC);
- linphone_call_stop_recording(call);
-
- callRecording = FALSE;
-
- NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
- NSString *writablePath = [paths objectAtIndex:0];
- writablePath = [writablePath stringByAppendingString:@"/"];
- NSArray *directoryContent = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:writablePath error:NULL];
- if (directoryContent) {
- return;
- }
-}
-
-- (IBAction)onRoutesBluetoothClick:(id)sender {
- [self hideRoutes:TRUE animated:TRUE];
- [CallManager.instance changeRouteToBluetooth];
-}
-
-- (IBAction)onRoutesEarpieceClick:(id)sender {
- [self hideRoutes:TRUE animated:TRUE];
- [CallManager.instance changeRouteToDefault];
-}
-
-- (IBAction)onRoutesSpeakerClick:(id)sender {
- [self hideRoutes:TRUE animated:TRUE];
- [CallManager.instance changeRouteToSpeaker];
-}
-
-- (IBAction)onRoutesClick:(id)sender {
- if ([_routesView isHidden]) {
- [self hideRoutes:FALSE animated:ANIMATED];
- } else {
- [self hideRoutes:TRUE animated:ANIMATED];
- }
-}
-
-- (IBAction)onOptionsClick:(id)sender {
- int confSize = linphone_core_get_conference_size(LC) - (CallManager.instance.isInConference ? 1 : 0);
- _optionsConferenceButton.enabled = (linphone_core_get_calls_nb(LC) > 1) && (linphone_core_get_calls_nb(LC) != confSize) && !CallManager.instance.hasConferenceAsGuest;
-
- if ([_optionsView isHidden]) {
- [self hideOptions:FALSE animated:ANIMATED];
- } else {
- [self hideOptions:TRUE animated:ANIMATED];
- }
-}
-
-- (IBAction)onOptionsTransferClick:(id)sender {
- [self hideOptions:TRUE animated:TRUE];
- DialerView *view = VIEW(DialerView);
- [view setAddress:@""];
- CallManager.instance.nextCallIsTransfer = TRUE;
- [PhoneMainView.instance changeCurrentView:view.compositeViewDescription];
-}
-
-- (IBAction)onOptionsAddClick:(id)sender {
- [self hideOptions:TRUE animated:TRUE];
- DialerView *view = VIEW(DialerView);
- [view setAddress:@""];
- CallManager.instance.nextCallIsTransfer = FALSE;
- [PhoneMainView.instance changeCurrentView:view.compositeViewDescription];
-}
-
-- (IBAction)onOptionsConferenceClick:(id)sender {
- [self hideOptions:TRUE animated:TRUE];
- [CallManager.instance groupCall];
-}
-
-#pragma mark - Animation
-
-- (void)hideAnimation:(BOOL)hidden forView:(UIView *)target completion:(void (^)(BOOL finished))completion {
- if (hidden) {
- int original_y = target.frame.origin.y;
- CGRect newFrame = target.frame;
- newFrame.origin.y = self.view.frame.size.height;
- [UIView animateWithDuration:0.5
- delay:0.0
- options:UIViewAnimationOptionCurveEaseIn
- animations:^{
- target.frame = newFrame;
- }
- completion:^(BOOL finished) {
- CGRect originFrame = target.frame;
- originFrame.origin.y = original_y;
- target.hidden = YES;
- target.frame = originFrame;
- if (completion)
- completion(finished);
- }];
- } else {
- CGRect frame = target.frame;
- int original_y = frame.origin.y;
- frame.origin.y = self.view.frame.size.height;
- target.frame = frame;
- frame.origin.y = original_y;
- target.hidden = NO;
-
- [UIView animateWithDuration:0.5
- delay:0.0
- options:UIViewAnimationOptionCurveEaseOut
- animations:^{
- target.frame = frame;
- }
- completion:^(BOOL finished) {
- target.frame = frame; // in case application did not finish
- if (completion)
- completion(finished);
- }];
- }
-}
-
-#pragma mark - Bounce
-- (void)messageReceived:(NSNotification *)notif {
- [self updateUnreadMessage:TRUE];
-}
-- (void)updateUnreadMessage:(BOOL)appear {
- int unreadMessage = [LinphoneManager unreadMessageCount];
- if (unreadMessage > 0) {
- _chatNotificationLabel.text = [NSString stringWithFormat:@"%i", unreadMessage];
- [_chatNotificationView startAnimating:appear];
- } else {
- [_chatNotificationView stopAnimating:appear];
- }
-}
-
-#pragma mark - Conference
-
-- (void)participantListChanged:(NSNotification *)notif {
- [self confStateChanged:nil];
- [_conferenceCallsTable update];
- _conferenceView.frame = CGRectMake(_conferenceView.frame.origin.x,_conferenceView.frame.origin.y,_conferenceView.frame.size.width,_conferenceCallsTable.tableView.frame.origin.y+[_conferenceCallsTable.tableView numberOfRowsInSection:0]*CONFERENCE_CELL_HEIGHT+10);
- [self onCurrentCallChange];
- _conferenceView.hidden = !CallManager.instance.isInConference;
-}
-
-- (void)confStateChanged:(NSNotification *)notif {
- dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
- if ([CallManager.instance inVideoConf]) {
- [self displayVideoCall:true];
- } else if (CallManager.instance.isInConference) {
- [self displayAudioConference];
- } else {
- [self displayAudioCall:true];
- _callPauseButton.hidden = NO;
- _nameLabel.hidden = NO;
- _durationLabel.hidden = NO;
- _avatarImage.hidden = NO;
- }
- [_conferenceCallsTable update];
- _conferenceView.frame = CGRectMake(_conferenceView.frame.origin.x,_conferenceView.frame.origin.y,_conferenceView.frame.size.width,_conferenceCallsTable.tableView.frame.origin.y+[_conferenceCallsTable.tableView numberOfRowsInSection:0]*CONFERENCE_CELL_HEIGHT+10);
- });
-}
-
--(void) displayAudioConference {
- _callPauseButton.hidden = true;
- _nameLabel.hidden = true;
- _conferenceView.frame = CGRectMake(_conferenceView.frame.origin.x,_conferenceView.frame.origin.y,_conferenceView.frame.size.width,_conferenceCallsTable.tableView.frame.origin.y+[_conferenceCallsTable.tableView numberOfRowsInSection:0]*CONFERENCE_CELL_HEIGHT+10);
- _durationLabel.hidden = true;
- _avatarImage.hidden = true;
-
- [_conferenceView removeFromSuperview];
- [_callView addSubview:_conferenceView];
-
- if ([CallManager.instance isInConference]) {
- [_conferenceView removeFromSuperview];
- [_callView addSubview:_conferenceView];
- _conferenceView.hidden = NO;
- } else {
- [_conferenceView removeFromSuperview];
- [self.view addSubview:_conferenceView];
- [self.view sendSubviewToBack:_conferenceView];
- }
-}
-
-
-
-@end
diff --git a/Classes/ChatConversationCreateView.h b/Classes/ChatConversationCreateView.h
index 557035c29..8c8f271ea 100644
--- a/Classes/ChatConversationCreateView.h
+++ b/Classes/ChatConversationCreateView.h
@@ -45,6 +45,11 @@
@property(nonatomic) Boolean isGroupChat;
@property(nonatomic) Boolean isEncrypted;
+@property(nonatomic) Boolean isForVoipConference;
+@property(nonatomic) Boolean isForOngoingVoipConference;
+
+@property (weak, nonatomic) IBOutlet UILabel *voipTitle;
+
- (IBAction)onBackClick:(id)sender;
- (IBAction)onNextClick:(id)sender;
- (IBAction)onChiffreClick:(id)sender;
diff --git a/Classes/ChatConversationCreateView.m b/Classes/ChatConversationCreateView.m
index 04e99b5df..30607e83f 100644
--- a/Classes/ChatConversationCreateView.m
+++ b/Classes/ChatConversationCreateView.m
@@ -20,6 +20,7 @@
#import "ChatConversationCreateView.h"
#import "PhoneMainView.h"
#import "UIChatCreateCollectionViewCell.h"
+#import "linphoneapp-Swift.h"
@implementation ChatConversationCreateView
@@ -81,6 +82,22 @@ static UICompositeViewDescription *compositeDescription = nil;
frame.origin.x = _linphoneButton.frame.origin.x;
_allButton.frame = frame;
}
+
+
+ if (_isForVoipConference) {
+ _switchView.hidden = true;
+ _chiffreOptionView.hidden = true;
+ _voipTitle.hidden = false;
+ if (_isForOngoingVoipConference) {
+ [_nextButton setImage:[UIImage imageNamed:@"valid_default"] forState:UIControlStateNormal];
+ } else {
+ [_nextButton setImage:[UIImage imageNamed:@"next_default"] forState:UIControlStateNormal];
+ }
+ } else {
+ _voipTitle.hidden = true;
+ [_nextButton setImage:[UIImage imageNamed:@"next_default"] forState:UIControlStateNormal];
+ }
+
}
- (void)viewUpdateEvent:(NSNotification *)notif {
@@ -153,11 +170,21 @@ static UICompositeViewDescription *compositeDescription = nil;
}
- (IBAction)onNextClick:(id)sender {
- ChatConversationInfoView *view = VIEW(ChatConversationInfoView);
- view.contacts = _tableController.contactsGroup;
- view.create = !_isForEditing;
- view.encrypted = _isEncrypted;
- [PhoneMainView.instance changeCurrentView:view.compositeViewDescription];
+ if (_isForVoipConference) {
+ if (_isForOngoingVoipConference) {
+ [PhoneMainView.instance changeCurrentView:VIEW(ActiveCallOrConferenceView).compositeViewDescription];
+ [ConferenceViewModelBridge updateParticipantsListWithAddresses:_tableController.contactsGroup];
+ } else {
+ [PhoneMainView.instance changeCurrentView:VIEW(ConferenceSchedulingSummaryView).compositeViewDescription];
+ [VIEW(ConferenceSchedulingSummaryView) setParticipantsWithAddresses:_tableController.contactsGroup];
+ }
+ } else {
+ ChatConversationInfoView *view = VIEW(ChatConversationInfoView);
+ view.contacts = _tableController.contactsGroup;
+ view.create = !_isForEditing;
+ view.encrypted = _isEncrypted;
+ [PhoneMainView.instance changeCurrentView:view.compositeViewDescription];
+ }
}
- (IBAction)onChiffreClick:(id)sender {
diff --git a/Classes/ChatConversationImdnView.h b/Classes/ChatConversationImdnView.h
index 78a9042b6..26607f58d 100644
--- a/Classes/ChatConversationImdnView.h
+++ b/Classes/ChatConversationImdnView.h
@@ -24,6 +24,7 @@
#import "UICompositeView.h"
#import "UIRoundBorderedButton.h"
+#import "UIChatBubbleTextCell.h"
@interface ChatConversationImdnView : UIViewController
{
diff --git a/Classes/ChatConversationTableView.m b/Classes/ChatConversationTableView.m
index 198569cbd..53d27638e 100644
--- a/Classes/ChatConversationTableView.m
+++ b/Classes/ChatConversationTableView.m
@@ -24,6 +24,7 @@
#import "UIChatBubblePhotoCell.h"
#import "UIChatNotifiedEventCell.h"
#import "PhoneMainView.h"
+#import "linphoneapp-Swift.h"
@implementation ChatConversationTableView
@@ -335,7 +336,8 @@ static const int BASIC_EVENT_LIST=15;
LinphoneEventLog *event = [[eventList objectAtIndex:indexPath.row] pointerValue];
if (linphone_event_log_get_type(event) == LinphoneEventLogTypeConferenceChatMessage) {
LinphoneChatMessage *chat = linphone_event_log_get_chat_message(event);
- if (linphone_chat_message_get_file_transfer_information(chat) || linphone_chat_message_get_external_body_url(chat))
+ BOOL isConferenceIcs = [ICSBubbleView isConferenceInvitationMessageWithCmessage:chat];
+ if (!isConferenceIcs && (linphone_chat_message_get_file_transfer_information(chat) || linphone_chat_message_get_external_body_url(chat)))
kCellId = NSStringFromClass(UIChatBubblePhotoCell.class);
else
kCellId = NSStringFromClass(UIChatBubbleTextCell.class);
@@ -382,14 +384,12 @@ static const CGFloat MESSAGE_SPACING_PERCENTAGE = 1.f;
LinphoneEventLog *event = [[eventList objectAtIndex:indexPath.row] pointerValue];
if (linphone_event_log_get_type(event) == LinphoneEventLogTypeConferenceChatMessage) {
LinphoneChatMessage *chat = linphone_event_log_get_chat_message(event);
-
- //If the message is followed by another one that is not from the same address, we add a little space under it
- CGFloat height = 0;
- if ([self isLastIndexInTableView:indexPath chat:chat])
- height += tableView.frame.size.height * MESSAGE_SPACING_PERCENTAGE / 100;
- if (![self isFirstIndexInTableView:indexPath chat:chat])
- height -= 20;
-
+ //If the message is followed by another one that is not from the same address, we add a little space under it
+ CGFloat height = 0;
+ if ([self isLastIndexInTableView:indexPath chat:chat])
+ height += tableView.frame.size.height * MESSAGE_SPACING_PERCENTAGE / 100;
+ if (![self isFirstIndexInTableView:indexPath chat:chat])
+ height -= 20;
return [UIChatBubbleTextCell ViewHeightForMessage:chat withWidth:self.view.frame.size.width].height + height;
}
return [UIChatNotifiedEventCell height];
diff --git a/Classes/ChatConversationView.h b/Classes/ChatConversationView.h
index b38d4b8f5..89965a457 100644
--- a/Classes/ChatConversationView.h
+++ b/Classes/ChatConversationView.h
@@ -81,7 +81,6 @@
@property(nonatomic, strong) IBOutlet UIButton *pictureButton;
@property(weak, nonatomic) IBOutlet UIButton *callButton;
@property(weak, nonatomic) IBOutlet UIBackToCallButton *backToCallButton;
-@property (weak, nonatomic) IBOutlet UIButton *infoButton;
@property (weak, nonatomic) IBOutlet UILabel *particpantsLabel;
@property NSMutableArray *qualitySettingsArray;
@property (weak, nonatomic) IBOutlet UICollectionView *imagesCollectionView;
@@ -161,4 +160,6 @@
-(void) initiateReplyViewForMessage:(LinphoneChatMessage *)message;
+-(void) stopVoiceRecording;
+
@end
diff --git a/Classes/ChatConversationView.m b/Classes/ChatConversationView.m
index ab1bf2877..f37ad3258 100644
--- a/Classes/ChatConversationView.m
+++ b/Classes/ChatConversationView.m
@@ -659,11 +659,32 @@ static UICompositeViewDescription *compositeDescription = nil;
}];
}
+- (BOOL) groupCallAvailable {
+ if (isOneToOne || !_backToCallButton.hidden || _tableController.tableView.isEditing)
+ return false;
+ LinphoneAccount *account = linphone_core_get_default_account(LC);
+ if (!account)
+ return false;
+ const LinphoneAccountParams *params = linphone_account_get_params(account);
+ if (!params)
+ return false;
+ return linphone_account_params_get_audio_video_conference_factory_address(params) != nil || linphone_account_params_get_conference_factory_uri(params) != nil;
+
+}
+
- (void)updateSuperposedButtons {
[_backToCallButton update];
- _infoButton.hidden = (isOneToOne|| !_backToCallButton.hidden || _tableController.tableView.isEditing);
- _callButton.hidden = !_backToCallButton.hidden || !_infoButton.hidden || _tableController.tableView.isEditing;
+ _callButton.hidden = !_backToCallButton.hidden || _tableController.tableView.isEditing;
_toggleMenuButton.hidden = _tableController.isEditing;
+
+ // Group call :
+ if (self.groupCallAvailable ) {
+ [_callButton setImage: [LinphoneUtils resizeImage:[UIImage imageNamed:@"voip_conference_new"] newSize:CGSizeMake(50, 50)] forState:UIControlStateNormal];
+ _callButton.hidden = false;
+ } else {
+ [_callButton setImage:[UIImage imageNamed:@"call_alt_start_default"] forState:UIControlStateNormal];
+ }
+
}
- (void)updateParticipantLabel {
@@ -861,7 +882,20 @@ static UICompositeViewDescription *compositeDescription = nil;
bctbx_list_t *participants = linphone_chat_room_get_participants(_chatRoom);
LinphoneParticipant *firstParticipant = participants ? (LinphoneParticipant *)participants->data : NULL;
const LinphoneAddress *addr = firstParticipant ? linphone_participant_get_address(firstParticipant) : linphone_chat_room_get_peer_address(_chatRoom);
- [LinphoneManager.instance call:addr];
+ if (self.groupCallAvailable) {
+ UIConfirmationDialog *d = [UIConfirmationDialog ShowWithMessage:VoipTexts.conference_start_group_call_dialog_message
+ cancelMessage:nil
+ confirmMessage:VoipTexts.conference_start_group_call_dialog_ok_button
+ onCancelClick:^() {}
+ onConfirmationClick:^() {
+ [ConferenceViewModelBridge startGroupCallWithCChatRoom:_chatRoom];
+ }];
+ d.groupCallImage.hidden = NO;
+ [d.groupCallImage setImageNamed:@"voip_conference_new" tintColor:UIColor.whiteColor];
+ [d setSpecialColor];
+ [d setWhiteCancel];
+ } else
+ [LinphoneManager.instance call:addr];
}
- (IBAction)onListSwipe:(id)sender {
@@ -1814,7 +1848,7 @@ void on_chat_room_conference_alert(LinphoneChatRoom *cr, const LinphoneEventLog
cell.textLabel.text = NSLocalizedString(@"Go to contact",nil);
}
} else {
- cell.imageView.image = [LinphoneUtils resizeImage:[UIImage imageNamed:@"chat_group_informations.png"] newSize:CGSizeMake(20, 25)];
+ cell.imageView.image = [LinphoneUtils resizeImage:[UIImage imageNamed:@"chat_group_informations.png"] newSize:CGSizeMake(25, 25)];
cell.textLabel.text = NSLocalizedString(@"Group infos",nil);
}
}
@@ -1847,7 +1881,7 @@ void on_chat_room_conference_alert(LinphoneChatRoom *cr, const LinphoneEventLog
if ((isEncrypted && ((!canEphemeral && indexPath.row == 4)||(canEphemeral && indexPath.row == 5)))
|| (!isEncrypted && indexPath.row == 3)) {
- cell.imageView.image = [LinphoneUtils resizeImage:[UIImage imageNamed:@"chat_group_informations.png"] newSize:CGSizeMake(20, 25)];
+ cell.imageView.image = [LinphoneUtils resizeImage:[UIImage imageNamed:@"chat_group_informations.png"] newSize:CGSizeMake(25, 25)];
cell.textLabel.text = NSLocalizedString(@"Show address and identity",nil);
}
diff --git a/Classes/DevicesListView.m b/Classes/DevicesListView.m
index 6216db51e..cd2b9e229 100644
--- a/Classes/DevicesListView.m
+++ b/Classes/DevicesListView.m
@@ -150,7 +150,7 @@ static UICompositeViewDescription *compositeDescription = nil;
[_tableView reloadData];
} else {
const LinphoneAddress *addr = linphone_participant_device_get_address(entry->device);
- [CallManager.instance startCallWithAddr:(LinphoneAddress *)addr isSas:TRUE];
+ [CallManager.instance startCallWithAddr:(LinphoneAddress *)addr isSas:TRUE isVideo:false isConference:false];
}
} else {
bctbx_list_t *devices = linphone_participant_get_devices(entry->participant);
diff --git a/Classes/DialerView.m b/Classes/DialerView.m
index 9bbde008d..eca3e9a8c 100644
--- a/Classes/DialerView.m
+++ b/Classes/DialerView.m
@@ -21,6 +21,8 @@
#import "LinphoneManager.h"
#import "PhoneMainView.h"
+#import "linphoneapp-Swift.h"
+
@implementation DialerView
@@ -143,6 +145,9 @@ static UICompositeViewDescription *compositeDescription = nil;
[_videoCameraSwitch setHidden:FALSE];
}
}
+ [_addContactButton setImage:[UIImage imageNamed:@"voip_conference_new"] forState:UIControlStateNormal];
+ _addContactButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
+ _addContactButton.enabled = true;
}
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
@@ -170,11 +175,10 @@ static UICompositeViewDescription *compositeDescription = nil;
_padView.hidden = !IPAD && UIInterfaceOrientationIsLandscape(toInterfaceOrientation);
if (linphone_core_get_calls_nb(LC)) {
_backButton.hidden = FALSE;
- _addContactButton.hidden = TRUE;
} else {
_backButton.hidden = TRUE;
- _addContactButton.hidden = FALSE;
}
+ _addContactButton.hidden = FALSE;
}
- (void)viewDidAppear:(BOOL)animated {
@@ -388,24 +392,19 @@ static UICompositeViewDescription *compositeDescription = nil;
#pragma mark - Action Functions
- (IBAction)onAddContactClick:(id)event {
- [ContactSelection setSelectionMode:ContactSelectionModeEdit];
- [ContactSelection setAddAddress:[_addressField text]];
- [ContactSelection enableSipFilter:FALSE];
- [PhoneMainView.instance changeCurrentView:ContactsListView.compositeViewDescription];
+ ConferenceSchedulingView *view = VIEW(ConferenceSchedulingView);
+ [view resetViewModel];
+ [PhoneMainView.instance changeCurrentView:ConferenceSchedulingView.compositeViewDescription];
}
- (IBAction)onBackClick:(id)event {
- [PhoneMainView.instance popToView:CallView.compositeViewDescription];
+ [PhoneMainView.instance popToView:ActiveCallOrConferenceView.compositeViewDescription];
}
- (IBAction)onAddressChange:(id)sender {
if ([self displayDebugPopup:_addressField.text]) {
_addressField.text = @"";
}
- _addContactButton.enabled = _backspaceButton.enabled = ([[_addressField text] length] > 0);
- if ([_addressField.text length] == 0) {
- [self.view endEditing:YES];
- }
}
- (IBAction)onBackspaceClick:(id)sender {
diff --git a/Classes/HistoryDetailsView.m b/Classes/HistoryDetailsView.m
index 5b676ec25..79aa9fa91 100644
--- a/Classes/HistoryDetailsView.m
+++ b/Classes/HistoryDetailsView.m
@@ -138,6 +138,7 @@ static UICompositeViewDescription *compositeDescription = nil;
_addContactButton.hidden = YES;
return;
}
+
_emptyLabel.hidden = YES;
const LinphoneAddress *addr = linphone_call_log_get_remote_address(callLog);
diff --git a/Classes/HistoryListTableView.h b/Classes/HistoryListTableView.h
index 4255763b5..2f9af3fc5 100644
--- a/Classes/HistoryListTableView.h
+++ b/Classes/HistoryListTableView.h
@@ -25,7 +25,11 @@
}
@property(nonatomic, assign) BOOL missedFilter;
+@property(nonatomic, assign) BOOL confFilter;
+
@property(strong, nonatomic) NSMutableDictionary *sections;
@property(strong, nonatomic) NSMutableArray *sortedDays;
+
+- (void)removeFIlters;
@end
diff --git a/Classes/HistoryListTableView.m b/Classes/HistoryListTableView.m
index 5eb719098..9ce13bdd4 100644
--- a/Classes/HistoryListTableView.m
+++ b/Classes/HistoryListTableView.m
@@ -22,15 +22,18 @@
#import "LinphoneManager.h"
#import "PhoneMainView.h"
#import "Utils.h"
+#import "linphoneapp-Swift.h"
+
@implementation HistoryListTableView
-@synthesize missedFilter;
+@synthesize missedFilter,confFilter;
#pragma mark - Lifecycle Functions
- (void)initHistoryTableViewController {
missedFilter = false;
+ confFilter = false;
}
- (id)init {
@@ -102,9 +105,30 @@
return;
}
missedFilter = amissedFilter;
+ if (missedFilter) {
+ confFilter = false;
+ }
[self loadData];
}
+- (void)setConfFilter:(BOOL)aconfFilter {
+ if (confFilter == aconfFilter) {
+ return;
+ }
+ confFilter = aconfFilter;
+ if (confFilter) {
+ missedFilter = false;
+ }
+ [self loadData];
+}
+
+- (void)removeFIlters {
+ confFilter = false;
+ missedFilter = false;
+ [self loadData];
+}
+
+
#pragma mark - UITableViewDataSource Functions
- (NSDate *)dateAtBeginningOfDayForDate:(NSDate *)inputDate {
@@ -129,7 +153,8 @@
self.sections = [NSMutableDictionary dictionary];
while (logs != NULL) {
LinphoneCallLog *log = (LinphoneCallLog *)logs->data;
- if (!missedFilter || linphone_call_log_get_status(log) == LinphoneCallMissed) {
+ BOOL keepIt = (!missedFilter || linphone_call_log_get_status(log) == LinphoneCallMissed) && (!confFilter||linphone_call_log_was_conference(log)) ;
+ if (keepIt) {
NSDate *startDate = [self
dateAtBeginningOfDayForDate:[NSDate
dateWithTimeIntervalSince1970:linphone_call_log_get_start_date(log)]];
@@ -143,7 +168,7 @@
// if this contact was already the previous entry, do not add it twice
LinphoneCallLog *prev = [eventsOnThisDay lastObject] ? [[eventsOnThisDay lastObject] pointerValue] : NULL;
- if (prev && linphone_address_weak_equal(linphone_call_log_get_remote_address(prev),
+ if (!linphone_call_log_was_conference(log) && prev && linphone_address_weak_equal(linphone_call_log_get_remote_address(prev),
linphone_call_log_get_remote_address(log))) {
bctbx_list_t *list = linphone_call_log_get_user_data(prev);
list = bctbx_list_append(list, linphone_call_log_ref(log));
@@ -241,8 +266,15 @@
UIHistoryCell *cell = (UIHistoryCell *)[self tableView:tableView cellForRowAtIndexPath:indexPath];
[cell onDetails:self];
} else {
- const LinphoneAddress *addr = linphone_call_log_get_remote_address(callLog);
- [LinphoneManager.instance call:addr];
+ if (linphone_call_log_was_conference(callLog)) {
+ LinphoneConferenceInfo *confInfo = linphone_call_log_get_conference_info(callLog);
+ ConferenceWaitingRoomFragment *view = VIEW(ConferenceWaitingRoomFragment);
+ [view setDetailsWithSubject:[NSString stringWithUTF8String:linphone_conference_info_get_subject(confInfo)] url:[NSString stringWithUTF8String:linphone_address_as_string(linphone_conference_info_get_uri(confInfo))]];
+ [PhoneMainView.instance changeCurrentView:ConferenceWaitingRoomFragment.compositeViewDescription];
+ } else {
+ const LinphoneAddress *addr = linphone_call_log_get_remote_address(callLog);
+ [LinphoneManager.instance call:addr];
+ }
}
}
}
diff --git a/Classes/HistoryListView.h b/Classes/HistoryListView.h
index 87118e2de..9e1a176bb 100644
--- a/Classes/HistoryListView.h
+++ b/Classes/HistoryListView.h
@@ -31,6 +31,7 @@
@property(nonatomic, strong) IBOutlet UIButton *allButton;
@property(nonatomic, strong) IBOutlet UIButton *missedButton;
+@property (weak, nonatomic) IBOutlet UIInterfaceStyleButton *conferenceButton;
@property(weak, nonatomic) IBOutlet UIImageView *selectedButtonImage;
@property (weak, nonatomic) IBOutlet UIInterfaceStyleButton *toggleSelectionButton;
diff --git a/Classes/HistoryListView.m b/Classes/HistoryListView.m
index efb6cc5ff..7c3e96de6 100644
--- a/Classes/HistoryListView.m
+++ b/Classes/HistoryListView.m
@@ -23,7 +23,7 @@
@implementation HistoryListView
-typedef enum _HistoryView { History_All, History_Missed, History_MAX } HistoryView;
+typedef enum _HistoryView { History_All, History_Missed, History_Conference, History_MAX } HistoryView;
#pragma mark - UICompositeViewDelegate Functions
@@ -48,6 +48,11 @@ static UICompositeViewDescription *compositeDescription = nil;
#pragma mark - ViewController Functions
+-(void) viewDidLoad {
+ [super viewDidLoad];
+ _conferenceButton.imageView.contentMode = UIViewContentModeScaleAspectFit;
+}
+
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
@@ -70,18 +75,27 @@ static UICompositeViewDescription *compositeDescription = nil;
#pragma mark -
+
- (void)changeView:(HistoryView)view {
CGRect frame = _selectedButtonImage.frame;
if (view == History_All) {
frame.origin.x = _allButton.frame.origin.x;
_allButton.selected = TRUE;
- [_tableController setMissedFilter:FALSE];
+ [_tableController removeFIlters];
_missedButton.selected = FALSE;
+ _conferenceButton.selected = false;
+ } else if (view == History_Conference) {
+ frame.origin.x = _conferenceButton.frame.origin.x;
+ _conferenceButton.selected = TRUE;
+ [_tableController setConfFilter:true];
+ _missedButton.selected = FALSE;
+ _allButton.selected = FALSE;
} else {
frame.origin.x = _missedButton.frame.origin.x;
_missedButton.selected = TRUE;
[_tableController setMissedFilter:TRUE];
_allButton.selected = FALSE;
+ _conferenceButton.selected = false;
}
_selectedButtonImage.frame = frame;
}
@@ -96,6 +110,10 @@ static UICompositeViewDescription *compositeDescription = nil;
[self changeView:History_Missed];
}
+- (IBAction)onConferenceClick:(id)sender {
+ [self changeView:History_Conference];
+}
+
- (IBAction)onDeleteClick:(id)event {
NSString *msg = [NSString stringWithFormat:NSLocalizedString(@"Do you want to delete selected logs?", nil)];
[UIConfirmationDialog ShowWithMessage:msg
diff --git a/Classes/LinphoneAppDelegate.m b/Classes/LinphoneAppDelegate.m
index 1bcb4f7a6..4d904dfa5 100644
--- a/Classes/LinphoneAppDelegate.m
+++ b/Classes/LinphoneAppDelegate.m
@@ -17,7 +17,6 @@
* along with this program. If not, see .
*/
-#import "linphoneapp-Swift.h"
#import "LinphoneAppDelegate.h"
#import "ContactDetailsView.h"
#import "ContactsListView.h"
@@ -138,7 +137,7 @@
if ((floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_9_x_Max)) {
if ([LinphoneManager.instance lpConfigBoolForKey:@"autoanswer_notif_preference"]) {
linphone_call_accept(call);
- [PhoneMainView.instance changeCurrentView:CallView.compositeViewDescription];
+ [PhoneMainView.instance changeCurrentView:ActiveCallOrConferenceView.compositeViewDescription];
} else {
[PhoneMainView.instance displayIncomingCall:call];
}
@@ -332,6 +331,8 @@
return NO;
}
+ [PhoneMainView.instance.mainViewController getCachedController:ActiveCallOrConferenceView.compositeViewDescription.name]; // This will create the single instance of the ActiveCallOrConferenceView including listeneres
+
return YES;
}
@@ -422,13 +423,6 @@
// used for callkit. Called when active video.
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler
{
-
-
- if ([userActivity.activityType isEqualToString:@"INStartVideoCallIntent"]) {
- LOGI(@"CallKit: satrt video.");
- CallView *view = VIEW(CallView);
- [view.videoButton setOn];
- }
if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"]) { // tel URI handler.
INInteraction *interaction = userActivity.interaction;
INStartAudioCallIntent *startAudioCallIntent = (INStartAudioCallIntent *)interaction.intent;
@@ -552,7 +546,7 @@
if ([response.actionIdentifier isEqual:@"Answer"]) {
// use the standard handler
- [PhoneMainView.instance changeCurrentView:CallView.compositeViewDescription];
+ [CallManager.instance acceptCallWithCall:call hasVideo:NO];
linphone_call_accept(call);
} else if ([response.actionIdentifier isEqual:@"Decline"]) {
linphone_call_decline(call, LinphoneReasonDeclined);
@@ -590,7 +584,6 @@
return;
[[UNUserNotificationCenter currentNotificationCenter] removeAllDeliveredNotifications];
- [PhoneMainView.instance changeCurrentView:CallView.compositeViewDescription];
[CallManager.instance acceptVideoWithCall:call confirm:TRUE];
} else if ([response.actionIdentifier isEqual:@"Confirm"]) {
if (linphone_core_get_current_call(LC) == call)
@@ -623,7 +616,7 @@
}
} else if ([response.notification.request.content.categoryIdentifier isEqual:@"video_request"]) {
if (!call) return;
- [PhoneMainView.instance changeCurrentView:CallView.compositeViewDescription];
+ [PhoneMainView.instance changeCurrentView:ActiveCallOrConferenceView.compositeViewDescription];
NSTimer *videoDismissTimer = nil;
UIConfirmationDialog *sheet = [UIConfirmationDialog ShowWithMessage:response.notification.request.content.body
cancelMessage:nil
@@ -707,8 +700,7 @@
if ([notification.category isEqualToString:@"incoming_call"]) {
if ([identifier isEqualToString:@"answer"]) {
// use the standard handler
- [PhoneMainView.instance changeCurrentView:CallView.compositeViewDescription];
- linphone_call_accept(call);
+ [CallManager.instance acceptCallWithCall:call hasVideo:NO];
} else if ([identifier isEqualToString:@"decline"]) {
LinphoneCall *call = linphone_core_get_current_call(LC);
if (call)
@@ -745,8 +737,7 @@
if ([notification.category isEqualToString:@"incoming_call"]) {
if ([identifier isEqualToString:@"answer"]) {
// use the standard handler
- [PhoneMainView.instance changeCurrentView:CallView.compositeViewDescription];
- linphone_call_accept(call);
+ [CallManager.instance acceptCallWithCall:call hasVideo:NO];
} else if ([identifier isEqualToString:@"decline"]) {
LinphoneCall *call = linphone_core_get_current_call(LC);
if (call)
diff --git a/Classes/LinphoneCoreSettingsStore.m b/Classes/LinphoneCoreSettingsStore.m
index dab9ae09b..72dae4570 100644
--- a/Classes/LinphoneCoreSettingsStore.m
+++ b/Classes/LinphoneCoreSettingsStore.m
@@ -407,6 +407,8 @@
{
[self setBool:[lm lpConfigBoolForKey:@"use_device_ringtone"] forKey:@"use_device_ringtone"];
+ [self setBool:linphone_core_is_record_aware_enabled(LC) forKey:@"record_aware"];
+
[self setBool:linphone_core_get_use_info_for_dtmf(LC) forKey:@"sipinfo_dtmf_preference"];
[self setBool:linphone_core_get_use_rfc2833_for_dtmf(LC) forKey:@"rfc_dtmf_preference"];
@@ -931,7 +933,9 @@
linphone_core_set_use_rfc2833_for_dtmf(LC, [self boolForKey:@"rfc_dtmf_preference"]);
[lm lpConfigSetBool:[self boolForKey:@"use_device_ringtone"] forKey:@"use_device_ringtone"];
[ProviderDelegate resetSharedProviderConfiguration];
-
+
+ linphone_core_set_record_aware_enabled(LC, [self boolForKey:@"record_aware"]);
+
linphone_core_set_use_info_for_dtmf(LC, [self boolForKey:@"sipinfo_dtmf_preference"]);
linphone_core_set_inc_timeout(LC, [self integerForKey:@"incoming_call_timeout_preference"]);
linphone_core_set_in_call_timeout(LC, [self integerForKey:@"in_call_timeout_preference"]);
diff --git a/Classes/LinphoneManager.m b/Classes/LinphoneManager.m
index 17fcdacb9..06a9b8fef 100644
--- a/Classes/LinphoneManager.m
+++ b/Classes/LinphoneManager.m
@@ -32,7 +32,6 @@
#import "LinphoneCoreSettingsStore.h"
#import "LinphoneAppDelegate.h"
#import "LinphoneManager.h"
-#import "Utils/AudioHelper.h"
#import "Utils/FileTransferDelegate.h"
#include "linphone/factory.h"
@@ -482,6 +481,20 @@ static int check_should_migrate_images(void *data, int argc, char **argv, char *
linphone_account_set_params(account, newAccountParams);
}
}
+ if (!linphone_account_params_get_audio_video_conference_factory_address(newAccountParams) && strcmp(appDomain.UTF8String, linphone_account_params_get_domain(newAccountParams)) == 0) {
+ NSString *uri = [self lpConfigStringForKey:@"default_audio_video_conference_factory_uri" withDefault:@"sip:videoconference-factory2@sip.linphone.org"];
+ LinphoneAddress *a = linphone_factory_create_address(linphone_factory_get(), uri.UTF8String);
+ if (a) {
+ linphone_account_params_set_audio_video_conference_factory_address(newAccountParams, a);
+ linphone_account_set_params(account, newAccountParams);
+ }
+ }
+
+ if (strcmp(appDomain.UTF8String, linphone_account_params_get_domain(newAccountParams)) == 0 && !linphone_account_params_rtp_bundle_enabled(newAccountParams)) {
+ linphone_account_params_enable_rtp_bundle(newAccountParams, true);
+ linphone_account_set_params(account,newAccountParams);
+ }
+
linphone_account_params_unref(newAccountParams);
accounts = accounts->next;
}
@@ -848,7 +861,7 @@ static void linphone_iphone_popup_password_request(LinphoneCore *lc, LinphoneAut
if ((linphone_core_get_max_size_for_auto_download_incoming_files(LC) > -1) && linphone_chat_message_get_file_transfer_information(msg))
hasFile = TRUE;
- if (!linphone_chat_message_is_file_transfer(msg) && !linphone_chat_message_is_text(msg) && !hasFile)
+ if (!linphone_chat_message_is_file_transfer(msg) && !linphone_chat_message_is_text(msg) && !hasFile && ![ICSBubbleView isConferenceInvitationMessageWithCmessage:msg])
return;
if (hasFile) {
@@ -1183,6 +1196,8 @@ static void linphone_iphone_is_composing_received(LinphoneCore *lc, LinphoneChat
[NSNotificationCenter.defaultCenter postNotificationName:kLinphoneCoreUpdate
object:LinphoneManager.instance
userInfo:dict];
+
+
}
static BOOL libStarted = FALSE;
@@ -1869,7 +1884,7 @@ static int comp_call_state_paused(const LinphoneCall *call, const void *param) {
}
[self checkLocalNetworkPermission];
// For OutgoingCall, show CallOutgoingView
- [CallManager.instance startCallWithAddr:iaddr isSas:FALSE];
+ [CallManager.instance startCallWithAddr:iaddr isSas:FALSE isVideo:false isConference:false];
}
#pragma mark - Misc Functions
diff --git a/Classes/LinphoneUI/Base.lproj/UICallConferenceCell.xib b/Classes/LinphoneUI/Base.lproj/UICallConferenceCell.xib
deleted file mode 100644
index a64c1fc9a..000000000
--- a/Classes/LinphoneUI/Base.lproj/UICallConferenceCell.xib
+++ /dev/null
@@ -1,76 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Classes/LinphoneUI/Base.lproj/UICallPausedCell.xib b/Classes/LinphoneUI/Base.lproj/UICallPausedCell.xib
deleted file mode 100644
index 423378a9e..000000000
--- a/Classes/LinphoneUI/Base.lproj/UICallPausedCell.xib
+++ /dev/null
@@ -1,80 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Classes/LinphoneUI/Base.lproj/UIConfirmationDialog.xib b/Classes/LinphoneUI/Base.lproj/UIConfirmationDialog.xib
index eb265f061..231454a0a 100644
--- a/Classes/LinphoneUI/Base.lproj/UIConfirmationDialog.xib
+++ b/Classes/LinphoneUI/Base.lproj/UIConfirmationDialog.xib
@@ -1,9 +1,9 @@
-
+
-
+
@@ -14,6 +14,7 @@
+
@@ -93,6 +94,10 @@
+
+
+
+
@@ -111,5 +116,6 @@
+
diff --git a/Classes/LinphoneUI/TabBarView.m b/Classes/LinphoneUI/TabBarView.m
index 6048f1a39..76ae20cc0 100644
--- a/Classes/LinphoneUI/TabBarView.m
+++ b/Classes/LinphoneUI/TabBarView.m
@@ -19,6 +19,7 @@
#import "TabBarView.h"
#import "PhoneMainView.h"
+#import "linphoneapp-Swift.h"
@implementation TabBarView
@@ -99,7 +100,8 @@
- (void)updateSelectedButton:(UICompositeViewDescription *)view {
_historyButton.selected = [view equal:HistoryListView.compositeViewDescription] ||
- [view equal:HistoryDetailsView.compositeViewDescription];
+ [view equal:HistoryDetailsView.compositeViewDescription] ||
+ [view equal:ConferenceHistoryDetailsView.compositeViewDescription];
_contactsButton.selected = [view equal:ContactsListView.compositeViewDescription] ||
[view equal:ContactDetailsView.compositeViewDescription];
_dialerButton.selected = [view equal:DialerView.compositeViewDescription];
diff --git a/Classes/LinphoneUI/UIBackToCallButton.m b/Classes/LinphoneUI/UIBackToCallButton.m
index 60c9f4df4..df44ff54f 100644
--- a/Classes/LinphoneUI/UIBackToCallButton.m
+++ b/Classes/LinphoneUI/UIBackToCallButton.m
@@ -20,6 +20,8 @@
#import "UIBackToCallButton.h"
#import "LinphoneManager.h"
#import "PhoneMainView.h"
+#import "linphoneapp-Swift.h"
+
@implementation UIBackToCallButton
@@ -46,7 +48,7 @@
}
- (IBAction)onBackToCallClick:(id)sender {
- [PhoneMainView.instance popToView:CallView.compositeViewDescription];
+ [PhoneMainView.instance popToView:ActiveCallOrConferenceView.compositeViewDescription];
}
@end
diff --git a/Classes/LinphoneUI/UICallConferenceCell.h b/Classes/LinphoneUI/UICallConferenceCell.h
deleted file mode 100644
index cf59dfe3c..000000000
--- a/Classes/LinphoneUI/UICallConferenceCell.h
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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 .
- */
-
-#import "UIRoundedImageView.h"
-#import "LinphoneManager.h"
-#import "UIInterfaceStyleButton.h"
-
-#define CONFERENCE_CELL_HEIGHT 60
-
-@interface UICallConferenceCell : UITableViewCell
-
-@property(weak, nonatomic) IBOutlet UIRoundedImageView *avatarImage;
-@property(weak, nonatomic) IBOutlet UILabel *nameLabel;
-@property(weak, nonatomic) IBOutlet UILabel *durationLabel;
-@property (weak, nonatomic) IBOutlet UIInterfaceStyleButton *kickButton;
-@property(nonatomic, setter=setParticipant:) LinphoneParticipant *participant;
-
-- (id)initWithIdentifier:(NSString *)identifier;
-- (IBAction)onKickClick:(id)sender;
-
-@end
diff --git a/Classes/LinphoneUI/UICallConferenceCell.m b/Classes/LinphoneUI/UICallConferenceCell.m
deleted file mode 100644
index f9c8d94a0..000000000
--- a/Classes/LinphoneUI/UICallConferenceCell.m
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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 .
- */
-
-#import "linphoneapp-Swift.h"
-#import "UICallConferenceCell.h"
-#import "Utils.h"
-#import "PhoneMainView.h"
-
-@implementation UICallConferenceCell
-
-- (id)initWithIdentifier:(NSString *)identifier {
- self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
- if (self != nil) {
- NSArray *arrayOfViews =
- [[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self.class) owner:self options:nil];
- if ([arrayOfViews count] >= 1) {
- // resize cell to match .nib size. It is needed when resized the cell to
- // correctly adapt its height too
- UIView *sub = ((UIView *)[arrayOfViews objectAtIndex:0]);
- [self setFrame:CGRectMake(0, 0, sub.frame.size.width, sub.frame.size.height)];
- [self addSubview:sub];
- }
- }
- return self;
-}
-
-- (void)setParticipant:(LinphoneParticipant *)p {
- _participant = p;
- if (!p) {
- return;
- }
- const LinphoneAddress *addr = linphone_participant_get_address(p);
- [ContactDisplay setDisplayNameLabel:_nameLabel forAddress:addr];
- _durationLabel.text = [LinphoneUtils durationToString:[NSDate date].timeIntervalSince1970 - linphone_participant_get_creation_time(p)];
- _kickButton.hidden = CallManager.instance.isInConferenceAsGuest;
-}
-
-
-- (IBAction)onKickClick:(id)sender {
- if (!_participant) {
- return;
- }
-
- if ([CallManager callKitEnabled]) {
- LinphoneCall *call = [CallManager.instance getCallForParticipant:_participant];
- if (call) {
- [CallManager.instance setHeldWithCall:call hold:true];
- }
- }
- linphone_conference_remove_participant_2([CallManager.instance getConference], _participant);
-
-
-}
-@end
diff --git a/Classes/LinphoneUI/UICallPausedCell.h b/Classes/LinphoneUI/UICallPausedCell.h
deleted file mode 100644
index 77bdedd71..000000000
--- a/Classes/LinphoneUI/UICallPausedCell.h
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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 .
- */
-
-#import "UIRoundedImageView.h"
-#import "LinphoneManager.h"
-#import "UIPauseButton.h"
-
-@interface UICallPausedCell : UITableViewCell
-
-@property(weak, nonatomic) IBOutlet UIRoundedImageView *avatarImage;
-@property(weak, nonatomic) IBOutlet UILabel *nameLabel;
-@property(weak, nonatomic) IBOutlet UILabel *durationLabel;
-@property(weak, nonatomic) IBOutlet UIPauseButton *pauseButton;
-
-- (id)initWithIdentifier:(NSString *)identifier;
-- (void)setCall:(LinphoneCall *)call;
-
-@end
diff --git a/Classes/LinphoneUI/UICallPausedCell.m b/Classes/LinphoneUI/UICallPausedCell.m
deleted file mode 100644
index fe51d9c5d..000000000
--- a/Classes/LinphoneUI/UICallPausedCell.m
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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 .
- */
-
-#import "UICallPausedCell.h"
-#import "Utils.h"
-
-@implementation UICallPausedCell
-
-- (id)initWithIdentifier:(NSString *)identifier {
- self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
- if (self != nil) {
- NSArray *arrayOfViews =
- [[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self.class) owner:self options:nil];
- if ([arrayOfViews count] >= 1) {
- // resize cell to match .nib size. It is needed when resized the cell to
- // correctly adapt its height too
- UIView *sub = ((UIView *)[arrayOfViews objectAtIndex:0]);
- [self setFrame:CGRectMake(0, 0, sub.frame.size.width, sub.frame.size.height)];
- [self addSubview:sub];
- }
- }
- return self;
-}
-
-- (void)setCall:(LinphoneCall *)call {
- // if no call is provided, we assume that this is a conference
- if (!call || linphone_call_get_conference(call)) {
- [_pauseButton setType:UIPauseButtonType_Conference call:call];
- _nameLabel.text = NSLocalizedString(@"Conference", nil);
- [_avatarImage setImage:[UIImage imageNamed:@"options_start_conference_default.png"]
- bordered:NO
- withRoundedRadius:YES];
- _durationLabel.text = @"";
- } else {
- [_pauseButton setType:UIPauseButtonType_Call call:call];
- const LinphoneAddress *addr = linphone_call_get_remote_address(call);
- [ContactDisplay setDisplayNameLabel:_nameLabel forAddress:addr];
- [_avatarImage setImage:[FastAddressBook imageForAddress:addr] bordered:NO withRoundedRadius:YES];
- _durationLabel.text = [LinphoneUtils durationToString:linphone_call_get_duration(call)];
- }
- [_pauseButton update];
-}
-
-@end
diff --git a/Classes/LinphoneUI/UICamSwitch.h b/Classes/LinphoneUI/UICamSwitch.h
index 9713703cb..545d71428 100644
--- a/Classes/LinphoneUI/UICamSwitch.h
+++ b/Classes/LinphoneUI/UICamSwitch.h
@@ -24,5 +24,6 @@
@interface UICamSwitch : UIIconButton
@property(nonatomic, weak) IBOutlet UIView *preview;
++ (void) switchCamera;
@end
diff --git a/Classes/LinphoneUI/UICamSwitch.m b/Classes/LinphoneUI/UICamSwitch.m
index 9aee5f2f5..22218a0e5 100644
--- a/Classes/LinphoneUI/UICamSwitch.m
+++ b/Classes/LinphoneUI/UICamSwitch.m
@@ -34,6 +34,10 @@ INIT_WITH_COMMON_CF {
#pragma mark -
- (void)touchUp:(id)sender {
+ [UICamSwitch switchCamera];
+}
+
++ (void) switchCamera {
const char *currentCamId = (char *)linphone_core_get_video_device(LC);
const char **cameras = linphone_core_get_video_devices(LC);
const char *newCamId = NULL;
@@ -50,10 +54,6 @@ INIT_WITH_COMMON_CF {
if (newCamId) {
LOGI(@"Switching from [%s] to [%s]", currentCamId, newCamId);
linphone_core_set_video_device(LC, newCamId);
- LinphoneCall *call = linphone_core_get_current_call(LC);
- if (call != NULL) {
- linphone_call_update(call, NULL);
- }
}
}
diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.h b/Classes/LinphoneUI/UIChatBubbleTextCell.h
index 6b583ecac..604af74d5 100644
--- a/Classes/LinphoneUI/UIChatBubbleTextCell.h
+++ b/Classes/LinphoneUI/UIChatBubbleTextCell.h
@@ -29,6 +29,9 @@
#define IMAGE_DEFAULT_MARGIN 5
#define VOICE_RECORDING_PLAYER_HEIGHT 60
#define VOICE_RECORDING_PLAYER_WIDTH 300
+#define CONFERENCE_INVITATION_HEIGHT 210
+#define CONFERENCE_INVITATION_WIDTH 300
+
@interface UIChatBubbleTextCell : UITableViewCell
@@ -66,6 +69,7 @@
@property (weak, nonatomic) IBOutlet UIImageView *replyTransferIcon;
@property (weak, nonatomic) IBOutlet UILabel *replyTransferLabel;
@property (weak, nonatomic) IBOutlet UIView *photoCellContentView;
+@property UIView *icsBubbleView;
@property(nonatomic) BOOL isFirst;
diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.m b/Classes/LinphoneUI/UIChatBubbleTextCell.m
index f3bc6d8f8..156bbb30f 100644
--- a/Classes/LinphoneUI/UIChatBubbleTextCell.m
+++ b/Classes/LinphoneUI/UIChatBubbleTextCell.m
@@ -29,6 +29,8 @@
@implementation UIChatBubbleTextCell
+
+
#pragma mark - Lifecycle Functions
@@ -43,6 +45,11 @@
UIView *sub = ((UIView *)[arrayOfViews objectAtIndex:arrayOfViews.count - 1]);
[self setFrame:CGRectMake(0, 0, sub.frame.size.width, sub.frame.size.height)];
[self addSubview:sub];
+ self.icsBubbleView = [[ICSBubbleView alloc] init];
+ self.icsBubbleView.frame = CGRectMake(_messageText.frame.origin.x, _messageText.frame.origin.y+25, CONFERENCE_INVITATION_WIDTH-80, CONFERENCE_INVITATION_HEIGHT-20);
+ [self.innerView addSubview:self.icsBubbleView];
+ [(ICSBubbleView*)self.icsBubbleView setLayoutConstraintsWithView:self.backgroundColorImage];
+
}
}
@@ -275,6 +282,18 @@
_replyView.view.hidden = true;
}
+ // ICS for conference invitations
+
+ if ([ICSBubbleView isConferenceInvitationMessageWithCmessage:self.message]) {
+ [(ICSBubbleView*)self.icsBubbleView setFromChatMessageWithCmessage:self.message];
+ self.icsBubbleView.hidden = false;
+ _messageText.hidden = true;
+ } else {
+ self.icsBubbleView.hidden = true;
+ _messageText.hidden = false;
+ }
+
+
}
- (void)setEditing:(BOOL)editing {
@@ -470,6 +489,11 @@ static const CGFloat REPLY_OR_FORWARD_TAG_HEIGHT = 18;
}
+ (CGSize)ViewHeightForMessageText:(LinphoneChatMessage *)chat withWidth:(int)width textForImdn:(NSString *)imdnText {
+
+ if ([ICSBubbleView isConferenceInvitationMessageWithCmessage:chat]) {
+ return CGSizeMake(CONFERENCE_INVITATION_WIDTH, CONFERENCE_INVITATION_HEIGHT);
+ }
+
NSString *messageText = [UIChatBubbleTextCell TextMessageForChat:chat];
static UIFont *messageFont = nil;
diff --git a/Classes/LinphoneUI/UICompositeView.h b/Classes/LinphoneUI/UICompositeView.h
index db15589b2..b7c4739cb 100644
--- a/Classes/LinphoneUI/UICompositeView.h
+++ b/Classes/LinphoneUI/UICompositeView.h
@@ -86,4 +86,5 @@
- (void)clearCache:(NSArray *)exclude;
- (IBAction)onRightSwipe:(id)sender;
+
@end
diff --git a/Classes/LinphoneUI/UICompositeView.m b/Classes/LinphoneUI/UICompositeView.m
index 2354b51b5..f7ed463b5 100644
--- a/Classes/LinphoneUI/UICompositeView.m
+++ b/Classes/LinphoneUI/UICompositeView.m
@@ -22,6 +22,7 @@
#import "LinphoneAppDelegate.h"
#import "Utils.h"
#import "SideMenuView.h"
+#import "linphoneapp-Swift.h"
@implementation UICompositeViewDescription
@@ -304,12 +305,15 @@
return nil;
}
+
- (void)clearCache:(NSArray *)exclude {
+
+
for (NSString *key in [viewControllerCache allKeys]) {
bool remove = true;
/*ImagePickerView can be used as popover and we do NOT want to free it*/;
- if ([key isEqualToString:ImagePickerView.compositeViewDescription.name]) {
+ if ([key isEqualToString:ImagePickerView.compositeViewDescription.name] || [key isEqualToString:ActiveCallOrConferenceView.compositeViewDescription.name]) {
remove = false;
} else if (exclude != nil) {
for (UICompositeViewDescription *description in exclude) {
diff --git a/Classes/LinphoneUI/UIConfirmationDialog.h b/Classes/LinphoneUI/UIConfirmationDialog.h
index c2d9feadc..4d1673e66 100644
--- a/Classes/LinphoneUI/UIConfirmationDialog.h
+++ b/Classes/LinphoneUI/UIConfirmationDialog.h
@@ -46,12 +46,14 @@ typedef void (^UIConfirmationBlock)(void);
@property(weak, nonatomic) IBOutlet UIRoundBorderedButton *cancelButton;
@property (weak, nonatomic) IBOutlet UIImageView *securityImage;
@property (weak, nonatomic) IBOutlet UIImageView *forwardImage;
+@property (weak, nonatomic) IBOutlet UIImageView *groupCallImage;
@property(weak, nonatomic) IBOutlet UIRoundBorderedButton *confirmationButton;
@property (weak, nonatomic) IBOutlet UIView *authView;
@property(weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UIButton *authButton;
- (void)setSpecialColor;
+-(void) setWhiteCancel;
- (IBAction)onCancelClick:(id)sender;
- (IBAction)onConfirmationClick:(id)sender;
- (IBAction)onAuthClick:(id)sender;
diff --git a/Classes/LinphoneUI/UIConfirmationDialog.m b/Classes/LinphoneUI/UIConfirmationDialog.m
index 1d73d7d94..6c8be2e3b 100644
--- a/Classes/LinphoneUI/UIConfirmationDialog.m
+++ b/Classes/LinphoneUI/UIConfirmationDialog.m
@@ -19,6 +19,7 @@
#import "UIConfirmationDialog.h"
#import "PhoneMainView.h"
+#import "linphoneapp-Swift.h""
@implementation UIConfirmationDialog
+ (UIConfirmationDialog *)initDialog:(NSString *)cancel
@@ -97,6 +98,13 @@
[[UIColor colorWithPatternImage:[UIImage imageNamed:@"color_A.png"]] CGColor];
}
+-(void) setWhiteCancel {
+ [_cancelButton setBackgroundImage:nil forState:UIControlStateNormal];
+ [_cancelButton setBackgroundColor:UIColor.whiteColor];
+ [_cancelButton setTitleColor:VoipTheme.voip_dark_gray forState:UIControlStateNormal];
+ _cancelButton.layer.borderColor = UIColor.whiteColor.CGColor;
+}
+
- (IBAction)onCancelClick:(id)sender {
[self.view removeFromSuperview];
[self removeFromParentViewController];
diff --git a/Classes/LinphoneUI/UIHangUpButton.m b/Classes/LinphoneUI/UIHangUpButton.m
deleted file mode 100644
index 2aab33916..000000000
--- a/Classes/LinphoneUI/UIHangUpButton.m
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * 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 .
- */
-
-#import "linphoneapp-Swift.h"
-#import "UIHangUpButton.h"
-#import "LinphoneManager.h"
-
-#import "linphoneapp-Swift.h"
-
-@implementation UIHangUpButton
-
-#pragma mark - Static Functions
-
-+ (bool)isInConference:(LinphoneCall *)call {
- if (!call)
- return false;
- return linphone_call_params_get_local_conference_mode(linphone_call_get_current_params(call));
-}
-
-+ (int)callCount {
- int count = 0;
- const MSList *calls = linphone_core_get_calls(LC);
-
- while (calls != 0) {
- if (![UIHangUpButton isInConference:((LinphoneCall *)calls->data)]) {
- count++;
- }
- calls = calls->next;
- }
- return count;
-}
-
-#pragma mark - Lifecycle Functions
-
-- (void)initUIHangUpButton {
- [self addTarget:self action:@selector(touchUp:) forControlEvents:UIControlEventTouchUpInside];
-}
-
-- (id)init {
- self = [super init];
- if (self) {
- [self initUIHangUpButton];
- }
- return self;
-}
-
-- (id)initWithCoder:(NSCoder *)decoder {
- self = [super initWithCoder:decoder];
- if (self) {
- [self initUIHangUpButton];
- }
- return self;
-}
-
-- (id)initWithFrame:(CGRect)frame {
- self = [super initWithFrame:frame];
- if (self) {
- [self initUIHangUpButton];
- }
- return self;
-}
-
-#pragma mark -
-
-- (void)update {
- if (linphone_core_get_calls_nb(LC) == 1 || // One call
- linphone_core_get_current_call(LC) != NULL || // In call
- linphone_core_is_in_conference(LC) || // In conference
- (linphone_core_get_conference_size(LC) > 0 && [UIHangUpButton callCount] == 0) // Only one conf
- ) {
- [self setEnabled:true];
- return;
- }
- [self setEnabled:false];
-}
-
-#pragma mark - Action Functions
-
-- (void)touchUp:(id)sender {
- LinphoneCall *currentcall = linphone_core_get_current_call(LC);
- if (linphone_core_is_in_conference(LC) || // In conference
- (linphone_core_get_conference_size(LC) > 0 && [UIHangUpButton callCount] == 0) // Only one conf
- ) {
- LinphoneManager.instance.conf = TRUE;
- linphone_core_terminate_conference(LC);
- } else if (currentcall != NULL) {
- [CallManager.instance terminateCallWithCall:currentcall];
- } else {
- const MSList *calls = linphone_core_get_calls(LC);
- if (bctbx_list_size(calls) == 1) { // Only one call
- [CallManager.instance terminateCallWithCall:(calls->data)];
- }
- }
-}
-
-@end
diff --git a/Classes/LinphoneUI/UIHistoryCell.m b/Classes/LinphoneUI/UIHistoryCell.m
index 8df28cd4b..346c029c6 100644
--- a/Classes/LinphoneUI/UIHistoryCell.m
+++ b/Classes/LinphoneUI/UIHistoryCell.m
@@ -21,6 +21,7 @@
#import "LinphoneManager.h"
#import "PhoneMainView.h"
#import "Utils.h"
+#import "linphoneapp-Swift.h"
@implementation UIHistoryCell
@@ -59,10 +60,16 @@
if (callLog != NULL) {
HistoryDetailsView *view = VIEW(HistoryDetailsView);
if (linphone_call_log_get_call_id(callLog) != NULL) {
- // Go to History details view
- [view setCallLogId:[NSString stringWithUTF8String:linphone_call_log_get_call_id(callLog)]];
+ if (linphone_call_log_was_conference(callLog)) {
+ ConferenceHistoryDetailsView *view = VIEW(ConferenceHistoryDetailsView);
+ [PhoneMainView.instance changeCurrentView:view.compositeViewDescription];
+ [view setCallLogWithCallLog:callLog];
+ } else {
+ // Go to History details view
+ [view setCallLogId:[NSString stringWithUTF8String:linphone_call_log_get_call_id(callLog)]];
+ [PhoneMainView.instance changeCurrentView:view.compositeViewDescription];
+ }
}
- [PhoneMainView.instance changeCurrentView:view.compositeViewDescription];
}
}
@@ -80,32 +87,39 @@
LOGW(@"Cannot update history cell: null callLog");
return;
}
-
+
// Set up the cell...
- const LinphoneAddress *addr;
- UIImage *image;
- if (linphone_call_log_get_dir(callLog) == LinphoneCallIncoming) {
- if (linphone_call_log_get_status(callLog) != LinphoneCallMissed) {
- image = [UIImage imageNamed:@"call_status_incoming.png"];
- } else {
- image = [UIImage imageNamed:@"call_status_missed.png"];
- }
- addr = linphone_call_log_get_from_address(callLog);
+ if (linphone_call_log_was_conference(callLog)) {
+ const char *subject = linphone_conference_info_get_subject(linphone_call_log_get_conference_info(callLog));
+ displayNameLabel.text = [NSString stringWithFormat:@"%s",subject];
+ [_avatarImage setImage:[UIImage imageNamed:@"voip_multiple_contacts_avatar"]];
+ _stateImage.hidden = true;
} else {
- image = [UIImage imageNamed:@"call_status_outgoing.png"];
- addr = linphone_call_log_get_to_address(callLog);
- }
- _stateImage.image = image;
-
- [ContactDisplay setDisplayNameLabel:displayNameLabel forAddress:addr];
-
- size_t count = bctbx_list_size(linphone_call_log_get_user_data(callLog)) + 1;
- if (count > 1) {
- displayNameLabel.text =
+ _stateImage.hidden = false;
+ const LinphoneAddress *addr;
+ UIImage *image;
+ if (linphone_call_log_get_dir(callLog) == LinphoneCallIncoming) {
+ if (linphone_call_log_get_status(callLog) != LinphoneCallMissed) {
+ image = [UIImage imageNamed:@"call_status_incoming.png"];
+ } else {
+ image = [UIImage imageNamed:@"call_status_missed.png"];
+ }
+ addr = linphone_call_log_get_from_address(callLog);
+ } else {
+ image = [UIImage imageNamed:@"call_status_outgoing.png"];
+ addr = linphone_call_log_get_to_address(callLog);
+ }
+ _stateImage.image = image;
+ [ContactDisplay setDisplayNameLabel:displayNameLabel forAddress:addr];
+
+ size_t count = bctbx_list_size(linphone_call_log_get_user_data(callLog)) + 1;
+ if (count > 1) {
+ displayNameLabel.text =
[displayNameLabel.text stringByAppendingString:[NSString stringWithFormat:@" (%lu)", count]];
+ }
+
+ [_avatarImage setImage:[FastAddressBook imageForAddress:addr] bordered:NO withRoundedRadius:YES];
}
-
- [_avatarImage setImage:[FastAddressBook imageForAddress:addr] bordered:NO withRoundedRadius:YES];
}
- (void)setEditing:(BOOL)editing {
diff --git a/Classes/LinphoneUI/UIPauseButton.h b/Classes/LinphoneUI/UIPauseButton.h
deleted file mode 100644
index 0146c3174..000000000
--- a/Classes/LinphoneUI/UIPauseButton.h
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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 .
- */
-
-#import "UIToggleButton.h"
-
-#include "linphone/linphonecore.h"
-
-typedef enum _UIPauseButtonType {
- UIPauseButtonType_CurrentCall,
- UIPauseButtonType_Call,
- UIPauseButtonType_Conference
-} UIPauseButtonType;
-
-@interface UIPauseButton : UIToggleButton {
- @private
- UIPauseButtonType type;
- LinphoneCall* call;
-}
-
-- (void)setType:(UIPauseButtonType) type call:(LinphoneCall*)call;
-
-@end
diff --git a/Classes/LinphoneUI/UIPauseButton.m b/Classes/LinphoneUI/UIPauseButton.m
deleted file mode 100644
index 15c258b1a..000000000
--- a/Classes/LinphoneUI/UIPauseButton.m
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * 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 .
- */
-
-#import "linphoneapp-Swift.h"
-#import "UIPauseButton.h"
-#import "LinphoneManager.h"
-#import "Utils.h"
-
-@implementation UIPauseButton
-
-#pragma mark - Lifecycle Functions
-
-- (void)initUIPauseButton {
- type = UIPauseButtonType_CurrentCall;
-}
-
-- (id)init {
- self = [super init];
- if (self) {
- [self initUIPauseButton];
- }
- return self;
-}
-
-- (id)initWithCoder:(NSCoder *)decoder {
- self = [super initWithCoder:decoder];
- if (self) {
- [self initUIPauseButton];
- }
- return self;
-}
-
-- (id)initWithFrame:(CGRect)frame {
- self = [super initWithFrame:frame];
- if (self) {
- [self initUIPauseButton];
- }
- return self;
-}
-
-#pragma mark - Static Functions
-
-+ (bool)isInConference:(LinphoneCall *)call {
- if (!call)
- return false;
- return linphone_call_params_get_local_conference_mode(linphone_call_get_current_params(call));
-}
-
-+ (LinphoneCall *)getCall {
- LinphoneCall *currentCall = linphone_core_get_current_call(LC);
- if (currentCall == nil && linphone_core_get_calls_nb(LC) == 1) {
- currentCall = (LinphoneCall *)linphone_core_get_calls(LC)->data;
- }
- return currentCall;
-}
-
-#pragma mark -
-
-- (void)setType:(UIPauseButtonType)atype call:(LinphoneCall *)acall {
- type = atype;
- call = acall;
-}
-
-#pragma mark - UIToggleButtonDelegate Functions
-
-- (void)onOn {
- switch (type) {
- case UIPauseButtonType_Call: {
- if (call != nil) {
- if ([CallManager callKitEnabled]) {
- [CallManager.instance setHeldWithCall:call hold:true];
- } else {
- CallManager.instance.speakerBeforePause = [CallManager.instance isSpeakerEnabled];
- linphone_call_pause(call);
- }
- } else {
- LOGW(@"Cannot toggle pause buttton, because no current call");
- }
- break;
- }
- case UIPauseButtonType_Conference: {
- linphone_conference_leave(CallManager.instance.getConference);
- // Fake event
- [NSNotificationCenter.defaultCenter postNotificationName:kLinphoneCallUpdate object:self];
- break;
- }
- case UIPauseButtonType_CurrentCall: {
- LinphoneCall *currentCall = [UIPauseButton getCall];
- if (currentCall != nil) {
- if ([CallManager callKitEnabled]) {
- [CallManager.instance setHeldWithCall:currentCall hold:true];
- } else {
- CallManager.instance.speakerBeforePause = [CallManager.instance isSpeakerEnabled];
- linphone_call_pause(currentCall);
- }
- } else {
- LOGW(@"Cannot toggle pause buttton, because no current call");
- }
- break;
- }
- }
-}
-
-- (void)onOff {
- switch (type) {
- case UIPauseButtonType_Call: {
- if (call != nil) {
- if ([CallManager callKitEnabled]) {
- [CallManager.instance setHeldWithCall:call hold:false];
- } else {
- linphone_call_resume(call);
- }
- } else {
- LOGW(@"Cannot toggle pause buttton, because no current call");
- }
- break;
- }
- case UIPauseButtonType_Conference: {
- linphone_conference_enter(CallManager.instance.getConference);
- // Fake event
- [NSNotificationCenter.defaultCenter postNotificationName:kLinphoneCallUpdate object:self];
- break;
- }
- case UIPauseButtonType_CurrentCall: {
- LinphoneCall *currentCall = [UIPauseButton getCall];
- if ([CallManager callKitEnabled]) {
- [CallManager.instance setHeldWithCall:currentCall hold:false];
- } else {
- linphone_call_resume(currentCall);
- }
- break;
- }
- }
-}
-
-- (bool)onUpdate {
- bool ret = false;
- LinphoneCall *c = call;
- switch (type) {
- case UIPauseButtonType_Conference: {
- self.enabled = CallManager.instance.getConference && (linphone_conference_get_participant_count(CallManager.instance.getConference)> 0);
- if (self.enabled) {
- ret = (!CallManager.instance.isInConference);
- }
- break;
- }
- case UIPauseButtonType_CurrentCall:
- c = [UIPauseButton getCall];
- case UIPauseButtonType_Call: {
- if (c != nil) {
- LinphoneCallState state = linphone_call_get_state(c);
- ret = (state == LinphoneCallPaused || state == LinphoneCallPausing);
- self.enabled = !linphone_core_sound_resources_locked(LC) &&
- (state == LinphoneCallPaused || state == LinphoneCallPausing ||
- state == LinphoneCallStreamsRunning);
- } else {
- self.enabled = FALSE;
- }
- break;
- }
- }
- return ret;
-}
-
-@end
diff --git a/Classes/LinphoneUI/UISpeakerButton.m b/Classes/LinphoneUI/UISpeakerButton.m
deleted file mode 100644
index 4c123133b..000000000
--- a/Classes/LinphoneUI/UISpeakerButton.m
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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 .
- */
-
-#import "linphoneapp-Swift.h"
-#import
-#import "UISpeakerButton.h"
-#import "Utils.h"
-#import "LinphoneManager.h"
-
-#include "linphone/linphonecore.h"
-
-@implementation UISpeakerButton
-
-INIT_WITH_COMMON_CF {
- [NSNotificationCenter.defaultCenter addObserver:self
- selector:@selector(audioRouteChangeListenerCallback:)
- name:AVAudioSessionRouteChangeNotification
- object:nil];
- return self;
-}
-
-- (void)onOn {
- [CallManager.instance changeRouteToSpeaker];
-}
-
-- (void)onOff {
- [CallManager.instance changeRouteToDefault];
-}
-
-
-- (void)dealloc {
- [NSNotificationCenter.defaultCenter removeObserver:self];
-}
-
-#pragma mark - UIToggleButtonDelegate Functions
-
-- (void)audioRouteChangeListenerCallback:(NSNotification *)notif {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self update];});
-}
-
-- (bool)onUpdate {
- return [CallManager.instance isSpeakerEnabled];
-}
-
-@end
diff --git a/Classes/LinphoneUI/VideoZoomHandler.h b/Classes/LinphoneUI/VideoZoomHandler.h
deleted file mode 100644
index a106d1f2c..000000000
--- a/Classes/LinphoneUI/VideoZoomHandler.h
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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 .
- */
-
-#import
-#import
-
-@interface VideoZoomHandler : NSObject {
- float zoomLevel, cx, cy;
- UIView* videoView;
-}
-
-- (void) setup: (UIView*) videoView;
-- (void) resetZoom;
-
-@end
diff --git a/Classes/LinphoneUI/VideoZoomHandler.m b/Classes/LinphoneUI/VideoZoomHandler.m
deleted file mode 100644
index 1539c0252..000000000
--- a/Classes/LinphoneUI/VideoZoomHandler.m
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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 .
- */
-
-#import "VideoZoomHandler.h"
-#include "linphone/linphonecore.h"
-#import "LinphoneManager.h"
-
-@implementation VideoZoomHandler
-
-- (void)zoomInOut:(UITapGestureRecognizer *)reco {
- if (zoomLevel != 1)
- zoomLevel = 1;
- else
- zoomLevel = 2;
-
- if (zoomLevel != 1) {
- CGPoint point = [reco locationInView:videoView];
- cx = point.x / videoView.frame.size.width;
- cy = 1 - point.y / videoView.frame.size.height;
- } else {
- cx = cy = 0.5;
- }
- linphone_call_zoom_video(linphone_core_get_current_call(LC), zoomLevel, &cx, &cy);
-}
-
-- (void)videoPan:(UIPanGestureRecognizer *)reco {
- if (zoomLevel <= 1.0)
- return;
-
- float x, y;
- CGPoint translation = [reco translationInView:videoView];
- if ([reco state] == UIGestureRecognizerStateEnded) {
- cx -= translation.x / videoView.frame.size.width;
- cy += translation.y / videoView.frame.size.height;
- x = cx;
- y = cy;
- } else if ([reco state] == UIGestureRecognizerStateChanged) {
- x = cx - translation.x / videoView.frame.size.width;
- y = cy + translation.y / videoView.frame.size.height;
- [reco setTranslation:CGPointMake(0, 0) inView:videoView];
- } else {
- return;
- }
-
- linphone_call_zoom_video(linphone_core_get_current_call(LC), zoomLevel, &x, &y);
- cx = x;
- cy = y;
-}
-
-- (void)pinch:(UIPinchGestureRecognizer *)reco {
- float s = zoomLevel;
- // CGPoint point = [reco locationInView:videoGroup];
- // float ccx = cx + (point.x / videoGroup.frame.size.width - 0.5) / s;
- // float ccy = cy - (point.y / videoGroup.frame.size.height - 0.5) / s;
- if ([reco state] == UIGestureRecognizerStateEnded) {
- zoomLevel = MAX(MIN(zoomLevel * reco.scale, 3.0), 1.0);
- s = zoomLevel;
- // cx = ccx;
- // cy = ccy;
- } else if ([reco state] == UIGestureRecognizerStateChanged) {
- s = zoomLevel * reco.scale;
- s = MAX(MIN(s, 3.0), 1.0);
- } else if ([reco state] == UIGestureRecognizerStateBegan) {
-
- } else {
- return;
- }
-
- linphone_call_zoom_video(linphone_core_get_current_call(LC), s, &cx, &cy);
-}
-
-- (void)resetZoom {
- zoomLevel = 1;
- cx = cy = 0.5;
-}
-
-- (void)setup:(UIView *)view {
- videoView = view;
-
- UITapGestureRecognizer *doubleFingerTap =
- [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(zoomInOut:)];
- [doubleFingerTap setNumberOfTapsRequired:2];
- [doubleFingerTap setNumberOfTouchesRequired:1];
- [videoView addGestureRecognizer:doubleFingerTap];
-
- UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(videoPan:)];
- [videoView addGestureRecognizer:pan];
- UIPinchGestureRecognizer *pinchReco =
- [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinch:)];
- [videoView addGestureRecognizer:pinchReco];
-
- [self resetZoom];
-}
-
-@end
diff --git a/Classes/LinphoneUI/fr.lproj/UICallConferenceCell.strings b/Classes/LinphoneUI/fr.lproj/UICallConferenceCell.strings
deleted file mode 100644
index 629130fa2..000000000
Binary files a/Classes/LinphoneUI/fr.lproj/UICallConferenceCell.strings and /dev/null differ
diff --git a/Classes/LinphoneUI/fr.lproj/UICallPausedCell.strings b/Classes/LinphoneUI/fr.lproj/UICallPausedCell.strings
deleted file mode 100644
index 4d0eb9bb9..000000000
Binary files a/Classes/LinphoneUI/fr.lproj/UICallPausedCell.strings and /dev/null differ
diff --git a/Classes/LinphoneUI/hu.lproj/UICallConferenceCell.strings b/Classes/LinphoneUI/hu.lproj/UICallConferenceCell.strings
deleted file mode 100644
index c90ef96ac..000000000
Binary files a/Classes/LinphoneUI/hu.lproj/UICallConferenceCell.strings and /dev/null differ
diff --git a/Classes/LinphoneUI/hu.lproj/UICallPausedCell.strings b/Classes/LinphoneUI/hu.lproj/UICallPausedCell.strings
deleted file mode 100644
index 7e626165f..000000000
Binary files a/Classes/LinphoneUI/hu.lproj/UICallPausedCell.strings and /dev/null differ
diff --git a/Classes/Log.h b/Classes/Log.h
index c2ee3d897..c838afa12 100644
--- a/Classes/Log.h
+++ b/Classes/Log.h
@@ -32,6 +32,11 @@
+ (void)log:(OrtpLogLevel)severity file:(const char *)file line:(int)line format:(NSString *)format, ...;
+ (void)enableLogs:(OrtpLogLevel)level;
+ (void)directLog:(OrtpLogLevel)level text:(NSString *)text;
++ (void)d:(NSString *)text;
++ (void)i:(NSString *)text;
++ (void)w:(NSString *)text;
++ (void)e:(NSString *)text;
++ (void)f:(NSString *)text;
void linphone_iphone_log_handler(const char *domain, OrtpLogLevel lev, const char *fmt, va_list args);
@end
diff --git a/Classes/PhoneMainView.h b/Classes/PhoneMainView.h
index adfe87b5a..672030668 100644
--- a/Classes/PhoneMainView.h
+++ b/Classes/PhoneMainView.h
@@ -27,10 +27,6 @@
#import "AboutView.h"
#import "AssistantLinkView.h"
#import "AssistantView.h"
-#import "CallIncomingView.h"
-#import "CallOutgoingView.h"
-#import "CallSideMenuView.h"
-#import "CallView.h"
#import "ChatConversationCreateView.h"
#import "ChatConversationInfoView.h"
#import "ChatConversationImdnView.h"
@@ -78,7 +74,7 @@
@end
-@interface PhoneMainView : UIViewController {
+@interface PhoneMainView : UIViewController {
@private
NSMutableArray *inhibitedEvents;
}
@@ -96,6 +92,7 @@
- (void)changeCurrentView:(UICompositeViewDescription *)view;
- (UIViewController*)popCurrentView;
+- (UIViewController *)popView:(UICompositeViewDescription *)view;
- (UIViewController *)popToView:(UICompositeViewDescription *)currentView;
- (void) setPreviousViewName:(NSString*)previous;
- (NSString*) getPreviousViewName;
diff --git a/Classes/PhoneMainView.m b/Classes/PhoneMainView.m
index 8a15d910c..1d3393bd6 100644
--- a/Classes/PhoneMainView.m
+++ b/Classes/PhoneMainView.m
@@ -17,12 +17,13 @@
* along with this program. If not, see .
*/
-#import "linphoneapp-Swift.h"
#import
#import
#import "LinphoneAppDelegate.h"
#import "Log.h"
#import "PhoneMainView.h"
+#import "linphoneapp-Swift.h"
+
static RootViewManager *rootViewManagerInstance = nil;
@@ -373,8 +374,16 @@ static RootViewManager *rootViewManagerInstance = nil;
}
break;
}
- case LinphoneCallOutgoingInit: {
- [self changeCurrentView:CallOutgoingView.compositeViewDescription];
+ case LinphoneCallOutgoingInit:
+ case LinphoneCallOutgoingEarlyMedia:
+ case LinphoneCallOutgoingProgress:
+ case LinphoneCallOutgoingRinging: {
+ CallAppData *data = [CallManager getAppDataWithCall:call];
+ if (!data.isConference) {
+ OutgoingCallView *v = VIEW(OutgoingCallView);
+ [self changeCurrentView:OutgoingCallView.compositeViewDescription];
+ [v setCallWithCall:call];
+ }
break;
}
case LinphoneCallPausedByRemote:
@@ -382,47 +391,16 @@ static RootViewManager *rootViewManagerInstance = nil;
if (![LinphoneManager.instance isCTCallCenterExist]) {
/*only register CT call center CB for connected call*/
[LinphoneManager.instance setupGSMInteraction];
- [[UIDevice currentDevice] setProximityMonitoringEnabled:!([CallManager.instance isSpeakerEnabled] || [CallManager.instance isBluetoothEnabled])];
- }
- break;
- }
- case LinphoneCallStreamsRunning: {
- [self changeCurrentView:CallView.compositeViewDescription];
- break;
- }
- case LinphoneCallUpdatedByRemote: {
- const LinphoneCallParams *current = linphone_call_get_current_params(call);
- const LinphoneCallParams *remote = linphone_call_get_remote_params(call);
-
- if (linphone_call_params_video_enabled(current) && !linphone_call_params_video_enabled(remote)) {
- [self changeCurrentView:CallView.compositeViewDescription];
}
break;
}
case LinphoneCallError: {
[self displayCallError:call message:message];
}
- case LinphoneCallEnd: {
- const MSList *calls = linphone_core_get_calls(LC);
- if (!calls || calls->data == call) {
- while ((currentView == CallView.compositeViewDescription) ||
- (currentView == CallIncomingView.compositeViewDescription) ||
- (currentView == CallOutgoingView.compositeViewDescription)) {
- [self popCurrentView];
- }
- } else {
- [self changeCurrentView:CallView.compositeViewDescription];
- }
- break;
- }
case LinphoneCallEarlyUpdatedByRemote:
case LinphoneCallEarlyUpdating:
case LinphoneCallIdle:
break;
- case LinphoneCallOutgoingEarlyMedia:
- case LinphoneCallOutgoingProgress: {
- break;
- }
case LinphoneCallReleased:
if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) {
dispatch_async(dispatch_get_main_queue(), ^{
@@ -431,7 +409,6 @@ static RootViewManager *rootViewManagerInstance = nil;
});
}
break;
- case LinphoneCallOutgoingRinging:
case LinphoneCallPaused:
case LinphoneCallPausing:
case LinphoneCallRefered:
@@ -634,6 +611,15 @@ static RootViewManager *rootViewManagerInstance = nil;
return [mainViewController getCurrentViewController];
}
+- (UIViewController *)popView:(UICompositeViewDescription *)view {
+ NSMutableArray *viewStack = [RootViewManager instance].viewDescriptionStack;
+ while (viewStack.count > 0 && [[viewStack lastObject] equal:view]) {
+ [viewStack removeLastObject];
+ }
+ return [self popToView:viewStack.lastObject ?: DialerView.compositeViewDescription];
+}
+
+
- (void)changeCurrentView:(UICompositeViewDescription *)view {
[self _changeCurrentView:view transition:nil animated:ANIMATED];
}
@@ -765,10 +751,10 @@ static RootViewManager *rootViewManagerInstance = nil;
[CallManager.instance acceptCallWithCall:call hasVideo:YES];
} else {
AudioServicesPlaySystemSound(lm.sounds.vibrate);
- CallIncomingView *view = VIEW(CallIncomingView);
+ IncomingCallView *view = VIEW(IncomingCallView);
[self changeCurrentView:view.compositeViewDescription];
- [view setCall:call];
- [view setDelegate:self];
+ [view setCallWithCall:call];
+ //CDFIX [view setDelegate:self];
}
}
}
diff --git a/Classes/SideMenuTableView.m b/Classes/SideMenuTableView.m
index 67ca16831..d34356814 100644
--- a/Classes/SideMenuTableView.m
+++ b/Classes/SideMenuTableView.m
@@ -27,6 +27,7 @@
#import "ShopView.h"
#import "LinphoneManager.h"
#import "RecordingsListView.h"
+#import "linphoneapp-Swift.h"
@implementation SideMenuEntry
@@ -101,6 +102,15 @@
changeCurrentView:ShopView.compositeViewDescription];
}]];
}
+
+ [_sideMenuEntries addObject:[[SideMenuEntry alloc] initWithTitle:VoipTexts.conference_scheduled
+ image:[UIImage imageNamed:@"voip_conference_new.png"]
+ tapBlock:^() {
+ [PhoneMainView.instance
+ changeCurrentView:ScheduledConferencesView.compositeViewDescription];
+
+ }]];
+
[_sideMenuEntries addObject:[[SideMenuEntry alloc] initWithTitle:NSLocalizedString(@"About", nil)
image:[UIImage imageNamed:@"menu_about.png"]
tapBlock:^() {
diff --git a/Classes/AppManager.swift b/Classes/Swift/AppManager.swift
similarity index 100%
rename from Classes/AppManager.swift
rename to Classes/Swift/AppManager.swift
diff --git a/Classes/CallManager.swift b/Classes/Swift/CallManager.swift
similarity index 81%
rename from Classes/CallManager.swift
rename to Classes/Swift/CallManager.swift
index c1d1feda8..844db2fc1 100644
--- a/Classes/CallManager.swift
+++ b/Classes/Swift/CallManager.swift
@@ -27,6 +27,8 @@ import AVFoundation
@objc class CallAppData: NSObject {
@objc var batteryWarningShown = false
@objc var videoRequested = false /*set when user has requested for video*/
+ @objc var isConference = true
+
}
/*
@@ -245,6 +247,19 @@ import AVFoundation
callParams.recordFile = writablePath
+ if let chatView : ChatConversationView = PhoneMainView.instance().VIEW(ChatConversationView.compositeViewDescription()), chatView.isVoiceRecording {
+ Log.directLog(BCTBX_LOG_MESSAGE, text: "Voice recording in progress, stopping it befoce accepting the call.")
+ chatView.stopVoiceRecording()
+ }
+
+ if (call.callLog?.wasConference() == true) {
+ // Prevent incoming group call to start in audio only layout
+ // Do the same as the conference waiting room
+ callParams.videoEnabled = true
+ callParams.videoDirection = Core.get().videoActivationPolicy?.automaticallyInitiate == true ? .SendRecv : .RecvOnly
+ Log.i("[Context] Enabling video on call params to prevent audio-only layout when answering")
+ }
+
try call.acceptWithParams(params: callParams)
} catch {
Log.directLog(BCTBX_LOG_ERROR, text: "accept call failed \(error)")
@@ -252,32 +267,41 @@ import AVFoundation
}
// for outgoing call. There is not yet callId
- @objc func startCall(addr: OpaquePointer?, isSas: Bool) {
+ @objc func startCall(addr: OpaquePointer?, isSas: Bool, isVideo: Bool, isConference: Bool = false) {
if (addr == nil) {
print("Can not start a call with null address!")
return
}
let sAddr = Address.getSwiftObject(cObject: addr!)
- if (CallManager.callKitEnabled() && !CallManager.instance().nextCallIsTransfer && !isInConference()) {
+ if (CallManager.callKitEnabled() && !CallManager.instance().nextCallIsTransfer && lc?.conference?.isIn != true) {
let uuid = UUID()
let name = FastAddressBook.displayName(for: addr) ?? "unknow"
let handle = CXHandle(type: .generic, value: sAddr.asStringUriOnly())
let startCallAction = CXStartCallAction(call: uuid, handle: handle)
let transaction = CXTransaction(action: startCallAction)
- let callInfo = CallInfo.newOutgoingCallInfo(addr: sAddr, isSas: isSas, displayName: name)
+ let callInfo = CallInfo.newOutgoingCallInfo(addr: sAddr, isSas: isSas, displayName: name, isVideo: isVideo, isConference:isConference)
providerDelegate.callInfos.updateValue(callInfo, forKey: uuid)
providerDelegate.uuids.updateValue(uuid, forKey: "")
setHeldOtherCalls(exceptCallid: "")
requestTransaction(transaction, action: "startCall")
}else {
- try? doCall(addr: sAddr, isSas: isSas)
+ try? doCall(addr: sAddr, isSas: isSas, isVideo:isVideo, isConference:isConference)
+ }
+ }
+
+ func startCall(addr:String, isSas: Bool = false, isVideo: Bool, isConference: Bool = false) {
+ do {
+ let address = try Factory.Instance.createAddress(addr: addr)
+ startCall(addr: address.getCobject,isSas: isSas, isVideo: isVideo, isConference:isConference)
+ } catch {
+ Log.e("[CallManager] unable to create address for a new outgoing call : \(addr) \(error) ")
}
}
- func doCall(addr: Address, isSas: Bool) throws {
+ func doCall(addr: Address, isSas: Bool, isVideo: Bool, isConference:Bool = false) throws {
let displayName = FastAddressBook.displayName(for: addr.getCobject)
let lcallParams = try CallManager.instance().lc!.createCallParams(call: nil)
@@ -306,6 +330,18 @@ import AVFoundation
if (isSas) {
lcallParams.mediaEncryption = .ZRTP
}
+ if (isConference) {
+ if (ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! != .AudioOnly) {
+ lcallParams.videoEnabled = true
+ lcallParams.videoDirection = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true ? .SendRecv : .RecvOnly
+ lcallParams.conferenceVideoLayout = ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! == .Grid ? .Grid : .ActiveSpeaker
+ } else {
+ lcallParams.videoEnabled = false
+ }
+ } else {
+ lcallParams.videoEnabled = isVideo
+ }
+
let call = CallManager.instance().lc!.inviteAddressWithParams(addr: addr, params: lcallParams)
if (call != nil) {
// The LinphoneCallAppData object should be set on call creation with callback
@@ -316,6 +352,7 @@ import AVFoundation
Log.directLog(BCTBX_LOG_ERROR, text: "New call instanciated but app data was not set. Expect it to crash.")
/* will be used later to notify user if video was not activated because of the linphone core*/
} else {
+ data!.isConference = isConference
data!.videoRequested = lcallParams.videoEnabled
CallManager.setAppData(sCall: call!, appData: data)
}
@@ -396,6 +433,14 @@ import AVFoundation
}
func setHeld(call: Call, hold: Bool) {
+
+ #if targetEnvironment(simulator)
+ if (hold) {
+ try?call.pause()
+ } else {
+ try?call.resume()
+ }
+ #else
let callid = call.callLog?.callId ?? ""
let uuid = providerDelegate.uuids["\(callid)"]
if (uuid == nil) {
@@ -405,6 +450,7 @@ import AVFoundation
let setHeldAction = CXSetHeldCallAction(call: uuid!, onHold: hold)
let transaction = CXTransaction(action: setHeldAction)
requestTransaction(transaction, action: "setHeld")
+ #endif
}
@objc func setHeldOtherCalls(exceptCallid: String) {
@@ -469,19 +515,9 @@ import AVFoundation
}
}
- func onConferenceStateChanged(core: Core, conference: Conference, state: Conference.State) {
- if (state == .Terminated) {
- CallManager.instance().conference = nil
- }
- }
-
- func onAudioDevicesListUpdated(core: Core) {
- let bluetoothAvailable = isBluetoothAvailable();
-
- var dict = Dictionary()
- dict["available"] = bluetoothAvailable
- NotificationCenter.default.post(name: Notification.Name("LinphoneBluetoothAvailabilityUpdate"), object: self, userInfo: dict)
-
+ func isConferenceCall(call:Call) -> Bool {
+ let remoteAddress = call.remoteAddress?.asStringUriOnly()
+ return remoteAddress?.contains("focus") == true || remoteAddress?.contains("audiovideo") == true
}
func onCallStateChanged(core: Core, call: Call, state cstate: Call.State, message: String) {
@@ -496,12 +532,40 @@ import AVFoundation
let appData = CallAppData()
CallManager.setAppData(sCall: call, appData: appData)
}
+
+ if let conference = call.conference, ConferenceViewModel.shared.conference.value == nil {
+ Log.i("[Call] Found conference attached to call and no conference in dedicated view model, init & configure it")
+ ConferenceViewModel.shared.initConference(conference)
+ ConferenceViewModel.shared.configureConference(conference)
+ }
switch cstate {
case .IncomingReceived:
- let addr = call.remoteAddress;
- let displayName = FastAddressBook.displayName(for: addr?.getCobject) ?? "Unknown"
+ let addr = call.remoteAddress
+ var displayName = ""
+ let isConference = isConferenceCall(call: call)
+ let isEarlyConference = isConference && CallsViewModel.shared.currentCallData.value??.isConferenceCall.value != true // Conference info not be received yet.
+ if (isConference) {
+ if (isEarlyConference) {
+ displayName = VoipTexts.conference_incoming_title
+ } else {
+ displayName = "\(VoipTexts.conference_incoming_title): \(CallsViewModel.shared.currentCallData.value??.remoteConferenceSubject.value ?? "") (\(CallsViewModel.shared.currentCallData.value??.conferenceParticipantsCountLabel.value ?? ""))"
+ }
+ } else {
+ displayName = FastAddressBook.displayName(for: addr?.getCobject) ?? "Unknown"
+ }
+
if (CallManager.callKitEnabled()) {
+ if (isEarlyConference) {
+ CallsViewModel.shared.currentCallData.readCurrentAndObserve { _ in
+ let uuid = CallManager.instance().providerDelegate.uuids["\(callId!)"]
+ if (uuid != nil) {
+ displayName = "\(VoipTexts.conference_incoming_title): \(CallsViewModel.shared.currentCallData.value??.remoteConferenceSubject.value ?? "") (\(CallsViewModel.shared.currentCallData.value??.conferenceParticipantsCountLabel.value ?? ""))"
+ CallManager.instance().providerDelegate.updateCall(uuid: uuid!, handle: addr!.asStringUriOnly(), hasVideo: video, displayName: displayName)
+ }
+ }
+ }
+
let uuid = CallManager.instance().providerDelegate.uuids["\(callId!)"]
if (uuid != nil) {
// Tha app is now registered, updated the call already existed.
@@ -672,116 +736,46 @@ import AVFoundation
return speakerCard != nil ? speakerCard : earpieceCard
}
-
-
- // Conference
-
- @objc func hostConference() -> Bool {
- return conference != nil
- }
+ // Local Conference
- func addAllToConference() {
- if (conference == nil) {
- guard let cp = try?lc?.createConferenceParams(conference: conference) else {
- Log.directLog(BCTBX_LOG_ERROR, text: "Unable to create conference parameters")
+ @objc func startLocalConference() {
+ if (CallManager.callKitEnabled()) {
+ let calls = lc?.calls
+ if (calls == nil || calls!.isEmpty) {
return
}
- if let currentCall = lc?.currentCall, let currentParams = currentCall.currentParams {
- cp.videoEnabled = currentParams.videoEnabled
- }
- conference = try?lc?.createConferenceWithParams(params: cp)
- }
- lc?.calls.forEach { call in
- if (call.conference == nil || call.conference?.participantCount == 1) {
- try?conference?.addParticipant(call: call)
- }
- }
- }
-
- @objc func getConference() -> OpaquePointer? {
- guard let core = lc else {
- return nil
- }
- return (core.conference != nil) ? core.conference?.getCobject : (core.currentCall?.conference != nil) ? core.currentCall!.conference!.getCobject : nil
- }
-
- func getConference() -> Conference? {
- guard let core = lc else {
- return nil
- }
- return (core.conference != nil) ? core.conference : (core.currentCall?.conference != nil) ? core.currentCall!.conference : nil
- }
-
- @objc func isInConference() -> Bool {
- return isInConferenceAsHost()||isInConferenceAsGuest()
- }
-
- @objc func isInConferenceAsGuest() -> Bool {
- guard let core = lc else {
- return false
- }
- return !isInConferenceAsHost() && core.currentCall != nil && core.currentCall?.conference != nil && (core.currentCall?.conference!.participantCount)! > 1
- }
-
- @objc func isInConferenceAsHost() -> Bool {
- guard let core = lc else {
- return false
- }
- return core.conference?.isIn == true
- }
-
- @objc func hasConferenceAsGuest() -> Bool {
- guard let core = lc else {
- return false
- }
- if (core.callsNb<=1) {
- return false
- }
- var found = false
- core.calls.forEach {
- let c = $0.conference
- if (c != nil && c!.participantCount > 1 && hostConference()) {
- found = true
+ let firstCall = calls!.first?.callLog?.callId ?? ""
+ let lastCall = (calls!.count > 1) ? calls!.last?.callLog?.callId ?? "" : ""
+
+ let currentUuid = CallManager.instance().providerDelegate.uuids["\(firstCall)"]
+ if (currentUuid == nil) {
+ Log.directLog(BCTBX_LOG_ERROR, text: "Can not find correspondant call to group.")
return
}
+
+ let newUuid = CallManager.instance().providerDelegate.uuids["\(lastCall)"]
+ let groupAction = CXSetGroupCallAction(call: currentUuid!, callUUIDToGroupWith: newUuid)
+ let transcation = CXTransaction(action: groupAction)
+ requestTransaction(transcation, action: "groupCall")
+
+ setResumeCalls()
+ } else {
+ addAllToLocalConference()
}
- return found
}
- @objc func getCallFor(participant : OpaquePointer) -> OpaquePointer? {
- let p = Participant.getSwiftObject(cObject: participant)
- guard let core = lc else {
- return nil
- }
- var call:Call? = nil
- core.calls.forEach { (callIt) in
- let c = callIt.conference
- c?.participantList.forEach { (p2) in
- if (p2.address?.asStringUriOnly() == p.address?.asStringUriOnly()) {
- call = callIt
- return
- }
+ func addAllToLocalConference() {
+ do {
+ if let core = lc, let params = try? core.createConferenceParams(conference: nil) {
+ params.videoEnabled = false // We disable video for local conferencing (cf Android)
+ let conference = core.conference != nil ? core.conference : try core.createConferenceWithParams(params: params)
+ try conference?.addParticipants(calls: core.calls)
}
+ } catch {
+ Log.directLog(BCTBX_LOG_ERROR, text: "accept call failed \(error)")
}
- return call?.getCobject
}
- @objc func inVideoConf() -> Bool {
- guard let core = lc else {
- return false
- }
- let result = isInConference() && (getConference()?.currentParams?.isVideoEnabled == true || core.currentCall?.currentParams?.videoEnabled == true)
- NSLog("cdes \(result) \(core.currentCall?.currentParams?.videoEnabled)")
- return result
- }
-
-
- @objc func inAudioConf() -> Bool {
- guard let core = lc else {
- return false
- }
- return core.conference?.isIn == true && core.conference != nil && core.currentCall?.conference?.currentParams?.isVideoEnabled == false
- }
}
diff --git a/Classes/Swift/Conference/Data/Duration.swift b/Classes/Swift/Conference/Data/Duration.swift
new file mode 100644
index 000000000..46e7954ef
--- /dev/null
+++ b/Classes/Swift/Conference/Data/Duration.swift
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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
+
+struct Duration : Comparable {
+ static func < (lhs: Duration, rhs: Duration) -> Bool {
+ return lhs.value < rhs.value
+ }
+
+ let value: Int
+ let display: String
+}
diff --git a/Classes/Swift/Conference/Data/ScheduledConferenceData.swift b/Classes/Swift/Conference/Data/ScheduledConferenceData.swift
new file mode 100644
index 000000000..22439ee5e
--- /dev/null
+++ b/Classes/Swift/Conference/Data/ScheduledConferenceData.swift
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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
+import linphonesw
+
+
+class ScheduledConferenceData {
+
+ let conferenceInfo: ConferenceInfo
+ let expanded = MutableLiveData()
+ let address = MutableLiveData()
+ let subject = MutableLiveData()
+ let description = MutableLiveData()
+ let time = MutableLiveData()
+ let date = MutableLiveData()
+ let duration = MutableLiveData()
+ let organizer = MutableLiveData()
+ let participantsShort = MutableLiveData()
+ let participantsExpanded = MutableLiveData()
+ let rawDate : Date
+
+
+ init (conferenceInfo: ConferenceInfo) {
+ self.conferenceInfo = conferenceInfo
+ expanded.value = false
+
+ address.value = conferenceInfo.uri?.asStringUriOnly()
+ subject.value = conferenceInfo.subject
+ description.value = conferenceInfo.description
+
+ time.value = TimestampUtils.timeToString(unixTimestamp: Double(conferenceInfo.dateTime))
+ date.value = TimestampUtils.toString(unixTimestamp:Double(conferenceInfo.dateTime), onlyDate:true, shortDate:false)
+ rawDate = Date(timeIntervalSince1970:TimeInterval(conferenceInfo.dateTime))
+
+ let durationFormatter = DateComponentsFormatter()
+ durationFormatter.unitsStyle = .positional
+ durationFormatter.allowedUnits = [.minute, .second ]
+ durationFormatter.zeroFormattingBehavior = [ .pad ]
+ duration.value = conferenceInfo.duration > 0 ? durationFormatter.string(from: TimeInterval(conferenceInfo.duration)) : nil
+
+ organizer.value = conferenceInfo.organizer?.addressBookEnhancedDisplayName()
+
+ computeParticipantsLists()
+ }
+
+ func destroy() {
+ }
+
+ func toggleExpand() {
+ expanded.value = expanded.value == false
+ }
+
+ private func computeParticipantsLists() {
+ participantsShort.value = conferenceInfo.participants.map {(participant) in
+ String(describing: participant.addressBookEnhancedDisplayName())
+ }.joined(separator: ", ")
+
+ participantsExpanded.value = conferenceInfo.participants.map {(participant) in
+ String(describing: participant.addressBookEnhancedDisplayName())+" ("+String(describing: participant.asStringUriOnly())+")"
+ }.joined(separator: "\n")
+ }
+
+ func gotoAssociatedChat() {
+
+ }
+}
diff --git a/Classes/Swift/Conference/Data/TimeZoneData.swift b/Classes/Swift/Conference/Data/TimeZoneData.swift
new file mode 100644
index 000000000..0cbd3a0ae
--- /dev/null
+++ b/Classes/Swift/Conference/Data/TimeZoneData.swift
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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
+import linphonesw
+
+
+struct TimeZoneData : Comparable {
+ let timeZone: TimeZone
+
+ static func == (lhs: TimeZoneData, rhs: TimeZoneData) -> Bool {
+ return lhs.timeZone.identifier == rhs.timeZone.identifier
+ }
+
+ static func < (lhs: TimeZoneData, rhs: TimeZoneData) -> Bool {
+ return lhs.timeZone.secondsFromGMT() < rhs.timeZone.secondsFromGMT()
+
+ }
+
+ func descWithOffset() -> String {
+ return "\(timeZone.identifier) - GMT\(timeZone.offsetInHours())"
+ }
+}
+
+extension TimeZone {
+
+ func offsetFromUTC() -> String
+ {
+ let localTimeZoneFormatter = DateFormatter()
+ localTimeZoneFormatter.timeZone = self
+ localTimeZoneFormatter.dateFormat = "Z"
+ return localTimeZoneFormatter.string(from: Date())
+ }
+
+ func offsetInHours() -> String
+ {
+
+ let hours = secondsFromGMT()/3600
+ let minutes = abs(secondsFromGMT()/60) % 60
+ let tz_hr = String(format: "%+.2d:%.2d", hours, minutes) // "+hh:mm"
+ return tz_hr
+ }
+}
diff --git a/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift b/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift
new file mode 100644
index 000000000..0f7a62b57
--- /dev/null
+++ b/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift
@@ -0,0 +1,249 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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
+ * aDouble with this program. If not, see .
+ */
+
+
+import Foundation
+import linphonesw
+
+class ConferenceSchedulingViewModel {
+
+ var core : Core { get { Core.get() } }
+ static let shared = ConferenceSchedulingViewModel()
+
+ let subject = MutableLiveData()
+ let description = MutableLiveData()
+
+ let scheduleForLater = MutableLiveData()
+ let scheduledDate = MutableLiveData()
+ let scheduledTime = MutableLiveData()
+
+ var scheduledTimeZone = MutableLiveData()
+ static let timeZones: [TimeZoneData] = computeTimeZonesList()
+
+ var scheduledDuration = MutableLiveData()
+ static let durationList: [Duration] = computeDurationList()
+
+ let isEncrypted = MutableLiveData()
+
+ let sendInviteViaChat = MutableLiveData()
+ let sendInviteViaEmail = MutableLiveData()
+
+ let address = MutableLiveData()
+
+ let conferenceCreationInProgress = MutableLiveData()
+
+ let conferenceCreationCompletedEvent: MutableLiveData> = MutableLiveData()
+ let onErrorEvent = MutableLiveData()
+
+ let continueEnabled: MutableLiveData = MutableLiveData()
+
+ let selectedAddresses = MutableLiveData<[Address]>([])
+
+ private var conferenceScheduler: ConferenceScheduler? = nil
+
+
+ private var hour: Int = 0
+ private var minutes: Int = 0
+
+ private var chatRooomDelegate : ChatRoomDelegate? = nil
+ private var conferenceSchedulerDelegate : ConferenceSchedulerDelegateStub? = nil
+
+ var existingConfInfo:ConferenceInfo? = nil
+
+ init () {
+
+ conferenceSchedulerDelegate = ConferenceSchedulerDelegateStub(
+ onStateChanged: { scheduler, state in
+ Log.i("[Conference Creation] Conference scheduler state is \(state)")
+ if (state == .Ready) {
+ Log.i("[Conference Creation] Conference info created, address will be \(scheduler.info?.uri?.asStringUriOnly())")
+ guard let conferenceAddress = scheduler.info?.uri else {
+ Log.e("[Conference Creation] conference address is null")
+ return
+ }
+ self.address.value = conferenceAddress
+
+ if (self.sendInviteViaChat.value == true) {
+ // Send conference info even when conf is not scheduled for later
+ // as the conference server doesn't invite participants automatically
+ if let chatRoomParams = try?self.core.createDefaultChatRoomParams() {
+ chatRoomParams.backend = ChatRoomBackend.FlexisipChat
+ chatRoomParams.groupEnabled = false
+ chatRoomParams.encryptionEnabled = true
+ chatRoomParams.subject = self.subject.value!
+ scheduler.sendInvitations(chatRoomParams: chatRoomParams)
+ }
+ } else {
+ self.conferenceCreationInProgress.value = false
+ self.conferenceCreationCompletedEvent.value = Pair(conferenceAddress.asStringUriOnly(),self.conferenceScheduler?.info?.subject)
+ }
+ }
+ }, onInvitationsSent: { conferenceScheduler, failedInvitations in
+ Log.i("[Conference Creation] Conference information successfully sent to all participants")
+ self.conferenceCreationInProgress.value = false
+
+ if (failedInvitations.count > 0) {
+ failedInvitations.forEach { address in
+ Log.e("[Conference Creation] Conference information wasn't sent to participant \(address.asStringUriOnly())")
+ self.onErrorEvent.value = VoipTexts.conference_schedule_info_not_sent_to_participant+" (\(address.username))"
+ }
+ }
+
+ guard let conferenceAddress = conferenceScheduler.info?.uri else {
+ Log.e("[Conference Creation] conference address is null")
+ return
+ }
+ self.conferenceCreationCompletedEvent.value = Pair(conferenceAddress.asStringUriOnly(),self.conferenceScheduler?.info?.subject)
+ }
+ )
+
+ chatRooomDelegate = ChatRoomDelegateStub(
+ onStateChanged : { (room: ChatRoom, state: ChatRoom.State) -> Void in
+ if (state == ChatRoom.State.Created) {
+ Log.i("[Conference Creation] Chat room created")
+ room.removeDelegate(delegate: self.chatRooomDelegate!)
+ } else if (state == ChatRoom.State.CreationFailed) {
+ Log.e("[Conference Creation] Group chat room creation has failed !")
+ room.removeDelegate(delegate: self.chatRooomDelegate!)
+ }
+ }
+ )
+
+ reset()
+
+ subject.observe { _ in
+ self.continueEnabled.value = self.allMandatoryFieldsFilled()
+ }
+ scheduleForLater.observe { _ in
+ self.continueEnabled.value = self.allMandatoryFieldsFilled()
+ }
+ scheduledDate.observe { _ in
+ self.continueEnabled.value = self.allMandatoryFieldsFilled()
+ }
+ scheduledTime.observe { _ in
+ self.continueEnabled.value = self.allMandatoryFieldsFilled()
+ }
+
+
+ }
+
+ func reset() {
+
+ subject.value = ""
+ scheduleForLater.value = false
+ isEncrypted.value = false
+ sendInviteViaChat.value = true
+ sendInviteViaEmail.value = false
+ let now = Date()
+ scheduledTime.value = Calendar.current.date(from: Calendar.current.dateComponents([.hour, .minute, .second], from: now))
+ scheduledDate.value = Calendar.current.date(from: Calendar.current.dateComponents([.year, .month, .day], from: now))
+
+ scheduledTimeZone.value = ConferenceSchedulingViewModel.timeZones.indices.filter {
+ ConferenceSchedulingViewModel.timeZones[$0].timeZone.identifier == NSTimeZone.default.identifier
+ }.first
+
+ scheduledDuration.value = ConferenceSchedulingViewModel.durationList.indices.filter {
+ ConferenceSchedulingViewModel.durationList[$0].value == 60
+ }.first
+ continueEnabled.value = false
+ selectedAddresses.value = []
+ existingConfInfo = nil
+ description.value = ""
+
+ }
+
+
+
+ func destroy() {
+ conferenceScheduler?.removeDelegate(delegate: conferenceSchedulerDelegate!)
+ }
+
+
+ func gotoChatRoom() {
+
+ }
+
+ func createConference() {
+
+ if (selectedAddresses.value?.count == 0) {
+ Log.e("[Conference Creation] Couldn't create conference without any participant!")
+ return
+ }
+
+ do {
+ conferenceCreationInProgress.value = true
+ guard let localAccount = core.defaultAccount, let localAddress = localAccount.params?.identityAddress else {
+ Log.e("[Conference Creation] Couldn't get local address from default account!")
+ return
+ }
+
+ conferenceScheduler = try? Core.get().createConferenceScheduler()
+ conferenceScheduler?.addDelegate(delegate: conferenceSchedulerDelegate!)
+
+ guard let conferenceInfo = existingConfInfo != nil ? existingConfInfo : try Factory.Instance.createConferenceInfo() else {
+ Log.e("[Conference Creation/Update] Failed, unable to get conf info.")
+ return
+ }
+ conferenceInfo.organizer = localAddress
+ subject.value.map { conferenceInfo.subject = $0}
+ description.value.map { conferenceInfo.description = $0}
+ conferenceInfo.participants = selectedAddresses.value!
+ if (scheduleForLater.value == true) {
+ let timestamp = getConferenceStartTimestamp()
+ conferenceInfo.dateTime = time_t(timestamp)
+ scheduledDuration.value.map { conferenceInfo.duration = UInt(ConferenceSchedulingViewModel.durationList[$0].value) }
+ }
+ conferenceScheduler?.account = localAccount
+ conferenceScheduler?.info = conferenceInfo // Will trigger the conference creation automatically
+ existingConfInfo = conferenceInfo
+
+ } catch {
+ Log.e("[Conference Creation] Failed \(error)")
+ }
+ }
+
+
+
+ private func allMandatoryFieldsFilled() -> Bool {
+ return subject.value != nil && subject.value!.count > 0 && (scheduleForLater.value != true || (scheduledDate.value != nil && scheduledTime.value != nil));
+ }
+
+
+ private func getConferenceStartTimestamp() -> Double {
+ return scheduleForLater.value == true ?
+ scheduledDate.value!.timeIntervalSince1970 +
+ scheduledTime.value!.timeIntervalSince1970 - Calendar.current.startOfDay(for: scheduledTime.value!).timeIntervalSince1970 +
+ Double(ConferenceSchedulingViewModel.timeZones[scheduledTimeZone.value!].timeZone.secondsFromGMT()-TimeZone.current.secondsFromGMT())
+ : Date().timeIntervalSince1970
+ }
+
+
+ private static func computeTimeZonesList() -> [TimeZoneData] {
+ return TimeZone.knownTimeZoneIdentifiers.map {
+ (ident) in TimeZoneData(timeZone:TimeZone(identifier:ident)!)
+ }.sorted()
+ }
+
+ private static func computeDurationList() -> [Duration] {
+ return [Duration(value: 30, display: "30min"), Duration(value: 60, display: "1h"), Duration(value: 120, display: "2h")]
+ }
+
+
+}
diff --git a/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift b/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift
new file mode 100644
index 000000000..14b34a5eb
--- /dev/null
+++ b/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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
+ * aDouble with this program. If not, see .
+ */
+
+
+import Foundation
+import linphonesw
+
+class ConferenceWaitingRoomViewModel: ControlsViewModel {
+
+
+ static let sharedModel = ConferenceWaitingRoomViewModel()
+
+
+ let joinLayout = MutableLiveData()
+ let joinInProgress = MutableLiveData(false)
+ let showLayoutPicker = MutableLiveData()
+
+
+ override init() {
+ super.init()
+ self.reset()
+ }
+
+ func reset() {
+ joinLayout.value = Core.get().defaultConferenceLayout == .Grid ? .Grid : .ActiveSpeaker
+ joinInProgress.value = false
+ isMicrophoneMuted.value = !micAuthorized()
+ isMuteMicrophoneEnabled.value = true
+ isSpeakerSelected.value = true
+ isVideoEnabled.value = false
+ isVideoAvailable.value = core.videoCaptureEnabled
+ showLayoutPicker.value = false
+ }
+
+ override func toggleMuteMicrophone() {
+ if (!micAuthorized()) {
+ AVAudioSession.sharedInstance().requestRecordPermission { granted in
+ if granted {
+ self.isMicrophoneMuted.value = self.isMicrophoneMuted.value != true
+ }
+ }
+ }
+ self.isMicrophoneMuted.value = self.isMicrophoneMuted.value != true
+ }
+
+ override func toggleSpeaker() {
+ isSpeakerSelected.value = isSpeakerSelected.value != true
+ }
+
+ override func toggleVideo() {
+ isVideoEnabled.value = isVideoEnabled.value != true
+ }
+
+ override func updateUI() {
+
+ }
+
+}
diff --git a/Classes/Swift/Conference/ViewModels/ScheduledConferencesViewModel.swift b/Classes/Swift/Conference/ViewModels/ScheduledConferencesViewModel.swift
new file mode 100644
index 000000000..9de2afc4f
--- /dev/null
+++ b/Classes/Swift/Conference/ViewModels/ScheduledConferencesViewModel.swift
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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
+import linphonesw
+
+
+class ScheduledConferencesViewModel {
+
+ var core : Core { get { Core.get() } }
+ static let shared = ScheduledConferencesViewModel()
+
+ var conferences : MutableLiveData<[ScheduledConferenceData]> = MutableLiveData([])
+ var daySplitted : [Date : [ScheduledConferenceData]] = [:]
+ var coreDelegate: CoreDelegateStub?
+
+ init () {
+
+ coreDelegate = CoreDelegateStub(
+ onConferenceInfoReceived: { (core, conferenceInfo) in
+ Log.i("[Scheduled Conferences] New conference info received")
+ self.conferences.value!.append(ScheduledConferenceData(conferenceInfo: conferenceInfo))
+ self.conferences.notifyValue()
+ }
+
+ )
+
+ computeConferenceInfoList()
+ }
+
+ func computeConferenceInfoList() {
+ conferences.value!.removeAll()
+ let now = Date().timeIntervalSince1970 // Linphone uses time_t in seconds
+ let oneHourAgo = now - 3600 // Show all conferences from 1 hour ago and forward
+ core.getConferenceInformationListAfterTime(time: time_t(oneHourAgo)).filter{$0.duration != 0}.forEach { conferenceInfo in
+ conferences.value!.append(ScheduledConferenceData(conferenceInfo: conferenceInfo))
+ }
+
+ daySplitted = [:]
+ conferences.value!.forEach { (conferenceInfo) in
+ let startDateDay = dateAtBeginningOfDay(for: conferenceInfo.rawDate)
+ if (daySplitted[startDateDay] == nil) {
+ daySplitted[startDateDay] = []
+ }
+ daySplitted[startDateDay]!.append(conferenceInfo)
+ }
+ }
+
+
+ func dateAtBeginningOfDay(for inputDate: Date) -> Date {
+ var calendar = Calendar.current
+ let timeZone = NSTimeZone.system as NSTimeZone
+ calendar.timeZone = timeZone as TimeZone
+ return calendar.date(from: calendar.dateComponents([.year, .month, .day], from: inputDate))!
+ }
+
+
+}
diff --git a/Classes/Swift/Conference/Views/ConferenceHistoryDetailsView.swift b/Classes/Swift/Conference/Views/ConferenceHistoryDetailsView.swift
new file mode 100644
index 000000000..f917130ea
--- /dev/null
+++ b/Classes/Swift/Conference/Views/ConferenceHistoryDetailsView.swift
@@ -0,0 +1,188 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import linphonesw
+
+@objc class ConferenceHistoryDetailsView: BackNextNavigationView, UICompositeViewDelegate, UITableViewDataSource {
+
+
+ let participantsListTableView = UITableView()
+ let organizerTableView = UITableView()
+
+ let conectionsListTableView = UITableView()
+ let participantsLabel = StyledLabel(VoipTheme.conference_scheduling_font, " "+VoipTexts.conference_schedule_participants_list)
+ let organiserLabel = StyledLabel(VoipTheme.conference_scheduling_font, " "+VoipTexts.conference_schedule_organizer)
+
+ let datePicker = StyledDatePicker(pickerMode: .date, readOnly:true)
+ let timePicker = StyledDatePicker(pickerMode: .time, readOnly:true)
+
+ var conferenceData : ScheduledConferenceData? {
+ didSet {
+ if let data = conferenceData {
+ super.titleLabel.text = data.subject.value!
+ self.participantsListTableView.reloadData()
+ self.participantsListTableView.removeConstraints().done()
+ self.participantsListTableView.matchParentSideBorders().alignUnder(view: participantsLabel,withMargin: self.form_margin).done()
+ self.participantsListTableView.height(Double(data.conferenceInfo.participants.count) * VoipParticipantCell.cell_height).alignParentBottom().done()
+ datePicker.liveValue = MutableLiveData(conferenceData!.rawDate)
+ timePicker.liveValue = MutableLiveData(conferenceData!.rawDate)
+ }
+ }
+ }
+
+
+ static let compositeDescription = UICompositeViewDescription(ConferenceHistoryDetailsView.self, statusBar: StatusBarView.self, tabBar: nil, sideMenu: SideMenuView.self, fullscreen: false, isLeftFragment: false,fragmentWith: nil)
+ static func compositeViewDescription() -> UICompositeViewDescription! { return compositeDescription }
+ func compositeViewDescription() -> UICompositeViewDescription! { return type(of: self).compositeDescription }
+
+ override func viewDidLoad() {
+
+ super.viewDidLoad(
+ backAction: {
+ PhoneMainView.instance().popView(self.compositeViewDescription())
+ },nextAction: {
+ },
+ nextActionEnableCondition: MutableLiveData(false),
+ title:"")
+ super.nextButton.isHidden = true
+
+
+ let schedulingStack = UIStackView()
+ schedulingStack.axis = .vertical
+ contentView.addSubview(schedulingStack)
+ schedulingStack.alignParentTop(withMargin: 2*form_margin).matchParentSideBorders(insetedByDx: form_margin).done()
+
+ let scheduleForm = UIView()
+ schedulingStack.addArrangedSubview(scheduleForm)
+ scheduleForm.matchParentSideBorders().done()
+
+ // Left column (Date)
+ let leftColumn = UIView()
+ scheduleForm.addSubview(leftColumn)
+ leftColumn.matchParentWidthDividedBy(2.2).alignParentLeft(withMargin: form_margin).alignParentTop(withMargin: form_margin).done()
+
+ let dateLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_date)
+ leftColumn.addSubview(dateLabel)
+ dateLabel.alignParentLeft().alignParentTop(withMargin: form_margin).done()
+
+ leftColumn.addSubview(datePicker)
+ datePicker.alignParentLeft().alignUnder(view: dateLabel,withMargin: form_margin).matchParentSideBorders().done()
+
+ leftColumn.wrapContentY().done()
+
+ // Right column (Time)
+ let rightColumn = UIView()
+ scheduleForm.addSubview(rightColumn)
+ rightColumn.matchParentWidthDividedBy(2.2).alignParentRight(withMargin: form_margin).alignParentTop().done()
+
+ let timeLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_time)
+ rightColumn.addSubview(timeLabel)
+ timeLabel.alignParentLeft().alignParentTop(withMargin: form_margin).done()
+
+ rightColumn.addSubview(timePicker)
+ timePicker.alignParentLeft().alignUnder(view: timeLabel,withMargin: form_margin).matchParentSideBorders().done()
+
+ rightColumn.wrapContentY().done()
+ scheduleForm.wrapContentY().done()
+
+ // Organiser
+
+ organiserLabel.backgroundColor = VoipTheme.voipFormBackgroundColor.get()
+ contentView.addSubview(organiserLabel)
+ organiserLabel.matchParentSideBorders().height(form_input_height).alignUnder(view: schedulingStack,withMargin: form_margin*2).done()
+ organiserLabel.textAlignment = .left
+
+ contentView.addSubview(organizerTableView)
+ organizerTableView.isScrollEnabled = false
+ organizerTableView.dataSource = self
+ organizerTableView.register(VoipParticipantCell.self, forCellReuseIdentifier: "VoipParticipantCellSSchedule")
+ organizerTableView.allowsSelection = false
+ if #available(iOS 15.0, *) {
+ organizerTableView.allowsFocus = false
+ }
+ organizerTableView.separatorStyle = .singleLine
+ organizerTableView.separatorColor = VoipTheme.light_grey_color
+ organizerTableView.tag = 1;
+ organizerTableView.matchParentSideBorders().height(VoipParticipantCell.cell_height).alignUnder(view: organiserLabel,withMargin: form_margin).done()
+
+ // Participants
+ participantsLabel.backgroundColor = VoipTheme.voipFormBackgroundColor.get()
+ contentView.addSubview(participantsLabel)
+ participantsLabel.matchParentSideBorders().height(form_input_height).alignUnder(view: organizerTableView,withMargin: form_margin).done()
+ participantsLabel.textAlignment = .left
+
+ contentView.addSubview(participantsListTableView)
+ participantsListTableView.isScrollEnabled = false
+ participantsListTableView.dataSource = self
+ participantsListTableView.register(VoipParticipantCell.self, forCellReuseIdentifier: "VoipParticipantCellSSchedule")
+ participantsListTableView.allowsSelection = false
+ if #available(iOS 15.0, *) {
+ participantsListTableView.allowsFocus = false
+ }
+ participantsListTableView.separatorStyle = .singleLine
+ participantsListTableView.separatorColor = VoipTheme.light_grey_color
+
+ // Goto chat - v2
+ /*
+ let chatButton = FormButton(title: VoipTexts.conference_go_to_chat.uppercased(), backgroundStateColors: VoipTheme.primary_colors_background)
+ contentView.addSubview(chatButton)
+ chatButton.onClick {
+ //let chatRoom = ChatRoom()
+ //PhoneMainView.instance().go(to: chatRoom?.getCobject)
+ }
+
+ chatButton.centerX().alignParentBottom(withMargin: 3*self.form_margin).alignUnder(view: participantsListTableView,withMargin: 3*self.form_margin).done()
+ */
+
+ }
+
+
+ // Objc - bridge, as can't access easily to the view model.
+ @objc func setCallLog(callLog:OpaquePointer) {
+ let log = CallLog.getSwiftObject(cObject: callLog)
+ if let conferenceInfo = log.conferenceInfo {
+ self.conferenceData = ScheduledConferenceData(conferenceInfo: conferenceInfo)
+ }
+ }
+
+
+ // TableView datasource delegate
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ guard let data = conferenceData else {
+ return 0
+ }
+ return tableView.tag == 1 ? 1 : data.conferenceInfo.participants.count
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell:VoipParticipantCell = tableView.dequeueReusableCell(withIdentifier: "VoipParticipantCellSSchedule") as! VoipParticipantCell
+ guard let data = conferenceData else {
+ return cell
+ }
+ cell.selectionStyle = .none
+ cell.scheduleConfParticipantAddress = tableView.tag == 1 ? data.conferenceInfo.participants.filter {$0.weakEqual(address2: data.conferenceInfo.organizer!)}.first : data.conferenceInfo.participants[indexPath.row]
+ cell.limeBadge.isHidden = true
+ return cell
+ }
+
+
+}
diff --git a/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift b/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift
new file mode 100644
index 000000000..d236acdf4
--- /dev/null
+++ b/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift
@@ -0,0 +1,274 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import linphonesw
+import SVProgressHUD
+
+@objc class ConferenceSchedulingSummaryView: BackNextNavigationView, UICompositeViewDelegate, UITableViewDataSource {
+
+ let CONFERENCE_CREATION_TIME_OUT_SEC = 15.0
+
+ let participantsListTableView = UITableView()
+
+ let datePicker = StyledDatePicker(liveValue: ConferenceSchedulingViewModel.shared.scheduledDate,pickerMode: .date, readOnly:true)
+ let timeZoneValue = StyledValuePicker(liveIndex: ConferenceSchedulingViewModel.shared.scheduledTimeZone,options: ConferenceSchedulingViewModel.timeZones.map({ (tzd: TimeZoneData) -> String in tzd.descWithOffset()}), readOnly:true)
+ let durationValue = StyledValuePicker(liveIndex: ConferenceSchedulingViewModel.shared.scheduledDuration,options: ConferenceSchedulingViewModel.durationList.map({ (duration: Duration) -> String in duration.display}), readOnly:true)
+ let timePicker = StyledDatePicker(liveValue: ConferenceSchedulingViewModel.shared.scheduledTime,pickerMode: .time, readOnly:true)
+ let descriptionInput = StyledTextView(VoipTheme.conference_scheduling_font, placeHolder:VoipTexts.conference_schedule_description_hint,liveValue: ConferenceSchedulingViewModel.shared.description, readOnly:true)
+ let createButton = FormButton(backgroundStateColors: VoipTheme.primary_colors_background)
+
+
+ static let compositeDescription = UICompositeViewDescription(ConferenceSchedulingSummaryView.self, statusBar: StatusBarView.self, tabBar: nil, sideMenu: SideMenuView.self, fullscreen: false, isLeftFragment: false,fragmentWith: nil)
+ static func compositeViewDescription() -> UICompositeViewDescription! { return compositeDescription }
+ func compositeViewDescription() -> UICompositeViewDescription! { return type(of: self).compositeDescription }
+
+ override func viewDidLoad() {
+
+ super.viewDidLoad(
+ backAction: {
+ self.goBackParticipantsListSelection()
+ },nextAction: {
+ },
+ nextActionEnableCondition: ConferenceSchedulingViewModel.shared.continueEnabled,
+ title:VoipTexts.conference_schedule_summary)
+ super.nextButton.isHidden = true
+
+ let subjectLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_subject_title)
+ contentView.addSubview(subjectLabel)
+ subjectLabel.alignParentLeft(withMargin: form_margin).alignParentTop().done()
+
+ let encryptedIcon = UIImageView(image: UIImage(named: "security_2_indicator"))
+ encryptedIcon.contentMode = .scaleAspectFit
+ contentView.addSubview(encryptedIcon)
+ encryptedIcon.height(form_input_height).alignParentTop().alignParentTop().alignParentRight(withMargin: form_margin).alignHorizontalCenterWith(subjectLabel).done()
+ ConferenceSchedulingViewModel.shared.isEncrypted.readCurrentAndObserve { (encrypt) in
+ encryptedIcon.isHidden = encrypt != true
+ }
+
+
+ let subjectInput = StyledTextView(VoipTheme.conference_scheduling_font, placeHolder:VoipTexts.conference_schedule_subject_hint, liveValue: ConferenceSchedulingViewModel.shared.subject, readOnly:true)
+ contentView.addSubview(subjectInput)
+ subjectInput.alignUnder(view: subjectLabel,withMargin: form_margin).matchParentSideBorders(insetedByDx: form_margin).height(form_input_height).done()
+
+
+ let schedulingStack = UIStackView()
+ schedulingStack.axis = .vertical
+ contentView.addSubview(schedulingStack)
+ schedulingStack.alignUnder(view: subjectInput,withMargin: 3*form_margin).matchParentSideBorders(insetedByDx: form_margin).done()
+
+
+ let scheduleForm = UIView()
+ schedulingStack.addArrangedSubview(scheduleForm)
+ scheduleForm.matchParentSideBorders().done()
+ ConferenceSchedulingViewModel.shared.scheduleForLater.readCurrentAndObserve { (forLater) in scheduleForm.isHidden = forLater != true }
+
+ // Left column (Date & Time)
+ let leftColumn = UIView()
+ scheduleForm.addSubview(leftColumn)
+ leftColumn.matchParentWidthDividedBy(2.2).alignParentLeft(withMargin: form_margin).alignParentTop(withMargin: form_margin).done()
+
+ let dateLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_date)
+ leftColumn.addSubview(dateLabel)
+ dateLabel.alignParentLeft().alignParentTop(withMargin: form_margin).done()
+
+ leftColumn.addSubview(datePicker)
+ datePicker.alignParentLeft().alignUnder(view: dateLabel,withMargin: form_margin).matchParentSideBorders().done()
+
+ let timeLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_time)
+ leftColumn.addSubview(timeLabel)
+ timeLabel.alignParentLeft().alignUnder(view: datePicker,withMargin: form_margin).done()
+
+ leftColumn.addSubview(timePicker)
+ timePicker.alignParentLeft().alignUnder(view: timeLabel,withMargin: form_margin).matchParentSideBorders().done()
+
+ leftColumn.wrapContentY().done()
+
+
+ // Right column (Duration & Timezone)
+ let rightColumn = UIView()
+ scheduleForm.addSubview(rightColumn)
+ rightColumn.matchParentWidthDividedBy(2.2).alignParentRight(withMargin: form_margin).alignParentTop().done()
+
+ let durationLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_duration)
+ rightColumn.addSubview(durationLabel)
+ durationLabel.alignParentLeft().alignParentTop(withMargin: form_margin).done()
+
+ rightColumn.addSubview(durationValue)
+ durationValue.alignParentLeft().alignUnder(view: durationLabel,withMargin: form_margin).matchParentSideBorders().done()
+
+ let timeZoneLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_timezone)
+ rightColumn.addSubview(timeZoneLabel)
+ timeZoneLabel.alignParentLeft().alignUnder(view: durationValue,withMargin: form_margin).done()
+
+ rightColumn.addSubview(timeZoneValue)
+ timeZoneValue.alignParentLeft().alignUnder(view: timeZoneLabel,withMargin: form_margin).matchParentSideBorders().done()
+
+ rightColumn.wrapContentY().done()
+
+ // Description
+ let descriptionLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_description_title)
+ scheduleForm.addSubview(descriptionLabel)
+ descriptionLabel.alignUnder(view: leftColumn,withMargin: form_margin).alignUnder(view: rightColumn,withMargin: form_margin).matchParentSideBorders(insetedByDx: form_margin).done()
+
+ descriptionInput.textContainer.maximumNumberOfLines = 5
+ descriptionInput.textContainer.lineBreakMode = .byWordWrapping
+ scheduleForm.addSubview(descriptionInput)
+ descriptionInput.alignUnder(view: descriptionLabel,withMargin: form_margin).matchParentSideBorders(insetedByDx: form_margin).height(description_height).alignParentBottom(withMargin: form_margin*2).done()
+
+ scheduleForm.wrapContentY().done()
+
+ // Sending method
+ let viaChatLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_send_invite_chat_summary)
+ contentView.addSubview(viaChatLabel)
+ viaChatLabel.matchParentSideBorders(insetedByDx: form_margin).alignUnder(view: schedulingStack,withMargin: 2*form_margin).done()
+ ConferenceSchedulingViewModel.shared.sendInviteViaChat.readCurrentAndObserve { (sendChat) in
+ viaChatLabel.isHidden = sendChat != true || ConferenceSchedulingViewModel.shared.scheduleForLater.value != true
+ }
+
+ // Participants
+ let participantsLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_participants_list)
+ participantsLabel.backgroundColor = VoipTheme.voipFormBackgroundColor.get()
+ contentView.addSubview(participantsLabel)
+ participantsLabel.matchParentSideBorders().height(form_input_height).alignUnder(view: viaChatLabel,withMargin: form_margin*2).done()
+ participantsLabel.textAlignment = .center
+
+
+ contentView.addSubview(participantsListTableView)
+ participantsListTableView.isScrollEnabled = false
+ participantsListTableView.dataSource = self
+ participantsListTableView.register(VoipParticipantCell.self, forCellReuseIdentifier: "VoipParticipantCellSSchedule")
+ participantsListTableView.allowsSelection = false
+ if #available(iOS 15.0, *) {
+ participantsListTableView.allowsFocus = false
+ }
+ participantsListTableView.separatorStyle = .singleLine
+ participantsListTableView.separatorColor = VoipTheme.light_grey_color
+
+ ConferenceSchedulingViewModel.shared.selectedAddresses.readCurrentAndObserve { (addresses) in
+ self.participantsListTableView.reloadData()
+ self.participantsListTableView.removeConstraints().done()
+ self.participantsListTableView.matchParentSideBorders().alignUnder(view: participantsLabel,withMargin: self.form_margin).done()
+ self.participantsListTableView.height(Double(addresses!.count) * VoipParticipantCell.cell_height).done()
+ }
+
+ // Create / Schedule
+ contentView.addSubview(createButton)
+ ConferenceSchedulingViewModel.shared.scheduleForLater.readCurrentAndObserve { _ in
+ self.createButton.title = ConferenceSchedulingViewModel.shared.scheduleForLater.value == true ? VoipTexts.conference_schedule_start.uppercased() : VoipTexts.conference_group_call_create.uppercased()
+ self.createButton.addSidePadding()
+ }
+
+ ConferenceSchedulingViewModel.shared.conferenceCreationInProgress.observe { progress in
+ if (progress == true) {
+ SVProgressHUD.show()
+ } else {
+ SVProgressHUD.dismiss()
+ }
+ }
+
+ var enableCreationTimeOut = false
+
+ ConferenceSchedulingViewModel.shared.conferenceCreationCompletedEvent.observe { pair in
+ enableCreationTimeOut = false
+ if (ConferenceSchedulingViewModel.shared.scheduleForLater.value == true) {
+ PhoneMainView.instance().pop(toView:ScheduledConferencesView.compositeDescription)
+ VoipDialog.toast(message: VoipTexts.conference_schedule_info_created)
+
+ }
+ }
+ ConferenceSchedulingViewModel.shared.onErrorEvent.observe { error in
+ VoipDialog.init(message: error!).show()
+ }
+ createButton.onClick {
+ enableCreationTimeOut = true
+ ConferenceSchedulingViewModel.shared.createConference()
+ DispatchQueue.main.asyncAfter(deadline: .now() + self.CONFERENCE_CREATION_TIME_OUT_SEC) {
+ if (enableCreationTimeOut) {
+ enableCreationTimeOut = false
+ ConferenceSchedulingViewModel.shared.conferenceCreationInProgress.value = false
+ ConferenceSchedulingViewModel.shared.onErrorEvent.value = VoipTexts.call_error_server_timeout
+ }
+ }
+ }
+ ConferenceSchedulingViewModel.shared.scheduleForLater.readCurrentAndObserve { (later) in
+ self.createButton.title = ConferenceSchedulingViewModel.shared.scheduleForLater.value == true ? VoipTexts.conference_schedule_start.uppercased() : VoipTexts.conference_group_call_create.uppercased()
+ viaChatLabel.isHidden = later != true || ConferenceSchedulingViewModel.shared.sendInviteViaChat.value != true
+ self.createButton.addSidePadding()
+ }
+
+ createButton.centerX().alignParentBottom(withMargin: 3*self.form_margin).alignUnder(view: participantsListTableView,withMargin: 3*self.form_margin).done()
+
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ datePicker.liveValue = ConferenceSchedulingViewModel.shared.scheduledDate
+ timeZoneValue.setIndex(index: ConferenceSchedulingViewModel.shared.scheduledTimeZone.value!)
+ durationValue.setIndex(index: ConferenceSchedulingViewModel.shared.scheduledDuration.value!)
+ timePicker.liveValue = ConferenceSchedulingViewModel.shared.scheduledTime
+ descriptionInput.text = ConferenceSchedulingViewModel.shared.description.value
+ createButton.addSidePadding()
+ super.viewWillAppear(animated)
+ }
+
+
+
+ func goBackParticipantsListSelection() {
+ let view: ChatConversationCreateView = VIEW(ChatConversationCreateView.compositeViewDescription())
+ let addresses = ConferenceSchedulingViewModel.shared.selectedAddresses.value!.map { (address) in String(address.asStringUriOnly()) }
+ view.tableController.contactsGroup = (addresses as NSArray).mutableCopy() as? NSMutableArray
+ view.tableController.notFirstTime = true
+ view.isForEditing = false
+ view.isForVoipConference = true
+ PhoneMainView.instance().pop(toView: view.compositeViewDescription())
+ }
+
+ // Objc - bridge, as can't access easily to the view model.
+ @objc func setParticipants(addresses:[String]) {
+ ConferenceSchedulingViewModel.shared.selectedAddresses.value = []
+ return addresses.forEach { (address) in
+ if let address = try?Factory.Instance.createAddress(addr: address) {
+ ConferenceSchedulingViewModel.shared.selectedAddresses.value?.append(address)
+ }
+ }
+ }
+
+ // TableView datasource delegate
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ guard let participants = ConferenceSchedulingViewModel.shared.selectedAddresses.value else {
+ return 0
+ }
+ return participants.count
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell:VoipParticipantCell = tableView.dequeueReusableCell(withIdentifier: "VoipParticipantCellSSchedule") as! VoipParticipantCell
+ guard let participant = ConferenceSchedulingViewModel.shared.selectedAddresses.value?[indexPath.row] else {
+ return cell
+ }
+ cell.selectionStyle = .none
+ cell.scheduleConfParticipantAddress = participant
+ cell.limeBadge.isHidden = ConferenceSchedulingViewModel.shared.isEncrypted.value != true
+ return cell
+ }
+
+
+}
diff --git a/Classes/Swift/Conference/Views/ConferenceSchedulingView.swift b/Classes/Swift/Conference/Views/ConferenceSchedulingView.swift
new file mode 100644
index 000000000..c2e85d1ef
--- /dev/null
+++ b/Classes/Swift/Conference/Views/ConferenceSchedulingView.swift
@@ -0,0 +1,227 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import linphonesw
+
+@objc class ConferenceSchedulingView: BackNextNavigationView, UICompositeViewDelegate {
+
+
+ static let compositeDescription = UICompositeViewDescription(ConferenceSchedulingView.self, statusBar: StatusBarView.self, tabBar: nil, sideMenu: SideMenuView.self, fullscreen: false, isLeftFragment: false,fragmentWith: nil)
+ static func compositeViewDescription() -> UICompositeViewDescription! { return compositeDescription }
+ func compositeViewDescription() -> UICompositeViewDescription! { return type(of: self).compositeDescription }
+
+ let datePicker = StyledDatePicker(liveValue: ConferenceSchedulingViewModel.shared.scheduledDate,pickerMode: .date)
+ let timeZoneValue = StyledValuePicker(liveIndex: ConferenceSchedulingViewModel.shared.scheduledTimeZone,options: ConferenceSchedulingViewModel.timeZones.map({ (tzd: TimeZoneData) -> String in tzd.descWithOffset()}))
+ let durationValue = StyledValuePicker(liveIndex: ConferenceSchedulingViewModel.shared.scheduledDuration,options: ConferenceSchedulingViewModel.durationList.map({ (duration: Duration) -> String in duration.display}))
+ let timePicker = StyledDatePicker(liveValue: ConferenceSchedulingViewModel.shared.scheduledTime,pickerMode: .time)
+ let descriptionInput = StyledTextView(VoipTheme.conference_scheduling_font, placeHolder:VoipTexts.conference_schedule_description_hint,liveValue: ConferenceSchedulingViewModel.shared.description)
+
+
+ override func viewDidLoad() {
+
+ super.viewDidLoad(
+ backAction: {
+ PhoneMainView.instance().popView(self.compositeViewDescription())
+ },nextAction: {
+ self.gotoParticipantsListSelection()
+ },
+ nextActionEnableCondition: ConferenceSchedulingViewModel.shared.continueEnabled,
+ title:VoipTexts.conference_group_call_title)
+
+ let subjectLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_subject_title)
+ subjectLabel.addIndicatorIcon(iconName: "voip_mandatory")
+ contentView.addSubview(subjectLabel)
+ subjectLabel.alignParentLeft(withMargin: form_margin).alignParentTop().done()
+
+ let subjectInput = StyledTextView(VoipTheme.conference_scheduling_font, placeHolder:VoipTexts.conference_schedule_subject_hint, liveValue: ConferenceSchedulingViewModel.shared.subject,maxLines:1)
+ contentView.addSubview(subjectInput)
+ subjectInput.alignUnder(view: subjectLabel,withMargin: form_margin).matchParentSideBorders(insetedByDx: form_margin).height(form_input_height).done()
+
+ let schedulingStack = UIStackView()
+ schedulingStack.axis = .vertical
+ schedulingStack.backgroundColor = VoipTheme.voipFormBackgroundColor.get()
+ contentView.addSubview(schedulingStack)
+ schedulingStack.alignUnder(view: subjectInput,withMargin: form_margin).matchParentSideBorders(insetedByDx: form_margin).done()
+
+ let scheduleForLater = UIView()
+ schedulingStack.addArrangedSubview(scheduleForLater)
+ scheduleForLater.matchParentSideBorders().height(schdule_for_later_height).done()
+
+ let laterSwitch = StyledSwitch(liveValue: ConferenceSchedulingViewModel.shared.scheduleForLater)
+ scheduleForLater.addSubview(laterSwitch)
+ laterSwitch.alignParentTop(withMargin: form_margin*2.5).alignParentLeft(withMargin: form_margin).centerY().done()
+
+ let laterLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_later)
+ laterLabel.numberOfLines = 2
+ scheduleForLater.addSubview(laterLabel)
+ laterLabel.alignParentTop(withMargin: form_margin*2).toRightOf(laterSwitch, withLeftMargin: form_margin*2).alignParentRight(withMargin: form_margin).done()
+
+ let scheduleForm = UIView()
+ schedulingStack.addArrangedSubview(scheduleForm)
+ scheduleForm.matchParentSideBorders().done()
+
+ // Left column (Date & Time)
+ let leftColumn = UIView()
+ scheduleForm.addSubview(leftColumn)
+ leftColumn.matchParentWidthDividedBy(2.2).alignParentLeft(withMargin: form_margin).alignParentTop(withMargin: form_margin).done()
+
+ let dateLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_date)
+ dateLabel.addIndicatorIcon(iconName: "voip_mandatory")
+ leftColumn.addSubview(dateLabel)
+ dateLabel.alignParentLeft().alignParentTop(withMargin: form_margin).done()
+
+ leftColumn.addSubview(datePicker)
+ datePicker.alignParentLeft().alignUnder(view: dateLabel,withMargin: form_margin).matchParentSideBorders().done()
+
+ let timeLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_time)
+ timeLabel.addIndicatorIcon(iconName: "voip_mandatory")
+ leftColumn.addSubview(timeLabel)
+ timeLabel.alignParentLeft().alignUnder(view: datePicker,withMargin: form_margin).done()
+
+ leftColumn.addSubview(timePicker)
+ timePicker.alignParentLeft().alignUnder(view: timeLabel,withMargin: form_margin).matchParentSideBorders().done()
+
+ leftColumn.wrapContentY().done()
+
+
+ // Right column (Duration & Timezone)
+ let rightColumn = UIView()
+ scheduleForm.addSubview(rightColumn)
+ rightColumn.matchParentWidthDividedBy(2.2).alignParentRight(withMargin: form_margin).alignParentTop().done()
+
+ let durationLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_duration)
+ rightColumn.addSubview(durationLabel)
+ durationLabel.alignParentLeft().alignParentTop(withMargin: form_margin).done()
+
+ rightColumn.addSubview(durationValue)
+ durationValue.alignParentLeft().alignUnder(view: durationLabel,withMargin: form_margin).matchParentSideBorders().done()
+
+ let timeZoneLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_timezone)
+ rightColumn.addSubview(timeZoneLabel)
+ timeZoneLabel.alignParentLeft().alignUnder(view: durationValue,withMargin: form_margin).done()
+
+ rightColumn.addSubview(timeZoneValue)
+ timeZoneValue.alignParentLeft().alignUnder(view: timeZoneLabel,withMargin: form_margin).matchParentSideBorders().done()
+
+ rightColumn.wrapContentY().done()
+
+ // Description
+ let descriptionLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_description_title)
+ scheduleForm.addSubview(descriptionLabel)
+ descriptionLabel.alignUnder(view: leftColumn,withMargin: form_margin).alignUnder(view: rightColumn,withMargin: form_margin).matchParentSideBorders(insetedByDx: form_margin).done()
+
+ descriptionInput.textContainer.maximumNumberOfLines = 5
+ descriptionInput.textContainer.lineBreakMode = .byWordWrapping
+ scheduleForm.addSubview(descriptionInput)
+ descriptionInput.alignUnder(view: descriptionLabel,withMargin: form_margin).matchParentSideBorders(insetedByDx: form_margin).height(description_height).alignParentBottom(withMargin: form_margin*2).done()
+
+ scheduleForm.wrapContentY().done()
+
+ // Sending methods
+ let viaChatSwitch = StyledCheckBox(liveValue: ConferenceSchedulingViewModel.shared.sendInviteViaChat)
+ contentView.addSubview(viaChatSwitch)
+ viaChatSwitch.alignParentLeft(withMargin: form_margin).alignUnder(view: schedulingStack,withMargin: 2*form_margin).done()
+
+ let viaChatLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_send_invite_chat)
+ contentView.addSubview(viaChatLabel)
+ viaChatLabel.toRightOf(viaChatSwitch,withLeftMargin: form_margin).alignUnder(view: schedulingStack,withMargin: 2*form_margin).alignHorizontalCenterWith(viaChatSwitch).done()
+
+ /* Hidden as in Android 9.6.2022
+
+ let viaMailSwitch = StyledCheckBox(liveValue: ConferenceSchedulingViewModel.shared.sendInviteViaEmail)
+ contentView.addSubview(viaMailSwitch)
+ viaMailSwitch.alignParentLeft(withMargin: form_margin).alignUnder(view: viaChatSwitch,withMargin: 2*form_margin).done()
+
+ let viaMailLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_send_invite_email)
+ contentView.addSubview(viaMailLabel)
+ viaMailLabel.toRightOf(viaMailSwitch,withLeftMargin: form_margin).alignUnder(view: viaChatLabel,withMargin: 2*form_margin).alignHorizontalCenterWith(viaMailSwitch).done()
+
+ // Encryption
+ let encryptLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_encryption)
+ contentView.addSubview(encryptLabel)
+ encryptLabel.alignUnder(view: viaMailLabel,withMargin: 4*form_margin).centerX().done()
+
+ let encryptCombo = UIStackView()
+ contentView.addSubview(encryptCombo)
+ encryptCombo.alignUnder(view: encryptLabel,withMargin: form_margin).centerX().height(form_input_height).done()
+
+ let unencryptedIcon = UIImageView(image: UIImage(named: "security_toggle_icon_grey"))
+ unencryptedIcon.contentMode = .scaleAspectFit
+ encryptCombo.addArrangedSubview(unencryptedIcon)
+
+ let encryptSwitch = StyledSwitch(liveValue: ConferenceSchedulingViewModel.shared.isEncrypted)
+ encryptCombo.addArrangedSubview(encryptSwitch)
+ encryptSwitch.centerY().alignParentTop(withMargin: form_margin).done()
+
+ let encryptedIcon = UIImageView(image: UIImage(named: "security_toggle_icon_green"))
+ encryptedIcon.contentMode = .scaleAspectFit
+ encryptCombo.addArrangedSubview(encryptedIcon)
+
+ */
+
+ // Mandatory label
+
+ let mandatoryLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_mandatory_field)
+ mandatoryLabel.addIndicatorIcon(iconName: "voip_mandatory", trailing: false)
+ contentView.addSubview(mandatoryLabel)
+ mandatoryLabel.alignUnder(view: viaChatSwitch,withMargin: 2*form_margin).centerX().matchParentSideBorders().done()
+ mandatoryLabel.textAlignment = .center
+
+ mandatoryLabel.alignParentBottom().done()
+
+ // Schedule for later observer
+ ConferenceSchedulingViewModel.shared.scheduleForLater.readCurrentAndObserve { (forLater) in
+ scheduleForm.isHidden = forLater != true
+ super.titleLabel.text = forLater == true ? VoipTexts.conference_schedule_title : VoipTexts.conference_group_call_title
+ viaChatSwitch.isHidden = forLater != true
+ viaChatLabel.isHidden = forLater != true
+ }
+
+
+ }
+
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ datePicker.liveValue = ConferenceSchedulingViewModel.shared.scheduledDate
+ timeZoneValue.setIndex(index: ConferenceSchedulingViewModel.shared.scheduledTimeZone.value!)
+ durationValue.setIndex(index: ConferenceSchedulingViewModel.shared.scheduledDuration.value!)
+ timePicker.liveValue = ConferenceSchedulingViewModel.shared.scheduledTime
+ descriptionInput.text = ConferenceSchedulingViewModel.shared.description.value
+ }
+
+ func gotoParticipantsListSelection() {
+ let view: ChatConversationCreateView = self.VIEW(ChatConversationCreateView.compositeViewDescription());
+ let addresses = ConferenceSchedulingViewModel.shared.selectedAddresses.value!.map { (address) in String(address.asStringUriOnly()) }
+ view.tableController.contactsGroup = (addresses as NSArray).mutableCopy() as? NSMutableArray
+ view.isForEditing = false
+ view.isForVoipConference = true
+ view.isForOngoingVoipConference = false
+ view.tableController.notFirstTime = true
+ view.isGroupChat = true
+ PhoneMainView.instance().changeCurrentView(view.compositeViewDescription())
+ }
+
+ @objc func resetViewModel() {
+ ConferenceSchedulingViewModel.shared.reset()
+ }
+}
diff --git a/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift
new file mode 100644
index 000000000..a7c6f0de9
--- /dev/null
+++ b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift
@@ -0,0 +1,209 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import linphonesw
+
+
+@objc class ConferenceWaitingRoomFragment: UIViewController, UICompositeViewDelegate { // Replaces CallView
+
+ // Layout constants
+ let common_margin = 17.0
+ let switch_camera_button_size = 50
+ let switch_camera_button_margins = 7.0
+ let content_inset = 12.0
+ let button_spacing = 15.0
+ let center_view_corner_radius = 20.0
+ let button_width = 150
+
+
+ var audioRoutesView : AudioRoutesView? = nil
+ let subject = StyledLabel(VoipTheme.conference_preview_subject_font)
+ let localVideo = UIView()
+ let switchCamera = UIImageView(image: UIImage(named:"voip_change_camera")?.tinted(with:.white))
+ let noVideoLabel = StyledLabel(VoipTheme.conference_waiting_room_no_video_font, VoipTexts.conference_waiting_room_video_disabled)
+
+ let buttonsView = UIStackView()
+ let cancel = FormButton(title: VoipTexts.cancel.uppercased(), backgroundStateColors: VoipTheme.primary_colors_background_gray, bold:false)
+ let start = FormButton(title: VoipTexts.conference_waiting_room_start_call.uppercased(), backgroundStateColors: VoipTheme.primary_colors_background)
+ let conferenceJoinSpinner = RotatingSpinner()
+
+ var conferenceUrl : String? = nil
+ let conferenceSubject = MutableLiveData()
+
+ static let compositeDescription = UICompositeViewDescription(ConferenceWaitingRoomFragment.self, statusBar: StatusBarView.self, tabBar: nil, sideMenu: nil, fullscreen: false, isLeftFragment: false,fragmentWith: nil)
+ static func compositeViewDescription() -> UICompositeViewDescription! { return compositeDescription }
+ func compositeViewDescription() -> UICompositeViewDescription! { return type(of: self).compositeDescription }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ view.backgroundColor = VoipTheme.voipBackgroundColor.get()
+
+ view.addSubview(subject)
+ subject.centerX().alignParentTop(withMargin: common_margin).done()
+ conferenceSubject.observe { subject in
+ self.subject.text = subject
+ }
+
+ // Controls
+ let controlsView = ControlsView(showVideo: true, controlsViewModel: ConferenceWaitingRoomViewModel.sharedModel)
+ view.addSubview(controlsView)
+ controlsView.alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done()
+
+ // Layoout picker
+ let layoutPicker = CallControlButton(imageInset : UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8),buttonTheme: VoipTheme.conf_waiting_room_layout_picker, onClickAction: {
+ ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.value = ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.value != true
+ })
+ view.addSubview(layoutPicker)
+ layoutPicker.alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).alignParentRight(withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+
+ ConferenceWaitingRoomViewModel.sharedModel.joinLayout.readCurrentAndObserve { layout in
+ var icon = ""
+ switch (layout!) {
+ case .Grid: icon = "voip_conference_mosaic"; break
+ case .ActiveSpeaker: icon = "voip_conference_active_speaker"; break
+ case .AudioOnly:
+ icon = "voip_conference_audio_only"
+ ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value = false
+ break
+ }
+ layoutPicker.applyTintedIcons(tintedIcons: [UIButton.State.normal.rawValue : TintableIcon(name: icon ,tintColor: LightDarkColor(.white,.white))])
+ }
+
+ let layoutPickerView = ConferenceLayoutPickerView()
+ view.addSubview(layoutPickerView)
+ layoutPickerView.alignAbove(view:layoutPicker,withMargin:button_spacing).alignVerticalCenterWith(layoutPicker).done()
+
+ ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.readCurrentAndObserve { show in
+ layoutPicker.isSelected = show == true
+ layoutPickerView.isHidden = show != true
+ if (show == true) {
+ self.view.bringSubviewToFront(layoutPickerView)
+ }
+ }
+
+ // Form buttons
+ buttonsView.axis = .horizontal
+ buttonsView.spacing = button_spacing
+ view.addSubview(buttonsView)
+ buttonsView.alignAbove(view: controlsView,withMargin: SharedLayoutConstants.buttons_bottom_margin).centerX().done()
+
+ start.width(button_width).done()
+ cancel.width(button_width).done()
+
+ buttonsView.addArrangedSubview(cancel)
+ buttonsView.addArrangedSubview(start)
+
+ cancel.onClick {
+ Core.get().calls.forEach { call in
+ if ([Call.State.OutgoingInit, Call.State.OutgoingRinging, Call.State.OutgoingProgress].contains(call.state)) {
+ CallManager.instance().terminateCall(call: call.getCobject)
+ }
+ }
+ ConferenceWaitingRoomViewModel.sharedModel.joinInProgress.value = false
+ PhoneMainView.instance().popView(self.compositeViewDescription())
+ }
+
+ start.onClick {
+ ConferenceWaitingRoomViewModel.sharedModel.joinInProgress.value = true
+ self.conferenceUrl.map{ CallManager.instance().startCall(addr: $0, isSas: false, isVideo: ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value!, isConference: true) }
+ }
+
+ ConferenceWaitingRoomViewModel.sharedModel.joinInProgress.readCurrentAndObserve { joining in
+ self.start.isEnabled = joining != true
+ //self.localVideo.isHidden = joining == true (UX question as video window goes black by the core, better black or hidden ?)
+ self.noVideoLabel.isHidden = joining == true
+ layoutPicker.isHidden = joining == true
+ if (joining == true) {
+ self.view.addSubview(self.conferenceJoinSpinner)
+ self.conferenceJoinSpinner.square(IncomingOutgoingCommonView.spinner_size).center().done()
+ self.conferenceJoinSpinner.startRotation()
+ controlsView.isHidden = true
+ } else {
+ self.conferenceJoinSpinner.stopRotation()
+ self.conferenceJoinSpinner.removeFromSuperview()
+ controlsView.isHidden = false
+ }
+ }
+
+
+ // localVideo view
+ localVideo.layer.cornerRadius = center_view_corner_radius
+ localVideo.clipsToBounds = true
+ localVideo.contentMode = .scaleAspectFill
+ localVideo.backgroundColor = .black
+ self.view.addSubview(localVideo)
+ localVideo.matchParentSideBorders(insetedByDx: content_inset).alignAbove(view:buttonsView,withMargin:SharedLayoutConstants.buttons_bottom_margin).alignUnder(view: subject,withMargin: common_margin).done()
+ localVideo.addSubview(switchCamera)
+ switchCamera.alignParentTop(withMargin: switch_camera_button_margins).alignParentRight(withMargin: switch_camera_button_margins).square(switch_camera_button_size).done()
+ switchCamera.contentMode = .scaleAspectFit
+ switchCamera.onClick {
+ Core.get().videoPreviewEnabled = false
+ Core.get().toggleCamera()
+ Core.get().nativePreviewWindow = self.localVideo
+ Core.get().videoPreviewEnabled = true
+ }
+
+ self.view.addSubview(noVideoLabel)
+ noVideoLabel.center().done()
+
+ ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.readCurrentAndObserve { videoEnabled in
+ Core.get().videoPreviewEnabled = videoEnabled == true
+ self.localVideo.isHidden = videoEnabled != true
+ self.switchCamera.isHidden = videoEnabled != true
+ self.noVideoLabel.isHidden = videoEnabled == true
+ }
+
+
+ // Audio Routes
+ audioRoutesView = AudioRoutesView()
+ view.addSubview(audioRoutesView!)
+ audioRoutesView!.alignBottomWith(otherView: controlsView).done()
+ ConferenceWaitingRoomViewModel.sharedModel.audioRoutesSelected.readCurrentAndObserve { (audioRoutesSelected) in
+ self.audioRoutesView!.isHidden = audioRoutesSelected != true
+ }
+ audioRoutesView!.alignAbove(view:controlsView,withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done()
+
+
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(true)
+ ConferenceWaitingRoomViewModel.sharedModel.audioRoutesSelected.value = false
+ ConferenceWaitingRoomViewModel.sharedModel.reset()
+ Core.get().nativePreviewWindow = localVideo
+ Core.get().videoPreviewEnabled = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ ControlsViewModel.shared.fullScreenMode.value = false
+ Core.get().nativePreviewWindow = nil
+ Core.get().videoPreviewEnabled = false
+ ConferenceWaitingRoomViewModel.sharedModel.joinInProgress.value = false
+ super.viewWillDisappear(animated)
+ }
+
+ @objc func setDetails(subject:String, url:String) {
+ self.conferenceSubject.value = subject
+ self.conferenceUrl = url
+ }
+
+}
diff --git a/Classes/Swift/Conference/Views/ICSBubbleView.swift b/Classes/Swift/Conference/Views/ICSBubbleView.swift
new file mode 100644
index 000000000..7b352ccba
--- /dev/null
+++ b/Classes/Swift/Conference/Views/ICSBubbleView.swift
@@ -0,0 +1,138 @@
+/*
+ * 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 .
+ */
+
+import UIKit
+import Foundation
+import linphonesw
+
+@objc class ICSBubbleView: UIView {
+
+ let corner_radius = 7.0
+ let border_width = 2.0
+ let rows_spacing = 6.0
+ let inner_padding = 8.0
+ let indicator_y = 3.0
+ let share_size = 25
+ let join_share_width = 150.0
+
+
+ let inviteTitle = StyledLabel(VoipTheme.conference_invite_title_font, VoipTexts.conference_invite_title)
+ let subject = StyledLabel(VoipTheme.conference_invite_subject_font)
+ let participants = StyledLabel(VoipTheme.conference_invite_desc_font)
+ let date = StyledLabel(VoipTheme.conference_invite_desc_font)
+ let timeDuration = StyledLabel(VoipTheme.conference_invite_desc_font)
+ let descriptionTitle = StyledLabel(VoipTheme.conference_invite_desc_title_font, VoipTexts.conference_description_title)
+ let descriptionValue = StyledLabel(VoipTheme.conference_invite_desc_font)
+ let joinShare = UIStackView()
+ let join = FormButton(title:VoipTexts.conference_invite_join.uppercased(), backgroundStateColors: VoipTheme.button_green_background)
+ let share = UIImageView(image:UIImage(named:"voip_export")?.tinted(with: VoipTheme.primaryTextColor.get()))
+
+ var icsFile : String? = nil
+
+ var conferenceData: ScheduledConferenceData? = nil {
+ didSet {
+ if let data = conferenceData {
+ subject.text = data.subject.value
+ participants.text = VoipTexts.conference_invite_participants_count.replacingOccurrences(of: "%d", with: String(data.conferenceInfo.participants.count+1))
+ participants.addIndicatorIcon(iconName: "conference_schedule_participants_default",padding : 0.0, y: -indicator_y, trailing: false)
+ date.text = " "+TimestampUtils.dateToString(date: data.rawDate)
+ date.addIndicatorIcon(iconName: "conference_schedule_calendar_default", padding: 0.0, y:-indicator_y, trailing:false)
+ timeDuration.text = " \(data.time.value) ( \(data.duration.value) )"
+ timeDuration.addIndicatorIcon(iconName: "conference_schedule_time_default",padding : 0.0, y: -indicator_y, trailing: false)
+ descriptionTitle.isHidden = data.description.value == nil || data.description.value!.count == 0
+ descriptionValue.isHidden = descriptionTitle.isHidden
+ descriptionValue.text = data.description.value
+ }
+ }
+ }
+
+ init() {
+ super.init(frame:.zero)
+
+ layer.cornerRadius = corner_radius
+ clipsToBounds = true
+ backgroundColor = VoipTheme.voip_light_gray
+
+ let rows = UIStackView()
+ rows.axis = .vertical
+ rows.spacing = rows_spacing
+
+ addSubview(rows)
+
+ rows.addArrangedSubview(inviteTitle)
+ rows.addArrangedSubview(subject)
+ rows.addArrangedSubview(participants)
+ rows.addArrangedSubview(date)
+ rows.addArrangedSubview(timeDuration)
+ rows.addArrangedSubview(descriptionTitle)
+ rows.addArrangedSubview(descriptionValue)
+
+
+ addSubview(joinShare)
+ joinShare.axis = .horizontal
+ joinShare.spacing = rows_spacing
+ joinShare.addArrangedSubview(share)
+ share.square(share_size).done()
+ joinShare.addArrangedSubview(join)
+ rows.matchParentSideBorders(insetedByDx: inner_padding).alignParentTop(withMargin: inner_padding).done()
+ joinShare.alignParentBottom(withMargin: inner_padding).width(join_share_width).alignParentRight(withMargin: inner_padding).done()
+
+ join.onClick {
+ let view : ConferenceWaitingRoomFragment = self.VIEW(ConferenceWaitingRoomFragment.compositeViewDescription())
+ PhoneMainView.instance().changeCurrentView(view.compositeViewDescription())
+ view.setDetails(subject: (self.conferenceData?.subject.value)!, url: (self.conferenceData?.address.value)!)
+ }
+
+ share.onClick {
+ let ics = URL(string: "file://"+self.icsFile!)
+ UIApplication.shared.open(ics!)
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc func setFromChatMessage(cmessage: OpaquePointer) {
+ let message = ChatMessage.getSwiftObject(cObject: cmessage)
+ message.contents.forEach { content in
+ if (content.isIcalendar) {
+ if let conferenceInfo = try? Factory.Instance.createConferenceInfoFromIcalendarContent(content: content) {
+ self.conferenceData = ScheduledConferenceData(conferenceInfo: conferenceInfo)
+ self.icsFile = content.filePath
+ }
+ }
+ }
+ }
+ @objc static func isConferenceInvitationMessage(cmessage: OpaquePointer) -> Bool {
+ var isConferenceInvitationMessage = false
+ let message = ChatMessage.getSwiftObject(cObject: cmessage)
+ message.contents.forEach { content in
+ if (content.isIcalendar) {
+ isConferenceInvitationMessage = true
+ }
+ }
+ return isConferenceInvitationMessage
+ }
+
+ @objc func setLayoutConstraints(view:UIView) {
+ matchDimensionsWith(view: view, insetedByDx: inner_padding).done()
+ }
+
+}
diff --git a/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift b/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift
new file mode 100644
index 000000000..1fd5dcc64
--- /dev/null
+++ b/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift
@@ -0,0 +1,205 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import linphonesw
+
+class ScheduledConferencesCell: UITableViewCell {
+
+ let corner_radius = 7.0
+ let border_width = 2.0
+ static let button_size = 40
+
+ let clockIcon = UIImageView(image: UIImage(named: "conference_schedule_time_default"))
+ let timeDuration = StyledLabel(VoipTheme.conference_invite_desc_font)
+ let organiser = StyledLabel(VoipTheme.conference_invite_desc_font)
+ let subject = StyledLabel(VoipTheme.conference_invite_subject_font)
+ let participantsIcon = UIImageView(image: UIImage(named: "conference_schedule_participants_default"))
+ let participants = StyledLabel(VoipTheme.conference_invite_desc_font)
+ let infoConf = UIButton()
+
+ let descriptionTitle = StyledLabel(VoipTheme.conference_invite_desc_font, VoipTexts.conference_description_title)
+ let descriptionValue = StyledLabel(VoipTheme.conference_invite_desc_font)
+ let urlTitle = StyledLabel(VoipTheme.conference_invite_desc_font, VoipTexts.conference_schedule_address_title)
+ let urlValue = StyledLabel(VoipTheme.conference_scheduling_font)
+ let copyLink = CallControlButton(width:button_size,height:button_size,buttonTheme: VoipTheme.scheduled_conference_action("voip_copy"))
+ let joinConf = FormButton(title:VoipTexts.conference_invite_join.uppercased(), backgroundStateColors: VoipTheme.button_green_background)
+ let deleteConf = CallControlButton(width:button_size,height:button_size,buttonTheme: VoipTheme.scheduled_conference_action("voip_delete"))
+ let editConf = CallControlButton(width:button_size,height:button_size,buttonTheme: VoipTheme.scheduled_conference_action("voip_edit"))
+ var owningTableView : UITableView? = nil
+ let joinEditDelete = UIStackView()
+ let expandedRows = UIStackView()
+
+ var conferenceData: ScheduledConferenceData? = nil {
+ didSet {
+ if let data = conferenceData {
+ timeDuration.text = "\(data.time.value)"+(data.duration.value != nil ? " ( \(data.duration.value) )" : "")
+ organiser.text = VoipTexts.conference_schedule_organizer+data.organizer.value!
+ subject.text = data.subject.value!
+ descriptionValue.text = data.description.value!
+ urlValue.text = data.address.value!
+ data.expanded.readCurrentAndObserve { expanded in
+ self.contentView.layer.borderWidth = expanded == true ? 2.0 : 0.0
+ self.descriptionTitle.isHidden = expanded != true || self.descriptionValue.text?.count == 0
+ self.descriptionValue.isHidden = expanded != true || self.descriptionValue.text?.count == 0
+ self.infoConf.isSelected = expanded == true
+ self.participants.text = expanded == true ? data.participantsExpanded.value : data.participantsShort.value
+ self.participants.numberOfLines = expanded == true ? 6 : 2
+ self.expandedRows.isHidden = expanded != true
+ self.joinEditDelete.isHidden = expanded != true
+ if let myAddress = Core.get().defaultAccount?.params?.identityAddress {
+ self.editConf.isHidden = expanded != true || data.conferenceInfo.organizer?.weakEqual(address2: myAddress) != true
+ } else {
+ self.editConf.isHidden = true
+ }
+ self.participants.removeConstraints().alignUnder(view: self.subject,withMargin: 15).toRightOf(self.participantsIcon,withLeftMargin:10).toRightOf(self.participantsIcon,withLeftMargin:10).toLeftOf(self.infoConf,withRightMargin: 15).done()
+ self.joinEditDelete.removeConstraints().alignUnder(view: self.expandedRows,withMargin: 15).alignParentRight(withMargin: 10).done()
+ if (expanded == true) {
+ self.joinEditDelete.alignParentBottom(withMargin: 10).done()
+ } else {
+ self.participants.alignParentBottom(withMargin: 10).done()
+ }
+
+ }
+ }
+ }
+ }
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ contentView.layer.cornerRadius = corner_radius
+ contentView.clipsToBounds = true
+ contentView.backgroundColor = VoipTheme.header_background_color
+ contentView.layer.borderColor = VoipTheme.primary_color.cgColor
+
+
+ contentView.addSubview(clockIcon)
+ clockIcon.alignParentTop(withMargin: 15).square(15).alignParentLeft(withMargin: 10).done()
+
+ contentView.addSubview(timeDuration)
+ timeDuration.alignParentTop(withMargin: 15).toRightOf(clockIcon,withLeftMargin:10).alignHorizontalCenterWith(clockIcon).done()
+
+ contentView.addSubview(organiser)
+ organiser.alignParentTop(withMargin: 15).toRightOf(timeDuration, withLeftMargin:10).alignParentRight(withMargin:10).alignHorizontalCenterWith(clockIcon).done()
+
+ contentView.addSubview(subject)
+ subject.alignUnder(view: timeDuration,withMargin: 15).alignParentLeft(withMargin: 10).done()
+
+ contentView.addSubview(participantsIcon)
+ participantsIcon.alignUnder(view: subject,withMargin: 15).square(15).alignParentLeft(withMargin: 10).done()
+
+ //infoConf.onClick {
+ contentView.onClick {
+ self.conferenceData?.toggleExpand()
+ self.owningTableView?.reloadData()
+ }
+ contentView.addSubview(infoConf)
+ infoConf.imageView?.contentMode = .scaleAspectFit
+ infoConf.alignUnder(view: subject,withMargin: 15).square(30).alignParentRight(withMargin: 10).alignHorizontalCenterWith(participantsIcon).done()
+ infoConf.applyTintedIcons(tintedIcons: VoipTheme.conference_info_button)
+
+
+ contentView.addSubview(participants)
+ participants.alignUnder(view: subject,withMargin: 15).toRightOf(participantsIcon,withLeftMargin:10).toRightOf(participantsIcon,withLeftMargin:10).toLeftOf(infoConf,withRightMargin: 15).done()
+
+ expandedRows.axis = .vertical
+ expandedRows.spacing = 10
+ contentView.addSubview(expandedRows)
+ expandedRows.alignUnder(view: participants,withMargin: 15).matchParentSideBorders(insetedByDx:10).done()
+
+ expandedRows.addArrangedSubview(descriptionTitle)
+ expandedRows.addArrangedSubview(descriptionValue)
+
+ expandedRows.addArrangedSubview(urlTitle)
+ let urlAndCopy = UIStackView()
+ urlAndCopy.addArrangedSubview(urlValue)
+ urlValue.backgroundColor = .white
+ self.urlValue.isEnabled = false
+ urlValue.alignParentLeft().done()
+ urlAndCopy.addArrangedSubview(copyLink)
+ copyLink.toRightOf(urlValue,withLeftMargin: 10).done()
+ expandedRows.addArrangedSubview(urlAndCopy)
+ copyLink.onClick {
+ UIPasteboard.general.string = self.conferenceData?.address.value!
+ VoipDialog.toast(message: VoipTexts.conference_schedule_address_copied_to_clipboard)
+ }
+
+ joinEditDelete.axis = .horizontal
+ joinEditDelete.spacing = 10
+ joinEditDelete.distribution = .equalSpacing
+
+ contentView.addSubview(joinEditDelete)
+ joinEditDelete.alignUnder(view: expandedRows,withMargin: 15).alignParentRight(withMargin: 10).done()
+
+
+ joinEditDelete.addArrangedSubview(joinConf)
+ joinConf.width(150).done()
+ joinConf.onClick {
+ let view : ConferenceWaitingRoomFragment = self.VIEW(ConferenceWaitingRoomFragment.compositeViewDescription())
+ PhoneMainView.instance().changeCurrentView(view.compositeViewDescription())
+ view.setDetails(subject: (self.conferenceData?.subject.value)!, url: (self.conferenceData?.address.value)!)
+ }
+
+ joinEditDelete.addArrangedSubview(editConf)
+ editConf.onClick {
+ guard let confData = self.conferenceData else {
+ Log.e("Invalid conference date, unable to edit")
+ VoipDialog.toast(message: VoipTexts.conference_edit_error)
+ return
+ }
+ let infoDate = Date(timeIntervalSince1970: Double(confData.conferenceInfo.dateTime))
+ ConferenceSchedulingViewModel.shared.reset()
+ ConferenceSchedulingViewModel.shared.scheduledDate.value = infoDate
+ ConferenceSchedulingViewModel.shared.scheduledTime.value = Date(timeIntervalSince1970: infoDate.timeIntervalSince1970 - Calendar.current.startOfDay(for: infoDate).timeIntervalSince1970)
+ ConferenceSchedulingViewModel.shared.description.value = confData.description.value
+ ConferenceSchedulingViewModel.shared.subject.value = confData.subject.value
+ ConferenceSchedulingViewModel.shared.scheduledDuration.value = ConferenceSchedulingViewModel.durationList.firstIndex(where: {$0.value == confData.conferenceInfo.duration})
+ ConferenceSchedulingViewModel.shared.scheduleForLater.value = true
+ ConferenceSchedulingViewModel.shared.selectedAddresses.value = []
+ confData.conferenceInfo.participants.forEach {
+ ConferenceSchedulingViewModel.shared.selectedAddresses.value?.append($0)
+ }
+ ConferenceSchedulingViewModel.shared.existingConfInfo = confData.conferenceInfo
+ // TOODO TimeZone (as Android 14.6.2022) ConferenceSchedulingViewModel.shared.scheduledTimeZone.value = self.conferenceData?.timezone
+ let view : ConferenceSchedulingView = self.VIEW(ConferenceSchedulingView.compositeViewDescription())
+ PhoneMainView.instance().changeCurrentView(view.compositeViewDescription())
+ }
+
+ joinEditDelete.addArrangedSubview(deleteConf)
+ deleteConf.onClick {
+ let delete = ButtonAttributes(text:VoipTexts.conference_info_confirm_removal_delete, action: {
+ Core.get().deleteConferenceInformation(conferenceInfo: self.conferenceData!.conferenceInfo)
+ ScheduledConferencesViewModel.shared.computeConferenceInfoList()
+ self.owningTableView?.reloadData()
+ VoipDialog.toast(message: VoipTexts.conference_info_removed)
+ }, isDestructive:false)
+ let cancel = ButtonAttributes(text:VoipTexts.cancel, action: {}, isDestructive:true)
+ VoipDialog(message:VoipTexts.conference_info_confirm_removal, givenButtons: [cancel,delete]).show()
+ }
+
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+}
diff --git a/Classes/Swift/Conference/Views/ScheduledConferencesView.swift b/Classes/Swift/Conference/Views/ScheduledConferencesView.swift
new file mode 100644
index 000000000..b37d77942
--- /dev/null
+++ b/Classes/Swift/Conference/Views/ScheduledConferencesView.swift
@@ -0,0 +1,124 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import linphonesw
+
+@objc class ScheduledConferencesView: BackNextNavigationView, UICompositeViewDelegate, UITableViewDataSource, UITableViewDelegate {
+
+ let conferenceListView = UITableView()
+ let noConference = StyledLabel(VoipTheme.empty_list_font,VoipTexts.conference_no_schedule)
+
+ static let compositeDescription = UICompositeViewDescription(ScheduledConferencesView.self, statusBar: StatusBarView.self, tabBar: nil, sideMenu: SideMenuView.self, fullscreen: false, isLeftFragment: false,fragmentWith: nil)
+ static func compositeViewDescription() -> UICompositeViewDescription! { return compositeDescription }
+ func compositeViewDescription() -> UICompositeViewDescription! { return type(of: self).compositeDescription }
+
+ override func viewDidLoad() {
+
+ super.viewDidLoad(
+ backAction: {
+ PhoneMainView.instance().popView(self.compositeViewDescription())
+ },nextAction: {
+ ConferenceSchedulingViewModel.shared.reset()
+ PhoneMainView.instance().changeCurrentView(ConferenceSchedulingView.compositeDescription)
+ },
+ nextActionEnableCondition: MutableLiveData(),
+ title:VoipTexts.conference_scheduled)
+
+ super.nextButton.applyTintedIcons(tintedIcons: VoipTheme.conference_create_button)
+
+ self.view.addSubview(conferenceListView)
+ conferenceListView.isScrollEnabled = true
+ conferenceListView.dataSource = self
+ conferenceListView.delegate = self
+ conferenceListView.register(ScheduledConferencesCell.self, forCellReuseIdentifier: "ScheduledConferencesCell")
+ conferenceListView.allowsSelection = false
+ conferenceListView.rowHeight = UITableView.automaticDimension
+ if #available(iOS 15.0, *) {
+ conferenceListView.allowsFocus = false
+ }
+ conferenceListView.separatorStyle = .singleLine
+ conferenceListView.separatorColor = .white
+
+ view.addSubview(noConference)
+ noConference.center().done()
+
+ }
+
+
+ override func viewWillAppear(_ animated: Bool) {
+ ScheduledConferencesViewModel.shared.computeConferenceInfoList()
+ super.viewWillAppear(animated)
+ self.conferenceListView.reloadData()
+ self.conferenceListView.removeConstraints().done()
+ self.conferenceListView.matchParentSideBorders(insetedByDx: 10).alignUnder(view: super.topBar,withMargin: self.form_margin).alignParentBottom().done()
+ noConference.isHidden = !ScheduledConferencesViewModel.shared.daySplitted.isEmpty
+ super.nextButton.isEnabled = Core.get().defaultAccount != nil
+ }
+
+ // TableView datasource delegate
+
+ func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ let daysArray = Array(ScheduledConferencesViewModel.shared.daySplitted.keys.sorted().reversed())
+ let day = daysArray[section]
+ return TimestampUtils.dateLongToString(date: day)
+ }
+
+ func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
+ guard let header = view as? UITableViewHeaderFooterView else { return }
+ header.textLabel?.applyStyle(VoipTheme.conference_invite_title_font)
+ header.textLabel?.matchParentSideBorders().done()
+ }
+
+ func numberOfSections(in tableView: UITableView) -> Int {
+ return ScheduledConferencesViewModel.shared.daySplitted.keys.count
+ }
+
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ let daysArray = Array(ScheduledConferencesViewModel.shared.daySplitted.keys.sorted().reversed())
+ let day = daysArray[section]
+ return ScheduledConferencesViewModel.shared.daySplitted[day]!.count
+ }
+
+ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+ let daysArray = Array(ScheduledConferencesViewModel.shared.daySplitted.keys.sorted().reversed())
+ let day = daysArray[indexPath.section]
+ guard let data = ScheduledConferencesViewModel.shared.daySplitted[day]?[indexPath.row] else {
+ return UITableView.automaticDimension
+ }
+ return data.expanded.value! ? UITableView.automaticDimension : 100
+ }
+
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell:ScheduledConferencesCell = tableView.dequeueReusableCell(withIdentifier: "ScheduledConferencesCell") as! ScheduledConferencesCell
+ let daysArray = Array(ScheduledConferencesViewModel.shared.daySplitted.keys.sorted().reversed())
+ let day = daysArray[indexPath.section]
+ guard let data = ScheduledConferencesViewModel.shared.daySplitted[day]?[indexPath.row] else {
+ return cell
+ }
+ cell.conferenceData = data
+ cell.owningTableView = tableView
+ return cell
+ }
+
+
+}
diff --git a/Classes/ConfigManager.swift b/Classes/Swift/ConfigManager.swift
similarity index 100%
rename from Classes/ConfigManager.swift
rename to Classes/Swift/ConfigManager.swift
diff --git a/Classes/LinphoneUI/UIMutedMicroButton.m b/Classes/Swift/Extensions/IOS/OptionalExtensions.swift
similarity index 68%
rename from Classes/LinphoneUI/UIMutedMicroButton.m
rename to Classes/Swift/Extensions/IOS/OptionalExtensions.swift
index 8c0679f1f..345b367d7 100644
--- a/Classes/LinphoneUI/UIMutedMicroButton.m
+++ b/Classes/Swift/Extensions/IOS/OptionalExtensions.swift
@@ -17,22 +17,27 @@
* along with this program. If not, see .
*/
-#import "UIMutedMicroButton.h"
-#import "LinphoneManager.h"
-
-@implementation UIMutedMicroButton
-
-- (void)onOn {
- linphone_core_enable_mic(LC, false);
+extension Optional {
+ var orNil: Any {
+ switch self {
+ case .none:
+ return "|⭕️"
+ case let .some(value):
+ return value
+ }
+ }
}
-- (void)onOff {
- linphone_core_enable_mic(LC, true);
+extension Optional: CustomStringConvertible {
+
+ public var description: String {
+ switch self {
+ case .some(let wrappedValue):
+ return "\(wrappedValue)"
+ default:
+ return "|⭕️"
+ }
+ }
}
-- (bool)onUpdate {
- return (linphone_core_get_current_call(LC) || linphone_core_is_in_conference(LC)) && !linphone_core_mic_enabled(LC);
-}
-
-@end
diff --git a/Classes/Swift/Extensions/IOS/UIApplication+Extension.swift b/Classes/Swift/Extensions/IOS/UIApplication+Extension.swift
new file mode 100644
index 000000000..50f5e9353
--- /dev/null
+++ b/Classes/Swift/Extensions/IOS/UIApplication+Extension.swift
@@ -0,0 +1,39 @@
+/*
+* Copyright (c) 2010-2020 Belledonne Communications SARL.
+*
+* This file is part of linhome
+*
+* 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
+import UIKit
+
+
+extension UIApplication {
+
+ class func getTopMostViewController() -> UIViewController? {
+ let keyWindow = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
+ if var topController = keyWindow?.rootViewController {
+ while let presentedViewController = topController.presentedViewController {
+ topController = presentedViewController
+ }
+ return topController
+ } else {
+ return nil
+ }
+ }
+}
diff --git a/Classes/LinphoneUI/UIBluetoothButton.m b/Classes/Swift/Extensions/IOS/UIButtonExtensions.swift
similarity index 58%
rename from Classes/LinphoneUI/UIBluetoothButton.m
rename to Classes/Swift/Extensions/IOS/UIButtonExtensions.swift
index 53abc1818..54d9ad997 100644
--- a/Classes/LinphoneUI/UIBluetoothButton.m
+++ b/Classes/Swift/Extensions/IOS/UIButtonExtensions.swift
@@ -17,20 +17,24 @@
* along with this program. If not, see .
*/
-#import "UIBluetoothButton.h"
-#import "../Utils/AudioHelper.h"
-#import "Utils.h"
-#import
+import Foundation
+import SnapKit
+import UIKit
-#include "linphone/linphonecore.h"
-
-@implementation UIBluetoothButton
-#define check_auresult(au, method) \
- if (au != 0) \
- LOGE(@"UIBluetoothButton error for %s: ret=%ld", method, au)
-
-- (bool)onUpdate {
- return false;
+extension UIButton {
+ func addSidePadding(p:CGFloat = 10) {
+ if let w = titleLabel?.textWidth {
+ width(w+2*p).done()
+ }
+ }
+
+ func applyTintedIcons(tintedIcons: [UInt: TintableIcon]) {
+ tintedIcons.keys.forEach { (stateRawValue) in
+ let tintedIcon = tintedIcons[stateRawValue]!
+ UIImage(named:tintedIcon.name).map {
+ setImage($0.tinted(with: tintedIcon.tintColor?.get()),for: UIButton.State(rawValue: stateRawValue))
+ }
+ }
+ }
+
}
-
-@end
diff --git a/Classes/Swift/Extensions/IOS/UIColorExtensions.swift b/Classes/Swift/Extensions/IOS/UIColorExtensions.swift
new file mode 100644
index 000000000..2d4b8efc9
--- /dev/null
+++ b/Classes/Swift/Extensions/IOS/UIColorExtensions.swift
@@ -0,0 +1,40 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+
+extension UIColor {
+ public convenience init(hex: String) {
+ let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
+ var int = UInt64()
+ Scanner(string: hex).scanHexInt64(&int)
+ let a, r, g, b: UInt64
+ switch hex.count {
+ case 3: // RGB (12-bit)
+ (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
+ case 6: // RGB (24-bit)
+ (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
+ case 8: // ARGB (32-bit)
+ (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
+ default:
+ (a, r, g, b) = (255, 0, 0, 0)
+ }
+ self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255)
+ }
+}
diff --git a/Classes/Swift/Extensions/IOS/UIDeviceExtensions.swift b/Classes/Swift/Extensions/IOS/UIDeviceExtensions.swift
new file mode 100644
index 000000000..fa00d6236
--- /dev/null
+++ b/Classes/Swift/Extensions/IOS/UIDeviceExtensions.swift
@@ -0,0 +1,57 @@
+/*
+* Copyright (c) 2010-2020 Belledonne Communications SARL.
+*
+* This file is part of linhome
+*
+* 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
+import UIKit
+import AVFoundation
+
+extension UIDevice {
+ static func ipad() -> Bool {
+ return UIDevice.current.userInterfaceIdiom == .pad
+ }
+ static func vibrate() {
+ if (!ipad()) {
+ AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
+ }
+ }
+ static func is5SorSEGen1() -> Bool {
+ return UIScreen.main.nativeBounds.height == 1136
+ }
+
+ static func hasNotch() -> Bool {
+ if (UserDefaults.standard.bool(forKey: "hasNotch")) {
+ return true
+ }
+ guard #available(iOS 11.0, *), let topPadding = UIApplication.shared.keyWindow?.safeAreaInsets.top, topPadding > 24 else {
+ return false
+ }
+ UserDefaults.standard.setValue(true, forKey: "hasNotch")
+ return true
+ }
+
+ static func notchHeight() -> CGFloat {
+ guard #available(iOS 11.0, *), let topPadding = UIApplication.shared.keyWindow?.safeAreaInsets.top else {
+ return 0
+ }
+ return topPadding
+ }
+
+}
diff --git a/Classes/Swift/Extensions/IOS/UIImageExtensions.swift b/Classes/Swift/Extensions/IOS/UIImageExtensions.swift
new file mode 100644
index 000000000..a409d782a
--- /dev/null
+++ b/Classes/Swift/Extensions/IOS/UIImageExtensions.swift
@@ -0,0 +1,50 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+
+extension UIImage {
+ func tinted(with color: UIColor?) -> UIImage? {
+ if (color == nil) {
+ return self
+ }
+ defer { UIGraphicsEndImageContext() }
+ UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
+ color!.set()
+ self.withRenderingMode(.alwaysTemplate).draw(in: CGRect(origin: .zero, size: self.size))
+ return UIGraphicsGetImageFromCurrentImageContext()
+ }
+
+ func withInsets(insets: UIEdgeInsets) -> UIImage? {
+ UIGraphicsBeginImageContextWithOptions(
+ CGSize(width: self.size.width + insets.left + insets.right,
+ height: self.size.height + insets.top + insets.bottom), false, self.scale)
+ let _ = UIGraphicsGetCurrentContext()
+ let origin = CGPoint(x: insets.left, y: insets.top)
+ self.draw(at: origin)
+ let imageWithInsets = UIGraphicsGetImageFromCurrentImageContext()
+ UIGraphicsEndImageContext()
+ return imageWithInsets
+ }
+
+ func withPadding(padding: CGFloat) -> UIImage? {
+ let insets = UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding)
+ return withInsets(insets: insets)
+ }
+}
diff --git a/Classes/LinphoneUI/UIBluetoothButton.h b/Classes/Swift/Extensions/IOS/UIImageViewExtensions.swift
similarity index 77%
rename from Classes/LinphoneUI/UIBluetoothButton.h
rename to Classes/Swift/Extensions/IOS/UIImageViewExtensions.swift
index f0f27205a..1ef56dd60 100644
--- a/Classes/LinphoneUI/UIBluetoothButton.h
+++ b/Classes/Swift/Extensions/IOS/UIImageViewExtensions.swift
@@ -1,7 +1,7 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
- * This file is part of linphone-iphone
+ * 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
@@ -17,11 +17,11 @@
* along with this program. If not, see .
*/
-#import
-#import "UIToggleButton.h"
-
-@interface UIBluetoothButton : UIToggleButton {
+import Foundation
+extension UIImageView {
+ func tint(_ color:UIColor) {
+ self.image = self.image?.withRenderingMode(.alwaysTemplate)
+ tintColor = color
+ }
}
-
-@end
diff --git a/Classes/Swift/Extensions/IOS/UILabelExtensions.swift b/Classes/Swift/Extensions/IOS/UILabelExtensions.swift
new file mode 100644
index 000000000..6cf1b23ed
--- /dev/null
+++ b/Classes/Swift/Extensions/IOS/UILabelExtensions.swift
@@ -0,0 +1,32 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import SnapKit
+import UIKit
+
+extension UILabel {
+ var textWidth: CGFloat? {
+ guard let myText = self.text else { return nil }
+ guard let myFont = self.font else { return nil }
+ let rect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
+ let labelSize = myText.boundingRect(with: rect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: myFont], context: nil)
+ return ceil(labelSize.width)
+ }
+}
diff --git a/Classes/LinphoneUI/UIMutedMicroButton.h b/Classes/Swift/Extensions/IOS/UIVIewControllerExtensions.swift
similarity index 72%
rename from Classes/LinphoneUI/UIMutedMicroButton.h
rename to Classes/Swift/Extensions/IOS/UIVIewControllerExtensions.swift
index 456f818d5..8c27c7dec 100644
--- a/Classes/LinphoneUI/UIMutedMicroButton.h
+++ b/Classes/Swift/Extensions/IOS/UIVIewControllerExtensions.swift
@@ -1,7 +1,7 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
- * This file is part of linphone-iphone
+ * 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
@@ -17,11 +17,13 @@
* along with this program. If not, see .
*/
-#import
+import Foundation
+import SnapKit
+import UIKit
-#import "UIToggleButton.h"
-
-@interface UIMutedMicroButton : UIToggleButton {
+extension UIViewController {
+ func VIEW( _ desc: UICompositeViewDescription) -> T{
+ return PhoneMainView.instance().mainViewController.getCachedController(desc.name) as! T
+ }
+
}
-
-@end
diff --git a/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift b/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift
new file mode 100644
index 000000000..7d974d818
--- /dev/null
+++ b/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift
@@ -0,0 +1,418 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import SnapKit
+import UIKit
+
+extension UIView {
+
+ // Few constraints wrapper to abstract SnapKit functions
+
+ func removeConstraints() -> UIView {
+ snp.removeConstraints()
+ return self
+ }
+
+
+ func square(_ size:Int) -> UIView {
+ snp.makeConstraints { (make) in
+ make.width.equalTo(size)
+ make.height.equalTo(size)
+ }
+ return self
+ }
+
+
+ func makeHeightMatchWidth() -> UIView {
+ snp.makeConstraints { (make) in
+ make.height.equalTo(snp.width)
+ }
+ return self
+ }
+
+ func size(w:CGFloat,h:CGFloat) -> UIView {
+ snp.makeConstraints { (make) in
+ make.width.equalTo(w)
+ make.height.equalTo(h)
+ }
+ return self
+ }
+
+ func height(_ h:CGFloat) -> UIView {
+ snp.makeConstraints { (make) in
+ make.height.equalTo(h)
+ }
+ return self
+ }
+
+ func height(_ h:Int) -> UIView {
+ return height(CGFloat(h))
+ }
+
+ func width(_ h:CGFloat) -> UIView {
+ snp.makeConstraints { (make) in
+ make.width.equalTo(h)
+ }
+ return self
+ }
+
+ func width(_ h:Int) -> UIView {
+ return width(CGFloat(h))
+ }
+
+ func maxHeight(_ h:CGFloat) -> UIView {
+ snp.makeConstraints { (make) in
+ make.height.lessThanOrEqualTo(h)
+ }
+ return self
+ }
+
+ func minWidth(_ h:CGFloat) -> UIView {
+ snp.makeConstraints { (make) in
+ make.width.greaterThanOrEqualTo(h)
+ }
+ return self
+ }
+
+
+ func matchParentSideBorders(insetedByDx:CGFloat = 0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.left.equalToSuperview().offset(insetedByDx)
+ make.right.equalToSuperview().offset(-insetedByDx)
+ }
+ return self
+ }
+
+ func matchBordersOf(view:UIView) -> UIView {
+ snp.makeConstraints { (make) in
+ make.left.right.equalTo(view)
+ }
+ return self
+ }
+
+ func matchParentDimmensions() -> UIView {
+ snp.makeConstraints { (make) in
+ make.left.right.top.bottom.equalToSuperview()
+ }
+ return self
+ }
+
+ func matchParentDimmensions(insetedByDx:CGFloat) -> UIView {
+ snp.makeConstraints { (make) in
+ make.left.top.equalToSuperview().offset(insetedByDx)
+ make.right.bottom.equalToSuperview().offset(-insetedByDx)
+ }
+ return self
+ }
+
+ func matchDimensionsWith(view:UIView, insetedByDx:CGFloat = 0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.left.top.equalTo(view).offset(insetedByDx)
+ make.right.bottom.equalTo(view).offset(-insetedByDx)
+ }
+ return self
+ }
+
+ func matchParentEdges() -> UIView {
+ snp.makeConstraints { (make) in
+ make.edges.equalToSuperview()
+ }
+ return self
+ }
+
+ func matchDimentionsOf(view:UIView) -> UIView {
+ snp.makeConstraints { (make) in
+ make.left.right.top.bottom.equalTo(view)
+ }
+ return self
+ }
+
+ func matchParentHeight() -> UIView {
+ snp.makeConstraints { (make) in
+ make.top.bottom.equalToSuperview()
+ }
+ return self
+ }
+
+ func addRightMargin(margin:CGFloat) -> UIView {
+ snp.makeConstraints { (make) in
+ make.rightMargin.equalTo(margin)
+ }
+ return self
+ }
+
+ func matchParentHeightDividedBy(_ divider : CGFloat) -> UIView {
+ snp.makeConstraints { (make) in
+ make.height.equalToSuperview().dividedBy(divider)
+ }
+ return self
+ }
+
+ func matchParentWidthDividedBy(_ divider : CGFloat) -> UIView {
+ snp.makeConstraints { (make) in
+ make.width.equalToSuperview().dividedBy(divider)
+ }
+ return self
+ }
+
+ func center() -> UIView {
+ snp.makeConstraints { (make) in
+ make.center.equalToSuperview()
+ }
+ return self
+ }
+
+ func alignParentTop(withMargin:CGFloat = 0.0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.top.equalToSuperview().offset(withMargin)
+ }
+ return self
+ }
+
+ func alignParentTop(withMargin:Int ) -> UIView {
+ return alignParentTop(withMargin:CGFloat(withMargin))
+ }
+
+
+ func alignUnder(view:UIView, withMargin:CGFloat = 0.0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.top.equalTo(view.snp.bottom).offset(withMargin)
+ }
+ return self
+ }
+ func alignUnder(view:UIView, withMargin:Int) -> UIView {
+ return alignUnder(view: view,withMargin:CGFloat(withMargin))
+ }
+
+ func matchRightOf(view:UIView, withMargin:CGFloat = 0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.right.equalTo(view).offset(withMargin)
+ }
+ return self
+ }
+
+ func updateAlignUnder(view:UIView, withMargin:CGFloat = 0.0) -> UIView {
+ snp.updateConstraints { (make) in
+ make.top.equalTo(view.snp.bottom).offset(withMargin)
+ }
+ return self
+ }
+
+ func alignParentBottom(withMargin:CGFloat = 0.0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.bottom.equalToSuperview().offset(-withMargin)
+ }
+ return self
+ }
+
+ func alignParentBottom(withMargin:Int) -> UIView {
+ return alignParentBottom(withMargin:CGFloat(withMargin))
+ }
+
+ func alignAbove(view:UIView, withMargin:CGFloat = 0.0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.bottom.equalTo(view.snp.top).offset(-withMargin)
+ }
+ return self
+ }
+
+ func alignAbove(view:UIView, withMargin:Int) -> UIView {
+ return alignAbove(view: view,withMargin:CGFloat(withMargin))
+ }
+
+ func alignBottomWith(otherView:UIView) -> UIView {
+ snp.makeConstraints { (make) in
+ make.bottom.equalTo(otherView)
+ }
+ return self
+ }
+
+ func marginLeft(_ m:CGFloat) -> UIView {
+ snp.makeConstraints { (make) in
+ make.left.equalToSuperview().offset(m)
+ }
+ return self
+ }
+
+ func alignParentLeft(withMargin:CGFloat = 0.0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.left.equalToSuperview().offset(withMargin)
+ }
+ return self
+ }
+
+ func alignParentLeft(withMargin:Int) -> UIView {
+ return alignParentLeft(withMargin:CGFloat(withMargin))
+ }
+
+ func alignParentRight(withMargin:Int = 0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.right.equalToSuperview().offset(-withMargin)
+ }
+ return self
+ }
+
+ func alignParentRight(withMargin:CGFloat) -> UIView {
+ return alignParentRight(withMargin:Int(withMargin))
+ }
+
+
+ func toRightOf(_ view:UIView, withLeftMargin:Int = 0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.left.equalTo(view.snp.right).offset(withLeftMargin)
+ }
+ return self
+ }
+
+ func toRightOf(_ view:UIView, withLeftMargin:CGFloat) -> UIView {
+ return toRightOf(view,withLeftMargin: Int(withLeftMargin))
+ }
+
+
+ func alignHorizontalCenterWith(_ view:UIView) -> UIView {
+ snp.makeConstraints { (make) in
+ make.centerY.equalTo(view)
+ }
+ return self
+ }
+
+ func alignVerticalCenterWith(_ view:UIView) -> UIView {
+ snp.makeConstraints { (make) in
+ make.centerX.equalTo(view)
+ }
+ return self
+ }
+
+
+ func toLeftOf(_ view:UIView) -> UIView {
+ snp.makeConstraints { (make) in
+ make.right.equalTo(view.snp.left)
+ }
+ return self
+ }
+
+ func toLeftOf(_ view:UIView, withRightMargin:CGFloat) -> UIView {
+ snp.makeConstraints { (make) in
+ make.right.equalTo(view.snp.left).offset(-withRightMargin)
+ }
+ return self
+ }
+
+ func centerX(withDx:Int = 0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.centerX.equalToSuperview().offset(withDx)
+ }
+ return self
+ }
+
+ func centerY(withDy:Int = 0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.centerY.equalToSuperview().offset(withDy)
+ }
+ return self
+ }
+
+ func matchCenterXOf(view:UIView, withDx:Int = 0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.centerX.equalTo(view).offset(withDx)
+ }
+ return self
+ }
+
+ func matchCenterYOf(view:UIView, withDy:Int = 0) -> UIView {
+ snp.makeConstraints { (make) in
+ make.centerY.equalTo(view).offset(withDy)
+ }
+ return self
+ }
+
+ func wrapContentY() -> UIView {
+ subviews.first?.snp.makeConstraints({ make in
+ make.top.equalToSuperview()
+ })
+ subviews.last?.snp.makeConstraints({ make in
+ make.bottom.equalToSuperview()
+ })
+ return self
+ }
+
+ func wrapContentX() -> UIView {
+ subviews.first?.snp.makeConstraints({ make in
+ make.left.equalToSuperview()
+ })
+ subviews.last?.snp.makeConstraints({ make in
+ make.right.equalToSuperview()
+ })
+ return self
+ }
+
+ func done() {
+ // to avoid the unused variable warning
+ }
+
+ // Onclick
+
+ class TapGestureRecognizer: UITapGestureRecognizer {
+ var action : (()->Void)? = nil
+ }
+
+ func onClick(action : @escaping ()->Void ){
+ let tap = TapGestureRecognizer(target: self , action: #selector(self.handleTap(_:)))
+ tap.action = action
+ tap.numberOfTapsRequired = 1
+ tap.cancelsTouchesInView = false
+
+ self.addGestureRecognizer(tap)
+ self.isUserInteractionEnabled = true
+
+ }
+ @objc func handleTap(_ sender: TapGestureRecognizer) {
+ sender.action!()
+ }
+
+ func VIEW( _ desc: UICompositeViewDescription) -> T{
+ return PhoneMainView.instance().mainViewController.getCachedController(desc.name) as! T
+ }
+
+ // Theming
+
+ func setFormInputBackground(readOnly:Bool) {
+ if (readOnly) {
+ backgroundColor = VoipTheme.voipFormDisabledFieldBackgroundColor.get()
+ } else {
+ layer.borderWidth = 1
+ layer.borderColor = VoipTheme.voipFormFieldBackgroundColor.get().cgColor
+ }
+ layer.cornerRadius = 3
+ clipsToBounds = true
+ }
+
+ @objc func toImage() -> UIImage? {
+ UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0)
+ guard let context = UIGraphicsGetCurrentContext() else { return nil }
+ context.saveGState()
+ layer.render(in: context)
+ context.restoreGState()
+ guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
+ UIGraphicsEndImageContext()
+ return image
+ }
+
+}
diff --git a/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift
new file mode 100644
index 000000000..39bd741f8
--- /dev/null
+++ b/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift
@@ -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 .
+ */
+
+import Foundation
+import linphonesw
+
+extension Address {
+
+ func initials() -> String? {
+ var initials = initials(displayName: addressBookEnhancedDisplayName())
+ if (initials == nil || initials!.isEmpty) {
+ initials = String(username.prefix(1))
+ }
+ return initials
+ }
+
+ private func initials(displayName: String?) -> String? { // Basic ImproveMe
+ return displayName?.components(separatedBy: " ")
+ .reduce("") {
+ ($0.isEmpty ? "" : "\($0.first?.uppercased() ?? "")") +
+ ($1.isEmpty ? "" : "\($1.first?.uppercased() ?? "")")
+ }
+ }
+
+ func addressBookEnhancedDisplayName() -> String? {
+ if let contact = FastAddressBook.getContactWith(getCobject) {
+ return contact.displayName
+ } else if (!displayName.isEmpty) {
+ return displayName
+ } else {
+ return username
+ }
+ }
+
+ func contact() -> Contact? {
+ return FastAddressBook.getContactWith(getCobject)
+ }
+
+}
diff --git a/Classes/Swift/Extensions/LinphoneCore/CallExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/CallExtensions.swift
new file mode 100644
index 000000000..197f48fbc
--- /dev/null
+++ b/Classes/Swift/Extensions/LinphoneCore/CallExtensions.swift
@@ -0,0 +1,48 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import linphonesw
+
+extension Call {
+ func answerVideoUpdateRequest(accept:Bool) {
+ guard let params = try?core? .createCallParams(call: self) else {
+ Log.i("[Call] \(self) unable to answerVideoUpdateRequest : could not create params ")
+ return
+ }
+ if (accept) {
+ params.videoEnabled = true
+ core?.videoCaptureEnabled = true
+ core?.videoDisplayEnabled = true
+ } else {
+ params.videoEnabled = false
+ }
+ try?acceptUpdate(params: params)
+ }
+}
+
+extension Call : CustomStringConvertible {
+ public var description: String {
+ if let callId = callLog?.callId {
+ return ""
+ }
+ return ""
+ }
+}
+
diff --git a/Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift
new file mode 100644
index 000000000..986c44007
--- /dev/null
+++ b/Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift
@@ -0,0 +1,34 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import linphonesw
+
+
+
+extension Conference : CustomStringConvertible {
+ public var description: String {
+ if let username = conferenceAddress?.username {
+ return "<\(username)>"
+ }
+ return ""
+ }
+
+}
+
diff --git a/Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift
new file mode 100644
index 000000000..158c5afec
--- /dev/null
+++ b/Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift
@@ -0,0 +1,43 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import linphonesw
+
+extension Core {
+ static func get() -> Core {
+ return CallManager.instance().lc!
+ }
+
+ func showSwitchCameraButton() -> Bool {
+ return videoDevicesList.count > 2 // Count StaticImage camera
+ }
+
+ func toggleCamera() {
+ Log.i("[Core] Current camera device is \(videoDevice)")
+ var switched = false
+ videoDevicesList.forEach {
+ if (!switched && $0 != videoDevice && $0 != "StaticImage: Static picture") {
+ Log.i("[Core] New camera device will be \($0)")
+ try?setVideodevice(newValue: $0)
+ switched = true
+ }
+ }
+ }
+}
diff --git a/Classes/Swift/Extensions/LinphoneCore/IceState.swift b/Classes/Swift/Extensions/LinphoneCore/IceState.swift
new file mode 100644
index 000000000..7274e2f7e
--- /dev/null
+++ b/Classes/Swift/Extensions/LinphoneCore/IceState.swift
@@ -0,0 +1,35 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import linphonesw
+
+extension IceState {
+ func toString()->String {
+ switch (self) {
+ case .NotActivated: return NSLocalizedString("Not activated", tableName:"ICE has not been activated for this call",comment : "")
+ case .Failed: return NSLocalizedString("Failed", tableName:"ICE processing has failed",comment :"")
+ case .InProgress: return NSLocalizedString("In progress", tableName:"ICE process is in progress",comment :"")
+ case .HostConnection: return NSLocalizedString("Direct connection", tableName:"ICE has established a direct connection to the remote host",comment :"")
+ case .ReflexiveConnection: return NSLocalizedString( "NAT(s) connection", tableName:"ICE has established a connection to the remote host through one or several NATs",comment :"")
+ case .RelayConnection: return NSLocalizedString("Relay connection", tableName:"ICE has established a connection through a relay",comment :"")
+ }
+
+ }
+}
diff --git a/Classes/Swift/Extensions/LinphoneCore/ParticipantExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/ParticipantExtensions.swift
new file mode 100644
index 000000000..9b7b07652
--- /dev/null
+++ b/Classes/Swift/Extensions/LinphoneCore/ParticipantExtensions.swift
@@ -0,0 +1,33 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import linphonesw
+
+
+
+extension Participant : CustomStringConvertible {
+ public var description: String {
+ if let address = address?.asStringUriOnly() {
+ return ""
+ }
+ return ""
+ }
+}
+
diff --git a/Classes/LinphoneUI/UISpeakerButton.h b/Classes/Swift/Extensions/LinphoneCore/PayloadType.swift
similarity index 85%
rename from Classes/LinphoneUI/UISpeakerButton.h
rename to Classes/Swift/Extensions/LinphoneCore/PayloadType.swift
index 34361634e..197a8d733 100644
--- a/Classes/LinphoneUI/UISpeakerButton.h
+++ b/Classes/Swift/Extensions/LinphoneCore/PayloadType.swift
@@ -17,12 +17,11 @@
* along with this program. If not, see .
*/
-#import
-
-#import "UIToggleButton.h"
-
-@interface UISpeakerButton : UIToggleButton {
+import Foundation
+import linphonesw
+import linphone
+extension linphonesw.PayloadType {
+
+
}
-
-@end
diff --git a/Classes/ProviderDelegate.swift b/Classes/Swift/ProviderDelegate.swift
similarity index 97%
rename from Classes/ProviderDelegate.swift
rename to Classes/Swift/ProviderDelegate.swift
index 2c6669a0a..0c4c8d7d7 100644
--- a/Classes/ProviderDelegate.swift
+++ b/Classes/Swift/ProviderDelegate.swift
@@ -32,6 +32,8 @@ import os
var connected = false
var reason: Reason = Reason.None
var displayName: String?
+ var videoEnabled = false
+ var isConference = false
static func newIncomingCallInfo(callId: String) -> CallInfo {
let callInfo = CallInfo()
@@ -39,12 +41,14 @@ import os
return callInfo
}
- static func newOutgoingCallInfo(addr: Address, isSas: Bool, displayName: String) -> CallInfo {
+ static func newOutgoingCallInfo(addr: Address, isSas: Bool, displayName: String, isVideo: Bool, isConference:Bool) -> CallInfo {
let callInfo = CallInfo()
callInfo.isOutgoing = true
callInfo.sasEnabled = isSas
callInfo.toAddr = addr
callInfo.displayName = displayName
+ callInfo.videoEnabled = isVideo
+ callInfo.isConference = isConference
return callInfo
}
}
@@ -254,7 +258,7 @@ extension ProviderDelegate: CXProviderDelegate {
}
CallManager.instance().lc?.configureAudioSession()
- try CallManager.instance().doCall(addr: addr!, isSas: callInfo?.sasEnabled ?? false)
+ try CallManager.instance().doCall(addr: addr!, isSas: callInfo?.sasEnabled ?? false, isVideo: callInfo?.videoEnabled ?? false, isConference: callInfo?.isConference ?? false)
} catch {
Log.directLog(BCTBX_LOG_ERROR, text: "CallKit: Call started failed because \(error)")
action.fail()
@@ -264,7 +268,7 @@ extension ProviderDelegate: CXProviderDelegate {
func provider(_ provider: CXProvider, perform action: CXSetGroupCallAction) {
Log.directLog(BCTBX_LOG_MESSAGE, text: "CallKit: Call grouped callUUid : \(action.callUUID) with callUUID: \(String(describing: action.callUUIDToGroupWith)).")
- CallManager.instance().addAllToConference()
+ CallManager.instance().addAllToLocalConference()
action.fulfill()
}
diff --git a/Classes/SwiftUtil.swift b/Classes/Swift/SwiftUtil.swift
similarity index 100%
rename from Classes/SwiftUtil.swift
rename to Classes/Swift/SwiftUtil.swift
diff --git a/Classes/Swift/Util/BackNextNavigationView.swift b/Classes/Swift/Util/BackNextNavigationView.swift
new file mode 100644
index 000000000..6bb4af676
--- /dev/null
+++ b/Classes/Swift/Util/BackNextNavigationView.swift
@@ -0,0 +1,92 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import linphonesw
+
+@objc class BackNextNavigationView: UIViewController {
+
+
+ // layout constants
+ let top_bar_height = 66.0
+ let navigation_buttons_padding = 18.0
+ let content_margin_top = 20
+ let side_buttons_margin = 5
+
+ // User by subviews
+ let form_margin = 10.0
+ let form_input_height = 40.0
+ let schdule_for_later_height = 80.0
+ let description_height = 150.0
+
+ let titleLabel = StyledLabel(VoipTheme.calls_list_header_font)
+
+ let topBar = UIView()
+ let scrollView = UIScrollView()
+ let contentView = UIView()
+ var backAction : (() -> Void)? = nil
+ var nextAction : (() -> Void)? = nil
+
+ let backButton = CallControlButton(buttonTheme:VoipTheme.nav_button("back_default"))
+ let nextButton = CallControlButton(buttonTheme:VoipTheme.nav_button("next_default"))
+
+ func viewDidLoad(backAction : @escaping () -> Void,
+ nextAction : @escaping () -> Void,
+ nextActionEnableCondition: MutableLiveData,
+ title:String) {
+ self.backAction = backAction
+ self.nextAction = nextAction
+
+ self.view.addSubview(topBar)
+ topBar.alignParentTop().height(top_bar_height).matchParentSideBorders().done()
+
+ topBar.addSubview(backButton)
+ backButton.alignParentLeft(withMargin: side_buttons_margin).matchParentHeight().done()
+ backButton.onClickAction = backAction
+
+ topBar.addSubview(nextButton)
+ nextButton.alignParentRight(withMargin: side_buttons_margin).matchParentHeight().done()
+ nextButton.onClickAction = nextAction
+ nextActionEnableCondition.readCurrentAndObserve { (enableNext) in
+ self.nextButton.isEnabled = enableNext == true
+ }
+
+ topBar.addSubview(titleLabel)
+ titleLabel.matchParentHeight().centerX().done()
+ titleLabel.text = title
+
+ super.viewDidLoad()
+
+ view.addSubview(scrollView)
+ scrollView.alignUnder(view: topBar, withMargin: content_margin_top).alignParentBottom().matchParentSideBorders().done()
+ scrollView.addSubview(contentView)
+ contentView.matchBordersOf(view: view).alignParentBottom().alignParentTop().done() // don't forget a bottom constraint b/w last element of contentview and contentview
+
+ }
+
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ topBar.backgroundColor = VoipTheme.voipToolbarBackgroundColor.get()
+
+ }
+
+}
diff --git a/Classes/Swift/Util/MutableLiveData.swift b/Classes/Swift/Util/MutableLiveData.swift
new file mode 100644
index 000000000..125939785
--- /dev/null
+++ b/Classes/Swift/Util/MutableLiveData.swift
@@ -0,0 +1,138 @@
+/*
+* Copyright (c) 2010-2020 Belledonne Communications SARL.
+*
+* This file is part of linhome
+*
+* 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
+
+
+class MutableLiveDataOnChangeClosure: NSObject {
+ let value: (Type?) -> Void
+ let onlyOnce: Bool
+ init(_ function: @escaping (Type?) -> Void, onlyOnce:Bool = false) {
+ value = function
+ self.onlyOnce = onlyOnce
+ }
+}
+
+class MutableLiveData {
+
+ private var _value : T? = nil
+ private var observers = [MutableLiveDataOnChangeClosure] ()
+ private var _opposite : MutableLiveData? = nil
+
+ init(_ initial:T) {
+ self.value = initial
+ }
+
+ init() {
+ }
+
+ var value : T? {
+ get {
+ return self._value
+ }
+ set {
+ self._value = newValue
+ self.notifyAllObservers(with: newValue)
+ }
+ }
+
+
+ func addObserver(observer: MutableLiveDataOnChangeClosure, andNotifyOnce: Bool = false) {
+ observers.append(observer)
+ if (andNotifyOnce) {
+ notifyValue()
+ }
+ }
+
+ func removeObserver(observer: MutableLiveDataOnChangeClosure) {
+ observers = observers.filter({$0 !== observer})
+ }
+
+
+ func clearObservers() {
+ observers.forEach {
+ removeObserver(observer: $0)
+ }
+ }
+
+
+ func notifyAllObservers(with newValue: T?) {
+ for observer in observers {
+ observer.value(newValue)
+ if (observer.onlyOnce) {
+ removeObserver(observer: observer)
+ }
+ }
+ }
+
+ func notifyValue() {
+ for observer in observers {
+ observer.value(value)
+ if (observer.onlyOnce) {
+ removeObserver(observer: observer)
+ }
+ }
+ }
+
+ func observe(onChange : @escaping (T?)->Void) {
+ let observer = MutableLiveDataOnChangeClosure({ value in
+ onChange(value)
+ }, onlyOnce: false)
+ addObserver(observer: observer)
+ }
+
+ func readCurrentAndObserve(onChange : @escaping (T?)->Void) {
+ let observer = MutableLiveDataOnChangeClosure({ value in
+ onChange(value)
+ }, onlyOnce: false)
+ addObserver(observer: observer)
+ observer.value(value)
+ }
+
+ func observeAsUniqueObserver (onChange : @escaping (T?)->Void, unique: Bool = false) {
+ let observer = MutableLiveDataOnChangeClosure({ value in
+ onChange(value)
+ }, onlyOnce: false)
+ if (unique) {
+ clearObservers()
+ }
+ addObserver(observer: observer)
+ }
+
+ func observeOnce(onChange : @escaping (T?)->Void) {
+ let observer = MutableLiveDataOnChangeClosure({ value in
+ onChange(value)
+ }, onlyOnce: true)
+ addObserver(observer: observer)
+ }
+
+ func opposite() -> MutableLiveData? {
+ if (_opposite != nil) {
+ return _opposite
+ }
+ _opposite = MutableLiveData(!(value! as! Bool))
+ observe { (value) in
+ self._opposite!.value = !(value! as! Bool)
+ }
+ return _opposite
+ }
+
+
+}
diff --git a/Classes/Swift/Util/Pair.swift b/Classes/Swift/Util/Pair.swift
new file mode 100644
index 000000000..f49b6fa98
--- /dev/null
+++ b/Classes/Swift/Util/Pair.swift
@@ -0,0 +1,33 @@
+/*
+* Copyright (c) 2010-2020 Belledonne Communications SARL.
+*
+* This file is part of linhome
+*
+* 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
+
+class Pair {
+ var first:T1
+ var second:T2
+
+ init(_ first:T1, _ second:T2) {
+ self.first = first
+ self.second = second
+ }
+
+
+}
diff --git a/Classes/Swift/Util/TimestampUtils.swift b/Classes/Swift/Util/TimestampUtils.swift
new file mode 100644
index 000000000..d217ee350
--- /dev/null
+++ b/Classes/Swift/Util/TimestampUtils.swift
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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 .
+ */
+
+class TimestampUtils {
+
+ static func is24Hour() -> Bool {
+ let dateFormat = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current)!
+ return dateFormat.firstIndex(of: "a") == nil
+ }
+
+ static func timeToString(unixTimestamp: Double, timestampInSecs: Bool = true) -> String {
+ let date = Date(timeIntervalSince1970: unixTimestamp)
+ let dateFormat = DateFormatter()
+ dateFormat.dateFormat = is24Hour() ? "HH'h'mm" : "h:mm a"
+ return dateFormat.string(from: date)
+ }
+
+ static func toString(
+ unixTimestamp: Double,
+ onlyDate: Bool = false,
+ timestampInSecs: Bool = true,
+ shortDate: Bool = true
+ ) -> String {
+ let date = Date(timeIntervalSince1970: unixTimestamp)
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateStyle = onlyDate ? .none : .long
+ dateFormatter.timeStyle = shortDate ? .short : .long
+ dateFormatter.doesRelativeDateFormatting = true
+ return dateFormatter.string(from: date)
+ }
+
+ static func dateToString(date:Date) -> String {
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateStyle = .short
+ dateFormatter.timeStyle = .none
+ return dateFormatter.string(from: date)
+ }
+
+ static func dateLongToString(date:Date) -> String {
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateStyle = .long
+ dateFormatter.timeStyle = .none
+ return dateFormatter.string(from: date)
+ }
+
+ static func timeToString(date:Date) -> String {
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateStyle = .none
+ dateFormatter.timeStyle = .short
+ return dateFormatter.string(from: date)
+ }
+
+
+}
+
diff --git a/Classes/Swift/Util/ViewModel/MediatorLiveData.swift b/Classes/Swift/Util/ViewModel/MediatorLiveData.swift
new file mode 100644
index 000000000..f040df597
--- /dev/null
+++ b/Classes/Swift/Util/ViewModel/MediatorLiveData.swift
@@ -0,0 +1,48 @@
+/*
+* Copyright (c) 2010-2020 Belledonne Communications SARL.
+*
+* This file is part of linhome
+*
+* 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
+
+
+class MediatorLiveData : MutableLiveData {
+
+ private var sources : [MutableLiveData?] = []
+
+ override init(_ initial:T) {
+ super.init(initial)
+ }
+
+ override init () {
+ super.init()
+ }
+
+ func addSource(_ source: MutableLiveData, _ onSourceChange:@escaping ()->Void) {
+ sources.append(source)
+ source.observe(onChange: { _ in
+ onSourceChange()
+ })
+ }
+
+ func destroy() {
+ sources.forEach { $0?.clearObservers() }
+ clearObservers()
+ }
+
+}
diff --git a/Classes/VFSUtil.swift b/Classes/Swift/VFSUtil.swift
similarity index 99%
rename from Classes/VFSUtil.swift
rename to Classes/Swift/VFSUtil.swift
index 5047fd21c..6dc09d5ae 100644
--- a/Classes/VFSUtil.swift
+++ b/Classes/Swift/VFSUtil.swift
@@ -188,6 +188,7 @@ import os
}
guard let secret = decrypt(encryptedText: encryptedKey) else {
log("[VFS] Unable to decryt encrypted key.", .error)
+ removeExistingVFSKeyIfAny()
return false
}
Factory.Instance.setVfsEncryption(encryptionModule: 2, secret: secret, secretSize: 32)
diff --git a/Classes/Swift/Voip/AudioRouteUtils.swift b/Classes/Swift/Voip/AudioRouteUtils.swift
new file mode 100644
index 000000000..0e1c5cc68
--- /dev/null
+++ b/Classes/Swift/Voip/AudioRouteUtils.swift
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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
+import AVFoundation
+import linphonesw
+
+@objc class AudioRouteUtils : NSObject {
+
+ static var core : Core { get { Core.get() } }
+
+ static private func applyAudioRouteChange( call: Call?, types: [AudioDeviceType], output: Bool = true) {
+ let typesNames = types.map { String(describing: $0) }.joined(separator: "/")
+
+ let currentCall = core.callsNb > 0 ? (call != nil) ? call : core.currentCall != nil ? core.currentCall : core.calls[0] : nil
+ if (currentCall == nil) {
+ Log.w("[Audio Route Helper] No call found, setting audio route on Core")
+ }
+ let conference = core.conference
+ let capability = output ? AudioDeviceCapabilities.CapabilityPlay : AudioDeviceCapabilities.CapabilityRecord
+
+ var found = false
+
+ core.audioDevices.forEach { (audioDevice) in
+ Log.i("[Audio Route Helper] registered coe audio devices are : [\(audioDevice.deviceName)] [\(audioDevice.type)] [\(audioDevice.capabilities)] ")
+ }
+
+ core.audioDevices.forEach { (audioDevice) in
+ if (!found && types.contains(audioDevice.type) && audioDevice.hasCapability(capability: capability)) {
+ if (conference != nil && conference?.isIn == true) {
+ Log.i("[Audio Route Helper] Found [\(audioDevice.type)] \(output ? "playback" : "recorder") audio device [\(audioDevice.deviceName)], routing conference audio to it")
+ if (output) {
+ conference?.outputAudioDevice = audioDevice
+ } else {
+ conference?.inputAudioDevice = audioDevice
+ }
+ } else if (currentCall != nil) {
+ Log.i("[Audio Route Helper] Found [\(audioDevice.type)] \(output ? "playback" : "recorder") audio device [\(audioDevice.deviceName)], routing call audio to it")
+ if (output) {
+ currentCall?.outputAudioDevice = audioDevice
+ }
+ else {
+ currentCall?.inputAudioDevice = audioDevice
+ }
+ } else {
+ Log.i("[Audio Route Helper] Found [\(audioDevice.type)] \(output ? "playback" : "recorder") audio device [\(audioDevice.deviceName)], changing core default audio device")
+ if (output) {
+ core.outputAudioDevice = audioDevice
+ } else {
+ core.inputAudioDevice = audioDevice
+ }
+ }
+ found = true
+ }
+ }
+ if (!found) {
+ Log.e("[Audio Route Helper] Couldn't find \(typesNames) audio device")
+ }
+ }
+
+ static private func changeCaptureDeviceToMatchAudioRoute(call: Call?, types: [AudioDeviceType]) {
+ switch (types.first) {
+ case .Bluetooth :if (isBluetoothAudioRecorderAvailable()) {
+ Log.i("[Audio Route Helper] Bluetooth device is able to record audio, also change input audio device")
+ applyAudioRouteChange(call: call, types: [AudioDeviceType.Bluetooth], output: false)
+ }
+ case .Headset, .Headphones : if (isHeadsetAudioRecorderAvailable()) {
+ Log.i("[Audio Route Helper] Headphones/headset device is able to record audio, also change input audio device")
+ applyAudioRouteChange(call:call,types: [AudioDeviceType.Headphones, AudioDeviceType.Headset], output:false)
+ }
+ default: applyAudioRouteChange(call:call,types: [AudioDeviceType.Microphone], output:false)
+ }
+ }
+
+ static private func routeAudioTo( call: Call?, types: [AudioDeviceType]) {
+ let currentCall = call != nil ? call : core.currentCall != nil ? core.currentCall : (core.callsNb > 0 ? core.calls[0] : nil)
+ if (call != nil || currentCall != nil) {
+ let callToUse = call != nil ? call : currentCall
+ applyAudioRouteChange(call: callToUse, types: types)
+ changeCaptureDeviceToMatchAudioRoute(call: callToUse, types: types)
+ } else {
+ applyAudioRouteChange(call: call, types: types)
+ changeCaptureDeviceToMatchAudioRoute(call: call, types: types)
+ }
+ }
+
+ static func routeAudioToEarpiece(call: Call? = nil) {
+ routeAudioTo(call: call, types: [AudioDeviceType.Microphone]) // on iOS Earpiece = Microphone
+ }
+
+ static func routeAudioToSpeaker(call: Call? = nil) {
+ routeAudioTo(call: call, types: [AudioDeviceType.Speaker])
+ }
+
+ @objc static func routeAudioToSpeaker() {
+ routeAudioTo(call: nil, types: [AudioDeviceType.Speaker])
+ }
+
+ static func routeAudioToBluetooth(call: Call? = nil) {
+ routeAudioTo(call: call, types: [AudioDeviceType.Bluetooth])
+ }
+
+ static func routeAudioToHeadset(call: Call? = nil) {
+ routeAudioTo(call: call, types: [AudioDeviceType.Headphones, AudioDeviceType.Headset])
+ }
+
+ static func isSpeakerAudioRouteCurrentlyUsed(call: Call? = nil) -> Bool {
+
+ let currentCall = core.callsNb > 0 ? (call != nil) ? call : core.currentCall != nil ? core.currentCall : core.calls[0] : nil
+ if (currentCall == nil) {
+ Log.w("[Audio Route Helper] No call found, setting audio route on Core")
+ }
+
+ let conference = core.conference
+ let audioDevice = conference != nil && conference?.isIn == true ? conference!.outputAudioDevice : currentCall != nil ? currentCall!.outputAudioDevice : core.outputAudioDevice
+ Log.i("[Audio Route Helper] Playback audio currently in use is [\(audioDevice?.deviceName ?? "n/a")] with type (\(audioDevice?.type ?? .Unknown)")
+ return audioDevice?.type == AudioDeviceType.Speaker
+ }
+
+ static func isBluetoothAudioRouteCurrentlyUsed(call: Call? = nil) -> Bool {
+ if (core.callsNb == 0) {
+ Log.w("[Audio Route Helper] No call found, so bluetooth audio route isn't used")
+ return false
+ }
+ let currentCall = call != nil ? call : core.currentCall != nil ? core.currentCall : core.calls[0]
+ let conference = core.conference
+
+ let audioDevice = conference != nil && conference?.isIn == true ? conference!.outputAudioDevice : currentCall?.outputAudioDevice
+ Log.i("[Audio Route Helper] Playback audio device currently in use is [\(audioDevice?.deviceName ?? "n/a")] with type (\(audioDevice?.type ?? .Unknown)")
+ return audioDevice?.type == AudioDeviceType.Bluetooth
+ }
+
+ static func isBluetoothAudioRouteAvailable() -> Bool {
+ if let device = core.audioDevices.first(where: { $0.type == AudioDeviceType.Bluetooth && $0.hasCapability(capability: .CapabilityPlay) }) {
+ Log.i("[Audio Route Helper] Found bluetooth audio device [\(device.deviceName)]")
+ return true
+ }
+ return false
+ }
+
+ static private func isBluetoothAudioRecorderAvailable() -> Bool {
+ if let device = core.audioDevices.first(where: { $0.type == AudioDeviceType.Bluetooth && $0.hasCapability(capability: .CapabilityRecord) }) {
+ Log.i("[Audio Route Helper] Found bluetooth audio recorder [\(device.deviceName)]")
+ return true
+ }
+ return false
+ }
+
+ static func isHeadsetAudioRouteAvailable() -> Bool {
+ if let device = core.audioDevices.first(where: { ($0.type == AudioDeviceType.Headset||$0.type == AudioDeviceType.Headphones) && $0.hasCapability(capability: .CapabilityPlay) }) {
+ Log.i("[Audio Route Helper] Found headset/headphones audio device [\(device.deviceName)]")
+ return true
+ }
+ return false
+ }
+
+ static private func isHeadsetAudioRecorderAvailable() -> Bool {
+ if let device = core.audioDevices.first(where: { ($0.type == AudioDeviceType.Headset||$0.type == AudioDeviceType.Headphones) && $0.hasCapability(capability: .CapabilityRecord) }) {
+ Log.i("[Audio Route Helper] Found headset/headphones audio recorder [\(device.deviceName)]")
+ return true
+ }
+ return false
+ }
+
+
+
+ static func isReceiverEnabled() -> Bool {
+ if let outputDevice = core.outputAudioDevice {
+ return outputDevice.type == AudioDeviceType.Microphone
+ }
+ return false
+ }
+
+}
diff --git a/Classes/Swift/Voip/Theme/ButtonTheme.swift b/Classes/Swift/Voip/Theme/ButtonTheme.swift
new file mode 100644
index 000000000..88b8306bb
--- /dev/null
+++ b/Classes/Swift/Voip/Theme/ButtonTheme.swift
@@ -0,0 +1,33 @@
+/*
+ * 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 .
+ */
+
+
+import Foundation
+import UIKit
+
+
+struct ButtonTheme {
+ var tintableStateIcons: [UInt: TintableIcon] // State indexed
+ var backgroundStateColors: [UInt: LightDarkColor] // State indexed
+}
+
+struct TintableIcon {
+ var name:String
+ var tintColor: LightDarkColor? = nil
+}
diff --git a/Classes/Swift/Voip/Theme/LightDarkColor.swift b/Classes/Swift/Voip/Theme/LightDarkColor.swift
new file mode 100644
index 000000000..a01e375b2
--- /dev/null
+++ b/Classes/Swift/Voip/Theme/LightDarkColor.swift
@@ -0,0 +1,42 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+
+class LightDarkColor {
+ var light: UIColor
+ var dark : UIColor
+ init(_ l:UIColor,_ d:UIColor){
+ light = l
+ dark = d
+ }
+
+ func get() -> UIColor {
+ if #available(iOS 13.0, *) {
+ if UITraitCollection.current.userInterfaceStyle == .light {
+ return light
+ } else {
+ return dark
+ }
+ } else {
+ return light
+ }
+ }
+
+}
diff --git a/Classes/Swift/Voip/Theme/TextStyle.swift b/Classes/Swift/Voip/Theme/TextStyle.swift
new file mode 100644
index 000000000..5e242ae43
--- /dev/null
+++ b/Classes/Swift/Voip/Theme/TextStyle.swift
@@ -0,0 +1,101 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import UIKit
+
+struct TextStyle {
+ var fgColor:LightDarkColor
+ var bgColor:LightDarkColor
+ var allCaps:Bool
+ var align:NSTextAlignment
+ var font:String
+ var size:Float
+
+ func boldEd() -> TextStyle {
+ return self.font.contains("Bold") ? self : TextStyle(fgColor: self.fgColor,bgColor: self.bgColor,allCaps: self.allCaps,align: self.align,font: self.font.replacingOccurrences(of: "Regular", with: "Bold"), size: self.size)
+ }
+}
+
+
+extension UILabel {
+ func applyStyle(_ style:TextStyle) {
+ textColor = style.fgColor.get()
+ backgroundColor = style.bgColor.get()
+ if (style.allCaps) {
+ text = self.text?.uppercased()
+ tag = 1
+ }
+ textAlignment = style.align
+ let fontSizeMultiplier: Float = (UIDevice.ipad() ? 1.25 : UIDevice.is5SorSEGen1() ? 0.9 : 1.0)
+ font = UIFont.init(name: style.font, size: CGFloat(style.size*fontSizeMultiplier))
+ }
+
+ func addIndicatorIcon(iconName:String, padding:CGFloat = 5.0, y:CGFloat = 4.0, trailing: Bool = true) {
+ let imageAttachment = NSTextAttachment()
+ imageAttachment.image = UIImage(named:iconName)
+ imageAttachment.bounds = CGRect(x: 5.0, y: y , width: font.lineHeight - 2*padding, height: font.lineHeight - 2*padding)
+ let iconString = NSMutableAttributedString(attachment: imageAttachment)
+ let textXtring = NSMutableAttributedString(string: text != nil ? text! : "")
+ if (trailing) {
+ textXtring.append(iconString)
+ self.text = nil
+ self.attributedText = textXtring
+ } else {
+ iconString.append(textXtring)
+ self.text = nil
+ self.attributedText = iconString
+ }
+ }
+}
+
+extension UIButton {
+ func applyTitleStyle(_ style:TextStyle) {
+ titleLabel?.applyStyle(style)
+ if (style.allCaps) {
+ setTitle(self.title(for: .normal)?.uppercased(), for: .normal)
+ tag = 1
+ }
+ setTitleColor(style.fgColor.get(), for: .normal)
+ contentHorizontalAlignment = style.align == .left ? .left : style.align == .center ? .center : style.align == .right ? .right : .left
+ }
+}
+
+extension UITextView {
+ func applyStyle(_ style:TextStyle) {
+ textColor = style.fgColor.get()
+ backgroundColor = style.bgColor.get()
+ if (style.allCaps) {
+ text = self.text?.uppercased()
+ tag = 1
+ }
+ textAlignment = style.align
+ let fontSizeMultiplier: Float = (UIDevice.ipad() ? 1.25 : UIDevice.is5SorSEGen1() ? 0.9 : 1.0)
+ font = UIFont.init(name: style.font, size: CGFloat(style.size*fontSizeMultiplier))
+ }
+ var numberOfCurrentlyDisplayedLines: Int {
+ return text.components(separatedBy: "\n").count
+ }
+ func removeTextUntilSatisfying(maxNumberOfLines: Int) {
+ while numberOfCurrentlyDisplayedLines > (maxNumberOfLines) {
+ text = String(text.dropLast())
+ }
+ }
+}
+
diff --git a/Classes/Swift/Voip/Theme/VoipTexts.swift b/Classes/Swift/Voip/Theme/VoipTexts.swift
new file mode 100644
index 000000000..3191ae6f6
--- /dev/null
+++ b/Classes/Swift/Voip/Theme/VoipTexts.swift
@@ -0,0 +1,149 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import UIKit
+
+@objc class VoipTexts : NSObject { // From android key names. Added intentionnally with NSLocalizedString calls for each key, so it can be picked up by translation system (Weblate or Xcode).
+
+ static let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as! String
+
+ // FROM ANDROID START
+
+ @objc static let call_action_add_call = NSLocalizedString("Start new call",comment:"")
+ @objc static let call_action_calls_list = NSLocalizedString("Calls list",comment:"")
+ @objc static let call_action_change_conf_layout = NSLocalizedString("Change layout",comment:"")
+ @objc static let call_action_chat = NSLocalizedString("Chat",comment:"")
+ @objc static let call_action_numpad = NSLocalizedString("Numpad",comment:"")
+ @objc static let call_action_participants_list = NSLocalizedString("Participants list",comment:"")
+ @objc static let call_action_statistics = NSLocalizedString("Call statistics",comment:"")
+ @objc static let call_action_transfer_call = NSLocalizedString("Transfer call",comment:"")
+ @objc static let call_context_action_answer = NSLocalizedString("Answer call",comment:"")
+ @objc static let call_context_action_hangup = NSLocalizedString("Terminate call",comment:"")
+ @objc static let call_context_action_pause = NSLocalizedString("Pause call",comment:"")
+ @objc static let call_context_action_resume = NSLocalizedString("Resume call",comment:"")
+ @objc static let call_context_action_transfer = NSLocalizedString("Transfer call",comment:"")
+ @objc static let call_error_declined = NSLocalizedString("Call has been declined",comment:"")
+ @objc static let call_error_generic = NSLocalizedString("Error: %s",comment:"")
+ @objc static let call_error_incompatible_media_params = NSLocalizedString("Incompatible media parameters",comment:"")
+ @objc static let call_error_io_error = NSLocalizedString("Service unavailable or network error",comment:"")
+ @objc static let call_error_network_unreachable = NSLocalizedString("Network is unreachable",comment:"")
+ @objc static let call_error_server_timeout = NSLocalizedString("Server timeout",comment:"")
+ @objc static let call_error_temporarily_unavailable = NSLocalizedString("Temporarily unavailable",comment:"")
+ @objc static let call_error_user_busy = NSLocalizedString("User is busy",comment:"")
+ @objc static let call_error_user_not_found = NSLocalizedString("User hasn't been found",comment:"")
+ @objc static let call_incoming_title = NSLocalizedString("Incoming Call",comment:"")
+ @objc static let call_locally_paused_subtitle = NSLocalizedString("Click on play button to resume it.",comment:"")
+ @objc static let call_locally_paused_title = NSLocalizedString("You have paused the call.",comment:"")
+ @objc static let call_notification_active = NSLocalizedString("Call running",comment:"")
+ @objc static let call_notification_outgoing = NSLocalizedString("Outgoing call",comment:"")
+ @objc static let call_notification_paused = NSLocalizedString("Paused call",comment:"")
+ @objc static let call_outgoing_title = NSLocalizedString("Outgoing Call",comment:"")
+ @objc static let call_remote_recording = NSLocalizedString("This call is being recorded.",comment:"")
+ @objc static let call_remotely_paused_title = NSLocalizedString("Call has been paused by remote.",comment:"")
+ @objc static let call_stats_audio = NSLocalizedString("Audio",comment:"")
+ @objc static let call_stats_capture_filter = NSLocalizedString("Capture filter:",comment:"")
+ @objc static let call_stats_codec = NSLocalizedString("Codec:",comment:"")
+ @objc static let call_stats_decoder_name = NSLocalizedString("Decoder:",comment:"")
+ @objc static let call_stats_download = NSLocalizedString("Download bandwidth:",comment:"")
+ @objc static let call_stats_encoder_name = NSLocalizedString("Encoder:",comment:"")
+ @objc static let call_stats_estimated_download = NSLocalizedString("Estimated download bandwidth:",comment:"")
+ @objc static let call_stats_ice = NSLocalizedString("ICE connectivity:",comment:"")
+ @objc static let call_stats_ip = NSLocalizedString("IP Family:",comment:"")
+ @objc static let call_stats_jitter_buffer = NSLocalizedString("Jitter buffer:",comment:"")
+ @objc static let call_stats_player_filter = NSLocalizedString("Player filter:",comment:"")
+ @objc static let call_stats_receiver_loss_rate = NSLocalizedString("Receiver loss rate:",comment:"")
+ @objc static let call_stats_sender_loss_rate = NSLocalizedString("Sender loss rate:",comment:"")
+ @objc static let call_stats_upload = NSLocalizedString("Upload bandwidth:",comment:"")
+ @objc static let call_stats_video = NSLocalizedString("Video",comment:"")
+ @objc static let call_stats_video_fps_received = NSLocalizedString("Received video fps:",comment:"")
+ @objc static let call_stats_video_fps_sent = NSLocalizedString("Sent video fps:",comment:"")
+ @objc static let call_stats_video_resolution_received = NSLocalizedString("Received video resolution:",comment:"")
+ @objc static let call_stats_video_resolution_sent = NSLocalizedString("Sent video resolution:",comment:"")
+ @objc static let call_video_update_requested_dialog = NSLocalizedString("Correspondent would like to turn the video on",comment:"")
+ @objc static let cancel = NSLocalizedString("Cancel",comment:"")
+ @objc static let chat_room_group_info_admin = NSLocalizedString("Admin",comment:"")
+ @objc static let conference_creation_failed = NSLocalizedString("Failed to create meeting",comment:"")
+ @objc static let conference_default_title = NSLocalizedString("Remote group call",comment:"")
+ @objc static let conference_description_title = NSLocalizedString("Description:",comment:"")
+ @objc static let conference_display_mode_active_speaker = NSLocalizedString("Active speaker mode",comment:"")
+ @objc static let conference_display_mode_audio_only = NSLocalizedString("Audio only mode",comment:"")
+ @objc static let conference_display_mode_mosaic = NSLocalizedString("Mosaic mode",comment:"")
+ @objc static let conference_first_to_join = NSLocalizedString("You're the first to join the group call",comment:"")
+ @objc static let conference_go_to_chat = NSLocalizedString("Meeting's chat room",comment:"")
+ @objc static let conference_group_call_create = NSLocalizedString("Start group call",comment:"")
+ @objc static let conference_group_call_title = NSLocalizedString("Start a group call",comment:"")
+ @objc static let conference_incoming_title = NSLocalizedString("Incoming group call",comment:"")
+ @objc static let conference_info_confirm_removal = NSLocalizedString("Do you really want to delete this meeting?",comment:"")
+ @objc static let conference_info_removed = NSLocalizedString("Meeting info has been deleted",comment:"")
+ @objc static let conference_invite_join = NSLocalizedString("Join",comment:"")
+ @objc static let conference_invite_participants_count = NSLocalizedString("%d participants",comment:"")
+ @objc static let conference_invite_title = NSLocalizedString("Meeting invite:",comment:"")
+ @objc static let conference_last_user = NSLocalizedString("All other participants have left the group call",comment:"")
+ @objc static let conference_local_title = NSLocalizedString("Local group call",comment:"")
+ @objc static let conference_no_schedule = NSLocalizedString("No scheduled meeting yet.",comment:"")
+ @objc static let conference_participant_paused = NSLocalizedString("(paused)",comment:"")
+ @objc static let conference_participants_title = NSLocalizedString("Participants (%d)",comment:"")
+ @objc static let conference_paused_subtitle = NSLocalizedString("Click on play button to join it back.",comment:"")
+ @objc static let conference_paused_title = NSLocalizedString("You are currently out of the meeting.",comment:"")
+ @objc static let conference_schedule_address_copied_to_clipboard = NSLocalizedString("Meeting address copied into clipboard",comment:"")
+ @objc static let conference_schedule_address_title = NSLocalizedString("Meeting address",comment:"")
+ @objc static let conference_schedule_date = NSLocalizedString("Date",comment:"")
+ @objc static let conference_schedule_description_hint = NSLocalizedString("Description",comment:"")
+ @objc static let conference_schedule_description_title = NSLocalizedString("Add a description",comment:"")
+ @objc static let conference_schedule_duration = NSLocalizedString("Duration",comment:"")
+ @objc static let conference_schedule_encryption = NSLocalizedString("Would you like to encrypt the meeting?",comment:"")
+ @objc static let conference_schedule_info_created = NSLocalizedString("Meeting has been scheduled",comment:"")
+ @objc static let conference_schedule_info_not_sent_to_participant = NSLocalizedString("Failed to send meeting info to a participant",comment:"")
+ @objc static let conference_schedule_later = NSLocalizedString("Do you want to schedule a meeting for later?",comment:"")
+ @objc static let conference_schedule_mandatory_field = NSLocalizedString("Mandatory",comment:"")
+ @objc static let conference_schedule_organizer = NSLocalizedString("Organizer:",comment:"")
+ @objc static let conference_schedule_participants_list = NSLocalizedString("Participants list",comment:"")
+ @objc static let conference_schedule_send_invite_chat = NSLocalizedString("Send invite via \(appName);",comment:"")
+ @objc static let conference_schedule_send_invite_chat_summary = NSLocalizedString("Invite will be sent out from my \(appName); account",comment:"")
+ @objc static let conference_schedule_send_invite_email = NSLocalizedString("Send invite via email",comment:"")
+ @objc static let conference_schedule_start = NSLocalizedString("Schedule meeting",comment:"")
+ @objc static let conference_schedule_subject_hint = NSLocalizedString("Meeting subject",comment:"")
+ @objc static let conference_schedule_subject_title = NSLocalizedString("Subject",comment:"")
+ @objc static let conference_schedule_summary = NSLocalizedString("Meeting info",comment:"")
+ @objc static let conference_schedule_time = NSLocalizedString("Time",comment:"")
+ @objc static let conference_schedule_timezone = NSLocalizedString("Timezone",comment:"")
+ @objc static let conference_schedule_title = NSLocalizedString("Schedule a meeting",comment:"")
+ @objc static let conference_scheduled = NSLocalizedString("Meetings",comment:"")
+ @objc static let conference_start_group_call_dialog_message = NSLocalizedString("Do you want to start a group call?\nEveryone in this group will receive a call to join the meeting.",comment:"")
+ @objc static let conference_start_group_call_dialog_ok_button = NSLocalizedString("Start",comment:"")
+ @objc static let conference_too_many_participants_for_mosaic_layout = NSLocalizedString("There is too many participants for mosaic layout, switching to active speaker",comment:"")
+ @objc static let conference_waiting_room_cancel_call = NSLocalizedString("Cancel",comment:"")
+ @objc static let conference_waiting_room_start_call = NSLocalizedString("Start",comment:"")
+ @objc static let conference_waiting_room_video_disabled = NSLocalizedString("Video is currently disabled",comment:"")
+ @objc static let dialog_accept = NSLocalizedString("Accept",comment:"")
+ @objc static let dialog_decline = NSLocalizedString("Decline",comment:"")
+ @objc static let conference_empty = NSLocalizedString("You are currently alone in this group call",comment:"")
+
+ // FROM ANDROID END
+
+
+ // Added in iOS
+ static let camera_required_for_video = NSLocalizedString("Camera use is not Authorized for \(appName). This permission is required to activate Video.",comment:"")
+ static let conference_edit_error = NSLocalizedString("Unable to edit conference this time, date is invalid",comment:"")
+ static let ok = NSLocalizedString("ok",comment:"")
+ static let conference_display_no_active_speaker = NSLocalizedString("No active speaker",comment:"")
+ static let conference_info_confirm_removal_delete = NSLocalizedString("DELETE",comment:"")
+
+}
diff --git a/Classes/Swift/Voip/Theme/VoipTheme.swift b/Classes/Swift/Voip/Theme/VoipTheme.swift
new file mode 100644
index 000000000..22383fe0a
--- /dev/null
+++ b/Classes/Swift/Voip/Theme/VoipTheme.swift
@@ -0,0 +1,399 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import UIKit
+
+@objc class VoipTheme : NSObject { // Names & values replicated from Android
+
+ // Voip Colors
+ static let voip_gray_blue_color = UIColor(hex:"#798791")
+ static let voip_light_gray = UIColor(hex:"#D0D8DE")
+ @objc static let voip_dark_gray = UIColor(hex:"#4B5964")
+ static let voip_gray = UIColor(hex:"#96A5B1")
+ static let voip_gray_background = UIColor(hex:"#AFAFAF")
+ static let voip_call_record_background = UIColor(hex:"#EBEBEB")
+ static let voip_calls_list_inactive_background = UIColor(hex:"#F0F1F2")
+ static let voip_translucent_popup_background = UIColor(hex:"#A64B5964")
+ static let voip_translucent_popup_alt_background = UIColor(hex:"#E64B5964")
+ static let voip_numpad_background = UIColor(hex:"#E4E4E4")
+ static let voip_contact_avatar_background_alt = UIColor(hex:"#AFAFAF")
+ static let voip_contact_avatar_calls_list = UIColor(hex:"#A1A1A1")
+ static let voip_conference_participant_paused_background = UIColor(hex:"#303030")
+ static let voip_drawable_color = UIColor(hex:"#A6B2BC")
+ static let voip_dark_color = UIColor(hex:"#252E35")
+ static let voip_dark_color2 = UIColor(hex:"#3F464B")
+ static let voip_dark_color3 = UIColor(hex:"#475663")
+ static let voip_dark_color4 = UIColor(hex:"#2D3841")
+
+ // General colors (used by VoIP)
+
+ static let primary_color = UIColor(hex:"#ff5e00")
+ static let primary_dark_color = UIColor(hex:"#e65000")
+ static let green_color = UIColor(hex:"#96c11f")
+ static let dark_green_color = UIColor(hex:"#7d9f21")
+ static let toolbar_color = UIColor(hex:"#e1e1e1")
+ static let form_field_gray_background = UIColor(hex:"#F7F7F7")
+ static let light_grey_color = UIColor(hex:"#c4c4c4")
+ static let header_background_color = UIColor(hex:"#f3f3f3")
+ static let dark_grey_color = UIColor(hex:"#444444")
+ static let voip_conference_invite_out = UIColor(hex:"ffeee5")
+ static let voip_conference_invite_in = header_background_color
+
+
+ // Light / Dark variations
+ static let voipBackgroundColor = LightDarkColor(voip_gray_blue_color,voip_dark_color)
+ static let voipBackgroundBWColor = LightDarkColor(UIColor.white,voip_dark_color)
+ static let voipParticipantBackgroundColor = LightDarkColor(voip_gray_background,voip_dark_color2)
+ static let voipExtraButtonsBackgroundColor = LightDarkColor(voip_gray,voip_dark_color3)
+ static let voipToolbarBackgroundColor = LightDarkColor(toolbar_color,voip_dark_color4)
+ static let voipDrawableColor = LightDarkColor(voip_dark_gray,.white)
+ static let voipDrawableColorHighlighted = LightDarkColor(voip_gray,voip_gray)
+ static let voipTextColor = LightDarkColor(voip_dark_gray,UIColor.white)
+ static let voipFormBackgroundColor = LightDarkColor(form_field_gray_background,voip_dark_color4)
+ static let voipFormFieldBackgroundColor = LightDarkColor(light_grey_color,voip_dark_color4)
+ static let voipFormDisabledFieldBackgroundColor = LightDarkColor(header_background_color,voip_dark_color4)
+ static let primarySubtextLightColor = LightDarkColor(light_grey_color,toolbar_color)
+ static let primaryTextColor = LightDarkColor(dark_grey_color,.white)
+
+
+
+
+
+ // Text styles
+ static let fontName = "Roboto"
+ static let call_header_title = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Bold", size: 18.0)
+ static let call_header_subtitle = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 14.0)
+ static let call_generated_avatar_large = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: true, align: .center, font: fontName+"-Regular", size: 53.0)
+ static let call_generated_avatar_medium = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: true, align: .center, font: fontName+"-Regular", size: 27.0)
+ static let call_generated_avatar_small = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: true, align: .center, font: fontName+"-Regular", size: 16.0)
+
+ static let dtmf_label = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 30.0)
+ static let call_remote_name = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Regular", size: 18.0)
+ static let call_remote_recording = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 16.0)
+ static let call_or_conference_title = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Bold", size: 30.0)
+ static let call_or_conference_subtitle = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Bold", size: 20.0)
+ static let basic_popup_title = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 21.0)
+ static let form_button_bold = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: true, align: .center, font: fontName+"-Bold", size: 17.0)
+ static let form_button_light = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: true, align: .center, font: fontName+"-Regular", size: 17.0)
+
+ static let call_display_name_duration = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Regular", size: 17.0)
+ static let call_sip_address = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Regular", size: 14.0)
+ static let voip_extra_button = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 12.0)
+ static let unread_count_font = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 11.0)
+ static let call_stats_font = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 12.0)
+ static let call_stats_font_title = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 18.0)
+ static let calls_list_header_font = TextStyle(fgColor: voipTextColor, bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 20.0)
+
+ static let call_list_active_name_font = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 18.0)
+ static let call_list_active_sip_uri_font = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 12.0)
+
+ static let call_list_name_font = TextStyle(fgColor: voipTextColor, bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 18.0)
+ static let call_list_sip_uri_font = TextStyle(fgColor: voipTextColor, bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 12.0)
+
+ static let call_context_menu_item_font = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: true, align: .left, font: fontName+"-Bold", size: 16.0)
+
+
+ static let conference_participant_admin_label = TextStyle(fgColor: primarySubtextLightColor, bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Bold", size: 13.0)
+ static let conference_participant_name_font = TextStyle(fgColor: LightDarkColor(dark_grey_color,dark_grey_color), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 18.0)
+ static let conference_participant_sip_uri_font = TextStyle(fgColor: LightDarkColor(primary_color,primary_color), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 12.0)
+ static let conference_participant_name_font_grid = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Bold", size: 15.0)
+ static let conference_participant_name_font_as = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Bold", size: 12.0)
+ static let conference_participant_name_font_audio_only = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName, size: 14.0)
+
+ static let conference_mode_title = TextStyle(fgColor: LightDarkColor(dark_grey_color,dark_grey_color), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Regular", size: 17.0)
+ static let conference_mode_title_selected = conference_mode_title.boldEd()
+ static let conference_scheduling_font = TextStyle(fgColor: voipTextColor, bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Regular", size: 17.0)
+ static let conference_invite_desc_font = TextStyle(fgColor: LightDarkColor(dark_grey_color,dark_grey_color), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Regular", size: 14.0)
+ static let conference_invite_desc_title_font = TextStyle(fgColor: LightDarkColor(voip_dark_gray,voip_dark_gray), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Bold", size: 14.0)
+ static let conference_invite_subject_font = TextStyle(fgColor: LightDarkColor(voip_dark_gray,voip_dark_gray), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Bold", size: 14.0)
+ static let conference_invite_title_font = TextStyle(fgColor: LightDarkColor(dark_grey_color,dark_grey_color), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Bold", size: 16.0)
+ static let conference_preview_subject_font = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .left, font: fontName+"-Regular", size: 24.0)
+ static let conference_waiting_room_no_video_font = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 16.0)
+
+ static let empty_list_font = TextStyle(fgColor: primaryTextColor, bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 18.0)
+
+
+
+
+
+
+
+
+
+ // Buttons Background (State colors)
+
+ static let button_background = [
+ UIButton.State.normal.rawValue : LightDarkColor(voip_gray,voip_gray),
+ UIButton.State.highlighted.rawValue : LightDarkColor(voip_dark_gray,voip_dark_gray),
+ UIButton.State.selected.union(.highlighted).rawValue : LightDarkColor(voip_dark_gray,voip_dark_gray),
+ UIButton.State.disabled.rawValue : LightDarkColor(voip_light_gray,voip_light_gray)
+ ]
+
+ static let button_background_reverse = [
+ UIButton.State.normal.rawValue : LightDarkColor(voip_dark_gray,voip_dark_gray),
+ UIButton.State.highlighted.rawValue : LightDarkColor(voip_gray,voip_gray),
+ UIButton.State.selected.union(.highlighted).rawValue : LightDarkColor(voip_gray,voip_gray),
+ UIButton.State.disabled.rawValue : LightDarkColor(voip_light_gray,voip_light_gray)
+ ]
+
+ static let button_call_recording_background = [
+ UIButton.State.normal.rawValue : LightDarkColor(voip_call_record_background,voip_call_record_background),
+ UIButton.State.selected.rawValue : LightDarkColor(primary_color,primary_color),
+ UIButton.State.disabled.rawValue : LightDarkColor(voip_light_gray,voip_light_gray)
+ ]
+
+ static let button_toggle_background = [
+ UIButton.State.normal.rawValue : LightDarkColor(voip_gray,voip_gray),
+ UIButton.State.selected.rawValue : LightDarkColor(primary_color,primary_color),
+ UIButton.State.highlighted.rawValue : LightDarkColor(voip_dark_gray,voip_dark_gray),
+ UIButton.State.disabled.rawValue : LightDarkColor(voip_light_gray,voip_light_gray)
+ ]
+
+ static let button_toggle_background_reverse = [
+ UIButton.State.normal.rawValue : LightDarkColor(voip_dark_gray,voip_dark_gray),
+ UIButton.State.selected.rawValue : LightDarkColor(primary_color,primary_color),
+ UIButton.State.highlighted.rawValue : LightDarkColor(voip_gray,voip_gray),
+ UIButton.State.disabled.rawValue : LightDarkColor(voip_light_gray,voip_light_gray)
+ ]
+
+ static let primary_colors_background = [
+ UIButton.State.normal.rawValue : LightDarkColor(primary_color,primary_color),
+ UIButton.State.highlighted.rawValue : LightDarkColor(primary_dark_color,primary_dark_color),
+ ]
+
+ static let button_green_background = [
+ UIButton.State.normal.rawValue : LightDarkColor(green_color,green_color),
+ UIButton.State.highlighted.rawValue : LightDarkColor(primary_color,primary_color),
+ ]
+
+ static let primary_colors_background_gray = [
+ UIButton.State.normal.rawValue : LightDarkColor(voip_gray,voip_gray),
+ UIButton.State.highlighted.rawValue : LightDarkColor(voip_dark_gray,voip_dark_gray),
+ ]
+
+ static let numpad_digit_background = [
+ UIButton.State.normal.rawValue : LightDarkColor(voip_numpad_background,voip_numpad_background),
+ UIButton.State.highlighted.rawValue : LightDarkColor(voip_gray_blue_color,voip_gray_blue_color)
+ ]
+
+ static let button_round_background = [
+ UIButton.State.normal.rawValue : LightDarkColor(primary_color,primary_color),
+ UIButton.State.highlighted.rawValue : LightDarkColor(dark_grey_color,dark_grey_color),
+ UIButton.State.disabled.rawValue : LightDarkColor(voip_light_gray,voip_light_gray)
+ ]
+
+ static let button_call_context_menu_background = [
+ UIButton.State.normal.rawValue : LightDarkColor(voip_gray,voip_gray),
+ UIButton.State.highlighted.rawValue : LightDarkColor(primary_color,primary_color),
+ ]
+
+ // Buttons Icons (State colors) + Background colors
+
+ static let call_terminate = ButtonTheme(
+ tintableStateIcons:[UIButton.State.normal.rawValue : TintableIcon(name: "voip_hangup",tintColor: LightDarkColor(.white,.white))],
+ backgroundStateColors: [
+ UIButton.State.normal.rawValue : LightDarkColor(primary_color,primary_color),
+ UIButton.State.highlighted.rawValue : LightDarkColor(primary_dark_color,primary_dark_color)
+ ])
+
+ static let call_record = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_call_record",tintColor: LightDarkColor(.white,.white)),
+ ],
+ backgroundStateColors: button_toggle_background)
+
+ static let call_pause = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_pause",tintColor: LightDarkColor(.white,.white)),
+ ],
+ backgroundStateColors: button_toggle_background)
+
+ static let call_accept = ButtonTheme(
+ tintableStateIcons:[UIButton.State.normal.rawValue : TintableIcon(name: "voip_call",tintColor: LightDarkColor(.white,.white))],
+ backgroundStateColors: [
+ UIButton.State.normal.rawValue : LightDarkColor(green_color,green_color),
+ UIButton.State.highlighted.rawValue : LightDarkColor(dark_green_color,dark_green_color)
+ ])
+
+ static let call_mute = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_micro_on",tintColor: LightDarkColor(.white,.white)),
+ UIButton.State.selected.rawValue : TintableIcon(name: "voip_micro_off",tintColor: LightDarkColor(.white,.white)),
+ ],
+ backgroundStateColors: button_background_reverse)
+
+ static let call_speaker = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_speaker_off",tintColor: LightDarkColor(.white,.white)),
+ UIButton.State.selected.rawValue : TintableIcon(name: "voip_speaker_on",tintColor: LightDarkColor(.white,.white)),
+ ],
+ backgroundStateColors: button_background_reverse)
+
+ static let call_audio_route = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_audio_routes",tintColor: LightDarkColor(.white,.white)),
+ ],
+ backgroundStateColors: button_toggle_background_reverse)
+
+
+ static let call_video = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_camera_off",tintColor: LightDarkColor(.white,.white)),
+ UIButton.State.selected.rawValue : TintableIcon(name: "voip_camera_on",tintColor: LightDarkColor(.white,.white)),
+ ],
+ backgroundStateColors: button_background_reverse)
+
+ static let call_numpad = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_call_numpad",tintColor: LightDarkColor(.white,.white)),
+ UIButton.State.highlighted.rawValue : TintableIcon(name: "voip_call_numpad",tintColor: LightDarkColor(voip_dark_gray,voip_dark_gray)),
+ UIButton.State.disabled.rawValue : TintableIcon(name: "voip_call_numpad",tintColor: LightDarkColor(voip_light_gray,voip_light_gray)),
+ ],
+ backgroundStateColors: button_background)
+
+ // Waiting room layout picker
+
+ static let conf_waiting_room_layout_picker = ButtonTheme(
+ tintableStateIcons:[:],
+ backgroundStateColors: button_toggle_background_reverse)
+
+ // AUdio routes
+ static let route_bluetooth = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_bluetooth",tintColor: LightDarkColor(.white,.white)),
+ ],
+ backgroundStateColors: button_toggle_background_reverse)
+
+ static let route_earpiece = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_earpiece",tintColor: LightDarkColor(.white,.white)),
+ ],
+ backgroundStateColors: button_toggle_background_reverse)
+
+ static let route_speaker = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_speaker_on",tintColor: LightDarkColor(.white,.white)),
+ ],
+ backgroundStateColors: button_toggle_background_reverse)
+
+
+
+ static let call_more = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_call_more",tintColor: LightDarkColor(.white,.white))
+ ],
+ backgroundStateColors: button_background)
+
+
+ static let voip_cancel = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_cancel",tintColor: voipDrawableColor),
+ UIButton.State.highlighted.rawValue : TintableIcon(name: "voip_cancel",tintColor: voipDrawableColorHighlighted)
+ ],
+ backgroundStateColors: [UIButton.State.normal.rawValue : LightDarkColor(.clear,.clear)])
+
+
+ static let voip_cancel_light = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_cancel",tintColor: LightDarkColor(voip_gray,voip_gray)),
+ UIButton.State.highlighted.rawValue : TintableIcon(name: "voip_cancel",tintColor: LightDarkColor(voip_dark_gray,voip_dark_gray))
+ ],
+ backgroundStateColors: [UIButton.State.normal.rawValue : LightDarkColor(.clear,.clear)])
+
+ static let voip_edit = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_edit",tintColor: LightDarkColor(dark_grey_color,dark_grey_color)),
+ UIButton.State.highlighted.rawValue : TintableIcon(name: "voip_edit",tintColor: voipDrawableColorHighlighted)
+ ],
+ backgroundStateColors: [UIButton.State.normal.rawValue : LightDarkColor(.clear,.clear)])
+
+ static let radio_button = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_radio_off",tintColor: LightDarkColor(dark_grey_color,dark_grey_color)),
+ UIButton.State.selected.rawValue : TintableIcon(name: "voip_radio_on",tintColor: LightDarkColor(primary_color,primary_color))
+ ],
+ backgroundStateColors: [UIButton.State.normal.rawValue : LightDarkColor(.clear,.clear)])
+
+ static let voip_call_list_active_menu = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_call_list_menu",tintColor: LightDarkColor(.white,.white)),
+ UIButton.State.highlighted.rawValue : TintableIcon(name: "voip_call_list_menu",tintColor: voipDrawableColorHighlighted)
+ ],
+ backgroundStateColors: [UIButton.State.normal.rawValue : LightDarkColor(.clear,.clear)])
+
+ static let voip_call_list_menu = ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_call_list_menu",tintColor: voipTextColor),
+ UIButton.State.highlighted.rawValue : TintableIcon(name: "voip_call_list_menu",tintColor: voipDrawableColorHighlighted)
+ ],
+ backgroundStateColors: [UIButton.State.normal.rawValue : LightDarkColor(.clear,.clear)])
+
+
+ static func call_action(_ iconName:String) -> ButtonTheme {
+ return ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: iconName,tintColor: LightDarkColor(.white,.white)),
+ UIButton.State.highlighted.rawValue : TintableIcon(name: iconName,tintColor: LightDarkColor(voip_dark_gray,voip_dark_gray)),
+ UIButton.State.disabled.rawValue : TintableIcon(name: iconName,tintColor: LightDarkColor(voip_light_gray,voip_light_gray)),
+ ],
+ backgroundStateColors: [:])
+ }
+
+ static let call_add = ButtonTheme(
+ tintableStateIcons:[UIButton.State.normal.rawValue : TintableIcon(name: "voip_call_add",tintColor: LightDarkColor(.white,.white))],
+ backgroundStateColors: button_round_background)
+
+ static let call_merge = ButtonTheme(
+ tintableStateIcons:[UIButton.State.normal.rawValue : TintableIcon(name: "voip_merge_calls",tintColor: LightDarkColor(.white,.white))],
+ backgroundStateColors: button_round_background)
+
+ // Navigation
+
+ static func nav_button(_ iconName:String) -> ButtonTheme {
+ return ButtonTheme(
+ tintableStateIcons:[
+ UIButton.State.normal.rawValue : TintableIcon(name: iconName,tintColor: LightDarkColor(.darkGray,.white)),
+ UIButton.State.highlighted.rawValue : TintableIcon(name: iconName,tintColor: LightDarkColor(primary_color,primary_color)),
+ UIButton.State.disabled.rawValue : TintableIcon(name: iconName,tintColor: LightDarkColor(light_grey_color,light_grey_color)),
+ ],
+ backgroundStateColors: [:])
+ }
+
+ // Conference scheduling
+ static func scheduled_conference_action(_ iconName:String) -> ButtonTheme {
+ return ButtonTheme(
+ tintableStateIcons:[UIButton.State.normal.rawValue : TintableIcon(name: iconName,tintColor: LightDarkColor(.white,.white))],
+ backgroundStateColors: button_background)
+ }
+
+ static let conference_info_button = [
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_info",tintColor: LightDarkColor(voip_drawable_color,voip_drawable_color)),
+ UIButton.State.selected.rawValue : TintableIcon(name: "voip_info",tintColor: LightDarkColor(primary_color,primary_color)),
+ ]
+
+ static let conference_create_button = [
+ UIButton.State.normal.rawValue : TintableIcon(name: "voip_conference_new",tintColor: LightDarkColor(voip_dark_gray,voip_dark_gray)),
+ UIButton.State.highlighted.rawValue : TintableIcon(name: "voip_conference_new",tintColor: LightDarkColor(primary_color,primary_color)),
+ UIButton.State.disabled.rawValue : TintableIcon(name: "voip_conference_new",tintColor: LightDarkColor(voip_light_gray,voip_light_gray)),
+ ]
+
+}
+
+
diff --git a/Classes/Swift/Voip/ViewModels/CallData.swift b/Classes/Swift/Voip/ViewModels/CallData.swift
new file mode 100644
index 000000000..c70a5cc8b
--- /dev/null
+++ b/Classes/Swift/Voip/ViewModels/CallData.swift
@@ -0,0 +1,203 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linhome
+ *
+ * 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
+import Foundation
+
+class CallData {
+
+ var call : Call
+ let address :String?
+
+ let isPaused = MutableLiveData()
+ let isRemotelyPaused = MutableLiveData()
+ let canBePaused = MutableLiveData()
+ let isRecording = MutableLiveData()
+ let isRemotelyRecorded = MutableLiveData()
+ let isInRemoteConference = MutableLiveData()
+ let remoteConferenceSubject = MutableLiveData()
+ let isConferenceCall = MediatorLiveData()
+ let conferenceParticipants = MutableLiveData<[Address]>()
+ let conferenceParticipantsCountLabel = MutableLiveData()
+ let callKitConferenceLabel = MutableLiveData()
+
+ let isOutgoing = MutableLiveData()
+ let isIncoming = MutableLiveData()
+ let callState = MutableLiveData()
+ let iFrameReceived = MutableLiveData(false)
+ let outgoingEarlyMedia = MutableLiveData()
+ let enteredDTMF = MutableLiveData(" ")
+
+ var chatRoom: ChatRoom? = nil
+
+ private var callDelegate : CallDelegateStub?
+
+ init (call:Call) {
+ self.call = call
+ address = call.remoteAddress?.asStringUriOnly()
+ callDelegate = CallDelegateStub(
+ onStateChanged : { (call: linphonesw.Call, state: linphonesw.Call.State, message: String) -> Void in
+ self.update()
+ },
+ onNextVideoFrameDecoded : { (call: linphonesw.Call) -> Void in
+ self.iFrameReceived.value = true
+ },
+ onRemoteRecording: { (call: linphonesw.Call, recording:Bool) -> Void in
+ self.isRemotelyRecorded.value = recording
+ }
+ )
+ call.addDelegate(delegate: callDelegate!)
+
+ remoteConferenceSubject.readCurrentAndObserve { _ in
+ self.isConferenceCall.value = self.remoteConferenceSubject.value?.count ?? 0 > 0 || self.conferenceParticipants.value?.count ?? 0 > 0
+ }
+ conferenceParticipants.readCurrentAndObserve { _ in
+ self.isConferenceCall.value = self.remoteConferenceSubject.value?.count ?? 0 > 0 || self.conferenceParticipants.value?.count ?? 0 > 0
+ }
+
+ update()
+ }
+
+
+ private func isCallPaused() -> Bool {
+ return [Call.State.Paused, Call.State.Pausing].contains(call.state)
+ }
+
+ private func isCallRemotelyPaused() -> Bool {
+ return [Call.State.PausedByRemote].contains(call.state)
+ }
+
+ private func isOutGoing() -> Bool {
+ return [Call.State.OutgoingInit, Call.State.OutgoingEarlyMedia, Call.State.OutgoingProgress, Call.State.OutgoingRinging].contains(call.state)
+ }
+
+ private func isInComing() -> Bool {
+ return [Call.State.IncomingReceived, Call.State.IncomingEarlyMedia].contains(call.state)
+ }
+
+ private func canCallBePaused() -> Bool {
+ return !call.mediaInProgress() && [Call.State.StreamsRunning, Call.State.PausedByRemote].contains(call.state)
+ }
+
+ private func update() {
+ isPaused.value = isCallPaused()
+ isRemotelyPaused.value = isCallRemotelyPaused()
+ canBePaused.value = canCallBePaused()
+
+ updateConferenceInfo()
+
+ isOutgoing.value = isOutGoing()
+ isIncoming.value = isInComing()
+
+ if (call.mediaInProgress()) {
+ DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
+ self.update()
+ }
+ }
+ outgoingEarlyMedia.value = callState.value == .OutgoingEarlyMedia
+ isRecording.value = call.params?.isRecording
+ callState.value = call.state
+ }
+
+ private func updateConferenceInfo() {
+ let conference = call.conference
+ isInRemoteConference.value = conference != nil
+ if (conference != nil) {
+ Log.d("[Call] Found conference attached to call")
+ remoteConferenceSubject.value = ConferenceViewModel.getConferenceSubject(conference: conference!)
+ Log.d("[Call] Found conference related to this call with subject \(remoteConferenceSubject.value)")
+ var participantsList:[Address] = []
+ conference?.participantList.forEach {
+ $0.address.map {participantsList.append($0)}
+ }
+ conferenceParticipants.value = participantsList
+ conferenceParticipantsCountLabel.value = VoipTexts.conference_participants_title.replacingOccurrences(of:"%d",with:String(participantsList.count))
+ } else {
+ if let conferenceAddress = getConferenceAddress(call: call), let conferenceInfo = Core.get().findConferenceInformationFromUri(uri:conferenceAddress) {
+ Log.d("[Call] Found matching conference info with subject: \(conferenceInfo.subject)")
+ remoteConferenceSubject.value = conferenceInfo.subject
+ var participantsList:[Address] = []
+ conferenceInfo.participants.forEach {
+ participantsList.append($0)
+ }
+ // Add organizer if not in participants list
+ if let organizer = conferenceInfo.organizer {
+ if (participantsList.filter { $0.weakEqual(address2: organizer) }.first == nil) {
+ participantsList.insert(organizer, at:0)
+ }
+ conferenceParticipants.value = participantsList
+ conferenceParticipantsCountLabel.value = VoipTexts.conference_participants_title.replacingOccurrences(of:"%d",with:String(participantsList.count))
+ }
+ }
+ }
+ }
+
+ func getConferenceAddress(call: Call) -> Address? {
+ let remoteContact = call.remoteContact
+ return call.dir == .Incoming ? (remoteContact != nil ? Core.get().interpretUrl(url: remoteContact) : nil) : call.remoteAddress
+ }
+
+ func sendDTMF(dtmf:String) {
+ enteredDTMF.value = enteredDTMF.value! + dtmf
+ Core.get().playDtmf(dtmf: dtmf.utf8CString[0], durationMs: 1000)
+ try?call.sendDtmf(dtmf: dtmf.utf8CString[0])
+ }
+
+ func destroy() {
+ call.removeDelegate(delegate: callDelegate!)
+ isPaused.clearObservers()
+ isRemotelyPaused.clearObservers()
+ canBePaused.clearObservers()
+ isRecording.clearObservers()
+ isRemotelyRecorded.clearObservers()
+ isInRemoteConference.clearObservers()
+ remoteConferenceSubject.clearObservers()
+ isOutgoing.clearObservers()
+ isIncoming.clearObservers()
+ callState.clearObservers()
+ iFrameReceived.clearObservers()
+ outgoingEarlyMedia.clearObservers()
+ enteredDTMF.clearObservers()
+ }
+
+ func toggleRecord() {
+ if (call.params?.isRecording == true) {
+ call.stopRecording()
+ } else {
+ call.startRecording()
+ }
+ isRecording.value = call.params?.isRecording
+ }
+
+ func togglePause() {
+ if (isCallPaused()) {
+ CallsViewModel.shared.callsData.value?.forEach {
+ if ($0.canCallBePaused()) {
+ CallManager.instance().setHeld(call: $0.call, hold: true)
+ }
+ }
+ CallManager.instance().setHeld(call: call, hold: false)
+ } else {
+ CallManager.instance().setHeld(call: call, hold: true)
+ }
+ isPaused.value = isCallPaused()
+ }
+
+
+}
diff --git a/Classes/Swift/Voip/ViewModels/CallStatisticsData.swift b/Classes/Swift/Voip/ViewModels/CallStatisticsData.swift
new file mode 100644
index 000000000..5d075d063
--- /dev/null
+++ b/Classes/Swift/Voip/ViewModels/CallStatisticsData.swift
@@ -0,0 +1,171 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * 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 CallStatisticsData {
+
+ var call : Call
+ var audioStats:[StatItemData] = []
+ var videoStats:[StatItemData] = []
+ let isVideoEnabled = MutableLiveData()
+ let statsUpdated = MutableLiveData(true)
+
+ private var callDelegate : CallDelegateStub?
+
+ init (call:Call) {
+ self.call = call
+ callDelegate = CallDelegateStub(
+ onStatsUpdated : { (call: Call, stats: CallStats) -> Void in
+ self.isVideoEnabled.value = call.currentParams?.videoEnabled
+ self.updateCallStats(stats: stats)
+ self.statsUpdated.value = true
+ }
+
+ )
+ call.addDelegate(delegate: callDelegate!)
+ initCallStats()
+ isVideoEnabled.value = call.currentParams?.videoEnabled
+ call.audioStats.map { updateCallStats(stats: $0) }
+ call.videoStats.map { updateCallStats(stats: $0) }
+ }
+
+ private func initCallStats() {
+
+ audioStats.append(StatItemData(type: StatType.CAPTURE))
+ audioStats.append(StatItemData(type: StatType.PLAYBACK))
+ audioStats.append(StatItemData(type: StatType.PAYLOAD))
+ audioStats.append(StatItemData(type: StatType.ENCODER))
+ audioStats.append(StatItemData(type: StatType.DECODER))
+ audioStats.append(StatItemData(type: StatType.DOWNLOAD_BW))
+ audioStats.append(StatItemData(type: StatType.UPLOAD_BW))
+ audioStats.append(StatItemData(type: StatType.ICE))
+ audioStats.append(StatItemData(type: StatType.IP_FAM))
+ audioStats.append(StatItemData(type: StatType.SENDER_LOSS))
+ audioStats.append(StatItemData(type: StatType.RECEIVER_LOSS))
+ audioStats.append(StatItemData(type: StatType.JITTER))
+
+ videoStats.append(StatItemData(type: StatType.CAPTURE))
+ videoStats.append(StatItemData(type: StatType.PLAYBACK))
+ videoStats.append(StatItemData(type: StatType.PAYLOAD))
+ videoStats.append(StatItemData(type: StatType.ENCODER))
+ videoStats.append(StatItemData(type: StatType.DECODER))
+ videoStats.append(StatItemData(type: StatType.DOWNLOAD_BW))
+ videoStats.append(StatItemData(type: StatType.UPLOAD_BW))
+ videoStats.append(StatItemData(type: StatType.ESTIMATED_AVAILABLE_DOWNLOAD_BW))
+ videoStats.append(StatItemData(type: StatType.ICE))
+ videoStats.append(StatItemData(type: StatType.IP_FAM))
+ videoStats.append(StatItemData(type: StatType.SENDER_LOSS))
+ videoStats.append(StatItemData(type: StatType.RECEIVER_LOSS))
+ videoStats.append(StatItemData(type: StatType.SENT_RESOLUTION))
+ videoStats.append(StatItemData(type: StatType.RECEIVED_RESOLUTION))
+ videoStats.append(StatItemData(type: StatType.SENT_FPS))
+ videoStats.append(StatItemData(type: StatType.RECEIVED_FPS))
+ }
+
+ private func updateCallStats(stats: CallStats) {
+ if (stats.type == StreamType.Audio) {
+ audioStats.forEach{ $0.update(call: call, stats: stats)}
+ } else if (stats.type == StreamType.Video) {
+ videoStats.forEach{ $0.update(call: call, stats: stats)}
+ }
+ }
+}
+
+
+enum StatType {
+ case CAPTURE
+ case PLAYBACK
+ case PAYLOAD
+ case ENCODER
+ case DECODER
+ case DOWNLOAD_BW
+ case UPLOAD_BW
+ case ICE
+ case IP_FAM
+ case SENDER_LOSS
+ case RECEIVER_LOSS
+ case JITTER
+ case SENT_RESOLUTION
+ case RECEIVED_RESOLUTION
+ case SENT_FPS
+ case RECEIVED_FPS
+ case ESTIMATED_AVAILABLE_DOWNLOAD_BW
+}
+
+struct StatItemData {
+ var type:StatType
+
+ let value = MutableLiveData()
+
+ func update(call: Call, stats: CallStats) {
+ guard let payloadType = stats.type == StreamType.Audio ? call.currentParams?.usedAudioPayloadType : call.currentParams?.usedVideoPayloadType, let core = call.core else {
+ value.value = "n/a"
+ return
+ }
+ switch(type) {
+ case StatType.CAPTURE: value.value = stats.type == StreamType.Audio ? core.captureDevice : core.videoDevice
+ case StatType.PLAYBACK: value.value = stats.type == StreamType.Audio ? core.playbackDevice : core.videoDisplayFilter
+ case StatType.PAYLOAD: value.value = "\(payloadType.mimeType)/\(payloadType.clockRate / 1000) kHz"
+ case StatType.ENCODER: value.value = payloadType.description
+ case StatType.DECODER: value.value = payloadType.description
+ case StatType.DOWNLOAD_BW: value.value = "\(stats.downloadBandwidth) kbits/s"
+ case StatType.UPLOAD_BW: value.value = "\(stats.uploadBandwidth) kbits/s"
+ case StatType.ICE: value.value = stats.iceState.toString()
+ case StatType.IP_FAM: value.value = stats.ipFamilyOfRemote == AddressFamily.Inet6 ? "IPv6" : "IPv4"
+ case StatType.SENDER_LOSS: value.value = String(format: "%.2f%",stats.senderLossRate)
+ case StatType.RECEIVER_LOSS: value.value = String(format: "%.2f%",stats.receiverLossRate)
+ case StatType.JITTER: value.value = String(format: "%.2f ms",stats.jitterBufferSizeMs)
+ case StatType.SENT_RESOLUTION: value.value = call.currentParams?.sentVideoDefinition?.name
+ case StatType.RECEIVED_RESOLUTION: value.value = call.currentParams?.receivedVideoDefinition?.name
+ case StatType.SENT_FPS: value.value = "\(call.currentParams?.sentFramerate ?? 0.0)"
+ case StatType.RECEIVED_FPS: value.value = "\(call.currentParams?.receivedFramerate ?? 0.0)"
+ case StatType.ESTIMATED_AVAILABLE_DOWNLOAD_BW: value.value = "\(stats.estimatedDownloadBandwidth) kbit/s"
+ }
+ }
+
+
+ func getTypeTitle() -> String {
+ switch (type) {
+ case .CAPTURE: return VoipTexts.call_stats_capture_filter
+ case .PLAYBACK: return VoipTexts.call_stats_player_filter
+ case .PAYLOAD: return VoipTexts.call_stats_codec
+ case .ENCODER: return VoipTexts.call_stats_encoder_name
+ case .DECODER: return VoipTexts.call_stats_decoder_name
+ case .DOWNLOAD_BW: return VoipTexts.call_stats_download
+ case .UPLOAD_BW: return VoipTexts.call_stats_upload
+ case .ICE: return VoipTexts.call_stats_ice
+ case .IP_FAM: return VoipTexts.call_stats_ip
+ case .SENDER_LOSS: return VoipTexts.call_stats_sender_loss_rate
+ case .RECEIVER_LOSS: return VoipTexts.call_stats_receiver_loss_rate
+ case .JITTER: return VoipTexts.call_stats_jitter_buffer
+ case .SENT_RESOLUTION: return VoipTexts.call_stats_video_resolution_sent
+ case .RECEIVED_RESOLUTION: return VoipTexts.call_stats_video_resolution_received
+ case .SENT_FPS: return VoipTexts.call_stats_video_fps_sent
+ case .RECEIVED_FPS: return VoipTexts.call_stats_video_fps_received
+ case .ESTIMATED_AVAILABLE_DOWNLOAD_BW: return VoipTexts.call_stats_estimated_download
+ }
+ }
+
+
+
+
+
+}
diff --git a/Classes/Swift/Voip/ViewModels/CallsViewModel.swift b/Classes/Swift/Voip/ViewModels/CallsViewModel.swift
new file mode 100644
index 000000000..284868eb0
--- /dev/null
+++ b/Classes/Swift/Voip/ViewModels/CallsViewModel.swift
@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linhome
+ *
+ * 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
+import linphonesw
+import AVFoundation
+
+
+class CallsViewModel {
+
+ let currentCallData = MutableLiveData(nil)
+ let callsData = MutableLiveData<[CallData]>([])
+ let inactiveCallsCount = MutableLiveData(0)
+ let currentCallUnreadChatMessageCount = MutableLiveData(0)
+ let chatAndCallsCount = MutableLiveData(0)
+ let callConnectedEvent = MutableLiveData()
+ let callUpdateEvent = MutableLiveData()
+ let noMoreCallEvent = MutableLiveData(false)
+
+ var core : Core { get { Core.get() } }
+
+ static let shared = CallsViewModel()
+
+ private var coreDelegate : CoreDelegateStub?
+
+ init () {
+ coreDelegate = CoreDelegateStub(
+ onCallStateChanged : { (core: Core, call: Call, state: Call.State, message:String) -> Void in
+ Log.i("[Calls] Call state changed: \(call) : \(state)")
+ let currentCall = core.currentCall
+ if (currentCall != nil && self.currentCallData.value??.call.getCobject != currentCall?.getCobject) {
+ self.updateCurrentCallData(currentCall: currentCall)
+ } else if (currentCall == nil && core.callsNb > 0) {
+ self.updateCurrentCallData(currentCall: currentCall)
+ }
+ if ([.End,.Released,.Error].contains(state)) {
+ self.removeCallFromList(call: call)
+ } else if ([.OutgoingInit].contains(state)) {
+ self.addCallToList(call:call)
+ } else if ([.IncomingReceived].contains(state)) {
+ self.addCallToList(call:call)
+ } else if (state == .UpdatedByRemote) {
+ let remoteVideo = call.remoteParams?.videoEnabled == true
+ let localVideo = call.currentParams?.videoEnabled == true
+ let autoAccept = call.core?.videoActivationPolicy?.automaticallyAccept == true
+ if (remoteVideo && !localVideo && !autoAccept) {
+ if (core.videoCaptureEnabled || core.videoDisplayEnabled) {
+ try?call.deferUpdate()
+ self.callUpdateEvent.value = call
+ } else {
+ call.answerVideoUpdateRequest(accept: false)
+ }
+ }
+ }else if (state == Call.State.Connected) {
+ self.callConnectedEvent.value = call
+ } else if (state == Call.State.StreamsRunning) {
+ self.callUpdateEvent.value = call
+ }
+ self.updateInactiveCallsCount()
+ self.callsData.notifyValue()
+ },
+
+ onMessageReceived : { (core: Core, room: ChatRoom, message: ChatMessage) -> Void in
+ self.updateUnreadChatCount()
+ },
+ onChatRoomRead : { (core: Core, room: ChatRoom) -> Void in
+ self.updateUnreadChatCount()
+ },
+ onLastCallEnded: { (core: Core) -> Void in
+ self.currentCallData.value??.destroy()
+ self.currentCallData.value = nil
+ self.noMoreCallEvent.value = true
+ }
+ )
+
+ Core.get().addDelegate(delegate: coreDelegate!)
+
+ if let currentCall = core.currentCall {
+ currentCallData.value??.destroy()
+ currentCallData.value = CallData(call:currentCall)
+ }
+
+ chatAndCallsCount.value = 0
+ inactiveCallsCount.readCurrentAndObserve { (_) in
+ self.updateCallsAndChatCount()
+ }
+ currentCallUnreadChatMessageCount.readCurrentAndObserve { (_) in
+ self.updateCallsAndChatCount()
+ }
+
+ initCallList()
+ updateInactiveCallsCount()
+ updateUnreadChatCount()
+
+ }
+
+ private func initCallList() {
+ core.calls.forEach { addCallToList(call: $0) }
+ }
+
+ private func removeCallFromList(call: Call) {
+ Log.i("[Calls] Removing call \(call) from calls list")
+ if let removeCandidate = callsData.value?.filter{$0.call.getCobject == call.getCobject}.first {
+ removeCandidate.destroy()
+ }
+
+ callsData.value = callsData.value?.filter(){$0.call.getCobject != call.getCobject}
+ callsData.notifyValue()
+ }
+
+ private func addCallToList(call: Call) {
+ Log.i("[Calls] Adding call \(call) to calls list")
+ callsData.value?.append(CallData(call: call))
+ callsData.notifyValue()
+ }
+
+ private func updateUnreadChatCount() {
+ guard let unread = currentCallData.value??.chatRoom?.unreadMessagesCount else {
+ currentCallUnreadChatMessageCount.value = 0
+ return
+ }
+ currentCallUnreadChatMessageCount.value = unread
+ }
+
+ private func updateInactiveCallsCount() {
+ inactiveCallsCount.value = core.callsNb - 1
+ }
+
+ private func updateCallsAndChatCount() {
+ var value = 0
+ if let calls = inactiveCallsCount.value {
+ value += calls
+ }
+ if let chats = currentCallUnreadChatMessageCount.value {
+ value += chats
+ }
+ chatAndCallsCount.value = value
+ }
+
+ func mergeCallsIntoLocalConference() {
+ CallManager.instance().startLocalConference()
+ }
+
+ private func updateCurrentCallData(currentCall: Call?) {
+ var callToUse = currentCall
+ if (currentCall == nil) {
+ Log.w("[Calls] Current call is now null")
+
+ let firstCall = core.calls.first
+ if (firstCall != nil && currentCallData.value??.call.getCobject != firstCall?.getCobject) {
+ Log.i("[Calls] Using first call as \"current\" call")
+ callToUse = firstCall
+ }
+ }
+
+ guard let callToUse = callToUse else {
+ Log.w("[Calls] No call found to be used as \"current\"")
+ return
+ }
+
+ let firstToUse = callsData.value?.filter{$0.call.getCobject != callToUse.getCobject}.first
+ if (firstToUse != nil) {
+ currentCallData.value = firstToUse
+ } else {
+ Log.w("[Calls] Call not found in calls data list, shouldn't happen!")
+ currentCallData.value = CallData(call: callToUse)
+ }
+
+ updateUnreadChatCount()
+ }
+
+
+}
diff --git a/Classes/Swift/Voip/ViewModels/ConferenceParticipantData.swift b/Classes/Swift/Voip/ViewModels/ConferenceParticipantData.swift
new file mode 100644
index 000000000..2ccdf95d9
--- /dev/null
+++ b/Classes/Swift/Voip/ViewModels/ConferenceParticipantData.swift
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linhome
+ *
+ * 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
+import Foundation
+
+class ConferenceParticipantData {
+
+ var conference:Conference
+ var participant:Participant
+
+ let isAdmin = MutableLiveData()
+ let isMeAdmin = MutableLiveData()
+
+ private var callDelegate : CallDelegateStub?
+
+ init (conference:Conference, participant:Participant) {
+ self.conference = conference
+ self.participant = participant
+ isAdmin.value = participant.isAdmin
+ isMeAdmin.value = conference.me?.isAdmin
+ Log.i("[Conference Participant] Participant \(sipUri!) is admin=\(isAdmin.value!)")
+
+ }
+
+ var sipUri:String? {
+ get {
+ return self.participant.address?.asString()
+ }
+ }
+
+ func destroy() {
+ isAdmin.clearObservers()
+ isMeAdmin.clearObservers()
+ }
+}
diff --git a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift
new file mode 100644
index 000000000..948622017
--- /dev/null
+++ b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linhome
+ *
+ * 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
+import Foundation
+
+class ConferenceParticipantDeviceData {
+
+ var participantDevice:ParticipantDevice
+ let isMe:Bool
+
+ let videoEnabled = MutableLiveData()
+ let activeSpeaker = MutableLiveData()
+ let micMuted = MutableLiveData()
+
+ let isInConference = MutableLiveData()
+
+ var core : Core { get { Core.get() } }
+
+ private var participantDeviceDelegate : ParticipantDeviceDelegate?
+
+ init (participantDevice:ParticipantDevice, isMe:Bool) {
+ self.isMe = isMe
+ self.participantDevice = participantDevice
+ participantDeviceDelegate = ParticipantDeviceDelegateStub(
+ onIsSpeakingChanged: { (participantDevice, isSpeaking) in
+ Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) isspeaking = \(isSpeaking)")
+ self.activeSpeaker.value = isSpeaking
+ },
+ onIsMuted: { (participantDevice, isMuted) in
+ Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) muted = \(isMuted)")
+ self.micMuted.value = isMuted
+ },
+ onConferenceJoined: { (participantDevice) in
+ Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) has joined the conference")
+ self.isInConference.value = true
+ },
+ onConferenceLeft: { (participantDevice) in
+ Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) has left the conference")
+ self.isInConference.value = false
+ },
+ onStreamCapabilityChanged: { (participantDevice, direction, streamType) in
+ Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) video stream direction changed: \(direction)")
+ self.videoEnabled.value = direction == MediaDirection.SendOnly || direction == MediaDirection.SendRecv
+ if (streamType == StreamType.Video) {
+ Log.i("[Conference Participant Device] Participant [\(participantDevice.address?.asStringUriOnly())] video capability changed to \(direction)")
+ }
+ },
+ onStreamAvailabilityChanged: { (participantDevice, available, streamType) in
+ if (streamType == StreamType.Video) {
+ Log.i("[Conference Participant Device] Participant [\(participantDevice.address?.asStringUriOnly())] video availability changed to \(available)")
+ self.videoEnabled.value = available
+ }
+ }
+
+ )
+
+ participantDevice.addDelegate(delegate: participantDeviceDelegate!)
+ activeSpeaker.value = false
+ micMuted.value = participantDevice.isMuted
+
+ videoEnabled.value = participantDevice.getStreamAvailability(streamType: .Video)
+
+ isInConference.value = participantDevice.isInConference
+ let videoCapability = participantDevice.getStreamCapability(streamType: .Video)
+ Log.i("[Conference Participant Device] Participant [\(participantDevice.address?.asStringUriOnly())], is in conf? \(isInConference.value), is video enabled? \(videoEnabled.value) \(videoCapability)")
+ }
+
+ func destroy() {
+ clearObservers()
+ participantDevice.removeDelegate(delegate: participantDeviceDelegate!)
+ }
+
+ func clearObservers() {
+ isInConference.clearObservers()
+ videoEnabled.clearObservers()
+ activeSpeaker.clearObservers()
+ }
+
+ func switchCamera() {
+ Core.get().toggleCamera()
+ }
+
+ func isSwitchCameraAvailable() -> Bool {
+ return isMe && Core.get().showSwitchCameraButton()
+ }
+
+ func setVideoView(view:UIView) {
+ Log.i("[Conference Participant Device] Setting textureView \(view) for participant \(participantDevice.address?.asStringUriOnly())")
+ view.contentMode = .scaleAspectFill
+ if (isMe) {
+ core.usePreviewWindow(yesno: false)
+ core.nativePreviewWindow = view
+ } else {
+ participantDevice.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque())
+ }
+ }
+}
diff --git a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift
new file mode 100644
index 000000000..7dbd071d0
--- /dev/null
+++ b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift
@@ -0,0 +1,475 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linhome
+ *
+ * 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
+import linphonesw
+import linphone
+import AVFoundation
+
+class ConferenceViewModel {
+
+ var core : Core { get { Core.get() } }
+ static let shared = ConferenceViewModel()
+
+ let conferenceExists = MutableLiveData()
+ let subject = MutableLiveData()
+ let isConferenceLocallyPaused = MutableLiveData()
+ let isVideoConference = MutableLiveData()
+ let isMeAdmin = MutableLiveData()
+
+ let conference = MutableLiveData()
+ let conferenceCreationPending = MutableLiveData()
+ let conferenceParticipants = MutableLiveData<[ConferenceParticipantData]>()
+ let conferenceParticipantDevices = MutableLiveData<[ConferenceParticipantDeviceData]>()
+ let conferenceDisplayMode = MutableLiveData()
+
+ let isRecording = MutableLiveData()
+ let isRemotelyRecorded = MutableLiveData()
+
+ let maxParticipantsForMosaicLayout = ConfigManager.instance().lpConfigIntForKey(key: "max_conf_part_mosaic_layout",defaultValue: 6)
+
+ let speakingParticipant = MutableLiveData()
+
+ let participantAdminStatusChangedEvent = MutableLiveData()
+
+ let firstToJoinEvent = MutableLiveData()
+
+ let allParticipantsLeftEvent = MutableLiveData()
+
+ private var conferenceDelegate : ConferenceDelegateStub?
+ private var coreDelegate : CoreDelegateStub?
+
+ var conferenceScheduler:ConferenceScheduler? = nil
+
+ init () {
+ conferenceDelegate = ConferenceDelegateStub(
+ onParticipantAdded: { (conference: Conference, participant: Participant) in
+ Log.i("[Conference] \(conference) Participant \(participant) added")
+ self.updateParticipantsList(conference)
+ let count = self.conferenceParticipantDevices.value!.count
+ if (count > self.maxParticipantsForMosaicLayout) {
+ Log.w("[Conference] \(conference) More than \(self.maxParticipantsForMosaicLayout) participants \(count), forcing active speaker layout")
+ self.conferenceDisplayMode.value = .ActiveSpeaker
+ self.changeLayout(layout: .ActiveSpeaker)
+ }
+ },
+ onParticipantRemoved: {(conference: Conference, participant: Participant) in
+ Log.i("[Conference] \(conference) \(participant) Participant removed")
+ self.updateParticipantsList(conference)
+ if (self.conferenceParticipants.value?.count == 0) {
+ self.allParticipantsLeftEvent.value = true
+ }
+ },
+ onParticipantDeviceAdded: {(conference: Conference, participantDevice: ParticipantDevice) in
+ Log.i("[Conference] \(conference) Participant device \(participantDevice) added")
+ self.addParticipantDevice(device: participantDevice)
+
+ },
+ onParticipantDeviceRemoved: { (conference: Conference, participantDevice: ParticipantDevice) in
+ Log.i("[Conference] \(conference) Participant device \(participantDevice) removed")
+ self.removeParticipantDevice(device: participantDevice)
+ },
+ onParticipantAdminStatusChanged: { (conference: Conference, participant: Participant) in
+ Log.i("[Conference] \(conference) Participant admin status changed")
+ self.isMeAdmin.value = conference.me?.isAdmin
+ self.updateParticipantsList(conference)
+ if let participantData = self.conferenceParticipants.value?.filter ({$0.participant.address!.weakEqual(address2: participant.address!)}).first {
+ self.participantAdminStatusChangedEvent.value = participantData
+ } else {
+ Log.w("[Conference] Failed to find participant [\(participant.address!.asStringUriOnly())] in conferenceParticipants list")
+ }
+ },
+ onParticipantDeviceLeft: { (conference: Conference, device: ParticipantDevice) in
+ if (conference.isMe(uri: device.address!)) {
+ Log.i("[Conference] Left conference")
+ self.isConferenceLocallyPaused.value = true
+ }
+ },
+ onParticipantDeviceJoined: { (conference: Conference, device: ParticipantDevice) in
+ if (conference.isMe(uri: device.address!)) {
+ Log.i("[Conference] Joined conference")
+ self.isConferenceLocallyPaused.value = false
+ }
+ },
+ onStateChanged: { (conference: Conference, state: Conference.State) in
+ Log.i("[Conference] State changed: \(state)")
+ self.isVideoConference.value = conference.currentParams?.videoEnabled
+ if (state == .Created) {
+ self.configureConference(conference)
+ self.conferenceCreationPending.value = false
+ }
+ if (state == .TerminationPending) {
+ self.terminateConference(conference)
+ }
+ },
+ onSubjectChanged: { (conference: Conference, subject: String) in
+ self.subject.value = subject
+ },
+ onParticipantDeviceIsSpeakingChanged: { (conference: Conference, participantDevice: ParticipantDevice, isSpeaking:Bool) in
+ Log.i("[Conference] Participant [\(participantDevice.address!.asStringUriOnly())] is speaking = \(isSpeaking)")
+ if (isSpeaking) {
+ if let device = self.conferenceParticipantDevices.value?.filter ({
+ $0.participantDevice.address!.weakEqual(address2: participantDevice.address!)
+ }).first {
+ self.speakingParticipant.value = device
+ } else {
+ Log.w("[Conference] Participant device [\((participantDevice.address?.asStringUriOnly()).orNil)] is speaking but couldn't find it in devices list")
+ }
+
+ }
+ }
+ )
+
+ coreDelegate = CoreDelegateStub(
+ onConferenceStateChanged: { (core, conference, state) in
+ Log.i("[Conference] \(conference) Conference state changed: \(state)")
+ if (state == Conference.State.Instantiated) {
+ self.conferenceCreationPending.value = true
+ self.initConference(conference)
+
+ }
+ }
+ )
+
+ Core.get().addDelegate(delegate: coreDelegate!)
+ conferenceParticipants.value = []
+ conferenceParticipantDevices.value = []
+
+ if let conference = core.conference != nil ? core.conference : core.currentCall?.conference {
+ Log.i("[Conference] Found an existing conference: \(conference) in state \(conference.state)")
+
+ if (conference.state != .TerminationPending && conference.state != .Terminated) {
+ initConference(conference)
+ if (conference.state == Conference.State.Created) {
+ configureConference(conference)
+ } else {
+ conferenceCreationPending.value = true
+ }
+ }
+ }
+ }
+
+ func pauseConference() {
+ Log.i("[Conference] Leaving conference with address \(conference) temporarily")
+ let _ = conference.value?.leave()
+ }
+
+ func resumeConference() {
+ Log.i("[Conference] entering conference with address \(conference)")
+ let _ = conference.value?.enter()
+ }
+
+ func toggleRecording() {
+ if (conference.value?.isRecording == true) {
+ Log.i("[Conference] Stopping conference recording")
+ let _ = conference.value?.stopRecording()
+ } else {
+ let writablePath = AppManager.recordingFilePathFromCall(address: (conference.value?.conferenceAddress!.asString())!)
+ Log.i("[Conference] Starting recording in file $path")
+ let _ = conference.value?.startRecording(path: writablePath)
+ }
+ isRecording.value = conference.value?.isRecording
+ }
+
+ func initConference(_ conference: Conference) {
+ conferenceExists.value = true
+
+ self.conference.value = conference
+ conference.addDelegate(delegate: self.conferenceDelegate!)
+
+ isRecording.value = conference.isRecording
+ subject.value = ConferenceViewModel.getConferenceSubject(conference: conference)
+
+ updateConferenceLayout(conference: conference)
+ }
+
+ func configureConference(_ conference: Conference) {
+ self.updateParticipantsList(conference)
+ if (conferenceParticipants.value?.count == 0) {
+ firstToJoinEvent.value = true
+ }
+ self.updateParticipantsDevicesList(conference)
+
+ isConferenceLocallyPaused.value = !conference.isIn
+ self.isMeAdmin.value = conference.me?.isAdmin == true
+ isVideoConference.value = conference.currentParams?.videoEnabled == true
+
+ subject.value = ConferenceViewModel.getConferenceSubject(conference: conference)
+ updateConferenceLayout(conference: conference)
+
+ }
+
+
+ func addCallsToConference() {
+ Log.i("[Conference] Trying to merge all calls into existing conference")
+ guard let conf = conference.value else {
+ return
+ }
+ core.calls.forEach { call in
+ if (call.conference == nil) {
+ try? conf.addParticipant(call: call)
+ }
+ }
+ if (conf.isIn) {
+ Log.i("[Conference] Conference was paused, resuming it")
+ let _ = conf.enter()
+ }
+ }
+
+
+ func changeLayout(layout: ConferenceDisplayMode) {
+ Log.i("[Conference] Trying to change conference layout to $layout")
+ if let conference = conference.value, let call = conference.call, let params = try?call.core?.createCallParams(call: call) {
+ params.videoEnabled = layout != .AudioOnly
+ params.conferenceVideoLayout = layout == ConferenceDisplayMode.Grid ? .Grid : .ActiveSpeaker
+ try?call.update(params: params)
+
+ conferenceDisplayMode.value = layout
+ let list = sortDevicesDataList(devices: conferenceParticipantDevices.value!)
+ conferenceParticipantDevices.value = list
+ } else {
+ Log.e("[Conference] Conference or Call Or Call Params is null in ConferenceViewModel")
+ }
+ }
+
+ private func updateConferenceLayout(conference: Conference) {
+ if let call = conference.call, let params = call.params {
+ conferenceDisplayMode.value = !params.videoEnabled ? ConferenceDisplayMode.AudioOnly : params.conferenceVideoLayout == .Grid ? .Grid : .ActiveSpeaker
+ let list = sortDevicesDataList(devices: conferenceParticipantDevices.value!)
+ conferenceParticipantDevices.value = list
+ Log.i("[Conference] Conference current layout is: \(conferenceDisplayMode.value)")
+ }
+ }
+
+
+
+ func terminateConference(_ conference: Conference) {
+ conferenceExists.value = false
+ isVideoConference.value = false
+
+ conference.removeDelegate(delegate: conferenceDelegate!)
+
+ self.conferenceParticipants.value?.forEach{ $0.destroy()}
+ self.conferenceParticipantDevices.value?.forEach{ $0.destroy()}
+ conferenceParticipants.value = []
+ conferenceParticipantDevices.value = []
+ }
+
+
+ private func updateParticipantsList(_ conference: Conference) {
+ self.conferenceParticipants.value?.forEach{ $0.destroy()}
+ var participants :[ConferenceParticipantData] = []
+
+ let participantsList = conference.participantList
+ Log.i("[Conference] \(conference) Conference has \(participantsList.count) participants")
+
+ participantsList.forEach { (participant) in
+ let participantDevices = participant.devices
+ Log.i("[Conference] \(conference) Participant found: \(participant) with \(participantDevices.count) device(s)")
+ let participantData = ConferenceParticipantData(conference: conference, participant: participant)
+ participants.append(participantData)
+ }
+
+ conferenceParticipants.value = participants
+ }
+
+ private func updateParticipantsDevicesList(_ conference: Conference) {
+ self.conferenceParticipantDevices.value?.forEach{ $0.destroy()}
+ var devices :[ConferenceParticipantDeviceData] = []
+
+ let participantsList = conference.participantList
+ Log.i("[Conference] \(conference) Conference has \(participantsList.count) participants")
+
+ participantsList.forEach { (participant) in
+ let participantDevices = participant.devices
+ Log.i("[Conference] \(conference) Participant found: \(participant) with \(participantDevices.count) device(s)")
+
+ participantDevices.forEach { (device) in
+ Log.i("[Conference] \(conference) Participant device found: \(device.name) (\(device.address!.asStringUriOnly()))")
+ let deviceData = ConferenceParticipantDeviceData(participantDevice: device, isMe: false)
+ devices.append(deviceData)
+ }
+
+ }
+ conference.me?.devices.forEach { (device) in
+ Log.i("[Conference] \(conference) Participant device for myself found: \(device.name) (\(device.address!.asStringUriOnly()))")
+ let deviceData = ConferenceParticipantDeviceData(participantDevice: device, isMe: true)
+ devices.append(deviceData)
+ }
+
+
+ conferenceParticipantDevices.value = devices
+ }
+
+ private func addParticipantDevice(device: ParticipantDevice) {
+ var devices :[ConferenceParticipantDeviceData] = []
+ conferenceParticipantDevices.value?.forEach{devices.append($0)}
+
+ if let deviceAddress = device.address, let _ = devices.filter({ $0.participantDevice.address!.weakEqual(address2: deviceAddress)}).first {
+ Log.e("[Conference] Participant is already in devices list: \(device.name) (\((device.address?.asStringUriOnly()) ?? "nil")")
+ return
+ }
+
+ Log.i("[Conference] New participant device found: \(device.name) (\((device.address?.asStringUriOnly()).orNil)")
+ let deviceData = ConferenceParticipantDeviceData(participantDevice: device, isMe: false)
+ devices.append(deviceData)
+
+ let sortedDevices = sortDevicesDataList(devices: devices)
+
+ if (speakingParticipant.value == nil) {
+ speakingParticipant.value = deviceData
+ }
+
+ conferenceParticipantDevices.value = sortedDevices
+ }
+
+ private func removeParticipantDevice(device: ParticipantDevice) {
+ let devices = conferenceParticipantDevices.value?.filter {
+ $0.participantDevice.address?.asStringUriOnly() != device.address?.asStringUriOnly()
+ }
+ if (devices?.count == conferenceParticipantDevices.value?.count) {
+ Log.e("[Conference] Failed to remove participant device: \(device.name) (\((device.address?.asStringUriOnly()).orNil)")
+ } else {
+ Log.i("[Conference] Participant device removed: \(device.name) (\((device.address?.asStringUriOnly()).orNil)")
+ }
+
+ conferenceParticipantDevices.value = devices
+ }
+
+
+ private func sortDevicesDataList(devices: [ConferenceParticipantDeviceData]) -> [ConferenceParticipantDeviceData] {
+ if let meDeviceData = devices.filter({$0.isMe}).first {
+ var devicesWithoutMe = devices.filter { !$0.isMe }
+ if (conferenceDisplayMode.value == .ActiveSpeaker) {
+ devicesWithoutMe.insert(meDeviceData, at: 0)
+ } else {
+ devicesWithoutMe.append(meDeviceData)
+ }
+ return devicesWithoutMe
+ }
+ return devices
+ }
+
+
+ func togglePlayPause () {
+ if (isConferenceLocallyPaused.value == true) {
+ resumeConference()
+ isConferenceLocallyPaused.value = false
+ } else {
+ pauseConference()
+ isConferenceLocallyPaused.value = true
+ }
+ }
+
+ // Review below (dynamic add/remove)
+
+ func updateParticipants(addresses:[Address]) {
+ guard let conference = conference.value else {
+ Log.w("[Conference Participants] conference not set, can't update participants")
+ return
+ }
+ do {
+ // Adding new participants first, because if we remove all of them (or all of them except one)
+ // It will terminate the conference first and we won't be able to add new participants after
+ try addresses.forEach { address in
+ let participant = conference.participantList.filter { $0.address?.asStringUriOnly() == address.asStringUriOnly() }.first
+ if (participant == nil) {
+ Log.i("[Conference Participants] Participant \(address.asStringUriOnly()) will be added to group")
+ try conference.addParticipant(uri: address)
+ }
+ }
+
+ // Removing participants
+ try conference.participantList.forEach { participant in
+ let member = addresses.filter { $0.asStringUriOnly() == participant.address?.asStringUriOnly() }.first
+ if (member == nil) {
+ Log.w("[Conference Participants] Participant \((participant.address?.asStringUriOnly()).orNil) will be removed from conference")
+ try conference.removeParticipant(participant: participant)
+ }
+ }
+ } catch {
+ Log.e("[Conference Participants] Error updating participant lists \(error)")
+ }
+ }
+
+ static func getConferenceSubject(conference:Conference) -> String? {
+ if (conference.subject.count > 0) {
+ return conference.subject
+ } else {
+ let conferenceInfo = Core.get().findConferenceInformationFromUri(uri: conference.conferenceAddress!)
+ if (conferenceInfo != nil) {
+ return conferenceInfo?.subject
+ } else {
+ if (conference.me?.isFocus == true) {
+ return VoipTexts.conference_local_title
+ } else {
+ return VoipTexts.conference_default_title
+
+ }
+ }
+ }
+ }
+
+
+}
+
+@objc class ConferenceViewModelBridge : NSObject {
+
+ @objc static func updateParticipantsList(addresses:[String]) {
+ do {
+ try ConferenceViewModel.shared.updateParticipants(addresses: addresses.map { try Factory.Instance.createAddress(addr: $0)} )
+ } catch {
+ Log.e("[ParticipantsListView] unable to update participants list \(error)")
+ }
+ }
+
+
+ @objc static func startGroupCall(cChatRoom: OpaquePointer ) {
+ let core = Core.get()
+ let chatRoom = ChatRoom.getSwiftObject(cObject: cChatRoom)
+ guard let localAddress = chatRoom.localAddress?.clone() else {
+ Log.e("[Group Call] Couldn't get local address from default account!")
+ return
+ }
+ localAddress.clean() // Remove GRUU
+ ConferenceViewModel.shared.conferenceScheduler = try?Core.get().createConferenceScheduler()
+ let conferenceInfo = try?Factory.Instance.createConferenceInfo()
+ conferenceInfo?.participants = chatRoom.participants.map {$0.address!}
+ conferenceInfo?.organizer = localAddress
+ conferenceInfo?.subject = chatRoom.subject
+ ConferenceViewModel.shared.conferenceScheduler?.account = core.accountList.filter { $0.params?.identityAddress?.weakEqual(address2: localAddress) == true}.first
+ ConferenceViewModel.shared.conferenceScheduler?.info = conferenceInfo // Will trigger the conference creation automatically
+ }
+
+}
+
+
+enum FlexDirection {
+ case ROW
+ case ROW_REVERSE
+ case COLUMN
+ case COLUMN_REVERSE
+}
+
+enum ConferenceDisplayMode {
+ case Grid
+ case ActiveSpeaker
+ case AudioOnly
+}
diff --git a/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift b/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift
new file mode 100644
index 000000000..691e4461d
--- /dev/null
+++ b/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift
@@ -0,0 +1,273 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linhome
+ *
+ * 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
+import linphonesw
+import AVFoundation
+
+
+class ControlsViewModel {
+ var core : Core { get { Core.get() } }
+
+ let isSpeakerSelected = MutableLiveData()
+ let isMicrophoneMuted = MutableLiveData()
+ let isMuteMicrophoneEnabled = MutableLiveData()
+ let isBluetoothHeadsetSelected = MutableLiveData()
+ let nonEarpieceOutputAudioDevice = MutableLiveData()
+ let audioRoutesSelected = MutableLiveData()
+ let audioRoutesEnabled = MutableLiveData()
+
+ let isVideoUpdateInProgress = MutableLiveData()
+ let isVideoEnabled = MutableLiveData()
+ let isVideoAvailable = MutableLiveData()
+
+ let fullScreenMode = MutableLiveData(false)
+ let numpadVisible = MutableLiveData(false)
+ let callStatsVisible = MutableLiveData(false)
+ let goToConferenceLayoutSettings = MutableLiveData(false)
+ let goToConferenceParticipantsListEvent = MutableLiveData(false)
+ let goToChatEvent = MutableLiveData(false)
+ let goToCallsListEvent = MutableLiveData(false)
+ let hideExtraButtons = MutableLiveData(true)
+
+ let proximitySensorEnabled = MutableLiveData()
+
+
+ static let shared = ControlsViewModel()
+ private var coreDelegate : CoreDelegateStub?
+ private var previousCallState = Call.State.Idle
+
+
+ init () {
+ coreDelegate = CoreDelegateStub(
+ onCallStateChanged : { (core: Core, call: Call, state: Call.State, message:String) -> Void in
+ Log.i("[Call Controls] Call state changed: \(call) : \(state)")
+ if (state == Call.State.StreamsRunning) {
+ self.isVideoUpdateInProgress.value = false
+ }
+ self.updateUI()
+ self.setAudioRoutes(call,state)
+ self.previousCallState = state
+ },
+ onAudioDeviceChanged : { (core: Core, audioDevice: AudioDevice) -> Void in
+ Log.i("[Call Controls] Audio device changed: \(audioDevice.deviceName)")
+ self.nonEarpieceOutputAudioDevice.value = audioDevice.type != AudioDeviceType.Microphone // on iOS Earpiece = Microphone
+ self.updateSpeakerState()
+ self.updateBluetoothHeadsetState()
+ }
+ )
+ Core.get().addDelegate(delegate: coreDelegate!)
+ proximitySensorEnabled.value = shouldProximitySensorBeEnabled()
+ isVideoEnabled.readCurrentAndObserve { _ in
+ self.proximitySensorEnabled.value = self.shouldProximitySensorBeEnabled()
+ }
+ nonEarpieceOutputAudioDevice.readCurrentAndObserve { _ in
+ self.proximitySensorEnabled.value = self.shouldProximitySensorBeEnabled()
+ }
+ proximitySensorEnabled.readCurrentAndObserve { (enabled) in
+ UIDevice.current.isProximityMonitoringEnabled = enabled == true
+ }
+ updateUI()
+ ConferenceViewModel.shared.conferenceDisplayMode.readCurrentAndObserve { _ in
+ self.updateVideoAvailable()
+ }
+ }
+
+ private func setAudioRoutes(_ call:Call,_ state:Call.State) {
+ if (state == .OutgoingProgress) {
+ if (core.callsNb == 1 && ConfigManager.instance().lpConfigBoolForKey(key: "route_audio_to_bluetooth_if_available",defaultValue:true)) {
+ AudioRouteUtils.routeAudioToBluetooth(call: call)
+ }
+ }
+ if (state == .StreamsRunning) {
+ if (core.callsNb == 1) {
+ // Only try to route bluetooth / headphone / headset when the call is in StreamsRunning for the first time
+ if (previousCallState == Call.State.Connected) {
+ Log.i("[Context] First call going into StreamsRunning state for the first time, trying to route audio to headset or bluetooth if available")
+ if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) {
+ AudioRouteUtils.routeAudioToHeadset(call: call)
+ } else if (ConfigManager.instance().lpConfigBoolForKey(key: "route_audio_to_bluetooth_if_available",defaultValue:true) &&
+ AudioRouteUtils.isBluetoothAudioRouteAvailable()) {
+ AudioRouteUtils.routeAudioToBluetooth(call: call)
+ }
+ }
+ }
+
+ if (ConfigManager.instance().lpConfigBoolForKey(key: "route_audio_to_speaker_when_video_enabled",defaultValue:true) && call.currentParams?.videoEnabled == true) {
+ // Do not turn speaker on when video is enabled if headset or bluetooth is used
+ if (!AudioRouteUtils.isHeadsetAudioRouteAvailable() &&
+ !AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed(call: call)
+ ) {
+ Log.i("[Context] Video enabled and no wired headset not bluetooth in use, routing audio to speaker")
+ AudioRouteUtils.routeAudioToSpeaker(call: call)
+ }
+ }
+ }
+ }
+
+
+ private func shouldProximitySensorBeEnabled() -> Bool {
+ return core.callsNb > 0 && isVideoEnabled.value != true && nonEarpieceOutputAudioDevice.value != true
+ }
+
+
+ func hangUp() {
+ if (core.currentCall != nil) {
+ try?core.currentCall?.terminate()
+ } else if (core.conference?.isIn == true) {
+ try?core.terminateConference()
+ } else {
+ try?core.terminateAllCalls()
+ }
+ }
+
+ func toggleVideo() {
+ if let currentCall = core.currentCall {
+ if (currentCall.conference != nil) {
+ if let params = try?core.createCallParams(call: currentCall) {
+ params.videoDirection = params.videoDirection == MediaDirection.RecvOnly ? MediaDirection.SendRecv : MediaDirection.RecvOnly
+ try?currentCall.update(params: params)
+ }
+ } else {
+ let state = currentCall.state
+ if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
+ return
+ }
+ isVideoUpdateInProgress.value = true
+ if let params = try?core.createCallParams(call: currentCall) {
+ params.videoEnabled = !(currentCall.currentParams?.videoEnabled == true)
+ try?currentCall.update(params: params)
+ if (params.videoEnabled) {
+ currentCall.requestNotifyNextVideoFrameDecoded()
+ }
+ }
+ }
+ }
+ }
+
+
+ func updateUI() {
+ updateVideoAvailable()
+ updateVideoEnabled()
+ updateMicState()
+ updateSpeakerState()
+ updateAudioRoutesState()
+ proximitySensorEnabled.value = shouldProximitySensorBeEnabled()
+ }
+
+ private func updateAudioRoutesState() {
+ let bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable()
+ audioRoutesEnabled.value = bluetoothDeviceAvailable
+
+ if (!bluetoothDeviceAvailable) {
+ audioRoutesSelected.value = false
+ audioRoutesEnabled.value = false
+ }
+ }
+
+ private func updateSpeakerState() {
+ isSpeakerSelected.value = AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed()
+ }
+
+ private func updateBluetoothHeadsetState() {
+ isBluetoothHeadsetSelected.value = AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed()
+ }
+
+ private func updateVideoAvailable() {
+ let currentCall = core.currentCall
+ isVideoAvailable.value =
+ (core.videoCaptureEnabled || core.videoPreviewEnabled) &&
+ currentCall?.state != .Paused &&
+ currentCall?.state != .PausedByRemote &&
+ ((currentCall != nil && currentCall?.mediaInProgress() != true) || (core.conference?.isIn == true)) &&
+ (ConferenceViewModel.shared.conferenceExists.value != true || ConferenceViewModel.shared.conferenceDisplayMode.value != .AudioOnly)
+ }
+
+ private func updateVideoEnabled() {
+ let enabled = isVideoCallOrConferenceActive()
+ isVideoEnabled.value = enabled
+ }
+
+ func updateMicState() {
+ isMicrophoneMuted.value = !micAuthorized() || !core.micEnabled
+ isMuteMicrophoneEnabled.value = core.currentCall != nil || core.conference?.isIn == true
+ }
+
+ func micAuthorized() -> Bool {
+ return AVCaptureDevice.authorizationStatus(for: .audio) == .authorized
+ }
+
+ func isVideoCallOrConferenceActive() -> Bool {
+ if let currentCall = core.currentCall, let params = currentCall.params {
+ return params.videoEnabled && (currentCall.conference == nil || params.videoDirection == MediaDirection.SendRecv)
+ } else {
+ return false
+ }
+ }
+
+ func toggleFullScreen() {
+ if (isVideoEnabled.value == true) {
+ fullScreenMode.value = fullScreenMode.value != true
+ }
+ }
+
+ func toggleMuteMicrophone() {
+ if (!micAuthorized()) {
+ AVAudioSession.sharedInstance().requestRecordPermission { granted in
+ if granted {
+ self.core.micEnabled = !self.core.micEnabled
+ self.updateMicState()
+ }
+ }
+ }
+ core.micEnabled = !core.micEnabled
+ updateMicState()
+ }
+
+ func forceEarpieceAudioRoute() {
+ if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) {
+ Log.i("[Call Controls] Headset found, route audio to it instead of earpiece")
+ AudioRouteUtils.routeAudioToHeadset()
+ } else {
+ AudioRouteUtils.routeAudioToEarpiece()
+ }
+ }
+
+ func forceSpeakerAudioRoute() {
+ AudioRouteUtils.routeAudioToSpeaker()
+ }
+
+ func forceBluetoothAudioRoute() {
+ AudioRouteUtils.routeAudioToBluetooth()
+ }
+
+ func toggleSpeaker() {
+ if (AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed()) {
+ forceEarpieceAudioRoute()
+ } else {
+ forceSpeakerAudioRoute()
+ }
+ }
+
+ func toggleRoutesMenu() {
+ audioRoutesSelected.value = audioRoutesSelected.value != true
+ }
+
+}
diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift
new file mode 100644
index 000000000..1c2bff27f
--- /dev/null
+++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift
@@ -0,0 +1,399 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import linphonesw
+
+
+@objc class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // Replaces CallView
+
+ // Layout constants
+ let content_inset = 12.0
+
+ var callPausedByRemoteView : PausedCallOrConferenceView? = nil
+ var callPausedByLocalView : PausedCallOrConferenceView? = nil
+
+ var conferencePausedView : PausedCallOrConferenceView? = nil
+
+ var currentCallView : ActiveCallView? = nil
+ var conferenceGridView: VoipConferenceGridView? = nil
+ var conferenceActiveSpeakerView: VoipConferenceActiveSpeakerView? = nil
+ var conferenceAudioOnlyView: VoipConferenceAudioOnlyView? = nil
+
+ let conferenceJoinSpinner = RotatingSpinner()
+
+
+ let extraButtonsView = VoipExtraButtonsView()
+ var numpadView : NumpadView? = nil
+ var currentCallStatsVew : CallStatsView? = nil
+ var shadingMask = UIView()
+ var videoAcceptDialog : VoipDialog? = nil
+ var dismissableView : DismissableView? = nil
+ @objc var participantsListView : ParticipantsListView? = nil
+
+ var audioRoutesView : AudioRoutesView? = nil
+
+
+ static let compositeDescription = UICompositeViewDescription(ActiveCallOrConferenceView.self, statusBar: StatusBarView.self, tabBar: nil, sideMenu: nil, fullscreen: false, isLeftFragment: false,fragmentWith: nil)
+ static func compositeViewDescription() -> UICompositeViewDescription! { return compositeDescription }
+ func compositeViewDescription() -> UICompositeViewDescription! { return type(of: self).compositeDescription }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ view.backgroundColor = VoipTheme.voipBackgroundColor.get()
+
+ // Hangup
+ let hangup = CallControlButton(width: 65, imageInset:IncomingOutgoingCommonView.answer_decline_inset, buttonTheme: VoipTheme.call_terminate, onClickAction: {
+ ControlsViewModel.shared.hangUp()
+ })
+ view.addSubview(hangup)
+ hangup.alignParentLeft(withMargin:SharedLayoutConstants.margin_call_view_side_controls_buttons).alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+
+
+ // Controls
+ let controlsView = ControlsView(showVideo: true, controlsViewModel: ControlsViewModel.shared)
+ view.addSubview(controlsView)
+ controlsView.alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done()
+
+
+ // Container view
+ let fullScreenMutableContainerView = UIView()
+ fullScreenMutableContainerView.backgroundColor = .clear
+ self.view.addSubview(fullScreenMutableContainerView)
+ fullScreenMutableContainerView.matchParentSideBorders(insetedByDx: content_inset).matchParentHeight().alignAbove(view:controlsView,withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+
+ // Current (Single) Call (VoipCallView)
+ currentCallView = ActiveCallView()
+ currentCallView!.isHidden = true
+ fullScreenMutableContainerView.addSubview(currentCallView!)
+ CallsViewModel.shared.currentCallData.readCurrentAndObserve { (currentCallData) in
+ self.updateNavigation()
+ self.currentCallView!.isHidden = currentCallData == nil || ConferenceViewModel.shared.conferenceExists.value == true
+ self.currentCallView!.callData = currentCallData != nil ? currentCallData! : nil
+ currentCallData??.isRemotelyPaused.readCurrentAndObserve { remotelyPaused in
+ self.callPausedByRemoteView?.isHidden = remotelyPaused != true || ConferenceViewModel.shared.conferenceExists.value == true
+ }
+ currentCallData??.isPaused.readCurrentAndObserve { locallyPaused in
+ self.callPausedByLocalView?.isHidden = locallyPaused != true || ConferenceViewModel.shared.conferenceExists.value == true
+ }
+ if (currentCallData == nil) {
+ self.callPausedByRemoteView?.isHidden = true
+ self.callPausedByLocalView?.isHidden = true
+
+ } else {
+ currentCallData??.isIncoming.readCurrentAndObserve { _ in self.updateNavigation() }
+ currentCallData??.isOutgoing.readCurrentAndObserve { _ in self.updateNavigation() }
+ }
+ self.extraButtonsView.isHidden = true
+ self.conferencePausedView?.isHidden = true
+ if (ConferenceViewModel.shared.conferenceExists.value != true) {
+ self.conferenceGridView?.isHidden = true
+ self.conferenceActiveSpeakerView?.isHidden = true
+ self.conferenceAudioOnlyView?.isHidden = true
+ }
+
+ }
+
+ currentCallView!.matchParentDimmensions().done()
+
+ // Paused by remote (Call)
+ callPausedByRemoteView = PausedCallOrConferenceView(iconName: "voip_conference_paused_big",titleText: VoipTexts.call_remotely_paused_title,subTitleText: nil)
+ view.addSubview(callPausedByRemoteView!)
+ callPausedByRemoteView?.matchParentSideBorders().matchParentHeight().alignAbove(view:controlsView,withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+ callPausedByRemoteView?.isHidden = true
+
+ // Paused by local (Call)
+ callPausedByLocalView = PausedCallOrConferenceView(iconName: "voip_conference_play_big",titleText: VoipTexts.call_locally_paused_title,subTitleText: VoipTexts.call_locally_paused_subtitle)
+ view.addSubview(callPausedByLocalView!)
+ callPausedByLocalView?.matchParentSideBorders().matchParentHeight().alignAbove(view:controlsView,withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+ callPausedByLocalView?.isHidden = true
+ callPausedByLocalView?.onClick {
+ CallsViewModel.shared.currentCallData.value??.togglePause()
+ }
+
+
+
+ // Conference paused
+ conferencePausedView = PausedCallOrConferenceView(iconName: "voip_conference_play_big",titleText: VoipTexts.conference_paused_title,subTitleText: VoipTexts.conference_paused_subtitle)
+ view.addSubview(conferencePausedView!)
+ conferencePausedView?.matchParentSideBorders().matchParentHeight().alignAbove(view:controlsView,withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+ conferencePausedView?.isHidden = true
+ conferencePausedView?.onClick {
+ ConferenceViewModel.shared.togglePlayPause()
+ }
+
+ // Conference grid
+ conferenceGridView = VoipConferenceGridView()
+ fullScreenMutableContainerView.addSubview(conferenceGridView!)
+ conferenceGridView?.matchParentDimmensions().done()
+ conferenceGridView?.isHidden = true
+ ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { (exists) in
+ self.updateNavigation()
+ if (exists == true) {
+ self.currentCallView!.isHidden = true
+ self.extraButtonsView.isHidden = true
+ self.conferencePausedView?.isHidden = true
+ self.displaySelectedConferenceLayout()
+ } else {
+ self.conferenceGridView?.isHidden = true
+ self.conferenceActiveSpeakerView?.isHidden = true
+ self.conferenceActiveSpeakerView?.isHidden = true
+ }
+ }
+
+ ConferenceViewModel.shared.conferenceCreationPending.readCurrentAndObserve { isCreationPending in
+ if (ConferenceViewModel.shared.conferenceExists.value == true && isCreationPending == true) {
+ fullScreenMutableContainerView.addSubview(self.conferenceJoinSpinner)
+ self.conferenceJoinSpinner.square(IncomingOutgoingCommonView.spinner_size).center().done()
+ self.conferenceJoinSpinner.startRotation()
+ } else {
+ self.conferenceJoinSpinner.removeFromSuperview()
+ self.conferenceJoinSpinner.stopRotation()
+ }
+ }
+
+ // Conference active speaker
+ conferenceActiveSpeakerView = VoipConferenceActiveSpeakerView()
+ fullScreenMutableContainerView.addSubview(conferenceActiveSpeakerView!)
+ conferenceActiveSpeakerView?.matchParentDimmensions().done()
+ conferenceActiveSpeakerView?.isHidden = true
+
+
+ // Conference audio only
+ conferenceAudioOnlyView = VoipConferenceAudioOnlyView()
+ fullScreenMutableContainerView.addSubview(conferenceAudioOnlyView!)
+ conferenceAudioOnlyView?.matchParentDimmensions().done()
+ conferenceAudioOnlyView?.isHidden = true
+
+ ConferenceViewModel.shared.conferenceDisplayMode.readCurrentAndObserve { (conferenceMode) in
+ if (ConferenceViewModel.shared.conferenceExists.value == true) {
+ self.displaySelectedConferenceLayout()
+ }
+ }
+ ConferenceViewModel.shared.isConferenceLocallyPaused.readCurrentAndObserve { (paused) in
+ self.conferencePausedView?.isHidden = paused != true || ConferenceViewModel.shared.conferenceExists.value != true
+ }
+
+
+ // Calls List
+ ControlsViewModel.shared.goToCallsListEvent.observe { (_) in
+ self.dismissableView = CallsListView()
+ self.view.addSubview(self.dismissableView!)
+ self.dismissableView?.matchParentDimmensions().done()
+ }
+
+ // Conference Participants List
+ ControlsViewModel.shared.goToConferenceParticipantsListEvent.observe { (_) in
+ self.participantsListView = ParticipantsListView()
+ self.view.addSubview(self.participantsListView!)
+ self.participantsListView?.matchParentDimmensions().done()
+ }
+
+ // Goto chat
+ ControlsViewModel.shared.goToChatEvent.observe { (_) in
+ self.goToChat()
+ }
+
+ // Conference mode selection
+ ControlsViewModel.shared.goToConferenceLayoutSettings.observe { (_) in
+ self.dismissableView = VoipConferenceDisplayModeSelectionView()
+ self.view.addSubview(self.dismissableView!)
+ self.dismissableView?.matchParentDimmensions().done()
+ let activeDisplayMode = ConferenceViewModel.shared.conferenceDisplayMode.value!
+ let indexPath = IndexPath(row: activeDisplayMode == .Grid ? 0 : activeDisplayMode == .ActiveSpeaker ? 1 : 2, section: 0)
+ (self.dismissableView as! VoipConferenceDisplayModeSelectionView).optionsListView.selectRow(at:indexPath, animated: true, scrollPosition: .bottom)
+ }
+
+ // Shading mask, everything before will be shaded upon displaying of the mask
+ shadingMask.backgroundColor = VoipTheme.voip_translucent_popup_background
+ shadingMask.isHidden = true
+ self.view.addSubview(shadingMask)
+ shadingMask.matchParentDimmensions().done()
+
+ // Extra Buttons
+ let showextraButtons = CallControlButton(imageInset:IncomingOutgoingCommonView.answer_decline_inset, buttonTheme: VoipTheme.call_more, onClickAction: {
+ self.showModalSubview(view: self.extraButtonsView)
+ ControlsViewModel.shared.audioRoutesSelected.value = false
+ })
+ view.addSubview(showextraButtons)
+ showextraButtons.alignParentRight(withMargin:SharedLayoutConstants.margin_call_view_side_controls_buttons).alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+
+ let boucingCounter = BouncingCounter(inButton:showextraButtons)
+ view.addSubview(boucingCounter)
+ boucingCounter.dataSource = CallsViewModel.shared.chatAndCallsCount
+
+ view.addSubview(extraButtonsView)
+ extraButtonsView.matchParentSideBorders(insetedByDx: content_inset).alignParentBottom(withMargin:SharedLayoutConstants.bottom_margin_notch_clearance).done()
+ ControlsViewModel.shared.hideExtraButtons.readCurrentAndObserve { (_) in
+ self.hideModalSubview(view: self.extraButtonsView)
+ }
+ self.view.onClick {
+ if (!self.extraButtonsView.isHidden) {
+ self.hideModalSubview(view: self.extraButtonsView)
+ }
+ ControlsViewModel.shared.audioRoutesSelected.value = false
+ }
+
+ // Numpad
+ ControlsViewModel.shared.numpadVisible.readCurrentAndObserve { (visible) in
+ if (visible == true && CallsViewModel.shared.currentCallData.value != nil ) {
+ self.numpadView?.removeFromSuperview()
+ self.shadingMask.isHidden = false
+ self.numpadView = NumpadView(superView: self.view,callData: CallsViewModel.shared.currentCallData.value!!,marginTop:self.currentCallView?.centerSection.frame.origin.y ?? 0.0, onDismissAction: {
+ self.numpadView?.removeFromSuperview()
+ self.shadingMask.isHidden = true
+ })
+ }
+ }
+
+ // Call stats
+ ControlsViewModel.shared.callStatsVisible.readCurrentAndObserve { (visible) in
+ if (visible == true && CallsViewModel.shared.currentCallData.value != nil ) {
+ self.currentCallStatsVew?.removeFromSuperview()
+ self.shadingMask.isHidden = false
+ self.currentCallStatsVew = CallStatsView(superView: self.view,callData: CallsViewModel.shared.currentCallData.value!!,marginTop:self.currentCallView?.centerSection.frame.origin.y ?? 0.0, onDismissAction: {
+ self.currentCallStatsVew?.removeFromSuperview()
+ self.shadingMask.isHidden = true
+ })
+ }
+ }
+
+ // Video activation dialog request
+ CallsViewModel.shared.callUpdateEvent.observe { (call) in
+ let core = Core.get()
+ if (call?.state == .StreamsRunning) {
+ self.videoAcceptDialog?.removeFromSuperview()
+ self.videoAcceptDialog = nil
+ } else if (call?.state == .UpdatedByRemote) {
+ if (core.videoCaptureEnabled || core.videoDisplayEnabled) {
+ if (call?.currentParams?.videoEnabled != call?.remoteParams?.videoEnabled) {
+ let accept = ButtonAttributes(text:VoipTexts.dialog_accept, action: {call?.answerVideoUpdateRequest(accept: true)}, isDestructive:false)
+ let cancel = ButtonAttributes(text:VoipTexts.dialog_decline, action: {call?.answerVideoUpdateRequest(accept: false)}, isDestructive:true)
+ self.videoAcceptDialog = VoipDialog(message:VoipTexts.call_video_update_requested_dialog, givenButtons: [cancel,accept])
+ self.videoAcceptDialog?.show()
+ }
+ } else {
+ Log.w("[Call] Video display & capture are disabled, don't show video dialog")
+ }
+ }
+ }
+
+ // Audio Routes
+ audioRoutesView = AudioRoutesView()
+ view.addSubview(audioRoutesView!)
+ audioRoutesView!.alignBottomWith(otherView: controlsView).done()
+ ControlsViewModel.shared.audioRoutesSelected.readCurrentAndObserve { (audioRoutesSelected) in
+ self.audioRoutesView!.isHidden = audioRoutesSelected != true
+ }
+ audioRoutesView!.alignAbove(view:controlsView,withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done()
+
+ // First/Last to join conference :
+
+ ConferenceViewModel.shared.allParticipantsLeftEvent.observe { (allLeft) in
+ if (allLeft == true) {
+ VoipDialog.toast(message: VoipTexts.conference_last_user)
+ }
+ }
+ ConferenceViewModel.shared.firstToJoinEvent.observe { (first) in
+ if (first == true) {
+ VoipDialog.toast(message: VoipTexts.conference_first_to_join)
+ }
+ }
+
+ }
+
+ func displaySelectedConferenceLayout() {
+ let conferenceMode = ConferenceViewModel.shared.conferenceDisplayMode.value
+ self.conferenceGridView!.isHidden = conferenceMode != .Grid
+ self.conferenceActiveSpeakerView!.isHidden = conferenceMode != .ActiveSpeaker
+ self.conferenceAudioOnlyView!.isHidden = conferenceMode != .AudioOnly
+ if (conferenceMode == .Grid) {
+ self.conferenceGridView?.conferenceViewModel = ConferenceViewModel.shared
+ }
+ if (conferenceMode == .AudioOnly) {
+ self.conferenceAudioOnlyView?.conferenceViewModel = ConferenceViewModel.shared
+ }
+ if (conferenceMode == .ActiveSpeaker) {
+ self.conferenceActiveSpeakerView?.conferenceViewModel = ConferenceViewModel.shared
+ }
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(true)
+ extraButtonsView.refresh()
+ ControlsViewModel.shared.callStatsVisible.notifyValue()
+ CallsViewModel.shared.currentCallData.notifyValue()
+ ControlsViewModel.shared.audioRoutesSelected.value = false
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ dismissableView?.removeFromSuperview()
+ dismissableView = nil
+
+ participantsListView?.removeFromSuperview()
+ participantsListView = nil
+
+ ControlsViewModel.shared.fullScreenMode.value = false
+ super.viewWillDisappear(animated)
+ }
+
+ func showModalSubview(view:UIView) {
+ view.isHidden = false
+ shadingMask.isHidden = false
+ }
+ func hideModalSubview(view:UIView) {
+ view.isHidden = true
+ shadingMask.isHidden = true
+ }
+
+ func updateNavigation() {
+ if (Core.get().callsNb == 0) {
+ PhoneMainView.instance().popView(self.compositeViewDescription())
+ } else {
+ if let data = CallsViewModel.shared.currentCallData.value {
+ if (data?.isOutgoing.value == true || data?.isIncoming.value == true) {
+ PhoneMainView.instance().popView(self.compositeViewDescription())
+ } else {
+ if (data!.isInRemoteConference.value == true) {
+ PhoneMainView.instance().pop(toView: self.compositeViewDescription())
+ } else {
+ PhoneMainView.instance().changeCurrentView(self.compositeViewDescription())
+ }
+ }
+ } else {
+ PhoneMainView.instance().changeCurrentView(self.compositeViewDescription())
+ }
+ }
+ }
+
+ func goToChat() {
+ /*guard
+ let chatRoom = CallsViewModel.shared.currentCallData.value??.chatRoom
+ else {
+ Log.w("[Call] Failed to find existing chat room associated to call")
+ return
+ }*/
+ PhoneMainView.instance().changeCurrentView(ChatsListView.compositeViewDescription())
+
+ }
+
+
+}
diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/IncomingCallView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/IncomingCallView.swift
new file mode 100644
index 000000000..6009bf0a5
--- /dev/null
+++ b/Classes/Swift/Voip/Views/CompositeViewControllers/IncomingCallView.swift
@@ -0,0 +1,81 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import linphonesw
+
+@objc class IncomingCallView: IncomingOutgoingCommonView, UICompositeViewDelegate {
+
+ // Layout constants
+ let buttons_distance_from_center_x = 38
+
+ static let compositeDescription = UICompositeViewDescription(IncomingCallView.self, statusBar: nil, tabBar: nil, sideMenu: nil, fullscreen: true, isLeftFragment: false,fragmentWith: nil)
+ static func compositeViewDescription() -> UICompositeViewDescription! { return compositeDescription }
+ func compositeViewDescription() -> UICompositeViewDescription! { return type(of: self).compositeDescription }
+
+ var earlyMediaView : UIView? = nil
+
+ override func viewDidLoad() {
+
+ super.viewDidLoad(forCallType: VoipTexts.call_incoming_title)
+
+ // Accept
+ let accept = CallControlButton(width: CallControlButton.hungup_width, imageInset:IncomingOutgoingCommonView.answer_decline_inset, buttonTheme: VoipTheme.call_accept, onClickAction: {
+ self.callData.map { CallManager.instance().acceptCall(call: $0.call.getCobject, hasVideo: false)}
+ })
+ view.addSubview(accept)
+ accept.centerX(withDx: buttons_distance_from_center_x).alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+
+ // Decline
+ let decline = CallControlButton(width: CallControlButton.hungup_width, imageInset:IncomingOutgoingCommonView.answer_decline_inset, buttonTheme: VoipTheme.call_terminate, onClickAction: {
+ self.callData.map { CallManager.instance().terminateCall(call: $0.call.getCobject)}
+ })
+ view.addSubview(decline)
+ decline.centerX(withDx: -buttons_distance_from_center_x).alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+ }
+
+ @objc override func setCall(call:OpaquePointer) {
+ super.setCall(call: call)
+ callData?.iFrameReceived.observe(onChange: { (video) in
+ if (video == true) {
+ Core.get().nativeVideoWindow = self.earlyMediaView
+ self.earlyMediaView?.isHidden = false
+ }
+ })
+ callData?.callState.readCurrentAndObserve(onChange: { (state) in
+ if (ConfigManager.instance().lpConfigBoolForKey(key: "pref_accept_early_media") && state == .IncomingReceived) {
+ try?self.callData?.call.acceptEarlyMedia()
+ self.callData?.call.requestNotifyNextVideoFrameDecoded()
+ }
+ })
+ callData?.isIncoming.readCurrentAndObserve { (incoming) in
+ if (incoming != true) {
+ PhoneMainView.instance().popView(self.compositeViewDescription())
+ }
+ }
+
+ if (ConfigManager.instance().lpConfigBoolForKey(key: "auto_answer")) {
+ CallManager.instance().acceptCall(call: call, hasVideo: false) // TODO check with old version for Video accept separate button - Not implemented in Android
+ }
+ }
+
+
+}
diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift
new file mode 100644
index 000000000..613f05eb4
--- /dev/null
+++ b/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift
@@ -0,0 +1,102 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import linphonesw
+
+@objc class OutgoingCallView: IncomingOutgoingCommonView, UICompositeViewDelegate {
+
+ // Layout constants
+ let numpad_icon_padding = 10.0
+
+ var numpadView : NumpadView? = nil
+ var showNumPad : CallControlButton? = nil
+ var shadingMask = UIView()
+
+
+ static let compositeDescription = UICompositeViewDescription(OutgoingCallView.self, statusBar: nil, tabBar: nil, sideMenu: nil, fullscreen: true, isLeftFragment: false,fragmentWith: nil)
+ static func compositeViewDescription() -> UICompositeViewDescription! { return compositeDescription }
+ func compositeViewDescription() -> UICompositeViewDescription! { return OutgoingCallView.compositeDescription }
+
+ override func viewDidLoad() {
+ super.viewDidLoad(forCallType: VoipTexts.call_outgoing_title)
+
+ // Cancel
+ let cancelCall = CallControlButton(width: CallControlButton.hungup_width, imageInset:IncomingOutgoingCommonView.answer_decline_inset, buttonTheme: VoipTheme.call_terminate, onClickAction: {
+ self.callData.map { CallManager.instance().terminateCall(call: $0.call.getCobject)}
+ })
+ view.addSubview(cancelCall)
+ cancelCall.alignParentLeft(withMargin:SharedLayoutConstants.margin_call_view_side_controls_buttons).alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+
+ // Controls
+ let controlsView = ControlsView(showVideo: false, controlsViewModel: ControlsViewModel.shared)
+ view.addSubview(controlsView)
+ controlsView.alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done()
+
+ // Shading mask, everything after will be shaded upon displayed
+ shadingMask.backgroundColor = VoipTheme.voip_translucent_popup_background
+ shadingMask.isHidden = true
+ self.view.addSubview(shadingMask)
+ shadingMask.matchParentDimmensions().done()
+
+ // Numpad
+ showNumPad = CallControlButton(imageInset:UIEdgeInsets(top: numpad_icon_padding, left: numpad_icon_padding, bottom: numpad_icon_padding, right: numpad_icon_padding), buttonTheme: VoipTheme.call_numpad, onClickAction: {
+ self.numpadView?.removeFromSuperview()
+ self.shadingMask.isHidden = false
+ self.numpadView = NumpadView(superView: self.view,callData: self.callData!, marginTop: 0.0, onDismissAction: {
+ self.numpadView?.removeFromSuperview()
+ self.shadingMask.isHidden = true
+ })
+ })
+ view.addSubview(showNumPad!)
+ showNumPad?.alignParentRight(withMargin:SharedLayoutConstants.margin_call_view_side_controls_buttons).alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+ showNumPad!.isHidden = true
+
+ // Audio Routes
+ let audioRoutesView = AudioRoutesView()
+ view.addSubview(audioRoutesView)
+ audioRoutesView.alignBottomWith(otherView: controlsView).done()
+ ControlsViewModel.shared.audioRoutesSelected.readCurrentAndObserve { (audioRoutesSelected) in
+ audioRoutesView.isHidden = audioRoutesSelected != true
+ }
+ audioRoutesView.alignAbove(view:controlsView,withMargin:SharedLayoutConstants.buttons_bottom_margin).matchRightOf(view: controlsView, withMargin:+ControlsView.controls_button_spacing).done()
+ }
+
+
+ override func viewWillAppear(_ animated: Bool) {
+ ControlsViewModel.shared.audioRoutesSelected.value = false
+ super.viewWillAppear(animated)
+ }
+
+ @objc override func setCall(call:OpaquePointer) {
+ super.setCall(call: call)
+ self.callData?.outgoingEarlyMedia.readCurrentAndObserve(onChange: { (outgoingEM) in
+ self.showNumPad!.isHidden = outgoingEM != true
+ })
+ callData?.isOutgoing.readCurrentAndObserve { (outgoing) in
+ if (outgoing != true) {
+ PhoneMainView.instance().popView(self.compositeViewDescription())
+ }
+ }
+ }
+
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/ActiveCall/ActiveCallView.swift b/Classes/Swift/Voip/Views/Fragments/ActiveCall/ActiveCallView.swift
new file mode 100644
index 000000000..e08dc14b2
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/ActiveCall/ActiveCallView.swift
@@ -0,0 +1,237 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class ActiveCallView: UIView { // = currentCall
+
+ // Layout constants :
+ static let top_displayname_margin_top = 20.0
+ let sip_address_margin_top = 4.0
+ static let remote_recording_margin_top = 10.0
+ static let remote_recording_height = 30
+ static let bottom_displayname_margin_bottom = 10.0
+ static let bottom_displayname_margin_left = 12.0
+ static let center_view_margin_top = 15.0
+ static let center_view_corner_radius = 20.0
+ let record_pause_button_size = 40
+ let record_pause_button_inset = UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7)
+ let record_pause_button_margin = 10.0
+ static let local_video_width = 150.0
+ static let local_video_margins = 15.0
+
+
+ let upperSection = UIStackView()
+ let displayNameTop = StyledLabel(VoipTheme.call_display_name_duration)
+ let duration = CallTimer(nil, VoipTheme.call_display_name_duration)
+ let sipAddress = StyledLabel(VoipTheme.call_sip_address)
+ let remotelyRecordedIndicator = RemotelyRecordingView(height: ActiveCallView.remote_recording_height,text: VoipTexts.call_remote_recording)
+
+ let centerSection = UIView()
+ let avatar = Avatar(diameter: CGFloat(Avatar.diameter_for_call_views), color:VoipTheme.voipBackgroundColor, textStyle: VoipTheme.call_generated_avatar_large)
+ let displayNameBottom = StyledLabel(VoipTheme.call_remote_name)
+ var recordCallButtons : [CallControlButton] = []
+ var pauseCallButtons : [CallControlButton] = []
+ let remoteVideo = UIView()
+ let localVideo = LocalVideoView(width: local_video_width)
+
+ var callData: CallData? = nil {
+ didSet {
+ duration.call = callData?.call
+ callData?.call.remoteAddress.map {
+ avatar.fillFromAddress(address: $0)
+ if let displayName = $0.addressBookEnhancedDisplayName() {
+ displayNameTop.text = displayName+" - "
+ displayNameBottom.text = displayName
+ }
+ sipAddress.text = $0.asStringUriOnly()
+ }
+ self.remotelyRecordedIndicator.isRemotelyRecorded = callData?.isRemotelyRecorded
+ callData?.isRecording.readCurrentAndObserve { (selected) in
+ self.recordCallButtons.forEach {
+ $0.isSelected = selected == true
+ }
+ }
+ callData?.isPaused.readCurrentAndObserve { (paused) in
+ self.pauseCallButtons.forEach {
+ $0.isSelected = paused == true
+ }
+ if (paused == true) {
+ self.localVideo.isHidden = true
+ }
+ }
+ callData?.isRemotelyRecorded.readCurrentAndObserve { (remotelyRecorded) in
+ self.centerSection.removeConstraints().matchParentSideBorders().alignUnder(view:remotelyRecorded == true ? self.remotelyRecordedIndicator : self.upperSection ,withMargin: ActiveCallView.center_view_margin_top).alignParentBottom().done()
+ self.setNeedsLayout()
+ }
+
+
+
+ Core.get().nativeVideoWindow = remoteVideo
+ Core.get().nativePreviewWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(localVideo).toOpaque())
+
+ ControlsViewModel.shared.isVideoEnabled.readCurrentAndObserve{ (video) in
+ self.remoteVideo.isHidden = video != true
+ self.localVideo.isHidden = video != true
+ self.recordCallButtons.first?.isHidden = video != true
+ self.pauseCallButtons.first?.isHidden = video != true
+ self.recordCallButtons.last?.isHidden = video == true
+ self.pauseCallButtons.last?.isHidden = video == true
+ }
+
+ }
+ }
+
+ init() {
+ super.init(frame: .zero)
+ let stack = UIStackView()
+ stack.distribution = .equalSpacing
+ stack.alignment = .bottom
+ stack.spacing = record_pause_button_margin
+ stack.axis = .vertical
+
+ let displayNameDurationSipAddress = UIView()
+
+ displayNameDurationSipAddress.addSubview(displayNameTop)
+ displayNameTop.alignParentLeft().done()
+
+ displayNameDurationSipAddress.addSubview(duration)
+ duration.toRightOf(displayNameTop).alignParentRight().done()
+
+ displayNameDurationSipAddress.addSubview(sipAddress)
+ sipAddress.matchParentSideBorders().alignUnder(view: displayNameTop,withMargin:sip_address_margin_top).done()
+
+ upperSection.distribution = .equalSpacing
+ upperSection.alignment = .center
+ upperSection.spacing = record_pause_button_margin
+ upperSection.axis = .horizontal
+
+ upperSection.addArrangedSubview(displayNameDurationSipAddress)
+ displayNameDurationSipAddress.wrapContentY().done()
+
+ let recordPauseView = UIStackView()
+ recordPauseView.spacing = record_pause_button_margin
+
+ // Record (with video)
+ var recordCall = CallControlButton(width: record_pause_button_size, height: record_pause_button_size, imageInset:record_pause_button_inset, buttonTheme: VoipTheme.call_record, onClickAction: {
+ self.callData.map { $0.toggleRecord() }
+ })
+ recordCallButtons.append(recordCall)
+ recordPauseView.addArrangedSubview(recordCall)
+
+ // Pause (with video)
+ var pauseCall = CallControlButton(width: record_pause_button_size, height: record_pause_button_size, imageInset:record_pause_button_inset, buttonTheme: VoipTheme.call_pause, onClickAction: {
+ self.callData.map { $0.togglePause() }
+ })
+ pauseCallButtons.append(pauseCall)
+ recordPauseView.addArrangedSubview(pauseCall)
+ upperSection.addArrangedSubview(recordPauseView)
+
+
+ stack.addArrangedSubview(upperSection)
+ upperSection.matchParentSideBorders().alignParentTop(withMargin:ActiveCallView.top_displayname_margin_top).done()
+
+
+ stack.addArrangedSubview(remotelyRecordedIndicator)
+ remotelyRecordedIndicator.matchParentSideBorders().height(CGFloat(ActiveCallView.remote_recording_height)).done()
+
+ // Center Section : Avatar + video + record/pause buttons + videos
+ centerSection.layer.cornerRadius = ActiveCallView.center_view_corner_radius
+ centerSection.clipsToBounds = true
+ centerSection.backgroundColor = VoipTheme.voipParticipantBackgroundColor.get()
+
+ // Record (w/o video)
+ recordCall = CallControlButton(width: record_pause_button_size, height: record_pause_button_size, imageInset:record_pause_button_inset, buttonTheme: VoipTheme.call_record, onClickAction: {
+ self.callData.map { $0.toggleRecord() }
+ })
+ recordCallButtons.append(recordCall)
+ centerSection.addSubview(recordCall)
+ recordCall.alignParentLeft(withMargin:record_pause_button_margin).alignParentTop(withMargin:record_pause_button_margin).done()
+
+ // Pause (w/o video)
+ pauseCall = CallControlButton(width: record_pause_button_size, height: record_pause_button_size, imageInset:record_pause_button_inset, buttonTheme: VoipTheme.call_pause, onClickAction: {
+ self.callData.map { $0.togglePause() }
+ })
+ pauseCallButtons.append(pauseCall)
+ centerSection.addSubview(pauseCall)
+ pauseCall.alignParentRight(withMargin:record_pause_button_margin).alignParentTop(withMargin:record_pause_button_margin).done()
+
+ // Avatar
+ centerSection.addSubview(avatar)
+ avatar.square(Avatar.diameter_for_call_views).center().done()
+
+ // Remote Video Display
+ centerSection.addSubview(remoteVideo)
+ remoteVideo.isHidden = true
+ remoteVideo.matchParentDimmensions().done()
+
+ // Local Video Display
+ centerSection.addSubview(localVideo)
+ localVideo.backgroundColor = .black
+ localVideo.alignParentBottom(withMargin: ActiveCallView.local_video_margins).alignParentRight(withMargin: ActiveCallView.local_video_margins).done()
+ localVideo.isHidden = true
+ localVideo.dragZone = centerSection
+
+ // Full screen video togggle
+ remoteVideo.onClick {
+ ControlsViewModel.shared.toggleFullScreen()
+ }
+ ControlsViewModel.shared.fullScreenMode.observe { (fullScreen) in
+ if (self.isHidden) {
+ return
+ }
+ self.remoteVideo.removeConstraints().done()
+ self.localVideo.removeConstraints().done()
+ if (fullScreen == true) {
+ self.remoteVideo.removeFromSuperview()
+ self.localVideo.removeFromSuperview()
+ PhoneMainView.instance().mainViewController.view?.addSubview(self.remoteVideo)
+ PhoneMainView.instance().mainViewController.view?.addSubview(self.localVideo)
+ } else {
+ self.remoteVideo.removeFromSuperview()
+ self.localVideo.removeFromSuperview()
+ self.centerSection.addSubview(self.remoteVideo)
+ self.centerSection.addSubview(self.localVideo)
+ }
+ self.remoteVideo.matchParentDimmensions().done()
+ self.localVideo.alignParentBottom(withMargin: ActiveCallView.local_video_margins).alignParentRight(withMargin: ActiveCallView.local_video_margins).done()
+ self.localVideo.setSizeConstraint()
+ }
+
+
+ // Bottom display name
+ centerSection.addSubview(displayNameBottom)
+ displayNameBottom.alignParentLeft(withMargin:ActiveCallView.bottom_displayname_margin_left).alignParentRight().alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done()
+
+ stack.addArrangedSubview(centerSection)
+
+ addSubview(stack)
+ stack.matchParentDimmensions().done()
+
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/AudioRoutesView.swift b/Classes/Swift/Voip/Views/Fragments/AudioRoutesView.swift
new file mode 100644
index 000000000..ad5a02544
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/AudioRoutesView.swift
@@ -0,0 +1,83 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import UIKit
+
+class AudioRoutesView: UIStackView {
+
+ // Layout constants
+ let corner_radius = 6.7
+ let margin = 10.0
+
+ init () {
+ super.init(frame: .zero)
+ axis = .vertical
+ distribution = .equalCentering
+ alignment = .center
+ spacing = ControlsView.controls_button_spacing
+ backgroundColor = VoipTheme.voip_gray
+ layer.cornerRadius = corner_radius
+ clipsToBounds = true
+
+ // bluetooth
+ let blueTooth = CallControlButton(buttonTheme: VoipTheme.route_bluetooth, onClickAction: {
+ ControlsViewModel.shared.forceBluetoothAudioRoute()
+ ControlsViewModel.shared.audioRoutesSelected.value = false
+ })
+ addArrangedSubview(blueTooth)
+
+ ControlsViewModel.shared.isBluetoothHeadsetSelected.readCurrentAndObserve { (selected) in
+ blueTooth.isSelected = selected == true
+ }
+
+ // Earpiece
+ let earpiece = CallControlButton(buttonTheme: VoipTheme.route_earpiece, onClickAction: {
+ ControlsViewModel.shared.forceEarpieceAudioRoute()
+ ControlsViewModel.shared.audioRoutesSelected.value = false
+ })
+ addArrangedSubview(earpiece)
+ ControlsViewModel.shared.isSpeakerSelected.readCurrentAndObserve { (isSpeakerSelected) in
+ earpiece.isSelected = isSpeakerSelected != true && ControlsViewModel.shared.isBluetoothHeadsetSelected.value != true
+ }
+ ControlsViewModel.shared.isBluetoothHeadsetSelected.readCurrentAndObserve { (isBluetoothHeadsetSelected) in
+ earpiece.isSelected = isBluetoothHeadsetSelected != true && ControlsViewModel.shared.isSpeakerSelected.value != true
+ }
+
+ // Speaker
+ let speaker = CallControlButton(buttonTheme: VoipTheme.route_speaker, onClickAction: {
+ ControlsViewModel.shared.forceSpeakerAudioRoute()
+ ControlsViewModel.shared.audioRoutesSelected.value = false
+ })
+ addArrangedSubview(speaker)
+ ControlsViewModel.shared.isSpeakerSelected.readCurrentAndObserve { (selected) in
+ speaker.isSelected = selected == true
+ }
+
+ size(w:CGFloat(CallControlButton.default_size)+margin, h : 3*CGFloat(CallControlButton.default_size)+2*CGFloat(ControlsView.controls_button_spacing)+margin).done()
+
+ }
+
+ required init(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+}
+
+
diff --git a/Classes/Swift/Voip/Views/Fragments/CallStatsView.swift b/Classes/Swift/Voip/Views/Fragments/CallStatsView.swift
new file mode 100644
index 000000000..27214817d
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/CallStatsView.swift
@@ -0,0 +1,100 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import linphonesw
+
+@objc class CallStatsView: UIView {
+
+ // Layout constants
+ let side_margins = 10.0
+ let margin_top = 50
+ let corner_radius = 20.0
+ let audio_video_margin = 20
+
+ init(superView:UIView, callData:CallData, marginTop:CGFloat, onDismissAction : @escaping ()->Void) {
+ super.init(frame:.zero)
+ backgroundColor = VoipTheme.voip_translucent_popup_background
+ layer.cornerRadius = corner_radius
+ clipsToBounds = true
+ superView.addSubview(self)
+ matchParentSideBorders(insetedByDx: side_margins).alignParentTop(withMargin: marginTop).alignParentBottom().done()
+
+ callData.callState.observe { state in
+ if (state == Call.State.End) {
+ onDismissAction()
+ }
+ }
+
+ let hide = CallControlButton(buttonTheme: VoipTheme.voip_cancel_light, onClickAction: {
+ onDismissAction()
+ })
+ addSubview(hide)
+ hide.alignParentRight(withMargin: side_margins).alignParentTop(withMargin: side_margins).done()
+
+
+ let model = CallStatisticsData(call: callData.call)
+ let audioTitle = StyledLabel(VoipTheme.call_stats_font_title,NSLocalizedString("Audio", comment: ""))
+ addSubview(audioTitle)
+ audioTitle.matchParentSideBorders().alignParentTop(withMargin: margin_top).done()
+
+ let audioStats = StyledLabel(VoipTheme.call_stats_font)
+
+ audioStats.numberOfLines = 0
+ addSubview(audioStats)
+ audioStats.matchParentSideBorders().alignUnder(view: audioTitle).done()
+
+ let videoTitle = StyledLabel(VoipTheme.call_stats_font_title,NSLocalizedString("Video", comment: ""))
+ addSubview(videoTitle)
+ videoTitle.alignUnder(view: audioStats, withMargin:audio_video_margin).matchParentSideBorders().done()
+
+ let videoStats = StyledLabel(VoipTheme.call_stats_font)
+
+ videoStats.numberOfLines = 0
+ addSubview(videoStats)
+ videoStats.matchParentSideBorders().alignUnder(view: videoTitle).done()
+
+ model.isVideoEnabled.readCurrentAndObserve { (video) in
+ videoTitle.isHidden = video != true
+ videoStats.isHidden = video != true
+ }
+
+ model.statsUpdated.readCurrentAndObserve { (updated) in
+ var stats = ""
+ model.audioStats.forEach {
+ stats += "\n\($0.getTypeTitle())\($0.value.value ?? "n/a")"
+ }
+ audioStats.text = stats
+ stats = ""
+ model.videoStats.forEach {
+ stats += "\n\($0.getTypeTitle())\($0.value.value ?? "n/a")"
+ }
+ videoStats.text = stats
+ }
+
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+}
+
+
+
diff --git a/Classes/Swift/Voip/Views/Fragments/CallsList/CallsListView.swift b/Classes/Swift/Voip/Views/Fragments/CallsList/CallsListView.swift
new file mode 100644
index 000000000..57324d918
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/CallsList/CallsListView.swift
@@ -0,0 +1,161 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import linphonesw
+
+@objc class CallsListView: DismissableView, UITableViewDataSource {
+
+ // Layout constants
+ let buttons_distance_from_center_x = 38
+ let buttons_size = 60
+
+ let callsListTableView = UITableView()
+ let menuView = VoipCallContextMenu()
+
+ var callsDataObserver : MutableLiveDataOnChangeClosure<[CallData]>? = nil
+
+
+
+ init() {
+ super.init(title: VoipTexts.call_action_calls_list)
+
+ // New Call
+ let newCall = CallControlButton(width: buttons_size,height: buttons_size, imageInset:UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), buttonTheme: VoipTheme.call_add, onClickAction: {
+ let view: DialerView = self.VIEW(DialerView.compositeViewDescription());
+ view.setAddress("")
+ CallManager.instance().nextCallIsTransfer = false
+ PhoneMainView.instance().changeCurrentView(view.compositeViewDescription())
+ })
+ addSubview(newCall)
+ newCall.centerX(withDx: -buttons_distance_from_center_x).alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+
+ // Merge Calls
+ let mergeIntoLocalConference = CallControlButton(width: buttons_size,height: buttons_size, buttonTheme: VoipTheme.call_merge, onClickAction: {
+ self.removeFromSuperview()
+ if (ConferenceViewModel.shared.conferenceExists.value == true) {
+ ConferenceViewModel.shared.addCallsToConference()
+ } else {
+ CallsViewModel.shared.mergeCallsIntoLocalConference()
+ }
+ })
+ addSubview(mergeIntoLocalConference)
+ mergeIntoLocalConference.centerX(withDx: buttons_distance_from_center_x).alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).done()
+
+
+ CallsViewModel.shared.callsData.readCurrentAndObserve { _ in
+ self.callsListTableView.reloadData()
+ mergeIntoLocalConference.isEnabled = self.mergeToConferencePossible()
+ }
+ ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { _ in
+ mergeIntoLocalConference.isEnabled = self.mergeToConferencePossible()
+ }
+
+
+ // CallsList
+ super.contentView.addSubview(callsListTableView)
+ callsListTableView.matchParentDimmensions().done()
+ callsListTableView.dataSource = self
+ callsListTableView.register(VoipCallCell.self, forCellReuseIdentifier: "VoipCallCell")
+ callsListTableView.allowsSelection = false
+ if #available(iOS 15.0, *) {
+ callsListTableView.allowsFocus = false
+ }
+ callsListTableView.separatorStyle = .singleLine
+ callsListTableView.separatorColor = .white
+ callsListTableView.onClick {
+ self.hideMenu()
+ }
+
+ // Floating menu
+ super.contentView.addSubview(menuView)
+
+ menuView.isHidden = true
+
+ }
+
+
+ func numberOfCallsNotInConf() -> Int {
+ let core = Core.get()
+ var result = 0
+ core.calls.forEach { call in
+ if (call.conference == nil && core.findConferenceInformationFromUri(uri: call.remoteAddress!) == nil) {
+ result += 1
+ }
+ }
+ return result
+ }
+
+ func mergeToConferencePossible() -> Bool { // 2 calls or more not in conf or 1 call or more and 1 conf
+ let callsNotInConf = numberOfCallsNotInConf()
+ return (ConferenceViewModel.shared.conferenceExists.value == true && callsNotInConf >= 1) || (ConferenceViewModel.shared.conferenceExists.value != true && callsNotInConf >= 2 )
+ }
+
+
+ func toggleMenu(forCell:VoipCallCell) {
+ if (menuView.isHidden) {
+ showMenu(forCell: forCell)
+ } else if (menuView.callData?.call.callLog?.callId != forCell.callData?.call.callLog?.callId) {
+ hideMenu()
+ showMenu(forCell: forCell)
+ } else {
+ hideMenu()
+ }
+ }
+
+ func showMenu(forCell:VoipCallCell) {
+ menuView.removeConstraints().alignUnder(view: forCell).alignParentRight().done()
+ menuView.callData = forCell.callData
+ menuView.isHidden = false
+ }
+
+ func hideMenu() {
+ menuView.isHidden = true
+ }
+
+ // TableView datasource delegate
+
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ guard let callsData = CallsViewModel.shared.callsData.value else {
+ return 0
+ }
+ return callsData.count
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell:VoipCallCell = tableView.dequeueReusableCell(withIdentifier: "VoipCallCell") as! VoipCallCell
+ guard let callData = CallsViewModel.shared.callsData.value?[indexPath.row] else {
+ return cell
+ }
+ cell.selectionStyle = .none
+ cell.callData = callData
+ cell.owningCallsListView = self
+ return cell
+ }
+
+ // View controller
+
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/CallsList/VoipCallCell.swift b/Classes/Swift/Voip/Views/Fragments/CallsList/VoipCallCell.swift
new file mode 100644
index 000000000..b2fc5ec5d
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/CallsList/VoipCallCell.swift
@@ -0,0 +1,100 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class VoipCallCell: UITableViewCell {
+
+ // Layout Constants
+ let cell_height = 80.0
+ let call_status_icon_size = 65.0
+ static let avatar_size = 45.0
+ let avatar_left_margin = 40.0
+ let texts_left_margin = 20.0
+ let side_menu_icon_size = 80.0
+
+
+ var onMenuClickAction : (()->Void) = {}
+ let callStatusIcon = UIImageView()
+ let avatar = Avatar(diameter:VoipCallCell.avatar_size,color:LightDarkColor(VoipTheme.voip_contact_avatar_calls_list,VoipTheme.voip_contact_avatar_calls_list), textStyle: VoipTheme.call_generated_avatar_small)
+ let displayName = StyledLabel(VoipTheme.call_list_active_name_font)
+ let sipAddress = StyledLabel(VoipTheme.call_list_active_sip_uri_font)
+ var menuButton : CallControlButton? = nil
+ var owningCallsListView : CallsListView? = nil
+
+ var callData: CallData? = nil {
+ didSet {
+ if let data = callData {
+ contentView.backgroundColor = data.isPaused.value == true ? VoipTheme.voip_calls_list_inactive_background : VoipTheme.voip_dark_gray
+ callStatusIcon.image =
+ data.isIncoming.value == true ? UIImage(named:"voip_call_header_incoming") :
+ data.isOutgoing.value == true ? UIImage(named:"voip_call_header_outgoing") :
+ data.isPaused.value == true ? UIImage(named:"voip_call_header_paused") :
+ UIImage(named:"voip_call_header_active")
+ if (data.isInRemoteConference.value == true) {
+ displayName.text = data.remoteConferenceSubject.value
+ //sipAddress.text = data.call.conference?.participantList.map{ String($0.address?.addressBookEnhancedDisplayName())}.joined(separator: ",")
+ avatar.fillFromAddress(address: data.call.remoteAddress!,isGroup:true)
+ } else {
+ displayName.text = data.call.remoteAddress?.addressBookEnhancedDisplayName()
+ avatar.fillFromAddress(address: data.call.remoteAddress!)
+ sipAddress.text = data.call.remoteAddress?.asStringUriOnly()
+ }
+ displayName.applyStyle(data.isPaused.value == true ? VoipTheme.call_list_name_font : VoipTheme.call_list_active_name_font)
+ sipAddress.applyStyle(data.isPaused.value == true ? VoipTheme.call_list_sip_uri_font : VoipTheme.call_list_active_sip_uri_font)
+ menuButton?.applyTintedIcons(tintedIcons: data.isPaused.value == true ? VoipTheme.voip_call_list_menu.tintableStateIcons : VoipTheme.voip_call_list_active_menu.tintableStateIcons)
+ }
+ }
+ }
+
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+ contentView.height(cell_height).matchParentSideBorders().done()
+
+ contentView.addSubview(callStatusIcon)
+ callStatusIcon.size(w: call_status_icon_size, h: call_status_icon_size).done()
+
+ contentView.addSubview(avatar)
+ avatar.size(w: VoipCallCell.avatar_size, h: VoipCallCell.avatar_size).centerY().alignParentLeft(withMargin: avatar_left_margin).done()
+
+ let nameAddress = UIView()
+ nameAddress.addSubview(displayName)
+ nameAddress.addSubview(sipAddress)
+ displayName.alignParentTop().done()
+ sipAddress.alignUnder(view: displayName).done()
+ contentView.addSubview(nameAddress)
+ nameAddress.toRightOf(avatar,withLeftMargin:texts_left_margin).wrapContentY().centerY().done()
+
+ menuButton = CallControlButton(buttonTheme: VoipTheme.voip_call_list_active_menu, onClickAction: {
+ self.owningCallsListView?.toggleMenu(forCell: self)
+ })
+ contentView.addSubview(menuButton!)
+ menuButton!.size(w: side_menu_icon_size, h: side_menu_icon_size).alignParentRight().centerY().done()
+
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/CallsList/VoipCallContextMenu.swift b/Classes/Swift/Voip/Views/Fragments/CallsList/VoipCallContextMenu.swift
new file mode 100644
index 000000000..79cdaa063
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/CallsList/VoipCallContextMenu.swift
@@ -0,0 +1,160 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class VoipCallContextMenu: UIStackView {
+
+ //Layout constants
+ static let item_height = 50.0
+ let width = 250.0
+ let margin_bw_items = 1.0
+ static let texts_margin_left = 10.0
+
+
+ let resume : ButtonWithStateBackgrounds
+ let pause : ButtonWithStateBackgrounds
+ let transfer : ButtonWithStateBackgrounds
+ let answer : ButtonWithStateBackgrounds
+ let terminate : ButtonWithStateBackgrounds
+
+ var callData: CallData? = nil {
+ didSet {
+ callData?.callState.readCurrentAndObserve(onChange: { (state) in
+ self.resume.isHidden = false
+ self.pause.isHidden = false
+ self.transfer.isHidden = false
+ self.answer.isHidden = false
+ self.terminate.isHidden = false
+ var count = 5.0
+
+ if let callData = self.callData {
+ if (callData.isPaused.value == true ||
+ callData.isIncoming.value == true ||
+ callData.isOutgoing.value == true ||
+ callData.isInRemoteConference.value == true
+ ) {
+ self.pause.isHidden = true
+ count -= 1
+ }
+
+ if (callData.isIncoming.value == true ||
+ callData.isOutgoing.value == true ||
+ callData.isInRemoteConference.value == true
+ ) {
+ self.resume.isHidden = true
+ self.transfer.isHidden = true
+ count -= 2
+ } else if (callData.isPaused.value == false) {
+ self.resume.isHidden = true
+ count -= 1
+ }
+
+ if (callData.isIncoming.value == false) {
+ count -= 1
+ self.answer.isHidden = true
+ }
+ self.size(w:self.width,h:count*VoipCallContextMenu.item_height).done()
+ }
+
+ })
+ }
+ }
+
+
+ init () {
+
+ resume = VoipCallContextMenu.getButton(title: VoipTexts.call_context_action_resume)
+ pause = VoipCallContextMenu.getButton(title: VoipTexts.call_context_action_pause)
+ transfer = VoipCallContextMenu.getButton(title: VoipTexts.call_context_action_transfer)
+ answer = VoipCallContextMenu.getButton(title: VoipTexts.call_context_action_answer)
+ terminate = VoipCallContextMenu.getButton(title: VoipTexts.call_context_action_hangup)
+
+ super.init(frame: .zero)
+ backgroundColor = .white
+ axis = .vertical
+ spacing = margin_bw_items
+
+
+ addArrangedSubview(resume)
+ addArrangedSubview(pause)
+ addArrangedSubview(transfer)
+ addArrangedSubview(answer)
+ addArrangedSubview(terminate)
+
+ resume.onClick {
+ self.isHidden = true
+ guard let call = self.callData?.call else { return }
+ if (CallManager.callKitEnabled()) {
+ CallManager.instance().setHeld(call:call,hold:false);
+ } else {
+ try?call.resume()
+ }
+ }
+ pause.onClick {
+ self.isHidden = true
+ guard let call = self.callData?.call else { return }
+ if (CallManager.callKitEnabled()) {
+ CallManager.instance().setHeld(call:call,hold:true);
+ } else {
+ try?call.pause()
+ }
+ }
+ transfer.onClick {
+ let view: DialerView = self.VIEW(DialerView.compositeViewDescription());
+ view.setAddress("")
+ CallManager.instance().nextCallIsTransfer = true
+ PhoneMainView.instance().changeCurrentView(view.compositeViewDescription())
+ }
+ answer.onClick {
+ self.isHidden = true
+ guard let call = self.callData?.call else { return }
+ if (CallManager.callKitEnabled()) {
+ CallManager.instance().acceptCall(call: call, hasVideo: false)
+ } else {
+ try?call.accept()
+ }
+ }
+ terminate.onClick {
+ self.isHidden = true
+ guard let call = self.callData?.call else { return }
+ try?call.terminate()
+ }
+ }
+
+ required init(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ static func getButton(title:String) -> ButtonWithStateBackgrounds {
+ let button = ButtonWithStateBackgrounds(backgroundStateColors: VoipTheme.button_call_context_menu_background)
+ button.setTitle(title, for: .normal)
+ button.applyTitleStyle(VoipTheme.call_context_menu_item_font)
+ button.titleEdgeInsets = UIEdgeInsets(top: 0, left: texts_margin_left, bottom: 0, right: 0)
+ button.height(VoipCallContextMenu.item_height).done()
+ return button
+ }
+
+
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/MicMuted.swift b/Classes/Swift/Voip/Views/Fragments/Conference/MicMuted.swift
new file mode 100644
index 000000000..14227ea9a
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/Conference/MicMuted.swift
@@ -0,0 +1,33 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+
+class MicMuted : UIImageView {
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+ init (_ diameter: Int) {
+ super.init(image: UIImage(named: "voip_micro_off")?.tinted(with: .white))
+ layer.cornerRadius = CGFloat(diameter/2)
+ clipsToBounds = true
+ backgroundColor = VoipTheme.voip_dark_gray
+ square(diameter).done()
+ }
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift
new file mode 100644
index 000000000..d0ada751c
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift
@@ -0,0 +1,149 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class VoipActiveSpeakerParticipantCell: UICollectionViewCell {
+
+ // Layout Constants
+ let corner_radius = 20.0
+ static let avatar_size = 50.0
+ let switch_camera_button_margins = 8.0
+ let switch_camera_button_size = 30
+ static let mute_size = 25
+ let mute_margin = 5
+
+
+ let videoView = UIView()
+ let avatar = Avatar(diameter:VoipActiveSpeakerParticipantCell.avatar_size,color:VoipTheme.voipBackgroundColor, textStyle: VoipTheme.call_generated_avatar_medium)
+ let pause = UIImageView(image: UIImage(named: "voip_pause")?.tinted(with: .white))
+ let switchCamera = UIImageView(image: UIImage(named:"voip_change_camera")?.tinted(with:.white))
+ let displayName = StyledLabel(VoipTheme.conference_participant_name_font_as)
+ let pauseLabel = StyledLabel(VoipTheme.conference_participant_name_font_as,VoipTexts.conference_participant_paused)
+ let muted = MicMuted(VoipActiveSpeakerParticipantCell.mute_size)
+
+ var participantData: ConferenceParticipantDeviceData? = nil {
+ didSet {
+ if let data = participantData {
+ data.isInConference.clearObservers()
+ data.isInConference.readCurrentAndObserve { (isIn) in
+ self.updateBackground()
+ self.pause.isHidden = isIn == true
+ self.pauseLabel.isHidden = self.pause.isHidden
+ self.videoView.isHidden = data.videoEnabled.value != true
+ self.switchCamera.isHidden = data.videoEnabled.value != true || !data.isSwitchCameraAvailable()
+ }
+ data.videoEnabled.clearObservers()
+ data.videoEnabled.readCurrentAndObserve { (videoEnabled) in
+ self.updateBackground()
+ if (videoEnabled == true) {
+ self.videoView.isHidden = false
+ data.setVideoView(view: self.videoView)
+ self.avatar.isHidden = true
+ } else {
+ self.videoView.isHidden = true
+ self.avatar.isHidden = false
+ }
+ self.switchCamera.isHidden = videoEnabled != true || !data.isSwitchCameraAvailable()
+ }
+ data.participantDevice.address.map {
+ avatar.fillFromAddress(address: $0)
+ if let displayName = $0.addressBookEnhancedDisplayName() {
+ self.displayName.text = displayName
+ }
+ }
+ data.activeSpeaker.clearObservers()
+ data.activeSpeaker.readCurrentAndObserve { (active) in
+ if (active == true) {
+ self.layer.borderWidth = 2
+ } else {
+ self.layer.borderWidth = 0
+ }
+ }
+ data.micMuted.clearObservers()
+ data.micMuted.readCurrentAndObserve { (muted) in
+ self.muted.isHidden = muted != true
+ }
+ }
+ }
+ }
+
+ func updateBackground() {
+ if let data = participantData {
+ if (data.isInConference.value != true) {
+ self.contentView.backgroundColor = VoipTheme.voip_conference_participant_paused_background
+ } else if (data.videoEnabled.value == true) {
+ self.contentView.backgroundColor = .black
+ } else {
+ self.contentView.backgroundColor = VoipTheme.voipParticipantBackgroundColor.get()
+
+ }
+ }
+ }
+
+
+ override init(frame:CGRect) {
+ super.init(frame:.zero)
+ layer.cornerRadius = corner_radius
+ clipsToBounds = true
+ layer.borderColor = VoipTheme.primary_color.cgColor
+
+ contentView.addSubview(videoView)
+ videoView.matchParentDimmensions().done()
+
+ contentView.addSubview(avatar)
+ avatar.size(w: VoipActiveSpeakerParticipantCell.avatar_size, h: VoipActiveSpeakerParticipantCell.avatar_size).center().done()
+
+ contentView.addSubview(pause)
+ pause.layer.cornerRadius = VoipActiveSpeakerParticipantCell.avatar_size/2
+ pause.clipsToBounds = true
+ pause.backgroundColor = VoipTheme.voip_gray
+ pause.size(w: VoipActiveSpeakerParticipantCell.avatar_size, h: VoipActiveSpeakerParticipantCell.avatar_size).center().done()
+
+ contentView.addSubview(switchCamera)
+ switchCamera.alignParentTop(withMargin: switch_camera_button_margins).alignParentRight(withMargin: switch_camera_button_margins).square(switch_camera_button_size).done()
+ switchCamera.contentMode = .scaleAspectFit
+
+ switchCamera.onClick {
+ Core.get().toggleCamera()
+ }
+
+ contentView.addSubview(displayName)
+ displayName.matchParentSideBorders(insetedByDx:ActiveCallView.bottom_displayname_margin_left).alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done()
+
+ // Paused label commented out as in Android 10.06.2022
+ // contentView.addSubview(pauseLabel)
+ //pauseLabel.toRightOf(displayName).alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done()
+
+ contentView.addSubview(muted)
+ muted.alignParentLeft(withMargin: mute_margin).alignParentTop(withMargin:mute_margin).done()
+
+ contentView.matchParentDimmensions().done()
+ makeHeightMatchWidth().done()
+
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipAudioOnlyParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipAudioOnlyParticipantCell.swift
new file mode 100644
index 000000000..1334dd97f
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipAudioOnlyParticipantCell.swift
@@ -0,0 +1,106 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class VoipAudioOnlyParticipantCell: UICollectionViewCell {
+
+ // Layout Constants
+ static let cell_height = 80.0
+ static let avatar_size = 40.0
+ static let mute_size = 30
+ let corner_radius = 6.7
+ let common_margin = 10.0
+
+
+ let avatar = Avatar(diameter:VoipCallCell.avatar_size,color:VoipTheme.voipBackgroundColor, textStyle: VoipTheme.call_generated_avatar_small)
+ let paused = UIImageView(image: UIImage(named: "voip_pause")?.tinted(with: .white))
+ let muted = MicMuted(VoipAudioOnlyParticipantCell.mute_size)
+
+ let displayName = StyledLabel(VoipTheme.conference_participant_name_font_as)
+
+ var participantData: ConferenceParticipantDeviceData? = nil {
+ didSet {
+ if let data = participantData {
+ self.displayName.text = ""
+ data.isInConference.clearObservers()
+ data.isInConference.readCurrentAndObserve { (isIn) in
+ self.avatar.isHidden = isIn != true
+ self.paused.isHidden = isIn == true
+ data.participantDevice.address.map {
+ self.avatar.fillFromAddress(address: $0)
+ if let displayName = $0.addressBookEnhancedDisplayName() {
+ self.displayName.text = displayName + (isIn == true ? "" : " \(VoipTexts.conference_participant_paused)")
+ }
+ }
+ }
+ if (data.participantDevice.address == nil) {
+ avatar.isHidden = true
+ }
+ data.activeSpeaker.clearObservers()
+ data.activeSpeaker.readCurrentAndObserve { (active) in
+ if (active == true) {
+ self.layer.borderWidth = 2
+ } else {
+ self.layer.borderWidth = 0
+ }
+ }
+ data.micMuted.clearObservers()
+ data.micMuted.readCurrentAndObserve { (muted) in
+ self.muted.isHidden = muted != true
+ }
+ }
+ }
+ }
+
+
+ override init(frame:CGRect) {
+ super.init(frame:.zero)
+ contentView.height(VoipAudioOnlyParticipantCell.cell_height).matchParentSideBorders().done()
+
+ layer.cornerRadius = corner_radius
+ clipsToBounds = true
+ contentView.backgroundColor = VoipTheme.voipParticipantBackgroundColor.get()
+ layer.borderColor = VoipTheme.primary_color.cgColor
+
+ contentView.addSubview(avatar)
+ avatar.size(w: VoipCallCell.avatar_size, h: VoipCallCell.avatar_size).centerY().alignParentLeft(withMargin: common_margin).done()
+
+ contentView.addSubview(paused)
+ paused.layer.cornerRadius = VoipAudioOnlyParticipantCell.avatar_size/2
+ paused.clipsToBounds = true
+ paused.backgroundColor = VoipTheme.voip_gray
+ paused.size(w: VoipAudioOnlyParticipantCell.avatar_size, h: VoipAudioOnlyParticipantCell.avatar_size).alignParentLeft(withMargin: common_margin).centerY().done()
+
+ contentView.addSubview(displayName)
+ displayName.centerY().toRightOf(avatar,withLeftMargin: common_margin).done()
+ displayName.numberOfLines = 3
+
+ contentView.addSubview(muted)
+ muted.alignParentRight(withMargin: common_margin).toRightOf(displayName,withLeftMargin: common_margin).centerY().done()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift
new file mode 100644
index 000000000..ac9bdefb0
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift
@@ -0,0 +1,261 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class VoipConferenceActiveSpeakerView: UIView, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
+
+ // Layout constants :
+ let inter_cell = 10.0
+ let record_pause_button_margin = 10.0
+ let duration_margin_top = 4.0
+ let record_pause_button_size = 40
+ let record_pause_button_inset = UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7)
+ let grid_height = 100.0
+ let cell_width = 100.0
+
+
+ let subjectLabel = StyledLabel(VoipTheme.call_display_name_duration)
+ let duration = CallTimer(nil, VoipTheme.call_display_name_duration)
+
+ let remotelyRecording = RemotelyRecordingView(height: ActiveCallView.remote_recording_height,text: VoipTexts.call_remote_recording)
+ var recordCallButtons : [CallControlButton] = []
+ var pauseCallButtons : [CallControlButton] = []
+
+ let activeSpeakerView = UIView()
+ let activeSpeakerVideoView = UIView()
+ let activeSpeakerAvatar = Avatar(diameter: CGFloat(Avatar.diameter_for_call_views), color:VoipTheme.voipBackgroundColor, textStyle: VoipTheme.call_generated_avatar_large)
+ let activeSpeakerDisplayName = StyledLabel(VoipTheme.call_remote_name)
+
+ var grid : UICollectionView
+
+
+ var conferenceViewModel: ConferenceViewModel? = nil {
+ didSet {
+ if let model = conferenceViewModel {
+ model.subject.readCurrentAndObserve { (subject) in
+ self.subjectLabel.text = subject
+ }
+ duration.conference = model.conference.value
+ self.remotelyRecording.isRemotelyRecorded = model.isRemotelyRecorded
+ model.conferenceParticipantDevices.readCurrentAndObserve { (_) in
+ self.reloadData()
+ }
+ model.isConferenceLocallyPaused.readCurrentAndObserve { (paused) in
+ self.pauseCallButtons.forEach {
+ $0.isSelected = paused == true
+ }
+ }
+ model.isRecording.readCurrentAndObserve { (selected) in
+ self.recordCallButtons.forEach {
+ $0.isSelected = selected == true
+ }
+ }
+ Core.get().nativeVideoWindow = self.activeSpeakerVideoView
+ self.activeSpeakerAvatar.isHidden = true
+ self.activeSpeakerVideoView.isHidden = true
+ self.activeSpeakerDisplayName.text = VoipTexts.conference_display_no_active_speaker
+ conferenceViewModel?.speakingParticipant.readCurrentAndObserve { speakingParticipant in
+ speakingParticipant?.participantDevice.address.map {
+ self.activeSpeakerAvatar.isHidden = false
+ self.activeSpeakerAvatar.fillFromAddress(address: $0)
+ self.activeSpeakerDisplayName.text = $0.addressBookEnhancedDisplayName()
+ }
+ self.activeSpeakerVideoView.isHidden = speakingParticipant?.videoEnabled.value != true
+ }
+ }
+ self.reloadData()
+
+ }
+ }
+
+ func reloadData() {
+ conferenceViewModel?.conferenceParticipantDevices.value?.forEach {
+ $0.clearObservers()
+ }
+ self.grid.reloadData()
+ }
+
+ init() {
+
+ let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
+ layout.minimumInteritemSpacing = 0
+ layout.minimumLineSpacing = 0
+ layout.scrollDirection = .horizontal
+ layout.itemSize = CGSize(width:cell_width, height:grid_height)
+ grid = UICollectionView(frame:.zero, collectionViewLayout: layout)
+
+ super.init(frame: .zero)
+
+ let headerView = UIStackView()
+ addSubview(headerView)
+ headerView.matchParentSideBorders().alignParentTop().done()
+
+ headerView.distribution = .equalSpacing
+ headerView.alignment = .bottom
+ headerView.spacing = record_pause_button_margin
+ headerView.axis = .vertical
+
+ let subjectDuration = UIView()
+
+ subjectDuration.addSubview(subjectLabel)
+ subjectLabel.alignParentLeft().done()
+
+ subjectDuration.addSubview(duration)
+ duration.alignParentLeft().alignUnder(view: subjectLabel,withMargin:duration_margin_top).done()
+
+ let upperSection = UIStackView()
+ upperSection.distribution = .equalSpacing
+ upperSection.alignment = .center
+ upperSection.spacing = record_pause_button_margin
+ upperSection.axis = .horizontal
+
+ upperSection.addArrangedSubview(subjectDuration)
+ subjectDuration.wrapContentY().done()
+
+ // Record (with video)
+ let recordCall = CallControlButton(width: record_pause_button_size, height: record_pause_button_size, imageInset:record_pause_button_inset, buttonTheme: VoipTheme.call_record, onClickAction: {
+ self.conferenceViewModel?.toggleRecording()
+ })
+
+ let recordPauseView = UIStackView()
+ recordPauseView.spacing = record_pause_button_margin
+ recordCallButtons.append(recordCall)
+ recordPauseView.addArrangedSubview(recordCall)
+
+ // Pause (with video)
+ let pauseCall = CallControlButton(width: record_pause_button_size, height: record_pause_button_size, imageInset:record_pause_button_inset, buttonTheme: VoipTheme.call_pause, onClickAction: {
+ self.conferenceViewModel?.togglePlayPause()
+
+ })
+ pauseCallButtons.append(pauseCall)
+ recordPauseView.addArrangedSubview(pauseCall)
+
+ upperSection.addArrangedSubview(recordPauseView)
+
+ headerView.addArrangedSubview(upperSection)
+ upperSection.matchParentSideBorders().alignParentTop(withMargin:ActiveCallView.top_displayname_margin_top).done()
+
+ headerView.addArrangedSubview(remotelyRecording)
+ remotelyRecording.matchParentSideBorders().alignUnder(view:upperSection, withMargin:ActiveCallView.remote_recording_margin_top).height(CGFloat(ActiveCallView.remote_recording_height)).done()
+
+
+ // Container view that can toggle full screen by ckick
+ let fullScreenMutableView = UIView()
+ addSubview(fullScreenMutableView)
+ fullScreenMutableView.backgroundColor = VoipTheme.voipBackgroundColor.get()
+ fullScreenMutableView.matchParentSideBorders().alignUnder(view:headerView,withMargin: ActiveCallView.center_view_margin_top).alignParentBottom().done()
+
+
+ // Active speaker
+ fullScreenMutableView.addSubview(activeSpeakerView)
+ activeSpeakerView.layer.cornerRadius = ActiveCallView.center_view_corner_radius
+ activeSpeakerView.clipsToBounds = true
+ activeSpeakerView.backgroundColor = VoipTheme.voipParticipantBackgroundColor.get()
+ activeSpeakerView.matchParentSideBorders().alignParentTop().done()
+
+ activeSpeakerView.addSubview(activeSpeakerAvatar)
+ activeSpeakerAvatar.square(Avatar.diameter_for_call_views).center().done()
+
+ activeSpeakerView.addSubview(activeSpeakerVideoView)
+ activeSpeakerVideoView.matchParentDimmensions().done()
+
+ activeSpeakerView.addSubview(activeSpeakerDisplayName)
+ activeSpeakerDisplayName.alignParentLeft(withMargin:ActiveCallView.bottom_displayname_margin_left).alignParentRight().alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done()
+
+ // CollectionView
+ grid.dataSource = self
+ grid.delegate = self
+ grid.register(VoipActiveSpeakerParticipantCell.self, forCellWithReuseIdentifier: "VoipActiveSpeakerParticipantCell")
+ grid.backgroundColor = .clear
+ grid.isScrollEnabled = true
+ fullScreenMutableView.addSubview(grid)
+
+ grid.matchParentSideBorders().height(grid_height).alignParentBottom().alignUnder(view: activeSpeakerView, withMargin:ActiveCallView.center_view_margin_top).done()
+
+ // Full screen video togggle
+ activeSpeakerView.onClick {
+ ControlsViewModel.shared.toggleFullScreen()
+ }
+
+ ControlsViewModel.shared.fullScreenMode.observe { (fullScreen) in
+ if (self.isHidden) {
+ return
+ }
+ fullScreenMutableView.removeConstraints().done()
+ if (fullScreen == true) {
+ fullScreenMutableView.removeFromSuperview()
+ PhoneMainView.instance().mainViewController.view?.addSubview(fullScreenMutableView)
+ fullScreenMutableView.matchParentDimmensions().done()
+ } else {
+ fullScreenMutableView.removeFromSuperview()
+ self.addSubview(fullScreenMutableView)
+ fullScreenMutableView.matchParentSideBorders().alignUnder(view:headerView,withMargin: ActiveCallView.center_view_margin_top).alignParentBottom().done()
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ self.reloadData()
+ }
+ }
+
+ }
+
+
+ // UICollectionView related delegates
+
+
+ func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
+ return inter_cell
+ }
+
+ func collectionView(_ collectionView: UICollectionView, layout
+ collectionViewLayout: UICollectionViewLayout,
+ minimumLineSpacingForSectionAt section: Int) -> CGFloat {
+ return inter_cell
+ }
+
+ func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+ if (self.isHidden || conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .ActiveSpeaker) {
+ return 0
+ }
+ guard let participantsCount = conferenceViewModel?.conferenceParticipantDevices.value?.count else {
+ return .zero
+ }
+ return participantsCount
+ }
+
+ func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+ let cell:VoipActiveSpeakerParticipantCell = collectionView.dequeueReusableCell(withReuseIdentifier: "VoipActiveSpeakerParticipantCell", for: indexPath) as! VoipActiveSpeakerParticipantCell
+ guard let participantData = conferenceViewModel?.conferenceParticipantDevices.value?[indexPath.row] else {
+ return cell
+ }
+ cell.participantData = participantData
+ return cell
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceAudioOnlyView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceAudioOnlyView.swift
new file mode 100644
index 000000000..664fb7aa6
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceAudioOnlyView.swift
@@ -0,0 +1,216 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class VoipConferenceAudioOnlyView: UIView, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
+
+ // Layout constants :
+ let inter_cell = 5.0
+ let record_pause_button_margin = 10.0
+ let duration_margin_top = 4.0
+ let record_pause_button_size = 40
+ let record_pause_button_inset = UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7)
+
+
+ let subjectLabel = StyledLabel(VoipTheme.call_display_name_duration)
+ let duration = CallTimer(nil, VoipTheme.call_display_name_duration)
+
+ let remotelyRecording = RemotelyRecordingView(height: ActiveCallView.remote_recording_height,text: VoipTexts.call_remote_recording)
+ var recordCallButtons : [CallControlButton] = []
+ var pauseCallButtons : [CallControlButton] = []
+ var grid : UICollectionView
+ var gridContainer = UIView()
+
+
+ var conferenceViewModel: ConferenceViewModel? = nil {
+ didSet {
+ if let model = conferenceViewModel {
+ model.subject.clearObservers()
+ model.subject.readCurrentAndObserve { (subject) in
+ self.subjectLabel.text = subject
+ }
+ duration.conference = model.conference.value
+ self.remotelyRecording.isRemotelyRecorded = model.isRemotelyRecorded
+ model.conferenceParticipantDevices.clearObservers()
+ model.conferenceParticipantDevices.readCurrentAndObserve { (_) in
+ self.reloadData()
+ }
+ model.isConferenceLocallyPaused.clearObservers()
+ model.isConferenceLocallyPaused.readCurrentAndObserve { (paused) in
+ self.pauseCallButtons.forEach {
+ $0.isSelected = paused == true
+ }
+ }
+ model.isRecording.clearObservers()
+ model.isRecording.readCurrentAndObserve { (selected) in
+ self.recordCallButtons.forEach {
+ $0.isSelected = selected == true
+ }
+ }
+ }
+ self.reloadData()
+ }
+ }
+
+ init() {
+
+ let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
+ layout.minimumInteritemSpacing = 0
+ layout.minimumLineSpacing = 0
+ layout.estimatedItemSize = .zero
+ grid = UICollectionView(frame:.zero, collectionViewLayout: layout)
+
+ super.init(frame: .zero)
+
+ let headerView = UIStackView()
+ addSubview(headerView)
+
+ headerView.distribution = .equalSpacing
+ headerView.alignment = .bottom
+ headerView.spacing = record_pause_button_margin
+ headerView.axis = .vertical
+
+ let subjectDuration = UIView()
+
+ subjectDuration.addSubview(subjectLabel)
+ subjectLabel.alignParentLeft().done()
+
+ subjectDuration.addSubview(duration)
+ duration.alignParentLeft().alignUnder(view: subjectLabel,withMargin:duration_margin_top).done()
+
+ let upperSection = UIStackView()
+ upperSection.distribution = .equalSpacing
+ upperSection.alignment = .center
+ upperSection.spacing = record_pause_button_margin
+ upperSection.axis = .horizontal
+
+ upperSection.addArrangedSubview(subjectDuration)
+ subjectDuration.wrapContentY().done()
+
+ // Record (with video)
+ let recordCall = CallControlButton(width: record_pause_button_size, height: record_pause_button_size, imageInset:record_pause_button_inset, buttonTheme: VoipTheme.call_record, onClickAction: {
+ self.conferenceViewModel?.toggleRecording()
+ })
+
+ let recordPauseView = UIStackView()
+ recordPauseView.spacing = record_pause_button_margin
+ recordCallButtons.append(recordCall)
+ recordPauseView.addArrangedSubview(recordCall)
+
+ // Pause (with video)
+ let pauseCall = CallControlButton(width: record_pause_button_size, height: record_pause_button_size, imageInset:record_pause_button_inset, buttonTheme: VoipTheme.call_pause, onClickAction: {
+ self.conferenceViewModel?.togglePlayPause()
+
+ })
+ pauseCallButtons.append(pauseCall)
+ recordPauseView.addArrangedSubview(pauseCall)
+
+ upperSection.addArrangedSubview(recordPauseView)
+
+ headerView.addArrangedSubview(upperSection)
+ upperSection.matchParentSideBorders().alignParentTop(withMargin:ActiveCallView.top_displayname_margin_top).done()
+
+ headerView.addArrangedSubview(remotelyRecording)
+ remotelyRecording.matchParentSideBorders().alignUnder(view:upperSection, withMargin:ActiveCallView.remote_recording_margin_top).height(CGFloat(ActiveCallView.remote_recording_height)).done()
+
+ // CollectionView
+ grid.dataSource = self
+ grid.delegate = self
+ grid.register(VoipAudioOnlyParticipantCell.self, forCellWithReuseIdentifier: "VoipAudioOnlyParticipantCell")
+ grid.backgroundColor = .clear
+ grid.isScrollEnabled = false
+ addSubview(gridContainer)
+ gridContainer.addSubview(grid)
+ gridContainer.backgroundColor = VoipTheme.voipBackgroundColor.get()
+
+ gridContainer.matchParentSideBorders(insetedByDx: inter_cell).alignUnder(view:headerView,withMargin: ActiveCallView.center_view_margin_top).alignParentBottom(withMargin: inter_cell).done()
+ grid.matchParentDimmensions().done()
+
+ headerView.matchParentSideBorders().alignParentTop().done()
+
+ }
+
+
+ // UICollectionView related delegates
+
+ func reloadData() {
+ conferenceViewModel?.conferenceParticipantDevices.value?.forEach {
+ $0.clearObservers()
+ }
+ if (self.isHidden) {
+ self.grid.reloadData()
+ return
+ }
+ self.grid.reloadData()
+ }
+
+
+ func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
+ return inter_cell
+ }
+
+ func collectionView(_ collectionView: UICollectionView, layout
+ collectionViewLayout: UICollectionViewLayout,
+ minimumLineSpacingForSectionAt section: Int) -> CGFloat {
+ return inter_cell
+ }
+
+ func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+ if (self.isHidden) {
+ return 0
+ }
+ guard let participantsCount = conferenceViewModel?.conferenceParticipantDevices.value?.count else {
+ return .zero
+ }
+ return participantsCount
+ }
+
+ func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+ let cell:VoipAudioOnlyParticipantCell = collectionView.dequeueReusableCell(withReuseIdentifier: "VoipAudioOnlyParticipantCell", for: indexPath) as! VoipAudioOnlyParticipantCell
+ guard let participantData = conferenceViewModel?.conferenceParticipantDevices.value?[indexPath.row] else {
+ return cell
+ }
+ cell.participantData = participantData
+ return cell
+ }
+
+ func collectionView(_ collectionView: UICollectionView,
+ layout collectionViewLayout: UICollectionViewLayout,
+ sizeForItemAt indexPath: IndexPath) -> CGSize {
+
+ guard let participantsCount:Int = conferenceViewModel?.conferenceParticipantDevices.value?.count else {
+ return .zero
+ }
+
+ return participantsCount == 1 ? CGSize(width:collectionView.frame.size.width,height:VoipAudioOnlyParticipantCell.cell_height) : CGSize(width:collectionView.frame.size.width / 2.0 - inter_cell / 2.0,height:VoipAudioOnlyParticipantCell.cell_height)
+ }
+
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift
new file mode 100644
index 000000000..fac51445a
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift
@@ -0,0 +1,155 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import linphonesw
+
+@objc class VoipConferenceDisplayModeSelectionView: DismissableView, UITableViewDataSource, UITableViewDelegate{
+
+ // Layout constants
+ let buttons_distance_from_center_x = 38
+ let buttons_size = 60
+
+ let optionsListView = UITableView()
+
+ init() {
+ super.init(title: VoipTexts.call_action_change_conf_layout)
+
+ super.contentView.addSubview(optionsListView)
+ optionsListView.alignParentTop().height(3*ConferenceDisplayModeSelectionCell.cell_height).matchParentSideBorders().done()
+ optionsListView.dataSource = self
+ optionsListView.delegate = self
+ optionsListView.register(ConferenceDisplayModeSelectionCell.self, forCellReuseIdentifier: "ConferenceDisplayModeSelectionCell")
+ optionsListView.separatorStyle = .singleLine
+ optionsListView.separatorColor = VoipTheme.light_grey_color
+ optionsListView.isScrollEnabled = false
+ super.contentView.backgroundColor = .white
+ }
+
+ // TableView datasource delegate
+
+ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+ return ConferenceDisplayModeSelectionCell.cell_height
+ }
+
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return 3
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell:ConferenceDisplayModeSelectionCell = tableView.dequeueReusableCell(withIdentifier: "ConferenceDisplayModeSelectionCell") as! ConferenceDisplayModeSelectionCell
+ cell.selectionStyle = .none
+ if (indexPath.row == 0) {
+ cell.setOption(title: VoipTexts.conference_display_mode_mosaic, onSelectAction: {
+ ConferenceViewModel.shared.changeLayout(layout: .Grid)
+ ConferenceViewModel.shared.conferenceDisplayMode.value = .Grid
+ }, image:(UIImage(named: "voip_conference_mosaic")?.tinted(with: VoipTheme.voipDrawableColor.get())!)!)
+ cell.isUserInteractionEnabled = ConferenceViewModel.shared.conferenceParticipantDevices.value!.count <= ConferenceViewModel.shared.maxParticipantsForMosaicLayout
+ }
+ if (indexPath.row == 1) {
+ cell.setOption(title: VoipTexts.conference_display_mode_active_speaker, onSelectAction: {
+ ConferenceViewModel.shared.changeLayout(layout: .ActiveSpeaker)
+ ConferenceViewModel.shared.conferenceDisplayMode.value = .ActiveSpeaker
+ }, image:(UIImage(named: "voip_conference_active_speaker")?.tinted(with: VoipTheme.voipDrawableColor.get())!)!)
+ cell.isUserInteractionEnabled = true
+ }
+
+ if (indexPath.row == 2) {
+ cell.setOption(title: VoipTexts.conference_display_mode_audio_only, onSelectAction: {
+ ConferenceViewModel.shared.changeLayout(layout: .AudioOnly)
+ ConferenceViewModel.shared.conferenceDisplayMode.value = .AudioOnly
+ }, image:(UIImage(named: "voip_conference_audio_only")?.tinted(with: VoipTheme.voipDrawableColor.get())!)!)
+ cell.isUserInteractionEnabled = true
+ }
+
+ cell.separatorInset = .zero
+ cell.selectionStyle = .none
+ return cell
+ }
+
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ let cell = tableView.cellForRow(at: indexPath) as! ConferenceDisplayModeSelectionCell
+ cell.onSelectAction?()
+ cell.isSelected = true
+ if (indexPath.row == 0) {
+ (tableView.cellForRow(at: IndexPath(row: 1, section: 0)) as! ConferenceDisplayModeSelectionCell).isSelected = false
+ (tableView.cellForRow(at: IndexPath(row: 2, section: 0)) as! ConferenceDisplayModeSelectionCell).isSelected = false
+ }
+ if (indexPath.row == 1) {
+ (tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as! ConferenceDisplayModeSelectionCell).isSelected = false
+ (tableView.cellForRow(at: IndexPath(row: 2, section: 0)) as! ConferenceDisplayModeSelectionCell).isSelected = false
+ }
+ if (indexPath.row == 2) {
+ (tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as! ConferenceDisplayModeSelectionCell).isSelected = false
+ (tableView.cellForRow(at: IndexPath(row: 1, section: 0)) as! ConferenceDisplayModeSelectionCell).isSelected = false
+ }
+ }
+
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+}
+
+class ConferenceDisplayModeSelectionCell : UITableViewCell {
+
+ static let cell_height = 60.0
+ let icon_size = 40.0
+ let side_margins = 20.0
+
+ let radio = CallControlButton(buttonTheme: VoipTheme.radio_button)
+ let label = StyledLabel(VoipTheme.conference_mode_title)
+ let icon = UIImageView()
+
+ var onSelectAction : (()->Void)? = nil
+
+ override var isSelected: Bool {
+ didSet {
+ radio.isSelected = isSelected
+ label.applyStyle(isSelected ? VoipTheme.conference_mode_title_selected : VoipTheme.conference_mode_title)
+ }
+ }
+
+
+ func setOption(title:String, onSelectAction:@escaping ()->Void, image:UIImage) {
+ self.onSelectAction = onSelectAction
+ label.text = title
+ icon.image = image
+ }
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+ contentView.matchParentDimmensions().done()
+ contentView.addSubview(radio)
+ radio.alignParentLeft(withMargin: side_margins).centerY().done()
+ contentView.addSubview(label)
+ label.toRightOf(radio).centerY().done()
+ contentView.addSubview(icon)
+ icon.size(w: icon_size, h: icon_size).alignParentRight(withMargin: side_margins).centerY().done()
+ radio.isUserInteractionEnabled = false
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift
new file mode 100644
index 000000000..dcccdf8c7
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift
@@ -0,0 +1,275 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
+
+ // Layout constants :
+ let inter_cell = 10.0
+ let record_pause_button_margin = 10.0
+ let duration_margin_top = 4.0
+ let record_pause_button_size = 40
+ let record_pause_button_inset = UIEdgeInsets(top: 7, left: 7, bottom: 7, right: 7)
+
+
+ let subjectLabel = StyledLabel(VoipTheme.call_display_name_duration)
+ let duration = CallTimer(nil, VoipTheme.call_display_name_duration)
+
+ let remotelyRecording = RemotelyRecordingView(height: ActiveCallView.remote_recording_height,text: VoipTexts.call_remote_recording)
+ var recordCallButtons : [CallControlButton] = []
+ var pauseCallButtons : [CallControlButton] = []
+ var grid : UICollectionView
+ var gridContainer = UIView()
+
+
+ var conferenceViewModel: ConferenceViewModel? = nil {
+ didSet {
+ if let model = conferenceViewModel {
+ model.subject.clearObservers()
+ model.subject.readCurrentAndObserve { (subject) in
+ self.subjectLabel.text = subject
+ }
+ duration.conference = model.conference.value
+ self.remotelyRecording.isRemotelyRecorded = model.isRemotelyRecorded
+ model.conferenceParticipantDevices.clearObservers()
+ model.conferenceParticipantDevices.readCurrentAndObserve { (_) in
+ self.reloadData()
+ }
+ model.isConferenceLocallyPaused.clearObservers()
+ model.isConferenceLocallyPaused.readCurrentAndObserve { (paused) in
+ self.pauseCallButtons.forEach {
+ $0.isSelected = paused == true
+ }
+ }
+ model.isRecording.clearObservers()
+ model.isRecording.readCurrentAndObserve { (selected) in
+ self.recordCallButtons.forEach {
+ $0.isSelected = selected == true
+ }
+ }
+ }
+ self.reloadData()
+ }
+ }
+
+ init() {
+
+ let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
+ layout.minimumInteritemSpacing = 0
+ layout.minimumLineSpacing = 0
+ layout.estimatedItemSize = .zero
+ grid = UICollectionView(frame:.zero, collectionViewLayout: layout)
+
+ super.init(frame: .zero)
+
+ let headerView = UIStackView()
+ addSubview(headerView)
+
+ headerView.distribution = .equalSpacing
+ headerView.alignment = .bottom
+ headerView.spacing = record_pause_button_margin
+ headerView.axis = .vertical
+
+ let subjectDuration = UIView()
+
+ subjectDuration.addSubview(subjectLabel)
+ subjectLabel.alignParentLeft().done()
+
+ subjectDuration.addSubview(duration)
+ duration.alignParentLeft().alignUnder(view: subjectLabel,withMargin:duration_margin_top).done()
+
+ let upperSection = UIStackView()
+ upperSection.distribution = .equalSpacing
+ upperSection.alignment = .center
+ upperSection.spacing = record_pause_button_margin
+ upperSection.axis = .horizontal
+
+ upperSection.addArrangedSubview(subjectDuration)
+ subjectDuration.wrapContentY().done()
+
+ // Record (with video)
+ let recordCall = CallControlButton(width: record_pause_button_size, height: record_pause_button_size, imageInset:record_pause_button_inset, buttonTheme: VoipTheme.call_record, onClickAction: {
+ self.conferenceViewModel?.toggleRecording()
+ })
+
+ let recordPauseView = UIStackView()
+ recordPauseView.spacing = record_pause_button_margin
+ recordCallButtons.append(recordCall)
+ recordPauseView.addArrangedSubview(recordCall)
+
+ // Pause (with video)
+ let pauseCall = CallControlButton(width: record_pause_button_size, height: record_pause_button_size, imageInset:record_pause_button_inset, buttonTheme: VoipTheme.call_pause, onClickAction: {
+ self.conferenceViewModel?.togglePlayPause()
+
+ })
+ pauseCallButtons.append(pauseCall)
+ recordPauseView.addArrangedSubview(pauseCall)
+
+ upperSection.addArrangedSubview(recordPauseView)
+
+ headerView.addArrangedSubview(upperSection)
+ upperSection.matchParentSideBorders().alignParentTop(withMargin:ActiveCallView.top_displayname_margin_top).done()
+
+ headerView.addArrangedSubview(remotelyRecording)
+ remotelyRecording.matchParentSideBorders().alignUnder(view:upperSection, withMargin:ActiveCallView.remote_recording_margin_top).height(CGFloat(ActiveCallView.remote_recording_height)).done()
+
+ // CollectionView
+ grid.dataSource = self
+ grid.delegate = self
+ grid.register(VoipGridParticipantCell.self, forCellWithReuseIdentifier: "VoipGridParticipantCell")
+ grid.backgroundColor = .clear
+ grid.isScrollEnabled = false
+ addSubview(gridContainer)
+ gridContainer.addSubview(grid)
+ gridContainer.backgroundColor = VoipTheme.voipBackgroundColor.get()
+
+ gridContainer.matchParentSideBorders(insetedByDx: inter_cell).alignUnder(view:headerView,withMargin: ActiveCallView.center_view_margin_top).alignParentBottom(withMargin: inter_cell).done()
+ grid.matchParentDimmensions().done()
+
+ headerView.matchParentSideBorders().alignParentTop().done()
+
+
+ // Full screen video togggle
+ gridContainer.onClick {
+ ControlsViewModel.shared.toggleFullScreen()
+ }
+
+ ControlsViewModel.shared.fullScreenMode.observe { (fullScreen) in
+ if (self.isHidden || self.conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .Grid) {
+ return
+ }
+ self.gridContainer.removeConstraints().done()
+ if (fullScreen == true) {
+ self.gridContainer.removeFromSuperview()
+ PhoneMainView.instance().mainViewController.view?.addSubview(self.gridContainer)
+ self.gridContainer.matchParentDimmensions().center().done()
+ } else {
+ self.gridContainer.removeFromSuperview()
+ self.addSubview(self.gridContainer)
+ self.gridContainer.matchParentSideBorders().alignUnder(view:headerView,withMargin: ActiveCallView.center_view_margin_top).alignParentBottom().done()
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ self.reloadData()
+ }
+ }
+ }
+
+
+ // UICollectionView related delegates
+
+ func reloadData() {
+ conferenceViewModel?.conferenceParticipantDevices.value?.forEach {
+ $0.clearObservers()
+ }
+ if (self.isHidden) {
+ self.grid.reloadData()
+ return
+ }
+ computeCellSize()
+ self.grid.reloadData()
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
+ let width:CGFloat = CGFloat(self.columnCount) * self.cellSize.width + (CGFloat(self.columnCount)-1.0)*self.inter_cell
+ let height:CGFloat = CGFloat(self.rowCount) * self.cellSize.height + (CGFloat(self.rowCount)-1.0)*self.inter_cell
+ if (width > 0) {
+ self.grid.removeConstraints().width(width).height(height).center().done()
+ }
+ }
+ }
+
+
+ func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
+ return inter_cell
+ }
+
+ func collectionView(_ collectionView: UICollectionView, layout
+ collectionViewLayout: UICollectionViewLayout,
+ minimumLineSpacingForSectionAt section: Int) -> CGFloat {
+ return inter_cell
+ }
+
+ func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
+ if (self.isHidden) {
+ return 0
+ }
+ guard let participantsCount = conferenceViewModel?.conferenceParticipantDevices.value?.count else {
+ return .zero
+ }
+ return participantsCount
+ }
+
+ func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
+ let cell:VoipGridParticipantCell = collectionView.dequeueReusableCell(withReuseIdentifier: "VoipGridParticipantCell", for: indexPath) as! VoipGridParticipantCell
+ guard let participantData = conferenceViewModel?.conferenceParticipantDevices.value?[indexPath.row] else {
+ return cell
+ }
+ cell.participantData = participantData
+ return cell
+ }
+
+ let placement = [[1, 2, 3, 4, 5, 6], [1, 1, 2, 2, 3,3], [1, 1, 1, 2, 2, 2], [1, 1, 1, 1, 2, 2], [1, 1, 1, 1, 1, 2], [1, 1, 1, 1, 1, 1]]
+ var cellSize: CGSize = .zero
+ var columnCount: Int = 0
+ var rowCount: Int = 0
+
+ func computeCellSize() {
+ let participantsCount = self.collectionView(self.grid, numberOfItemsInSection: 0)
+ if (participantsCount == 0) {
+ return
+ }
+ let availableSize = gridContainer.frame.size
+ var maxSize = 0.0
+ for rowCount in 1...participantsCount {
+ let neededColumns = placement[rowCount-1][participantsCount-1]
+ let candidateWidth = availableSize.width / CGFloat(neededColumns) - CGFloat((neededColumns-1) * Int(inter_cell))
+ let candidateHeight = availableSize.height / CGFloat(rowCount) - CGFloat((rowCount - 1) * Int(inter_cell))
+ let candidateSize = min(candidateWidth,candidateHeight)
+ if (candidateSize > maxSize) {
+ self.columnCount = neededColumns
+ self.rowCount = rowCount
+ maxSize = candidateSize
+ }
+ Log.i("neededColumns \(neededColumns) rowCount \(rowCount) availableSize \(availableSize) participantsCount \(participantsCount) candidateWidth \(candidateWidth) candidateHeight \(candidateHeight) candidateSize \(candidateSize) maxSize \(maxSize)")
+ }
+
+ cellSize = CGSize(width: maxSize ,height: maxSize)
+ }
+
+ func collectionView(_ collectionView: UICollectionView,
+ layout collectionViewLayout: UICollectionViewLayout,
+ sizeForItemAt indexPath: IndexPath) -> CGSize {
+
+ guard let _ = conferenceViewModel?.conferenceParticipantDevices.value?.count else {
+ return .zero
+ }
+
+ return cellSize
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift
new file mode 100644
index 000000000..837163684
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift
@@ -0,0 +1,151 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class VoipGridParticipantCell: UICollectionViewCell {
+
+ // Layout Constants
+ let corner_radius = 20.0
+ static let avatar_size = 80.0
+ let switch_camera_button_margins = 8.0
+ let switch_camera_button_size = 30
+ let pause_label_left_margin = 5
+ static let mute_size = 25
+ let mute_margin = 5
+
+
+ let videoView = UIView()
+ let avatar = Avatar(diameter:VoipGridParticipantCell.avatar_size,color:VoipTheme.voipBackgroundColor, textStyle: VoipTheme.call_generated_avatar_medium)
+ let pause = UIImageView(image: UIImage(named: "voip_pause")?.tinted(with: .white))
+ let switchCamera = UIImageView(image: UIImage(named:"voip_change_camera")?.tinted(with:.white))
+ let displayName = StyledLabel(VoipTheme.conference_participant_name_font_grid)
+ let pauseLabel = StyledLabel(VoipTheme.conference_participant_name_font_grid,VoipTexts.conference_participant_paused)
+ let muted = MicMuted(VoipActiveSpeakerParticipantCell.mute_size)
+
+ var participantData: ConferenceParticipantDeviceData? = nil {
+ didSet {
+ if let data = participantData {
+ data.isInConference.clearObservers()
+ data.isInConference.readCurrentAndObserve { (isIn) in
+ self.updateBackground()
+ self.pause.isHidden = isIn == true
+ self.pauseLabel.isHidden = self.pause.isHidden
+ self.videoView.isHidden = data.videoEnabled.value != true
+ self.switchCamera.isHidden = data.videoEnabled.value != true || !data.isSwitchCameraAvailable()
+ }
+ data.videoEnabled.clearObservers()
+ data.videoEnabled.readCurrentAndObserve { (videoEnabled) in
+ self.updateBackground()
+ if (videoEnabled == true) {
+ self.videoView.isHidden = false
+ data.setVideoView(view: self.videoView)
+ self.avatar.isHidden = true
+ } else {
+ self.videoView.isHidden = true
+ self.avatar.isHidden = false
+ }
+ self.switchCamera.isHidden = videoEnabled != true || !data.isSwitchCameraAvailable()
+ }
+ if (data.participantDevice.address == nil) {
+ avatar.isHidden = true
+ }
+ self.displayName.text = ""
+ data.participantDevice.address.map {
+ avatar.fillFromAddress(address: $0)
+ if let displayName = $0.addressBookEnhancedDisplayName() {
+ self.displayName.text = displayName
+ }
+ }
+ data.activeSpeaker.clearObservers()
+ data.activeSpeaker.readCurrentAndObserve { (active) in
+ if (active == true) {
+ self.layer.borderWidth = 2
+ } else {
+ self.layer.borderWidth = 0
+ }
+ }
+ data.micMuted.clearObservers()
+ data.micMuted.readCurrentAndObserve { (muted) in
+ self.muted.isHidden = muted != true
+ }
+ }
+ }
+ }
+
+ func updateBackground() {
+ if let data = participantData {
+ if (data.isInConference.value != true) {
+ self.contentView.backgroundColor = VoipTheme.voip_conference_participant_paused_background
+ } else if (data.videoEnabled.value == true) {
+ self.contentView.backgroundColor = .black
+ } else {
+ self.contentView.backgroundColor = VoipTheme.voipParticipantBackgroundColor.get()
+
+ }
+ }
+ }
+
+
+ override init(frame:CGRect) {
+ super.init(frame:.zero)
+ layer.cornerRadius = corner_radius
+ clipsToBounds = true
+ layer.borderColor = VoipTheme.primary_color.cgColor
+
+ contentView.addSubview(videoView)
+ videoView.matchParentDimmensions().done()
+
+ contentView.addSubview(avatar)
+ avatar.size(w: VoipGridParticipantCell.avatar_size, h: VoipGridParticipantCell.avatar_size).center().done()
+
+ contentView.addSubview(pause)
+ pause.layer.cornerRadius = VoipGridParticipantCell.avatar_size/2
+ pause.clipsToBounds = true
+ pause.backgroundColor = VoipTheme.voip_gray
+ pause.size(w: VoipGridParticipantCell.avatar_size, h: VoipGridParticipantCell.avatar_size).center().done()
+
+ contentView.addSubview(switchCamera)
+ switchCamera.alignParentTop(withMargin: switch_camera_button_margins).alignParentRight(withMargin: switch_camera_button_margins).square(switch_camera_button_size).done()
+ switchCamera.contentMode = .scaleAspectFit
+
+ switchCamera.onClick {
+ Core.get().toggleCamera()
+ }
+
+ contentView.addSubview(displayName)
+ displayName.alignParentLeft(withMargin:ActiveCallView.bottom_displayname_margin_left).alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done()
+
+ contentView.addSubview(pauseLabel)
+ pauseLabel.toRightOf(displayName,withLeftMargin: pause_label_left_margin).alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done()
+
+ contentView.addSubview(muted)
+ muted.alignParentLeft(withMargin: mute_margin).alignParentTop(withMargin:mute_margin).done()
+
+ contentView.matchParentDimmensions().done()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift
new file mode 100644
index 000000000..dfa77a918
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift
@@ -0,0 +1,86 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import UIKit
+
+class ConferenceLayoutPickerView: UIStackView {
+
+ // Layout constants
+ let corner_radius = 6.7
+ let margin = 10.0
+
+ init () {
+ super.init(frame: .zero)
+ axis = .vertical
+ distribution = .equalCentering
+ alignment = .center
+ spacing = ControlsView.controls_button_spacing
+ backgroundColor = VoipTheme.voip_gray
+ layer.cornerRadius = corner_radius
+ clipsToBounds = true
+
+ let padding = UIView()
+ padding.height(margin/2).done()
+ addArrangedSubview(padding)
+
+
+ let grid = CallControlButton(imageInset : UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5),buttonTheme: VoipTheme.conf_waiting_room_layout_picker, onClickAction: {
+ ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value = .Grid
+ ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.value = false
+
+ })
+ grid.applyTintedIcons(tintedIcons: [UIButton.State.normal.rawValue : TintableIcon(name: "voip_conference_mosaic" ,tintColor: LightDarkColor(.white,.white))])
+ addArrangedSubview(grid)
+
+ let activeSpeaker = CallControlButton(imageInset : UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5),buttonTheme: VoipTheme.conf_waiting_room_layout_picker, onClickAction: {
+ ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value = .ActiveSpeaker
+ ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.value = false
+ })
+ activeSpeaker.applyTintedIcons(tintedIcons: [UIButton.State.normal.rawValue : TintableIcon(name: "voip_conference_active_speaker" ,tintColor: LightDarkColor(.white,.white))])
+ addArrangedSubview(activeSpeaker)
+
+ let audioOnly = CallControlButton(imageInset : UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5),buttonTheme: VoipTheme.conf_waiting_room_layout_picker, onClickAction: {
+ ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value = .AudioOnly
+ ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.value = false
+ })
+ audioOnly.applyTintedIcons(tintedIcons: [UIButton.State.normal.rawValue : TintableIcon(name: "voip_conference_audio_only" ,tintColor: LightDarkColor(.white,.white))])
+ addArrangedSubview(audioOnly)
+
+ ConferenceWaitingRoomViewModel.sharedModel.joinLayout.readCurrentAndObserve { layout in
+ grid.isSelected = layout == .Grid
+ activeSpeaker.isSelected = layout == .ActiveSpeaker
+ audioOnly.isSelected = layout == .AudioOnly
+ }
+
+ let padding2 = UIView()
+ padding2.height(margin/2).done()
+ addArrangedSubview(padding2)
+
+ size(w:CGFloat(CallControlButton.default_size)+margin, h : 3*CGFloat(CallControlButton.default_size)+3*CGFloat(ControlsView.controls_button_spacing)+2*margin).done()
+
+ }
+
+ required init(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+}
+
+
diff --git a/Classes/Swift/Voip/Views/Fragments/ControlsView.swift b/Classes/Swift/Voip/Views/Fragments/ControlsView.swift
new file mode 100644
index 000000000..b091a049c
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/ControlsView.swift
@@ -0,0 +1,109 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import UIKit
+
+class ControlsView: UIStackView {
+
+ // Layout constants
+ static let controls_button_spacing = 5.0
+
+ init (showVideo:Bool, controlsViewModel:ControlsViewModel) {
+ super.init(frame: .zero)
+ axis = .horizontal
+ distribution = .equalSpacing
+ alignment = .center
+ spacing = ControlsView.controls_button_spacing
+
+ // Mute
+ let mute = CallControlButton(buttonTheme: VoipTheme.call_mute, onClickAction: {
+ controlsViewModel.toggleMuteMicrophone()
+ })
+ addArrangedSubview(mute)
+ controlsViewModel.isMicrophoneMuted.readCurrentAndObserve { (muted) in
+ mute.isSelected = muted == true
+ }
+ controlsViewModel.isMuteMicrophoneEnabled.readCurrentAndObserve { (enabled) in
+ mute.isEnabled = enabled == true
+ }
+
+ // Speaker
+ let speaker = CallControlButton(buttonTheme: VoipTheme.call_speaker, onClickAction: {
+ controlsViewModel.toggleSpeaker()
+ })
+ addArrangedSubview(speaker)
+ controlsViewModel.isSpeakerSelected.readCurrentAndObserve { (selected) in
+ speaker.isSelected = selected == true
+ }
+
+ // Audio routes
+ let routes = CallControlButton(buttonTheme: VoipTheme.call_audio_route, onClickAction: {
+ controlsViewModel.toggleRoutesMenu()
+ })
+ addArrangedSubview(routes)
+ controlsViewModel.audioRoutesSelected.readCurrentAndObserve { (selected) in
+ routes.isSelected = selected == true
+ }
+
+ controlsViewModel.audioRoutesEnabled.readCurrentAndObserve { (routesEnabled) in
+ speaker.isHidden = routesEnabled == true
+ routes.isHidden = !speaker.isHidden
+ }
+
+ // Video
+ if (showVideo) {
+ let video = CallControlButton(buttonTheme: VoipTheme.call_video, onClickAction: {
+ if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
+ controlsViewModel.toggleVideo()
+ } else {
+ AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
+ if granted {
+ controlsViewModel.toggleVideo()
+ } else {
+ VoipDialog(message:VoipTexts.camera_required_for_video).show()
+ }
+ })
+ }
+ })
+ addArrangedSubview(video)
+ video.showActivityIndicatorDataSource = controlsViewModel.isVideoUpdateInProgress
+ controlsViewModel.isVideoEnabled.readCurrentAndObserve { (selected) in
+ video.isSelected = selected == true
+ }
+ controlsViewModel.isVideoAvailable.readCurrentAndObserve { (available) in
+ video.isEnabled = available == true && controlsViewModel.isVideoUpdateInProgress.value != true
+ }
+ controlsViewModel.isVideoUpdateInProgress.readCurrentAndObserve { (updateInProgress) in
+ video.isEnabled = updateInProgress != true && controlsViewModel.isVideoAvailable.value == true
+ }
+
+ }
+
+ height(CallControlButton.default_size).done()
+
+ }
+
+ required init(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+}
+
+
diff --git a/Classes/Swift/Voip/Views/Fragments/DismissableView.swift b/Classes/Swift/Voip/Views/Fragments/DismissableView.swift
new file mode 100644
index 000000000..9bb1f7d9b
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/DismissableView.swift
@@ -0,0 +1,61 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+
+class DismissableView: UIView {
+
+ // Layout constants
+ let header_height = 60.0
+ let title_left_margin = 20
+ let dismiss_right_margin = 10
+ let dismiss_icon_inset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
+ let headerView = UIView()
+ let contentView = UIView()
+ var dismiss : CallControlButton? = nil
+
+ init(title:String) {
+ super.init(frame:.zero)
+
+ headerView.backgroundColor = VoipTheme.voipToolbarBackgroundColor.get()
+ self.addSubview(headerView)
+ headerView.matchParentSideBorders().alignParentTop().height(header_height).done()
+
+ dismiss = CallControlButton(imageInset:dismiss_icon_inset,buttonTheme: VoipTheme.voip_cancel, onClickAction: {
+ self.removeFromSuperview()
+ })
+ headerView.addSubview(dismiss!)
+ dismiss?.alignParentRight(withMargin: dismiss_right_margin).centerY().done()
+
+ let title = StyledLabel(VoipTheme.calls_list_header_font,title)
+ headerView.addSubview(title)
+ title.alignParentTop().alignParentLeft(withMargin: title_left_margin).centerY().done()
+
+ self.addSubview(contentView)
+ contentView.alignUnder(view: headerView).matchParentSideBorders().alignParentBottom().done()
+
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/IncomingOuntgoingCommonView.swift b/Classes/Swift/Voip/Views/Fragments/IncomingOuntgoingCommonView.swift
new file mode 100644
index 000000000..38ff5aeb0
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/IncomingOuntgoingCommonView.swift
@@ -0,0 +1,98 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import linphonesw
+
+@objc class IncomingOutgoingCommonView: UIViewController { // Shared between IncomingCallView and OutgoingCallVIew (all upper part except control buttons)
+
+ // Layout constants
+ static let spinner_size = 50
+ static let spinner_margin_top = 8.0
+ static let call_type_margin_top = 10.0
+ static let duration_margin_top = 10.0
+ static let display_name_height = 20.0
+ static let display_name_margin_top = 18.0
+ static let sip_address_height = 16.0
+ static let sip_address_margin_top = 6.0
+ static let answer_decline_inset = UIEdgeInsets(top: 2, left: 7, bottom: 2, right: 7)
+
+ let spinner = RotatingSpinner()
+ let duration = CallTimer(nil, VoipTheme.call_header_subtitle)
+ let avatar = Avatar(diameter: CGFloat(Avatar.diameter_for_call_views),color:VoipTheme.voipParticipantBackgroundColor, textStyle: VoipTheme.call_generated_avatar_large)
+ let displayName = StyledLabel(VoipTheme.call_header_title)
+ let sipAddress = StyledLabel(VoipTheme.call_header_subtitle)
+
+ var callData: CallData? = nil {
+ didSet {
+ duration.call = callData?.call.dir == .Incoming ? callData?.call : nil
+ callData?.call.remoteAddress.map {
+ avatar.fillFromAddress(address: $0)
+ displayName.text = $0.addressBookEnhancedDisplayName()
+ sipAddress.text = $0.asStringUriOnly()
+ }
+ }
+ }
+
+ func viewDidLoad(forCallType:String) {
+ super.viewDidLoad()
+
+ view.backgroundColor = VoipTheme.voipBackgroundColor.get()
+
+ view.addSubview(spinner)
+ spinner.square(IncomingOutgoingCommonView.spinner_size).matchParentSideBorders().alignParentTop(withMargin:IncomingOutgoingCommonView.spinner_margin_top + UIDevice.notchHeight()).done()
+
+ let callType = StyledLabel(VoipTheme.call_header_title,forCallType)
+ view.addSubview(callType)
+ callType.matchParentSideBorders().alignUnder(view:spinner,withMargin:IncomingOutgoingCommonView.call_type_margin_top).done()
+
+ self.view.addSubview(duration)
+ duration.matchParentSideBorders().alignUnder(view:callType,withMargin:IncomingOutgoingCommonView.duration_margin_top).done()
+
+ // Center : Avatar + Display name + SIP Address
+ let centerSection = UIView()
+ centerSection.addSubview(avatar)
+ avatar.square(Avatar.diameter_for_call_views).center().done()
+ centerSection.addSubview(displayName)
+ displayName.height(IncomingOutgoingCommonView.display_name_height).matchParentSideBorders().alignUnder(view:avatar,withMargin:IncomingOutgoingCommonView.display_name_margin_top).done()
+ centerSection.addSubview(sipAddress)
+ sipAddress.height(IncomingOutgoingCommonView.sip_address_height).matchParentSideBorders().alignUnder(view:displayName,withMargin:IncomingOutgoingCommonView.sip_address_margin_top).done()
+ self.view.addSubview(centerSection)
+ centerSection.matchParentSideBorders().center().done()
+
+
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ super.viewWillAppear(animated)
+ spinner.startRotation()
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ spinner.stopRotation()
+ super.viewWillDisappear(animated)
+ }
+
+ @objc func setCall(call:OpaquePointer) {
+ callData = CallData(call: Call.getSwiftObject(cObject: call))
+ }
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/LocalVideoView.swift b/Classes/Swift/Voip/Views/Fragments/LocalVideoView.swift
new file mode 100644
index 000000000..dba74002d
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/LocalVideoView.swift
@@ -0,0 +1,87 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class LocalVideoView: UIView {
+
+ //Layout constants
+ let corner_radius = 15.0
+ let aspect_ratio = 4.0/3.0
+ let switch_camera_button_margins = 8.0
+ let switch_camera_button_size = 30
+
+ var width : CGFloat
+
+ var dragZone : UIView? {
+ didSet {
+ let panGesture = UIPanGestureRecognizer(target: self, action: #selector(drag))
+ isUserInteractionEnabled = true
+ addGestureRecognizer(panGesture)
+ }
+ }
+
+ let switchCamera = UIImageView(image: UIImage(named:"voip_change_camera")?.tinted(with:.white))
+
+ var callData: CallData? = nil {
+ didSet {
+ callData?.isRemotelyRecorded.readCurrentAndObserve(onChange: { (isRemotelyRecording) in
+ self.isHidden = !(isRemotelyRecording == true)
+ })
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ width = 0.0
+ super.init(coder: coder)
+ }
+
+ init (width:CGFloat) {
+ self.width = width
+ super.init(frame: .zero)
+ layer.cornerRadius = corner_radius
+ clipsToBounds = true
+
+ addSubview(switchCamera)
+ switchCamera.alignParentTop(withMargin: switch_camera_button_margins).alignParentRight(withMargin: switch_camera_button_margins).square(switch_camera_button_size).done()
+ switchCamera.contentMode = .scaleAspectFit
+
+ switchCamera.onClick {
+ Core.get().toggleCamera()
+ }
+ setSizeConstraint()
+ }
+
+ func setSizeConstraint() {
+ size(w: width, h: width*aspect_ratio).done()
+ }
+
+
+ @objc func drag(_ sender:UIPanGestureRecognizer){
+ dragZone?.bringSubviewToFront(self)
+ let translation = sender.translation(in: dragZone)
+ center = CGPoint(x: center.x + translation.x, y: center.y + translation.y)
+ sender.setTranslation(CGPoint.zero, in: dragZone)
+ }
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/NumpadView.swift b/Classes/Swift/Voip/Views/Fragments/NumpadView.swift
new file mode 100644
index 000000000..d8f65364e
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/NumpadView.swift
@@ -0,0 +1,119 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import linphonesw
+
+@objc class NumpadView: UIView {
+
+ // Layout constants
+ let side_margins = 10.0
+ let margin_top = 100.0
+ let button_size = 70
+ let button_vertical_space = 17.0
+ let button_horizontal_space = 14.0
+ let digit_icon_inset = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
+ let corner_radius = 20.0
+ let pad_height = 550
+ let side_padding = 50.0
+
+
+ init(superView:UIView, callData:CallData, marginTop:CGFloat, onDismissAction : @escaping ()->Void) {
+ super.init(frame:.zero)
+ backgroundColor = VoipTheme.voip_translucent_popup_background
+ layer.cornerRadius = corner_radius
+ clipsToBounds = true
+ superView.addSubview(self)
+ matchParentSideBorders(insetedByDx: side_margins).alignParentTop(withMargin: marginTop).alignParentBottom().done()
+
+ callData.callState.observe { state in
+ if (state == Call.State.End) {
+ onDismissAction()
+ }
+ }
+
+ // Hide numpad button
+ let hide = CallControlButton(buttonTheme: VoipTheme.voip_cancel_light, onClickAction: {
+ onDismissAction()
+ })
+ addSubview(hide)
+ hide.alignParentRight(withMargin: side_margins).alignParentTop(withMargin: side_margins).done()
+
+ // DTMF History :
+
+ let eneteredDtmf = StyledLabel(VoipTheme.dtmf_label)
+ addSubview(eneteredDtmf)
+ _ = eneteredDtmf.matchParentSideBorders().alignUnder(view:hide,withMargin:side_margins)
+ callData.enteredDTMF.readCurrentAndObserve { (dtmfs) in
+ eneteredDtmf.text = dtmfs
+ }
+
+ // Digit buttons
+
+ let allRows = UIStackView()
+ allRows.axis = .vertical
+ allRows.distribution = .equalSpacing
+ allRows.alignment = .center
+ allRows.spacing = button_vertical_space
+ allRows.layoutMargins = UIEdgeInsets(top: 0, left: side_padding, bottom: 0, right: side_padding)
+ allRows.isLayoutMarginsRelativeArrangement = true
+ addSubview(allRows)
+ _ = allRows.matchParentSideBorders().alignUnder(view:eneteredDtmf,withMargin: side_margins)
+
+
+ for key in [["1","2","3"],["4","5","6"],["7","8","9"],["*","0","#"]] {
+ let newRow = addRow(allRows: allRows)
+ for subkey in key {
+ let digit = CallControlButton(width:button_size, height:button_size, imageInset: digit_icon_inset, buttonTheme: ButtonTheme(tintableStateIcons:[UIButton.State.normal.rawValue : TintableIcon(name: "voip_numpad_\(iconNameForDigit(digit: subkey))")],backgroundStateColors:VoipTheme.numpad_digit_background), onClickAction: {
+ callData.sendDTMF(dtmf: "\(subkey)")
+ })
+ newRow.addArrangedSubview(digit)
+ }
+ }
+ }
+
+ func iconNameForDigit(digit:String) -> String {
+ if (digit == "*") {
+ return "star"
+ }
+ if (digit == "#") {
+ return "hash"
+ }
+ return digit
+ }
+
+ func addRow(allRows:UIStackView) -> UIStackView {
+ let row = UIStackView()
+ row.axis = .horizontal
+ row.distribution = .equalSpacing
+ row.alignment = .center
+ row.spacing = button_vertical_space
+ row.isLayoutMarginsRelativeArrangement = true
+ allRows.addArrangedSubview(row)
+ return row
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+}
+
+
+
diff --git a/Classes/Swift/Voip/Views/Fragments/ParticipantsList/ParticipantsListView.swift b/Classes/Swift/Voip/Views/Fragments/ParticipantsList/ParticipantsListView.swift
new file mode 100644
index 000000000..9cf76340a
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/ParticipantsList/ParticipantsListView.swift
@@ -0,0 +1,118 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import linphonesw
+
+@objc class ParticipantsListView: DismissableView, UITableViewDataSource {
+
+ // Layout constants
+
+ let participantsListTableView = UITableView()
+ let noParticipantsLabel = StyledLabel(VoipTheme.empty_list_font,VoipTexts.conference_empty)
+
+
+
+ var callsDataObserver : MutableLiveDataOnChangeClosure<[CallData]>? = nil
+
+ init() {
+ super.init(title: VoipTexts.call_action_participants_list)
+
+
+ let edit = CallControlButton(buttonTheme: VoipTheme.voip_edit, onClickAction: {
+ self.gotoParticipantsListSelection()
+ })
+ super.headerView.addSubview(edit)
+ edit.centerY().done()
+ super.dismiss?.toRightOf(edit,withLeftMargin: dismiss_right_margin).centerY().done()
+
+
+ // ParticipantsList
+ super.contentView.addSubview(participantsListTableView)
+ participantsListTableView.matchParentDimmensions().done()
+ participantsListTableView.dataSource = self
+ participantsListTableView.register(VoipParticipantCell.self, forCellReuseIdentifier: "VoipParticipantCell")
+ participantsListTableView.allowsSelection = false
+ if #available(iOS 15.0, *) {
+ participantsListTableView.allowsFocus = false
+ }
+ participantsListTableView.separatorStyle = .singleLine
+ participantsListTableView.separatorColor = .white
+
+
+ CallsViewModel.shared.callsData.readCurrentAndObserve{ (callsData) in
+ self.participantsListTableView.reloadData()
+ }
+
+ ConferenceViewModel.shared.isMeAdmin.readCurrentAndObserve { (meAdmin) in
+ edit.isHidden = meAdmin != true
+ }
+
+ super.contentView.addSubview(noParticipantsLabel)
+ noParticipantsLabel.center().done()
+ noParticipantsLabel.isHidden = ConferenceViewModel.shared.conferenceParticipants.value?.count ?? 0 > 0
+
+ }
+
+
+ // TableView datasource delegate
+
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ guard let participants = ConferenceViewModel.shared.conferenceParticipants.value else {
+ return 0
+ }
+ return participants.count
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell:VoipParticipantCell = tableView.dequeueReusableCell(withIdentifier: "VoipParticipantCell") as! VoipParticipantCell
+ guard let participantData = ConferenceViewModel.shared.conferenceParticipants.value?[indexPath.row] else {
+ return cell
+ }
+ cell.selectionStyle = .none
+ cell.participantData = participantData
+ cell.owningParticpantsListView = self
+ return cell
+ }
+
+ // View controller
+
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func gotoParticipantsListSelection() {
+ let view: ChatConversationCreateView = self.VIEW(ChatConversationCreateView.compositeViewDescription());
+ let addresses = ConferenceViewModel.shared.conferenceParticipants.value!.map { (data) in String(data.participant.address!.asStringUriOnly()) }
+ view.tableController.contactsGroup = (addresses as NSArray).mutableCopy() as? NSMutableArray
+ view.isForEditing = false
+ view.isForVoipConference = true
+ view.isForOngoingVoipConference = true
+ view.tableController.notFirstTime = true
+ view.isGroupChat = true
+ PhoneMainView.instance().changeCurrentView(view.compositeViewDescription())
+ }
+
+
+
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/ParticipantsList/VoipParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/ParticipantsList/VoipParticipantCell.swift
new file mode 100644
index 000000000..e5befc446
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/ParticipantsList/VoipParticipantCell.swift
@@ -0,0 +1,133 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class VoipParticipantCell: UITableViewCell {
+
+ // Layout Constants
+
+ let dismiss_icon_inset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
+ let dismiss_right_margin = 10
+ let check_box_size = 20.0
+ static let cell_height = 80.0
+ let avatar_left_margin = 15.0
+ let texts_left_margin = 20.0
+ let lime_badge_width = 18.0
+ let lime_badge_offset = -10.0
+
+
+ let avatar = Avatar(diameter:VoipCallCell.avatar_size,color:VoipTheme.primaryTextColor, textStyle: VoipTheme.call_generated_avatar_small)
+ let limeBadge = UIImageView(image: UIImage(named: "security_toggle_icon_green"))
+ let displayName = StyledLabel(VoipTheme.conference_participant_name_font)
+ let sipAddress = StyledLabel(VoipTheme.conference_participant_sip_uri_font)
+ let isAdminView = UIView()
+ var removePart : CallControlButton?
+
+
+ var owningParticpantsListView : ParticipantsListView? = nil
+
+ var participantData: ConferenceParticipantData? = nil {
+ didSet {
+ if let data = participantData {
+ limeBadge.isHidden = true
+ avatar.fillFromAddress(address: data.participant.address!)
+ displayName.text = data.participant.address?.addressBookEnhancedDisplayName()
+ sipAddress.text = data.participant.address?.asStringUriOnly()
+ data.isAdmin.readCurrentAndObserve { (isAdmin) in self.isAdminView.isHidden = isAdmin != true
+
+ }
+ data.isMeAdmin.readCurrentAndObserve { (isMeAdmin) in
+ self.removePart!.isHidden = isMeAdmin != true
+ self.isAdminView.alpha = isMeAdmin == true ? 1.0 : 0.6
+ self.isAdminView.isUserInteractionEnabled = isMeAdmin == true
+ }
+ self.isAdminView.onClick {
+ data.conference.setParticipantAdminStatus(participant: data.participant, isAdmin: data.isAdmin.value != true)
+ self.owningParticpantsListView?.participantsListTableView.reloadData()
+ }
+ self.removePart?.onClick {
+ try?data.conference.removeParticipant(participant: data.participant)
+ self.owningParticpantsListView?.participantsListTableView.reloadData()
+ }
+ }
+ }
+ }
+
+ var scheduleConfParticipantAddress: Address? = nil {
+ didSet {
+ if let address = scheduleConfParticipantAddress {
+ avatar.fillFromAddress(address: address)
+ displayName.text = address.addressBookEnhancedDisplayName()
+ sipAddress.text = address.asStringUriOnly()
+ self.isAdminView.isHidden = true
+ self.removePart?.isHidden = true
+ }
+ }
+ }
+
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+ contentView.height(VoipParticipantCell.cell_height).matchParentSideBorders().done()
+
+ contentView.addSubview(avatar)
+ avatar.size(w: VoipCallCell.avatar_size, h: VoipCallCell.avatar_size).centerY().alignParentLeft(withMargin: avatar_left_margin).done()
+
+ limeBadge.contentMode = .scaleAspectFit
+ contentView.addSubview(limeBadge)
+ limeBadge.toRightOf(avatar,withLeftMargin: lime_badge_offset).width(lime_badge_width).done()
+
+ let nameAddress = UIView()
+ nameAddress.addSubview(displayName)
+ nameAddress.addSubview(sipAddress)
+ displayName.alignParentTop().done()
+ sipAddress.alignUnder(view: displayName).done()
+ contentView.addSubview(nameAddress)
+ nameAddress.toRightOf(avatar,withLeftMargin:texts_left_margin).wrapContentY().centerY().done()
+
+ removePart = CallControlButton(imageInset:dismiss_icon_inset,buttonTheme: VoipTheme.voip_cancel, onClickAction: {
+ self.removeFromSuperview()
+ })
+ contentView.addSubview(removePart!)
+ removePart!.alignParentRight(withMargin: dismiss_right_margin).centerY().done()
+
+ let isAdminLabel = StyledLabel(VoipTheme.conference_participant_admin_label,VoipTexts.chat_room_group_info_admin)
+ isAdminView.addSubview(isAdminLabel)
+ isAdminLabel.alignParentRight().centerY().done()
+
+ let isAdminCheck = UIImageView(image: UIImage(named:("check_unselected")))
+ isAdminView.addSubview(isAdminCheck)
+ isAdminCheck.size(w: check_box_size, h: check_box_size).toLeftOf(isAdminLabel).done()
+
+ contentView.addSubview(isAdminView)
+ isAdminView.height(check_box_size).toLeftOf(removePart!).centerY().done()
+
+
+
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/PausedCallOrConferenceView.swift b/Classes/Swift/Voip/Views/Fragments/PausedCallOrConferenceView.swift
new file mode 100644
index 000000000..45a1a9a98
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/PausedCallOrConferenceView.swift
@@ -0,0 +1,70 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class PausedCallOrConferenceView: UIView {
+
+ // Layout constants
+ let icon_size = 200
+ let icon_padding = 80.0
+ let title_margin_top = 20
+
+ var icon : UIImageView? = nil
+ let title = StyledLabel(VoipTheme.call_or_conference_title)
+ let subtitle = StyledLabel(VoipTheme.call_or_conference_subtitle)
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ init (iconName:String, titleText:String, subTitleText:String? = nil) {
+ super.init(frame: .zero)
+
+ backgroundColor = VoipTheme.voip_translucent_popup_background
+
+ let centeredView = UIView()
+ icon = UIImageView(image: UIImage(named:iconName)?.withPadding(padding: icon_padding))
+ icon!.backgroundColor = VoipTheme.primary_color
+ icon!.layer.cornerRadius = CGFloat(icon_size/2)
+ icon!.clipsToBounds = true
+ icon!.contentMode = .scaleAspectFit
+ centeredView.addSubview(icon!)
+ icon!.square(icon_size).centerX().done()
+
+ title.numberOfLines = 0
+ centeredView.addSubview(title)
+ title.alignUnder(view:icon!, withMargin:title_margin_top).matchParentSideBorders().done()
+ title.text = titleText
+
+ subtitle.numberOfLines = 0
+ centeredView.addSubview(subtitle)
+ subtitle.alignUnder(view: title).matchParentSideBorders().done()
+ subtitle.text = subTitleText
+
+ self.addSubview(centeredView)
+ centeredView.center().matchParentSideBorders().wrapContentY().done()
+
+ }
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/RemotelyRecording.swift b/Classes/Swift/Voip/Views/Fragments/RemotelyRecording.swift
new file mode 100644
index 000000000..abaaba42c
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/RemotelyRecording.swift
@@ -0,0 +1,61 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class RemotelyRecordingView: UIView {
+
+ let label = StyledLabel(VoipTheme.call_remote_recording)
+ let icon = UIImageView(image: UIImage(named:"voip_remote_recording"))
+
+ var isRemotelyRecorded: MutableLiveData? = nil {
+ didSet {
+ isRemotelyRecorded?.readCurrentAndObserve(onChange: { (isRemotelyRecording) in
+ self.isHidden = isRemotelyRecording != true
+ })
+ }
+ }
+
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ init (height:Int, text:String) {
+ super.init(frame: .zero)
+ backgroundColor = VoipTheme.dark_grey_color
+ layer.cornerRadius = CGFloat(height/2)
+ clipsToBounds = true
+
+ addSubview(label)
+ label.center().height(CGFloat(height)).done()
+ label.text = text
+
+ addSubview(icon)
+ icon.square(height).toLeftOf(label).done()
+
+ isHidden = true
+
+ }
+
+}
diff --git a/Classes/Swift/Voip/Views/Fragments/VoipExtraButtonsView.swift b/Classes/Swift/Voip/Views/Fragments/VoipExtraButtonsView.swift
new file mode 100644
index 000000000..6c180a3ca
--- /dev/null
+++ b/Classes/Swift/Voip/Views/Fragments/VoipExtraButtonsView.swift
@@ -0,0 +1,133 @@
+/*
+ * 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 .
+ */
+
+
+import UIKit
+import Foundation
+import SnapKit
+import linphonesw
+
+class VoipExtraButtonsView: UIStackView {
+
+ //Layout constants
+ let height = 200.0
+ let corner_radius = 20.0
+
+ required init(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ init () {
+ super.init(frame: .zero)
+
+ axis = .vertical
+ distribution = .fillEqually
+ alignment = .center
+
+ layer.cornerRadius = corner_radius
+ clipsToBounds = true
+
+ backgroundColor = VoipTheme.voipExtraButtonsBackgroundColor.get()
+ height(height).done()
+
+ let row1 = UIStackView()
+ row1.axis = .horizontal
+ row1.distribution = .fillEqually
+ row1.alignment = .center
+
+
+ // First row
+ let numpad = VoipExtraButton(text: VoipTexts.call_action_numpad, buttonTheme: VoipTheme.call_action("voip_call_numpad"),onClickAction: {
+ ControlsViewModel.shared.numpadVisible.notifyAllObservers(with: true)
+ })
+ row1.addArrangedSubview(numpad)
+
+ let stats = VoipExtraButton(text: VoipTexts.call_action_statistics, buttonTheme: VoipTheme.call_action("voip_call_stats"),onClickAction: {
+ ControlsViewModel.shared.callStatsVisible.notifyAllObservers(with: true)
+ })
+ row1.addArrangedSubview(stats)
+
+ let chats = VoipExtraButton(text: VoipTexts.call_action_chat, buttonTheme: VoipTheme.call_action("voip_call_chat"),withbBoucinCounterDataSource:CallsViewModel.shared.currentCallUnreadChatMessageCount, onClickAction: {
+ ControlsViewModel.shared.goToChatEvent.notifyAllObservers(with: true)
+ })
+ row1.addArrangedSubview(chats)
+
+ addArrangedSubview(row1)
+ row1.matchParentSideBorders().done()
+
+ // Second row
+
+ let row2 = UIStackView()
+ row2.axis = .horizontal
+ row2.distribution = .fillEqually
+ row2.alignment = .center
+
+ let transfer = VoipExtraButton(text: VoipTexts.call_action_transfer_call, buttonTheme: VoipTheme.call_action("voip_call_forward"),onClickAction: {
+ let view: DialerView = self.VIEW(DialerView.compositeViewDescription());
+ view.setAddress("")
+ CallManager.instance().nextCallIsTransfer = true
+ PhoneMainView.instance().changeCurrentView(view.compositeViewDescription())
+ })
+ row2.addArrangedSubview(transfer)
+
+ let participants = VoipExtraButton(text: VoipTexts.call_action_participants_list, buttonTheme: VoipTheme.call_action("voip_call_participants"),onClickAction: {
+ ControlsViewModel.shared.goToConferenceParticipantsListEvent.notifyAllObservers(with: true)
+ })
+ row2.addArrangedSubview(participants)
+
+
+ let addcall = VoipExtraButton(text: VoipTexts.call_action_add_call, buttonTheme: VoipTheme.call_action("voip_call_add"),onClickAction: {
+ let view: DialerView = self.VIEW(DialerView.compositeViewDescription());
+ view.setAddress("")
+ CallManager.instance().nextCallIsTransfer = false
+ PhoneMainView.instance().changeCurrentView(view.compositeViewDescription())
+ })
+ row2.addArrangedSubview(addcall)
+
+
+ let layoutselect = VoipExtraButton(text: VoipTexts.call_action_change_conf_layout, buttonTheme: VoipTheme.call_action("voip_conference_mosaic"),onClickAction: {
+ ControlsViewModel.shared.goToConferenceLayoutSettings.notifyAllObservers(with: true)
+ })
+ row2.addArrangedSubview(layoutselect)
+
+ let calls = VoipExtraButton(text: VoipTexts.call_action_calls_list, buttonTheme: VoipTheme.call_action("voip_calls_list"), withbBoucinCounterDataSource: CallsViewModel.shared.inactiveCallsCount, onClickAction: {
+ ControlsViewModel.shared.goToCallsListEvent.notifyAllObservers(with: true)
+ })
+ row2.addArrangedSubview(calls)
+
+ addArrangedSubview(row2)
+ row2.matchParentSideBorders().done()
+
+ ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { (isIn) in
+ participants.isHidden = isIn != true
+ layoutselect.isHidden = isIn != true
+ transfer.isHidden = isIn == true
+ addcall.isHidden = isIn == true
+ }
+
+ }
+
+ func refresh() {
+ CallsViewModel.shared.currentCallUnreadChatMessageCount.notifyValue()
+ CallsViewModel.shared.inactiveCallsCount.notifyValue()
+ }
+
+
+
+}
diff --git a/Classes/Swift/Voip/Views/SharedLayoutConstants.swift b/Classes/Swift/Voip/Views/SharedLayoutConstants.swift
new file mode 100644
index 000000000..b03960573
--- /dev/null
+++ b/Classes/Swift/Voip/Views/SharedLayoutConstants.swift
@@ -0,0 +1,28 @@
+/*
+ * 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 .
+ */
+
+
+import Foundation
+
+class SharedLayoutConstants {
+ static let buttons_bottom_margin = UIDevice.hasNotch() ? 30 : 15
+ static let margin_call_view_side_controls_buttons = 12
+ static let bottom_margin_notch_clearance = UIDevice.hasNotch() ? 30.0 : 0.0
+
+}
diff --git a/Classes/Swift/Voip/VoipDialog.swift b/Classes/Swift/Voip/VoipDialog.swift
new file mode 100644
index 000000000..696e8d190
--- /dev/null
+++ b/Classes/Swift/Voip/VoipDialog.swift
@@ -0,0 +1,115 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import UIKit
+
+class VoipDialog : UIView{
+
+ // Layout constants
+ let center_corner_radius = 7.0
+ let title_margin_top = 20
+ let button_margin = 20.0
+ let button_width = 135.0
+ let button_height = 40.0
+ let button_radius = 3.0
+ let button_spacing = 15.0
+
+ let center_view_sides_margin = 13.0
+
+
+ let title = StyledLabel(VoipTheme.basic_popup_title)
+
+ init(message:String, givenButtons:[ButtonAttributes]? = nil) {
+
+ super.init(frame: .zero)
+ backgroundColor = VoipTheme.voip_translucent_popup_background
+
+ let centerView = UIView()
+ centerView.backgroundColor = VoipTheme.dark_grey_color.withAlphaComponent(0.8)
+ centerView.layer.cornerRadius = center_corner_radius
+ centerView.clipsToBounds = true
+ addSubview(centerView)
+
+ title.numberOfLines = 0
+ centerView.addSubview(title)
+ title.alignParentTop(withMargin:title_margin_top).matchParentSideBorders().done()
+ title.text = message
+
+ let buttonsView = UIStackView()
+ buttonsView.axis = .horizontal
+ buttonsView.spacing = button_spacing
+
+ var buttons = givenButtons
+
+ if (buttons == nil) { // assuming info popup, just putting an ok button
+ let ok = ButtonAttributes(text:VoipTexts.ok, action: {}, isDestructive:false)
+ buttons = [ok]
+ }
+
+ buttons?.forEach {
+ let b = ButtonWithStateBackgrounds(backgroundStateColors: $0.isDestructive ? VoipTheme.primary_colors_background_gray : VoipTheme.primary_colors_background)
+ b.setTitle($0.text, for: .normal)
+ b.layer.cornerRadius = button_radius
+ b.clipsToBounds = true
+ buttonsView.addArrangedSubview(b)
+ b.applyTitleStyle(VoipTheme.form_button_bold)
+ let action = $0.action
+ b.onClick {
+ self.removeFromSuperview()
+ action()
+ }
+ b.size(w: button_width,h: button_height).done()
+ }
+ centerView.addSubview(buttonsView)
+ buttonsView.alignUnder(view:title,withMargin:button_margin).alignParentBottom(withMargin:button_margin).centerX().done()
+
+
+
+ centerView.matchParentSideBorders(insetedByDx: center_view_sides_margin).center().done()
+ }
+
+ func show() {
+ VoipDialog.rootVC()?.view.addSubview(self)
+ matchParentDimmensions().done()
+ }
+
+ private static func rootVC() -> UIViewController? {
+ return UIApplication.getTopMostViewController()
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ static func toast(message:String, timeout:CGFloat = 1.5) {
+ let alert = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet)
+ rootVC()?.present(alert, animated: true)
+ DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + timeout) {
+ alert.dismiss(animated: true)
+ }
+ }
+
+}
+
+struct ButtonAttributes {
+ let text:String
+ let action: (()->Void)
+ let isDestructive: Bool
+}
diff --git a/Classes/Swift/Voip/Widgets/Avatar.swift b/Classes/Swift/Voip/Widgets/Avatar.swift
new file mode 100644
index 000000000..78b6ddffb
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/Avatar.swift
@@ -0,0 +1,62 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import linphonesw
+
+class Avatar : UIImageView {
+
+ static let diameter_for_call_views = 191
+
+ required init?(coder: NSCoder) {
+ initialsLabel = StyledLabel(VoipTheme.call_generated_avatar_large)
+ super.init(coder: coder)
+ }
+
+ let initialsLabel: StyledLabel
+
+ init (diameter: CGFloat, color:LightDarkColor,textStyle:TextStyle) {
+ initialsLabel = StyledLabel(textStyle)
+ super.init(frame: .zero)
+ layer.cornerRadius = diameter/2.0
+ clipsToBounds = true
+ self.backgroundColor = color.get()
+ addSubview(initialsLabel)
+ _ = initialsLabel.matchParentSideBorders().matchParentHeight()
+ }
+
+
+ func fillFromAddress(address:Address, isGroup:Bool = false) {
+ if (isGroup) {
+ self.image = UIImage(named:"voip_multiple_contacts_avatar")?.withPadding(padding: 50)
+ initialsLabel.isHidden = true
+ } else if let image = address.contact()?.avatar() {
+ self.image = image
+ initialsLabel.isHidden = true
+ } else {
+ self.image = nil
+ initialsLabel.text = address.initials()
+ initialsLabel.isHidden = false
+ }
+ }
+
+
+
+}
+
diff --git a/Classes/Swift/Voip/Widgets/BouncingCounter.swift b/Classes/Swift/Voip/Widgets/BouncingCounter.swift
new file mode 100644
index 000000000..a07f5124b
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/BouncingCounter.swift
@@ -0,0 +1,75 @@
+/*
+ * 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 .
+ */
+
+
+import Foundation
+import UIKit
+import SwiftUI
+
+class BouncingCounter : UIBouncingView {
+
+ // Layout constants
+ let size = 20.0
+ let center_offset = 20
+
+ let owningButton : UIButton
+ let label : StyledLabel
+
+ var dataSource : MutableLiveData? {
+ didSet {
+ if let dataSource = dataSource {
+ self.size(w:self.size,h:self.size).matchCenterXOf(view: self.owningButton, withDx: self.center_offset).matchCenterYOf(view: self.owningButton, withDy: -self.center_offset).done()
+ dataSource.readCurrentAndObserve { (value) in
+ if (value! > 0) {
+ self.label.text = value! < 100 ? String(value!) : "99+"
+ self.isHidden = true // to force legacy startAnimating to unhide and animate
+ self.startAnimating(true)
+ } else {
+ self.isHidden = false // to force legacy startAnimating to hide and animate
+ self.stopAnimating(true)
+ }
+ }
+ } else {
+ self.isHidden = false
+ self.stopAnimating(true)
+ }
+ }
+ }
+
+
+ init (inButton:UIButton) {
+ owningButton = inButton
+ label = StyledLabel(VoipTheme.unread_count_font)
+ super.init(frame:.zero)
+ addSubview(label)
+ label.matchParentDimmensions().done()
+ backgroundColor = VoipTheme.primary_color
+ layer.masksToBounds = true
+ layer.cornerRadius = size/2
+ self.isHidden = true
+ }
+
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+
+
+}
diff --git a/Classes/Swift/Voip/Widgets/ButtonWithStateBackgrounds.swift b/Classes/Swift/Voip/Widgets/ButtonWithStateBackgrounds.swift
new file mode 100644
index 000000000..5e0221c15
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/ButtonWithStateBackgrounds.swift
@@ -0,0 +1,50 @@
+/*
+ * 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 .
+ */
+
+
+import Foundation
+import UIKit
+import SwiftUI
+
+class ButtonWithStateBackgrounds : UIButton {
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ init (backgroundStateColors: [UInt: LightDarkColor], iconName:String? = nil) {
+ super.init(frame: .zero)
+ backgroundStateColors.keys.forEach { (stateRawValue) in
+ setBackgroundColor(color: backgroundStateColors[stateRawValue]!.get(), forState: UIButton.State(rawValue: stateRawValue))
+ }
+ iconName.map { setImage(UIImage(named: $0), for: .normal) }
+ }
+
+ func setBackgroundColor(color: UIColor, forState: UIControl.State) {
+ UIGraphicsBeginImageContext(CGSize(width: 1, height: 1))
+ UIGraphicsGetCurrentContext()!.setFillColor(color.cgColor)
+ UIGraphicsGetCurrentContext()!.fill(CGRect(x: 0, y: 0, width: 1, height: 1))
+ let colorImage = UIGraphicsGetImageFromCurrentImageContext()
+ UIGraphicsEndImageContext()
+ self.setBackgroundImage(colorImage, for: forState)
+ }
+
+
+
+}
diff --git a/Classes/Swift/Voip/Widgets/CallControlButton.swift b/Classes/Swift/Voip/Widgets/CallControlButton.swift
new file mode 100644
index 000000000..474926248
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/CallControlButton.swift
@@ -0,0 +1,85 @@
+/*
+ * 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 .
+ */
+
+
+import Foundation
+import UIKit
+import SwiftUI
+
+class CallControlButton : ButtonWithStateBackgrounds {
+
+ // Layout constants
+ static let default_size = 50
+ static let hungup_width = 65
+
+ var showActivityIndicatorDataSource : MutableLiveData? = nil {
+ didSet {
+ if let showActivityIndicatorDataSource = self.showActivityIndicatorDataSource {
+ let spinner = UIActivityIndicatorView(style: .white)
+ spinner.color = VoipTheme.primary_color
+ self.addSubview(spinner)
+ spinner.matchParentDimmensions().center().done()
+
+ showActivityIndicatorDataSource.readCurrentAndObserve { (show) in
+ if (show == true) {
+ spinner.startAnimating()
+ spinner.isHidden = false
+ self.isEnabled = false
+ } else {
+ spinner.stopAnimating()
+ spinner.isHidden = true
+ self.isEnabled = true
+ }
+ }
+ }
+ }
+ }
+
+ var onClickAction : (()->Void)? = nil
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ init (width:Int = CallControlButton.default_size, height:Int = CallControlButton.default_size, imageInset:UIEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2), buttonTheme: ButtonTheme, onClickAction : (()->Void)? = nil ) {
+ super.init(backgroundStateColors: buttonTheme.backgroundStateColors)
+
+ layer.cornerRadius = CGFloat(height/2)
+ clipsToBounds = true
+ contentMode = .scaleAspectFit
+
+ applyTintedIcons(tintedIcons: buttonTheme.tintableStateIcons)
+
+ imageView?.contentMode = .scaleAspectFit
+
+ imageEdgeInsets = imageInset
+
+ size(w: CGFloat(width), h: CGFloat(height)).done()
+
+ self.onClickAction = onClickAction
+ onClick {
+ self.onClickAction?()
+ }
+
+ }
+
+
+
+
+}
diff --git a/Classes/Swift/Voip/Widgets/FormButton.swift b/Classes/Swift/Voip/Widgets/FormButton.swift
new file mode 100644
index 000000000..618491852
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/FormButton.swift
@@ -0,0 +1,60 @@
+/*
+ * 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 .
+ */
+
+
+import Foundation
+import UIKit
+import SwiftUI
+
+class FormButton : ButtonWithStateBackgrounds {
+
+ let button_radius = 3.0
+ let button_height = 40.0
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ var title: String? {
+ didSet {
+ setTitle(title, for: .normal)
+ addSidePadding()
+ }
+ }
+
+ init (backgroundStateColors: [UInt: LightDarkColor], bold:Bool = true) {
+ super.init(backgroundStateColors: backgroundStateColors)
+ layer.cornerRadius = button_radius
+ clipsToBounds = true
+ applyTitleStyle(bold ? VoipTheme.form_button_bold : VoipTheme.form_button_light)
+ height(button_height).done()
+ addSidePadding()
+ }
+
+ convenience init (title:String, backgroundStateColors: [UInt: LightDarkColor], bold:Bool = true, fixedSize:Bool = true) {
+ self.init(backgroundStateColors: backgroundStateColors,bold:bold)
+ self.title = title
+ setTitle(title, for: .normal)
+ if (!fixedSize) {
+ addSidePadding()
+ }
+ }
+
+
+}
diff --git a/Classes/Swift/Voip/Widgets/RotatingSpinner.swift b/Classes/Swift/Voip/Widgets/RotatingSpinner.swift
new file mode 100644
index 000000000..a03f12671
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/RotatingSpinner.swift
@@ -0,0 +1,48 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+
+class RotatingSpinner : UIImageView {
+
+ init () {
+ super.init(frame: .zero)
+ self.image = UIImage(named: "voip_spinner")
+ self.tint(UIColor.white)
+ self.contentMode = .scaleAspectFit
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ func startRotation() {
+ let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
+ rotation.toValue = NSNumber(value: Double.pi * 2)
+ rotation.duration = 2.2
+ rotation.isCumulative = true
+ rotation.repeatCount = Float.greatestFiniteMagnitude
+ self.layer.add(rotation, forKey: "rotationAnimation")
+ }
+
+ func stopRotation() {
+ self.layer.removeAllAnimations()
+ }
+}
+
diff --git a/Classes/Swift/Voip/Widgets/StyledCheckBox.swift b/Classes/Swift/Voip/Widgets/StyledCheckBox.swift
new file mode 100644
index 000000000..b5bf8323f
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/StyledCheckBox.swift
@@ -0,0 +1,54 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import UIKit
+import DropDown
+
+
+class StyledCheckBox: UIButton {
+
+ // layout constants
+ let button_size = 25.0
+
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ init (liveValue:MutableLiveData) {
+ super.init(frame: .zero)
+ setBackgroundImage(UIImage(named:"voip_checkbox_unchecked")/*?.tinted(with: VoipTheme.light_grey_color)*/,for: .normal) // tinting not working with those icons
+ setBackgroundImage(UIImage(named:"voip_checkbox_checked")/*?.tinted(with: VoipTheme.primary_color)*/,for: .selected)
+ onClick {
+ liveValue.value = !liveValue.value!
+ self.isSelected = liveValue.value!
+ }
+
+ size(w: button_size,h: button_size).done()
+
+ liveValue.readCurrentAndObserve { (value) in
+ self.isSelected = value!
+ }
+
+
+ }
+
+
+}
diff --git a/Classes/Swift/Voip/Widgets/StyledDatePicker.swift b/Classes/Swift/Voip/Widgets/StyledDatePicker.swift
new file mode 100644
index 000000000..cd2aedb8d
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/StyledDatePicker.swift
@@ -0,0 +1,82 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import UIKit
+
+class StyledDatePicker: UIView {
+
+ // layout constants
+ let chevron_margin = 10
+ let form_input_height = 38.0
+
+ let datePicker = UIDatePicker()
+
+ var liveValue:MutableLiveData? {
+ didSet {
+ if let liveValue = liveValue {
+ datePicker.date = liveValue.value!
+ self.valueChanged(datePicker: datePicker)
+ }
+ }
+
+ }
+ let formattedLabel = StyledLabel(VoipTheme.conference_scheduling_font)
+ var pickerMode:UIDatePicker.Mode = .date
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ init (liveValue:MutableLiveData? = nil, pickerMode:UIDatePicker.Mode, readOnly:Bool = false) {
+ super.init(frame: .zero)
+ self.pickerMode = pickerMode
+
+ addSubview(datePicker)
+ datePicker.datePickerMode = pickerMode
+ datePicker.addTarget(self, action: #selector(valueChanged), for: .valueChanged)
+ datePicker.matchParentDimmensions().done()
+
+ formattedLabel.isUserInteractionEnabled = false
+ formattedLabel.backgroundColor = VoipTheme.voipFormBackgroundColor.get()
+ addSubview(formattedLabel)
+ formattedLabel.matchParentDimmensions().done()
+
+ let chevron = UIImageView(image: UIImage(named: "chevron_list_close"))
+ addSubview(chevron)
+ chevron.alignParentRight(withMargin: chevron_margin).centerY().done()
+ chevron.isHidden = readOnly
+
+ setFormInputBackground(readOnly:readOnly)
+ height(form_input_height).done()
+
+ if (readOnly) {
+ formattedLabel.textColor = formattedLabel.textColor.withAlphaComponent(0.5)
+ }
+ isUserInteractionEnabled = !readOnly
+ self.liveValue = liveValue
+
+ }
+
+
+ @objc func valueChanged(datePicker: UIDatePicker) {
+ liveValue!.value = datePicker.date
+ formattedLabel.text = " "+(pickerMode == .date ? TimestampUtils.dateToString(date: datePicker.date) : TimestampUtils.timeToString(date: datePicker.date))
+ }
+}
diff --git a/Classes/LinphoneUI/UIHangUpButton.h b/Classes/Swift/Voip/Widgets/StyledLabel.swift
similarity index 72%
rename from Classes/LinphoneUI/UIHangUpButton.h
rename to Classes/Swift/Voip/Widgets/StyledLabel.swift
index 784369828..29c15da3e 100644
--- a/Classes/LinphoneUI/UIHangUpButton.h
+++ b/Classes/Swift/Voip/Widgets/StyledLabel.swift
@@ -1,7 +1,7 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
*
- * This file is part of linphone-iphone
+ * 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
@@ -17,13 +17,18 @@
* along with this program. If not, see .
*/
-#import
+import Foundation
-#import "UIIconButton.h"
+class StyledLabel: UILabel {
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ init (_ style:TextStyle, _ text:String? = nil) {
+ super.init(frame: .zero)
+ self.text = text
+ applyStyle(style)
+ }
-@interface UIHangUpButton : UIIconButton {
}
-
-- (void)update;
-
-@end
diff --git a/Classes/Swift/Voip/Widgets/StyledSwitch.swift b/Classes/Swift/Voip/Widgets/StyledSwitch.swift
new file mode 100644
index 000000000..cc96c80d9
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/StyledSwitch.swift
@@ -0,0 +1,46 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import UIKit
+
+class StyledSwitch: UISwitch {
+
+ var liveValue:MutableLiveData?
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ init (liveValue:MutableLiveData) {
+ super.init(frame: .zero)
+ self.liveValue = liveValue
+ tintColor = VoipTheme.light_grey_color
+ onTintColor = VoipTheme.green_color
+ addTarget(self, action: #selector(valueChanged), for: .valueChanged)
+ liveValue.readCurrentAndObserve { (value) in
+ self.isOn = value == true
+ }
+ transform = CGAffineTransform(scaleX: 0.75, y: 0.75)
+ }
+
+ @objc func valueChanged(mySwitch: UISwitch) {
+ liveValue!.value = mySwitch.isOn
+ }
+}
diff --git a/Classes/Swift/Voip/Widgets/StyledTextView.swift b/Classes/Swift/Voip/Widgets/StyledTextView.swift
new file mode 100644
index 000000000..6e2f54984
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/StyledTextView.swift
@@ -0,0 +1,83 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+
+class StyledTextView: UITextView, UITextViewDelegate {
+
+ var placeholder:String?
+ var style:TextStyle?
+ var liveValue: MutableLiveData? = nil
+ var maxLines:Int
+
+ required init?(coder: NSCoder) {
+ maxLines = 0
+ super.init(coder: coder)
+ }
+
+ init (_ style:TextStyle, placeHolder:String? = nil, liveValue: MutableLiveData, readOnly:Bool = false, maxLines:Int = 999) {
+ self.maxLines = maxLines
+ self.style = style
+ self.liveValue = liveValue
+ super.init(frame:.zero, textContainer: nil)
+ textContainer.maximumNumberOfLines = maxLines
+ applyStyle(style)
+ setFormInputBackground(readOnly:readOnly)
+ placeHolder.map {
+ self.placeholder = $0
+ }
+ delegate = self
+ liveValue.readCurrentAndObserve { (value) in
+ self.text = value
+ if (value == nil || value?.count == 0) {
+ self.showPlaceHolder()
+ self.resignFirstResponder()
+ }
+ }
+ if (readOnly) {
+ textColor = textColor?.withAlphaComponent(0.5)
+ }
+ isUserInteractionEnabled = !readOnly
+ }
+
+ func textViewDidBeginEditing(_ textView: UITextView) {
+ if text == placeholder {
+ placeholder = textView.text
+ text = ""
+ textColor = style?.fgColor.get().withAlphaComponent(1.0)
+ }
+ }
+
+ func textViewDidEndEditing(_ textView: UITextView) {
+ if text == "" {
+ showPlaceHolder()
+ }
+ }
+
+ private func showPlaceHolder() {
+ text = placeholder
+ textColor = style?.fgColor.get().withAlphaComponent(0.5)
+ }
+
+ func textViewDidChange(_ textView: UITextView) {
+ textView.removeTextUntilSatisfying(maxNumberOfLines: self.maxLines)
+ liveValue?.value = textView.text
+ }
+
+}
diff --git a/Classes/Swift/Voip/Widgets/StyledValuePicker.swift b/Classes/Swift/Voip/Widgets/StyledValuePicker.swift
new file mode 100644
index 000000000..e13a9117e
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/StyledValuePicker.swift
@@ -0,0 +1,103 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import UIKit
+import DropDown
+
+
+class StyledValuePicker: UIView {
+
+ // layout constants
+ let chevron_margin = 10.0
+ let form_input_height = 38.0
+ let dropDown = DropDown()
+
+
+ let formattedLabel = StyledLabel(VoipTheme.conference_scheduling_font)
+ var pickerMode:UIDatePicker.Mode = .date
+ var options : [String]
+
+ required init?(coder: NSCoder) {
+ self.options = []
+ super.init(coder: coder)
+ }
+
+ init (liveIndex:MutableLiveData, options:[String], readOnly:Bool = false) {
+ self.options = options
+ super.init(frame: .zero)
+
+ formattedLabel.isUserInteractionEnabled = false
+ formattedLabel.backgroundColor = VoipTheme.voipFormBackgroundColor.get()
+ liveIndex.value.map { formattedLabel.text = " "+options[$0] }
+
+ if (readOnly) {
+ formattedLabel.textColor = formattedLabel.textColor.withAlphaComponent(0.5)
+ }
+
+ addSubview(formattedLabel)
+ formattedLabel.alignParentLeft().alignParentRight(withMargin: (readOnly ? chevron_margin : form_input_height)).matchParentHeight().done()
+
+ let chevron = UIImageView(image: UIImage(named: "chevron_list_close"))
+ addSubview(chevron)
+ chevron.alignParentRight(withMargin: chevron_margin).centerY().done()
+ chevron.isHidden = readOnly
+
+ setFormInputBackground(readOnly:readOnly)
+
+
+ DropDown.appearance().textColor = VoipTheme.conference_scheduling_font.fgColor.get()
+ DropDown.appearance().selectedTextColor = VoipTheme.conference_scheduling_font.fgColor.get()
+ DropDown.appearance().textFont = formattedLabel.font
+ DropDown.appearance().backgroundColor = .white
+ DropDown.appearance().selectionBackgroundColor = VoipTheme.light_grey_color
+ DropDown.appearance().cellHeight = form_input_height
+
+ dropDown.anchorView = self
+ dropDown.bottomOffset = CGPoint(x: 0, y:(dropDown.anchorView?.plainView.bounds.height)!)
+ dropDown.dataSource = options
+ dropDown.backgroundColor = .white
+
+ dropDown.selectionAction = { [unowned self] (index: Int, item: String) in
+ liveIndex.value = index
+ dropDown.selectRow(at: index)
+ //dropDown.tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
+ formattedLabel.text = " "+options[liveIndex.value!]
+ dropDown.hide()
+ }
+
+ onClick {
+ self.dropDown.show()
+ }
+
+ height(form_input_height).done()
+
+ liveIndex.readCurrentAndObserve { (value) in
+ self.dropDown.selectRow(value!)
+ }
+ isUserInteractionEnabled = !readOnly
+
+ }
+
+ func setIndex(index: Int) {
+ self.dropDown.selectRow(index)
+ formattedLabel.text = " "+options[index]
+ }
+
+}
diff --git a/Classes/Swift/Voip/Widgets/UICallTimer.swift b/Classes/Swift/Voip/Widgets/UICallTimer.swift
new file mode 100644
index 000000000..154062bbf
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/UICallTimer.swift
@@ -0,0 +1,78 @@
+/*
+ * 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 .
+ */
+
+import Foundation
+import linphonesw
+
+class CallTimer : StyledLabel {
+
+ let min_width = 50.0
+
+ let formatter = DateComponentsFormatter()
+ var call:Call? = nil {
+ didSet {
+ if (self.call != nil) {
+ self.format()
+ }
+ }
+ }
+
+ var conference:Conference? = nil {
+ didSet {
+ if (self.conference != nil) {
+ self.format()
+ }
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ init (_ text:String?, _ style:TextStyle, _ call:Call? = nil) {
+ super.init(style,text)
+ self.call = call
+ formatter.unitsStyle = .positional
+ formatter.allowedUnits = [.minute, .second ]
+ formatter.zeroFormattingBehavior = [ .pad ]
+ let startDate = Date()
+ Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
+ if (self.call != nil || self.conference != nil) {
+ self.format()
+ } else {
+ let elapsedTime = Date().timeIntervalSince(startDate)
+ self.formatter.string(from: elapsedTime).map {
+ self.text = $0.hasPrefix("0:") ? "0" + $0 : $0
+ }
+ }
+ }
+ minWidth(min_width).done()
+
+ }
+
+ func format() {
+ guard let duration = self.call != nil ? self.call!.duration : self.conference != nil ? self.conference!.duration: nil else {
+ return
+ }
+ formatter.string(from: TimeInterval(duration)).map {
+ self.text = $0.hasPrefix("0:") ? "0" + $0 : $0
+ }
+ }
+
+}
diff --git a/Classes/Swift/Voip/Widgets/VoipExtraButton.swift b/Classes/Swift/Voip/Widgets/VoipExtraButton.swift
new file mode 100644
index 000000000..67fce96c1
--- /dev/null
+++ b/Classes/Swift/Voip/Widgets/VoipExtraButton.swift
@@ -0,0 +1,75 @@
+/*
+ * 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 .
+ */
+
+
+import Foundation
+import UIKit
+import SwiftUI
+
+class VoipExtraButton : UIButton {
+
+ // Layout constants
+ let width = 60.0
+ let image_size = 50.0
+ let bouncing_label_size = 17.0
+
+ var boucingCounter : BouncingCounter? = nil
+
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+ }
+
+ init ( text:String, buttonTheme: ButtonTheme, withbBoucinCounterDataSource:MutableLiveData? = nil, onClickAction : @escaping ()->Void ) {
+ super.init(frame: .zero)
+
+
+ contentMode = .scaleToFill
+
+ buttonTheme.tintableStateIcons.keys.forEach { (stateRawValue) in
+ let tintedIcon = buttonTheme.tintableStateIcons[stateRawValue]!
+ UIImage(named:tintedIcon.name).map {
+ setImage($0.tinted(with: tintedIcon.tintColor?.get()),for: UIButton.State(rawValue: stateRawValue))
+ }
+ setTitleColor(tintedIcon.tintColor?.get(), for: UIButton.State(rawValue: stateRawValue))
+ }
+ imageView?.contentMode = .scaleAspectFit
+ imageView?.size(w: image_size,h: image_size).centerX().alignParentTop().done()
+ titleLabel?.alignUnder(view: imageView!).centerX().done()
+
+
+ size(w: width,h: image_size).done()
+ setTitle(text, for: .normal)
+ applyTitleStyle(VoipTheme.voip_extra_button)
+
+ onClick {
+ ControlsViewModel.shared.hideExtraButtons.value = true
+ onClickAction()
+ }
+
+ if (withbBoucinCounterDataSource != nil) {
+ boucingCounter = BouncingCounter(inButton:self)
+ addSubview(boucingCounter!)
+ boucingCounter?.dataSource = withbBoucinCounterDataSource
+ }
+
+ }
+
+
+}
diff --git a/Classes/Utils/AudioHelper.h b/Classes/Utils/AudioHelper.h
deleted file mode 100644
index fdf2845b1..000000000
--- a/Classes/Utils/AudioHelper.h
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * 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 .
- */
-
-#ifndef AudioHelper_h
-#define AudioHelper_h
-
-#import
-
-@import AVFoundation;
-
-@interface AudioHelper : NSObject
-
-+ (NSArray *)bluetoothRoutes;
-+ (AVAudioSessionPortDescription *)bluetoothAudioDevice;
-+ (AVAudioSessionPortDescription *)builtinAudioDevice;
-+ (AVAudioSessionPortDescription *)speakerAudioDevice;
-+ (AVAudioSessionPortDescription *)audioDeviceFromTypes:(NSArray *)types;
-@end
-
-#endif /* AudioHelper_h */
diff --git a/Classes/Utils/Log.m b/Classes/Utils/Log.m
index c7fe474c8..a156f66fb 100644
--- a/Classes/Utils/Log.m
+++ b/Classes/Utils/Log.m
@@ -67,6 +67,22 @@
bctbx_log(BCTBX_LOG_DOMAIN, level, "%s", [text cStringUsingEncoding:NSUTF8StringEncoding]);
}
++(void)d:(NSString *)text {
+ [Log directLog:BCTBX_LOG_DEBUG text:text];
+}
++(void)i:(NSString *)text {
+ [Log directLog:BCTBX_LOG_MESSAGE text:text];
+}
++(void)w:(NSString *)text {
+ [Log directLog:BCTBX_LOG_WARNING text:text];
+}
++(void)e:(NSString *)text {
+ [Log directLog:BCTBX_LOG_ERROR text:text];
+}
++(void)f:(NSString *)text {
+ [Log directLog:BCTBX_LOG_FATAL text:text];
+}
+
#pragma mark - Logs Functions callbacks
void linphone_iphone_log_handler(const char *domain, OrtpLogLevel lev, const char *fmt, va_list args) {
diff --git a/Classes/Utils/Utils.m b/Classes/Utils/Utils.m
index b9cbfaaa1..6a4d8e433 100644
--- a/Classes/Utils/Utils.m
+++ b/Classes/Utils/Utils.m
@@ -631,6 +631,13 @@
}
+ (void)setDisplayNameLabel:(UILabel *)label forAddress:(const LinphoneAddress *)addr {
+
+ const LinphoneConferenceInfo * ci = linphone_core_find_conference_information_from_uri(LC, (LinphoneAddress *)addr);
+ if (ci != nil) {
+ label.text = [NSString stringWithUTF8String:linphone_conference_info_get_subject(ci)];
+ return;
+ }
+
Contact *contact = [FastAddressBook getContactWithAddress:addr];
if (contact) {
[ContactDisplay setDisplayNameLabel:label forContact:contact];
@@ -640,6 +647,14 @@
}
+ (void)setDisplayNameLabel:(UILabel *)label forAddress:(const LinphoneAddress *)addr withAddressLabel:(UILabel*)addressLabel{
+
+ const LinphoneConferenceInfo * ci = linphone_core_find_conference_information_from_uri(LC, (LinphoneAddress *)addr);
+ if (ci != nil) {
+ label.text = [NSString stringWithUTF8String:linphone_conference_info_get_subject(ci)];
+ addressLabel.text = NSLocalizedString(@"Conference",nil);
+ return;
+ }
+
Contact *contact = [FastAddressBook getContactWithAddress:addr];
NSString *tmpAddress = nil;
char *uri = linphone_address_as_string_uri_only(addr);
diff --git a/Classes/ar.lproj/CallIncomingView.strings b/Classes/ar.lproj/CallIncomingView.strings
deleted file mode 100644
index c5adc5123..000000000
Binary files a/Classes/ar.lproj/CallIncomingView.strings and /dev/null differ
diff --git a/Classes/ar.lproj/CallView.strings b/Classes/ar.lproj/CallView.strings
deleted file mode 100644
index 6544e6570..000000000
Binary files a/Classes/ar.lproj/CallView.strings and /dev/null differ
diff --git a/Classes/fr.lproj/CallIncomingView.strings b/Classes/fr.lproj/CallIncomingView.strings
deleted file mode 100644
index b59db541e..000000000
Binary files a/Classes/fr.lproj/CallIncomingView.strings and /dev/null differ
diff --git a/Classes/fr.lproj/CallOutgoingView.strings b/Classes/fr.lproj/CallOutgoingView.strings
deleted file mode 100644
index f507fda06..000000000
Binary files a/Classes/fr.lproj/CallOutgoingView.strings and /dev/null differ
diff --git a/Classes/fr.lproj/CallView.strings b/Classes/fr.lproj/CallView.strings
deleted file mode 100644
index 8303fcfed..000000000
Binary files a/Classes/fr.lproj/CallView.strings and /dev/null differ
diff --git a/Classes/fr.lproj/CallView~ipad.strings b/Classes/fr.lproj/CallView~ipad.strings
deleted file mode 100644
index b4c093b13..000000000
Binary files a/Classes/fr.lproj/CallView~ipad.strings and /dev/null differ
diff --git a/Classes/hu.lproj/CallIncomingView.strings b/Classes/hu.lproj/CallIncomingView.strings
deleted file mode 100644
index de84a65e8..000000000
Binary files a/Classes/hu.lproj/CallIncomingView.strings and /dev/null differ
diff --git a/Classes/hu.lproj/CallOutgoingView.strings b/Classes/hu.lproj/CallOutgoingView.strings
deleted file mode 100644
index 912463c42..000000000
Binary files a/Classes/hu.lproj/CallOutgoingView.strings and /dev/null differ
diff --git a/Classes/hu.lproj/CallView.strings b/Classes/hu.lproj/CallView.strings
deleted file mode 100644
index 95effba3d..000000000
Binary files a/Classes/hu.lproj/CallView.strings and /dev/null differ
diff --git a/Classes/hu.lproj/CallView~ipad.strings b/Classes/hu.lproj/CallView~ipad.strings
deleted file mode 100644
index 3bd40d4d4..000000000
Binary files a/Classes/hu.lproj/CallView~ipad.strings and /dev/null differ
diff --git a/Classes/linphone-Bridging-Header.h b/Classes/linphone-Bridging-Header.h
index 5eaded494..1a5d236e9 100644
--- a/Classes/linphone-Bridging-Header.h
+++ b/Classes/linphone-Bridging-Header.h
@@ -5,7 +5,15 @@
#import
+#import "FastAddressBook.h"
#import "LinphoneManager.h"
#import "Log.h"
-#import "AudioHelper.h"
+#import "LinphoneUI/UICompositeView.h"
+#import "Contact.h"
+#import "StatusBarView.h"
+#import "LinphoneUI/UIBouncingView.h"
+#import "PhoneMainView.h"
+#import "UICamSwitch.h"
+#import "UIChatBubbleTextCell.h"
#import "ChatConversationTableView.h"
+
diff --git a/Classes/ru.lproj/CallIncomingView.strings b/Classes/ru.lproj/CallIncomingView.strings
deleted file mode 100644
index f433ea682..000000000
Binary files a/Classes/ru.lproj/CallIncomingView.strings and /dev/null differ
diff --git a/Classes/ru.lproj/CallView.strings b/Classes/ru.lproj/CallView.strings
deleted file mode 100644
index c3217f4ee..000000000
Binary files a/Classes/ru.lproj/CallView.strings and /dev/null differ
diff --git a/Resources/assistant_external_sip.rc b/Resources/assistant_external_sip.rc
index dd3090139..91365e833 100644
--- a/Resources/assistant_external_sip.rc
+++ b/Resources/assistant_external_sip.rc
@@ -11,6 +11,7 @@
1
+ 0
diff --git a/Resources/assistant_linphone_create.rc b/Resources/assistant_linphone_create.rc
index 4328453d5..603eaeffa 100644
--- a/Resources/assistant_linphone_create.rc
+++ b/Resources/assistant_linphone_create.rc
@@ -16,7 +16,9 @@
nat_policy_default_values
1
1
+ 1
sip:conference-factory@sip.linphone.org
+ sip:videoconference-factory2@sip.linphone.org
1
diff --git a/Resources/assistant_linphone_existing.rc b/Resources/assistant_linphone_existing.rc
index 8d4eb8f78..2a8539b08 100644
--- a/Resources/assistant_linphone_existing.rc
+++ b/Resources/assistant_linphone_existing.rc
@@ -16,7 +16,9 @@
nat_policy_default_values
1
1
+ 1
sip:conference-factory@sip.linphone.org
+ sip:videoconference-factory2@sip.linphone.org
diff --git a/Resources/fonts/Roboto-Bold.ttf b/Resources/fonts/Roboto-Bold.ttf
new file mode 100644
index 000000000..8d6cf0551
Binary files /dev/null and b/Resources/fonts/Roboto-Bold.ttf differ
diff --git a/Resources/fonts/Roboto-Italic.ttf b/Resources/fonts/Roboto-Italic.ttf
new file mode 100644
index 000000000..737244bc0
Binary files /dev/null and b/Resources/fonts/Roboto-Italic.ttf differ
diff --git a/Resources/fonts/Roboto-Regular.ttf b/Resources/fonts/Roboto-Regular.ttf
new file mode 100644
index 000000000..3a4973c78
Binary files /dev/null and b/Resources/fonts/Roboto-Regular.ttf differ
diff --git a/Resources/images/conference_schedule_calendar_default.png b/Resources/images/conference_schedule_calendar_default.png
new file mode 100644
index 000000000..59fe950fb
Binary files /dev/null and b/Resources/images/conference_schedule_calendar_default.png differ
diff --git a/Resources/images/conference_schedule_participants_default.png b/Resources/images/conference_schedule_participants_default.png
new file mode 100644
index 000000000..a58af55e1
Binary files /dev/null and b/Resources/images/conference_schedule_participants_default.png differ
diff --git a/Resources/images/conference_schedule_time_default.png b/Resources/images/conference_schedule_time_default.png
new file mode 100644
index 000000000..7a9534617
Binary files /dev/null and b/Resources/images/conference_schedule_time_default.png differ
diff --git a/Resources/images/voip_audio_routes.png b/Resources/images/voip_audio_routes.png
new file mode 100644
index 000000000..fae4e84e3
Binary files /dev/null and b/Resources/images/voip_audio_routes.png differ
diff --git a/Resources/images/voip_bluetooth.png b/Resources/images/voip_bluetooth.png
new file mode 100644
index 000000000..3010fb418
Binary files /dev/null and b/Resources/images/voip_bluetooth.png differ
diff --git a/Resources/images/voip_call.png b/Resources/images/voip_call.png
new file mode 100644
index 000000000..1a95c052e
Binary files /dev/null and b/Resources/images/voip_call.png differ
diff --git a/Resources/images/voip_call_add.png b/Resources/images/voip_call_add.png
new file mode 100644
index 000000000..f4d80e5f5
Binary files /dev/null and b/Resources/images/voip_call_add.png differ
diff --git a/Resources/images/voip_call_chat.png b/Resources/images/voip_call_chat.png
new file mode 100644
index 000000000..4ff01de6f
Binary files /dev/null and b/Resources/images/voip_call_chat.png differ
diff --git a/Resources/images/voip_call_forward.png b/Resources/images/voip_call_forward.png
new file mode 100644
index 000000000..46a319028
Binary files /dev/null and b/Resources/images/voip_call_forward.png differ
diff --git a/Resources/images/voip_call_header_active.png b/Resources/images/voip_call_header_active.png
new file mode 100644
index 000000000..3ef0106bc
Binary files /dev/null and b/Resources/images/voip_call_header_active.png differ
diff --git a/Resources/images/voip_call_header_incoming.png b/Resources/images/voip_call_header_incoming.png
new file mode 100644
index 000000000..7a8d458ad
Binary files /dev/null and b/Resources/images/voip_call_header_incoming.png differ
diff --git a/Resources/images/voip_call_header_outgoing.png b/Resources/images/voip_call_header_outgoing.png
new file mode 100644
index 000000000..474abe754
Binary files /dev/null and b/Resources/images/voip_call_header_outgoing.png differ
diff --git a/Resources/images/voip_call_header_paused.png b/Resources/images/voip_call_header_paused.png
new file mode 100644
index 000000000..fdcfaf5d2
Binary files /dev/null and b/Resources/images/voip_call_header_paused.png differ
diff --git a/Resources/images/voip_call_list_menu.png b/Resources/images/voip_call_list_menu.png
new file mode 100644
index 000000000..b4e2ff3bf
Binary files /dev/null and b/Resources/images/voip_call_list_menu.png differ
diff --git a/Resources/images/voip_call_more.png b/Resources/images/voip_call_more.png
new file mode 100644
index 000000000..73bb13b36
Binary files /dev/null and b/Resources/images/voip_call_more.png differ
diff --git a/Resources/images/voip_call_numpad.png b/Resources/images/voip_call_numpad.png
new file mode 100644
index 000000000..a0fd33835
Binary files /dev/null and b/Resources/images/voip_call_numpad.png differ
diff --git a/Resources/images/voip_call_participants.png b/Resources/images/voip_call_participants.png
new file mode 100644
index 000000000..b2b766b14
Binary files /dev/null and b/Resources/images/voip_call_participants.png differ
diff --git a/Resources/images/voip_call_record.png b/Resources/images/voip_call_record.png
new file mode 100644
index 000000000..9c392dc47
Binary files /dev/null and b/Resources/images/voip_call_record.png differ
diff --git a/Resources/images/voip_call_stats.png b/Resources/images/voip_call_stats.png
new file mode 100644
index 000000000..3dd39d43b
Binary files /dev/null and b/Resources/images/voip_call_stats.png differ
diff --git a/Resources/images/voip_calls_list.png b/Resources/images/voip_calls_list.png
new file mode 100644
index 000000000..a832384ca
Binary files /dev/null and b/Resources/images/voip_calls_list.png differ
diff --git a/Resources/images/voip_camera_off.png b/Resources/images/voip_camera_off.png
new file mode 100644
index 000000000..dd41c338b
Binary files /dev/null and b/Resources/images/voip_camera_off.png differ
diff --git a/Resources/images/voip_camera_on.png b/Resources/images/voip_camera_on.png
new file mode 100644
index 000000000..7109c577d
Binary files /dev/null and b/Resources/images/voip_camera_on.png differ
diff --git a/Resources/images/voip_cancel.png b/Resources/images/voip_cancel.png
new file mode 100644
index 000000000..493b35e79
Binary files /dev/null and b/Resources/images/voip_cancel.png differ
diff --git a/Resources/images/voip_change_camera.png b/Resources/images/voip_change_camera.png
new file mode 100644
index 000000000..d6dc15cb8
Binary files /dev/null and b/Resources/images/voip_change_camera.png differ
diff --git a/Resources/images/voip_chat_rooms_list.png b/Resources/images/voip_chat_rooms_list.png
new file mode 100644
index 000000000..edf722c7e
Binary files /dev/null and b/Resources/images/voip_chat_rooms_list.png differ
diff --git a/Resources/images/voip_checkbox_checked.png b/Resources/images/voip_checkbox_checked.png
new file mode 100644
index 000000000..042f73d9d
Binary files /dev/null and b/Resources/images/voip_checkbox_checked.png differ
diff --git a/Resources/images/voip_checkbox_unchecked.png b/Resources/images/voip_checkbox_unchecked.png
new file mode 100644
index 000000000..b01cbcb1e
Binary files /dev/null and b/Resources/images/voip_checkbox_unchecked.png differ
diff --git a/Resources/images/voip_conference_active_speaker.png b/Resources/images/voip_conference_active_speaker.png
new file mode 100644
index 000000000..18f21106a
Binary files /dev/null and b/Resources/images/voip_conference_active_speaker.png differ
diff --git a/Resources/images/voip_conference_audio_only.png b/Resources/images/voip_conference_audio_only.png
new file mode 100644
index 000000000..fd57a3f27
Binary files /dev/null and b/Resources/images/voip_conference_audio_only.png differ
diff --git a/Resources/images/voip_conference_mosaic.png b/Resources/images/voip_conference_mosaic.png
new file mode 100644
index 000000000..8fa0137b7
Binary files /dev/null and b/Resources/images/voip_conference_mosaic.png differ
diff --git a/Resources/images/voip_conference_new.png b/Resources/images/voip_conference_new.png
new file mode 100644
index 000000000..8985782ae
Binary files /dev/null and b/Resources/images/voip_conference_new.png differ
diff --git a/Resources/images/voip_conference_new_selected.png b/Resources/images/voip_conference_new_selected.png
new file mode 100644
index 000000000..54ea3e2b0
Binary files /dev/null and b/Resources/images/voip_conference_new_selected.png differ
diff --git a/Resources/images/voip_conference_paused_big.png b/Resources/images/voip_conference_paused_big.png
new file mode 100644
index 000000000..745f17220
Binary files /dev/null and b/Resources/images/voip_conference_paused_big.png differ
diff --git a/Resources/images/voip_conference_play_big.png b/Resources/images/voip_conference_play_big.png
new file mode 100644
index 000000000..303d05faa
Binary files /dev/null and b/Resources/images/voip_conference_play_big.png differ
diff --git a/Resources/images/voip_copy.png b/Resources/images/voip_copy.png
new file mode 100644
index 000000000..43639693e
Binary files /dev/null and b/Resources/images/voip_copy.png differ
diff --git a/Resources/images/voip_delete.png b/Resources/images/voip_delete.png
new file mode 100644
index 000000000..3022d156d
Binary files /dev/null and b/Resources/images/voip_delete.png differ
diff --git a/Resources/images/voip_dropdown.png b/Resources/images/voip_dropdown.png
new file mode 100644
index 000000000..d9fccac91
Binary files /dev/null and b/Resources/images/voip_dropdown.png differ
diff --git a/Resources/images/voip_earpiece.png b/Resources/images/voip_earpiece.png
new file mode 100644
index 000000000..e95a30801
Binary files /dev/null and b/Resources/images/voip_earpiece.png differ
diff --git a/Resources/images/voip_edit.png b/Resources/images/voip_edit.png
new file mode 100644
index 000000000..c24930212
Binary files /dev/null and b/Resources/images/voip_edit.png differ
diff --git a/Resources/images/voip_export.png b/Resources/images/voip_export.png
new file mode 100644
index 000000000..3fdfa078a
Binary files /dev/null and b/Resources/images/voip_export.png differ
diff --git a/Resources/images/voip_hangup.png b/Resources/images/voip_hangup.png
new file mode 100644
index 000000000..a2ceab5d8
Binary files /dev/null and b/Resources/images/voip_hangup.png differ
diff --git a/Resources/images/voip_info.png b/Resources/images/voip_info.png
new file mode 100644
index 000000000..ae8dd86f8
Binary files /dev/null and b/Resources/images/voip_info.png differ
diff --git a/Resources/images/voip_mandatory.png b/Resources/images/voip_mandatory.png
new file mode 100644
index 000000000..4be37f7e4
Binary files /dev/null and b/Resources/images/voip_mandatory.png differ
diff --git a/Resources/images/voip_menu_more.png b/Resources/images/voip_menu_more.png
new file mode 100644
index 000000000..7f7f7d8fd
Binary files /dev/null and b/Resources/images/voip_menu_more.png differ
diff --git a/Resources/images/voip_merge_calls.png b/Resources/images/voip_merge_calls.png
new file mode 100644
index 000000000..6c4da5988
Binary files /dev/null and b/Resources/images/voip_merge_calls.png differ
diff --git a/Resources/images/voip_micro_off.png b/Resources/images/voip_micro_off.png
new file mode 100644
index 000000000..57569b4f2
Binary files /dev/null and b/Resources/images/voip_micro_off.png differ
diff --git a/Resources/images/voip_micro_on.png b/Resources/images/voip_micro_on.png
new file mode 100644
index 000000000..6552d35d5
Binary files /dev/null and b/Resources/images/voip_micro_on.png differ
diff --git a/Resources/images/voip_multiple_contacts_avatar.png b/Resources/images/voip_multiple_contacts_avatar.png
new file mode 100644
index 000000000..78b10f11b
Binary files /dev/null and b/Resources/images/voip_multiple_contacts_avatar.png differ
diff --git a/Resources/images/voip_numpad_0.png b/Resources/images/voip_numpad_0.png
new file mode 100644
index 000000000..115bdb17d
Binary files /dev/null and b/Resources/images/voip_numpad_0.png differ
diff --git a/Resources/images/voip_numpad_1.png b/Resources/images/voip_numpad_1.png
new file mode 100644
index 000000000..4d8b7f5cc
Binary files /dev/null and b/Resources/images/voip_numpad_1.png differ
diff --git a/Resources/images/voip_numpad_2.png b/Resources/images/voip_numpad_2.png
new file mode 100644
index 000000000..6b561c468
Binary files /dev/null and b/Resources/images/voip_numpad_2.png differ
diff --git a/Resources/images/voip_numpad_3.png b/Resources/images/voip_numpad_3.png
new file mode 100644
index 000000000..386715586
Binary files /dev/null and b/Resources/images/voip_numpad_3.png differ
diff --git a/Resources/images/voip_numpad_4.png b/Resources/images/voip_numpad_4.png
new file mode 100644
index 000000000..e3dfdcc51
Binary files /dev/null and b/Resources/images/voip_numpad_4.png differ
diff --git a/Resources/images/voip_numpad_5.png b/Resources/images/voip_numpad_5.png
new file mode 100644
index 000000000..a18af28e5
Binary files /dev/null and b/Resources/images/voip_numpad_5.png differ
diff --git a/Resources/images/voip_numpad_6.png b/Resources/images/voip_numpad_6.png
new file mode 100644
index 000000000..79279cb99
Binary files /dev/null and b/Resources/images/voip_numpad_6.png differ
diff --git a/Resources/images/voip_numpad_7.png b/Resources/images/voip_numpad_7.png
new file mode 100644
index 000000000..c68656fd3
Binary files /dev/null and b/Resources/images/voip_numpad_7.png differ
diff --git a/Resources/images/voip_numpad_8.png b/Resources/images/voip_numpad_8.png
new file mode 100644
index 000000000..8d84c96ba
Binary files /dev/null and b/Resources/images/voip_numpad_8.png differ
diff --git a/Resources/images/voip_numpad_9.png b/Resources/images/voip_numpad_9.png
new file mode 100644
index 000000000..af3e0e0bf
Binary files /dev/null and b/Resources/images/voip_numpad_9.png differ
diff --git a/Resources/images/voip_numpad_hash.png b/Resources/images/voip_numpad_hash.png
new file mode 100644
index 000000000..790e7d12e
Binary files /dev/null and b/Resources/images/voip_numpad_hash.png differ
diff --git a/Resources/images/voip_numpad_star.png b/Resources/images/voip_numpad_star.png
new file mode 100644
index 000000000..5a2649de4
Binary files /dev/null and b/Resources/images/voip_numpad_star.png differ
diff --git a/Resources/images/voip_pause.png b/Resources/images/voip_pause.png
new file mode 100644
index 000000000..e888da937
Binary files /dev/null and b/Resources/images/voip_pause.png differ
diff --git a/Resources/images/voip_radio_off.png b/Resources/images/voip_radio_off.png
new file mode 100644
index 000000000..b703dea80
Binary files /dev/null and b/Resources/images/voip_radio_off.png differ
diff --git a/Resources/images/voip_radio_on.png b/Resources/images/voip_radio_on.png
new file mode 100644
index 000000000..feaf00e11
Binary files /dev/null and b/Resources/images/voip_radio_on.png differ
diff --git a/Resources/images/voip_remote_recording.png b/Resources/images/voip_remote_recording.png
new file mode 100644
index 000000000..3e4e94009
Binary files /dev/null and b/Resources/images/voip_remote_recording.png differ
diff --git a/Resources/images/voip_single_contact_avatar.png b/Resources/images/voip_single_contact_avatar.png
new file mode 100644
index 000000000..5d158fa49
Binary files /dev/null and b/Resources/images/voip_single_contact_avatar.png differ
diff --git a/Resources/images/voip_speaker_off.png b/Resources/images/voip_speaker_off.png
new file mode 100644
index 000000000..c018c9899
Binary files /dev/null and b/Resources/images/voip_speaker_off.png differ
diff --git a/Resources/images/voip_speaker_on.png b/Resources/images/voip_speaker_on.png
new file mode 100644
index 000000000..07f13c9b6
Binary files /dev/null and b/Resources/images/voip_speaker_on.png differ
diff --git a/Resources/images/voip_spinner.png b/Resources/images/voip_spinner.png
new file mode 100644
index 000000000..8238de56b
Binary files /dev/null and b/Resources/images/voip_spinner.png differ
diff --git a/Resources/linphonerc b/Resources/linphonerc
index 59e9aeda6..89d9ad73f 100644
--- a/Resources/linphonerc
+++ b/Resources/linphonerc
@@ -52,6 +52,7 @@ reg_expires=1314000
file_transfer_server_url=https://www.linphone.org:444/lft.php
real_early_media=1
prefer_basic_chat_room=-1
+conference_layout=1
[net]
edge_bw=10
diff --git a/Resources/linphonerc-factory b/Resources/linphonerc-factory
index b94c8e21f..fde4f8242 100644
--- a/Resources/linphonerc-factory
+++ b/Resources/linphonerc-factory
@@ -36,6 +36,7 @@ linphone_specs=groupchat,lime
version_check_url_root=https://linphone.org/releases
prefer_basic_chat_room=-1
+
[sound]
eq_location=mic
diff --git a/Settings/InAppSettings.bundle/Call.plist b/Settings/InAppSettings.bundle/Call.plist
index ff9d16866..2ce526c4a 100644
--- a/Settings/InAppSettings.bundle/Call.plist
+++ b/Settings/InAppSettings.bundle/Call.plist
@@ -14,6 +14,16 @@
DefaultValue
+
+ Type
+ PSToggleSwitchSpecifier
+ Title
+ Notify and get notified when call is recorded
+ Key
+ record_aware
+ DefaultValue
+
+
Type
PSToggleSwitchSpecifier
diff --git a/linphone-Info.plist b/linphone-Info.plist
index ecba6e556..b9d24c1fb 100644
--- a/linphone-Info.plist
+++ b/linphone-Info.plist
@@ -126,6 +126,12 @@
NSVoIPUsageDescription
Make audio/video calls
+ UIAppFonts
+
+ Roboto-Regular.ttf
+ Roboto-Bold.ttf
+ Roboto-Italic.ttf
+
UIApplicationShortcutItems
diff --git a/linphone.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj
index ad0ea7056..59cbd12b6 100644
--- a/linphone.xcodeproj/project.pbxproj
+++ b/linphone.xcodeproj/project.pbxproj
@@ -13,8 +13,6 @@
1D60589F0D05DD5A006BFB54 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D30AB110D05D00D00671497 /* Foundation.framework */; };
1DF5F4E00D08C38300B7A737 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1DF5F4DF0D08C38300B7A737 /* UIKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
2214EB7A12F846B1002A5394 /* UICallButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 2214EB7912F846B1002A5394 /* UICallButton.m */; };
- 2214EB8912F84EBB002A5394 /* UIHangUpButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 2214EB8812F84EBB002A5394 /* UIHangUpButton.m */; };
- 2214EBF312F86360002A5394 /* UIMutedMicroButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 2214EBF212F86360002A5394 /* UIMutedMicroButton.m */; };
22276E8913C73DC000210156 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22276E8813C73DC000210156 /* CoreMedia.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
22405EEE1600B4E400B92522 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22405EED1600B4E400B92522 /* AssetsLibrary.framework */; };
22405F001601C19200B92522 /* ImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 22405EFE1601C19100B92522 /* ImageView.m */; };
@@ -25,9 +23,7 @@
2274401A106F31BD006EC466 /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22744019106F31BD006EC466 /* CoreAudio.framework */; };
2274402F106F335E006EC466 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2274402E106F335E006EC466 /* AudioToolbox.framework */; };
228697C411AC29B800E9E0CA /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 228697C311AC29B800E9E0CA /* CFNetwork.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
- 22968A5F12F875C600588287 /* UISpeakerButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 22968A5E12F875C600588287 /* UISpeakerButton.m */; };
22AA8B0113D83F6300B30535 /* UICamSwitch.m in Sources */ = {isa = PBXBuildFile; fileRef = 22AA8B0013D83F6300B30535 /* UICamSwitch.m */; };
- 22C755601317E59C007BC101 /* UIBluetoothButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 22C7555F1317E59C007BC101 /* UIBluetoothButton.m */; };
22D1B68112A3E0BE001AE361 /* libresolv.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 22D1B68012A3E0BE001AE361 /* libresolv.dylib */; };
22E0A822111C44E100B04932 /* AboutView.m in Sources */ = {isa = PBXBuildFile; fileRef = 22E0A81C111C44E100B04932 /* AboutView.m */; };
22F2508E107141E100AC9B3F /* DialerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 22F2508C107141E100AC9B3F /* DialerView.m */; };
@@ -55,19 +51,14 @@
308C3FD5D6C427D5592A2CD6 /* Pods_msgNotificationContent.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2AB0AB106BE1526DC105F515 /* Pods_msgNotificationContent.framework */; };
340751971506459A00B89C47 /* CoreTelephony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 340751961506459A00B89C47 /* CoreTelephony.framework */; };
340751E7150F38FD00B89C47 /* UIVideoButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 340751E6150F38FD00B89C47 /* UIVideoButton.m */; };
- 34216F401547EBCD00EA9777 /* VideoZoomHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 34216F3F1547EBCD00EA9777 /* VideoZoomHandler.m */; };
344ABDF114850AE9007420B6 /* libc++.1.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 344ABDEF14850AE9007420B6 /* libc++.1.dylib */; settings = {ATTRIBUTES = (Weak, ); }; };
570742581D5A0691004B9C84 /* ShopView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 570742561D5A0691004B9C84 /* ShopView.xib */; };
570742611D5A09B8004B9C84 /* ShopView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5707425F1D5A09B8004B9C84 /* ShopView.m */; };
570742671D5A63DB004B9C84 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 570742661D5A63DB004B9C84 /* StoreKit.framework */; };
6112A01C243B31A700DBD5F5 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 6112A01B243B31A600DBD5F5 /* GoogleService-Info.plist */; };
6112A01E243B5FD500DBD5F5 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 6112A01D243B5FD500DBD5F5 /* GoogleService-Info.plist */; };
- 6134812D2406CECC00695B41 /* ConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6134812C2406CECC00695B41 /* ConfigManager.swift */; };
- 6134812F2407B35200695B41 /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6134812E2407B35200695B41 /* AppManager.swift */; };
6135761C240E81BB005304D4 /* UIInterfaceStyleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 6135761B240E81BA005304D4 /* UIInterfaceStyleButton.m */; };
6135761F240E81D0005304D4 /* UIInterfaceStyleToggleButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 6135761E240E81D0005304D4 /* UIInterfaceStyleToggleButton.m */; };
- 614C087823D1A35F00217F80 /* ProviderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614C087723D1A35F00217F80 /* ProviderDelegate.swift */; };
- 614C087A23D1A37400217F80 /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614C087923D1A37400217F80 /* CallManager.swift */; };
614D09CE21E74D5400C43EDF /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 614D09CD21E74D5400C43EDF /* GoogleService-Info.plist */; };
61586B81217A16EE0038AC45 /* menu_about.png in Resources */ = {isa = PBXBuildFile; fileRef = 61586B7A217A16EE0038AC45 /* menu_about.png */; };
61586B83217A16FD0038AC45 /* menu_about@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 61586B82217A16FD0038AC45 /* menu_about@2x.png */; };
@@ -562,11 +553,7 @@
6341807C1BBC103100F71761 /* ChatConversationCreateTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6341807B1BBC103100F71761 /* ChatConversationCreateTableView.m */; };
63423C0A1C4501D000D9A050 /* Contact.m in Sources */ = {isa = PBXBuildFile; fileRef = 63423C091C4501D000D9A050 /* Contact.m */; };
634610061B61330300548952 /* UILabel+Boldify.m in Sources */ = {isa = PBXBuildFile; fileRef = 634610051B61330300548952 /* UILabel+Boldify.m */; };
- 6346100F1B61409800548952 /* CallOutgoingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6346100E1B61409800548952 /* CallOutgoingView.m */; };
- 634610121B6140A500548952 /* CallOutgoingView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 634610101B6140A500548952 /* CallOutgoingView.xib */; };
635173F91BA082A40095EB0A /* UIChatBubblePhotoCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 635173F81BA082A40095EB0A /* UIChatBubblePhotoCell.m */; };
- 6352A5751BE0D4B800594C1C /* CallSideMenuView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6352A5731BE0D4B800594C1C /* CallSideMenuView.m */; };
- 6352A5761BE0D4B800594C1C /* CallSideMenuView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6352A5741BE0D4B800594C1C /* CallSideMenuView.xib */; };
635775251B6673EC00C8B704 /* HistoryDetailsTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 635775241B6673EC00C8B704 /* HistoryDetailsTableView.m */; };
636316D11A1DEBCB0009B839 /* AboutView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 636316D31A1DEBCB0009B839 /* AboutView.xib */; };
636316D41A1DEC650009B839 /* SettingsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 636316D61A1DEC650009B839 /* SettingsView.xib */; };
@@ -576,7 +563,6 @@
6377AC801BDE4069007F7625 /* UIBackToCallButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 6377AC7F1BDE4069007F7625 /* UIBackToCallButton.m */; };
6381DA7D1C1AD5EA00DF3BBD /* UIBouncingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6381DA7C1C1AD5EA00DF3BBD /* UIBouncingView.m */; };
638F1A621C2021B2004B8E02 /* DialerView~ipad.xib in Resources */ = {isa = PBXBuildFile; fileRef = 638F1A601C2021B2004B8E02 /* DialerView~ipad.xib */; };
- 638F1A881C2167C2004B8E02 /* CallView~ipad.xib in Resources */ = {isa = PBXBuildFile; fileRef = 638F1A861C2167C2004B8E02 /* CallView~ipad.xib */; };
638F1A911C21993D004B8E02 /* UICompositeView~ipad.xib in Resources */ = {isa = PBXBuildFile; fileRef = 638F1A8F1C21993D004B8E02 /* UICompositeView~ipad.xib */; };
639CEAFD1A1DF4D9004DE38F /* StatusBarView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 639CEAFF1A1DF4D9004DE38F /* StatusBarView.xib */; };
639CEB001A1DF4E4004DE38F /* UIHistoryCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 639CEB021A1DF4E4004DE38F /* UIHistoryCell.xib */; };
@@ -584,7 +570,6 @@
639CEB091A1DF4FA004DE38F /* UIChatCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 639CEB0B1A1DF4FA004DE38F /* UIChatCell.xib */; };
639E9C801C0DB13D00019A75 /* UICheckBoxTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 639E9C7F1C0DB13D00019A75 /* UICheckBoxTableView.m */; };
639E9C931C0DB7BE00019A75 /* FirstLoginView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 639E9C951C0DB7BE00019A75 /* FirstLoginView.xib */; };
- 639E9C9D1C0DB7DF00019A75 /* UICallPausedCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 639E9C9F1C0DB7DF00019A75 /* UICallPausedCell.xib */; };
639E9CA01C0DB7E500019A75 /* UIChatBubblePhotoCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 639E9CA21C0DB7E500019A75 /* UIChatBubblePhotoCell.xib */; };
639E9CA31C0DB7EA00019A75 /* UIChatBubbleTextCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 639E9CA51C0DB7EA00019A75 /* UIChatBubbleTextCell.xib */; };
639E9CA61C0DB7F200019A75 /* UIChatCreateCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 639E9CA81C0DB7F200019A75 /* UIChatCreateCell.xib */; };
@@ -608,7 +593,6 @@
63B81A101B57DA33009604A6 /* UIScrollView+TPKeyboardAvoidingAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 63B81A0B1B57DA33009604A6 /* UIScrollView+TPKeyboardAvoidingAdditions.m */; };
63B8D68C1BCBE65600C12B09 /* ChatConversationCreateView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 63B8D68E1BCBE65600C12B09 /* ChatConversationCreateView.xib */; };
63B8D6A21BCBF43100C12B09 /* UIChatCreateCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 63B8D6A01BCBF43100C12B09 /* UIChatCreateCell.m */; };
- 63BC49E21BA2CDFC004EC273 /* UICallPausedCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 63BC49E11BA2CDFC004EC273 /* UICallPausedCell.m */; };
63BE7A781D75BDF6000990EF /* ShopTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 63BE7A771D75BDF6000990EF /* ShopTableView.m */; };
63C441C31BBC23ED0053DC5E /* UIAssistantTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 63C441C21BBC23ED0053DC5E /* UIAssistantTextField.m */; };
63CD4B4F1A5AAC8C00B84282 /* DTAlertView.m in Sources */ = {isa = PBXBuildFile; fileRef = 63CD4B4E1A5AAC8C00B84282 /* DTAlertView.m */; };
@@ -624,19 +608,15 @@
63E802DB1C625AEF000D5509 /* (null) in Resources */ = {isa = PBXBuildFile; };
63EC8D391D7438660066547B /* AssistantLinkView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 63EC8D3B1D7438660066547B /* AssistantLinkView.xib */; };
63F1DF441BCE618E00EDED90 /* UIAddressTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 63F1DF431BCE618E00EDED90 /* UIAddressTextField.m */; };
- 63F1DF4B1BCE983200EDED90 /* CallConferenceTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 63F1DF4A1BCE983200EDED90 /* CallConferenceTableView.m */; };
- 63F1DF4F1BCE985F00EDED90 /* UICallConferenceCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 63F1DF4D1BCE985F00EDED90 /* UICallConferenceCell.m */; };
- 63F1DF511BCE986A00EDED90 /* UICallConferenceCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 63F1DF531BCE986A00EDED90 /* UICallConferenceCell.xib */; };
63FB30351A680E73008CA393 /* UIRoundedImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 63FB30341A680E73008CA393 /* UIRoundedImageView.m */; };
662553B427EDFB35007F67D8 /* MagicSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662553B327EDFB35007F67D8 /* MagicSearch.swift */; };
669B140827A1821F0012220A /* scroll_to_bottom_default.png in Resources */ = {isa = PBXBuildFile; fileRef = 669B140727A1821F0012220A /* scroll_to_bottom_default.png */; };
669B140C27A29D140012220A /* FloatingScrollDownButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 669B140B27A29D140012220A /* FloatingScrollDownButton.swift */; };
- 6F3A2542B1FC7C128439D37C /* Pods_linphone.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CFCC14A580A05DEC78090273 /* Pods_linphone.framework */; };
66E399F72857869300E73456 /* menu_notifications_off.png in Resources */ = {isa = PBXBuildFile; fileRef = 66E399F52857869200E73456 /* menu_notifications_off.png */; };
66E399F82857869300E73456 /* menu_notifications_on.png in Resources */ = {isa = PBXBuildFile; fileRef = 66E399F62857869200E73456 /* menu_notifications_on.png */; };
+ 6F3A2542B1FC7C128439D37C /* Pods_linphone.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CFCC14A580A05DEC78090273 /* Pods_linphone.framework */; };
70E542F313E147E3002BA2C0 /* OpenGLES.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70E542F213E147E3002BA2C0 /* OpenGLES.framework */; };
70E542F513E147EB002BA2C0 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70E542F413E147EB002BA2C0 /* QuartzCore.framework */; };
- 8C1B67061E671826001EA2FE /* AudioHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C1B67051E671826001EA2FE /* AudioHelper.m */; };
8C2595DF1DEDCC8E007A6424 /* CallKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8C2595DE1DEDCC8E007A6424 /* CallKit.framework */; };
8C2A81951F87B8000012A66B /* chat_group_avatar@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8C2A81931F87B7FF0012A66B /* chat_group_avatar@2x.png */; };
8C2A81961F87B8000012A66B /* chat_group_avatar.png in Resources */ = {isa = PBXBuildFile; fileRef = 8C2A81941F87B8000012A66B /* chat_group_avatar.png */; };
@@ -685,11 +665,179 @@
C622E3F226A81290004F5434 /* vr_off.png in Resources */ = {isa = PBXBuildFile; fileRef = C622E3EC26A8128F004F5434 /* vr_off.png */; };
C622E3F326A81290004F5434 /* vr_pause.png in Resources */ = {isa = PBXBuildFile; fileRef = C622E3ED26A8128F004F5434 /* vr_pause.png */; };
C622E3F426A81290004F5434 /* vr_play.png in Resources */ = {isa = PBXBuildFile; fileRef = C622E3EE26A81290004F5434 /* vr_play.png */; };
+ C63F720D285A24B10066163B /* ConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F7197285A24B10066163B /* ConfigManager.swift */; };
+ C63F720E285A24B10066163B /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F7198285A24B10066163B /* CallManager.swift */; };
+ C63F720F285A24B10066163B /* ConferenceWaitingRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F719B285A24B10066163B /* ConferenceWaitingRoomViewModel.swift */; };
+ C63F7210285A24B10066163B /* ConferenceSchedulingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F719C285A24B10066163B /* ConferenceSchedulingViewModel.swift */; };
+ C63F7211285A24B10066163B /* ScheduledConferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F719D285A24B10066163B /* ScheduledConferencesViewModel.swift */; };
+ C63F7212285A24B10066163B /* ScheduledConferenceData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F719F285A24B10066163B /* ScheduledConferenceData.swift */; };
+ C63F7213285A24B10066163B /* TimeZoneData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71A0285A24B10066163B /* TimeZoneData.swift */; };
+ C63F7214285A24B10066163B /* Duration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71A1285A24B10066163B /* Duration.swift */; };
+ C63F7215285A24B10066163B /* ConferenceWaitingRoomFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71A3285A24B10066163B /* ConferenceWaitingRoomFragment.swift */; };
+ C63F7216285A24B10066163B /* ScheduledConferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71A4285A24B10066163B /* ScheduledConferencesView.swift */; };
+ C63F7217285A24B10066163B /* ICSBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71A5285A24B10066163B /* ICSBubbleView.swift */; };
+ C63F7218285A24B10066163B /* ScheduledConferencesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71A6285A24B10066163B /* ScheduledConferencesCell.swift */; };
+ C63F7219285A24B10066163B /* ConferenceHistoryDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71A7285A24B10066163B /* ConferenceHistoryDetailsView.swift */; };
+ C63F721A285A24B10066163B /* ConferenceSchedulingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71A8285A24B10066163B /* ConferenceSchedulingView.swift */; };
+ C63F721B285A24B10066163B /* ConferenceSchedulingSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71A9285A24B10066163B /* ConferenceSchedulingSummaryView.swift */; };
+ C63F721C285A24B10066163B /* MediatorLiveData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71AC285A24B10066163B /* MediatorLiveData.swift */; };
+ C63F721D285A24B10066163B /* MutableLiveData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71AD285A24B10066163B /* MutableLiveData.swift */; };
+ C63F721E285A24B10066163B /* Pair.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71AE285A24B10066163B /* Pair.swift */; };
+ C63F721F285A24B10066163B /* BackNextNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71AF285A24B10066163B /* BackNextNavigationView.swift */; };
+ C63F7220285A24B10066163B /* TimestampUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71B0285A24B10066163B /* TimestampUtils.swift */; };
+ C63F7221285A24B10066163B /* AppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71B1285A24B10066163B /* AppManager.swift */; };
+ C63F7222285A24B10066163B /* UIApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71B4285A24B10066163B /* UIApplication+Extension.swift */; };
+ C63F7224285A24B10066163B /* UIVIewControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71B6285A24B10066163B /* UIVIewControllerExtensions.swift */; };
+ C63F7225285A24B10066163B /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71B7285A24B10066163B /* UIImageExtensions.swift */; };
+ C63F7226285A24B10066163B /* UIVIewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71B8285A24B10066163B /* UIVIewExtensions.swift */; };
+ C63F7227285A24B10066163B /* UILabelExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71B9285A24B10066163B /* UILabelExtensions.swift */; };
+ C63F7228285A24B10066163B /* OptionalExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71BA285A24B10066163B /* OptionalExtensions.swift */; };
+ C63F7229285A24B10066163B /* UIButtonExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71BB285A24B10066163B /* UIButtonExtensions.swift */; };
+ C63F722A285A24B10066163B /* UIImageViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71BC285A24B10066163B /* UIImageViewExtensions.swift */; };
+ C63F722B285A24B10066163B /* UIDeviceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71BD285A24B10066163B /* UIDeviceExtensions.swift */; };
+ C63F722C285A24B10066163B /* UIColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71BE285A24B10066163B /* UIColorExtensions.swift */; };
+ C63F722D285A24B10066163B /* CoreExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71C0285A24B10066163B /* CoreExtensions.swift */; };
+ C63F722E285A24B10066163B /* IceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71C1285A24B10066163B /* IceState.swift */; };
+ C63F722F285A24B10066163B /* AddressExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71C2285A24B10066163B /* AddressExtensions.swift */; };
+ C63F7230285A24B10066163B /* ParticipantExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71C3285A24B10066163B /* ParticipantExtensions.swift */; };
+ C63F7231285A24B10066163B /* PayloadType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71C4285A24B10066163B /* PayloadType.swift */; };
+ C63F7232285A24B10066163B /* CallExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71C5285A24B10066163B /* CallExtensions.swift */; };
+ C63F7233285A24B10066163B /* ConferenceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71C6285A24B10066163B /* ConferenceExtensions.swift */; };
+ C63F7234285A24B10066163B /* ConferenceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71C9285A24B10066163B /* ConferenceViewModel.swift */; };
+ C63F7235285A24B10066163B /* CallsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71CA285A24B10066163B /* CallsViewModel.swift */; };
+ C63F7236285A24B10066163B /* ControlsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71CB285A24B10066163B /* ControlsViewModel.swift */; };
+ C63F7237285A24B10066163B /* CallStatisticsData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71CC285A24B10066163B /* CallStatisticsData.swift */; };
+ C63F7238285A24B10066163B /* ConferenceParticipantData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71CD285A24B10066163B /* ConferenceParticipantData.swift */; };
+ C63F7239285A24B10066163B /* ConferenceParticipantDeviceData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71CE285A24B10066163B /* ConferenceParticipantDeviceData.swift */; };
+ C63F723A285A24B10066163B /* CallData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71CF285A24B10066163B /* CallData.swift */; };
+ C63F723B285A24B10066163B /* AudioRouteUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71D0285A24B10066163B /* AudioRouteUtils.swift */; };
+ C63F723C285A24B10066163B /* LightDarkColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71D2285A24B10066163B /* LightDarkColor.swift */; };
+ C63F723D285A24B10066163B /* TextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71D3285A24B10066163B /* TextStyle.swift */; };
+ C63F723E285A24B10066163B /* VoipTexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71D4285A24B10066163B /* VoipTexts.swift */; };
+ C63F723F285A24B10066163B /* ButtonTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71D5285A24B10066163B /* ButtonTheme.swift */; };
+ C63F7240285A24B10066163B /* VoipTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71D6285A24B10066163B /* VoipTheme.swift */; };
+ C63F7241285A24B10066163B /* ParticipantsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71DA285A24B10066163B /* ParticipantsListView.swift */; };
+ C63F7242285A24B10066163B /* VoipParticipantCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71DB285A24B10066163B /* VoipParticipantCell.swift */; };
+ C63F7243285A24B10066163B /* AudioRoutesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71DC285A24B10066163B /* AudioRoutesView.swift */; };
+ C63F7244285A24B10066163B /* VoipActiveSpeakerParticipantCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71DE285A24B10066163B /* VoipActiveSpeakerParticipantCell.swift */; };
+ C63F7245285A24B10066163B /* VoipConferenceAudioOnlyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71DF285A24B10066163B /* VoipConferenceAudioOnlyView.swift */; };
+ C63F7246285A24B10066163B /* VoipGridParticipantCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71E0285A24B10066163B /* VoipGridParticipantCell.swift */; };
+ C63F7247285A24B10066163B /* VoipAudioOnlyParticipantCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71E1285A24B10066163B /* VoipAudioOnlyParticipantCell.swift */; };
+ C63F7248285A24B10066163B /* MicMuted.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71E2285A24B10066163B /* MicMuted.swift */; };
+ C63F7249285A24B10066163B /* VoipConferenceGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71E3285A24B10066163B /* VoipConferenceGridView.swift */; };
+ C63F724A285A24B10066163B /* VoipConferenceActiveSpeakerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71E4285A24B10066163B /* VoipConferenceActiveSpeakerView.swift */; };
+ C63F724B285A24B10066163B /* VoipConferenceDisplayModeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71E5285A24B10066163B /* VoipConferenceDisplayModeSelectionView.swift */; };
+ C63F724C285A24B10066163B /* ActiveCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71E7285A24B10066163B /* ActiveCallView.swift */; };
+ C63F724D285A24B10066163B /* IncomingOuntgoingCommonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71E8285A24B10066163B /* IncomingOuntgoingCommonView.swift */; };
+ C63F724E285A24B10066163B /* PausedCallOrConferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71E9285A24B10066163B /* PausedCallOrConferenceView.swift */; };
+ C63F724F285A24B10066163B /* LocalVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71EA285A24B10066163B /* LocalVideoView.swift */; };
+ C63F7250285A24B10066163B /* CallStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71EB285A24B10066163B /* CallStatsView.swift */; };
+ C63F7251285A24B10066163B /* NumpadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71EC285A24B10066163B /* NumpadView.swift */; };
+ C63F7252285A24B10066163B /* VoipExtraButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71ED285A24B10066163B /* VoipExtraButtonsView.swift */; };
+ C63F7253285A24B10066163B /* VoipCallContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71EF285A24B10066163B /* VoipCallContextMenu.swift */; };
+ C63F7254285A24B10066163B /* CallsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71F0285A24B10066163B /* CallsListView.swift */; };
+ C63F7255285A24B10066163B /* VoipCallCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71F1285A24B10066163B /* VoipCallCell.swift */; };
+ C63F7256285A24B10066163B /* DismissableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71F2285A24B10066163B /* DismissableView.swift */; };
+ C63F7257285A24B10066163B /* ConferenceLayoutPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71F3285A24B10066163B /* ConferenceLayoutPickerView.swift */; };
+ C63F7258285A24B10066163B /* ControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71F4285A24B10066163B /* ControlsView.swift */; };
+ C63F7259285A24B10066163B /* RemotelyRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71F5285A24B10066163B /* RemotelyRecording.swift */; };
+ C63F725A285A24B10066163B /* OutgoingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71F7285A24B10066163B /* OutgoingCallView.swift */; };
+ C63F725B285A24B10066163B /* ActiveCallOrConferenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71F8285A24B10066163B /* ActiveCallOrConferenceView.swift */; };
+ C63F725C285A24B10066163B /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71F9285A24B10066163B /* IncomingCallView.swift */; };
+ C63F725D285A24B10066163B /* SharedLayoutConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71FA285A24B10066163B /* SharedLayoutConstants.swift */; };
+ C63F725E285A24B10066163B /* VoipDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71FB285A24B10066163B /* VoipDialog.swift */; };
+ C63F725F285A24B10066163B /* StyledValuePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71FD285A24B10066163B /* StyledValuePicker.swift */; };
+ C63F7260285A24B10066163B /* StyledSwitch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71FE285A24B10066163B /* StyledSwitch.swift */; };
+ C63F7261285A24B10066163B /* CallControlButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F71FF285A24B10066163B /* CallControlButton.swift */; };
+ C63F7262285A24B10066163B /* RotatingSpinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F7200285A24B10066163B /* RotatingSpinner.swift */; };
+ C63F7263285A24B10066163B /* FormButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F7201285A24B10066163B /* FormButton.swift */; };
+ C63F7264285A24B10066163B /* BouncingCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F7202285A24B10066163B /* BouncingCounter.swift */; };
+ C63F7265285A24B10066163B /* VoipExtraButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F7203285A24B10066163B /* VoipExtraButton.swift */; };
+ C63F7266285A24B10066163B /* UICallTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F7204285A24B10066163B /* UICallTimer.swift */; };
+ C63F7267285A24B10066163B /* StyledCheckBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F7205285A24B10066163B /* StyledCheckBox.swift */; };
+ C63F7268285A24B10066163B /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F7206285A24B10066163B /* Avatar.swift */; };
+ C63F7269285A24B10066163B /* StyledLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F7207285A24B10066163B /* StyledLabel.swift */; };
+ C63F726A285A24B10066163B /* StyledDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F7208285A24B10066163B /* StyledDatePicker.swift */; };
+ C63F726B285A24B10066163B /* ButtonWithStateBackgrounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F7209285A24B10066163B /* ButtonWithStateBackgrounds.swift */; };
+ C63F726C285A24B10066163B /* StyledTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F720A285A24B10066163B /* StyledTextView.swift */; };
+ C63F726D285A24B10066163B /* ProviderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F720B285A24B10066163B /* ProviderDelegate.swift */; };
+ C63F726E285A24B10066163B /* VFSUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F720C285A24B10066163B /* VFSUtil.swift */; };
+ C63F726F285A24E90066163B /* VFSUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F720C285A24B10066163B /* VFSUtil.swift */; };
+ C63F7270285A24E90066163B /* VFSUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F720C285A24B10066163B /* VFSUtil.swift */; };
+ C63F72B5285A2F1D0066163B /* voip_dropdown.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7271285A2F140066163B /* voip_dropdown.png */; };
+ C63F72B6285A2F1D0066163B /* voip_spinner.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7272285A2F140066163B /* voip_spinner.png */; };
+ C63F72B7285A2F1D0066163B /* voip_numpad_7.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7273285A2F140066163B /* voip_numpad_7.png */; };
+ C63F72B8285A2F1D0066163B /* voip_radio_off.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7274285A2F150066163B /* voip_radio_off.png */; };
+ C63F72B9285A2F1D0066163B /* voip_export.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7275285A2F150066163B /* voip_export.png */; };
+ C63F72BA285A2F1D0066163B /* voip_call_numpad.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7276285A2F150066163B /* voip_call_numpad.png */; };
+ C63F72BB285A2F1D0066163B /* voip_chat_rooms_list.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7277285A2F150066163B /* voip_chat_rooms_list.png */; };
+ C63F72BC285A2F1D0066163B /* voip_numpad_4.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7278285A2F150066163B /* voip_numpad_4.png */; };
+ C63F72BD285A2F1D0066163B /* voip_call_header_incoming.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7279285A2F150066163B /* voip_call_header_incoming.png */; };
+ C63F72BE285A2F1D0066163B /* voip_calls_list.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F727A285A2F150066163B /* voip_calls_list.png */; };
+ C63F72BF285A2F1D0066163B /* voip_call_header_paused.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F727B285A2F160066163B /* voip_call_header_paused.png */; };
+ C63F72C0285A2F1D0066163B /* voip_numpad_0.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F727C285A2F160066163B /* voip_numpad_0.png */; };
+ C63F72C1285A2F1D0066163B /* voip_numpad_3.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F727D285A2F160066163B /* voip_numpad_3.png */; };
+ C63F72C2285A2F1D0066163B /* voip_numpad_9.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F727E285A2F160066163B /* voip_numpad_9.png */; };
+ C63F72C3285A2F1D0066163B /* voip_speaker_on.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F727F285A2F160066163B /* voip_speaker_on.png */; };
+ C63F72C4285A2F1D0066163B /* voip_audio_routes.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7280285A2F160066163B /* voip_audio_routes.png */; };
+ C63F72C5285A2F1E0066163B /* voip_call_record.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7281285A2F160066163B /* voip_call_record.png */; };
+ C63F72C6285A2F1E0066163B /* voip_call_forward.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7282285A2F170066163B /* voip_call_forward.png */; };
+ C63F72C7285A2F1E0066163B /* voip_change_camera.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7283285A2F170066163B /* voip_change_camera.png */; };
+ C63F72C8285A2F1E0066163B /* voip_checkbox_checked.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7284285A2F170066163B /* voip_checkbox_checked.png */; };
+ C63F72C9285A2F1E0066163B /* voip_info.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7285285A2F170066163B /* voip_info.png */; };
+ C63F72CA285A2F1E0066163B /* voip_speaker_off.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7286285A2F170066163B /* voip_speaker_off.png */; };
+ C63F72CB285A2F1E0066163B /* voip_call_more.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7287285A2F170066163B /* voip_call_more.png */; };
+ C63F72CC285A2F1E0066163B /* voip_call_stats.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7288285A2F170066163B /* voip_call_stats.png */; };
+ C63F72CD285A2F1E0066163B /* voip_numpad_8.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7289285A2F180066163B /* voip_numpad_8.png */; };
+ C63F72CE285A2F1E0066163B /* voip_call_add.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F728A285A2F180066163B /* voip_call_add.png */; };
+ C63F72CF285A2F1E0066163B /* voip_copy.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F728B285A2F180066163B /* voip_copy.png */; };
+ C63F72D0285A2F1E0066163B /* voip_conference_paused_big.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F728C285A2F180066163B /* voip_conference_paused_big.png */; };
+ C63F72D1285A2F1E0066163B /* voip_numpad_star.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F728D285A2F180066163B /* voip_numpad_star.png */; };
+ C63F72D2285A2F1E0066163B /* voip_numpad_hash.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F728E285A2F180066163B /* voip_numpad_hash.png */; };
+ C63F72D3285A2F1E0066163B /* voip_multiple_contacts_avatar.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F728F285A2F180066163B /* voip_multiple_contacts_avatar.png */; };
+ C63F72D4285A2F1E0066163B /* voip_remote_recording.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7290285A2F180066163B /* voip_remote_recording.png */; };
+ C63F72D5285A2F1E0066163B /* voip_hangup.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7291285A2F190066163B /* voip_hangup.png */; };
+ C63F72D6285A2F1E0066163B /* voip_pause.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7292285A2F190066163B /* voip_pause.png */; };
+ C63F72D7285A2F1E0066163B /* voip_numpad_1.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7293285A2F190066163B /* voip_numpad_1.png */; };
+ C63F72D8285A2F1E0066163B /* voip_mandatory.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7294285A2F190066163B /* voip_mandatory.png */; };
+ C63F72D9285A2F1E0066163B /* voip_earpiece.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7295285A2F190066163B /* voip_earpiece.png */; };
+ C63F72DA285A2F1E0066163B /* voip_numpad_2.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7296285A2F190066163B /* voip_numpad_2.png */; };
+ C63F72DB285A2F1E0066163B /* voip_conference_audio_only.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7297285A2F190066163B /* voip_conference_audio_only.png */; };
+ C63F72DC285A2F1E0066163B /* voip_menu_more.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7298285A2F1A0066163B /* voip_menu_more.png */; };
+ C63F72DD285A2F1E0066163B /* voip_conference_new.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F7299285A2F1A0066163B /* voip_conference_new.png */; };
+ C63F72DE285A2F1E0066163B /* voip_call_header_active.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F729A285A2F1A0066163B /* voip_call_header_active.png */; };
+ C63F72DF285A2F1E0066163B /* voip_bluetooth.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F729B285A2F1A0066163B /* voip_bluetooth.png */; };
+ C63F72E0285A2F1E0066163B /* voip_micro_off.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F729C285A2F1A0066163B /* voip_micro_off.png */; };
+ C63F72E1285A2F1E0066163B /* voip_camera_on.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F729D285A2F1A0066163B /* voip_camera_on.png */; };
+ C63F72E2285A2F1E0066163B /* voip_conference_play_big.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F729E285A2F1A0066163B /* voip_conference_play_big.png */; };
+ C63F72E3285A2F1E0066163B /* voip_call.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F729F285A2F1B0066163B /* voip_call.png */; };
+ C63F72E4285A2F1E0066163B /* voip_call_list_menu.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72A0285A2F1B0066163B /* voip_call_list_menu.png */; };
+ C63F72E5285A2F1E0066163B /* voip_conference_active_speaker.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72A1285A2F1B0066163B /* voip_conference_active_speaker.png */; };
+ C63F72E6285A2F1E0066163B /* voip_numpad_6.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72A2285A2F1B0066163B /* voip_numpad_6.png */; };
+ C63F72E7285A2F1E0066163B /* voip_call_participants.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72A3285A2F1B0066163B /* voip_call_participants.png */; };
+ C63F72E8285A2F1E0066163B /* conference_schedule_calendar_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72A4285A2F1B0066163B /* conference_schedule_calendar_default.png */; };
+ C63F72E9285A2F1E0066163B /* voip_call_chat.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72A5285A2F1B0066163B /* voip_call_chat.png */; };
+ C63F72EA285A2F1E0066163B /* voip_checkbox_unchecked.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72A6285A2F1B0066163B /* voip_checkbox_unchecked.png */; };
+ C63F72EB285A2F1E0066163B /* voip_edit.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72A7285A2F1C0066163B /* voip_edit.png */; };
+ C63F72EC285A2F1E0066163B /* conference_schedule_time_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72A8285A2F1C0066163B /* conference_schedule_time_default.png */; };
+ C63F72ED285A2F1E0066163B /* voip_merge_calls.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72A9285A2F1C0066163B /* voip_merge_calls.png */; };
+ C63F72EE285A2F1E0066163B /* voip_camera_off.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72AA285A2F1C0066163B /* voip_camera_off.png */; };
+ C63F72EF285A2F1E0066163B /* voip_micro_on.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72AB285A2F1C0066163B /* voip_micro_on.png */; };
+ C63F72F0285A2F1E0066163B /* voip_radio_on.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72AC285A2F1C0066163B /* voip_radio_on.png */; };
+ C63F72F1285A2F1E0066163B /* conference_schedule_participants_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72AD285A2F1C0066163B /* conference_schedule_participants_default.png */; };
+ C63F72F2285A2F1E0066163B /* voip_conference_mosaic.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72AE285A2F1D0066163B /* voip_conference_mosaic.png */; };
+ C63F72F3285A2F1E0066163B /* voip_numpad_5.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72AF285A2F1D0066163B /* voip_numpad_5.png */; };
+ C63F72F4285A2F1E0066163B /* voip_conference_new_selected.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72B0285A2F1D0066163B /* voip_conference_new_selected.png */; };
+ C63F72F5285A2F1E0066163B /* voip_single_contact_avatar.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72B1285A2F1D0066163B /* voip_single_contact_avatar.png */; };
+ C63F72F6285A2F1E0066163B /* voip_delete.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72B2285A2F1D0066163B /* voip_delete.png */; };
+ C63F72F7285A2F1E0066163B /* voip_cancel.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72B3285A2F1D0066163B /* voip_cancel.png */; };
+ C63F72F8285A2F1E0066163B /* voip_call_header_outgoing.png in Resources */ = {isa = PBXBuildFile; fileRef = C63F72B4285A2F1D0066163B /* voip_call_header_outgoing.png */; };
+ C63F72FD285A31DA0066163B /* Roboto-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C63F72FA285A31DA0066163B /* Roboto-Regular.ttf */; };
+ C63F72FE285A31DA0066163B /* Roboto-Italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C63F72FB285A31DA0066163B /* Roboto-Italic.ttf */; };
+ C63F72FF285A31DA0066163B /* Roboto-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C63F72FC285A31DA0066163B /* Roboto-Bold.ttf */; };
C64A854E2667B67200252AD2 /* EphemeralSettingsView.m in Sources */ = {isa = PBXBuildFile; fileRef = C64A854D2667B67200252AD2 /* EphemeralSettingsView.m */; };
C64A85502667B67A00252AD2 /* EphemeralSettingsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C64A854F2667B67A00252AD2 /* EphemeralSettingsView.xib */; };
C64A85522667B74100252AD2 /* ephemeral_messages_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C64A85512667B74100252AD2 /* ephemeral_messages_default.png */; };
- C666756F264C925800A0273C /* VFSUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DA657B261C950C0020CB43 /* VFSUtil.swift */; };
- C6667571264C925B00A0273C /* VFSUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DA657B261C950C0020CB43 /* VFSUtil.swift */; };
C66B03BB26E8EB1A009B5EDC /* UIChatReplyBubbleView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C66B03BD26E8EB1A009B5EDC /* UIChatReplyBubbleView.xib */; };
C66B040A26EFDA55009B5EDC /* reply_cancel.png in Resources */ = {isa = PBXBuildFile; fileRef = C66B040926EFDA54009B5EDC /* reply_cancel.png */; };
C66B040E26F095D1009B5EDC /* cancel_forward.png in Resources */ = {isa = PBXBuildFile; fileRef = C66B040D26F095CE009B5EDC /* cancel_forward.png */; };
@@ -708,7 +856,6 @@
C6B4444526AAD0980076C517 /* file_audio_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C6B4444026AAD0970076C517 /* file_audio_default.png */; };
C6B4444626AAD0980076C517 /* file_pdf_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C6B4444126AAD0970076C517 /* file_pdf_default.png */; };
C6B4444826AADA530076C517 /* SwiftUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B4444726AADA530076C517 /* SwiftUtil.swift */; };
- C6DA657C261C950C0020CB43 /* VFSUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DA657B261C950C0020CB43 /* VFSUtil.swift */; };
C90FAA7915AF54E6002091CB /* HistoryDetailsView.m in Sources */ = {isa = PBXBuildFile; fileRef = C90FAA7715AF54E6002091CB /* HistoryDetailsView.m */; };
CF15F21E20E4F9A3008B1DE6 /* UIImageViewDeletable.m in Sources */ = {isa = PBXBuildFile; fileRef = CF15F21C20E4F9A3008B1DE6 /* UIImageViewDeletable.m */; };
CF15F21F20E4F9A3008B1DE6 /* UIImageViewDeletable.xib in Resources */ = {isa = PBXBuildFile; fileRef = CF15F21D20E4F9A3008B1DE6 /* UIImageViewDeletable.xib */; };
@@ -725,7 +872,6 @@
CFBD7A2A20E504AE007C5286 /* delete_img.png in Resources */ = {isa = PBXBuildFile; fileRef = CFBD7A2320E504AD007C5286 /* delete_img.png */; };
D306459E1611EC2A00BB571E /* UILoadingImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = D306459D1611EC2900BB571E /* UILoadingImageView.m */; };
D3128FE115AABC7E00A2147A /* ContactDetailsView.m in Sources */ = {isa = PBXBuildFile; fileRef = D3128FDF15AABC7E00A2147A /* ContactDetailsView.m */; };
- D31AAF5E159B3919002C6B02 /* CallPausedTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = D31AAF5D159B3919002C6B02 /* CallPausedTableView.m */; };
D31B4B21159876C0002E6C72 /* UICompositeView.m in Sources */ = {isa = PBXBuildFile; fileRef = D31B4B1F159876C0002E6C72 /* UICompositeView.m */; };
D31C9C98158A1CDF00756B45 /* UIHistoryCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D31C9C97158A1CDE00756B45 /* UIHistoryCell.m */; };
D326483815887D5200930C67 /* OrderedDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = D326483715887D5200930C67 /* OrderedDictionary.m */; };
@@ -740,7 +886,6 @@
D35860D615B549B500513429 /* Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = D35860D515B549B500513429 /* Utils.m */; };
D35E7597159460580066B1C1 /* ChatsListView.m in Sources */ = {isa = PBXBuildFile; fileRef = D35E7595159460560066B1C1 /* ChatsListView.m */; };
D35E759F159460B70066B1C1 /* SettingsView.m in Sources */ = {isa = PBXBuildFile; fileRef = D35E759D159460B50066B1C1 /* SettingsView.m */; };
- D36FB2D51589EF7C0036F6F2 /* UIPauseButton.m in Sources */ = {isa = PBXBuildFile; fileRef = D36FB2D41589EF7C0036F6F2 /* UIPauseButton.m */; };
D378AB2A15DCDB4A0098505D /* ImagePickerView.m in Sources */ = {isa = PBXBuildFile; fileRef = D378AB2915DCDB490098505D /* ImagePickerView.m */; };
D37C639B15AADEF6009D0BAC /* ContactDetailsTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = D37C639A15AADEF5009D0BAC /* ContactDetailsTableView.m */; };
D37DC6C11594AE1800B2A5EB /* LinphoneCoreSettingsStore.m in Sources */ = {isa = PBXBuildFile; fileRef = D37DC6C01594AE1800B2A5EB /* LinphoneCoreSettingsStore.m */; };
@@ -773,10 +918,8 @@
D38187C115FE345B00C3EDCA /* DialerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D38187C415FE345B00C3EDCA /* DialerView.xib */; };
D38187CD15FE346700C3EDCA /* HistoryDetailsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D38187D015FE346700C3EDCA /* HistoryDetailsView.xib */; };
D38187D115FE346B00C3EDCA /* HistoryListView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D38187D415FE346B00C3EDCA /* HistoryListView.xib */; };
- D38187D915FE347700C3EDCA /* CallIncomingView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D38187DC15FE347700C3EDCA /* CallIncomingView.xib */; };
D38187DD15FE348A00C3EDCA /* AssistantView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D38187E015FE348A00C3EDCA /* AssistantView.xib */; };
D38187F815FE355D00C3EDCA /* TabBarView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D38187FB15FE355D00C3EDCA /* TabBarView.xib */; };
- D381881915FE3FCA00C3EDCA /* CallView.xib in Resources */ = {isa = PBXBuildFile; fileRef = D381881C15FE3FCA00C3EDCA /* CallView.xib */; };
D3A55FBC15877E5E003FD403 /* UIContactCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D3A55FBB15877E5E003FD403 /* UIContactCell.m */; };
D3A8BB7015A6C7D500F96BE5 /* UIChatBubbleTextCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D3A8BB6F15A6C7D500F96BE5 /* UIChatBubbleTextCell.m */; };
D3C6526715AC1A8F0092A874 /* UIContactDetailsCell.m in Sources */ = {isa = PBXBuildFile; fileRef = D3C6526615AC1A8F0092A874 /* UIContactDetailsCell.m */; };
@@ -787,10 +930,8 @@
D3ED3E871586291E006C0DE4 /* TabBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = D3ED3E851586291B006C0DE4 /* TabBarView.m */; };
D3ED3EA71587334E006C0DE4 /* HistoryListTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = D3ED3EA51587334C006C0DE4 /* HistoryListTableView.m */; };
D3ED3EB81587392C006C0DE4 /* HistoryListView.m in Sources */ = {isa = PBXBuildFile; fileRef = D3ED3EB615873929006C0DE4 /* HistoryListView.m */; };
- D3F26BF115986B73005F9CAB /* CallIncomingView.m in Sources */ = {isa = PBXBuildFile; fileRef = D3F26BEF15986B71005F9CAB /* CallIncomingView.m */; };
D3F795D615A582810077328B /* ChatConversationView.m in Sources */ = {isa = PBXBuildFile; fileRef = D3F795D415A582800077328B /* ChatConversationView.m */; };
D3F7998115BD32370018C273 /* TPMultiLayoutViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D3F7998015BD32370018C273 /* TPMultiLayoutViewController.m */; };
- D3F83EEC1582021700336684 /* CallView.m in Sources */ = {isa = PBXBuildFile; fileRef = D3F83EEA1582021700336684 /* CallView.m */; };
D3F83F8E15822ABE00336684 /* PhoneMainView.m in Sources */ = {isa = PBXBuildFile; fileRef = D3F83F8D15822ABD00336684 /* PhoneMainView.m */; };
EA0007A62356008F003CC6BF /* msgNotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = EA5F25D9232BD3E200475F2E /* msgNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
EA3650DB2330D2E30001148A /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5F25DB232BD3E200475F2E /* NotificationService.swift */; };
@@ -874,10 +1015,6 @@
1FB08967C4E9D7B85F6A595B /* Pods-msgNotificationContent.distribution.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-msgNotificationContent.distribution.xcconfig"; path = "Target Support Files/Pods-msgNotificationContent/Pods-msgNotificationContent.distribution.xcconfig"; sourceTree = ""; };
2214EB7812F846B1002A5394 /* UICallButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UICallButton.h; sourceTree = ""; };
2214EB7912F846B1002A5394 /* UICallButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UICallButton.m; sourceTree = ""; };
- 2214EB8712F84EBB002A5394 /* UIHangUpButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIHangUpButton.h; sourceTree = ""; };
- 2214EB8812F84EBB002A5394 /* UIHangUpButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIHangUpButton.m; sourceTree = ""; };
- 2214EBF112F86360002A5394 /* UIMutedMicroButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIMutedMicroButton.h; sourceTree = ""; };
- 2214EBF212F86360002A5394 /* UIMutedMicroButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIMutedMicroButton.m; sourceTree = ""; };
22276E8613C73D8A00210156 /* CoreVideo.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreVideo.framework; path = System/Library/Frameworks/CoreVideo.framework; sourceTree = SDKROOT; };
22276E8813C73DC000210156 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
22405EED1600B4E400B92522 /* AssetsLibrary.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AssetsLibrary.framework; path = System/Library/Frameworks/AssetsLibrary.framework; sourceTree = SDKROOT; };
@@ -893,14 +1030,10 @@
22744043106F33FC006EC466 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
22744056106F9BC9006EC466 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; };
228697C311AC29B800E9E0CA /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; };
- 22968A5D12F875C600588287 /* UISpeakerButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UISpeakerButton.h; sourceTree = ""; };
- 22968A5E12F875C600588287 /* UISpeakerButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UISpeakerButton.m; sourceTree = ""; };
22AA8AFF13D83F6300B30535 /* UICamSwitch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UICamSwitch.h; sourceTree = ""; };
22AA8B0013D83F6300B30535 /* UICamSwitch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UICamSwitch.m; sourceTree = ""; };
22B5EFA210CE50BD00777D97 /* AddressBookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBookUI.framework; path = System/Library/Frameworks/AddressBookUI.framework; sourceTree = SDKROOT; };
22B5F03410CE6B2F00777D97 /* AddressBook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBook.framework; path = System/Library/Frameworks/AddressBook.framework; sourceTree = SDKROOT; };
- 22C7555E1317E59C007BC101 /* UIBluetoothButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIBluetoothButton.h; sourceTree = ""; };
- 22C7555F1317E59C007BC101 /* UIBluetoothButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIBluetoothButton.m; sourceTree = ""; };
22D1B68012A3E0BE001AE361 /* libresolv.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libresolv.dylib; path = usr/lib/libresolv.dylib; sourceTree = SDKROOT; };
22E0A81C111C44E100B04932 /* AboutView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AboutView.m; sourceTree = ""; };
22E0A81D111C44E100B04932 /* AboutView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AboutView.h; sourceTree = ""; };
@@ -936,8 +1069,6 @@
340751961506459A00B89C47 /* CoreTelephony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; };
340751E5150F38FC00B89C47 /* UIVideoButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIVideoButton.h; sourceTree = ""; };
340751E6150F38FD00B89C47 /* UIVideoButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIVideoButton.m; sourceTree = ""; };
- 34216F3E1547EBCD00EA9777 /* VideoZoomHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VideoZoomHandler.h; path = LinphoneUI/VideoZoomHandler.h; sourceTree = ""; };
- 34216F3F1547EBCD00EA9777 /* VideoZoomHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = VideoZoomHandler.m; path = LinphoneUI/VideoZoomHandler.m; sourceTree = ""; };
344ABDEF14850AE9007420B6 /* libc++.1.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libc++.1.dylib"; path = "usr/lib/libc++.1.dylib"; sourceTree = SDKROOT; };
344ABDF014850AE9007420B6 /* libstdc++.6.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libstdc++.6.dylib"; path = "usr/lib/libstdc++.6.dylib"; sourceTree = SDKROOT; };
4DF6C8E3533E18B9BDDF7F15 /* Pods-msgNotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-msgNotificationService.debug.xcconfig"; path = "Target Support Files/Pods-msgNotificationService/Pods-msgNotificationService.debug.xcconfig"; sourceTree = ""; };
@@ -950,15 +1081,11 @@
6112A01B243B31A600DBD5F5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; };
6112A01D243B5FD500DBD5F5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; };
6130C85B22BBB493009CC79C /* LaunchScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LaunchScreen.h; sourceTree = ""; };
- 6134812C2406CECC00695B41 /* ConfigManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigManager.swift; sourceTree = ""; };
- 6134812E2407B35200695B41 /* AppManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = ""; };
6135761A240E81AC005304D4 /* UIInterfaceStyleButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIInterfaceStyleButton.h; sourceTree = ""; };
6135761B240E81BA005304D4 /* UIInterfaceStyleButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIInterfaceStyleButton.m; sourceTree = ""; };
6135761D240E81C7005304D4 /* UIInterfaceStyleToggleButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIInterfaceStyleToggleButton.h; sourceTree = ""; };
6135761E240E81D0005304D4 /* UIInterfaceStyleToggleButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIInterfaceStyleToggleButton.m; sourceTree = ""; };
614C087623D1A35E00217F80 /* linphone-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "linphone-Bridging-Header.h"; sourceTree = ""; };
- 614C087723D1A35F00217F80 /* ProviderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProviderDelegate.swift; sourceTree = ""; };
- 614C087923D1A37400217F80 /* CallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = ""; };
614D09CD21E74D5400C43EDF /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; };
61586B7A217A16EE0038AC45 /* menu_about.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_about.png; sourceTree = ""; };
61586B82217A16FD0038AC45 /* menu_about@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "menu_about@2x.png"; sourceTree = ""; };
@@ -1004,10 +1131,6 @@
6187B1B624B3271500D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/AssistantLinkView.strings; sourceTree = ""; };
6187B1B724B3271600D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/AssistantView.strings; sourceTree = ""; };
6187B1B824B3271600D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/AssistantViewScreens.strings; sourceTree = ""; };
- 6187B1B924B3271700D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/CallIncomingView.strings; sourceTree = ""; };
- 6187B1BA24B3271700D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/CallOutgoingView.strings; sourceTree = ""; };
- 6187B1BB24B3271700D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/CallView.strings; sourceTree = ""; };
- 6187B1BC24B3271800D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = "hu.lproj/CallView~ipad.strings"; sourceTree = ""; };
6187B1BD24B3271800D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/ChatConversationCreateView.strings; sourceTree = ""; };
6187B1BE24B3271900D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/ChatConversationImdnView.strings; sourceTree = ""; };
6187B1BF24B3271900D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/ChatConversationInfoView.strings; sourceTree = ""; };
@@ -1022,8 +1145,6 @@
6187B1C824B3271D00D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/HistoryDetailsView.strings; sourceTree = ""; };
6187B1C924B3271D00D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/HistoryListView.strings; sourceTree = ""; };
6187B1CA24B3271E00D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/ImageView.strings; sourceTree = ""; };
- 6187B1CB24B3271E00D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/UICallConferenceCell.strings; sourceTree = ""; };
- 6187B1CC24B3271E00D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/UICallPausedCell.strings; sourceTree = ""; };
6187B1CD24B3271F00D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/UIChatBubblePhotoCell.strings; sourceTree = ""; };
6187B1CE24B3271F00D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/UIChatBubbleTextCell.strings; sourceTree = ""; };
6187B1CF24B3271F00D580FB /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/UIChatCell.strings; sourceTree = ""; };
@@ -1523,14 +1644,8 @@
63423C091C4501D000D9A050 /* Contact.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Contact.m; sourceTree = ""; };
634610041B61330300548952 /* UILabel+Boldify.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UILabel+Boldify.h"; sourceTree = ""; };
634610051B61330300548952 /* UILabel+Boldify.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UILabel+Boldify.m"; sourceTree = ""; };
- 6346100D1B61409800548952 /* CallOutgoingView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CallOutgoingView.h; sourceTree = ""; };
- 6346100E1B61409800548952 /* CallOutgoingView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallOutgoingView.m; sourceTree = ""; };
- 634610111B6140A500548952 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/CallOutgoingView.xib; sourceTree = ""; };
635173F71BA082A40095EB0A /* UIChatBubblePhotoCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIChatBubblePhotoCell.h; sourceTree = ""; };
635173F81BA082A40095EB0A /* UIChatBubblePhotoCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIChatBubblePhotoCell.m; sourceTree = ""; };
- 6352A5721BE0D4B800594C1C /* CallSideMenuView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CallSideMenuView.h; sourceTree = ""; };
- 6352A5731BE0D4B800594C1C /* CallSideMenuView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallSideMenuView.m; sourceTree = ""; };
- 6352A5741BE0D4B800594C1C /* CallSideMenuView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = CallSideMenuView.xib; sourceTree = ""; };
635775231B6673EC00C8B704 /* HistoryDetailsTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HistoryDetailsTableView.h; sourceTree = ""; };
635775241B6673EC00C8B704 /* HistoryDetailsTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HistoryDetailsTableView.m; sourceTree = ""; };
636316D21A1DEBCB0009B839 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/AboutView.xib; sourceTree = ""; };
@@ -1547,7 +1662,6 @@
6381DA7B1C1AD5EA00DF3BBD /* UIBouncingView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIBouncingView.h; sourceTree = ""; };
6381DA7C1C1AD5EA00DF3BBD /* UIBouncingView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIBouncingView.m; sourceTree = ""; };
638F1A611C2021B2004B8E02 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Base.lproj/DialerView~ipad.xib"; sourceTree = ""; };
- 638F1A871C2167C2004B8E02 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Base.lproj/CallView~ipad.xib"; sourceTree = ""; };
638F1A901C21993D004B8E02 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Base.lproj/UICompositeView~ipad.xib"; sourceTree = ""; };
639CEAFE1A1DF4D9004DE38F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/StatusBarView.xib; sourceTree = ""; };
639CEB011A1DF4E4004DE38F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/UIHistoryCell.xib; sourceTree = ""; };
@@ -1556,7 +1670,6 @@
639E9C7E1C0DB13D00019A75 /* UICheckBoxTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UICheckBoxTableView.h; sourceTree = ""; };
639E9C7F1C0DB13D00019A75 /* UICheckBoxTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UICheckBoxTableView.m; sourceTree = ""; };
639E9C941C0DB7BE00019A75 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/FirstLoginView.xib; sourceTree = ""; };
- 639E9C9E1C0DB7DF00019A75 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/UICallPausedCell.xib; sourceTree = ""; };
639E9CA11C0DB7E500019A75 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/UIChatBubblePhotoCell.xib; sourceTree = ""; };
639E9CA41C0DB7EA00019A75 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/UIChatBubbleTextCell.xib; sourceTree = ""; };
639E9CA71C0DB7F200019A75 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/UIChatCreateCell.xib; sourceTree = ""; };
@@ -1590,8 +1703,6 @@
63B8D68D1BCBE65600C12B09 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ChatConversationCreateView.xib; sourceTree = ""; };
63B8D69F1BCBF43100C12B09 /* UIChatCreateCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIChatCreateCell.h; sourceTree = ""; };
63B8D6A01BCBF43100C12B09 /* UIChatCreateCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIChatCreateCell.m; sourceTree = ""; };
- 63BC49E01BA2CDFC004EC273 /* UICallPausedCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UICallPausedCell.h; sourceTree = ""; };
- 63BC49E11BA2CDFC004EC273 /* UICallPausedCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UICallPausedCell.m; sourceTree = ""; };
63BE7A761D75BDF6000990EF /* ShopTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShopTableView.h; sourceTree = ""; };
63BE7A771D75BDF6000990EF /* ShopTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShopTableView.m; sourceTree = ""; };
63C441C11BBC23ED0053DC5E /* UIAssistantTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIAssistantTextField.h; sourceTree = ""; };
@@ -1615,11 +1726,6 @@
63EEE40D1BBA9B250087D3AF /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; };
63F1DF421BCE618E00EDED90 /* UIAddressTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIAddressTextField.h; sourceTree = ""; };
63F1DF431BCE618E00EDED90 /* UIAddressTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIAddressTextField.m; sourceTree = "