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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 = ""; }; - 63F1DF491BCE983100EDED90 /* CallConferenceTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CallConferenceTableView.h; sourceTree = ""; }; - 63F1DF4A1BCE983200EDED90 /* CallConferenceTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallConferenceTableView.m; sourceTree = ""; }; - 63F1DF4C1BCE985F00EDED90 /* UICallConferenceCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UICallConferenceCell.h; sourceTree = ""; }; - 63F1DF4D1BCE985F00EDED90 /* UICallConferenceCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UICallConferenceCell.m; sourceTree = ""; }; - 63F1DF521BCE986A00EDED90 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/UICallConferenceCell.xib; sourceTree = ""; }; 63FB30331A680E73008CA393 /* UIRoundedImageView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIRoundedImageView.h; sourceTree = ""; }; 63FB30341A680E73008CA393 /* UIRoundedImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIRoundedImageView.m; sourceTree = ""; }; 662553B327EDFB35007F67D8 /* MagicSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MagicSearch.swift; sourceTree = ""; }; @@ -1632,8 +1738,6 @@ 70E542F413E147EB002BA2C0 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 87F00D1935382CCA03DF2F02 /* Pods-linphone.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-linphone.debug.xcconfig"; path = "Target Support Files/Pods-linphone/Pods-linphone.debug.xcconfig"; sourceTree = ""; }; 8C1A1F7C1FA331D40064BE00 /* libsoci_sqlite3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libsoci_sqlite3.a; path = "liblinphone-sdk/apple-darwin/lib/libsoci_sqlite3.a"; sourceTree = ""; }; - 8C1B67051E671826001EA2FE /* AudioHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioHelper.m; sourceTree = ""; }; - 8C1B67081E6718BC001EA2FE /* AudioHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = AudioHelper.h; path = Utils/AudioHelper.h; sourceTree = ""; }; 8C23BCB71D82AAC3005F19BB /* linphone.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = linphone.entitlements; sourceTree = ""; }; 8C2595DE1DEDCC8E007A6424 /* CallKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CallKit.framework; path = System/Library/Frameworks/CallKit.framework; sourceTree = SDKROOT; }; 8C2A81931F87B7FF0012A66B /* chat_group_avatar@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "chat_group_avatar@2x.png"; sourceTree = ""; }; @@ -1669,10 +1773,6 @@ 8CBD7BAD20B6B82F00E5DCC0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/UIChatCreateCollectionViewCell.xib; sourceTree = ""; }; 8CBD7BB120B6B86900E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/AssistantView.strings; sourceTree = ""; }; 8CBD7BB220B6B86A00E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/AssistantViewScreens.strings; sourceTree = ""; }; - 8CBD7BB320B6B86B00E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/CallIncomingView.strings; sourceTree = ""; }; - 8CBD7BB420B6B86B00E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/CallOutgoingView.strings; sourceTree = ""; }; - 8CBD7BB520B6B86C00E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/CallView.strings; sourceTree = ""; }; - 8CBD7BB620B6B86D00E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = "fr.lproj/CallView~ipad.strings"; sourceTree = ""; }; 8CBD7BB720B6B86E00E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/ChatConversationInfoView.strings; sourceTree = ""; }; 8CBD7BB820B6B86F00E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/ChatConversationImdnView.strings; sourceTree = ""; }; 8CBD7BB920B6B87000E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/ChatConversationCreateView.strings; sourceTree = ""; }; @@ -1687,8 +1787,6 @@ 8CBD7BC220B6B87600E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/HistoryDetailsView.strings; sourceTree = ""; }; 8CBD7BC320B6B87700E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/HistoryListView.strings; sourceTree = ""; }; 8CBD7BC420B6B87800E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/ImageView.strings; sourceTree = ""; }; - 8CBD7BC520B6B87800E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/UICallConferenceCell.strings; sourceTree = ""; }; - 8CBD7BC620B6B87900E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/UICallPausedCell.strings; sourceTree = ""; }; 8CBD7BC720B6B87A00E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/UIChatBubblePhotoCell.strings; sourceTree = ""; }; 8CBD7BC820B6B87B00E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/UIChatBubbleTextCell.strings; sourceTree = ""; }; 8CBD7BC920B6B87B00E5DCC0 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/UIChatCell.strings; sourceTree = ""; }; @@ -1736,6 +1834,174 @@ C622E3EC26A8128F004F5434 /* vr_off.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vr_off.png; sourceTree = ""; }; C622E3ED26A8128F004F5434 /* vr_pause.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vr_pause.png; sourceTree = ""; }; C622E3EE26A81290004F5434 /* vr_play.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = vr_play.png; sourceTree = ""; }; + C63F7197285A24B10066163B /* ConfigManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigManager.swift; sourceTree = ""; }; + C63F7198285A24B10066163B /* CallManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = ""; }; + C63F719B285A24B10066163B /* ConferenceWaitingRoomViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceWaitingRoomViewModel.swift; sourceTree = ""; }; + C63F719C285A24B10066163B /* ConferenceSchedulingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceSchedulingViewModel.swift; sourceTree = ""; }; + C63F719D285A24B10066163B /* ScheduledConferencesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduledConferencesViewModel.swift; sourceTree = ""; }; + C63F719F285A24B10066163B /* ScheduledConferenceData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduledConferenceData.swift; sourceTree = ""; }; + C63F71A0285A24B10066163B /* TimeZoneData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimeZoneData.swift; sourceTree = ""; }; + C63F71A1285A24B10066163B /* Duration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Duration.swift; sourceTree = ""; }; + C63F71A3285A24B10066163B /* ConferenceWaitingRoomFragment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceWaitingRoomFragment.swift; sourceTree = ""; }; + C63F71A4285A24B10066163B /* ScheduledConferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduledConferencesView.swift; sourceTree = ""; }; + C63F71A5285A24B10066163B /* ICSBubbleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICSBubbleView.swift; sourceTree = ""; }; + C63F71A6285A24B10066163B /* ScheduledConferencesCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduledConferencesCell.swift; sourceTree = ""; }; + C63F71A7285A24B10066163B /* ConferenceHistoryDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceHistoryDetailsView.swift; sourceTree = ""; }; + C63F71A8285A24B10066163B /* ConferenceSchedulingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceSchedulingView.swift; sourceTree = ""; }; + C63F71A9285A24B10066163B /* ConferenceSchedulingSummaryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceSchedulingSummaryView.swift; sourceTree = ""; }; + C63F71AC285A24B10066163B /* MediatorLiveData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediatorLiveData.swift; sourceTree = ""; }; + C63F71AD285A24B10066163B /* MutableLiveData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutableLiveData.swift; sourceTree = ""; }; + C63F71AE285A24B10066163B /* Pair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pair.swift; sourceTree = ""; }; + C63F71AF285A24B10066163B /* BackNextNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackNextNavigationView.swift; sourceTree = ""; }; + C63F71B0285A24B10066163B /* TimestampUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimestampUtils.swift; sourceTree = ""; }; + C63F71B1285A24B10066163B /* AppManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppManager.swift; sourceTree = ""; }; + C63F71B4285A24B10066163B /* UIApplication+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extension.swift"; sourceTree = ""; }; + C63F71B6285A24B10066163B /* UIVIewControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIVIewControllerExtensions.swift; sourceTree = ""; }; + C63F71B7285A24B10066163B /* UIImageExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtensions.swift; sourceTree = ""; }; + C63F71B8285A24B10066163B /* UIVIewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIVIewExtensions.swift; sourceTree = ""; }; + C63F71B9285A24B10066163B /* UILabelExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UILabelExtensions.swift; sourceTree = ""; }; + C63F71BA285A24B10066163B /* OptionalExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionalExtensions.swift; sourceTree = ""; }; + C63F71BB285A24B10066163B /* UIButtonExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIButtonExtensions.swift; sourceTree = ""; }; + C63F71BC285A24B10066163B /* UIImageViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageViewExtensions.swift; sourceTree = ""; }; + C63F71BD285A24B10066163B /* UIDeviceExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIDeviceExtensions.swift; sourceTree = ""; }; + C63F71BE285A24B10066163B /* UIColorExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColorExtensions.swift; sourceTree = ""; }; + C63F71C0285A24B10066163B /* CoreExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreExtensions.swift; sourceTree = ""; }; + C63F71C1285A24B10066163B /* IceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IceState.swift; sourceTree = ""; }; + C63F71C2285A24B10066163B /* AddressExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddressExtensions.swift; sourceTree = ""; }; + C63F71C3285A24B10066163B /* ParticipantExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParticipantExtensions.swift; sourceTree = ""; }; + C63F71C4285A24B10066163B /* PayloadType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PayloadType.swift; sourceTree = ""; }; + C63F71C5285A24B10066163B /* CallExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallExtensions.swift; sourceTree = ""; }; + C63F71C6285A24B10066163B /* ConferenceExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceExtensions.swift; sourceTree = ""; }; + C63F71C9285A24B10066163B /* ConferenceViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceViewModel.swift; sourceTree = ""; }; + C63F71CA285A24B10066163B /* CallsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallsViewModel.swift; sourceTree = ""; }; + C63F71CB285A24B10066163B /* ControlsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlsViewModel.swift; sourceTree = ""; }; + C63F71CC285A24B10066163B /* CallStatisticsData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallStatisticsData.swift; sourceTree = ""; }; + C63F71CD285A24B10066163B /* ConferenceParticipantData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceParticipantData.swift; sourceTree = ""; }; + C63F71CE285A24B10066163B /* ConferenceParticipantDeviceData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceParticipantDeviceData.swift; sourceTree = ""; }; + C63F71CF285A24B10066163B /* CallData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallData.swift; sourceTree = ""; }; + C63F71D0285A24B10066163B /* AudioRouteUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioRouteUtils.swift; sourceTree = ""; }; + C63F71D2285A24B10066163B /* LightDarkColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LightDarkColor.swift; sourceTree = ""; }; + C63F71D3285A24B10066163B /* TextStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextStyle.swift; sourceTree = ""; }; + C63F71D4285A24B10066163B /* VoipTexts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipTexts.swift; sourceTree = ""; }; + C63F71D5285A24B10066163B /* ButtonTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonTheme.swift; sourceTree = ""; }; + C63F71D6285A24B10066163B /* VoipTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipTheme.swift; sourceTree = ""; }; + C63F71DA285A24B10066163B /* ParticipantsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParticipantsListView.swift; sourceTree = ""; }; + C63F71DB285A24B10066163B /* VoipParticipantCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipParticipantCell.swift; sourceTree = ""; }; + C63F71DC285A24B10066163B /* AudioRoutesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioRoutesView.swift; sourceTree = ""; }; + C63F71DE285A24B10066163B /* VoipActiveSpeakerParticipantCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipActiveSpeakerParticipantCell.swift; sourceTree = ""; }; + C63F71DF285A24B10066163B /* VoipConferenceAudioOnlyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipConferenceAudioOnlyView.swift; sourceTree = ""; }; + C63F71E0285A24B10066163B /* VoipGridParticipantCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipGridParticipantCell.swift; sourceTree = ""; }; + C63F71E1285A24B10066163B /* VoipAudioOnlyParticipantCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipAudioOnlyParticipantCell.swift; sourceTree = ""; }; + C63F71E2285A24B10066163B /* MicMuted.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MicMuted.swift; sourceTree = ""; }; + C63F71E3285A24B10066163B /* VoipConferenceGridView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipConferenceGridView.swift; sourceTree = ""; }; + C63F71E4285A24B10066163B /* VoipConferenceActiveSpeakerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipConferenceActiveSpeakerView.swift; sourceTree = ""; }; + C63F71E5285A24B10066163B /* VoipConferenceDisplayModeSelectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipConferenceDisplayModeSelectionView.swift; sourceTree = ""; }; + C63F71E7285A24B10066163B /* ActiveCallView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveCallView.swift; sourceTree = ""; }; + C63F71E8285A24B10066163B /* IncomingOuntgoingCommonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncomingOuntgoingCommonView.swift; sourceTree = ""; }; + C63F71E9285A24B10066163B /* PausedCallOrConferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PausedCallOrConferenceView.swift; sourceTree = ""; }; + C63F71EA285A24B10066163B /* LocalVideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalVideoView.swift; sourceTree = ""; }; + C63F71EB285A24B10066163B /* CallStatsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallStatsView.swift; sourceTree = ""; }; + C63F71EC285A24B10066163B /* NumpadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumpadView.swift; sourceTree = ""; }; + C63F71ED285A24B10066163B /* VoipExtraButtonsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipExtraButtonsView.swift; sourceTree = ""; }; + C63F71EF285A24B10066163B /* VoipCallContextMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipCallContextMenu.swift; sourceTree = ""; }; + C63F71F0285A24B10066163B /* CallsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallsListView.swift; sourceTree = ""; }; + C63F71F1285A24B10066163B /* VoipCallCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipCallCell.swift; sourceTree = ""; }; + C63F71F2285A24B10066163B /* DismissableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissableView.swift; sourceTree = ""; }; + C63F71F3285A24B10066163B /* ConferenceLayoutPickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceLayoutPickerView.swift; sourceTree = ""; }; + C63F71F4285A24B10066163B /* ControlsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlsView.swift; sourceTree = ""; }; + C63F71F5285A24B10066163B /* RemotelyRecording.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemotelyRecording.swift; sourceTree = ""; }; + C63F71F7285A24B10066163B /* OutgoingCallView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutgoingCallView.swift; sourceTree = ""; }; + C63F71F8285A24B10066163B /* ActiveCallOrConferenceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveCallOrConferenceView.swift; sourceTree = ""; }; + C63F71F9285A24B10066163B /* IncomingCallView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = ""; }; + C63F71FA285A24B10066163B /* SharedLayoutConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharedLayoutConstants.swift; sourceTree = ""; }; + C63F71FB285A24B10066163B /* VoipDialog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipDialog.swift; sourceTree = ""; }; + C63F71FD285A24B10066163B /* StyledValuePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyledValuePicker.swift; sourceTree = ""; }; + C63F71FE285A24B10066163B /* StyledSwitch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyledSwitch.swift; sourceTree = ""; }; + C63F71FF285A24B10066163B /* CallControlButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallControlButton.swift; sourceTree = ""; }; + C63F7200285A24B10066163B /* RotatingSpinner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RotatingSpinner.swift; sourceTree = ""; }; + C63F7201285A24B10066163B /* FormButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormButton.swift; sourceTree = ""; }; + C63F7202285A24B10066163B /* BouncingCounter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BouncingCounter.swift; sourceTree = ""; }; + C63F7203285A24B10066163B /* VoipExtraButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipExtraButton.swift; sourceTree = ""; }; + C63F7204285A24B10066163B /* UICallTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICallTimer.swift; sourceTree = ""; }; + C63F7205285A24B10066163B /* StyledCheckBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyledCheckBox.swift; sourceTree = ""; }; + C63F7206285A24B10066163B /* Avatar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = ""; }; + C63F7207285A24B10066163B /* StyledLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyledLabel.swift; sourceTree = ""; }; + C63F7208285A24B10066163B /* StyledDatePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyledDatePicker.swift; sourceTree = ""; }; + C63F7209285A24B10066163B /* ButtonWithStateBackgrounds.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ButtonWithStateBackgrounds.swift; sourceTree = ""; }; + C63F720A285A24B10066163B /* StyledTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyledTextView.swift; sourceTree = ""; }; + C63F720B285A24B10066163B /* ProviderDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProviderDelegate.swift; sourceTree = ""; }; + C63F720C285A24B10066163B /* VFSUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VFSUtil.swift; sourceTree = ""; }; + C63F7271285A2F140066163B /* voip_dropdown.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_dropdown.png; sourceTree = ""; }; + C63F7272285A2F140066163B /* voip_spinner.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_spinner.png; sourceTree = ""; }; + C63F7273285A2F140066163B /* voip_numpad_7.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_numpad_7.png; sourceTree = ""; }; + C63F7274285A2F150066163B /* voip_radio_off.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_radio_off.png; sourceTree = ""; }; + C63F7275285A2F150066163B /* voip_export.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_export.png; sourceTree = ""; }; + C63F7276285A2F150066163B /* voip_call_numpad.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_numpad.png; sourceTree = ""; }; + C63F7277285A2F150066163B /* voip_chat_rooms_list.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_chat_rooms_list.png; sourceTree = ""; }; + C63F7278285A2F150066163B /* voip_numpad_4.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_numpad_4.png; sourceTree = ""; }; + C63F7279285A2F150066163B /* voip_call_header_incoming.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_header_incoming.png; sourceTree = ""; }; + C63F727A285A2F150066163B /* voip_calls_list.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_calls_list.png; sourceTree = ""; }; + C63F727B285A2F160066163B /* voip_call_header_paused.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_header_paused.png; sourceTree = ""; }; + C63F727C285A2F160066163B /* voip_numpad_0.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_numpad_0.png; sourceTree = ""; }; + C63F727D285A2F160066163B /* voip_numpad_3.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_numpad_3.png; sourceTree = ""; }; + C63F727E285A2F160066163B /* voip_numpad_9.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_numpad_9.png; sourceTree = ""; }; + C63F727F285A2F160066163B /* voip_speaker_on.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_speaker_on.png; sourceTree = ""; }; + C63F7280285A2F160066163B /* voip_audio_routes.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_audio_routes.png; sourceTree = ""; }; + C63F7281285A2F160066163B /* voip_call_record.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_record.png; sourceTree = ""; }; + C63F7282285A2F170066163B /* voip_call_forward.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_forward.png; sourceTree = ""; }; + C63F7283285A2F170066163B /* voip_change_camera.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_change_camera.png; sourceTree = ""; }; + C63F7284285A2F170066163B /* voip_checkbox_checked.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_checkbox_checked.png; sourceTree = ""; }; + C63F7285285A2F170066163B /* voip_info.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_info.png; sourceTree = ""; }; + C63F7286285A2F170066163B /* voip_speaker_off.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_speaker_off.png; sourceTree = ""; }; + C63F7287285A2F170066163B /* voip_call_more.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_more.png; sourceTree = ""; }; + C63F7288285A2F170066163B /* voip_call_stats.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_stats.png; sourceTree = ""; }; + C63F7289285A2F180066163B /* voip_numpad_8.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_numpad_8.png; sourceTree = ""; }; + C63F728A285A2F180066163B /* voip_call_add.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_add.png; sourceTree = ""; }; + C63F728B285A2F180066163B /* voip_copy.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_copy.png; sourceTree = ""; }; + C63F728C285A2F180066163B /* voip_conference_paused_big.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_conference_paused_big.png; sourceTree = ""; }; + C63F728D285A2F180066163B /* voip_numpad_star.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_numpad_star.png; sourceTree = ""; }; + C63F728E285A2F180066163B /* voip_numpad_hash.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_numpad_hash.png; sourceTree = ""; }; + C63F728F285A2F180066163B /* voip_multiple_contacts_avatar.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_multiple_contacts_avatar.png; sourceTree = ""; }; + C63F7290285A2F180066163B /* voip_remote_recording.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_remote_recording.png; sourceTree = ""; }; + C63F7291285A2F190066163B /* voip_hangup.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_hangup.png; sourceTree = ""; }; + C63F7292285A2F190066163B /* voip_pause.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_pause.png; sourceTree = ""; }; + C63F7293285A2F190066163B /* voip_numpad_1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_numpad_1.png; sourceTree = ""; }; + C63F7294285A2F190066163B /* voip_mandatory.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_mandatory.png; sourceTree = ""; }; + C63F7295285A2F190066163B /* voip_earpiece.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_earpiece.png; sourceTree = ""; }; + C63F7296285A2F190066163B /* voip_numpad_2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_numpad_2.png; sourceTree = ""; }; + C63F7297285A2F190066163B /* voip_conference_audio_only.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_conference_audio_only.png; sourceTree = ""; }; + C63F7298285A2F1A0066163B /* voip_menu_more.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_menu_more.png; sourceTree = ""; }; + C63F7299285A2F1A0066163B /* voip_conference_new.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_conference_new.png; sourceTree = ""; }; + C63F729A285A2F1A0066163B /* voip_call_header_active.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_header_active.png; sourceTree = ""; }; + C63F729B285A2F1A0066163B /* voip_bluetooth.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_bluetooth.png; sourceTree = ""; }; + C63F729C285A2F1A0066163B /* voip_micro_off.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_micro_off.png; sourceTree = ""; }; + C63F729D285A2F1A0066163B /* voip_camera_on.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_camera_on.png; sourceTree = ""; }; + C63F729E285A2F1A0066163B /* voip_conference_play_big.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_conference_play_big.png; sourceTree = ""; }; + C63F729F285A2F1B0066163B /* voip_call.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call.png; sourceTree = ""; }; + C63F72A0285A2F1B0066163B /* voip_call_list_menu.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_list_menu.png; sourceTree = ""; }; + C63F72A1285A2F1B0066163B /* voip_conference_active_speaker.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_conference_active_speaker.png; sourceTree = ""; }; + C63F72A2285A2F1B0066163B /* voip_numpad_6.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_numpad_6.png; sourceTree = ""; }; + C63F72A3285A2F1B0066163B /* voip_call_participants.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_participants.png; sourceTree = ""; }; + C63F72A4285A2F1B0066163B /* conference_schedule_calendar_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = conference_schedule_calendar_default.png; sourceTree = ""; }; + C63F72A5285A2F1B0066163B /* voip_call_chat.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_chat.png; sourceTree = ""; }; + C63F72A6285A2F1B0066163B /* voip_checkbox_unchecked.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_checkbox_unchecked.png; sourceTree = ""; }; + C63F72A7285A2F1C0066163B /* voip_edit.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_edit.png; sourceTree = ""; }; + C63F72A8285A2F1C0066163B /* conference_schedule_time_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = conference_schedule_time_default.png; sourceTree = ""; }; + C63F72A9285A2F1C0066163B /* voip_merge_calls.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_merge_calls.png; sourceTree = ""; }; + C63F72AA285A2F1C0066163B /* voip_camera_off.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_camera_off.png; sourceTree = ""; }; + C63F72AB285A2F1C0066163B /* voip_micro_on.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_micro_on.png; sourceTree = ""; }; + C63F72AC285A2F1C0066163B /* voip_radio_on.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_radio_on.png; sourceTree = ""; }; + C63F72AD285A2F1C0066163B /* conference_schedule_participants_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = conference_schedule_participants_default.png; sourceTree = ""; }; + C63F72AE285A2F1D0066163B /* voip_conference_mosaic.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_conference_mosaic.png; sourceTree = ""; }; + C63F72AF285A2F1D0066163B /* voip_numpad_5.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_numpad_5.png; sourceTree = ""; }; + C63F72B0285A2F1D0066163B /* voip_conference_new_selected.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_conference_new_selected.png; sourceTree = ""; }; + C63F72B1285A2F1D0066163B /* voip_single_contact_avatar.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_single_contact_avatar.png; sourceTree = ""; }; + C63F72B2285A2F1D0066163B /* voip_delete.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_delete.png; sourceTree = ""; }; + C63F72B3285A2F1D0066163B /* voip_cancel.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_cancel.png; sourceTree = ""; }; + C63F72B4285A2F1D0066163B /* voip_call_header_outgoing.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_call_header_outgoing.png; sourceTree = ""; }; + C63F72FA285A31DA0066163B /* Roboto-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Regular.ttf"; sourceTree = ""; }; + C63F72FB285A31DA0066163B /* Roboto-Italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Italic.ttf"; sourceTree = ""; }; + C63F72FC285A31DA0066163B /* Roboto-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Roboto-Bold.ttf"; sourceTree = ""; }; C64A854C2667B66900252AD2 /* EphemeralSettingsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EphemeralSettingsView.h; sourceTree = ""; }; C64A854D2667B67200252AD2 /* EphemeralSettingsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EphemeralSettingsView.m; sourceTree = ""; }; C64A854F2667B67A00252AD2 /* EphemeralSettingsView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = EphemeralSettingsView.xib; sourceTree = ""; }; @@ -1761,7 +2027,6 @@ C6B4444026AAD0970076C517 /* file_audio_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = file_audio_default.png; sourceTree = ""; }; C6B4444126AAD0970076C517 /* file_pdf_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = file_pdf_default.png; sourceTree = ""; }; C6B4444726AADA530076C517 /* SwiftUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUtil.swift; sourceTree = ""; }; - C6DA657B261C950C0020CB43 /* VFSUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VFSUtil.swift; sourceTree = ""; }; C90FAA7615AF54E6002091CB /* HistoryDetailsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HistoryDetailsView.h; sourceTree = ""; }; C90FAA7715AF54E6002091CB /* HistoryDetailsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HistoryDetailsView.m; sourceTree = ""; }; C9B3A6FD15B485DB006F52EE /* Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Utils.h; path = Utils/Utils.h; sourceTree = ""; }; @@ -1789,8 +2054,6 @@ D306459D1611EC2900BB571E /* UILoadingImageView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UILoadingImageView.m; sourceTree = ""; }; D3128FDE15AABC7E00A2147A /* ContactDetailsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactDetailsView.h; sourceTree = ""; }; D3128FDF15AABC7E00A2147A /* ContactDetailsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContactDetailsView.m; sourceTree = ""; }; - D31AAF5C159B3919002C6B02 /* CallPausedTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CallPausedTableView.h; sourceTree = ""; }; - D31AAF5D159B3919002C6B02 /* CallPausedTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallPausedTableView.m; sourceTree = ""; }; D31B4B1E159876C0002E6C72 /* UICompositeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UICompositeView.h; sourceTree = ""; }; D31B4B1F159876C0002E6C72 /* UICompositeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UICompositeView.m; sourceTree = ""; }; D31C9C96158A1CDE00756B45 /* UIHistoryCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIHistoryCell.h; sourceTree = ""; }; @@ -1818,8 +2081,6 @@ D35E7595159460560066B1C1 /* ChatsListView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatsListView.m; sourceTree = ""; }; D35E759C159460B50066B1C1 /* SettingsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsView.h; sourceTree = ""; }; D35E759D159460B50066B1C1 /* SettingsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsView.m; sourceTree = ""; }; - D36FB2D31589EF7C0036F6F2 /* UIPauseButton.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIPauseButton.h; sourceTree = ""; }; - D36FB2D41589EF7C0036F6F2 /* UIPauseButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIPauseButton.m; sourceTree = ""; }; D378AB2815DCDB480098505D /* ImagePickerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImagePickerView.h; sourceTree = ""; }; D378AB2915DCDB490098505D /* ImagePickerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImagePickerView.m; sourceTree = ""; }; D37C639915AADEF4009D0BAC /* ContactDetailsTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactDetailsTableView.h; sourceTree = ""; }; @@ -1886,14 +2147,10 @@ D3ED3EA51587334C006C0DE4 /* HistoryListTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = HistoryListTableView.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; D3ED3EB515873928006C0DE4 /* HistoryListView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HistoryListView.h; sourceTree = ""; }; D3ED3EB615873929006C0DE4 /* HistoryListView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = HistoryListView.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; - D3F26BEE15986B71005F9CAB /* CallIncomingView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CallIncomingView.h; sourceTree = ""; }; - D3F26BEF15986B71005F9CAB /* CallIncomingView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallIncomingView.m; sourceTree = ""; }; D3F795D315A582800077328B /* ChatConversationView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChatConversationView.h; sourceTree = ""; }; D3F795D415A582800077328B /* ChatConversationView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChatConversationView.m; sourceTree = ""; }; D3F7997F15BD32370018C273 /* TPMultiLayoutViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TPMultiLayoutViewController.h; path = Utils/TPMultiLayoutViewController/TPMultiLayoutViewController.h; sourceTree = ""; }; D3F7998015BD32370018C273 /* TPMultiLayoutViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TPMultiLayoutViewController.m; path = Utils/TPMultiLayoutViewController/TPMultiLayoutViewController.m; sourceTree = ""; }; - D3F83EE91582021700336684 /* CallView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CallView.h; sourceTree = ""; }; - D3F83EEA1582021700336684 /* CallView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallView.m; sourceTree = ""; }; D3F83F8C158229C500336684 /* PhoneMainView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhoneMainView.h; sourceTree = ""; }; D3F83F8D15822ABD00336684 /* PhoneMainView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhoneMainView.m; sourceTree = ""; }; DF241FDC6C7431777AB3BD58 /* Pods-msgNotificationContent.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-msgNotificationContent.debug.xcconfig"; path = "Target Support Files/Pods-msgNotificationContent/Pods-msgNotificationContent.debug.xcconfig"; sourceTree = ""; }; @@ -1926,8 +2183,6 @@ F09548201883F15400E8A69B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/HistoryDetailsView.xib; sourceTree = ""; }; F09548211883F15400E8A69B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/HistoryListView.xib; sourceTree = ""; }; F09548221883F15400E8A69B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/ImageView.xib; sourceTree = ""; }; - F09548231883F15400E8A69B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/CallView.xib; sourceTree = ""; }; - F09548241883F15400E8A69B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/CallIncomingView.xib; sourceTree = ""; }; F095482C1883F15400E8A69B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/TabBarView.xib; sourceTree = ""; }; F095482E1883F15500E8A69B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/AssistantView.xib; sourceTree = ""; }; F09548301883F15500E8A69B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/AssistantViewScreens.xib; sourceTree = ""; }; @@ -1938,8 +2193,6 @@ F095483C1883F2E300E8A69B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/DialerView.strings; sourceTree = ""; }; F09548421883F51B00E8A69B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/HistoryListView.strings; sourceTree = ""; }; F09548441883F52900E8A69B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/ImageView.strings; sourceTree = ""; }; - F09548461883F54200E8A69B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/CallView.strings; sourceTree = ""; }; - F09548481883F55800E8A69B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/CallIncomingView.strings; sourceTree = ""; }; F09548561883F61600E8A69B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/TabBarView.strings; sourceTree = ""; }; F095485A1883F67B00E8A69B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/AssistantView.strings; sourceTree = ""; }; F095485E1883F6EA00E8A69B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/AssistantViewScreens.strings; sourceTree = ""; }; @@ -1951,8 +2204,6 @@ F0AF06F91A24BA760086C9C1 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/HistoryDetailsView.strings; sourceTree = ""; }; F0AF06FA1A24BA770086C9C1 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/HistoryListView.strings; sourceTree = ""; }; F0AF06FB1A24BA770086C9C1 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/ImageView.strings; sourceTree = ""; }; - F0AF06FC1A24BA770086C9C1 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/CallView.strings; sourceTree = ""; }; - F0AF06FD1A24BA770086C9C1 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/CallIncomingView.strings; sourceTree = ""; }; F0AF07021A24BA770086C9C1 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/UIChatCell.strings; sourceTree = ""; }; F0AF07041A24BA770086C9C1 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/UICompositeView.strings; sourceTree = ""; }; F0AF07061A24BA770086C9C1 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/UIContactCell.strings; sourceTree = ""; }; @@ -2045,6 +2296,8 @@ 080E96DDFE201D6D7F000001 /* Classes */ = { isa = PBXGroup; children = ( + 614C087623D1A35E00217F80 /* linphone-Bridging-Header.h */, + C63F7196285A24B10066163B /* Swift */, 22E0A81D111C44E100B04932 /* AboutView.h */, 22E0A81C111C44E100B04932 /* AboutView.m */, 636316D31A1DEBCB0009B839 /* AboutView.xib */, @@ -2055,23 +2308,6 @@ D350F20C15A43BB100149E54 /* AssistantView.m */, D38187E015FE348A00C3EDCA /* AssistantView.xib */, D3D5126A160B3A8E00946DF8 /* AssistantViewScreens.xib */, - 63F1DF491BCE983100EDED90 /* CallConferenceTableView.h */, - 63F1DF4A1BCE983200EDED90 /* CallConferenceTableView.m */, - D3F26BEE15986B71005F9CAB /* CallIncomingView.h */, - D3F26BEF15986B71005F9CAB /* CallIncomingView.m */, - D38187DC15FE347700C3EDCA /* CallIncomingView.xib */, - 6346100D1B61409800548952 /* CallOutgoingView.h */, - 6346100E1B61409800548952 /* CallOutgoingView.m */, - 634610101B6140A500548952 /* CallOutgoingView.xib */, - D31AAF5C159B3919002C6B02 /* CallPausedTableView.h */, - D31AAF5D159B3919002C6B02 /* CallPausedTableView.m */, - 6352A5721BE0D4B800594C1C /* CallSideMenuView.h */, - 6352A5731BE0D4B800594C1C /* CallSideMenuView.m */, - 6352A5741BE0D4B800594C1C /* CallSideMenuView.xib */, - D3F83EE91582021700336684 /* CallView.h */, - D3F83EEA1582021700336684 /* CallView.m */, - D381881C15FE3FCA00C3EDCA /* CallView.xib */, - 638F1A861C2167C2004B8E02 /* CallView~ipad.xib */, 8C9C5E0B1F83B2EF006987FA /* ChatConversationCreateCollectionViewController.h */, 8C9C5E0C1F83B2EF006987FA /* ChatConversationCreateCollectionViewController.m */, 6341807A1BBC103100F71761 /* ChatConversationCreateTableView.h */, @@ -2178,15 +2414,7 @@ D3ED3E851586291B006C0DE4 /* TabBarView.m */, D38187FB15FE355D00C3EDCA /* TabBarView.xib */, D326483415887D4400930C67 /* Utils */, - 34216F3E1547EBCD00EA9777 /* VideoZoomHandler.h */, - 34216F3F1547EBCD00EA9777 /* VideoZoomHandler.m */, - C6DA657B261C950C0020CB43 /* VFSUtil.swift */, - 614C087723D1A35F00217F80 /* ProviderDelegate.swift */, - 614C087923D1A37400217F80 /* CallManager.swift */, 669B140B27A29D140012220A /* FloatingScrollDownButton.swift */, - 6134812C2406CECC00695B41 /* ConfigManager.swift */, - 6134812E2407B35200695B41 /* AppManager.swift */, - 614C087623D1A35E00217F80 /* linphone-Bridging-Header.h */, ); path = Classes; sourceTree = ""; @@ -2216,18 +2444,10 @@ 633FEF571D3CD5E00014B822 /* UIAvatarPresence.m */, 6377AC7E1BDE4068007F7625 /* UIBackToCallButton.h */, 6377AC7F1BDE4069007F7625 /* UIBackToCallButton.m */, - 22C7555E1317E59C007BC101 /* UIBluetoothButton.h */, - 22C7555F1317E59C007BC101 /* UIBluetoothButton.m */, 6381DA7B1C1AD5EA00DF3BBD /* UIBouncingView.h */, 6381DA7C1C1AD5EA00DF3BBD /* UIBouncingView.m */, 2214EB7812F846B1002A5394 /* UICallButton.h */, 2214EB7912F846B1002A5394 /* UICallButton.m */, - 63F1DF4C1BCE985F00EDED90 /* UICallConferenceCell.h */, - 63F1DF4D1BCE985F00EDED90 /* UICallConferenceCell.m */, - 63F1DF531BCE986A00EDED90 /* UICallConferenceCell.xib */, - 63BC49E01BA2CDFC004EC273 /* UICallPausedCell.h */, - 63BC49E11BA2CDFC004EC273 /* UICallPausedCell.m */, - 639E9C9F1C0DB7DF00019A75 /* UICallPausedCell.xib */, 22AA8AFF13D83F6300B30535 /* UICamSwitch.h */, 22AA8B0013D83F6300B30535 /* UICamSwitch.m */, 635173F71BA082A40095EB0A /* UIChatBubblePhotoCell.h */, @@ -2276,8 +2496,6 @@ 61CCC3D721933B380060EDEA /* UIDeviceCell.h */, 61CCC3DE21933B580060EDEA /* UIDeviceCell.m */, 61CCC3E021933B660060EDEA /* UIDeviceCell.xib */, - 2214EB8712F84EBB002A5394 /* UIHangUpButton.h */, - 2214EB8812F84EBB002A5394 /* UIHangUpButton.m */, D31C9C96158A1CDE00756B45 /* UIHistoryCell.h */, D31C9C97158A1CDE00756B45 /* UIHistoryCell.m */, 639CEB021A1DF4E4004DE38F /* UIHistoryCell.xib */, @@ -2297,10 +2515,6 @@ CF1DE92B210A0F5B00A0A97E /* UILinphoneAudioPlayer.xib */, D306459C1611EC2900BB571E /* UILoadingImageView.h */, D306459D1611EC2900BB571E /* UILoadingImageView.m */, - 2214EBF112F86360002A5394 /* UIMutedMicroButton.h */, - 2214EBF212F86360002A5394 /* UIMutedMicroButton.m */, - D36FB2D31589EF7C0036F6F2 /* UIPauseButton.h */, - D36FB2D41589EF7C0036F6F2 /* UIPauseButton.m */, CF7602E42108759A00749F76 /* UIRecordingCell.h */, CF7602E52108759A00749F76 /* UIRecordingCell.m */, CF7602E62108759A00749F76 /* UIRecordingCell.xib */, @@ -2311,8 +2525,6 @@ 8C5D1B991D9BC48100DC6539 /* UIShopTableCell.h */, 8C5D1B9B1D9BC48100DC6539 /* UIShopTableCell.xib */, 24A345A71D95799900881A5C /* UIShopTableCell.h */, - 22968A5D12F875C600588287 /* UISpeakerButton.h */, - 22968A5E12F875C600588287 /* UISpeakerButton.m */, 630CF5551AF7CE1500539F7A /* UITextField+DoneButton.h */, 630CF5561AF7CE1500539F7A /* UITextField+DoneButton.m */, F03CA84118C72F1A0008889D /* UITextViewNoDefine.h */, @@ -2448,6 +2660,74 @@ 633FEBE11D3CD5570014B822 /* images */ = { isa = PBXGroup; children = ( + C63F72A4285A2F1B0066163B /* conference_schedule_calendar_default.png */, + C63F72AD285A2F1C0066163B /* conference_schedule_participants_default.png */, + C63F72A8285A2F1C0066163B /* conference_schedule_time_default.png */, + C63F7280285A2F160066163B /* voip_audio_routes.png */, + C63F729B285A2F1A0066163B /* voip_bluetooth.png */, + C63F728A285A2F180066163B /* voip_call_add.png */, + C63F72A5285A2F1B0066163B /* voip_call_chat.png */, + C63F7282285A2F170066163B /* voip_call_forward.png */, + C63F729A285A2F1A0066163B /* voip_call_header_active.png */, + C63F7279285A2F150066163B /* voip_call_header_incoming.png */, + C63F72B4285A2F1D0066163B /* voip_call_header_outgoing.png */, + C63F727B285A2F160066163B /* voip_call_header_paused.png */, + C63F72A0285A2F1B0066163B /* voip_call_list_menu.png */, + C63F7287285A2F170066163B /* voip_call_more.png */, + C63F7276285A2F150066163B /* voip_call_numpad.png */, + C63F72A3285A2F1B0066163B /* voip_call_participants.png */, + C63F7281285A2F160066163B /* voip_call_record.png */, + C63F7288285A2F170066163B /* voip_call_stats.png */, + C63F729F285A2F1B0066163B /* voip_call.png */, + C63F727A285A2F150066163B /* voip_calls_list.png */, + C63F72AA285A2F1C0066163B /* voip_camera_off.png */, + C63F729D285A2F1A0066163B /* voip_camera_on.png */, + C63F72B3285A2F1D0066163B /* voip_cancel.png */, + C63F7283285A2F170066163B /* voip_change_camera.png */, + C63F7277285A2F150066163B /* voip_chat_rooms_list.png */, + C63F7284285A2F170066163B /* voip_checkbox_checked.png */, + C63F72A6285A2F1B0066163B /* voip_checkbox_unchecked.png */, + C63F72A1285A2F1B0066163B /* voip_conference_active_speaker.png */, + C63F7297285A2F190066163B /* voip_conference_audio_only.png */, + C63F72AE285A2F1D0066163B /* voip_conference_mosaic.png */, + C63F72B0285A2F1D0066163B /* voip_conference_new_selected.png */, + C63F7299285A2F1A0066163B /* voip_conference_new.png */, + C63F728C285A2F180066163B /* voip_conference_paused_big.png */, + C63F729E285A2F1A0066163B /* voip_conference_play_big.png */, + C63F728B285A2F180066163B /* voip_copy.png */, + C63F72B2285A2F1D0066163B /* voip_delete.png */, + C63F7271285A2F140066163B /* voip_dropdown.png */, + C63F7295285A2F190066163B /* voip_earpiece.png */, + C63F72A7285A2F1C0066163B /* voip_edit.png */, + C63F7275285A2F150066163B /* voip_export.png */, + C63F7291285A2F190066163B /* voip_hangup.png */, + C63F7285285A2F170066163B /* voip_info.png */, + C63F7294285A2F190066163B /* voip_mandatory.png */, + C63F7298285A2F1A0066163B /* voip_menu_more.png */, + C63F72A9285A2F1C0066163B /* voip_merge_calls.png */, + C63F729C285A2F1A0066163B /* voip_micro_off.png */, + C63F72AB285A2F1C0066163B /* voip_micro_on.png */, + C63F728F285A2F180066163B /* voip_multiple_contacts_avatar.png */, + C63F727C285A2F160066163B /* voip_numpad_0.png */, + C63F7293285A2F190066163B /* voip_numpad_1.png */, + C63F7296285A2F190066163B /* voip_numpad_2.png */, + C63F727D285A2F160066163B /* voip_numpad_3.png */, + C63F7278285A2F150066163B /* voip_numpad_4.png */, + C63F72AF285A2F1D0066163B /* voip_numpad_5.png */, + C63F72A2285A2F1B0066163B /* voip_numpad_6.png */, + C63F7273285A2F140066163B /* voip_numpad_7.png */, + C63F7289285A2F180066163B /* voip_numpad_8.png */, + C63F727E285A2F160066163B /* voip_numpad_9.png */, + C63F728E285A2F180066163B /* voip_numpad_hash.png */, + C63F728D285A2F180066163B /* voip_numpad_star.png */, + C63F7292285A2F190066163B /* voip_pause.png */, + C63F7274285A2F150066163B /* voip_radio_off.png */, + C63F72AC285A2F1C0066163B /* voip_radio_on.png */, + C63F7290285A2F180066163B /* voip_remote_recording.png */, + C63F72B1285A2F1D0066163B /* voip_single_contact_avatar.png */, + C63F7286285A2F170066163B /* voip_speaker_off.png */, + C63F727F285A2F160066163B /* voip_speaker_on.png */, + C63F7272285A2F140066163B /* voip_spinner.png */, C66B040D26F095CE009B5EDC /* cancel_forward.png */, C66B040926EFDA54009B5EDC /* reply_cancel.png */, C6A1BB4426E890BD00540D50 /* file_voice_default.png */, @@ -2988,6 +3268,7 @@ 63AADBC31B6A0FF200AA16FD /* Resources */ = { isa = PBXGroup; children = ( + C63F72F9285A31DA0066163B /* fonts */, 63AADBE31B6A0FF200AA16FD /* assistant_external_sip.rc */, 63AADBE41B6A0FF200AA16FD /* assistant_linphone_create.rc */, 63AADBE51B6A0FF200AA16FD /* assistant_linphone_existing.rc */, @@ -3059,6 +3340,281 @@ path = Pods; sourceTree = ""; }; + C63F7196285A24B10066163B /* Swift */ = { + isa = PBXGroup; + children = ( + C6B4444726AADA530076C517 /* SwiftUtil.swift */, + C63F7197285A24B10066163B /* ConfigManager.swift */, + C63F7198285A24B10066163B /* CallManager.swift */, + C63F720B285A24B10066163B /* ProviderDelegate.swift */, + C63F720C285A24B10066163B /* VFSUtil.swift */, + C63F71B1285A24B10066163B /* AppManager.swift */, + C63F7199285A24B10066163B /* Conference */, + C63F71AA285A24B10066163B /* Util */, + C63F71B2285A24B10066163B /* Extensions */, + C63F71C7285A24B10066163B /* Voip */, + ); + path = Swift; + sourceTree = ""; + }; + C63F7199285A24B10066163B /* Conference */ = { + isa = PBXGroup; + children = ( + C63F719A285A24B10066163B /* ViewModels */, + C63F719E285A24B10066163B /* Data */, + C63F71A2285A24B10066163B /* Views */, + ); + path = Conference; + sourceTree = ""; + }; + C63F719A285A24B10066163B /* ViewModels */ = { + isa = PBXGroup; + children = ( + C63F719B285A24B10066163B /* ConferenceWaitingRoomViewModel.swift */, + C63F719C285A24B10066163B /* ConferenceSchedulingViewModel.swift */, + C63F719D285A24B10066163B /* ScheduledConferencesViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + C63F719E285A24B10066163B /* Data */ = { + isa = PBXGroup; + children = ( + C63F719F285A24B10066163B /* ScheduledConferenceData.swift */, + C63F71A0285A24B10066163B /* TimeZoneData.swift */, + C63F71A1285A24B10066163B /* Duration.swift */, + ); + path = Data; + sourceTree = ""; + }; + C63F71A2285A24B10066163B /* Views */ = { + isa = PBXGroup; + children = ( + C63F71A3285A24B10066163B /* ConferenceWaitingRoomFragment.swift */, + C63F71A4285A24B10066163B /* ScheduledConferencesView.swift */, + C63F71A5285A24B10066163B /* ICSBubbleView.swift */, + C63F71A6285A24B10066163B /* ScheduledConferencesCell.swift */, + C63F71A7285A24B10066163B /* ConferenceHistoryDetailsView.swift */, + C63F71A8285A24B10066163B /* ConferenceSchedulingView.swift */, + C63F71A9285A24B10066163B /* ConferenceSchedulingSummaryView.swift */, + ); + path = Views; + sourceTree = ""; + }; + C63F71AA285A24B10066163B /* Util */ = { + isa = PBXGroup; + children = ( + C63F71AB285A24B10066163B /* ViewModel */, + C63F71AD285A24B10066163B /* MutableLiveData.swift */, + C63F71AE285A24B10066163B /* Pair.swift */, + C63F71AF285A24B10066163B /* BackNextNavigationView.swift */, + C63F71B0285A24B10066163B /* TimestampUtils.swift */, + ); + path = Util; + sourceTree = ""; + }; + C63F71AB285A24B10066163B /* ViewModel */ = { + isa = PBXGroup; + children = ( + C63F71AC285A24B10066163B /* MediatorLiveData.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + C63F71B2285A24B10066163B /* Extensions */ = { + isa = PBXGroup; + children = ( + C63F71B3285A24B10066163B /* IOS */, + C63F71BF285A24B10066163B /* LinphoneCore */, + ); + path = Extensions; + sourceTree = ""; + }; + C63F71B3285A24B10066163B /* IOS */ = { + isa = PBXGroup; + children = ( + C63F71B4285A24B10066163B /* UIApplication+Extension.swift */, + C63F71B6285A24B10066163B /* UIVIewControllerExtensions.swift */, + C63F71B7285A24B10066163B /* UIImageExtensions.swift */, + C63F71B8285A24B10066163B /* UIVIewExtensions.swift */, + C63F71B9285A24B10066163B /* UILabelExtensions.swift */, + C63F71BA285A24B10066163B /* OptionalExtensions.swift */, + C63F71BB285A24B10066163B /* UIButtonExtensions.swift */, + C63F71BC285A24B10066163B /* UIImageViewExtensions.swift */, + C63F71BD285A24B10066163B /* UIDeviceExtensions.swift */, + C63F71BE285A24B10066163B /* UIColorExtensions.swift */, + ); + path = IOS; + sourceTree = ""; + }; + C63F71BF285A24B10066163B /* LinphoneCore */ = { + isa = PBXGroup; + children = ( + C63F71C0285A24B10066163B /* CoreExtensions.swift */, + C63F71C1285A24B10066163B /* IceState.swift */, + C63F71C2285A24B10066163B /* AddressExtensions.swift */, + C63F71C3285A24B10066163B /* ParticipantExtensions.swift */, + C63F71C4285A24B10066163B /* PayloadType.swift */, + C63F71C5285A24B10066163B /* CallExtensions.swift */, + C63F71C6285A24B10066163B /* ConferenceExtensions.swift */, + ); + path = LinphoneCore; + sourceTree = ""; + }; + C63F71C7285A24B10066163B /* Voip */ = { + isa = PBXGroup; + children = ( + C63F71C8285A24B10066163B /* ViewModels */, + C63F71D0285A24B10066163B /* AudioRouteUtils.swift */, + C63F71D1285A24B10066163B /* Theme */, + C63F71D7285A24B10066163B /* Views */, + C63F71FB285A24B10066163B /* VoipDialog.swift */, + C63F71FC285A24B10066163B /* Widgets */, + ); + path = Voip; + sourceTree = ""; + }; + C63F71C8285A24B10066163B /* ViewModels */ = { + isa = PBXGroup; + children = ( + C63F71C9285A24B10066163B /* ConferenceViewModel.swift */, + C63F71CA285A24B10066163B /* CallsViewModel.swift */, + C63F71CB285A24B10066163B /* ControlsViewModel.swift */, + C63F71CC285A24B10066163B /* CallStatisticsData.swift */, + C63F71CD285A24B10066163B /* ConferenceParticipantData.swift */, + C63F71CE285A24B10066163B /* ConferenceParticipantDeviceData.swift */, + C63F71CF285A24B10066163B /* CallData.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + C63F71D1285A24B10066163B /* Theme */ = { + isa = PBXGroup; + children = ( + C63F71D2285A24B10066163B /* LightDarkColor.swift */, + C63F71D3285A24B10066163B /* TextStyle.swift */, + C63F71D4285A24B10066163B /* VoipTexts.swift */, + C63F71D5285A24B10066163B /* ButtonTheme.swift */, + C63F71D6285A24B10066163B /* VoipTheme.swift */, + ); + path = Theme; + sourceTree = ""; + }; + C63F71D7285A24B10066163B /* Views */ = { + isa = PBXGroup; + children = ( + C63F71D8285A24B10066163B /* Fragments */, + C63F71F6285A24B10066163B /* CompositeViewControllers */, + C63F71FA285A24B10066163B /* SharedLayoutConstants.swift */, + ); + path = Views; + sourceTree = ""; + }; + C63F71D8285A24B10066163B /* Fragments */ = { + isa = PBXGroup; + children = ( + C63F71D9285A24B10066163B /* ParticipantsList */, + C63F71DC285A24B10066163B /* AudioRoutesView.swift */, + C63F71DD285A24B10066163B /* Conference */, + C63F71E6285A24B10066163B /* ActiveCall */, + C63F71E8285A24B10066163B /* IncomingOuntgoingCommonView.swift */, + C63F71E9285A24B10066163B /* PausedCallOrConferenceView.swift */, + C63F71EA285A24B10066163B /* LocalVideoView.swift */, + C63F71EB285A24B10066163B /* CallStatsView.swift */, + C63F71EC285A24B10066163B /* NumpadView.swift */, + C63F71ED285A24B10066163B /* VoipExtraButtonsView.swift */, + C63F71EE285A24B10066163B /* CallsList */, + C63F71F2285A24B10066163B /* DismissableView.swift */, + C63F71F3285A24B10066163B /* ConferenceLayoutPickerView.swift */, + C63F71F4285A24B10066163B /* ControlsView.swift */, + C63F71F5285A24B10066163B /* RemotelyRecording.swift */, + ); + path = Fragments; + sourceTree = ""; + }; + C63F71D9285A24B10066163B /* ParticipantsList */ = { + isa = PBXGroup; + children = ( + C63F71DA285A24B10066163B /* ParticipantsListView.swift */, + C63F71DB285A24B10066163B /* VoipParticipantCell.swift */, + ); + path = ParticipantsList; + sourceTree = ""; + }; + C63F71DD285A24B10066163B /* Conference */ = { + isa = PBXGroup; + children = ( + C63F71DE285A24B10066163B /* VoipActiveSpeakerParticipantCell.swift */, + C63F71DF285A24B10066163B /* VoipConferenceAudioOnlyView.swift */, + C63F71E0285A24B10066163B /* VoipGridParticipantCell.swift */, + C63F71E1285A24B10066163B /* VoipAudioOnlyParticipantCell.swift */, + C63F71E2285A24B10066163B /* MicMuted.swift */, + C63F71E3285A24B10066163B /* VoipConferenceGridView.swift */, + C63F71E4285A24B10066163B /* VoipConferenceActiveSpeakerView.swift */, + C63F71E5285A24B10066163B /* VoipConferenceDisplayModeSelectionView.swift */, + ); + path = Conference; + sourceTree = ""; + }; + C63F71E6285A24B10066163B /* ActiveCall */ = { + isa = PBXGroup; + children = ( + C63F71E7285A24B10066163B /* ActiveCallView.swift */, + ); + path = ActiveCall; + sourceTree = ""; + }; + C63F71EE285A24B10066163B /* CallsList */ = { + isa = PBXGroup; + children = ( + C63F71EF285A24B10066163B /* VoipCallContextMenu.swift */, + C63F71F0285A24B10066163B /* CallsListView.swift */, + C63F71F1285A24B10066163B /* VoipCallCell.swift */, + ); + path = CallsList; + sourceTree = ""; + }; + C63F71F6285A24B10066163B /* CompositeViewControllers */ = { + isa = PBXGroup; + children = ( + C63F71F7285A24B10066163B /* OutgoingCallView.swift */, + C63F71F8285A24B10066163B /* ActiveCallOrConferenceView.swift */, + C63F71F9285A24B10066163B /* IncomingCallView.swift */, + ); + path = CompositeViewControllers; + sourceTree = ""; + }; + C63F71FC285A24B10066163B /* Widgets */ = { + isa = PBXGroup; + children = ( + C63F71FD285A24B10066163B /* StyledValuePicker.swift */, + C63F71FE285A24B10066163B /* StyledSwitch.swift */, + C63F71FF285A24B10066163B /* CallControlButton.swift */, + C63F7200285A24B10066163B /* RotatingSpinner.swift */, + C63F7201285A24B10066163B /* FormButton.swift */, + C63F7202285A24B10066163B /* BouncingCounter.swift */, + C63F7203285A24B10066163B /* VoipExtraButton.swift */, + C63F7204285A24B10066163B /* UICallTimer.swift */, + C63F7205285A24B10066163B /* StyledCheckBox.swift */, + C63F7206285A24B10066163B /* Avatar.swift */, + C63F7207285A24B10066163B /* StyledLabel.swift */, + C63F7208285A24B10066163B /* StyledDatePicker.swift */, + C63F7209285A24B10066163B /* ButtonWithStateBackgrounds.swift */, + C63F720A285A24B10066163B /* StyledTextView.swift */, + ); + path = Widgets; + sourceTree = ""; + }; + C63F72F9285A31DA0066163B /* fonts */ = { + isa = PBXGroup; + children = ( + C63F72FA285A31DA0066163B /* Roboto-Regular.ttf */, + C63F72FB285A31DA0066163B /* Roboto-Italic.ttf */, + C63F72FC285A31DA0066163B /* Roboto-Bold.ttf */, + ); + path = fonts; + sourceTree = ""; + }; D326483415887D4400930C67 /* Utils */ = { isa = PBXGroup; children = ( @@ -3085,9 +3641,6 @@ 63D11C541C3D503A00E8FCEE /* Log.h */, 63423C081C4501D000D9A050 /* Contact.h */, 63423C091C4501D000D9A050 /* Contact.m */, - 8C1B67081E6718BC001EA2FE /* AudioHelper.h */, - 8C1B67051E671826001EA2FE /* AudioHelper.m */, - C6B4444726AADA530076C517 /* SwiftUtil.swift */, ); name = Utils; sourceTree = ""; @@ -3425,6 +3978,7 @@ 633FEDC41D3CD5590014B822 /* call_hangup_disabled.png in Resources */, C66B03BB26E8EB1A009B5EDC /* UIChatReplyBubbleView.xib in Resources */, 633FEDA81D3CD5590014B822 /* backspace_default.png in Resources */, + C63F72C0285A2F1D0066163B /* voip_numpad_0.png in Resources */, 636316D11A1DEBCB0009B839 /* AboutView.xib in Resources */, 8CBD7BA620B6B82400E5DCC0 /* UIChatConversationInfoTableViewCell.xib in Resources */, 244523AF1E8266CC0037A187 /* chat_delivered.png in Resources */, @@ -3453,34 +4007,42 @@ 638F1A621C2021B2004B8E02 /* DialerView~ipad.xib in Resources */, 615A2817217F280C0060F920 /* chat_list_indicator.png in Resources */, 633FEEFF1D3CD55A0014B822 /* options_add_call_disabled@2x.png in Resources */, + C63F72FD285A31DA0066163B /* Roboto-Regular.ttf in Resources */, 633FEF091D3CD55A0014B822 /* options_start_conference_disabled@2x.png in Resources */, C622E3F326A81290004F5434 /* vr_pause.png in Resources */, 633FEE051D3CD5590014B822 /* cancel_edit_disabled@2x.png in Resources */, 633FEE5F1D3CD5590014B822 /* edit_list_default@2x.png in Resources */, + C63F72F6285A2F1E0066163B /* voip_delete.png in Resources */, + C63F72F0285A2F1E0066163B /* voip_radio_on.png in Resources */, + C63F72EB285A2F1E0066163B /* voip_edit.png in Resources */, 633FEEB61D3CD55A0014B822 /* numpad_3_over@2x.png in Resources */, 633FEF371D3CD55A0014B822 /* routes_disabled@2x.png in Resources */, + C63F72D1285A2F1E0066163B /* voip_numpad_star.png in Resources */, 633FEE0F1D3CD5590014B822 /* chat_attachment_over@2x.png in Resources */, 633FEF381D3CD55A0014B822 /* routes_selected.png in Resources */, 633FEE0B1D3CD5590014B822 /* chat_attachment_default@2x.png in Resources */, 633FEED41D3CD55A0014B822 /* numpad_7_default@2x.png in Resources */, 633FEEE01D3CD55A0014B822 /* numpad_8_over~ipad@2x.png in Resources */, 633FEDDC1D3CD5590014B822 /* call_start_body_disabled~ipad.png in Resources */, + C63F72FF285A31DA0066163B /* Roboto-Bold.ttf in Resources */, 63E802DB1C625AEF000D5509 /* (null) in Resources */, 633FEE2E1D3CD5590014B822 /* color_F.png in Resources */, 633FEDC51D3CD5590014B822 /* call_hangup_disabled@2x.png in Resources */, 633FEEDF1D3CD55A0014B822 /* numpad_8_over~ipad.png in Resources */, + C63F72C3285A2F1D0066163B /* voip_speaker_on.png in Resources */, 8CD99A372090A824008A7CDA /* splashscreen@2x.png in Resources */, 24BFAAA9209B0630004F47A7 /* linphone_logo@2x.png in Resources */, 633FEE071D3CD5590014B822 /* chat_add_default@2x.png in Resources */, 633FEF551D3CD55A0014B822 /* waiting_time@2x.png in Resources */, 633FEEAC1D3CD55A0014B822 /* numpad_2_default@2x.png in Resources */, 633FEE541D3CD5590014B822 /* dialer_back_default.png in Resources */, + C63F72DE285A2F1E0066163B /* voip_call_header_active.png in Resources */, 633FEF0B1D3CD55A0014B822 /* options_transfer_call_default@2x.png in Resources */, 633FEDE81D3CD5590014B822 /* call_status_missed~ipad.png in Resources */, 63AADBFF1B6A0FF200AA16FD /* assistant_external_sip.rc in Resources */, 61CCC3E121933B660060EDEA /* UIDeviceCell.xib in Resources */, + C63F72F8285A2F1E0066163B /* voip_call_header_outgoing.png in Resources */, 633FEE9E1D3CD55A0014B822 /* numpad_0_over@2x.png in Resources */, - 634610121B6140A500548952 /* CallOutgoingView.xib in Resources */, 8CE24F581F8268850077AC0A /* conference_delete@2x.png in Resources */, 633FEDC21D3CD5590014B822 /* call_hangup_default.png in Resources */, 633FEEB41D3CD55A0014B822 /* numpad_3_default@2x.png in Resources */, @@ -3494,9 +4056,12 @@ 633FED9C1D3CD5590014B822 /* add_field_default.png in Resources */, 633FEE411D3CD5590014B822 /* contacts_all_selected@2x.png in Resources */, D38187F815FE355D00C3EDCA /* TabBarView.xib in Resources */, + C63F72D2285A2F1E0066163B /* voip_numpad_hash.png in Resources */, + C63F72F4285A2F1E0066163B /* voip_conference_new_selected.png in Resources */, 633FEE2F1D3CD5590014B822 /* color_G.png in Resources */, 633FEECE1D3CD55A0014B822 /* numpad_6_over@2x.png in Resources */, 8C92ABE81FA773190006FB5D /* UIChatNotifiedEventCell.xib in Resources */, + C63F72C7285A2F1E0066163B /* voip_change_camera.png in Resources */, 633FEDAB1D3CD5590014B822 /* backspace_disabled@2x.png in Resources */, 633FEDD91D3CD5590014B822 /* call_start_body_default~ipad@2x.png in Resources */, 633FEE401D3CD5590014B822 /* contacts_all_selected.png in Resources */, @@ -3533,12 +4098,12 @@ 633FEF1A1D3CD55A0014B822 /* presence_away.png in Resources */, 639E9CB51C0DB88200019A75 /* PhoneMainView.xib in Resources */, 633FEDB61D3CD5590014B822 /* call_alt_start_default.png in Resources */, - 6352A5761BE0D4B800594C1C /* CallSideMenuView.xib in Resources */, 633FEF0D1D3CD55A0014B822 /* options_transfer_call_disabled@2x.png in Resources */, 633FEDEF1D3CD5590014B822 /* call_transfer_default@2x.png in Resources */, 633FEF391D3CD55A0014B822 /* routes_selected@2x.png in Resources */, 633FEE491D3CD5590014B822 /* delete_default@2x.png in Resources */, 633FEF291D3CD55A0014B822 /* route_earpiece_default@2x.png in Resources */, + C63F72EA285A2F1E0066163B /* voip_checkbox_unchecked.png in Resources */, 633FEE271D3CD5590014B822 /* checkbox_checked@2x.png in Resources */, 61586B85217A17070038AC45 /* menu_assistant.png in Resources */, 633FEDCC1D3CD5590014B822 /* call_quality_indicator_0.png in Resources */, @@ -3569,12 +4134,14 @@ 633FEF301D3CD55A0014B822 /* route_speaker_disabled.png in Resources */, 639CEAFD1A1DF4D9004DE38F /* StatusBarView.xib in Resources */, 633FEDE91D3CD5590014B822 /* call_status_missed~ipad@2x.png in Resources */, + C63F72C9285A2F1E0066163B /* voip_info.png in Resources */, 8CE24F4C1F8234A30077AC0A /* next_default@2x.png in Resources */, 244523B11E8266CC0037A187 /* chat_read.png in Resources */, 61AEBEC62191E47500F35E7F /* chevron_list_close.png in Resources */, 617B4A60260A2B7800A87337 /* RecordingsListView.xib in Resources */, 639E9CAC1C0DB80300019A75 /* UIContactDetailsCell.xib in Resources */, 633FEE511D3CD5590014B822 /* deselect_all@2x.png in Resources */, + C63F72D4285A2F1E0066163B /* voip_remote_recording.png in Resources */, 8CF25D951F9F336100BEA0C1 /* check_unselected@2x.png in Resources */, F088488A19FF8C41007FFCF3 /* UIContactCell.xib in Resources */, 633FEE381D3CD5590014B822 /* contact_add_default.png in Resources */, @@ -3583,6 +4150,7 @@ 61586B8D217A173F0038AC45 /* menu_options.png in Resources */, 61AEBEC82191E48400F35E7F /* chevron_list_close@2x.png in Resources */, 633FEF341D3CD55A0014B822 /* routes_default.png in Resources */, + C63F72ED285A2F1E0066163B /* voip_merge_calls.png in Resources */, 633FEE061D3CD5590014B822 /* chat_add_default.png in Resources */, 633FEDF21D3CD5590014B822 /* call_video_start_default.png in Resources */, 633FEF491D3CD55A0014B822 /* speaker_selected@2x.png in Resources */, @@ -3598,6 +4166,7 @@ C64A85502667B67A00252AD2 /* EphemeralSettingsView.xib in Resources */, 633FEEC81D3CD55A0014B822 /* numpad_5_over~ipad@2x.png in Resources */, 61586B91217A175D0038AC45 /* menu_recordings.png in Resources */, + C63F72DA285A2F1E0066163B /* voip_numpad_2.png in Resources */, 633FEF1B1D3CD55A0014B822 /* presence_away@2x.png in Resources */, C6A1BB3A26E881E100540D50 /* menu_delete.png in Resources */, 633FEE281D3CD5590014B822 /* checkbox_unchecked.png in Resources */, @@ -3619,6 +4188,7 @@ 63CDC4661C3BDE370085F529 /* shortring.caf in Resources */, C6A1BB4126E889AD00540D50 /* forward_message_default.png in Resources */, 633FEDD51D3CD5590014B822 /* call_quality_indicator_4@2x.png in Resources */, + C63F72CA285A2F1E0066163B /* voip_speaker_off.png in Resources */, 633FEDE71D3CD5590014B822 /* call_status_missed@2x.png in Resources */, 615A2821217F6FBF0060F920 /* security_alert_indicator@2x.png in Resources */, 633FEE1E1D3CD5590014B822 /* chat_start_body_disabled.png in Resources */, @@ -3626,6 +4196,7 @@ 633FEE841D3CD5590014B822 /* led_error.png in Resources */, 633FEDEA1D3CD5590014B822 /* call_status_outgoing.png in Resources */, 633FEF511D3CD55A0014B822 /* valid_disabled@2x.png in Resources */, + C63F72C5285A2F1E0066163B /* voip_call_record.png in Resources */, 633FEEBB1D3CD55A0014B822 /* numpad_4_default.png in Resources */, 633FEF2B1D3CD55A0014B822 /* route_earpiece_disabled@2x.png in Resources */, 639E9CA31C0DB7EA00019A75 /* UIChatBubbleTextCell.xib in Resources */, @@ -3636,24 +4207,29 @@ 633FEF161D3CD55A0014B822 /* pause_small_disabled.png in Resources */, 633FEF521D3CD55A0014B822 /* voicemail.png in Resources */, 633FEE711D3CD5590014B822 /* footer_history_disabled@2x.png in Resources */, + C63F72E5285A2F1E0066163B /* voip_conference_active_speaker.png in Resources */, 633FEF141D3CD55A0014B822 /* pause_small_default.png in Resources */, D38187B115FE340500C3EDCA /* ChatsListView.xib in Resources */, 633FEDA41D3CD5590014B822 /* back_default.png in Resources */, + C63F72BE285A2F1D0066163B /* voip_calls_list.png in Resources */, + C63F72DF285A2F1E0066163B /* voip_bluetooth.png in Resources */, 633FEE2C1D3CD5590014B822 /* color_D.png in Resources */, 615A280F217F1FD50060F920 /* chat_add_group.png in Resources */, 633FEEC41D3CD55A0014B822 /* numpad_5_default@2x.png in Resources */, 633FEDAC1D3CD5590014B822 /* backspace_over.png in Resources */, - 639E9C9D1C0DB7DF00019A75 /* UICallPausedCell.xib in Resources */, 633FEE1B1D3CD5590014B822 /* chat_start_body_default@2x.png in Resources */, 633FEE021D3CD5590014B822 /* cancel_edit_default.png in Resources */, + C63F72D7285A2F1E0066163B /* voip_numpad_1.png in Resources */, 633FEEE31D3CD55A0014B822 /* numpad_9_default.png in Resources */, 633FEE651D3CD5590014B822 /* footer_chat_disabled@2x.png in Resources */, 633FEEDD1D3CD55A0014B822 /* numpad_8_over.png in Resources */, 633FEE8E1D3CD55A0014B822 /* list_details_default.png in Resources */, 633FEED71D3CD55A0014B822 /* numpad_7_over~ipad.png in Resources */, + C63F72D3285A2F1E0066163B /* voip_multiple_contacts_avatar.png in Resources */, 633FEF2A1D3CD55A0014B822 /* route_earpiece_disabled.png in Resources */, 633FEDDB1D3CD5590014B822 /* call_start_body_disabled@2x.png in Resources */, CFBD7A2A20E504AE007C5286 /* delete_img.png in Resources */, + C63F72C8285A2F1E0066163B /* voip_checkbox_checked.png in Resources */, 633FEDFD1D3CD5590014B822 /* camera_switch_default@2x.png in Resources */, 633FEEC51D3CD55A0014B822 /* numpad_5_over.png in Resources */, 633FEE721D3CD5590014B822 /* history_all_default.png in Resources */, @@ -3663,7 +4239,9 @@ 633FEDA51D3CD5590014B822 /* back_default@2x.png in Resources */, 633FEF311D3CD55A0014B822 /* route_speaker_disabled@2x.png in Resources */, 61586B81217A16EE0038AC45 /* menu_about.png in Resources */, + C63F72D0285A2F1E0066163B /* voip_conference_paused_big.png in Resources */, 633FEEE41D3CD55A0014B822 /* numpad_9_default@2x.png in Resources */, + C63F72CD285A2F1E0066163B /* voip_numpad_8.png in Resources */, 8C2A81961F87B8000012A66B /* chat_group_avatar.png in Resources */, 633FEDA31D3CD5590014B822 /* avatar~ipad@2x.png in Resources */, 633FEF461D3CD55A0014B822 /* speaker_disabled.png in Resources */, @@ -3685,6 +4263,7 @@ CF7602E82108759A00749F76 /* UIRecordingCell.xib in Resources */, C6B4444626AAD0980076C517 /* file_pdf_default.png in Resources */, 633FEED21D3CD55A0014B822 /* numpad_6~ipad@2x.png in Resources */, + C63F72E0285A2F1E0066163B /* voip_micro_off.png in Resources */, 633FEDCD1D3CD5590014B822 /* call_quality_indicator_0@2x.png in Resources */, 636316D41A1DEC650009B839 /* SettingsView.xib in Resources */, 24BFAAA6209B0630004F47A7 /* linphone_user~ipad.png in Resources */, @@ -3701,8 +4280,11 @@ 633FEF051D3CD55A0014B822 /* options_selected@2x.png in Resources */, 633FEDD61D3CD5590014B822 /* call_start_body_default.png in Resources */, 633FEEEC1D3CD55A0014B822 /* numpad_hash_default@2x.png in Resources */, + C63F72D8285A2F1E0066163B /* voip_mandatory.png in Resources */, 633FEE831D3CD5590014B822 /* led_disconnected@2x.png in Resources */, 244523BE1E8D3A6C0037A187 /* chat_unsecure.png in Resources */, + C63F72DC285A2F1E0066163B /* voip_menu_more.png in Resources */, + C63F72D5285A2F1E0066163B /* voip_hangup.png in Resources */, 633FEE031D3CD5590014B822 /* cancel_edit_default@2x.png in Resources */, 633FEDE01D3CD5590014B822 /* call_start_body_over~ipad.png in Resources */, 8CBD7BA920B6B82A00E5DCC0 /* UIChatConversationImdnTableViewCell.xib in Resources */, @@ -3712,14 +4294,15 @@ 615A2811217F1FDE0060F920 /* chat_add_group@2x.png in Resources */, D34F6F9E1594D3FB0095705B /* InAppSettings.bundle in Resources */, 633FEE4D1D3CD5590014B822 /* delete_field_default@2x.png in Resources */, + C63F72D9285A2F1E0066163B /* voip_earpiece.png in Resources */, 615A28362180720D0060F920 /* security_toogle_background_grey@2x.png in Resources */, 639CEB091A1DF4FA004DE38F /* UIChatCell.xib in Resources */, 633FEE961D3CD55A0014B822 /* micro_disabled.png in Resources */, 63AADBF61B6A0FF200AA16FD /* linphonerc-factory in Resources */, + C63F72C6285A2F1E0066163B /* voip_call_forward.png in Resources */, 633FEE671D3CD5590014B822 /* footer_contacts_default@2x.png in Resources */, 615A2830218071E80060F920 /* security_toogle_background_green.png in Resources */, 63B8D68C1BCBE65600C12B09 /* ChatConversationCreateView.xib in Resources */, - D38187D915FE347700C3EDCA /* CallIncomingView.xib in Resources */, 63CDC45F1C3BDE370085F529 /* ringback.wav in Resources */, 8CD99A1C20908C27008A7CDA /* callkit_logo@2x.png in Resources */, 633FEE251D3CD5590014B822 /* chat_start_body_over~ipad@2x.png in Resources */, @@ -3737,6 +4320,7 @@ 633FEF221D3CD55A0014B822 /* route_bluetooth_default.png in Resources */, 633FEE701D3CD5590014B822 /* footer_history_disabled.png in Resources */, 633FEDC91D3CD5590014B822 /* call_missed@2x.png in Resources */, + C63F72E8285A2F1E0066163B /* conference_schedule_calendar_default.png in Resources */, 633FEEAE1D3CD55A0014B822 /* numpad_2_over@2x.png in Resources */, 633FEDB51D3CD5590014B822 /* call_alt_back_disabled@2x.png in Resources */, 631098521D4660630041F2B3 /* CountryListView.xib in Resources */, @@ -3759,6 +4343,7 @@ 633FEDF71D3CD5590014B822 /* camera_default@2x.png in Resources */, C64A85522667B74100252AD2 /* ephemeral_messages_default.png in Resources */, 633FEDB31D3CD5590014B822 /* call_alt_back_default@2x.png in Resources */, + C63F72F2285A2F1E0066163B /* voip_conference_mosaic.png in Resources */, 633FEDCF1D3CD5590014B822 /* call_quality_indicator_1@2x.png in Resources */, 633FEF131D3CD55A0014B822 /* pause_big_over_selected@2x.png in Resources */, 8CDC61951F84D89B0087CF7F /* check_selected.png in Resources */, @@ -3766,6 +4351,7 @@ 63AADBF81B6A0FF200AA16FD /* linphonerc~ipad in Resources */, 633FEE8F1D3CD55A0014B822 /* list_details_default@2x.png in Resources */, 633FEE5E1D3CD5590014B822 /* edit_list_default.png in Resources */, + C63F72F7285A2F1E0066163B /* voip_cancel.png in Resources */, 615A282621805B320060F920 /* security_toogle_icon_green@2x.png in Resources */, 633FEDB11D3CD5590014B822 /* call_add_disabled@2x.png in Resources */, CF7602F7210898CC00749F76 /* rec_off_default.png in Resources */, @@ -3774,8 +4360,10 @@ 633FEF251D3CD55A0014B822 /* route_bluetooth_disabled@2x.png in Resources */, 633FEDD81D3CD5590014B822 /* call_start_body_default~ipad.png in Resources */, 61586B83217A16FD0038AC45 /* menu_about@2x.png in Resources */, + C63F72E6285A2F1E0066163B /* voip_numpad_6.png in Resources */, 633FEED81D3CD55A0014B822 /* numpad_7_over~ipad@2x.png in Resources */, 633FEDD71D3CD5590014B822 /* call_start_body_default@2x.png in Resources */, + C63F72D6285A2F1E0066163B /* voip_pause.png in Resources */, 633FEE571D3CD5590014B822 /* dialer_back_disabled@2x.png in Resources */, 63CDC45E1C3BDE370085F529 /* msg.caf in Resources */, 633FEE6D1D3CD5590014B822 /* footer_dialer_disabled@2x.png in Resources */, @@ -3784,7 +4372,6 @@ D38187DD15FE348A00C3EDCA /* AssistantView.xib in Resources */, 633FEDA61D3CD5590014B822 /* back_disabled.png in Resources */, 633FEED61D3CD55A0014B822 /* numpad_7_over@2x.png in Resources */, - 638F1A881C2167C2004B8E02 /* CallView~ipad.xib in Resources */, 633FEDA11D3CD5590014B822 /* avatar@2x.png in Resources */, 633FED9E1D3CD5590014B822 /* add_field_over.png in Resources */, 633FEE0A1D3CD5590014B822 /* chat_attachment_default.png in Resources */, @@ -3793,8 +4380,11 @@ 633FEDE11D3CD5590014B822 /* call_start_body_over~ipad@2x.png in Resources */, 633FEF4F1D3CD55A0014B822 /* valid_default@2x.png in Resources */, 8CD99A382090A824008A7CDA /* splashscreen.png in Resources */, + C63F72B7285A2F1D0066163B /* voip_numpad_7.png in Resources */, 633FEE241D3CD5590014B822 /* chat_start_body_over~ipad.png in Resources */, 8C2A81951F87B8000012A66B /* chat_group_avatar@2x.png in Resources */, + C63F72C4285A2F1D0066163B /* voip_audio_routes.png in Resources */, + C63F72E1285A2F1E0066163B /* voip_camera_on.png in Resources */, 633FEE091D3CD5590014B822 /* chat_add_disabled@2x.png in Resources */, 633FEE191D3CD5590014B822 /* chat_send_over@2x.png in Resources */, 633FEF181D3CD55A0014B822 /* pause_small_over_selected.png in Resources */, @@ -3811,6 +4401,7 @@ 633FEF351D3CD55A0014B822 /* routes_default@2x.png in Resources */, 633FEEDB1D3CD55A0014B822 /* numpad_8_default.png in Resources */, 633FEE5C1D3CD5590014B822 /* edit_disabled.png in Resources */, + C63F72BD285A2F1D0066163B /* voip_call_header_incoming.png in Resources */, 8CF25D9E1F9F76BD00BEA0C1 /* chat_group_informations@2x.png in Resources */, 633FEDCA1D3CD5590014B822 /* call_outgoing.png in Resources */, 633FEDF81D3CD5590014B822 /* camera_disabled.png in Resources */, @@ -3823,6 +4414,7 @@ 633FEF411D3CD55A0014B822 /* select_all_default@2x.png in Resources */, 633FEEFD1D3CD55A0014B822 /* options_add_call_default@2x.png in Resources */, 633FEEA81D3CD55A0014B822 /* numpad_1_over~ipad@2x.png in Resources */, + C63F72B9285A2F1D0066163B /* voip_export.png in Resources */, D38187AD15FE340100C3EDCA /* ChatConversationView.xib in Resources */, 633FEE7C1D3CD5590014B822 /* history_missed_disabled.png in Resources */, CF1DE92E210A0F5D00A0A97E /* UILinphoneAudioPlayer.xib in Resources */, @@ -3834,12 +4426,14 @@ D38187B915FE342200C3EDCA /* ContactDetailsView.xib in Resources */, 633FEE921D3CD55A0014B822 /* menu.png in Resources */, C622E3F026A81290004F5434 /* vr_wave.png in Resources */, + C63F72E3285A2F1E0066163B /* voip_call.png in Resources */, 633FEDE41D3CD5590014B822 /* call_status_incoming~ipad.png in Resources */, 633FEE4C1D3CD5590014B822 /* delete_field_default.png in Resources */, 633FEE391D3CD5590014B822 /* contact_add_default@2x.png in Resources */, 633FEE741D3CD5590014B822 /* history_all_disabled.png in Resources */, 633FEE081D3CD5590014B822 /* chat_add_disabled.png in Resources */, 615A28422180C0870060F920 /* recording.png in Resources */, + C63F72EC285A2F1E0066163B /* conference_schedule_time_default.png in Resources */, 633FEF1D1D3CD55A0014B822 /* presence_offline@2x.png in Resources */, 24A3459E1D95797700881A5C /* UIShopTableCell.xib in Resources */, 633FEE231D3CD5590014B822 /* chat_start_body_over@2x.png in Resources */, @@ -3852,6 +4446,7 @@ 633FEE901D3CD55A0014B822 /* list_details_over.png in Resources */, 633FEDE31D3CD5590014B822 /* call_status_incoming@2x.png in Resources */, 633FEE821D3CD5590014B822 /* led_disconnected.png in Resources */, + C63F72BC285A2F1D0066163B /* voip_numpad_4.png in Resources */, 633FEDB01D3CD5590014B822 /* call_add_disabled.png in Resources */, 63EC8D391D7438660066547B /* AssistantLinkView.xib in Resources */, 633FEE971D3CD55A0014B822 /* micro_disabled@2x.png in Resources */, @@ -3868,9 +4463,12 @@ 633FEF2E1D3CD55A0014B822 /* route_speaker_default.png in Resources */, 633FEDA91D3CD5590014B822 /* backspace_default@2x.png in Resources */, 633FEDF61D3CD5590014B822 /* camera_default.png in Resources */, + C63F72E2285A2F1E0066163B /* voip_conference_play_big.png in Resources */, F0642EF119DAC891009DB336 /* MainStoryboard.storyboard in Resources */, 633FEECC1D3CD55A0014B822 /* numpad_6_default@2x.png in Resources */, 633FEEB91D3CD55A0014B822 /* numpad_3~ipad.png in Resources */, + C63F72F5285A2F1E0066163B /* voip_single_contact_avatar.png in Resources */, + C63F72EF285A2F1E0066163B /* voip_micro_on.png in Resources */, 633FEE731D3CD5590014B822 /* history_all_default@2x.png in Resources */, 633FEDE21D3CD5590014B822 /* call_status_incoming.png in Resources */, 633FEDDA1D3CD5590014B822 /* call_start_body_disabled.png in Resources */, @@ -3899,15 +4497,17 @@ C66B040E26F095D1009B5EDC /* cancel_forward.png in Resources */, CF7602F6210898CC00749F76 /* rec_on_default@2x.png in Resources */, 633FEF081D3CD55A0014B822 /* options_start_conference_disabled.png in Resources */, - 63F1DF511BCE986A00EDED90 /* UICallConferenceCell.xib in Resources */, 633FEE301D3CD5590014B822 /* color_H.png in Resources */, 633FEE7D1D3CD5590014B822 /* history_missed_disabled@2x.png in Resources */, 633FEE941D3CD55A0014B822 /* micro_default.png in Resources */, 633FEE611D3CD5590014B822 /* edit_list_disabled@2x.png in Resources */, + C63F72EE285A2F1E0066163B /* voip_camera_off.png in Resources */, 633FEE761D3CD5590014B822 /* history_all_selected.png in Resources */, + C63F72E7285A2F1E0066163B /* voip_call_participants.png in Resources */, 8C300D9B1E40E0CC00728EF3 /* lime_ko@2x.png in Resources */, 633FEF321D3CD55A0014B822 /* route_speaker_selected.png in Resources */, 61AEBEBF2191991F00F35E7F /* DevicesListView.xib in Resources */, + C63F72BA285A2F1D0066163B /* voip_call_numpad.png in Resources */, 633FEDF51D3CD5590014B822 /* call_video_start_disabled@2x.png in Resources */, 63B81A0C1B57DA33009604A6 /* LICENSE.txt in Resources */, 633FEEDA1D3CD55A0014B822 /* numpad_7~ipad@2x.png in Resources */, @@ -3918,6 +4518,7 @@ 633FEE0E1D3CD5590014B822 /* chat_attachment_over.png in Resources */, 633FEE7B1D3CD5590014B822 /* history_missed_default@2x.png in Resources */, 633FEEC71D3CD55A0014B822 /* numpad_5_over~ipad.png in Resources */, + C63F72BF285A2F1D0066163B /* voip_call_header_paused.png in Resources */, 633FEEAA1D3CD55A0014B822 /* numpad_1~ipad@2x.png in Resources */, 633FEDEC1D3CD5590014B822 /* call_status_outgoing~ipad.png in Resources */, 633FEDE51D3CD5590014B822 /* call_status_incoming~ipad@2x.png in Resources */, @@ -3928,6 +4529,7 @@ D38187C115FE345B00C3EDCA /* DialerView.xib in Resources */, C6A1BB3726E8815400540D50 /* menu_copy_text_default.png in Resources */, D37EE10D16035793003608A6 /* ImageView.xib in Resources */, + C63F72B8285A2F1D0066163B /* voip_radio_off.png in Resources */, 633FEE9F1D3CD55A0014B822 /* numpad_0_over~ipad.png in Resources */, 633FEED51D3CD55A0014B822 /* numpad_7_over.png in Resources */, 633FEE561D3CD5590014B822 /* dialer_back_disabled.png in Resources */, @@ -3936,9 +4538,11 @@ 633FEE141D3CD5590014B822 /* chat_send_default.png in Resources */, 633FEF421D3CD55A0014B822 /* select_all_disabled.png in Resources */, 63AADBE81B6A0FF200AA16FD /* Localizable.strings in Resources */, + C63F72CF285A2F1E0066163B /* voip_copy.png in Resources */, 633FEEF21D3CD55A0014B822 /* numpad_hash~ipad@2x.png in Resources */, 633FEE3F1D3CD5590014B822 /* contacts_all_disabled@2x.png in Resources */, 633FEF3B1D3CD55A0014B822 /* security_ko@2x.png in Resources */, + C63F72F1285A2F1E0066163B /* conference_schedule_participants_default.png in Resources */, 633FEE4A1D3CD5590014B822 /* delete_disabled.png in Resources */, 614D09CE21E74D5400C43EDF /* GoogleService-Info.plist in Resources */, C6B4444526AAD0980076C517 /* file_audio_default.png in Resources */, @@ -3955,28 +4559,36 @@ 633FEEA01D3CD55A0014B822 /* numpad_0_over~ipad@2x.png in Resources */, 66E399F72857869300E73456 /* menu_notifications_off.png in Resources */, 633FEF3E1D3CD55A0014B822 /* security_pending.png in Resources */, - D381881915FE3FCA00C3EDCA /* CallView.xib in Resources */, 633FEE7E1D3CD5590014B822 /* history_missed_selected.png in Resources */, 633FEE261D3CD5590014B822 /* checkbox_checked.png in Resources */, + C63F72B6285A2F1D0066163B /* voip_spinner.png in Resources */, 633FEF531D3CD55A0014B822 /* voicemail@2x.png in Resources */, 633FEF2C1D3CD55A0014B822 /* route_earpiece_selected.png in Resources */, 633FEE681D3CD5590014B822 /* footer_contacts_disabled.png in Resources */, + C63F72B5285A2F1D0066163B /* voip_dropdown.png in Resources */, + C63F72CC285A2F1E0066163B /* voip_call_stats.png in Resources */, 633FEDB71D3CD5590014B822 /* call_alt_start_default@2x.png in Resources */, D3D5126C160B3A8E00946DF8 /* AssistantViewScreens.xib in Resources */, 63AADBEA1B6A0FF200AA16FD /* Images.xcassets in Resources */, 633FEECB1D3CD55A0014B822 /* numpad_6_default.png in Resources */, + C63F72DD285A2F1E0066163B /* voip_conference_new.png in Resources */, 633FEDC71D3CD5590014B822 /* call_incoming@2x.png in Resources */, 633FEDB81D3CD5590014B822 /* call_alt_start_disabled.png in Resources */, 615A281D217F6FA80060F920 /* security_2_indicator@2x.png in Resources */, + C63F72E9285A2F1E0066163B /* voip_call_chat.png in Resources */, 633FEF3C1D3CD55A0014B822 /* security_ok.png in Resources */, 633FEEAF1D3CD55A0014B822 /* numpad_2_over~ipad.png in Resources */, 633FEEB81D3CD55A0014B822 /* numpad_3_over~ipad@2x.png in Resources */, 633FEEFB1D3CD55A0014B822 /* numpad_star~ipad@2x.png in Resources */, 633FED9F1D3CD5590014B822 /* add_field_over@2x.png in Resources */, 633FEEB71D3CD55A0014B822 /* numpad_3_over~ipad.png in Resources */, + C63F72F3285A2F1E0066163B /* voip_numpad_5.png in Resources */, 633FEEF51D3CD55A0014B822 /* numpad_star_default@2x.png in Resources */, 639E9CA91C0DB7FB00019A75 /* UIConfirmationDialog.xib in Resources */, + C63F72CB285A2F1E0066163B /* voip_call_more.png in Resources */, 633FEF111D3CD55A0014B822 /* pause_big_disabled@2x.png in Resources */, + C63F72FE285A31DA0066163B /* Roboto-Italic.ttf in Resources */, + C63F72C1285A2F1D0066163B /* voip_numpad_3.png in Resources */, 633FEE321D3CD5590014B822 /* color_L.png in Resources */, 615A281F217F6FB40060F920 /* security_alert_indicator.png in Resources */, CF7602F5210898CC00749F76 /* rec_off_default@2x.png in Resources */, @@ -3987,6 +4599,7 @@ 633FEDC11D3CD5590014B822 /* call_back_disabled@2x.png in Resources */, 633FEEEE1D3CD55A0014B822 /* numpad_hash_over@2x.png in Resources */, 633FEDD01D3CD5590014B822 /* call_quality_indicator_2.png in Resources */, + C63F72BB285A2F1D0066163B /* voip_chat_rooms_list.png in Resources */, 633FEE551D3CD5590014B822 /* dialer_back_default@2x.png in Resources */, 633FEF361D3CD55A0014B822 /* routes_disabled.png in Resources */, 633FEF261D3CD55A0014B822 /* route_bluetooth_selected.png in Resources */, @@ -3996,6 +4609,7 @@ 61586B8F217A174F0038AC45 /* menu_options@2x.png in Resources */, 633FEE131D3CD5590014B822 /* chat_message_not_delivered@2x.png in Resources */, 615A282A21805B4C0060F920 /* security_toogle_icon_grey@2x.png in Resources */, + C63F72CE285A2F1E0066163B /* voip_call_add.png in Resources */, 63AADBF51B6A0FF200AA16FD /* linphonerc in Resources */, 633FEF0C1D3CD55A0014B822 /* options_transfer_call_disabled.png in Resources */, 633FEE911D3CD55A0014B822 /* list_details_over@2x.png in Resources */, @@ -4003,6 +4617,7 @@ 633FEDCE1D3CD5590014B822 /* call_quality_indicator_1.png in Resources */, 66E399F82857869300E73456 /* menu_notifications_on.png in Resources */, 8CB2B8F91F86229E0015CEE2 /* chat_secure.png in Resources */, + C63F72DB285A2F1E0066163B /* voip_conference_audio_only.png in Resources */, 633FEF4E1D3CD55A0014B822 /* valid_default.png in Resources */, 570742581D5A0691004B9C84 /* ShopView.xib in Resources */, 633FEE361D3CD5590014B822 /* conference_exit_over.png in Resources */, @@ -4028,6 +4643,8 @@ 633FEDA21D3CD5590014B822 /* avatar~ipad.png in Resources */, 24BFAAA1209B0630004F47A7 /* linphone_user@2x.png in Resources */, 633FEF211D3CD55A0014B822 /* presence_unregistered@2x.png in Resources */, + C63F72C2285A2F1D0066163B /* voip_numpad_9.png in Resources */, + C63F72E4285A2F1E0066163B /* voip_call_list_menu.png in Resources */, 633FEEA71D3CD55A0014B822 /* numpad_1_over~ipad.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4266,142 +4883,219 @@ 63B81A0F1B57DA33009604A6 /* TPKeyboardAvoidingTableView.m in Sources */, CF1DE92D210A0F5D00A0A97E /* UILinphoneAudioPlayer.m in Sources */, 1D60589B0D05DD56006BFB54 /* main.m in Sources */, + C63F725C285A24B10066163B /* IncomingCallView.swift in Sources */, + C63F726B285A24B10066163B /* ButtonWithStateBackgrounds.swift in Sources */, + C63F7218285A24B10066163B /* ScheduledConferencesCell.swift in Sources */, + C63F722E285A24B10066163B /* IceState.swift in Sources */, 6135761C240E81BB005304D4 /* UIInterfaceStyleButton.m in Sources */, + C63F721C285A24B10066163B /* MediatorLiveData.swift in Sources */, 8CD99A3C2090B9FA008A7CDA /* ChatConversationImdnView.m in Sources */, 1D3623260D0F684500981E51 /* LinphoneAppDelegate.m in Sources */, + C63F7264285A24B10066163B /* BouncingCounter.swift in Sources */, C6B4444826AADA530076C517 /* SwiftUtil.swift in Sources */, - 614C087A23D1A37400217F80 /* CallManager.swift in Sources */, + C63F7257285A24B10066163B /* ConferenceLayoutPickerView.swift in Sources */, CF15F21E20E4F9A3008B1DE6 /* UIImageViewDeletable.m in Sources */, 22F2508E107141E100AC9B3F /* DialerView.m in Sources */, + C63F724F285A24B10066163B /* LocalVideoView.swift in Sources */, 633756451B67D2B200E21BAD /* SideMenuView.m in Sources */, 8CD99A422090CE6F008A7CDA /* UIChatConversationImdnTableViewCell.m in Sources */, 22E0A822111C44E100B04932 /* AboutView.m in Sources */, 633671611BCBAAD200BFCBDE /* ChatConversationCreateView.m in Sources */, 634610061B61330300548952 /* UILabel+Boldify.m in Sources */, 2248E90E12F7E4CF00220D9C /* UIDigitButton.m in Sources */, - 6134812F2407B35200695B41 /* AppManager.swift in Sources */, 633756391B67BAF400E21BAD /* SideMenuTableView.m in Sources */, + C63F7245285A24B10066163B /* VoipConferenceAudioOnlyView.swift in Sources */, 2214EB7A12F846B1002A5394 /* UICallButton.m in Sources */, - 6352A5751BE0D4B800594C1C /* CallSideMenuView.m in Sources */, - 2214EB8912F84EBB002A5394 /* UIHangUpButton.m in Sources */, + C63F7258285A24B10066163B /* ControlsView.swift in Sources */, 630CF5571AF7CE1500539F7A /* UITextField+DoneButton.m in Sources */, - 2214EBF312F86360002A5394 /* UIMutedMicroButton.m in Sources */, + C63F720D285A24B10066163B /* ConfigManager.swift in Sources */, 8C9C5E111F83BD97006987FA /* UIChatCreateCollectionViewCell.m in Sources */, - 22968A5F12F875C600588287 /* UISpeakerButton.m in Sources */, + C63F7231285A24B10066163B /* PayloadType.swift in Sources */, 63701DDF1BA32039006A9AE3 /* UIConfirmationDialog.m in Sources */, - 6134812D2406CECC00695B41 /* ConfigManager.swift in Sources */, - 22C755601317E59C007BC101 /* UIBluetoothButton.m in Sources */, 22AA8B0113D83F6300B30535 /* UICamSwitch.m in Sources */, 63B8D6A21BCBF43100C12B09 /* UIChatCreateCell.m in Sources */, 636BC9971B5F921B00C754CE /* UIIconButton.m in Sources */, + C63F7263285A24B10066163B /* FormButton.swift in Sources */, + C63F725B285A24B10066163B /* ActiveCallOrConferenceView.swift in Sources */, + C63F7215285A24B10066163B /* ConferenceWaitingRoomFragment.swift in Sources */, 63423C0A1C4501D000D9A050 /* Contact.m in Sources */, + C63F7262285A24B10066163B /* RotatingSpinner.swift in Sources */, 340751E7150F38FD00B89C47 /* UIVideoButton.m in Sources */, - 34216F401547EBCD00EA9777 /* VideoZoomHandler.m in Sources */, - 614C087823D1A35F00217F80 /* ProviderDelegate.swift in Sources */, - D3F83EEC1582021700336684 /* CallView.m in Sources */, + C63F723A285A24B10066163B /* CallData.swift in Sources */, CF7602D7210867E800749F76 /* RecordingsListView.m in Sources */, - 63F1DF4B1BCE983200EDED90 /* CallConferenceTableView.m in Sources */, + C63F7226285A24B10066163B /* UIVIewExtensions.swift in Sources */, + C63F7259285A24B10066163B /* RemotelyRecording.swift in Sources */, + C63F7236285A24B10066163B /* ControlsViewModel.swift in Sources */, + C63F7214285A24B10066163B /* Duration.swift in Sources */, + C63F723B285A24B10066163B /* AudioRouteUtils.swift in Sources */, + C63F7269285A24B10066163B /* StyledLabel.swift in Sources */, + C63F722C285A24B10066163B /* UIColorExtensions.swift in Sources */, D3F83F8E15822ABE00336684 /* PhoneMainView.m in Sources */, + C63F7251285A24B10066163B /* NumpadView.swift in Sources */, + C63F723F285A24B10066163B /* ButtonTheme.swift in Sources */, + C63F7213285A24B10066163B /* TimeZoneData.swift in Sources */, C6A1BB3E26E882D000540D50 /* UIChatReplyBubbleView.m in Sources */, 6377AC801BDE4069007F7625 /* UIBackToCallButton.m in Sources */, 6308F9C51BF0DD6600D1234B /* XMLRPCHelper.m in Sources */, + C63F7235285A24B10066163B /* CallsViewModel.swift in Sources */, + C63F722D285A24B10066163B /* CoreExtensions.swift in Sources */, + C63F722F285A24B10066163B /* AddressExtensions.swift in Sources */, D3ED3E871586291E006C0DE4 /* TabBarView.m in Sources */, 617C242A263022690042FB4A /* UIChatContentView.m in Sources */, + C63F7261285A24B10066163B /* CallControlButton.swift in Sources */, D3ED3EA71587334E006C0DE4 /* HistoryListTableView.m in Sources */, + C63F7220285A24B10066163B /* TimestampUtils.swift in Sources */, 61AEBEBD2191990A00F35E7F /* DevicesListView.m in Sources */, D3ED3EB81587392C006C0DE4 /* HistoryListView.m in Sources */, 24A345A61D95798A00881A5C /* UIShopTableCell.m in Sources */, - 8C1B67061E671826001EA2FE /* AudioHelper.m in Sources */, + C63F7268285A24B10066163B /* Avatar.swift in Sources */, + C63F7247285A24B10066163B /* VoipAudioOnlyParticipantCell.swift in Sources */, D35497FE15875372000081D8 /* ContactsListView.m in Sources */, 635173F91BA082A40095EB0A /* UIChatBubblePhotoCell.m in Sources */, + C63F723E285A24B10066163B /* VoipTexts.swift in Sources */, + C63F722A285A24B10066163B /* UIImageViewExtensions.swift in Sources */, D3549816158761D0000081D8 /* ContactsListTableView.m in Sources */, 633888461BFB2C49001D5E7B /* HPTextViewInternal.m in Sources */, + C63F7242285A24B10066163B /* VoipParticipantCell.swift in Sources */, D35498211587716B000081D8 /* StatusBarView.m in Sources */, D3A55FBC15877E5E003FD403 /* UIContactCell.m in Sources */, 6341807C1BBC103100F71761 /* ChatConversationCreateTableView.m in Sources */, + C63F7254285A24B10066163B /* CallsListView.swift in Sources */, 63BE7A781D75BDF6000990EF /* ShopTableView.m in Sources */, D326483815887D5200930C67 /* OrderedDictionary.m in Sources */, D32648441588F6FC00930C67 /* UIToggleButton.m in Sources */, - D36FB2D51589EF7C0036F6F2 /* UIPauseButton.m in Sources */, D31C9C98158A1CDF00756B45 /* UIHistoryCell.m in Sources */, D35E7597159460580066B1C1 /* ChatsListView.m in Sources */, D35E759F159460B70066B1C1 /* SettingsView.m in Sources */, + C63F7216285A24B10066163B /* ScheduledConferencesView.swift in Sources */, + C63F7255285A24B10066163B /* VoipCallCell.swift in Sources */, 63B81A101B57DA33009604A6 /* UIScrollView+TPKeyboardAvoidingAdditions.m in Sources */, + C63F7211285A24B10066163B /* ScheduledConferencesViewModel.swift in Sources */, F03CA84318C72F1A0008889D /* UITextViewNoDefine.m in Sources */, + C63F7212285A24B10066163B /* ScheduledConferenceData.swift in Sources */, + C63F7239285A24B10066163B /* ConferenceParticipantDeviceData.swift in Sources */, + C63F7238285A24B10066163B /* ConferenceParticipantData.swift in Sources */, 63B81A0D1B57DA33009604A6 /* TPKeyboardAvoidingCollectionView.m in Sources */, + C63F726D285A24B10066163B /* ProviderDelegate.swift in Sources */, + C63F7266285A24B10066163B /* UICallTimer.swift in Sources */, + C63F726C285A24B10066163B /* StyledTextView.swift in Sources */, 570742611D5A09B8004B9C84 /* ShopView.m in Sources */, - 63F1DF4F1BCE985F00EDED90 /* UICallConferenceCell.m in Sources */, D37DC6C11594AE1800B2A5EB /* LinphoneCoreSettingsStore.m in Sources */, 63CD4B4F1A5AAC8C00B84282 /* DTAlertView.m in Sources */, D3EA53FD159850E80037DC6B /* LinphoneManager.m in Sources */, - C6DA657C261C950C0020CB43 /* VFSUtil.swift in Sources */, 63B81A0E1B57DA33009604A6 /* TPKeyboardAvoidingScrollView.m in Sources */, 633888451BFB2C49001D5E7B /* HPGrowingTextView.m in Sources */, 63F1DF441BCE618E00EDED90 /* UIAddressTextField.m in Sources */, D3EA540D1598528B0037DC6B /* ChatsListTableView.m in Sources */, + C63F725E285A24B10066163B /* VoipDialog.swift in Sources */, + C63F724E285A24B10066163B /* PausedCallOrConferenceView.swift in Sources */, D3EA5411159853750037DC6B /* UIChatCell.m in Sources */, - D3F26BF115986B73005F9CAB /* CallIncomingView.m in Sources */, D31B4B21159876C0002E6C72 /* UICompositeView.m in Sources */, - D31AAF5E159B3919002C6B02 /* CallPausedTableView.m in Sources */, + C63F7267285A24B10066163B /* StyledCheckBox.swift in Sources */, 8C9C5E0D1F83B2EF006987FA /* ChatConversationCreateCollectionViewController.m in Sources */, 631098491D4660580041F2B3 /* CountryListView.m in Sources */, D32B9DFC15A2F131000B6DEC /* FastAddressBook.m in Sources */, + C63F7228285A24B10066163B /* OptionalExtensions.swift in Sources */, + C63F7241285A24B10066163B /* ParticipantsListView.swift in Sources */, D350F20E15A43BB100149E54 /* AssistantView.m in Sources */, D3F795D615A582810077328B /* ChatConversationView.m in Sources */, + C63F723C285A24B10066163B /* LightDarkColor.swift in Sources */, D32B6E2915A5BC440033019F /* ChatConversationTableView.m in Sources */, + C63F7233285A24B10066163B /* ConferenceExtensions.swift in Sources */, + C63F7260285A24B10066163B /* StyledSwitch.swift in Sources */, + C63F7252285A24B10066163B /* VoipExtraButtonsView.swift in Sources */, 669B140C27A29D140012220A /* FloatingScrollDownButton.swift in Sources */, + C63F7219285A24B10066163B /* ConferenceHistoryDetailsView.swift in Sources */, D3A8BB7015A6C7D500F96BE5 /* UIChatBubbleTextCell.m in Sources */, 63D11C531C3D501200E8FCEE /* Log.m in Sources */, D3128FE115AABC7E00A2147A /* ContactDetailsView.m in Sources */, 6135761F240E81D0005304D4 /* UIInterfaceStyleToggleButton.m in Sources */, + C63F7221285A24B10066163B /* AppManager.swift in Sources */, D37C639B15AADEF6009D0BAC /* ContactDetailsTableView.m in Sources */, 63E59A3F1ADE70D900646FB3 /* InAppProductsManager.m in Sources */, D3C6526715AC1A8F0092A874 /* UIContactDetailsCell.m in Sources */, + C63F7227285A24B10066163B /* UILabelExtensions.swift in Sources */, 631348301B6F7B6600C6BDCB /* UIRoundBorderedButton.m in Sources */, C90FAA7915AF54E6002091CB /* HistoryDetailsView.m in Sources */, + C63F7253285A24B10066163B /* VoipCallContextMenu.swift in Sources */, + C63F725A285A24B10066163B /* OutgoingCallView.swift in Sources */, 63FB30351A680E73008CA393 /* UIRoundedImageView.m in Sources */, 635775251B6673EC00C8B704 /* HistoryDetailsTableView.m in Sources */, + C63F720E285A24B10066163B /* CallManager.swift in Sources */, 63C441C31BBC23ED0053DC5E /* UIAssistantTextField.m in Sources */, - 6346100F1B61409800548952 /* CallOutgoingView.m in Sources */, D35860D615B549B500513429 /* Utils.m in Sources */, D3F7998115BD32370018C273 /* TPMultiLayoutViewController.m in Sources */, + C63F721A285A24B10066163B /* ConferenceSchedulingView.swift in Sources */, + C63F7232285A24B10066163B /* CallExtensions.swift in Sources */, D3807FBF15C28940005BE9BC /* DCRoundSwitch.m in Sources */, D3807FC115C28940005BE9BC /* DCRoundSwitchKnobLayer.m in Sources */, 61CCC3DF21933B580060EDEA /* UIDeviceCell.m in Sources */, + C63F725D285A24B10066163B /* SharedLayoutConstants.swift in Sources */, + C63F7250285A24B10066163B /* CallStatsView.swift in Sources */, 6306440E1BECB08500134C72 /* FirstLoginView.m in Sources */, + C63F721F285A24B10066163B /* BackNextNavigationView.swift in Sources */, D3807FC315C28940005BE9BC /* DCRoundSwitchOutlineLayer.m in Sources */, + C63F723D285A24B10066163B /* TextStyle.swift in Sources */, + C63F7229285A24B10066163B /* UIButtonExtensions.swift in Sources */, + C63F722B285A24B10066163B /* UIDeviceExtensions.swift in Sources */, + C63F724B285A24B10066163B /* VoipConferenceDisplayModeSelectionView.swift in Sources */, CF7602E221086EB200749F76 /* RecordingsListTableView.m in Sources */, + C63F721E285A24B10066163B /* Pair.swift in Sources */, D3807FC515C28940005BE9BC /* DCRoundSwitchToggleLayer.m in Sources */, 633E41821D74259000320475 /* AssistantLinkView.m in Sources */, + C63F724A285A24B10066163B /* VoipConferenceActiveSpeakerView.swift in Sources */, + C63F7210285A24B10066163B /* ConferenceSchedulingViewModel.swift in Sources */, D3807FE815C2894A005BE9BC /* IASKAppSettingsViewController.m in Sources */, D3807FEC15C2894A005BE9BC /* IASKSpecifierValuesViewController.m in Sources */, 8CA70AE41F9E39E400A3D2EB /* UIChatConversationInfoTableViewCell.m in Sources */, D3807FEE15C2894A005BE9BC /* IASKSettingsReader.m in Sources */, D3807FF015C2894A005BE9BC /* IASKSettingsStore.m in Sources */, + C63F7234285A24B10066163B /* ConferenceViewModel.swift in Sources */, 8CA70AD11F9E0AE100A3D2EB /* ChatConversationInfoView.m in Sources */, D3807FF215C2894A005BE9BC /* IASKSettingsStoreFile.m in Sources */, + C63F720F285A24B10066163B /* ConferenceWaitingRoomViewModel.swift in Sources */, + C63F7248285A24B10066163B /* MicMuted.swift in Sources */, + C63F7243285A24B10066163B /* AudioRoutesView.swift in Sources */, D3807FF415C2894A005BE9BC /* IASKSettingsStoreUserDefaults.m in Sources */, + C63F7240285A24B10066163B /* VoipTheme.swift in Sources */, + C63F7265285A24B10066163B /* VoipExtraButton.swift in Sources */, 639E9C801C0DB13D00019A75 /* UICheckBoxTableView.m in Sources */, CF7602E72108759A00749F76 /* UIRecordingCell.m in Sources */, D3807FF615C2894A005BE9BC /* IASKSpecifier.m in Sources */, D3807FF815C2894A005BE9BC /* IASKPSSliderSpecifierViewCell.m in Sources */, + C63F726A285A24B10066163B /* StyledDatePicker.swift in Sources */, + C63F7237285A24B10066163B /* CallStatisticsData.swift in Sources */, D3807FFA15C2894A005BE9BC /* IASKPSTextFieldSpecifierViewCell.m in Sources */, D3807FFC15C2894A005BE9BC /* IASKPSTitleValueSpecifierViewCell.m in Sources */, + C63F721B285A24B10066163B /* ConferenceSchedulingSummaryView.swift in Sources */, D3807FFE15C2894A005BE9BC /* IASKSlider.m in Sources */, D380800015C2894A005BE9BC /* IASKSwitch.m in Sources */, 662553B427EDFB35007F67D8 /* MagicSearch.swift in Sources */, + C63F725F285A24B10066163B /* StyledValuePicker.swift in Sources */, + C63F7224285A24B10066163B /* UIVIewControllerExtensions.swift in Sources */, D380800215C2894A005BE9BC /* IASKTextField.m in Sources */, D380801315C299D0005BE9BC /* ColorSpaceUtilites.m in Sources */, + C63F721D285A24B10066163B /* MutableLiveData.swift in Sources */, + C63F7256285A24B10066163B /* DismissableView.swift in Sources */, + C63F724C285A24B10066163B /* ActiveCallView.swift in Sources */, + C63F7225285A24B10066163B /* UIImageExtensions.swift in Sources */, C64A854E2667B67200252AD2 /* EphemeralSettingsView.m in Sources */, 8C92ABF31FA773E50006FB5D /* UIChatNotifiedEventCell.m in Sources */, + C63F726E285A24B10066163B /* VFSUtil.swift in Sources */, 633FEF581D3CD5E00014B822 /* UIAvatarPresence.m in Sources */, + C63F7230285A24B10066163B /* ParticipantExtensions.swift in Sources */, 637157A11B283FE200C91677 /* FileTransferDelegate.m in Sources */, D378AB2A15DCDB4A0098505D /* ImagePickerView.m in Sources */, 22405F001601C19200B92522 /* ImageView.m in Sources */, - 63BC49E21BA2CDFC004EC273 /* UICallPausedCell.m in Sources */, + C63F724D285A24B10066163B /* IncomingOuntgoingCommonView.swift in Sources */, D37EE162160377D7003608A6 /* DTActionSheet.m in Sources */, D306459E1611EC2A00BB571E /* UILoadingImageView.m in Sources */, + C63F7246285A24B10066163B /* VoipGridParticipantCell.swift in Sources */, + C63F7244285A24B10066163B /* VoipActiveSpeakerParticipantCell.swift in Sources */, 6381DA7D1C1AD5EA00DF3BBD /* UIBouncingView.m in Sources */, + C63F7217285A24B10066163B /* ICSBubbleView.swift in Sources */, + C63F7222285A24B10066163B /* UIApplication+Extension.swift in Sources */, + C63F7249285A24B10066163B /* VoipConferenceGridView.swift in Sources */, D37E3ECD1619C27A0087659A /* CAAnimation+Blocks.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4418,7 +5112,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C666756F264C925800A0273C /* VFSUtil.swift in Sources */, + C63F726F285A24E90066163B /* VFSUtil.swift in Sources */, EA3650DB2330D2E30001148A /* NotificationService.swift in Sources */, EAE6C88423FABF690076A018 /* Utils.swift in Sources */, ); @@ -4428,7 +5122,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C6667571264C925B00A0273C /* VFSUtil.swift in Sources */, + C63F7270285A24E90066163B /* VFSUtil.swift in Sources */, EA8CB82C239F96CA00C330CC /* NotificationViewController.swift in Sources */, EAE6C88523FABF690076A018 /* Utils.swift in Sources */, ); @@ -4503,16 +5197,6 @@ name = "SideMenuView~ipad.xib"; sourceTree = ""; }; - 634610101B6140A500548952 /* CallOutgoingView.xib */ = { - isa = PBXVariantGroup; - children = ( - 634610111B6140A500548952 /* Base */, - 8CBD7BB420B6B86B00E5DCC0 /* fr */, - 6187B1BA24B3271700D580FB /* hu */, - ); - name = CallOutgoingView.xib; - sourceTree = ""; - }; 636316D31A1DEBCB0009B839 /* AboutView.xib */ = { isa = PBXVariantGroup; children = ( @@ -4545,16 +5229,6 @@ name = "DialerView~ipad.xib"; sourceTree = ""; }; - 638F1A861C2167C2004B8E02 /* CallView~ipad.xib */ = { - isa = PBXVariantGroup; - children = ( - 638F1A871C2167C2004B8E02 /* Base */, - 8CBD7BB620B6B86D00E5DCC0 /* fr */, - 6187B1BC24B3271800D580FB /* hu */, - ); - name = "CallView~ipad.xib"; - sourceTree = ""; - }; 638F1A8F1C21993D004B8E02 /* UICompositeView~ipad.xib */ = { isa = PBXVariantGroup; children = ( @@ -4620,16 +5294,6 @@ name = FirstLoginView.xib; sourceTree = ""; }; - 639E9C9F1C0DB7DF00019A75 /* UICallPausedCell.xib */ = { - isa = PBXVariantGroup; - children = ( - 639E9C9E1C0DB7DF00019A75 /* Base */, - 8CBD7BC620B6B87900E5DCC0 /* fr */, - 6187B1CC24B3271E00D580FB /* hu */, - ); - name = UICallPausedCell.xib; - sourceTree = ""; - }; 639E9CA21C0DB7E500019A75 /* UIChatBubblePhotoCell.xib */ = { isa = PBXVariantGroup; children = ( @@ -4734,16 +5398,6 @@ name = AssistantLinkView.xib; sourceTree = ""; }; - 63F1DF531BCE986A00EDED90 /* UICallConferenceCell.xib */ = { - isa = PBXVariantGroup; - children = ( - 63F1DF521BCE986A00EDED90 /* Base */, - 8CBD7BC520B6B87800E5DCC0 /* fr */, - 6187B1CB24B3271E00D580FB /* hu */, - ); - name = UICallConferenceCell.xib; - sourceTree = ""; - }; 8CBD7BA220B6B7FD00E5DCC0 /* ChatConversationInfoView.xib */ = { isa = PBXVariantGroup; children = ( @@ -4900,18 +5554,6 @@ name = HistoryListView.xib; sourceTree = ""; }; - D38187DC15FE347700C3EDCA /* CallIncomingView.xib */ = { - isa = PBXVariantGroup; - children = ( - F09548241883F15400E8A69B /* Base */, - F09548481883F55800E8A69B /* ru */, - F0AF06FD1A24BA770086C9C1 /* ar */, - 8CBD7BB320B6B86B00E5DCC0 /* fr */, - 6187B1B924B3271700D580FB /* hu */, - ); - name = CallIncomingView.xib; - sourceTree = ""; - }; D38187E015FE348A00C3EDCA /* AssistantView.xib */ = { isa = PBXVariantGroup; children = ( @@ -4937,18 +5579,6 @@ path = LinphoneUI; sourceTree = ""; }; - D381881C15FE3FCA00C3EDCA /* CallView.xib */ = { - isa = PBXVariantGroup; - children = ( - F09548231883F15400E8A69B /* Base */, - F09548461883F54200E8A69B /* ru */, - F0AF06FC1A24BA770086C9C1 /* ar */, - 8CBD7BB520B6B86C00E5DCC0 /* fr */, - 6187B1BB24B3271700D580FB /* hu */, - ); - name = CallView.xib; - sourceTree = ""; - }; D3D5126A160B3A8E00946DF8 /* AssistantViewScreens.xib */ = { isa = PBXVariantGroup; children = ( diff --git a/linphone_Prefix.pch b/linphone_Prefix.pch index 6f87a986f..60e104960 100644 --- a/linphone_Prefix.pch +++ b/linphone_Prefix.pch @@ -12,8 +12,5 @@ #import "Contact.h" #import "UIToggleButton.h" -#import "UISpeakerButton.h" -#import "UIBluetoothButton.h" -#import "UIMutedMicroButton.h" #import "UIChatBubbleTextCell.h" diff --git a/msgNotificationService/NotificationService.swift b/msgNotificationService/NotificationService.swift index c657f797c..a47699e24 100644 --- a/msgNotificationService/NotificationService.swift +++ b/msgNotificationService/NotificationService.swift @@ -149,7 +149,7 @@ class NotificationService: UNNotificationServiceExtension { } func parseMessage(message: PushNotificationMessage) -> MsgData? { - let content = message.isText ? message.textContent : "🗻" + let content = message.isIcalendar ? NSLocalizedString("You are invited to a conference", comment: "") : message.isText ? message.textContent : "🗻" let fromAddr = message.fromAddr?.username let callId = message.callId let localUri = message.localAddr?.asStringUriOnly()