From be282ff47be7bcf2d2f01c1d52777d49d91defae Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 17 Dec 2021 11:45:41 +0100 Subject: [PATCH 01/75] Additional harnessing on VFS activation --- Classes/VFSUtil.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Classes/VFSUtil.swift b/Classes/VFSUtil.swift index 4fae043bc..79343d154 100644 --- a/Classes/VFSUtil.swift +++ b/Classes/VFSUtil.swift @@ -186,7 +186,10 @@ import os log("[VFS] Unable to retrieve encrypted key.", .error) return false } - let secret = decrypt(encryptedText: encryptedKey) + guard let secret = decrypt(encryptedText: encryptedKey) else { + log(log: "[VFS] Unable to decryt encrypted key.", level: .error) + return false + } Factory.Instance.setVfsEncryption(encryptionModule: 2, secret: secret, secretSize: 32) log("[VFS] activated", .info) return true From 0afc2036d61cc92358f5b984ae84d0d605411099 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Mon, 29 Nov 2021 16:07:00 +0100 Subject: [PATCH 02/75] APPLICATION_EXTENSION_API_ONLY=YES on all flavors --- linphone.xcodeproj/project.pbxproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/linphone.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj index 0e9223620..afad71813 100644 --- a/linphone.xcodeproj/project.pbxproj +++ b/linphone.xcodeproj/project.pbxproj @@ -6008,6 +6008,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -6050,6 +6051,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -6091,6 +6093,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -6133,6 +6136,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -6435,6 +6439,7 @@ baseConfigurationReference = 1060E68152C51FCE5ACBF779 /* Pods-msgNotificationContent.debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -6490,6 +6495,7 @@ baseConfigurationReference = 02DBDD5A09F46796AEC2485B /* Pods-msgNotificationContent.release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -6541,6 +6547,7 @@ baseConfigurationReference = 507103607396F28FF4427108 /* Pods-msgNotificationContent.distribution.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -6592,6 +6599,7 @@ baseConfigurationReference = 6150F32455334A0A7B3D46C8 /* Pods-msgNotificationContent.distributionadhoc.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; From 69a885df4fc9a808e33aace800ea25321ee612e3 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Wed, 12 Jan 2022 10:28:22 +0100 Subject: [PATCH 03/75] Bulk refactorisation + new ConferenceViewModel from Android + Bulk fixes --- Classes/Base.lproj/HistoryListView.xib | 41 ++- Classes/ChatConversationCreateView.h | 2 + Classes/ChatConversationCreateView.m | 15 +- Classes/ChatConversationTableView.m | 18 +- Classes/ChatsListTableView.m | 4 + Classes/HistoryDetailsView.m | 1 + Classes/HistoryListTableView.h | 4 + Classes/HistoryListTableView.m | 29 +- Classes/HistoryListView.h | 1 + Classes/HistoryListView.m | 22 +- Classes/LinphoneCoreSettingsStore.m | 6 +- Classes/LinphoneManager.m | 9 + Classes/LinphoneUI/TabBarView.m | 4 +- Classes/LinphoneUI/UICamSwitch.h | 1 + Classes/LinphoneUI/UICamSwitch.m | 6 +- Classes/LinphoneUI/UIChatBubbleTextCell.h | 3 + Classes/LinphoneUI/UIChatBubbleTextCell.m | 25 ++ Classes/LinphoneUI/UICompositeView.h | 2 + Classes/LinphoneUI/UICompositeView.m | 9 + Classes/LinphoneUI/UIHistoryCell.m | 64 +++-- Classes/LinphoneUI/VideoZoomHandler.h | 31 --- Classes/LinphoneUI/VideoZoomHandler.m | 111 -------- Classes/PhoneMainView.m | 10 +- Classes/SideMenuTableView.m | 10 + Classes/{ => Swift}/AppManager.swift | 0 Classes/{ => Swift}/CallManager.swift | 15 ++ .../Conference/data/Duration.swift | 0 .../data/ScheduledConferenceData.swift | 8 +- .../Conference/data/TimeZoneData.swift | 0 .../ConferenceSchedulingViewModel.swift | 129 +++++---- .../ScheduledConferencesViewModel.swift | 74 ++++++ .../views/ConferenceHistoryDetailsView.swift | 165 ++++++++++++ .../ConferenceSchedulingSummaryView.swift | 45 +++- .../views/ConferenceSchedulingView.swift | 3 +- .../views/ConferenceWaitingRoomFragment.swift | 138 ++++++++++ .../Conference/views/ICSBubbleView.swift | 138 ++++++++++ .../views/ScheduledConferencesCell.swift | 126 +++++++++ .../views/ScheduledConferencesView.swift | 108 ++++++++ Classes/{ => Swift}/ConfigManager.swift | 0 .../Extensions/IOS/OptionalExtensions.swift | 0 .../IOS/UIApplication+Extension.swift | 0 .../Extensions/IOS/UIButtonExtensions.swift | 10 + .../Extensions/IOS/UIColorExtensions.swift | 0 .../Extensions/IOS/UIDeviceExtensions.swift | 0 .../Extensions/IOS/UIImageExtensions.swift | 0 .../IOS/UIImageViewExtensions.swift | 0 .../Extensions/IOS/UILabelExtensions.swift | 0 .../IOS/UIVIewControllerExtensions.swift | 0 .../Extensions/IOS/UIVIewExtensions.swift | 16 ++ .../LinphoneCore/AddressExtensions.swift | 4 +- .../LinphoneCore/CallExtensions.swift | 0 .../LinphoneCore/ConferenceExtensions.swift | 0 .../LinphoneCore/CoreExtensions.swift | 5 +- .../Extensions/LinphoneCore/IceState.swift | 0 .../LinphoneCore/ParticipantExtensions.swift | 0 .../Extensions/LinphoneCore/PayloadType.swift | 0 Classes/{ => Swift}/ProviderDelegate.swift | 0 .../Util/BackNextNavigationView.swift} | 8 +- .../Util}/MutableLiveData.swift | 0 Classes/Swift/Util/Pair.swift | 33 +++ .../Util}/TimestampUtils.swift | 0 .../Util}/ViewModel/MediatorLiveData.swift | 0 Classes/{ => Swift}/VFSUtil.swift | 2 +- .../{ => Swift}/Voip/AudioRouteUtils.swift | 0 .../{ => Swift}/Voip/Models/CallData.swift | 35 ++- .../Voip/Models/CallStatisticsData.swift | 0 .../Voip/Models/CallsViewModel.swift | 2 +- .../Models/ConferenceParticipantData.swift | 0 .../ConferenceParticipantDeviceData.swift | 0 .../Voip/Models/ConferenceViewModel.swift | 248 +++++++++--------- .../Voip/Models/ControlsViewModel.swift | 0 .../{ => Swift}/Voip/Theme/ButtonTheme.swift | 0 .../Voip/Theme/LightDarkColor.swift | 0 .../{ => Swift}/Voip/Theme/TextStyle.swift | 4 +- .../{ => Swift}/Voip/Theme/VoipTexts.swift | 9 +- .../{ => Swift}/Voip/Theme/VoipTheme.swift | 41 ++- .../ActiveCallOrConferenceView.swift | 50 +--- .../IncomingCallView.swift | 0 .../OutgoingCallView.swift | 2 +- .../Fragments/ActiveCall/ActiveCallView.swift | 20 +- .../Views/Fragments/AudioRoutesView.swift | 0 .../Voip/Views/Fragments/CallStatsView.swift | 11 +- .../Fragments/CallsList/CallsListView.swift | 36 ++- .../Fragments/CallsList/VoipCallCell.swift | 16 +- .../CallsList/VoipCallContextMenu.swift | 0 .../VoipActiveSpeakerParticipantCell.swift | 0 .../VoipConferenceActiveSpeakerView.swift | 2 +- ...ipConferenceDisplayModeSelectionView.swift | 0 .../Conference/VoipConferenceGridView.swift | 2 +- .../Conference/VoipGridParticipantCell.swift | 0 .../Voip/Views/Fragments/ControlsView.swift | 0 .../Views/Fragments/DismissableView.swift | 0 .../IncomingOuntgoingCommonView.swift | 0 .../Voip/Views/Fragments/LocalVideoView.swift | 0 .../Voip/Views/Fragments/NumpadView.swift | 10 +- .../ParticipantsListView.swift | 17 +- .../VoipParticipantCell.swift | 0 .../PausedCallOrConferenceView.swift | 0 .../Views/Fragments/RemotelyRecording.swift | 2 +- .../Fragments/VoipExtraButtonsView.swift | 2 +- .../Voip/Views/SharedLayoutConstants.swift | 3 +- Classes/{ => Swift}/Voip/VoipDialog.swift | 2 +- Classes/{ => Swift}/Voip/Widgets/Avatar.swift | 9 +- .../Voip/Widgets/BouncingCounter.swift | 0 .../Widgets/ButtonWithStateBackgrounds.swift | 0 .../Voip/Widgets/CallControlButton.swift | 9 +- .../{ => Swift}/Voip/Widgets/FormButton.swift | 16 +- .../Voip/Widgets/RotatingSpinner.swift | 0 .../Voip/Widgets/StyledCheckBox.swift | 0 .../Voip/Widgets/StyledDatePicker.swift | 25 +- .../Voip/Widgets/StyledLabel.swift | 0 .../Voip/Widgets/StyledSwitch.swift | 0 .../Voip/Widgets/StyledTextView.swift | 0 .../Voip/Widgets/StyledValuePicker.swift | 0 .../Voip/Widgets/UICallTimer.swift | 0 .../Voip/Widgets/VoipExtraButton.swift | 0 Classes/Utils/Utils.m | 15 ++ Classes/linphone-Bridging-Header.h | 3 + .../conference_schedule_calendar_default.png | Bin 0 -> 182 bytes ...nference_schedule_participants_default.png | Bin 0 -> 660 bytes .../conference_schedule_time_default.png | Bin 0 -> 442 bytes Resources/images/voip_call_add.png | Bin 14157 -> 21446 bytes Resources/images/voip_call_forward.png | Bin 14584 -> 22196 bytes Resources/images/voip_calls_list.png | Bin 14703 -> 23221 bytes .../images/voip_conference_new_selected.png | Bin 0 -> 13189 bytes Settings/InAppSettings.bundle/Call.plist | 10 + linphone.xcodeproj/project.pbxproj | 154 +++++++---- 127 files changed, 1636 insertions(+), 574 deletions(-) delete mode 100644 Classes/LinphoneUI/VideoZoomHandler.h delete mode 100644 Classes/LinphoneUI/VideoZoomHandler.m rename Classes/{ => Swift}/AppManager.swift (100%) rename Classes/{ => Swift}/CallManager.swift (97%) rename Classes/{ => Swift}/Conference/data/Duration.swift (100%) rename Classes/{ => Swift}/Conference/data/ScheduledConferenceData.swift (95%) rename Classes/{ => Swift}/Conference/data/TimeZoneData.swift (100%) rename Classes/{Conference/viewmodels => Swift/Conference/models}/ConferenceSchedulingViewModel.swift (65%) create mode 100644 Classes/Swift/Conference/models/ScheduledConferencesViewModel.swift create mode 100644 Classes/Swift/Conference/views/ConferenceHistoryDetailsView.swift rename Classes/{ => Swift}/Conference/views/ConferenceSchedulingSummaryView.swift (86%) rename Classes/{ => Swift}/Conference/views/ConferenceSchedulingView.swift (98%) create mode 100644 Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift create mode 100644 Classes/Swift/Conference/views/ICSBubbleView.swift create mode 100644 Classes/Swift/Conference/views/ScheduledConferencesCell.swift create mode 100644 Classes/Swift/Conference/views/ScheduledConferencesView.swift rename Classes/{ => Swift}/ConfigManager.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/IOS/OptionalExtensions.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/IOS/UIApplication+Extension.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/IOS/UIButtonExtensions.swift (74%) rename Classes/{SwiftUtil => Swift}/Extensions/IOS/UIColorExtensions.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/IOS/UIDeviceExtensions.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/IOS/UIImageExtensions.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/IOS/UIImageViewExtensions.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/IOS/UILabelExtensions.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/IOS/UIVIewControllerExtensions.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/IOS/UIVIewExtensions.swift (94%) rename Classes/{SwiftUtil => Swift}/Extensions/LinphoneCore/AddressExtensions.swift (90%) rename Classes/{SwiftUtil => Swift}/Extensions/LinphoneCore/CallExtensions.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/LinphoneCore/ConferenceExtensions.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/LinphoneCore/CoreExtensions.swift (96%) rename Classes/{SwiftUtil => Swift}/Extensions/LinphoneCore/IceState.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/LinphoneCore/ParticipantExtensions.swift (100%) rename Classes/{SwiftUtil => Swift}/Extensions/LinphoneCore/PayloadType.swift (100%) rename Classes/{ => Swift}/ProviderDelegate.swift (100%) rename Classes/{SwiftUtil/GenericViews/NavigationView.swift => Swift/Util/BackNextNavigationView.swift} (97%) rename Classes/{SwiftUtil/ViewModel => Swift/Util}/MutableLiveData.swift (100%) create mode 100644 Classes/Swift/Util/Pair.swift rename Classes/{SwiftUtil => Swift/Util}/TimestampUtils.swift (100%) rename Classes/{SwiftUtil => Swift/Util}/ViewModel/MediatorLiveData.swift (100%) rename Classes/{ => Swift}/VFSUtil.swift (99%) rename Classes/{ => Swift}/Voip/AudioRouteUtils.swift (100%) rename Classes/{ => Swift}/Voip/Models/CallData.swift (78%) rename Classes/{ => Swift}/Voip/Models/CallStatisticsData.swift (100%) rename Classes/{ => Swift}/Voip/Models/CallsViewModel.swift (99%) rename Classes/{ => Swift}/Voip/Models/ConferenceParticipantData.swift (100%) rename Classes/{ => Swift}/Voip/Models/ConferenceParticipantDeviceData.swift (100%) rename Classes/{ => Swift}/Voip/Models/ConferenceViewModel.swift (58%) rename Classes/{ => Swift}/Voip/Models/ControlsViewModel.swift (100%) rename Classes/{ => Swift}/Voip/Theme/ButtonTheme.swift (100%) rename Classes/{ => Swift}/Voip/Theme/LightDarkColor.swift (100%) rename Classes/{ => Swift}/Voip/Theme/TextStyle.swift (92%) rename Classes/{ => Swift}/Voip/Theme/VoipTexts.swift (95%) rename Classes/{ => Swift}/Voip/Theme/VoipTheme.swift (89%) rename Classes/{ => Swift}/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift (85%) rename Classes/{ => Swift}/Voip/Views/CompositeViewControllers/IncomingCallView.swift (100%) rename Classes/{ => Swift}/Voip/Views/CompositeViewControllers/OutgoingCallView.swift (98%) rename Classes/{ => Swift}/Voip/Views/Fragments/ActiveCall/ActiveCallView.swift (92%) rename Classes/{ => Swift}/Voip/Views/Fragments/AudioRoutesView.swift (100%) rename Classes/{ => Swift}/Voip/Views/Fragments/CallStatsView.swift (89%) rename Classes/{ => Swift}/Voip/Views/Fragments/CallsList/CallsListView.swift (79%) rename Classes/{ => Swift}/Voip/Views/Fragments/CallsList/VoipCallCell.swift (87%) rename Classes/{ => Swift}/Voip/Views/Fragments/CallsList/VoipCallContextMenu.swift (100%) rename Classes/{ => Swift}/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift (100%) rename Classes/{ => Swift}/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift (99%) rename Classes/{ => Swift}/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift (100%) rename Classes/{ => Swift}/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift (99%) rename Classes/{ => Swift}/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift (100%) rename Classes/{ => Swift}/Voip/Views/Fragments/ControlsView.swift (100%) rename Classes/{ => Swift}/Voip/Views/Fragments/DismissableView.swift (100%) rename Classes/{ => Swift}/Voip/Views/Fragments/IncomingOuntgoingCommonView.swift (100%) rename Classes/{ => Swift}/Voip/Views/Fragments/LocalVideoView.swift (100%) rename Classes/{ => Swift}/Voip/Views/Fragments/NumpadView.swift (91%) rename Classes/{ => Swift}/Voip/Views/Fragments/ParticipantsList/ParticipantsListView.swift (81%) rename Classes/{ => Swift}/Voip/Views/Fragments/ParticipantsList/VoipParticipantCell.swift (100%) rename Classes/{ => Swift}/Voip/Views/Fragments/PausedCallOrConferenceView.swift (100%) rename Classes/{ => Swift}/Voip/Views/Fragments/RemotelyRecording.swift (96%) rename Classes/{ => Swift}/Voip/Views/Fragments/VoipExtraButtonsView.swift (98%) rename Classes/{ => Swift}/Voip/Views/SharedLayoutConstants.swift (85%) rename Classes/{ => Swift}/Voip/VoipDialog.swift (98%) rename Classes/{ => Swift}/Voip/Widgets/Avatar.swift (85%) rename Classes/{ => Swift}/Voip/Widgets/BouncingCounter.swift (100%) rename Classes/{ => Swift}/Voip/Widgets/ButtonWithStateBackgrounds.swift (100%) rename Classes/{ => Swift}/Voip/Widgets/CallControlButton.swift (88%) rename Classes/{ => Swift}/Voip/Widgets/FormButton.swift (69%) rename Classes/{ => Swift}/Voip/Widgets/RotatingSpinner.swift (100%) rename Classes/{ => Swift}/Voip/Widgets/StyledCheckBox.swift (100%) rename Classes/{ => Swift}/Voip/Widgets/StyledDatePicker.swift (87%) rename Classes/{ => Swift}/Voip/Widgets/StyledLabel.swift (100%) rename Classes/{ => Swift}/Voip/Widgets/StyledSwitch.swift (100%) rename Classes/{ => Swift}/Voip/Widgets/StyledTextView.swift (100%) rename Classes/{ => Swift}/Voip/Widgets/StyledValuePicker.swift (100%) rename Classes/{ => Swift}/Voip/Widgets/UICallTimer.swift (100%) rename Classes/{ => Swift}/Voip/Widgets/VoipExtraButton.swift (100%) create mode 100644 Resources/images/conference_schedule_calendar_default.png create mode 100644 Resources/images/conference_schedule_participants_default.png create mode 100644 Resources/images/conference_schedule_time_default.png create mode 100644 Resources/images/voip_conference_new_selected.png diff --git a/Classes/Base.lproj/HistoryListView.xib b/Classes/Base.lproj/HistoryListView.xib index 24c74a7a0..90a4022b3 100644 --- a/Classes/Base.lproj/HistoryListView.xib +++ b/Classes/Base.lproj/HistoryListView.xib @@ -1,13 +1,17 @@ - + + - + + + + @@ -29,7 +33,7 @@ - + + @@ -127,12 +146,12 @@ - + - + @@ -143,7 +162,7 @@ diff --git a/Classes/ChatConversationCreateView.h b/Classes/ChatConversationCreateView.h index b51116e61..8c8f271ea 100644 --- a/Classes/ChatConversationCreateView.h +++ b/Classes/ChatConversationCreateView.h @@ -46,6 +46,8 @@ @property(nonatomic) Boolean isEncrypted; @property(nonatomic) Boolean isForVoipConference; +@property(nonatomic) Boolean isForOngoingVoipConference; + @property (weak, nonatomic) IBOutlet UILabel *voipTitle; - (IBAction)onBackClick:(id)sender; diff --git a/Classes/ChatConversationCreateView.m b/Classes/ChatConversationCreateView.m index 83eacf09a..b5b949769 100644 --- a/Classes/ChatConversationCreateView.m +++ b/Classes/ChatConversationCreateView.m @@ -90,8 +90,14 @@ static UICompositeViewDescription *compositeDescription = nil; _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]; } } @@ -171,8 +177,13 @@ static UICompositeViewDescription *compositeDescription = nil; - (IBAction)onNextClick:(id)sender { if (_isForVoipConference) { - [PhoneMainView.instance changeCurrentView:VIEW(ConferenceSchedulingSummaryView).compositeViewDescription]; - [VIEW(ConferenceSchedulingSummaryView) setParticipantsWithAddresses:_tableController.contactsGroup]; + 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; diff --git a/Classes/ChatConversationTableView.m b/Classes/ChatConversationTableView.m index 5fe80f82d..7c12fa3d3 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 @@ -326,7 +327,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); @@ -373,14 +375,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/ChatsListTableView.m b/Classes/ChatsListTableView.m index ba82444af..6f8c3fd36 100644 --- a/Classes/ChatsListTableView.m +++ b/Classes/ChatsListTableView.m @@ -24,6 +24,8 @@ #import "linphone/linphonecore.h" #import "PhoneMainView.h" #import "Utils.h" +#import "SVProgressHUD.h" + @implementation ChatsListTableView @@ -202,11 +204,13 @@ void deletion_chat_room_state_changed(LinphoneChatRoom *cr, LinphoneChatRoomStat // will force a call to [self loadData] [NSNotificationCenter.defaultCenter postNotificationName:kLinphoneMessageReceived object:view]; view.waitView.hidden = TRUE; + [SVProgressHUD dismiss]; } } - (void) deleteChatRooms { _waitView.hidden = FALSE; + [SVProgressHUD show]; bctbx_list_t *chatRooms = bctbx_list_copy(_chatRooms); while (chatRooms) { LinphoneChatRoom *chatRoom = (LinphoneChatRoom *)chatRooms->data; diff --git a/Classes/HistoryDetailsView.m b/Classes/HistoryDetailsView.m index 3dda8aa54..a3f5e46d6 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 e2bbcc374..97f50fbce 100644 --- a/Classes/HistoryListTableView.m +++ b/Classes/HistoryListTableView.m @@ -25,12 +25,13 @@ @implementation HistoryListTableView -@synthesize missedFilter; +@synthesize missedFilter,confFilter; #pragma mark - Lifecycle Functions - (void)initHistoryTableViewController { missedFilter = false; + confFilter = false; } - (id)init { @@ -102,9 +103,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 +151,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 +166,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, log); 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/LinphoneCoreSettingsStore.m b/Classes/LinphoneCoreSettingsStore.m index d9a59ac84..f40d673bc 100644 --- a/Classes/LinphoneCoreSettingsStore.m +++ b/Classes/LinphoneCoreSettingsStore.m @@ -331,6 +331,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"]; @@ -787,7 +789,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 8cb934663..59fc88be5 100644 --- a/Classes/LinphoneManager.m +++ b/Classes/LinphoneManager.m @@ -487,6 +487,15 @@ 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); + } + } + linphone_account_params_unref(newAccountParams); accounts = accounts->next; } diff --git a/Classes/LinphoneUI/TabBarView.m b/Classes/LinphoneUI/TabBarView.m index ec171fbf9..35a242be0 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/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..ce88d3120 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; @@ -52,7 +56,7 @@ INIT_WITH_COMMON_CF { linphone_core_set_video_device(LC, newCamId); LinphoneCall *call = linphone_core_get_current_call(LC); if (call != NULL) { - linphone_call_update(call, NULL); + linphone_core_update_call(LC, call, NULL); } } } diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.h b/Classes/LinphoneUI/UIChatBubbleTextCell.h index 6b583ecac..941d44364 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 diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.m b/Classes/LinphoneUI/UIChatBubbleTextCell.m index 079891029..da123c580 100644 --- a/Classes/LinphoneUI/UIChatBubbleTextCell.m +++ b/Classes/LinphoneUI/UIChatBubbleTextCell.m @@ -30,6 +30,9 @@ @implementation UIChatBubbleTextCell +ICSBubbleView *icsBubbleView; + + #pragma mark - Lifecycle Functions @@ -44,6 +47,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]; + icsBubbleView = [[ICSBubbleView alloc] init]; + icsBubbleView.frame = CGRectMake(_messageText.frame.origin.x, _messageText.frame.origin.y+25, CONFERENCE_INVITATION_WIDTH-80, CONFERENCE_INVITATION_HEIGHT-20); + [self.innerView addSubview:icsBubbleView]; + [icsBubbleView setLayoutConstraintsWithView:self.backgroundColorImage]; + } } @@ -276,6 +284,18 @@ _replyView.view.hidden = true; } + // ICS for conference invitations + + if ([ICSBubbleView isConferenceInvitationMessageWithCmessage:self.message]) { + [icsBubbleView setFromChatMessageWithCmessage:self.message]; + icsBubbleView.hidden = false; + _messageText.hidden = true; + } else { + icsBubbleView.hidden = true; + _messageText.hidden = false; + } + + } - (void)setEditing:(BOOL)editing { @@ -455,6 +475,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..e91b21fdb 100644 --- a/Classes/LinphoneUI/UICompositeView.h +++ b/Classes/LinphoneUI/UICompositeView.h @@ -85,5 +85,7 @@ - (UIInterfaceOrientation)currentOrientation; - (void)clearCache:(NSArray *)exclude; - (IBAction)onRightSwipe:(id)sender; +- (void) removeCallViewFromCache; + @end diff --git a/Classes/LinphoneUI/UICompositeView.m b/Classes/LinphoneUI/UICompositeView.m index 2354b51b5..08baa4963 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,6 +305,14 @@ return nil; } +-(void) removeCallViewFromCache { + for (NSString *key in [viewControllerCache allKeys]) { + if ([key isEqualToString:ActiveCallOrConferenceView.compositeViewDescription.name]) { + [viewControllerCache removeObjectForKey:key]; + } + } +} + - (void)clearCache:(NSArray *)exclude { for (NSString *key in [viewControllerCache allKeys]) { bool remove = true; 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/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/PhoneMainView.m b/Classes/PhoneMainView.m index a45b51376..49f6aa3f8 100644 --- a/Classes/PhoneMainView.m +++ b/Classes/PhoneMainView.m @@ -374,7 +374,10 @@ static RootViewManager *rootViewManagerInstance = nil; } break; } - case LinphoneCallOutgoingInit: { + case LinphoneCallOutgoingInit: + case LinphoneCallOutgoingEarlyMedia: + case LinphoneCallOutgoingProgress: + case LinphoneCallOutgoingRinging: { OutgoingCallView *v = VIEW(OutgoingCallView); [self changeCurrentView:OutgoingCallView.compositeViewDescription]; [v setCallWithCall:call]; @@ -395,10 +398,6 @@ static RootViewManager *rootViewManagerInstance = nil; case LinphoneCallEarlyUpdating: case LinphoneCallIdle: break; - case LinphoneCallOutgoingEarlyMedia: - case LinphoneCallOutgoingProgress: { - break; - } case LinphoneCallReleased: if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -407,7 +406,6 @@ static RootViewManager *rootViewManagerInstance = nil; }); } break; - case LinphoneCallOutgoingRinging: case LinphoneCallPaused: case LinphoneCallPausing: case LinphoneCallRefered: diff --git a/Classes/SideMenuTableView.m b/Classes/SideMenuTableView.m index 67ca16831..a70462fde 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:NSLocalizedString(@"Conferences", nil) + 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 97% rename from Classes/CallManager.swift rename to Classes/Swift/CallManager.swift index cc2e380e2..9d72c6bdb 100644 --- a/Classes/CallManager.swift +++ b/Classes/Swift/CallManager.swift @@ -231,6 +231,15 @@ import AVFoundation try? doCall(addr: sAddr, isSas: isSas) } } + + func startCall(addr:String, isSas: Bool = false) { + do { + let address = try Factory.Instance.createAddress(addr: addr) + startCall(addr: address.getCobject,isSas: isSas) + } catch { + Log.e("[CallManager] unable to create address for a new outgoing call : \(addr) \(error) ") + } + } func doCall(addr: Address, isSas: Bool) throws { let displayName = FastAddressBook.displayName(for: addr.getCobject) @@ -410,6 +419,12 @@ 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: diff --git a/Classes/Conference/data/Duration.swift b/Classes/Swift/Conference/data/Duration.swift similarity index 100% rename from Classes/Conference/data/Duration.swift rename to Classes/Swift/Conference/data/Duration.swift diff --git a/Classes/Conference/data/ScheduledConferenceData.swift b/Classes/Swift/Conference/data/ScheduledConferenceData.swift similarity index 95% rename from Classes/Conference/data/ScheduledConferenceData.swift rename to Classes/Swift/Conference/data/ScheduledConferenceData.swift index 58b2019d0..0115c250c 100644 --- a/Classes/Conference/data/ScheduledConferenceData.swift +++ b/Classes/Swift/Conference/data/ScheduledConferenceData.swift @@ -36,6 +36,7 @@ class ScheduledConferenceData { let organizer = MutableLiveData() let participantsShort = MutableLiveData() let participantsExpanded = MutableLiveData() + let rawDate : Date init (conferenceInfo: ConferenceInfo) { @@ -48,7 +49,8 @@ class ScheduledConferenceData { 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 ] @@ -76,4 +78,8 @@ class ScheduledConferenceData { String(describing: participant.addressBookEnhancedDisplayName())+" ("+String(describing: participant.asStringUriOnly())+")" }.joined(separator: "\n") } + + func gotoAssociatedChat() { + + } } diff --git a/Classes/Conference/data/TimeZoneData.swift b/Classes/Swift/Conference/data/TimeZoneData.swift similarity index 100% rename from Classes/Conference/data/TimeZoneData.swift rename to Classes/Swift/Conference/data/TimeZoneData.swift diff --git a/Classes/Conference/viewmodels/ConferenceSchedulingViewModel.swift b/Classes/Swift/Conference/models/ConferenceSchedulingViewModel.swift similarity index 65% rename from Classes/Conference/viewmodels/ConferenceSchedulingViewModel.swift rename to Classes/Swift/Conference/models/ConferenceSchedulingViewModel.swift index d373f0673..22eb92419 100644 --- a/Classes/Conference/viewmodels/ConferenceSchedulingViewModel.swift +++ b/Classes/Swift/Conference/models/ConferenceSchedulingViewModel.swift @@ -22,7 +22,6 @@ import Foundation import linphonesw - class ConferenceSchedulingViewModel { let core = Core.get() @@ -45,54 +44,78 @@ class ConferenceSchedulingViewModel { let sendInviteViaChat = MutableLiveData() let sendInviteViaEmail = MutableLiveData() - + let address = MutableLiveData
() let conferenceCreationInProgress = MutableLiveData() - let conferenceCreationCompletedEvent: MutableLiveData = MediatorLiveData() + let conferenceCreationCompletedEvent: MutableLiveData> = MutableLiveData() let onErrorEvent = MutableLiveData() - let continueEnabled: MediatorLiveData = MediatorLiveData() + let continueEnabled: MutableLiveData = MutableLiveData() let selectedAddresses = MutableLiveData<[Address]>([]) + private let conferenceScheduler = try? Core.get().createConferenceScheduler() + private var hour: Int = 0 private var minutes: Int = 0 - private var coreDelegate : CoreDelegateStub? = nil private var chatRooomDelegate : ChatRoomDelegate? = nil + private var conferenceSchedulerDelegate : ConferenceSchedulerDelegateStub? = nil init () { - coreDelegate = CoreDelegateStub( - onConferenceStateChanged : { (core: Core, conference: Conference, state: Conference.State?) -> Void in - Log.i("[Conference Creation] Conference state changed: \(state)") - if (state == .CreationPending) { - Log.i("[Conference Creation] Conference address will be \(conference.conferenceAddress?.asStringUriOnly())") - self.address.value = conference.conferenceAddress + + 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.scheduleForLater.value == true) { - self.sendConferenceInfo() + 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 = true + self.conferenceCreationCompletedEvent.value = Pair(conferenceAddress.asStringUriOnly(),self.conferenceScheduler?.info?.subject) } } - }, - onConferenceInfoOnSent : { (core: Core, conferenceInfo:ConferenceInfo) -> Void in + }, onInvitationsSent: { conferenceScheduler, failedInvitations in Log.i("[Conference Creation] Conference information successfully sent to all participants") self.conferenceCreationInProgress.value = false - self.conferenceCreationCompletedEvent.value = true - }, - onConferenceInfoOnParticipantError : { (core: Core, conferenceInfo: ConferenceInfo, participant: Address, error: ConferenceInfoError?) -> Void in - Log.e("[Conference Creation] Conference information wasn't sent to participant \(participant.asStringUriOnly())") - self.onErrorEvent.value = VoipTexts.conference_schedule_info_not_sent_to_participant - 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) } ) - Core.get().addDelegate(delegate: coreDelegate!) + + + conferenceScheduler?.addDelegate(delegate: conferenceSchedulerDelegate!) + chatRooomDelegate = ChatRoomDelegateStub( onStateChanged : { (room: ChatRoom, state: ChatRoom.State) -> Void in @@ -134,7 +157,7 @@ class ConferenceSchedulingViewModel { 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 @@ -149,10 +172,14 @@ class ConferenceSchedulingViewModel { func destroy() { - core.removeDelegate(delegate: coreDelegate!) + conferenceScheduler?.removeDelegate(delegate: conferenceSchedulerDelegate!) } + func gotoChatRoom() { + + } + func createConference() { @@ -163,8 +190,12 @@ class ConferenceSchedulingViewModel { do { conferenceCreationInProgress.value = true - let localAddress = core.defaultAccount?.params?.identityAddress + guard let localAddress = core.defaultAccount?.params?.identityAddress else { + Log.e("[Conference Creation] Couldn't get local address from default account!") + return + } + /* // TODO: Temporary workaround for chat room, to be removed once we can get matching chat room from conference let chatRoomParams = try core.createDefaultChatRoomParams() chatRoomParams.backend = ChatRoomBackend.FlexisipChat @@ -173,16 +204,21 @@ class ConferenceSchedulingViewModel { let chatRoom = try core.createChatRoom(params: chatRoomParams, localAddr: localAddress, participants: selectedAddresses.value!) Log.i("[Conference Creation] Creating chat room with same subject [\(subject.value)] & participants as for conference") chatRoom.addDelegate(delegate: chatRooomDelegate!) - let params = try core.createConferenceParams() - params.videoEnabled = true // TODO: Keep this to true ? - params.subject = subject.value! - let startTime = getConferenceStartTimestamp() - params.startTime = time_t(startTime) + // END OF TODO +*/ - scheduledDuration.value.map { - params.endTime = params.startTime + $0 + let conferenceInfo = try Factory.Instance.createConferenceInfo() + 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($0) } } - try core.createConferenceOnServer(params: params, localAddr: localAddress, participants: selectedAddresses.value!) + conferenceScheduler?.info = conferenceInfo // Will trigger the conference creation automatically + } catch { Log.e("[Conference Creation] Failed \(error)") } @@ -194,30 +230,7 @@ class ConferenceSchedulingViewModel { return subject.value != nil && subject.value!.count > 0 && (scheduleForLater.value != true || (scheduledDate.value != nil && scheduledTime.value != nil)); } - private func sendConferenceInfo() { - let participants :[Address] = [] - - do { - let conferenceInfo = try Factory.Instance.createConferenceInfo() - conferenceInfo.uri = try Factory.Instance.createAddress(addr: "sip:video-conference-0@sip.linphone.org") // TODO: use address.value - conferenceInfo.participants = participants - conferenceInfo.organizer = core.defaultAccount?.params?.identityAddress - subject.value.map { conferenceInfo.subject = $0} - description.value.map { conferenceInfo.description = $0} - scheduledDuration.value.map {conferenceInfo.duration = $0 } - let timestamp = getConferenceStartTimestamp() - conferenceInfo.dateTime = time_t(timestamp) - - Log.i("[Conference Creation] Conference date & time set to ${TimestampUtils.dateToString(timestamp)} ${TimestampUtils.timeToString(timestamp)}, duration = ${conferenceInfo.duration}") - core.sendConferenceInformation(conferenceInformation: conferenceInfo, text: "") - - conferenceCreationInProgress.value = false - conferenceCreationCompletedEvent.value = true - } catch { - Log.e("[Conference Creation] unable to create conference \(error)") - } - } - + private func getConferenceStartTimestamp() -> Double { return scheduleForLater.value == true ? scheduledDate.value!.timeIntervalSince1970 + scheduledTime.value!.timeIntervalSince1970 : Date().timeIntervalSince1970 diff --git a/Classes/Swift/Conference/models/ScheduledConferencesViewModel.swift b/Classes/Swift/Conference/models/ScheduledConferencesViewModel.swift new file mode 100644 index 000000000..15a69d549 --- /dev/null +++ b/Classes/Swift/Conference/models/ScheduledConferencesViewModel.swift @@ -0,0 +1,74 @@ +/* + * 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 { + + let core = 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() + core.futureConferenceInformationList.forEach { conferenceInfo in // Sorted in the sdk + 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..2c38a8c47 --- /dev/null +++ b/Classes/Swift/Conference/views/ConferenceHistoryDetailsView.swift @@ -0,0 +1,165 @@ +/* + * 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 conectionsListTableView = UITableView() + let participantsLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_participants_list) + 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).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 & 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() + + leftColumn.wrapContentY().done() + + // Right column (Duration & Timezone) + 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().alignUnder(view: datePicker,withMargin: form_margin).done() + + rightColumn.addSubview(timePicker) + timePicker.alignParentLeft().alignUnder(view: timeLabel,withMargin: form_margin).matchParentSideBorders().done() + + + rightColumn.wrapContentY().done() + + + scheduleForm.wrapContentY().done() + + // Participants + participantsLabel.backgroundColor = VoipTheme.voipFormBackgroundColor.get() + contentView.addSubview(participantsLabel) + participantsLabel.matchParentSideBorders().height(form_input_height).alignUnder(view: schedulingStack,withMargin: form_margin*2).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 + 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) { + // TODO when available : create view model from the conference that should be retreivable via call log + } + + + // TableView datasource delegate + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + guard let data = conferenceData else { + return 0 + } + return 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 = data.conferenceInfo.participants[indexPath.row] + cell.limeBadge.isHidden = true + return cell + } + + +} diff --git a/Classes/Conference/views/ConferenceSchedulingSummaryView.swift b/Classes/Swift/Conference/views/ConferenceSchedulingSummaryView.swift similarity index 86% rename from Classes/Conference/views/ConferenceSchedulingSummaryView.swift rename to Classes/Swift/Conference/views/ConferenceSchedulingSummaryView.swift index 2d809e857..cabdebde2 100644 --- a/Classes/Conference/views/ConferenceSchedulingSummaryView.swift +++ b/Classes/Swift/Conference/views/ConferenceSchedulingSummaryView.swift @@ -21,8 +21,11 @@ import UIKit import Foundation import linphonesw +import SVProgressHUD -@objc class ConferenceSchedulingSummaryView: NavigationView, UICompositeViewDelegate, UITableViewDataSource { +@objc class ConferenceSchedulingSummaryView: BackNextNavigationView, UICompositeViewDelegate, UITableViewDataSource { + + let CONFERENCE_CREATION_TIME_OUT_SEC = 15.0 let viewModel = ConferenceSchedulingViewModel.shared let participantsListTableView = UITableView() @@ -166,14 +169,52 @@ import linphonesw } // Create / Schedule - let createButton = FormButton() + let createButton = FormButton(backgroundStateColors: VoipTheme.primary_colors_background) contentView.addSubview(createButton) + viewModel.scheduleForLater.readCurrentAndObserve { _ in + createButton.title = self.viewModel.scheduleForLater.value == true ? VoipTexts.conference_schedule.uppercased() : VoipTexts.conference_schedule_create.uppercased() + createButton.addSidePadding() + } + + self.viewModel.conferenceCreationInProgress.observe { progress in + if (progress == true) { + SVProgressHUD.show() + } else { + SVProgressHUD.dismiss() + } + } + + var enableCreationTimeOut = false + + viewModel.conferenceCreationCompletedEvent.observe { pair in + enableCreationTimeOut = false + if (self.viewModel.scheduleForLater.value == true) { + PhoneMainView.instance().pop(toView:ScheduledConferencesView.compositeDescription) + } else { + let view: ConferenceWaitingRoomFragment = self.VIEW(ConferenceWaitingRoomFragment.compositeViewDescription()); + PhoneMainView.instance().pop(toView:view.compositeViewDescription()) + view.setDetails(subject: pair!.second!, url: pair!.first!) + } + } + viewModel.onErrorEvent.observe { error in + VoipDialog.init(message: error!).show() + } createButton.onClick { + enableCreationTimeOut = true self.viewModel.createConference() + DispatchQueue.main.asyncAfter(deadline: .now() + self.CONFERENCE_CREATION_TIME_OUT_SEC) { + if (enableCreationTimeOut) { + enableCreationTimeOut = false + self.viewModel.conferenceCreationInProgress.value = false + self.viewModel.onErrorEvent.value = VoipTexts.call_error_server_timeout + } + } } viewModel.scheduleForLater.readCurrentAndObserve { _ in createButton.title = self.viewModel.scheduleForLater.value == true ? VoipTexts.conference_schedule.uppercased() : VoipTexts.conference_schedule_create.uppercased() + createButton.addSidePadding() } + createButton.centerX().alignParentBottom(withMargin: 3*self.form_margin).alignUnder(view: participantsListTableView,withMargin: 3*self.form_margin).done() } diff --git a/Classes/Conference/views/ConferenceSchedulingView.swift b/Classes/Swift/Conference/views/ConferenceSchedulingView.swift similarity index 98% rename from Classes/Conference/views/ConferenceSchedulingView.swift rename to Classes/Swift/Conference/views/ConferenceSchedulingView.swift index a471138f8..a64402d96 100644 --- a/Classes/Conference/views/ConferenceSchedulingView.swift +++ b/Classes/Swift/Conference/views/ConferenceSchedulingView.swift @@ -22,7 +22,7 @@ import UIKit import Foundation import linphonesw -@objc class ConferenceSchedulingView: NavigationView, UICompositeViewDelegate { +@objc class ConferenceSchedulingView: BackNextNavigationView, UICompositeViewDelegate { let viewModel = ConferenceSchedulingViewModel.shared @@ -195,6 +195,7 @@ import linphonesw 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()) diff --git a/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift b/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift new file mode 100644 index 000000000..867ca5f31 --- /dev/null +++ b/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.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 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 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) + + 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) + view.addSubview(controlsView) + controlsView.alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done() + + + // 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 { + PhoneMainView.instance().popView(self.compositeViewDescription()) + } + + start.onClick { + self.conferenceUrl.map{ CallManager.instance().startCall(addr: $0, isSas: false) } + } + + + // localVideo view + localVideo.layer.cornerRadius = center_view_corner_radius + localVideo.clipsToBounds = true + 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 + } + + // 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() + + + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(true) + ControlsViewModel.shared.audioRoutesSelected.value = false + Core.get().nativePreviewWindow = localVideo + Core.get().videoPreviewEnabled = true + } + + override func viewWillDisappear(_ animated: Bool) { + ControlsViewModel.shared.fullScreenMode.value = false + Core.get().nativePreviewWindow = nil + Core.get().videoPreviewEnabled = 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..0b5f21a87 --- /dev/null +++ b/Classes/Swift/Conference/views/ScheduledConferencesCell.swift @@ -0,0 +1,126 @@ +/* + * 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 + + 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 participants = StyledLabel(VoipTheme.conference_invite_desc_font) + let infoConf = UIButton() + + let descriptionTitle = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_description_title) + let descriptionValue = StyledLabel(VoipTheme.conference_scheduling_font) + let urlTitle = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_address_title) + let urlAndCopy = UIView() + let urlValue = StyledLabel(VoipTheme.conference_scheduling_font) + let copyLink = CallControlButton(buttonTheme: VoipTheme.scheduled_conference_action("voip_copy")) + let joinEditDelete = UIView() + let joinConf = FormButton(title:VoipTexts.conference_invite_join.uppercased(), backgroundStateColors: VoipTheme.button_green_background) + let deleteConf = CallControlButton(buttonTheme: VoipTheme.scheduled_conference_action("voip_delete")) + let editConf = CallControlButton(buttonTheme: VoipTheme.scheduled_conference_action("voip_edit")) + + var conferenceData: ScheduledConferenceData? = nil { + didSet { + if let data = conferenceData { + timeDuration.text = "\(data.time) ( \(data.duration) )" + timeDuration.addIndicatorIcon(iconName: "conference_schedule_time_default", trailing: false) + 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.isHidden = expanded != true + self.urlAndCopy.isHidden = expanded != true + self.joinEditDelete.isHidden = expanded != true + self.infoConf.isSelected = expanded == true + self.participants.text = expanded == true ? data.participantsExpanded.value : data.participantsShort.value + self.participants.addIndicatorIcon(iconName: "conference_schedule_participants_default", trailing: false) + } + } + } + } + + 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 + + let rows = UIStackView() + rows.axis = .vertical + rows.addArrangedSubview(timeDuration) + rows.addArrangedSubview(subject) + + let participantsAndInfos = UIView() + participantsAndInfos.addSubview(participants) + participants.alignParentLeft().done() + participantsAndInfos.addSubview(infoConf) + infoConf.toRightOf(participants).done() + rows.addArrangedSubview(participantsAndInfos) + infoConf.applyTintedIcons(tintedIcons: VoipTheme.conference_info_button) + infoConf.onClick { + self.conferenceData?.toggleExpand() + } + + rows.addArrangedSubview(descriptionTitle) + rows.addArrangedSubview(descriptionValue) + + rows.addArrangedSubview(urlTitle) + urlAndCopy.addSubview(urlValue) + urlValue.backgroundColor = .white + urlValue.alignParentLeft().done() + urlAndCopy.addSubview(copyLink) + copyLink.toLeftOf(urlValue).done() + rows.addArrangedSubview(urlAndCopy) + + joinEditDelete.addSubview(joinConf) + joinEditDelete.addSubview(editConf) + joinEditDelete.addSubview(deleteConf) + deleteConf.alignParentRight().done() + editConf.toLeftOf(deleteConf).done() + joinConf.toLeftOf(deleteConf).done() + + joinConf.onClick { + /* + ConferenceWaitingRoomFragment *view = VIEW(ConferenceWaitingRoomFragment); + [PhoneMainView.instance changeCurrentView:ConferenceWaitingRoomFragment.compositeViewDescription]; + [view setDetailsWithSubject:@"Sujet de la conférence" url:@"toto"]; + return; + */ + } + } + + 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..f8139e375 --- /dev/null +++ b/Classes/Swift/Conference/views/ScheduledConferencesView.swift @@ -0,0 +1,108 @@ +/* + * 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 { + + 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: { + }, + nextActionEnableCondition: MutableLiveData(false), + title:VoipTexts.conference_scheduled) + super.nextButton.isHidden = true + + + contentView.addSubview(conferenceListView) + conferenceListView.isScrollEnabled = false + conferenceListView.dataSource = self + conferenceListView.register(ScheduledConferencesCell.self, forCellReuseIdentifier: "ScheduledConferencesCell") + conferenceListView.allowsSelection = false + if #available(iOS 15.0, *) { + conferenceListView.allowsFocus = false + } + conferenceListView.separatorStyle = .singleLine + conferenceListView.separatorColor = VoipTheme.light_grey_color + + 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().alignUnder(view: super.topBar,withMargin: self.form_margin).alignParentBottom().done() + noConference.isHidden = !ScheduledConferencesViewModel.shared.daySplitted.isEmpty + } + + // TableView datasource delegate + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + let daysArray = Array(ScheduledConferencesViewModel.shared.daySplitted.keys) + let day = daysArray[section] + return TimestampUtils.dateToString(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) + } + + 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) + let day = daysArray[section] + return ScheduledConferencesViewModel.shared.daySplitted[day]!.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell:ScheduledConferencesCell = tableView.dequeueReusableCell(withIdentifier: "ScheduledConferencesCell") as! ScheduledConferencesCell + let daysArray = Array(ScheduledConferencesViewModel.shared.daySplitted.keys) + let day = daysArray[indexPath.section] + guard let data = ScheduledConferencesViewModel.shared.daySplitted[day]?[indexPath.row] else { + return cell + } + cell.conferenceData = data + 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/SwiftUtil/Extensions/IOS/OptionalExtensions.swift b/Classes/Swift/Extensions/IOS/OptionalExtensions.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/IOS/OptionalExtensions.swift rename to Classes/Swift/Extensions/IOS/OptionalExtensions.swift diff --git a/Classes/SwiftUtil/Extensions/IOS/UIApplication+Extension.swift b/Classes/Swift/Extensions/IOS/UIApplication+Extension.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/IOS/UIApplication+Extension.swift rename to Classes/Swift/Extensions/IOS/UIApplication+Extension.swift diff --git a/Classes/SwiftUtil/Extensions/IOS/UIButtonExtensions.swift b/Classes/Swift/Extensions/IOS/UIButtonExtensions.swift similarity index 74% rename from Classes/SwiftUtil/Extensions/IOS/UIButtonExtensions.swift rename to Classes/Swift/Extensions/IOS/UIButtonExtensions.swift index 2e7825594..5ceed069a 100644 --- a/Classes/SwiftUtil/Extensions/IOS/UIButtonExtensions.swift +++ b/Classes/Swift/Extensions/IOS/UIButtonExtensions.swift @@ -27,4 +27,14 @@ extension UIButton { width(w+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)) + } + } + } + } diff --git a/Classes/SwiftUtil/Extensions/IOS/UIColorExtensions.swift b/Classes/Swift/Extensions/IOS/UIColorExtensions.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/IOS/UIColorExtensions.swift rename to Classes/Swift/Extensions/IOS/UIColorExtensions.swift diff --git a/Classes/SwiftUtil/Extensions/IOS/UIDeviceExtensions.swift b/Classes/Swift/Extensions/IOS/UIDeviceExtensions.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/IOS/UIDeviceExtensions.swift rename to Classes/Swift/Extensions/IOS/UIDeviceExtensions.swift diff --git a/Classes/SwiftUtil/Extensions/IOS/UIImageExtensions.swift b/Classes/Swift/Extensions/IOS/UIImageExtensions.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/IOS/UIImageExtensions.swift rename to Classes/Swift/Extensions/IOS/UIImageExtensions.swift diff --git a/Classes/SwiftUtil/Extensions/IOS/UIImageViewExtensions.swift b/Classes/Swift/Extensions/IOS/UIImageViewExtensions.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/IOS/UIImageViewExtensions.swift rename to Classes/Swift/Extensions/IOS/UIImageViewExtensions.swift diff --git a/Classes/SwiftUtil/Extensions/IOS/UILabelExtensions.swift b/Classes/Swift/Extensions/IOS/UILabelExtensions.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/IOS/UILabelExtensions.swift rename to Classes/Swift/Extensions/IOS/UILabelExtensions.swift diff --git a/Classes/SwiftUtil/Extensions/IOS/UIVIewControllerExtensions.swift b/Classes/Swift/Extensions/IOS/UIVIewControllerExtensions.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/IOS/UIVIewControllerExtensions.swift rename to Classes/Swift/Extensions/IOS/UIVIewControllerExtensions.swift diff --git a/Classes/SwiftUtil/Extensions/IOS/UIVIewExtensions.swift b/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift similarity index 94% rename from Classes/SwiftUtil/Extensions/IOS/UIVIewExtensions.swift rename to Classes/Swift/Extensions/IOS/UIVIewExtensions.swift index d557a2294..46008f213 100644 --- a/Classes/SwiftUtil/Extensions/IOS/UIVIewExtensions.swift +++ b/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift @@ -106,6 +106,22 @@ extension UIView { 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() diff --git a/Classes/SwiftUtil/Extensions/LinphoneCore/AddressExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift similarity index 90% rename from Classes/SwiftUtil/Extensions/LinphoneCore/AddressExtensions.swift rename to Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift index 39bd741f8..d4aff4ef7 100644 --- a/Classes/SwiftUtil/Extensions/LinphoneCore/AddressExtensions.swift +++ b/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift @@ -39,7 +39,9 @@ extension Address { } func addressBookEnhancedDisplayName() -> String? { - if let contact = FastAddressBook.getContactWith(getCobject) { + if (username == Core.get().defaultAccount?.contactAddress?.username) { + return VoipTexts.me + } else if let contact = FastAddressBook.getContactWith(getCobject) { return contact.displayName } else if (!displayName.isEmpty) { return displayName diff --git a/Classes/SwiftUtil/Extensions/LinphoneCore/CallExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/CallExtensions.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/LinphoneCore/CallExtensions.swift rename to Classes/Swift/Extensions/LinphoneCore/CallExtensions.swift diff --git a/Classes/SwiftUtil/Extensions/LinphoneCore/ConferenceExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/LinphoneCore/ConferenceExtensions.swift rename to Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift diff --git a/Classes/SwiftUtil/Extensions/LinphoneCore/CoreExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift similarity index 96% rename from Classes/SwiftUtil/Extensions/LinphoneCore/CoreExtensions.swift rename to Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift index ee0c99edb..f9a4c15f1 100644 --- a/Classes/SwiftUtil/Extensions/LinphoneCore/CoreExtensions.swift +++ b/Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift @@ -30,6 +30,9 @@ extension Core { } func toggleCamera() { + + UICamSwitch.switchCamera() + /* Not working Log.i("[Core] Current camera device is \(videoDevice)") videoDevicesList.forEach { @@ -43,6 +46,6 @@ extension Core { let inConference = conference != nil && conference!.isIn if !inConference, let call = currentCall { try?call.update(params: nil) - } + }*/ } } diff --git a/Classes/SwiftUtil/Extensions/LinphoneCore/IceState.swift b/Classes/Swift/Extensions/LinphoneCore/IceState.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/LinphoneCore/IceState.swift rename to Classes/Swift/Extensions/LinphoneCore/IceState.swift diff --git a/Classes/SwiftUtil/Extensions/LinphoneCore/ParticipantExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/ParticipantExtensions.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/LinphoneCore/ParticipantExtensions.swift rename to Classes/Swift/Extensions/LinphoneCore/ParticipantExtensions.swift diff --git a/Classes/SwiftUtil/Extensions/LinphoneCore/PayloadType.swift b/Classes/Swift/Extensions/LinphoneCore/PayloadType.swift similarity index 100% rename from Classes/SwiftUtil/Extensions/LinphoneCore/PayloadType.swift rename to Classes/Swift/Extensions/LinphoneCore/PayloadType.swift diff --git a/Classes/ProviderDelegate.swift b/Classes/Swift/ProviderDelegate.swift similarity index 100% rename from Classes/ProviderDelegate.swift rename to Classes/Swift/ProviderDelegate.swift diff --git a/Classes/SwiftUtil/GenericViews/NavigationView.swift b/Classes/Swift/Util/BackNextNavigationView.swift similarity index 97% rename from Classes/SwiftUtil/GenericViews/NavigationView.swift rename to Classes/Swift/Util/BackNextNavigationView.swift index a44bf3d89..ca61e6b20 100644 --- a/Classes/SwiftUtil/GenericViews/NavigationView.swift +++ b/Classes/Swift/Util/BackNextNavigationView.swift @@ -22,11 +22,11 @@ import UIKit import Foundation import linphonesw -@objc class NavigationView: UIViewController { +@objc class BackNextNavigationView: UIViewController { // layout constants - let top_bar_height = 60.0 + let top_bar_height = 66.0 let navigation_buttons_padding = 18.0 let content_margin_top = 20 @@ -35,9 +35,7 @@ import linphonesw 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() diff --git a/Classes/SwiftUtil/ViewModel/MutableLiveData.swift b/Classes/Swift/Util/MutableLiveData.swift similarity index 100% rename from Classes/SwiftUtil/ViewModel/MutableLiveData.swift rename to Classes/Swift/Util/MutableLiveData.swift 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/SwiftUtil/TimestampUtils.swift b/Classes/Swift/Util/TimestampUtils.swift similarity index 100% rename from Classes/SwiftUtil/TimestampUtils.swift rename to Classes/Swift/Util/TimestampUtils.swift diff --git a/Classes/SwiftUtil/ViewModel/MediatorLiveData.swift b/Classes/Swift/Util/ViewModel/MediatorLiveData.swift similarity index 100% rename from Classes/SwiftUtil/ViewModel/MediatorLiveData.swift rename to Classes/Swift/Util/ViewModel/MediatorLiveData.swift 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 79343d154..5047fd21c 100644 --- a/Classes/VFSUtil.swift +++ b/Classes/Swift/VFSUtil.swift @@ -187,7 +187,7 @@ import os return false } guard let secret = decrypt(encryptedText: encryptedKey) else { - log(log: "[VFS] Unable to decryt encrypted key.", level: .error) + log("[VFS] Unable to decryt encrypted key.", .error) return false } Factory.Instance.setVfsEncryption(encryptionModule: 2, secret: secret, secretSize: 32) diff --git a/Classes/Voip/AudioRouteUtils.swift b/Classes/Swift/Voip/AudioRouteUtils.swift similarity index 100% rename from Classes/Voip/AudioRouteUtils.swift rename to Classes/Swift/Voip/AudioRouteUtils.swift diff --git a/Classes/Voip/Models/CallData.swift b/Classes/Swift/Voip/Models/CallData.swift similarity index 78% rename from Classes/Voip/Models/CallData.swift rename to Classes/Swift/Voip/Models/CallData.swift index 49f5fa2a5..fb69db380 100644 --- a/Classes/Voip/Models/CallData.swift +++ b/Classes/Swift/Voip/Models/CallData.swift @@ -54,7 +54,7 @@ class CallData { self.iFrameReceived.value = true }, onRemoteRecording: { (call: linphonesw.Call, recording:Bool) -> Void in - self.isRemotelyRecorded.value = true + self.isRemotelyRecorded.value = recording } ) call.addDelegate(delegate: callDelegate!) @@ -105,8 +105,13 @@ class CallData { } private func initChatRoom() { + + return // V1 work around + let localSipUri = Core.get().defaultAccount?.params?.identityAddress?.asStringUriOnly() let remoteSipUri = call.remoteAddress?.asStringUriOnly() + let conference = call.conference + guard let localSipUri = Core.get().defaultAccount?.params?.identityAddress?.asStringUriOnly(), @@ -118,15 +123,31 @@ class CallData { return } do { - chatRoom = Core.get().searchChatRoom(params: nil, localAddr: localAddress, remoteAddr: remoteSipAddress, participants: []) - if (chatRoom == nil) { - chatRoom = Core.get().searchChatRoom(params: nil, localAddr: localAddress, remoteAddr: nil, participants: [remoteSipAddress]) + if let conferenceInfo = Core.get().findConferenceInformationFromUri(uri: call.remoteAddress!), let params = try?Core.get().createDefaultChatRoomParams() { + params.subject = conferenceInfo.subject + params.backend = ChatRoomBackend.FlexisipChat + params.groupEnabled = true + chatRoom = Core.get().searchChatRoom(params: params, localAddr: localAddress, remoteAddr: nil, participants: conferenceInfo.participants) + } else { + chatRoom = Core.get().searchChatRoom(params: nil, localAddr: localAddress, remoteAddr: remoteSipAddress, participants: []) + if (chatRoom == nil) { + chatRoom = Core.get().searchChatRoom(params: nil, localAddr: localAddress, remoteAddr: nil, participants: [remoteSipAddress]) + } } + if (chatRoom == nil) { - Log.w("[Call] Failed to find existing chat room for local address [$localSipUri] and remote address [$remoteSipUri]") let chatRoomParams = try Core.get().createDefaultChatRoomParams() - // TODO: configure chat room params - chatRoom = try Core.get().createChatRoom(params: chatRoomParams, localAddr: localAddress, participants: [remoteSipAddress]) + if let conferenceInfo = Core.get().findConferenceInformationFromUri(uri: call.remoteAddress!) { + Log.w("[Call] Failed to find existing chat room with same subject & participants, creating it") + chatRoomParams.backend = ChatRoomBackend.FlexisipChat + chatRoomParams.groupEnabled = true + chatRoomParams.subject = conferenceInfo.subject + chatRoom = try?Core.get().createChatRoom(params: chatRoomParams, localAddr: localAddress, participants: conferenceInfo.participants) + } else { + Log.w("[Call] Failed to find existing chat room with same participants, creating it") + // TODO: configure chat room params + chatRoom = try?Core.get().createChatRoom(params: chatRoomParams, localAddr: localAddress, participants: [remoteSipAddress]) + } } if (chatRoom == nil) { diff --git a/Classes/Voip/Models/CallStatisticsData.swift b/Classes/Swift/Voip/Models/CallStatisticsData.swift similarity index 100% rename from Classes/Voip/Models/CallStatisticsData.swift rename to Classes/Swift/Voip/Models/CallStatisticsData.swift diff --git a/Classes/Voip/Models/CallsViewModel.swift b/Classes/Swift/Voip/Models/CallsViewModel.swift similarity index 99% rename from Classes/Voip/Models/CallsViewModel.swift rename to Classes/Swift/Voip/Models/CallsViewModel.swift index 2caa75232..b539b8e5c 100644 --- a/Classes/Voip/Models/CallsViewModel.swift +++ b/Classes/Swift/Voip/Models/CallsViewModel.swift @@ -46,7 +46,7 @@ class CallsViewModel { let currentCall = core.currentCall if (currentCall != nil && self.currentCallData.value??.call.getCobject != currentCall?.getCobject) { self.updateCurrentCallData(currentCall: currentCall) - } else if (currentCall == nil && core.callsNb > 1) { + } else if (currentCall == nil && core.callsNb > 0) { self.updateCurrentCallData(currentCall: currentCall) } if ([.End,.Released,.Error].contains(state)) { diff --git a/Classes/Voip/Models/ConferenceParticipantData.swift b/Classes/Swift/Voip/Models/ConferenceParticipantData.swift similarity index 100% rename from Classes/Voip/Models/ConferenceParticipantData.swift rename to Classes/Swift/Voip/Models/ConferenceParticipantData.swift diff --git a/Classes/Voip/Models/ConferenceParticipantDeviceData.swift b/Classes/Swift/Voip/Models/ConferenceParticipantDeviceData.swift similarity index 100% rename from Classes/Voip/Models/ConferenceParticipantDeviceData.swift rename to Classes/Swift/Voip/Models/ConferenceParticipantDeviceData.swift diff --git a/Classes/Voip/Models/ConferenceViewModel.swift b/Classes/Swift/Voip/Models/ConferenceViewModel.swift similarity index 58% rename from Classes/Voip/Models/ConferenceViewModel.swift rename to Classes/Swift/Voip/Models/ConferenceViewModel.swift index 26206c306..0b0f4601a 100644 --- a/Classes/Voip/Models/ConferenceViewModel.swift +++ b/Classes/Swift/Voip/Models/ConferenceViewModel.swift @@ -27,60 +27,36 @@ class ConferenceViewModel { let core = Core.get() static let shared = ConferenceViewModel() - - let isConferencePaused = MutableLiveData() - let canResumeConference = MutableLiveData() - - let isMeConferenceFocus = MutableLiveData() + let conferenceExists = MutableLiveData() + let subject = MutableLiveData() + let isConferenceLocallyPaused = MutableLiveData() + let isVideoConference = MutableLiveData() let isMeAdmin = MutableLiveData() - - let conferenceAddress = MutableLiveData
() - + + let conference = MutableLiveData() let conferenceParticipants = MutableLiveData<[ConferenceParticipantData]>() let conferenceParticipantDevices = MutableLiveData<[ConferenceParticipantDeviceData]>() let conferenceDisplayMode = MutableLiveData() - - let isInConference = MutableLiveData() - - let isVideoConference = MutableLiveData() - let isRecording = MutableLiveData() let isRemotelyRecorded = MutableLiveData() - - let subject = MutableLiveData() - - let conference = MutableLiveData() - let maxParticipantsForMosaicLayout = ConfigManager.instance().lpConfigIntForKey(key: "max_conf_part_mosaic_layout",defaultValue: 6) + private var conferenceDelegate : ConferenceDelegateStub? private var coreDelegate : CoreDelegateStub? init () { - conferenceDelegate = ConferenceDelegateStub(onParticipantAdded: { (conference: Conference, participant: Participant)in - if (conference.isMe(uri: participant.address!)) { - Log.i("[Conference] \(conference) Entered conference") - self.isConferencePaused.value = false - } else { - Log.i("[Conference] \(conference) Participant \(participant) added") - } + conferenceDelegate = ConferenceDelegateStub(onParticipantAdded: { (conference: Conference, participant: Participant) in + Log.i("[Conference] \(conference) Participant \(participant) added") self.updateParticipantsList(conference) - self.updateParticipantsDevicesList(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 } }, onParticipantRemoved: {(conference: Conference, participant: Participant) in - if (conference.isMe(uri: participant.address!)) { - Log.i("[Conference] \(conference) \(participant) Left conference") - self.isConferencePaused.value = true - } else { - Log.i("[Conference] \(conference) \(participant) Participant removed") - } + Log.i("[Conference] \(conference) \(participant) Participant removed") self.updateParticipantsList(conference) - self.updateParticipantsDevicesList(conference) }, onParticipantDeviceAdded: {(conference: Conference, participantDevice: ParticipantDevice) in Log.i("[Conference] \(conference) Participant device \(participantDevice) added") self.updateParticipantsDevicesList(conference) @@ -91,43 +67,29 @@ class ConferenceViewModel { Log.i("[Conference] \(conference) Participant admin status changed") self.isMeAdmin.value = conference.me?.isAdmin self.updateParticipantsList(conference) + }, onParticipantDeviceLeft: { (conference: Conference, device: ParticipantDevice) in + Log.i("[Conference] onParticipantDeviceJoined Entered conference") + self.isConferenceLocallyPaused.value = true + }, onParticipantDeviceJoined: { (conference: Conference, device: ParticipantDevice) in + Log.i("[Conference] onParticipantDeviceJoined Entered conference") + self.isConferenceLocallyPaused.value = false + }, onSubjectChanged: { (conference: Conference, subject: String) in + self.subject.value = subject } ) coreDelegate = CoreDelegateStub( onConferenceStateChanged: { (core, conference, state) in Log.i("[Conference] \(conference) Conference state changed: \(state)") - self.isConferencePaused.value = !conference.isIn - self.canResumeConference.value = true // TODO: How can this value be false? self.isVideoConference.value = conference.currentParams?.isVideoEnabled == true if (state == Conference.State.Instantiated) { - self.conference.value = conference - self.isInConference.value = true - conference.addDelegate(delegate: self.conferenceDelegate!) + self.initConference(conference) } else if (state == Conference.State.Created) { - self.updateParticipantsList(conference) - self.updateParticipantsDevicesList(conference) - self.isMeConferenceFocus.value = conference.me?.isFocus == true - self.isMeAdmin.value = conference.me?.isAdmin == true - self.conferenceAddress.value = conference.conferenceAddress - self.subject.value = conference.subject.isEmpty ? ( - conference.me?.isFocus == true ? ( - VoipTexts.conference_local_title - ) : ( - VoipTexts.conference_default_title - ) - ) : ( - conference.subject - ) + self.initConference(conference) + self.configureConference(conference) } else if (state == Conference.State.Terminated || state == Conference.State.TerminationFailed) { - self.isInConference.value = false - self.isVideoConference.value = false - conference.removeDelegate(delegate: self.conferenceDelegate!) - self.conferenceParticipants.value?.forEach{ $0.destroy()} - self.conferenceParticipantDevices.value?.forEach{ $0.destroy()} - self.conferenceParticipants.value = [] - self.conferenceParticipantDevices.value = [] + self.terminateConference(conference) } let layout = conference.layout == .None ? .Grid : conference.layout @@ -137,103 +99,91 @@ class ConferenceViewModel { ) Core.get().addDelegate(delegate: coreDelegate!) - - conferenceParticipants.value = [] conferenceParticipantDevices.value = [] conferenceDisplayMode.value = .Grid - subject.value = VoipTexts.conference_default_title if let conference = core.conference != nil ? core.conference : core.currentCall?.conference { Log.i("[Conference] Found an existing conference: \(conference)") - self.conference.value = conference - conference.addDelegate(delegate: self.conferenceDelegate!) - - - isInConference.value = true - isConferencePaused.value = !conference.isIn - isMeConferenceFocus.value = conference.me?.isFocus == true - isMeAdmin.value = conference.me?.isAdmin == true - isVideoConference.value = conference.currentParams?.isVideoEnabled == true - conferenceAddress.value = conference.conferenceAddress - if (!conference.subject.isEmpty) { - subject.value = conference.subject - } - - let layout = conference.layout == .None ? .Grid : conference.layout - conferenceDisplayMode.value = layout - Log.i("[Conference] \(conference) Conference current layout is: \(layout)") - - updateParticipantsList(conference) - updateParticipantsDevicesList(conference) + initConference(conference) + configureConference(conference) } } + func initConference(_ conference: Conference) { + conferenceExists.value = true + self.conference.value = conference + conference.addDelegate(delegate: self.conferenceDelegate!) + isRecording.value = conference.isRecording + } - func destroy() { - core.removeDelegate(delegate: self.coreDelegate!) + func terminateConference(_ conference: Conference) { + conferenceExists.value = false + isVideoConference.value = false self.conferenceParticipants.value?.forEach{ $0.destroy()} self.conferenceParticipantDevices.value?.forEach{ $0.destroy()} + conferenceParticipants.value = [] + conferenceParticipantDevices.value = [] } + func configureConference(_ conference: Conference) { + self.updateParticipantsList(conference) + self.updateParticipantsDevicesList(conference) + + isConferenceLocallyPaused.value = !conference.isIn + self.isMeAdmin.value = conference.me?.isAdmin == true + isVideoConference.value = conference.currentParams?.isVideoEnabled == true + + self.subject.value = conference.subject.isEmpty ? ( + conference.me?.isFocus == true ? ( + VoipTexts.conference_local_title + ) : ( + VoipTexts.conference_default_title + ) + ) : ( + conference.subject + ) + } + + func pauseConference() { - let defaultProxyConfig = core.defaultProxyConfig - let localAddress = defaultProxyConfig?.identityAddress - let participants : [Address] = [] - let remoteConference = core.searchConference(params: nil, localAddr: localAddress, remoteAddr: conferenceAddress.value, participants: participants) - let localConference = core.searchConference(params: nil, localAddr: conferenceAddress.value, remoteAddr: conferenceAddress.value, participants: participants) - let conference = remoteConference != nil ? remoteConference : localConference - - if (conference != nil) { - Log.i("[Conference] Leaving conference with address \(conference) temporarily") - conference!.leave() - } else { - Log.w("[Conference] Unable to find conference with address \(conference)") - } + Log.i("[Conference] Leaving conference with address \(conference) temporarily") + conference.value?.leave() } func resumeConference() { - let defaultProxyConfig = core.defaultProxyConfig - let localAddress = defaultProxyConfig?.identityAddress - let participants : [Address] = [] - let remoteConference = core.searchConference(params: nil, localAddr: localAddress, remoteAddr: conferenceAddress.value, participants: participants) - let localConference = core.searchConference(params: nil, localAddr: conferenceAddress.value, remoteAddr: conferenceAddress.value, participants: participants) - - if let conference = remoteConference != nil ? remoteConference : localConference { - Log.i("[Conference] Entering again conference with address \(conference)") - conference.enter() - } else { - Log.w("[Conference] Unable to find conference with address \(conference)") - } + Log.i("[Conference] entering conference with address \(conference)") + conference.value?.enter() } func togglePlayPause () { - if (isConferencePaused.value == true) { + if (isConferenceLocallyPaused.value == true) { resumeConference() - isConferencePaused.value = false + isConferenceLocallyPaused.value = false } else { pauseConference() - isConferencePaused.value = true + isConferenceLocallyPaused.value = true } } func toggleRecording() { - guard let conference = core.conference else { + guard let conference = conference.value else { Log.e("[Conference] Failed to find conference!") return } - + /* frogtrust has is own recording method if (conference.isRecording == true) { conference.stopRecording() } else { let path = AppManager.recordingFilePathFromCall(address: conference.conferenceAddress?.asStringUriOnly() ?? "") Log.i("[Conference] Starting recording \(conference) in file \(path)") conference.startRecording(path: path) - } + }*/ + isRecording.value = conference.isRecording } @@ -250,7 +200,7 @@ class ConferenceViewModel { let participantData = ConferenceParticipantData(conference: conference, participant: participant) participants.append(participantData) } - + conferenceParticipants.value = participants } @@ -266,24 +216,80 @@ class ConferenceViewModel { 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()))") + Log.i("[Conference] \(conference) Participant device found: \(device.name) (\(device.address!.asStringUriOnly()))") let deviceData = ConferenceParticipantDeviceData(participantDevice: device, isMe: false) - devices.append(deviceData) + 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 } + 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()) will be removed from conference") + try conference.removeParticipant(participant: participant) + } + } + } catch { + Log.e("[Conference Participants] Error updating participant lists \(error)") + } + } + + 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) + } + } + } + + } +@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)") + } + } +} + + + + + enum FlexDirection { case ROW case ROW_REVERSE diff --git a/Classes/Voip/Models/ControlsViewModel.swift b/Classes/Swift/Voip/Models/ControlsViewModel.swift similarity index 100% rename from Classes/Voip/Models/ControlsViewModel.swift rename to Classes/Swift/Voip/Models/ControlsViewModel.swift diff --git a/Classes/Voip/Theme/ButtonTheme.swift b/Classes/Swift/Voip/Theme/ButtonTheme.swift similarity index 100% rename from Classes/Voip/Theme/ButtonTheme.swift rename to Classes/Swift/Voip/Theme/ButtonTheme.swift diff --git a/Classes/Voip/Theme/LightDarkColor.swift b/Classes/Swift/Voip/Theme/LightDarkColor.swift similarity index 100% rename from Classes/Voip/Theme/LightDarkColor.swift rename to Classes/Swift/Voip/Theme/LightDarkColor.swift diff --git a/Classes/Voip/Theme/TextStyle.swift b/Classes/Swift/Voip/Theme/TextStyle.swift similarity index 92% rename from Classes/Voip/Theme/TextStyle.swift rename to Classes/Swift/Voip/Theme/TextStyle.swift index 66c6d45e1..bf1f93c30 100644 --- a/Classes/Voip/Theme/TextStyle.swift +++ b/Classes/Swift/Voip/Theme/TextStyle.swift @@ -47,10 +47,10 @@ extension UILabel { font = UIFont.init(name: style.font, size: CGFloat(style.size*fontSizeMultiplier)) } - func addIndicatorIcon(iconName:String, _ padding:CGFloat = 5.0, trailing: Bool = true) { + 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: 4.0, width: font.lineHeight - 2*padding, height: font.lineHeight - 2*padding) + 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) { diff --git a/Classes/Voip/Theme/VoipTexts.swift b/Classes/Swift/Voip/Theme/VoipTexts.swift similarity index 95% rename from Classes/Voip/Theme/VoipTexts.swift rename to Classes/Swift/Voip/Theme/VoipTexts.swift index b5c1e8f21..f8e1d2649 100644 --- a/Classes/Voip/Theme/VoipTexts.swift +++ b/Classes/Swift/Voip/Theme/VoipTexts.swift @@ -23,7 +23,8 @@ 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 - + static let me = NSLocalizedString("Me",comment:"") + // Calls static let call_incoming_title = NSLocalizedString("Incoming Call",comment:"") static let call_outgoing_title = NSLocalizedString("Outgoing Call",comment:"") @@ -97,7 +98,11 @@ import UIKit static let conference_scheduled = NSLocalizedString("Conferences",comment:"") static let conference_too_many_participants_for_mosaic_layout = NSLocalizedString("You can't change conference layout as there is too many participants",comment:"") static let conference_participant_paused = NSLocalizedString("(paused)",comment:"") - + static let conference_no_schedule = NSLocalizedString("No scheduled conference yet.",comment:"") + static let conference_schedule_organizer = NSLocalizedString("Organizer:",comment:"") + static let conference_go_to_chat = NSLocalizedString("Conference's chat room",comment:"") + static let conference_creation_failed = NSLocalizedString("Failed to create conference",comment:"") + // Call Stats diff --git a/Classes/Voip/Theme/VoipTheme.swift b/Classes/Swift/Voip/Theme/VoipTheme.swift similarity index 89% rename from Classes/Voip/Theme/VoipTheme.swift rename to Classes/Swift/Voip/Theme/VoipTheme.swift index c17d33e09..72d6ae29e 100644 --- a/Classes/Voip/Theme/VoipTheme.swift +++ b/Classes/Swift/Voip/Theme/VoipTheme.swift @@ -53,6 +53,9 @@ class VoipTheme { // Names & values replicated from Android 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) @@ -72,6 +75,7 @@ class VoipTheme { // Names & values replicated from Android + // 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) @@ -86,7 +90,8 @@ class VoipTheme { // Names & values replicated from Android 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 big_button = TextStyle(fgColor: LightDarkColor(.white,.white), bgColor: LightDarkColor(.clear,.clear), allCaps: true, align: .center, font: fontName+"-Bold", size: 17.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) @@ -104,18 +109,27 @@ class VoipTheme { // Names & values replicated from Android 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_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 empty_list_font = TextStyle(fgColor: primaryTextColor, bgColor: LightDarkColor(.clear,.clear), allCaps: false, align: .center, font: fontName+"-Regular", size: 18.0) + + + + + @@ -156,13 +170,16 @@ class VoipTheme { // Names & values replicated from Android 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), @@ -353,6 +370,18 @@ class VoipTheme { // Names & values replicated from Android 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)), + ] + } diff --git a/Classes/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift similarity index 85% rename from Classes/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift rename to Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index f85be7062..924d2d95f 100644 --- a/Classes/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -22,7 +22,7 @@ import UIKit import linphonesw -class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // Replaces CallView +@objc class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // Replaces CallView // Layout constants let content_inset = 12.0 @@ -40,7 +40,7 @@ class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // var shadingMask = UIView() var videoAcceptDialog : VoipDialog? = nil var dismissableView : DismissableView? = nil - var participantsListView : ParticipantsListView? = nil + @objc var participantsListView : ParticipantsListView? = nil var audioRoutesView : AudioRoutesView? = nil @@ -68,7 +68,7 @@ class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // controlsView.alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done() - // Container fiew + // Container view let fullScreenMutableContainerView = UIView() fullScreenMutableContainerView.backgroundColor = .clear self.view.addSubview(fullScreenMutableContainerView) @@ -80,7 +80,7 @@ class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // fullScreenMutableContainerView.addSubview(currentCallView!) CallsViewModel.shared.currentCallData.readCurrentAndObserve { (currentCallData) in self.updateNavigation() - self.currentCallView!.isHidden = currentCallData == nil || ConferenceViewModel.shared.isInConference.value == true + 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 @@ -114,7 +114,7 @@ class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // fullScreenMutableContainerView.addSubview(conferenceGridView!) conferenceGridView?.matchParentDimmensions().done() conferenceGridView?.isHidden = true - ConferenceViewModel.shared.isInConference.readCurrentAndObserve { (isInConference) in + ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { (isInConference) in self.updateNavigation() if (isInConference == true) { self.currentCallView!.isHidden = true @@ -137,7 +137,7 @@ class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // // Conference mode switching ConferenceViewModel.shared.conferenceDisplayMode.readCurrentAndObserve { (conferenceMode) in - if (ConferenceViewModel.shared.isInConference.value == true) { + if (ConferenceViewModel.shared.conferenceExists.value == true) { self.conferenceGridView!.isHidden = conferenceMode != .Grid self.conferenceActiveSpeakerView!.isHidden = conferenceMode != .ActiveSpeaker self.conferenceActiveSpeakerView?.conferenceViewModel = ConferenceViewModel.shared @@ -146,7 +146,7 @@ class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // } } - ConferenceViewModel.shared.isInConference.readCurrentAndObserve { (isInConference) in + ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { (isInConference) in self.updateNavigation() } @@ -199,7 +199,7 @@ class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // boucingCounter.dataSource = CallsViewModel.shared.chatAndCallsCount view.addSubview(extraButtonsView) - extraButtonsView.matchParentSideBorders(insetedByDx: content_inset).alignParentBottom().done() + extraButtonsView.matchParentSideBorders(insetedByDx: content_inset).alignParentBottom(withMargin:SharedLayoutConstants.bottom_margin_notch_clearance).done() ControlsViewModel.shared.hideExtraButtons.readCurrentAndObserve { (_) in self.hideModalSubview(view: self.extraButtonsView) } @@ -215,7 +215,7 @@ class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // 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!!, onDismissAction: { + 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 }) @@ -227,7 +227,7 @@ class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // 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!!, onDismissAction: { + 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 }) @@ -296,6 +296,7 @@ class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // func updateNavigation() { if (Core.get().callsNb == 0) { PhoneMainView.instance().popView(self.compositeViewDescription()) + PhoneMainView.instance().mainViewController.removeCallFromCache() } else { if let data = CallsViewModel.shared.currentCallData.value { if (data?.isOutgoing.value == true || data?.isIncoming.value == true) { @@ -312,35 +313,12 @@ class ActiveCallOrConferenceView: UIViewController, UICompositeViewDelegate { // func goToChat() { let core = Core.get() guard - let localSipUri = core.defaultAccount?.params?.identityAddress?.asStringUriOnly(), - let remoteSipUri = ConferenceViewModel.shared.isInConference.value == true ? ConferenceViewModel.shared.conferenceAddress.value?.asStringUriOnly() : core.currentCall?.remoteAddress?.asStringUriOnly(), - let localAddress = try?Factory.Instance.createAddress(addr: localSipUri), - let remoteSipAddress = try?Factory.Instance.createAddress(addr: remoteSipUri), - let chatRoomParams = try?core.createDefaultChatRoomParams() + let chatRoom = CallsViewModel.shared.currentCallData.value??.chatRoom else { + Log.w("[Call] Failed to find existing chat room associated to call") return - } - - var chatRoom = core.searchChatRoom(params: nil, localAddr: localAddress, remoteAddr: remoteSipAddress, participants: []) - if (chatRoom == nil) { - chatRoom = core.searchChatRoom(params: nil, localAddr: localAddress, remoteAddr: nil, participants: [remoteSipAddress]) - } - if (chatRoom == nil) { - Log.w("[Call] Failed to find existing chat room for local address \(localSipUri) and remote address \(remoteSipUri)") - - // TODO: configure chat room params - if (ConferenceViewModel.shared.isInConference.value == true) { - // TODO: compute conference participants addresses list - } else { - chatRoom = try?core.createChatRoom(params: chatRoomParams, localAddr: localAddress, participants: [remoteSipAddress]) - } - } - - if (chatRoom != nil) { - PhoneMainView.instance().go(to: chatRoom?.getCobject) - } else { - Log.w("[Call] Failed to create chat room for local address \(localSipUri) and remote address \(remoteSipUri)") } + PhoneMainView.instance().go(to: chatRoom.getCobject) } diff --git a/Classes/Voip/Views/CompositeViewControllers/IncomingCallView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/IncomingCallView.swift similarity index 100% rename from Classes/Voip/Views/CompositeViewControllers/IncomingCallView.swift rename to Classes/Swift/Voip/Views/CompositeViewControllers/IncomingCallView.swift diff --git a/Classes/Voip/Views/CompositeViewControllers/OutgoingCallView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift similarity index 98% rename from Classes/Voip/Views/CompositeViewControllers/OutgoingCallView.swift rename to Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift index d2898a148..31286af27 100644 --- a/Classes/Voip/Views/CompositeViewControllers/OutgoingCallView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift @@ -61,7 +61,7 @@ import linphonesw 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!, onDismissAction: { + self.numpadView = NumpadView(superView: self.view,callData: self.callData!, marginTop: 0.0, onDismissAction: { self.numpadView?.removeFromSuperview() self.shadingMask.isHidden = true }) diff --git a/Classes/Voip/Views/Fragments/ActiveCall/ActiveCallView.swift b/Classes/Swift/Voip/Views/Fragments/ActiveCall/ActiveCallView.swift similarity index 92% rename from Classes/Voip/Views/Fragments/ActiveCall/ActiveCallView.swift rename to Classes/Swift/Voip/Views/Fragments/ActiveCall/ActiveCallView.swift index 2b8194b70..e08dc14b2 100644 --- a/Classes/Voip/Views/Fragments/ActiveCall/ActiveCallView.swift +++ b/Classes/Swift/Voip/Views/Fragments/ActiveCall/ActiveCallView.swift @@ -41,10 +41,13 @@ class ActiveCallView: UIView { // = currentCall 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] = [] @@ -77,9 +80,15 @@ class ActiveCallView: UIView { // = currentCall 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().nativePreviewWindow = localVideo + Core.get().nativePreviewWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(localVideo).toOpaque()) ControlsViewModel.shared.isVideoEnabled.readCurrentAndObserve{ (video) in self.remoteVideo.isHidden = video != true @@ -112,7 +121,6 @@ class ActiveCallView: UIView { // = currentCall displayNameDurationSipAddress.addSubview(sipAddress) sipAddress.matchParentSideBorders().alignUnder(view: displayNameTop,withMargin:sip_address_margin_top).done() - let upperSection = UIStackView() upperSection.distribution = .equalSpacing upperSection.alignment = .center upperSection.spacing = record_pause_button_margin @@ -145,10 +153,9 @@ class ActiveCallView: UIView { // = currentCall stack.addArrangedSubview(remotelyRecordedIndicator) - remotelyRecordedIndicator.matchParentSideBorders().alignUnder(view:upperSection, withMargin:ActiveCallView.remote_recording_margin_top).height(CGFloat(ActiveCallView.remote_recording_height)).done() + remotelyRecordedIndicator.matchParentSideBorders().height(CGFloat(ActiveCallView.remote_recording_height)).done() // Center Section : Avatar + video + record/pause buttons + videos - let centerSection = UIView() centerSection.layer.cornerRadius = ActiveCallView.center_view_corner_radius centerSection.clipsToBounds = true centerSection.backgroundColor = VoipTheme.voipParticipantBackgroundColor.get() @@ -203,8 +210,8 @@ class ActiveCallView: UIView { // = currentCall } else { self.remoteVideo.removeFromSuperview() self.localVideo.removeFromSuperview() - centerSection.addSubview(self.remoteVideo) - centerSection.addSubview(self.localVideo) + 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() @@ -217,7 +224,6 @@ class ActiveCallView: UIView { // = currentCall displayNameBottom.alignParentLeft(withMargin:ActiveCallView.bottom_displayname_margin_left).alignParentRight().alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done() stack.addArrangedSubview(centerSection) - centerSection.matchParentSideBorders().alignUnder(view:upperSection,withMargin: ActiveCallView.center_view_margin_top).alignParentBottom().done() addSubview(stack) stack.matchParentDimmensions().done() diff --git a/Classes/Voip/Views/Fragments/AudioRoutesView.swift b/Classes/Swift/Voip/Views/Fragments/AudioRoutesView.swift similarity index 100% rename from Classes/Voip/Views/Fragments/AudioRoutesView.swift rename to Classes/Swift/Voip/Views/Fragments/AudioRoutesView.swift diff --git a/Classes/Voip/Views/Fragments/CallStatsView.swift b/Classes/Swift/Voip/Views/Fragments/CallStatsView.swift similarity index 89% rename from Classes/Voip/Views/Fragments/CallStatsView.swift rename to Classes/Swift/Voip/Views/Fragments/CallStatsView.swift index 142cc03d6..27214817d 100644 --- a/Classes/Voip/Views/Fragments/CallStatsView.swift +++ b/Classes/Swift/Voip/Views/Fragments/CallStatsView.swift @@ -26,21 +26,16 @@ import linphonesw let side_margins = 10.0 let margin_top = 50 let corner_radius = 20.0 - let view_height = 600 let audio_video_margin = 20 - init(superView:UIView, callData:CallData, onDismissAction : @escaping ()->Void) { + 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) - snp.makeConstraints { make in - make.left.equalToSuperview().offset(side_margins) - make.right.equalToSuperview().offset(-side_margins) - make.height.equalTo(view_height) - make.bottom.equalToSuperview().offset(-side_margins) - } + matchParentSideBorders(insetedByDx: side_margins).alignParentTop(withMargin: marginTop).alignParentBottom().done() + callData.callState.observe { state in if (state == Call.State.End) { onDismissAction() diff --git a/Classes/Voip/Views/Fragments/CallsList/CallsListView.swift b/Classes/Swift/Voip/Views/Fragments/CallsList/CallsListView.swift similarity index 79% rename from Classes/Voip/Views/Fragments/CallsList/CallsListView.swift rename to Classes/Swift/Voip/Views/Fragments/CallsList/CallsListView.swift index 6fad29f47..33fdcf922 100644 --- a/Classes/Voip/Views/Fragments/CallsList/CallsListView.swift +++ b/Classes/Swift/Voip/Views/Fragments/CallsList/CallsListView.swift @@ -33,6 +33,8 @@ import linphonesw var callsDataObserver : MutableLiveDataOnChangeClosure<[CallData]>? = nil + + init() { super.init(title: VoipTexts.call_action_calls_list) @@ -49,19 +51,22 @@ import linphonesw // Merge Calls let mergeIntoLocalConference = CallControlButton(width: buttons_size,height: buttons_size, buttonTheme: VoipTheme.call_merge, onClickAction: { self.removeFromSuperview() - CallsViewModel.shared.mergeCallsIntoLocalConference() + 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{ (callsData) in - if let callsData = callsData { - mergeIntoLocalConference.isEnabled = callsData.count >= 2 && Core.get().conference?.isIn != true - } else { - mergeIntoLocalConference.isEnabled = false - } + CallsViewModel.shared.callsData.readCurrentAndObserve { _ in self.callsListTableView.reloadData() + mergeIntoLocalConference.isEnabled = self.mergeToConferencePossible() + } + ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { _ in + mergeIntoLocalConference.isEnabled = self.mergeToConferencePossible() } @@ -86,6 +91,23 @@ import linphonesw 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) { diff --git a/Classes/Voip/Views/Fragments/CallsList/VoipCallCell.swift b/Classes/Swift/Voip/Views/Fragments/CallsList/VoipCallCell.swift similarity index 87% rename from Classes/Voip/Views/Fragments/CallsList/VoipCallCell.swift rename to Classes/Swift/Voip/Views/Fragments/CallsList/VoipCallCell.swift index 22c92def4..b2fc5ec5d 100644 --- a/Classes/Voip/Views/Fragments/CallsList/VoipCallCell.swift +++ b/Classes/Swift/Voip/Views/Fragments/CallsList/VoipCallCell.swift @@ -37,7 +37,6 @@ class VoipCallCell: UITableViewCell { 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 conferenceAvatar = UIImageView(image:UIImage(named:"voip_multiple_contacts_avatar")) let displayName = StyledLabel(VoipTheme.call_list_active_name_font) let sipAddress = StyledLabel(VoipTheme.call_list_active_sip_uri_font) var menuButton : CallControlButton? = nil @@ -53,16 +52,14 @@ class VoipCallCell: UITableViewCell { data.isPaused.value == true ? UIImage(named:"voip_call_header_paused") : UIImage(named:"voip_call_header_active") if (data.isInRemoteConference.value == true) { - avatar.isHidden = true - conferenceAvatar.isHidden = false 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!) - avatar.isHidden = false - conferenceAvatar.isHidden = true + sipAddress.text = data.call.remoteAddress?.asStringUriOnly() } - 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) @@ -80,17 +77,14 @@ class VoipCallCell: UITableViewCell { contentView.addSubview(avatar) avatar.size(w: VoipCallCell.avatar_size, h: VoipCallCell.avatar_size).centerY().alignParentLeft(withMargin: avatar_left_margin).done() - - contentView.addSubview(conferenceAvatar) - conferenceAvatar.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).toRightOf(conferenceAvatar,withLeftMargin:texts_left_margin).wrapContentY().centerY().done() + 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) diff --git a/Classes/Voip/Views/Fragments/CallsList/VoipCallContextMenu.swift b/Classes/Swift/Voip/Views/Fragments/CallsList/VoipCallContextMenu.swift similarity index 100% rename from Classes/Voip/Views/Fragments/CallsList/VoipCallContextMenu.swift rename to Classes/Swift/Voip/Views/Fragments/CallsList/VoipCallContextMenu.swift diff --git a/Classes/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift similarity index 100% rename from Classes/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift rename to Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift diff --git a/Classes/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift similarity index 99% rename from Classes/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift rename to Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift index e0bc2d937..a1683ee9e 100644 --- a/Classes/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift @@ -62,7 +62,7 @@ class VoipConferenceActiveSpeakerView: UIView, UICollectionViewDataSource, UICol model.conferenceParticipantDevices.readCurrentAndObserve { (_) in self.grid.reloadData() } - model.isConferencePaused.readCurrentAndObserve { (paused) in + model.isConferenceLocallyPaused.readCurrentAndObserve { (paused) in self.pauseCallButtons.forEach { $0.isSelected = paused == true } diff --git a/Classes/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift similarity index 100% rename from Classes/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift rename to Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift diff --git a/Classes/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift similarity index 99% rename from Classes/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift rename to Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift index 1b6509c03..b14fdb48d 100644 --- a/Classes/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift @@ -53,7 +53,7 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi model.conferenceParticipantDevices.readCurrentAndObserve { (_) in self.grid.reloadData() } - model.isConferencePaused.readCurrentAndObserve { (paused) in + model.isConferenceLocallyPaused.readCurrentAndObserve { (paused) in self.pauseCallButtons.forEach { $0.isSelected = paused == true } diff --git a/Classes/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift similarity index 100% rename from Classes/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift rename to Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift diff --git a/Classes/Voip/Views/Fragments/ControlsView.swift b/Classes/Swift/Voip/Views/Fragments/ControlsView.swift similarity index 100% rename from Classes/Voip/Views/Fragments/ControlsView.swift rename to Classes/Swift/Voip/Views/Fragments/ControlsView.swift diff --git a/Classes/Voip/Views/Fragments/DismissableView.swift b/Classes/Swift/Voip/Views/Fragments/DismissableView.swift similarity index 100% rename from Classes/Voip/Views/Fragments/DismissableView.swift rename to Classes/Swift/Voip/Views/Fragments/DismissableView.swift diff --git a/Classes/Voip/Views/Fragments/IncomingOuntgoingCommonView.swift b/Classes/Swift/Voip/Views/Fragments/IncomingOuntgoingCommonView.swift similarity index 100% rename from Classes/Voip/Views/Fragments/IncomingOuntgoingCommonView.swift rename to Classes/Swift/Voip/Views/Fragments/IncomingOuntgoingCommonView.swift diff --git a/Classes/Voip/Views/Fragments/LocalVideoView.swift b/Classes/Swift/Voip/Views/Fragments/LocalVideoView.swift similarity index 100% rename from Classes/Voip/Views/Fragments/LocalVideoView.swift rename to Classes/Swift/Voip/Views/Fragments/LocalVideoView.swift diff --git a/Classes/Voip/Views/Fragments/NumpadView.swift b/Classes/Swift/Voip/Views/Fragments/NumpadView.swift similarity index 91% rename from Classes/Voip/Views/Fragments/NumpadView.swift rename to Classes/Swift/Voip/Views/Fragments/NumpadView.swift index e949e82f8..d8f65364e 100644 --- a/Classes/Voip/Views/Fragments/NumpadView.swift +++ b/Classes/Swift/Voip/Views/Fragments/NumpadView.swift @@ -34,18 +34,14 @@ import linphonesw let side_padding = 50.0 - init(superView:UIView, callData:CallData, onDismissAction : @escaping ()->Void) { + 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) - snp.makeConstraints { make in - make.left.equalToSuperview().offset(side_margins) - make.right.equalToSuperview().offset(-side_margins) - make.height.equalTo(pad_height) - make.bottom.equalToSuperview().offset(-side_margins) - } + matchParentSideBorders(insetedByDx: side_margins).alignParentTop(withMargin: marginTop).alignParentBottom().done() + callData.callState.observe { state in if (state == Call.State.End) { onDismissAction() diff --git a/Classes/Voip/Views/Fragments/ParticipantsList/ParticipantsListView.swift b/Classes/Swift/Voip/Views/Fragments/ParticipantsList/ParticipantsListView.swift similarity index 81% rename from Classes/Voip/Views/Fragments/ParticipantsList/ParticipantsListView.swift rename to Classes/Swift/Voip/Views/Fragments/ParticipantsList/ParticipantsListView.swift index b81357bb7..509cb31cd 100644 --- a/Classes/Voip/Views/Fragments/ParticipantsList/ParticipantsListView.swift +++ b/Classes/Swift/Voip/Views/Fragments/ParticipantsList/ParticipantsListView.swift @@ -36,7 +36,7 @@ import linphonesw let edit = CallControlButton(buttonTheme: VoipTheme.voip_edit, onClickAction: { - // Todo (not implemented in Android yet as of 22.11.21) + self.gotoParticipantsListSelection() }) super.headerView.addSubview(edit) edit.centerY().done() @@ -94,4 +94,19 @@ import linphonesw 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/Voip/Views/Fragments/ParticipantsList/VoipParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/ParticipantsList/VoipParticipantCell.swift similarity index 100% rename from Classes/Voip/Views/Fragments/ParticipantsList/VoipParticipantCell.swift rename to Classes/Swift/Voip/Views/Fragments/ParticipantsList/VoipParticipantCell.swift diff --git a/Classes/Voip/Views/Fragments/PausedCallOrConferenceView.swift b/Classes/Swift/Voip/Views/Fragments/PausedCallOrConferenceView.swift similarity index 100% rename from Classes/Voip/Views/Fragments/PausedCallOrConferenceView.swift rename to Classes/Swift/Voip/Views/Fragments/PausedCallOrConferenceView.swift diff --git a/Classes/Voip/Views/Fragments/RemotelyRecording.swift b/Classes/Swift/Voip/Views/Fragments/RemotelyRecording.swift similarity index 96% rename from Classes/Voip/Views/Fragments/RemotelyRecording.swift rename to Classes/Swift/Voip/Views/Fragments/RemotelyRecording.swift index a3e957865..abaaba42c 100644 --- a/Classes/Voip/Views/Fragments/RemotelyRecording.swift +++ b/Classes/Swift/Voip/Views/Fragments/RemotelyRecording.swift @@ -31,7 +31,7 @@ class RemotelyRecordingView: UIView { var isRemotelyRecorded: MutableLiveData? = nil { didSet { isRemotelyRecorded?.readCurrentAndObserve(onChange: { (isRemotelyRecording) in - self.isHidden = !(isRemotelyRecording == true) + self.isHidden = isRemotelyRecording != true }) } } diff --git a/Classes/Voip/Views/Fragments/VoipExtraButtonsView.swift b/Classes/Swift/Voip/Views/Fragments/VoipExtraButtonsView.swift similarity index 98% rename from Classes/Voip/Views/Fragments/VoipExtraButtonsView.swift rename to Classes/Swift/Voip/Views/Fragments/VoipExtraButtonsView.swift index 2a7deca61..6c180a3ca 100644 --- a/Classes/Voip/Views/Fragments/VoipExtraButtonsView.swift +++ b/Classes/Swift/Voip/Views/Fragments/VoipExtraButtonsView.swift @@ -114,7 +114,7 @@ class VoipExtraButtonsView: UIStackView { addArrangedSubview(row2) row2.matchParentSideBorders().done() - ConferenceViewModel.shared.isInConference.readCurrentAndObserve { (isIn) in + ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { (isIn) in participants.isHidden = isIn != true layoutselect.isHidden = isIn != true transfer.isHidden = isIn == true diff --git a/Classes/Voip/Views/SharedLayoutConstants.swift b/Classes/Swift/Voip/Views/SharedLayoutConstants.swift similarity index 85% rename from Classes/Voip/Views/SharedLayoutConstants.swift rename to Classes/Swift/Voip/Views/SharedLayoutConstants.swift index 163d8ba07..b03960573 100644 --- a/Classes/Voip/Views/SharedLayoutConstants.swift +++ b/Classes/Swift/Voip/Views/SharedLayoutConstants.swift @@ -21,7 +21,8 @@ import Foundation class SharedLayoutConstants { - static let buttons_bottom_margin = 15 + 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/Voip/VoipDialog.swift b/Classes/Swift/Voip/VoipDialog.swift similarity index 98% rename from Classes/Voip/VoipDialog.swift rename to Classes/Swift/Voip/VoipDialog.swift index 8488e8764..7ece3a8d8 100644 --- a/Classes/Voip/VoipDialog.swift +++ b/Classes/Swift/Voip/VoipDialog.swift @@ -69,7 +69,7 @@ class VoipDialog : UIView{ b.layer.cornerRadius = button_radius b.clipsToBounds = true buttonsView.addArrangedSubview(b) - b.applyTitleStyle(VoipTheme.big_button) + b.applyTitleStyle(VoipTheme.form_button_bold) let action = $0.action b.onClick { self.removeFromSuperview() diff --git a/Classes/Voip/Widgets/Avatar.swift b/Classes/Swift/Voip/Widgets/Avatar.swift similarity index 85% rename from Classes/Voip/Widgets/Avatar.swift rename to Classes/Swift/Voip/Widgets/Avatar.swift index 930dd8731..78b6ddffb 100644 --- a/Classes/Voip/Widgets/Avatar.swift +++ b/Classes/Swift/Voip/Widgets/Avatar.swift @@ -42,8 +42,11 @@ class Avatar : UIImageView { } - func fillFromAddress(address:Address) { - if let image = address.contact()?.avatar() { + 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 { @@ -52,7 +55,7 @@ class Avatar : UIImageView { initialsLabel.isHidden = false } } - + } diff --git a/Classes/Voip/Widgets/BouncingCounter.swift b/Classes/Swift/Voip/Widgets/BouncingCounter.swift similarity index 100% rename from Classes/Voip/Widgets/BouncingCounter.swift rename to Classes/Swift/Voip/Widgets/BouncingCounter.swift diff --git a/Classes/Voip/Widgets/ButtonWithStateBackgrounds.swift b/Classes/Swift/Voip/Widgets/ButtonWithStateBackgrounds.swift similarity index 100% rename from Classes/Voip/Widgets/ButtonWithStateBackgrounds.swift rename to Classes/Swift/Voip/Widgets/ButtonWithStateBackgrounds.swift diff --git a/Classes/Voip/Widgets/CallControlButton.swift b/Classes/Swift/Voip/Widgets/CallControlButton.swift similarity index 88% rename from Classes/Voip/Widgets/CallControlButton.swift rename to Classes/Swift/Voip/Widgets/CallControlButton.swift index 03a8a4835..474926248 100644 --- a/Classes/Voip/Widgets/CallControlButton.swift +++ b/Classes/Swift/Voip/Widgets/CallControlButton.swift @@ -79,14 +79,7 @@ class CallControlButton : ButtonWithStateBackgrounds { } - 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)) - } - } - } + } diff --git a/Classes/Voip/Widgets/FormButton.swift b/Classes/Swift/Voip/Widgets/FormButton.swift similarity index 69% rename from Classes/Voip/Widgets/FormButton.swift rename to Classes/Swift/Voip/Widgets/FormButton.swift index 309334afc..618491852 100644 --- a/Classes/Voip/Widgets/FormButton.swift +++ b/Classes/Swift/Voip/Widgets/FormButton.swift @@ -38,13 +38,23 @@ class FormButton : ButtonWithStateBackgrounds { } } - init () { - super.init(backgroundStateColors: VoipTheme.primary_colors_background) + init (backgroundStateColors: [UInt: LightDarkColor], bold:Bool = true) { + super.init(backgroundStateColors: backgroundStateColors) layer.cornerRadius = button_radius clipsToBounds = true - applyTitleStyle(VoipTheme.big_button) + 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/Voip/Widgets/RotatingSpinner.swift b/Classes/Swift/Voip/Widgets/RotatingSpinner.swift similarity index 100% rename from Classes/Voip/Widgets/RotatingSpinner.swift rename to Classes/Swift/Voip/Widgets/RotatingSpinner.swift diff --git a/Classes/Voip/Widgets/StyledCheckBox.swift b/Classes/Swift/Voip/Widgets/StyledCheckBox.swift similarity index 100% rename from Classes/Voip/Widgets/StyledCheckBox.swift rename to Classes/Swift/Voip/Widgets/StyledCheckBox.swift diff --git a/Classes/Voip/Widgets/StyledDatePicker.swift b/Classes/Swift/Voip/Widgets/StyledDatePicker.swift similarity index 87% rename from Classes/Voip/Widgets/StyledDatePicker.swift rename to Classes/Swift/Voip/Widgets/StyledDatePicker.swift index fc51cfd3b..cd2aedb8d 100644 --- a/Classes/Voip/Widgets/StyledDatePicker.swift +++ b/Classes/Swift/Voip/Widgets/StyledDatePicker.swift @@ -26,8 +26,17 @@ class StyledDatePicker: UIView { let chevron_margin = 10 let form_input_height = 38.0 - - var liveValue:MutableLiveData? + 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 @@ -35,12 +44,10 @@ class StyledDatePicker: UIView { super.init(coder: coder) } - init (liveValue:MutableLiveData, pickerMode:UIDatePicker.Mode, readOnly:Bool = false) { + init (liveValue:MutableLiveData? = nil, pickerMode:UIDatePicker.Mode, readOnly:Bool = false) { super.init(frame: .zero) - self.liveValue = liveValue self.pickerMode = pickerMode - let datePicker = UIDatePicker() addSubview(datePicker) datePicker.datePickerMode = pickerMode datePicker.addTarget(self, action: #selector(valueChanged), for: .valueChanged) @@ -58,15 +65,13 @@ class StyledDatePicker: UIView { setFormInputBackground(readOnly:readOnly) height(form_input_height).done() - - datePicker.date = liveValue.value! - self.valueChanged(datePicker: datePicker) - + if (readOnly) { formattedLabel.textColor = formattedLabel.textColor.withAlphaComponent(0.5) } isUserInteractionEnabled = !readOnly - + self.liveValue = liveValue + } diff --git a/Classes/Voip/Widgets/StyledLabel.swift b/Classes/Swift/Voip/Widgets/StyledLabel.swift similarity index 100% rename from Classes/Voip/Widgets/StyledLabel.swift rename to Classes/Swift/Voip/Widgets/StyledLabel.swift diff --git a/Classes/Voip/Widgets/StyledSwitch.swift b/Classes/Swift/Voip/Widgets/StyledSwitch.swift similarity index 100% rename from Classes/Voip/Widgets/StyledSwitch.swift rename to Classes/Swift/Voip/Widgets/StyledSwitch.swift diff --git a/Classes/Voip/Widgets/StyledTextView.swift b/Classes/Swift/Voip/Widgets/StyledTextView.swift similarity index 100% rename from Classes/Voip/Widgets/StyledTextView.swift rename to Classes/Swift/Voip/Widgets/StyledTextView.swift diff --git a/Classes/Voip/Widgets/StyledValuePicker.swift b/Classes/Swift/Voip/Widgets/StyledValuePicker.swift similarity index 100% rename from Classes/Voip/Widgets/StyledValuePicker.swift rename to Classes/Swift/Voip/Widgets/StyledValuePicker.swift diff --git a/Classes/Voip/Widgets/UICallTimer.swift b/Classes/Swift/Voip/Widgets/UICallTimer.swift similarity index 100% rename from Classes/Voip/Widgets/UICallTimer.swift rename to Classes/Swift/Voip/Widgets/UICallTimer.swift diff --git a/Classes/Voip/Widgets/VoipExtraButton.swift b/Classes/Swift/Voip/Widgets/VoipExtraButton.swift similarity index 100% rename from Classes/Voip/Widgets/VoipExtraButton.swift rename to Classes/Swift/Voip/Widgets/VoipExtraButton.swift 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/linphone-Bridging-Header.h b/Classes/linphone-Bridging-Header.h index c09603899..ba7081a2a 100644 --- a/Classes/linphone-Bridging-Header.h +++ b/Classes/linphone-Bridging-Header.h @@ -12,3 +12,6 @@ #import "StatusBarView.h" #import "LinphoneUI/UIBouncingView.h" #import "PhoneMainView.h" +#import "UICamSwitch.h" +#import "UIChatBubbleTextCell.h" + diff --git a/Resources/images/conference_schedule_calendar_default.png b/Resources/images/conference_schedule_calendar_default.png new file mode 100644 index 0000000000000000000000000000000000000000..59fe950fb19eb989b09c5e74e85847332a92a418 GIT binary patch literal 182 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8t*47)h{y5dgav{H2`kv0*t@PW zi2h+eRMmAg;IKkrTgieabsHA_O*sCziNo}zLu|%@mWQ$tvqFCGtq^9FQ9LanVAGU! zfq%t*&t?T%`vBgi8Gc#^7G>X%c9D}2V(WGLzrt}bTd$PK2e!bqaf_pTy|?a?zb2)~ fo0Qobz{6m_X!0?!iA=0OhcI}$`njxgN@xNAQlUMB literal 0 HcmV?d00001 diff --git a/Resources/images/conference_schedule_participants_default.png b/Resources/images/conference_schedule_participants_default.png new file mode 100644 index 0000000000000000000000000000000000000000..a58af55e1403888e283eb14c024bfa103f2508ce GIT binary patch literal 660 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBSkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6Xz}5igjtE6@fg!ItFh?gFHN;HUHMdLYGF z;1O92q!YmSL$=d-1_s7yo-U3d5r^MiJD(-uD8Tk1KFNo#Nnq~}c1M&_xp`y8wIy5+R+jIfI%s+_TJTBy+dHNa5eMq zyFyL#QaPN95y%^lx}#vD$a z)3-PON5l3C`|OX)H?+C`Ncs{`&v$=o$6Tp%j~Ms$DJI>xb@=atRYzA$yL-s_W$}c^ z0aDMdSEz68y0U9^#8Kbl`MH5xt#9N7&6IwTnqS#+d`It7!$;>^-cRn3{qUt=Z|4GW z$&+GXvnH^FE!JY*_uSWmPwB_x``-6ixPwmrIvCseQ;=l|_xl2_rn(=d+1!WcylUGu zKlIgO5tf6hbJm=;?OPDJ(CGUT`wZQvEkE9W5MH1-`74L^ni61Csg}4#l%ynFVdQ&MBb@0JL8ZNdN!< literal 0 HcmV?d00001 diff --git a/Resources/images/conference_schedule_time_default.png b/Resources/images/conference_schedule_time_default.png new file mode 100644 index 0000000000000000000000000000000000000000..7a953461708206779772642d22c110e5c325840d GIT binary patch literal 442 zcmV;r0Y(0aP)h#V$p#kW?YTM-a>41Go<$_Cc|5NkGLa z7HK5Eg_Fo$X5EFY^h|g5%r|qcv+xh4K#d+Ak9;uW0gnr+tYw1~IpYn*tQSsLPm4Lk zkA*DPGtD#(ov$|d3~5lt#idMx;p%_|-DD>VTpQCVEu6@AhZa*~ag~Hx-e!brwQ(64 ztFF!DfCWPIDg5Go-#FNe&y;U&6U6M&;V6O{nXnh5pi_jHPzvcg2rpt?VuBDS(d)h< zL_4x29t3SuSD`S}Ov0ViAjgEF8dUcXVi2>z27&=X^nywcA?lexb%eMLD#r*>$poq( z#5ky&Ar#42GN^?IYXy+y)K-kZZLEn;uluxunZt$mEdPo#t~lG|X*an$BG{BwJb ka(p0jeX?`>iTD%p1#$}@xR9YyEdT%j07*qoM6N<$f_fpoJ^%m! literal 0 HcmV?d00001 diff --git a/Resources/images/voip_call_add.png b/Resources/images/voip_call_add.png index 8237f3305006cb821191df94a5a40462c0a54aaf..f4d80e5f5f340e9585741cd6316fdb9ec3cd2415 100644 GIT binary patch delta 18132 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 delta 10961 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 diff --git a/Resources/images/voip_call_forward.png b/Resources/images/voip_call_forward.png index cc61de4823e8400be56bd628e971295c8354456e..46a31902886b29f8722a50f416340984cc00bad2 100644 GIT binary patch delta 18883 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 RkNW@s002ovPDHLkV1fu&Hqih8 delta 11404 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 VVh%yE|NT!uDafcu*GZZN{~v8r&*cCB diff --git a/Resources/images/voip_calls_list.png b/Resources/images/voip_calls_list.png index a3d69b84eed51b5a68c5e211ab2d0251c220d932..a832384ca37d704f41a8665dd22d7028644cc4e6 100644 GIT binary patch delta 19951 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 tERKz19xd{&Q~6nwz9%0EdxULs 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 literal 0 HcmV?d00001 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.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj index afad71813..031fbca04 100644 --- a/linphone.xcodeproj/project.pbxproj +++ b/linphone.xcodeproj/project.pbxproj @@ -51,7 +51,6 @@ 24E1C7C01F9A235600D3F981 /* Contacts.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24E1C7B91F9A235500D3F981 /* Contacts.framework */; }; 288765FD0DF74451002DB57D /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 288765FC0DF74451002DB57D /* CoreGraphics.framework */; }; 340751971506459A00B89C47 /* CoreTelephony.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 340751961506459A00B89C47 /* CoreTelephony.framework */; }; - 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 */; }; @@ -610,7 +609,7 @@ 63E27A321C4FECD000D332AE /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 63E27A311C4FECD000D332AE /* LaunchScreen.xib */; }; 63E27A521C50EDB000D332AE /* hold.mkv in Resources */ = {isa = PBXBuildFile; fileRef = 63E27A511C50EB2700D332AE /* hold.mkv */; }; 63E59A3F1ADE70D900646FB3 /* InAppProductsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 63E59A3E1ADE70D900646FB3 /* InAppProductsManager.m */; }; - 63E802DB1C625AEF000D5509 /* (null) in Resources */ = {isa = PBXBuildFile; }; + 63E802DB1C625AEF000D5509 /* BuildFile 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 */; }; 63FB30351A680E73008CA393 /* UIRoundedImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 63FB30341A680E73008CA393 /* UIRoundedImageView.m */; }; @@ -656,6 +655,7 @@ 8CF25D9E1F9F76BD00BEA0C1 /* chat_group_informations@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8CF25D9C1F9F76BD00BEA0C1 /* chat_group_informations@2x.png */; }; 9C0B30F54D61774AFD1473CE /* Pods_linphone.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B464E44A606CB50A65A96FE2 /* Pods_linphone.framework */; }; C60B66682721AFFA0026AC7D /* CallStatisticsData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60B66672721AFFA0026AC7D /* CallStatisticsData.swift */; }; + C60C9F37278C3D36009A8F5B /* Pair.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60C9F36278C3D36009A8F5B /* Pair.swift */; }; C60D265627299C94006238BB /* ControlsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60D265527299C94006238BB /* ControlsViewModel.swift */; }; C60D265827299F70006238BB /* CoreExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60D265727299F6F006238BB /* CoreExtensions.swift */; }; C60D265C272AA0BD006238BB /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60D265B272AA0BD006238BB /* UIImageExtensions.swift */; }; @@ -771,7 +771,7 @@ C683B20E2722702300D4E15C /* VoipTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = C683B20D2722702300D4E15C /* VoipTheme.swift */; }; C683B213272276CF00D4E15C /* UIColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C683B212272276CF00D4E15C /* UIColorExtensions.swift */; }; C690CCB1275764CD00609077 /* ConferenceSchedulingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C690CCB0275764CD00609077 /* ConferenceSchedulingView.swift */; }; - C690CCB42757683800609077 /* NavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C690CCB32757683800609077 /* NavigationView.swift */; }; + C690CCB42757683800609077 /* BackNextNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C690CCB32757683800609077 /* BackNextNavigationView.swift */; }; C6A1BB3526E8815400540D50 /* menu_info.png in Resources */ = {isa = PBXBuildFile; fileRef = C6A1BB3126E8815300540D50 /* menu_info.png */; }; C6A1BB3626E8815400540D50 /* menu_forward_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C6A1BB3226E8815400540D50 /* menu_forward_default.png */; }; C6A1BB3726E8815400540D50 /* menu_copy_text_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C6A1BB3326E8815400540D50 /* menu_copy_text_default.png */; }; @@ -781,6 +781,16 @@ C6A1BB4126E889AD00540D50 /* forward_message_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C6A1BB4026E889AD00540D50 /* forward_message_default.png */; }; C6A1BB4326E88F7C00540D50 /* menu_resend_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C6A1BB4226E88F7C00540D50 /* menu_resend_default.png */; }; C6A1BB4526E890BD00540D50 /* file_voice_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C6A1BB4426E890BD00540D50 /* file_voice_default.png */; }; + C6AF920E275D38090087ACDE /* ScheduledConferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6AF920D275D38090087ACDE /* ScheduledConferencesView.swift */; }; + C6AF9210275D4DD60087ACDE /* ScheduledConferencesCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6AF920F275D4DD60087ACDE /* ScheduledConferencesCell.swift */; }; + C6AF9212275D61420087ACDE /* conference_schedule_participants_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C6AF9211275D613E0087ACDE /* conference_schedule_participants_default.png */; }; + C6AF9214275D67EB0087ACDE /* conference_schedule_time_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C6AF9213275D67EB0087ACDE /* conference_schedule_time_default.png */; }; + C6AF9218275E13790087ACDE /* ScheduledConferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6AF9217275E13790087ACDE /* ScheduledConferencesViewModel.swift */; }; + C6AF921A275E2E010087ACDE /* ConferenceWaitingRoomFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6AF9219275E2E010087ACDE /* ConferenceWaitingRoomFragment.swift */; }; + C6AF921C275E4AF50087ACDE /* ICSBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6AF921B275E4AF50087ACDE /* ICSBubbleView.swift */; }; + C6AF921E275E51860087ACDE /* conference_schedule_calendar_default.png in Resources */ = {isa = PBXBuildFile; fileRef = C6AF921D275E51860087ACDE /* conference_schedule_calendar_default.png */; }; + C6AF9226275F3D890087ACDE /* voip_conference_new_selected.png in Resources */ = {isa = PBXBuildFile; fileRef = C6AF9225275F3D890087ACDE /* voip_conference_new_selected.png */; }; + C6AF922A275F6BA10087ACDE /* ConferenceHistoryDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6AF9229275F6BA10087ACDE /* ConferenceHistoryDetailsView.swift */; }; C6B04D61274B954500F70559 /* ParticipantsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B04D60274B954500F70559 /* ParticipantsListView.swift */; }; C6B04D63274B95D500F70559 /* VoipParticipantCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B04D62274B95D400F70559 /* VoipParticipantCell.swift */; }; C6B04D67274BD61300F70559 /* VoipConferenceActiveSpeakerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B04D66274BD61200F70559 /* VoipConferenceActiveSpeakerView.swift */; }; @@ -1051,8 +1061,6 @@ 32CA4F630368D1EE00C91783 /* linphone_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = linphone_Prefix.pch; sourceTree = ""; }; 340751961506459A00B89C47 /* CoreTelephony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; }; 3411568BE5527EB500F75EBB /* Pods-msgNotificationService.distributionadhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-msgNotificationService.distributionadhoc.xcconfig"; path = "Target Support Files/Pods-msgNotificationService/Pods-msgNotificationService.distributionadhoc.xcconfig"; 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; }; 507103607396F28FF4427108 /* 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 = ""; }; @@ -1815,6 +1823,7 @@ B9F41097CE0124A05554DB9C /* Pods-msgNotificationService.distribution.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-msgNotificationService.distribution.xcconfig"; path = "Target Support Files/Pods-msgNotificationService/Pods-msgNotificationService.distribution.xcconfig"; sourceTree = ""; }; C589627B9D9D2A4F9C816051 /* 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 = ""; }; C60B66672721AFFA0026AC7D /* CallStatisticsData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallStatisticsData.swift; sourceTree = ""; }; + C60C9F36278C3D36009A8F5B /* Pair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pair.swift; sourceTree = ""; }; C60D265527299C94006238BB /* ControlsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControlsViewModel.swift; sourceTree = ""; }; C60D265727299F6F006238BB /* CoreExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreExtensions.swift; sourceTree = ""; }; C60D265B272AA0BD006238BB /* UIImageExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtensions.swift; sourceTree = ""; }; @@ -1931,7 +1940,7 @@ C683B20D2722702300D4E15C /* VoipTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoipTheme.swift; sourceTree = ""; }; C683B212272276CF00D4E15C /* UIColorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColorExtensions.swift; sourceTree = ""; }; C690CCB0275764CD00609077 /* ConferenceSchedulingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceSchedulingView.swift; sourceTree = ""; }; - C690CCB32757683800609077 /* NavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationView.swift; sourceTree = ""; }; + C690CCB32757683800609077 /* BackNextNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackNextNavigationView.swift; sourceTree = ""; }; C6A1BB3126E8815300540D50 /* menu_info.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_info.png; sourceTree = ""; }; C6A1BB3226E8815400540D50 /* menu_forward_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_forward_default.png; sourceTree = ""; }; C6A1BB3326E8815400540D50 /* menu_copy_text_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_copy_text_default.png; sourceTree = ""; }; @@ -1942,6 +1951,16 @@ C6A1BB4026E889AD00540D50 /* forward_message_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = forward_message_default.png; sourceTree = ""; }; C6A1BB4226E88F7C00540D50 /* menu_resend_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = menu_resend_default.png; sourceTree = ""; }; C6A1BB4426E890BD00540D50 /* file_voice_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = file_voice_default.png; sourceTree = ""; }; + C6AF920D275D38090087ACDE /* ScheduledConferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduledConferencesView.swift; sourceTree = ""; }; + C6AF920F275D4DD60087ACDE /* ScheduledConferencesCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduledConferencesCell.swift; sourceTree = ""; }; + C6AF9211275D613E0087ACDE /* conference_schedule_participants_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = conference_schedule_participants_default.png; sourceTree = ""; }; + C6AF9213275D67EB0087ACDE /* conference_schedule_time_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = conference_schedule_time_default.png; sourceTree = ""; }; + C6AF9217275E13790087ACDE /* ScheduledConferencesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduledConferencesViewModel.swift; sourceTree = ""; }; + C6AF9219275E2E010087ACDE /* ConferenceWaitingRoomFragment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceWaitingRoomFragment.swift; sourceTree = ""; }; + C6AF921B275E4AF50087ACDE /* ICSBubbleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICSBubbleView.swift; sourceTree = ""; }; + C6AF921D275E51860087ACDE /* conference_schedule_calendar_default.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = conference_schedule_calendar_default.png; sourceTree = ""; }; + C6AF9225275F3D890087ACDE /* voip_conference_new_selected.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = voip_conference_new_selected.png; sourceTree = ""; }; + C6AF9229275F6BA10087ACDE /* ConferenceHistoryDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ConferenceHistoryDetailsView.swift; path = Classes/Swift/Conference/views/ConferenceHistoryDetailsView.swift; sourceTree = SOURCE_ROOT; }; C6B04D60274B954500F70559 /* ParticipantsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParticipantsListView.swift; sourceTree = ""; }; C6B04D62274B95D400F70559 /* VoipParticipantCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipParticipantCell.swift; sourceTree = ""; }; C6B04D66274BD61200F70559 /* VoipConferenceActiveSpeakerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipConferenceActiveSpeakerView.swift; sourceTree = ""; }; @@ -2262,9 +2281,8 @@ 080E96DDFE201D6D7F000001 /* Classes */ = { isa = PBXGroup; children = ( - C6EA2F492752237C008E60F8 /* Conference */, - C65A5D41272184CE005BA038 /* SwiftUtil */, - C65A5D2D2721683B005BA038 /* Voip */, + 614C087623D1A35E00217F80 /* linphone-Bridging-Header.h */, + C6649AAF275D28F400615896 /* Swift */, 22E0A81D111C44E100B04932 /* AboutView.h */, 22E0A81C111C44E100B04932 /* AboutView.m */, 636316D31A1DEBCB0009B839 /* AboutView.xib */, @@ -2380,14 +2398,6 @@ D3ED3E851586291B006C0DE4 /* TabBarView.m */, D38187FB15FE355D00C3EDCA /* TabBarView.xib */, D326483415887D4400930C67 /* Utils */, - 34216F3E1547EBCD00EA9777 /* VideoZoomHandler.h */, - 34216F3F1547EBCD00EA9777 /* VideoZoomHandler.m */, - C6DA657B261C950C0020CB43 /* VFSUtil.swift */, - 614C087723D1A35F00217F80 /* ProviderDelegate.swift */, - 614C087923D1A37400217F80 /* CallManager.swift */, - 6134812C2406CECC00695B41 /* ConfigManager.swift */, - 6134812E2407B35200695B41 /* AppManager.swift */, - 614C087623D1A35E00217F80 /* linphone-Bridging-Header.h */, ); path = Classes; sourceTree = ""; @@ -2514,7 +2524,7 @@ path = LinphoneUI; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA /* CustomTemplate */ = { + 29B97314FDCFA39411CA2CEA = { isa = PBXGroup; children = ( 8C23BCB71D82AAC3005F19BB /* linphone.entitlements */, @@ -2654,6 +2664,10 @@ 633FEBE11D3CD5570014B822 /* images */ = { isa = PBXGroup; children = ( + C6AF9225275F3D890087ACDE /* voip_conference_new_selected.png */, + C6AF921D275E51860087ACDE /* conference_schedule_calendar_default.png */, + C6AF9213275D67EB0087ACDE /* conference_schedule_time_default.png */, + C6AF9211275D613E0087ACDE /* conference_schedule_participants_default.png */, C6D1E42327595988008EB388 /* security_toggle_icon_green.png */, C6D1E42227595987008EB388 /* security_toggle_icon_grey.png */, C6D1E41D2759396B008EB388 /* voip_checkbox_checked.png */, @@ -3359,21 +3373,37 @@ C65A5D3427216CAB005BA038 /* ViewModel */ = { isa = PBXGroup; children = ( - C65A5D3727216CC0005BA038 /* MutableLiveData.swift */, C6EA2F6C2754E3DC008E60F8 /* MediatorLiveData.swift */, ); path = ViewModel; sourceTree = ""; }; - C65A5D41272184CE005BA038 /* SwiftUtil */ = { + C65A5D41272184CE005BA038 /* Util */ = { isa = PBXGroup; children = ( - C690CCB22757674700609077 /* GenericViews */, + C690CCB32757683800609077 /* BackNextNavigationView.swift */, C6EA2F5E2754CFB0008E60F8 /* TimestampUtils.swift */, - C683B211272276C100D4E15C /* Extensions */, + C65A5D3727216CC0005BA038 /* MutableLiveData.swift */, + C60C9F36278C3D36009A8F5B /* Pair.swift */, C65A5D3427216CAB005BA038 /* ViewModel */, ); - path = SwiftUtil; + path = Util; + sourceTree = ""; + }; + C6649AAF275D28F400615896 /* Swift */ = { + isa = PBXGroup; + children = ( + C6DA657B261C950C0020CB43 /* VFSUtil.swift */, + 614C087723D1A35F00217F80 /* ProviderDelegate.swift */, + 614C087923D1A37400217F80 /* CallManager.swift */, + 6134812C2406CECC00695B41 /* ConfigManager.swift */, + 6134812E2407B35200695B41 /* AppManager.swift */, + C6EA2F492752237C008E60F8 /* Conference */, + C683B211272276C100D4E15C /* Extensions */, + C65A5D41272184CE005BA038 /* Util */, + C65A5D2D2721683B005BA038 /* Voip */, + ); + path = Swift; sourceTree = ""; }; C6710F4D2722900A00ED888F /* Widgets */ = { @@ -3507,14 +3537,6 @@ path = Extensions; sourceTree = ""; }; - C690CCB22757674700609077 /* GenericViews */ = { - isa = PBXGroup; - children = ( - C690CCB32757683800609077 /* NavigationView.swift */, - ); - path = GenericViews; - sourceTree = ""; - }; C6D52B4727481D3F00904660 /* Conference */ = { isa = PBXGroup; children = ( @@ -3532,7 +3554,7 @@ children = ( C6EA2F4C2754C691008E60F8 /* data */, C6EA2F4B2754C683008E60F8 /* views */, - C6EA2F4D2754C69F008E60F8 /* viewmodels */, + C6EA2F4D2754C69F008E60F8 /* models */, ); path = Conference; sourceTree = ""; @@ -3542,6 +3564,11 @@ children = ( C690CCB0275764CD00609077 /* ConferenceSchedulingView.swift */, C61E409A275A20A300CCE602 /* ConferenceSchedulingSummaryView.swift */, + C6AF920D275D38090087ACDE /* ScheduledConferencesView.swift */, + C6AF920F275D4DD60087ACDE /* ScheduledConferencesCell.swift */, + C6AF921B275E4AF50087ACDE /* ICSBubbleView.swift */, + C6AF9229275F6BA10087ACDE /* ConferenceHistoryDetailsView.swift */, + C6AF9219275E2E010087ACDE /* ConferenceWaitingRoomFragment.swift */, ); path = views; sourceTree = ""; @@ -3556,12 +3583,13 @@ path = data; sourceTree = ""; }; - C6EA2F4D2754C69F008E60F8 /* viewmodels */ = { + C6EA2F4D2754C69F008E60F8 /* models */ = { isa = PBXGroup; children = ( C6EA2F662754DC45008E60F8 /* ConferenceSchedulingViewModel.swift */, + C6AF9217275E13790087ACDE /* ScheduledConferencesViewModel.swift */, ); - path = viewmodels; + path = models; sourceTree = ""; }; D326483415887D4400930C67 /* Utils */ = { @@ -3905,7 +3933,7 @@ fr, hu, ); - mainGroup = 29B97314FDCFA39411CA2CEA /* CustomTemplate */; + mainGroup = 29B97314FDCFA39411CA2CEA; productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; projectRoot = ""; @@ -3971,7 +3999,7 @@ 633FEEE01D3CD55A0014B822 /* numpad_8_over~ipad@2x.png in Resources */, C6710FA42722B20000ED888F /* voip_single_contact_avatar.png in Resources */, 633FEDDC1D3CD5590014B822 /* call_start_body_disabled~ipad.png in Resources */, - 63E802DB1C625AEF000D5509 /* (null) in Resources */, + 63E802DB1C625AEF000D5509 /* BuildFile in Resources */, 633FEE2E1D3CD5590014B822 /* color_F.png in Resources */, C6710FA72722B20000ED888F /* voip_call_more.png in Resources */, 633FEDC51D3CD5590014B822 /* call_hangup_disabled@2x.png in Resources */, @@ -3999,6 +4027,7 @@ 633FEE101D3CD5590014B822 /* chat_list_indicator~ipad.png in Resources */, 633FEF331D3CD55A0014B822 /* route_speaker_selected@2x.png in Resources */, 633FEE6C1D3CD5590014B822 /* footer_dialer_disabled.png in Resources */, + C6AF921E275E51860087ACDE /* conference_schedule_calendar_default.png in Resources */, 633FEF231D3CD55A0014B822 /* route_bluetooth_default@2x.png in Resources */, C6A1BB4526E890BD00540D50 /* file_voice_default.png in Resources */, 633FED9C1D3CD5590014B822 /* add_field_default.png in Resources */, @@ -4053,6 +4082,7 @@ 633FEE491D3CD5590014B822 /* delete_default@2x.png in Resources */, 633FEF291D3CD55A0014B822 /* route_earpiece_default@2x.png in Resources */, C6710FAF2722B20000ED888F /* voip_conference_new.png in Resources */, + C6AF9226275F3D890087ACDE /* voip_conference_new_selected.png in Resources */, 633FEE271D3CD5590014B822 /* checkbox_checked@2x.png in Resources */, 61586B85217A17070038AC45 /* menu_assistant.png in Resources */, 633FEDCC1D3CD5590014B822 /* call_quality_indicator_0.png in Resources */, @@ -4146,6 +4176,7 @@ 615A2821217F6FBF0060F920 /* security_alert_indicator@2x.png in Resources */, 633FEE1E1D3CD5590014B822 /* chat_start_body_disabled.png in Resources */, 639CEB001A1DF4E4004DE38F /* UIHistoryCell.xib in Resources */, + C6AF9214275D67EB0087ACDE /* conference_schedule_time_default.png in Resources */, 633FEE841D3CD5590014B822 /* led_error.png in Resources */, 633FEDEA1D3CD5590014B822 /* call_status_outgoing.png in Resources */, 633FEF511D3CD55A0014B822 /* valid_disabled@2x.png in Resources */, @@ -4171,6 +4202,7 @@ 633FEE1B1D3CD5590014B822 /* chat_start_body_default@2x.png in Resources */, C6710FB22722B20000ED888F /* voip_calls_list.png in Resources */, 633FEE021D3CD5590014B822 /* cancel_edit_default.png in Resources */, + C6AF9212275D61420087ACDE /* conference_schedule_participants_default.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 */, @@ -4772,22 +4804,22 @@ "${BUILT_PRODUCTS_DIR}/IQKeyboardManager/IQKeyboardManager.framework", "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework", "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/bctoolbox-ios.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/bctoolbox.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/belcard.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/belle-sip.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/belr.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/lime.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/linphone.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/linphonetester.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/mediastreamer2.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/msamr.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/mscodec2.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/msopenh264.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/mssilk.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/mswebrtc.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/msx264.framework", - "${PODS_ROOT}/../../../../../../../Volumes/dada/bc/linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/ortp.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/bctoolbox-ios.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/bctoolbox.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/belcard.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/belle-sip.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/belr.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/lime.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/linphone.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/linphonetester.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/mediastreamer2.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/msamr.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/mscodec2.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/msopenh264.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/mssilk.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/mswebrtc.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/msx264.framework", + "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/ortp.framework", "${BUILT_PRODUCTS_DIR}/linphone-sdk/linphonesw.framework", ); name = "[CP] Embed Pods Frameworks"; @@ -4876,7 +4908,6 @@ 636BC9971B5F921B00C754CE /* UIIconButton.m in Sources */, 63423C0A1C4501D000D9A050 /* Contact.m in Sources */, C6710FE12722F0E400ED888F /* Avatar.swift in Sources */, - 34216F401547EBCD00EA9777 /* VideoZoomHandler.m in Sources */, 614C087823D1A35F00217F80 /* ProviderDelegate.swift in Sources */, CF7602D7210867E800749F76 /* RecordingsListView.m in Sources */, D3F83F8E15822ABE00336684 /* PhoneMainView.m in Sources */, @@ -4933,12 +4964,14 @@ D3EA540D1598528B0037DC6B /* ChatsListTableView.m in Sources */, D3EA5411159853750037DC6B /* UIChatCell.m in Sources */, D31B4B21159876C0002E6C72 /* UICompositeView.m in Sources */, + C6AF921A275E2E010087ACDE /* ConferenceWaitingRoomFragment.swift in Sources */, 8C9C5E0D1F83B2EF006987FA /* ChatConversationCreateCollectionViewController.m in Sources */, 631098491D4660580041F2B3 /* CountryListView.m in Sources */, D32B9DFC15A2F131000B6DEC /* FastAddressBook.m in Sources */, C6DB1DE22757E35F00A22704 /* StyledTextView.swift in Sources */, D350F20E15A43BB100149E54 /* AssistantView.m in Sources */, D3F795D615A582810077328B /* ChatConversationView.m in Sources */, + C6AF921C275E4AF50087ACDE /* ICSBubbleView.swift in Sources */, C60D265627299C94006238BB /* ControlsViewModel.swift in Sources */, D32B6E2915A5BC440033019F /* ChatConversationTableView.m in Sources */, C6D09F4B27438707003C2173 /* CallsListView.swift in Sources */, @@ -4978,20 +5011,23 @@ C6586149273E595700A0DBFC /* VoipExtraButtonsView.swift in Sources */, 633E41821D74259000320475 /* AssistantLinkView.m in Sources */, C6710F5727229DEE00ED888F /* TextStyle.swift in Sources */, + C6AF9218275E13790087ACDE /* ScheduledConferencesViewModel.swift in Sources */, D3807FE815C2894A005BE9BC /* IASKAppSettingsViewController.m in Sources */, D3807FEC15C2894A005BE9BC /* IASKSpecifierValuesViewController.m in Sources */, 8CA70AE41F9E39E400A3D2EB /* UIChatConversationInfoTableViewCell.m in Sources */, - C690CCB42757683800609077 /* NavigationView.swift in Sources */, + C690CCB42757683800609077 /* BackNextNavigationView.swift in Sources */, D3807FEE15C2894A005BE9BC /* IASKSettingsReader.m in Sources */, C6C65E8B2727274A00E48FC6 /* NumpadView.swift in Sources */, C6F2D501273B0EFC0071BA52 /* VoipDialog.swift in Sources */, C6710F53272297C400ED888F /* VoipTexts.swift in Sources */, D3807FF015C2894A005BE9BC /* IASKSettingsStore.m in Sources */, C6710FEB2726874D00ED888F /* ButtonTheme.swift in Sources */, + C6AF9210275D4DD60087ACDE /* ScheduledConferencesCell.swift in Sources */, 8CA70AD11F9E0AE100A3D2EB /* ChatConversationInfoView.m in Sources */, D3807FF215C2894A005BE9BC /* IASKSettingsStoreFile.m in Sources */, D3807FF415C2894A005BE9BC /* IASKSettingsStoreUserDefaults.m in Sources */, C6710FDC2722C3BB00ED888F /* UIVIewExtensions.swift in Sources */, + C60C9F37278C3D36009A8F5B /* Pair.swift in Sources */, C6F2D4F32739475C0071BA52 /* ActiveCallView.swift in Sources */, 639E9C801C0DB13D00019A75 /* UICheckBoxTableView.m in Sources */, CF7602E72108759A00749F76 /* UIRecordingCell.m in Sources */, @@ -5000,6 +5036,7 @@ C6710FE92723DD7D00ED888F /* CallControlButton.swift in Sources */, D3807FF815C2894A005BE9BC /* IASKPSSliderSpecifierViewCell.m in Sources */, C6EA2F592754C91C008E60F8 /* ScheduledConferenceData.swift in Sources */, + C6AF922A275F6BA10087ACDE /* ConferenceHistoryDetailsView.swift in Sources */, C6EA2F672754DC45008E60F8 /* ConferenceSchedulingViewModel.swift in Sources */, D3807FFA15C2894A005BE9BC /* IASKPSTextFieldSpecifierViewCell.m in Sources */, D3807FFC15C2894A005BE9BC /* IASKPSTitleValueSpecifierViewCell.m in Sources */, @@ -5008,6 +5045,7 @@ C6C65E89272723DC00E48FC6 /* UIVIewControllerExtensions.swift in Sources */, D3807FFE15C2894A005BE9BC /* IASKSlider.m in Sources */, D380800015C2894A005BE9BC /* IASKSwitch.m in Sources */, + C6AF920E275D38090087ACDE /* ScheduledConferencesView.swift in Sources */, D380800215C2894A005BE9BC /* IASKTextField.m in Sources */, D380801315C299D0005BE9BC /* ColorSpaceUtilites.m in Sources */, C64A854E2667B67200252AD2 /* EphemeralSettingsView.m in Sources */, @@ -5604,7 +5642,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.36+bd3b432\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.94+f4cb5df\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5731,7 +5769,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.36+bd3b432\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.94+f4cb5df\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5857,7 +5895,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.36+bd3b432\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.94+f4cb5df\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5982,7 +6020,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.36+bd3b432\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.94+f4cb5df\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; From a2112ba9f09946b4d6aa561b21189c81a9a4fa39 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Tue, 1 Feb 2022 10:10:33 +0100 Subject: [PATCH 04/75] Bulk video conference adjustements --- Classes/ChatConversationView.m | 2 +- Classes/LinphoneManager.m | 2 +- Classes/LinphoneUI/UICamSwitch.m | 4 -- .../ConferenceSchedulingViewModel.swift | 2 +- .../ScheduledConferencesViewModel.swift | 2 +- .../views/ConferenceSchedulingView.swift | 3 +- .../views/ConferenceWaitingRoomFragment.swift | 1 + .../Extensions/IOS/UITextViewExtensions.swift | 35 +++++++++++++++ .../LinphoneCore/CoreExtensions.swift | 6 +-- Classes/Swift/Voip/AudioRouteUtils.swift | 4 +- .../Swift/Voip/Models/CallsViewModel.swift | 4 +- .../ConferenceParticipantDeviceData.swift | 43 ++++++++++++------- .../Voip/Models/ConferenceViewModel.swift | 6 +-- .../Swift/Voip/Models/ControlsViewModel.swift | 6 +-- Classes/Swift/Voip/Theme/TextStyle.swift | 8 ++++ .../Conference/VoipGridParticipantCell.swift | 4 ++ .../Swift/Voip/Widgets/StyledTextView.swift | 8 +++- .../Voip/Widgets/StyledValuePicker.swift | 4 +- Podfile | 2 +- linphone.xcodeproj/project.pbxproj | 8 ++-- .../NotificationService.swift | 2 +- 21 files changed, 107 insertions(+), 49 deletions(-) create mode 100644 Classes/Swift/Extensions/IOS/UITextViewExtensions.swift diff --git a/Classes/ChatConversationView.m b/Classes/ChatConversationView.m index 6ae1f25b4..50d89e1a8 100644 --- a/Classes/ChatConversationView.m +++ b/Classes/ChatConversationView.m @@ -1344,7 +1344,7 @@ void on_chat_room_chat_message_received(LinphoneChatRoom *cr, const LinphoneEven if ((linphone_core_get_max_size_for_auto_download_incoming_files(LC) > -1) && linphone_chat_message_get_file_transfer_information(chat)) hasFile = TRUE; - if (!linphone_chat_message_is_file_transfer(chat) && !linphone_chat_message_is_text(chat) && !hasFile) /*probably an imdn*/ + if (!linphone_chat_message_is_file_transfer(chat) && !linphone_chat_message_is_text(chat) && !hasFile && ![ICSBubbleView isConferenceInvitationMessageWithCmessage:chat]) /*probably an imdn*/ return; const LinphoneAddress *from = linphone_chat_message_get_from_address(chat); diff --git a/Classes/LinphoneManager.m b/Classes/LinphoneManager.m index 59fc88be5..afcd795e0 100644 --- a/Classes/LinphoneManager.m +++ b/Classes/LinphoneManager.m @@ -862,7 +862,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) { diff --git a/Classes/LinphoneUI/UICamSwitch.m b/Classes/LinphoneUI/UICamSwitch.m index ce88d3120..22218a0e5 100644 --- a/Classes/LinphoneUI/UICamSwitch.m +++ b/Classes/LinphoneUI/UICamSwitch.m @@ -54,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_core_update_call(LC, call, NULL); - } } } diff --git a/Classes/Swift/Conference/models/ConferenceSchedulingViewModel.swift b/Classes/Swift/Conference/models/ConferenceSchedulingViewModel.swift index 22eb92419..458106791 100644 --- a/Classes/Swift/Conference/models/ConferenceSchedulingViewModel.swift +++ b/Classes/Swift/Conference/models/ConferenceSchedulingViewModel.swift @@ -24,7 +24,7 @@ import linphonesw class ConferenceSchedulingViewModel { - let core = Core.get() + var core : Core { get { Core.get() } } static let shared = ConferenceSchedulingViewModel() let subject = MutableLiveData() diff --git a/Classes/Swift/Conference/models/ScheduledConferencesViewModel.swift b/Classes/Swift/Conference/models/ScheduledConferencesViewModel.swift index 15a69d549..72449a192 100644 --- a/Classes/Swift/Conference/models/ScheduledConferencesViewModel.swift +++ b/Classes/Swift/Conference/models/ScheduledConferencesViewModel.swift @@ -25,7 +25,7 @@ import linphonesw class ScheduledConferencesViewModel { - let core = Core.get() + var core : Core { get { Core.get() } } static let shared = ScheduledConferencesViewModel() var conferences : MutableLiveData<[ScheduledConferenceData]> = MutableLiveData([]) diff --git a/Classes/Swift/Conference/views/ConferenceSchedulingView.swift b/Classes/Swift/Conference/views/ConferenceSchedulingView.swift index a64402d96..372ee752c 100644 --- a/Classes/Swift/Conference/views/ConferenceSchedulingView.swift +++ b/Classes/Swift/Conference/views/ConferenceSchedulingView.swift @@ -46,11 +46,10 @@ import linphonesw contentView.addSubview(subjectLabel) subjectLabel.alignParentLeft(withMargin: form_margin).alignParentTop().done() - let subjectInput = StyledTextView(VoipTheme.conference_scheduling_font, placeHolder:VoipTexts.conference_schedule_subject_hint, liveValue: viewModel.subject) + let subjectInput = StyledTextView(VoipTheme.conference_scheduling_font, placeHolder:VoipTexts.conference_schedule_subject_hint, liveValue: viewModel.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() diff --git a/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift b/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift index 867ca5f31..690338032 100644 --- a/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift +++ b/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift @@ -91,6 +91,7 @@ import linphonesw // localVideo view localVideo.layer.cornerRadius = center_view_corner_radius localVideo.clipsToBounds = true + localVideo.contentMode = .scaleAspectFit 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() diff --git a/Classes/Swift/Extensions/IOS/UITextViewExtensions.swift b/Classes/Swift/Extensions/IOS/UITextViewExtensions.swift new file mode 100644 index 000000000..a5341619e --- /dev/null +++ b/Classes/Swift/Extensions/IOS/UITextViewExtensions.swift @@ -0,0 +1,35 @@ +/* +* 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 UITextView { + 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/Extensions/LinphoneCore/CoreExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift index f9a4c15f1..5de3e26d7 100644 --- a/Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift +++ b/Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift @@ -42,10 +42,6 @@ extension Core { return } } - - let inConference = conference != nil && conference!.isIn - if !inConference, let call = currentCall { - try?call.update(params: nil) - }*/ + */ } } diff --git a/Classes/Swift/Voip/AudioRouteUtils.swift b/Classes/Swift/Voip/AudioRouteUtils.swift index 94a26f71e..91944d7e9 100644 --- a/Classes/Swift/Voip/AudioRouteUtils.swift +++ b/Classes/Swift/Voip/AudioRouteUtils.swift @@ -24,8 +24,8 @@ import linphonesw @objc class AudioRouteUtils : NSObject { - static let core = Core.get() - + 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: "/") diff --git a/Classes/Swift/Voip/Models/CallsViewModel.swift b/Classes/Swift/Voip/Models/CallsViewModel.swift index b539b8e5c..b68707700 100644 --- a/Classes/Swift/Voip/Models/CallsViewModel.swift +++ b/Classes/Swift/Voip/Models/CallsViewModel.swift @@ -33,8 +33,8 @@ class CallsViewModel { let callConnectedEvent = MutableLiveData() let callUpdateEvent = MutableLiveData() let noMoreCallEvent = MutableLiveData(false) - let core = Core.get() - + var core : Core { get { Core.get() } } + static let shared = CallsViewModel() private var coreDelegate : CoreDelegateStub? diff --git a/Classes/Swift/Voip/Models/ConferenceParticipantDeviceData.swift b/Classes/Swift/Voip/Models/ConferenceParticipantDeviceData.swift index b10dd635f..1756d6cb2 100644 --- a/Classes/Swift/Voip/Models/ConferenceParticipantDeviceData.swift +++ b/Classes/Swift/Voip/Models/ConferenceParticipantDeviceData.swift @@ -28,7 +28,7 @@ class ConferenceParticipantDeviceData { let videoEnabled = MutableLiveData() let activeSpeaker = MutableLiveData() let isInConference = MutableLiveData() - let core = Core.get() + var core : Core { get { Core.get() } } private var participantDeviceDelegate : ParticipantDeviceDelegate? @@ -45,28 +45,43 @@ class ConferenceParticipantDeviceData { }, onConferenceLeft: { (participantDevice) in Log.i("[Conference Participant Device] Participant \(participantDevice) has left the conference") self.isInConference.value = false - }, onAudioDirectionChanged: { (participantDevice, direction) in - Log.i("[Conference Participant Device] Participant \(participantDevice) audio stream direction changed: \(direction)") - }, onVideoDirectionChanged: { (participantDevice, direction) in + }, onStreamCapabilityChanged: { (participantDevice, direction, streamType) in Log.i("[Conference Participant Device] Participant \(participantDevice) video stream direction changed: \(direction)") self.videoEnabled.value = direction == MediaDirection.SendOnly || direction == MediaDirection.SendRecv - }, onTextDirectionChanged: { (participantDevice, direction) in - Log.i("[Conference Participant Device] Participant \(participantDevice) text stream direction changed: \(direction)") - }) + 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 - // TODO: What happens if we have disabled video locally? - videoEnabled.value = participantDevice.videoDirection == MediaDirection.SendOnly || participantDevice.videoDirection == MediaDirection.SendRecv - isInConference.value = participantDevice.isInConference + 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() } @@ -76,12 +91,10 @@ class ConferenceParticipantDeviceData { } func setVideoView(view:UIView) { - if (!isMe && participantDevice.videoDirection != MediaDirection.SendRecv) { - Log.e("[Conference Participant Device] Participant \(participantDevice) device video direction is \(participantDevice.videoDirection), don't set video window!") - return - } Log.i("[Conference Participant Device] Setting textureView \(view) for participant \(participantDevice)") - if (isMe) { // TODO: remove + if (isMe) { + core.usePreviewWindow(yesno: false) + view.contentMode = .scaleAspectFit core.nativePreviewWindow = view } else { participantDevice.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque()) diff --git a/Classes/Swift/Voip/Models/ConferenceViewModel.swift b/Classes/Swift/Voip/Models/ConferenceViewModel.swift index 0b0f4601a..63af09c47 100644 --- a/Classes/Swift/Voip/Models/ConferenceViewModel.swift +++ b/Classes/Swift/Voip/Models/ConferenceViewModel.swift @@ -24,7 +24,7 @@ import AVFoundation class ConferenceViewModel { - let core = Core.get() + var core : Core { get { Core.get() } } static let shared = ConferenceViewModel() let conferenceExists = MutableLiveData() @@ -81,7 +81,7 @@ class ConferenceViewModel { coreDelegate = CoreDelegateStub( onConferenceStateChanged: { (core, conference, state) in Log.i("[Conference] \(conference) Conference state changed: \(state)") - self.isVideoConference.value = conference.currentParams?.isVideoEnabled == true + self.isVideoConference.value = conference.currentParams?.videoEnabled == true if (state == Conference.State.Instantiated) { self.initConference(conference) @@ -135,7 +135,7 @@ class ConferenceViewModel { isConferenceLocallyPaused.value = !conference.isIn self.isMeAdmin.value = conference.me?.isAdmin == true - isVideoConference.value = conference.currentParams?.isVideoEnabled == true + isVideoConference.value = conference.currentParams?.videoEnabled == true self.subject.value = conference.subject.isEmpty ? ( conference.me?.isFocus == true ? ( diff --git a/Classes/Swift/Voip/Models/ControlsViewModel.swift b/Classes/Swift/Voip/Models/ControlsViewModel.swift index 53029a73e..cfc333f14 100644 --- a/Classes/Swift/Voip/Models/ControlsViewModel.swift +++ b/Classes/Swift/Voip/Models/ControlsViewModel.swift @@ -24,8 +24,8 @@ import AVFoundation class ControlsViewModel { - let core = Core.get() - + var core : Core { get { Core.get() } } + let isSpeakerSelected = MutableLiveData() let isMicrophoneMuted = MutableLiveData() let isMuteMicrophoneEnabled = MutableLiveData() @@ -138,7 +138,7 @@ class ControlsViewModel { func toggleVideo() { if let conference = core.conference, conference.isIn { if let params = try?core.createConferenceParams() { - let videoEnabled = conference.currentParams?.isVideoEnabled == true + let videoEnabled = conference.currentParams?.videoEnabled == true params.videoEnabled = !videoEnabled _ = conference.updateParams(params: params) } diff --git a/Classes/Swift/Voip/Theme/TextStyle.swift b/Classes/Swift/Voip/Theme/TextStyle.swift index bf1f93c30..5e242ae43 100644 --- a/Classes/Swift/Voip/Theme/TextStyle.swift +++ b/Classes/Swift/Voip/Theme/TextStyle.swift @@ -89,5 +89,13 @@ extension UITextView { 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/Views/Fragments/Conference/VoipGridParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift index 6339a0d15..79ebb474c 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift @@ -57,6 +57,10 @@ class VoipGridParticipantCell: UICollectionViewCell { } 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() { diff --git a/Classes/Swift/Voip/Widgets/StyledTextView.swift b/Classes/Swift/Voip/Widgets/StyledTextView.swift index 190203675..6e2f54984 100644 --- a/Classes/Swift/Voip/Widgets/StyledTextView.swift +++ b/Classes/Swift/Voip/Widgets/StyledTextView.swift @@ -24,15 +24,19 @@ 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) { + 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 { @@ -43,6 +47,7 @@ class StyledTextView: UITextView, UITextViewDelegate { self.text = value if (value == nil || value?.count == 0) { self.showPlaceHolder() + self.resignFirstResponder() } } if (readOnly) { @@ -71,6 +76,7 @@ class StyledTextView: UITextView, UITextViewDelegate { } 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 index a8bd5b857..f8ca9d3c6 100644 --- a/Classes/Swift/Voip/Widgets/StyledValuePicker.swift +++ b/Classes/Swift/Voip/Widgets/StyledValuePicker.swift @@ -41,8 +41,8 @@ class StyledValuePicker: UIView { formattedLabel.isUserInteractionEnabled = false formattedLabel.backgroundColor = VoipTheme.voipFormBackgroundColor.get() - formattedLabel.text = " "+options[liveIndex.value!] - + liveIndex.value.map { formattedLabel.text = " "+options[$0] } + if (readOnly) { formattedLabel.textColor = formattedLabel.textColor.withAlphaComponent(0.5) } diff --git a/Podfile b/Podfile index d8ffbb540..6affc64ca 100644 --- a/Podfile +++ b/Podfile @@ -5,7 +5,7 @@ source "https://github.com/CocoaPods/Specs.git" def all_pods if ENV['PODFILE_PATH'].nil? - pod 'linphone-sdk', '~> 5.1.0-alpha.75+d4a0bd2' + pod 'linphone-sdk', '~> 5.2.0-alpha.125+0f20296' else pod 'linphone-sdk', :path => ENV['PODFILE_PATH'] # local sdk end diff --git a/linphone.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj index 031fbca04..5f2485bfe 100644 --- a/linphone.xcodeproj/project.pbxproj +++ b/linphone.xcodeproj/project.pbxproj @@ -5642,7 +5642,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.94+f4cb5df\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.125+0f20296\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5769,7 +5769,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.94+f4cb5df\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.125+0f20296\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5895,7 +5895,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.94+f4cb5df\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.125+0f20296\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -6020,7 +6020,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.94+f4cb5df\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.125+0f20296\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; 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() From 4f4d8a2a0930baa9045211736fe65cd95dc893db Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Mon, 21 Mar 2022 17:03:14 +0100 Subject: [PATCH 05/75] SDK Upda to 5.2.0-alpha.44+75de6a92 --- Classes/Swift/CallManager.swift | 2 +- .../Voip/Models/ConferenceViewModel.swift | 2 +- .../Swift/Voip/Models/ControlsViewModel.swift | 2 +- Podfile | 2 +- linphone.xcodeproj/project.pbxproj | 44 ++++++++++--------- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/Classes/Swift/CallManager.swift b/Classes/Swift/CallManager.swift index 9d72c6bdb..ef52ebe34 100644 --- a/Classes/Swift/CallManager.swift +++ b/Classes/Swift/CallManager.swift @@ -609,7 +609,7 @@ import AVFoundation func addAllToLocalConference() { do { - if let core = core, let params = try? core.createConferenceParams() { + if let core = core, 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) diff --git a/Classes/Swift/Voip/Models/ConferenceViewModel.swift b/Classes/Swift/Voip/Models/ConferenceViewModel.swift index 63af09c47..40f9df8bf 100644 --- a/Classes/Swift/Voip/Models/ConferenceViewModel.swift +++ b/Classes/Swift/Voip/Models/ConferenceViewModel.swift @@ -92,7 +92,7 @@ class ConferenceViewModel { self.terminateConference(conference) } - let layout = conference.layout == .None ? .Grid : conference.layout + let layout = conference.layout == .Legacy ? .Grid : conference.layout self.conferenceDisplayMode.value = layout Log.i("[Conference] \(conference) Conference current layout is: \(layout)") } diff --git a/Classes/Swift/Voip/Models/ControlsViewModel.swift b/Classes/Swift/Voip/Models/ControlsViewModel.swift index cfc333f14..5ee71fd59 100644 --- a/Classes/Swift/Voip/Models/ControlsViewModel.swift +++ b/Classes/Swift/Voip/Models/ControlsViewModel.swift @@ -137,7 +137,7 @@ class ControlsViewModel { func toggleVideo() { if let conference = core.conference, conference.isIn { - if let params = try?core.createConferenceParams() { + if let params = try?core.createConferenceParams(conference:conference) { let videoEnabled = conference.currentParams?.videoEnabled == true params.videoEnabled = !videoEnabled _ = conference.updateParams(params: params) diff --git a/Podfile b/Podfile index 6affc64ca..908001766 100644 --- a/Podfile +++ b/Podfile @@ -5,7 +5,7 @@ source "https://github.com/CocoaPods/Specs.git" def all_pods if ENV['PODFILE_PATH'].nil? - pod 'linphone-sdk', '~> 5.2.0-alpha.125+0f20296' + pod 'linphone-sdk', '5.2.0-alpha.44+75de6a92' else pod 'linphone-sdk', :path => ENV['PODFILE_PATH'] # local sdk end diff --git a/linphone.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj index 5f2485bfe..a9f83623c 100644 --- a/linphone.xcodeproj/project.pbxproj +++ b/linphone.xcodeproj/project.pbxproj @@ -4804,22 +4804,24 @@ "${BUILT_PRODUCTS_DIR}/IQKeyboardManager/IQKeyboardManager.framework", "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework", "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/bctoolbox-ios.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/bctoolbox.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/belcard.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/belle-sip.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/belr.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/lime.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/linphone.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/linphonetester.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/mediastreamer2.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/msamr.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/mscodec2.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/msopenh264.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/mssilk.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/mswebrtc.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/msx264.framework", - "${PODS_ROOT}/../../linphone-sdk/ios/linphone-sdk/apple-darwin/Frameworks/ortp.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/bctoolbox-ios.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/bctoolbox-tester.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/bctoolbox.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/belcard.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/belle-sip.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/belr.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/lime.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/limetester.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/linphone.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/linphonetester.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/mediastreamer2.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/msamr.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/mscodec2.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/msopenh264.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/mssilk.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/mswebrtc.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/msx264.framework", + "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/ortp.framework", "${BUILT_PRODUCTS_DIR}/linphone-sdk/linphonesw.framework", ); name = "[CP] Embed Pods Frameworks"; @@ -4829,11 +4831,13 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bctoolbox-ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bctoolbox-tester.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bctoolbox.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/belcard.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/belle-sip.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/belr.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/lime.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/limetester.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/linphone.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/linphonetester.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/mediastreamer2.framework", @@ -5642,7 +5646,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.125+0f20296\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.44+75de6a92\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5769,7 +5773,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.125+0f20296\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.44+75de6a92\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5895,7 +5899,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.125+0f20296\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.44+75de6a92\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -6020,7 +6024,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.125+0f20296\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.44+75de6a92\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; From 38a9e888da0bd8792093859b773e8d37aacb4c4e Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Mon, 21 Mar 2022 18:17:45 +0100 Subject: [PATCH 06/75] GridBoxLayout rework --- .../ConferenceParticipantDeviceData.swift | 2 +- .../ActiveCallOrConferenceView.swift | 2 +- .../Conference/VoipConferenceGridView.swift | 112 ++++++++++-------- 3 files changed, 67 insertions(+), 49 deletions(-) diff --git a/Classes/Swift/Voip/Models/ConferenceParticipantDeviceData.swift b/Classes/Swift/Voip/Models/ConferenceParticipantDeviceData.swift index 1756d6cb2..960dd961d 100644 --- a/Classes/Swift/Voip/Models/ConferenceParticipantDeviceData.swift +++ b/Classes/Swift/Voip/Models/ConferenceParticipantDeviceData.swift @@ -92,9 +92,9 @@ class ConferenceParticipantDeviceData { func setVideoView(view:UIView) { Log.i("[Conference Participant Device] Setting textureView \(view) for participant \(participantDevice)") + view.contentMode = .scaleAspectFit if (isMe) { core.usePreviewWindow(yesno: false) - view.contentMode = .scaleAspectFit core.nativePreviewWindow = view } else { participantDevice.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque()) diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index 924d2d95f..630b1cb01 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -121,7 +121,7 @@ import linphonesw self.extraButtonsView.isHidden = true self.conferencePausedView?.isHidden = true self.conferenceGridView!.isHidden = false - self.conferenceActiveSpeakerView!.isHidden = true + self.conferenceActiveSpeakerView?.isHidden = true self.conferenceGridView?.conferenceViewModel = ConferenceViewModel.shared } else { self.conferenceGridView?.isHidden = true diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift index b14fdb48d..c99d5f240 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift @@ -40,7 +40,8 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi var recordCallButtons : [CallControlButton] = [] var pauseCallButtons : [CallControlButton] = [] var grid : UICollectionView - + var gridContainer = UIView() + var conferenceViewModel: ConferenceViewModel? = nil { didSet { @@ -51,7 +52,7 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi duration.conference = model.conference.value self.remotelyRecording.isRemotelyRecorded = model.isRemotelyRecorded model.conferenceParticipantDevices.readCurrentAndObserve { (_) in - self.grid.reloadData() + self.reloadData() } model.isConferenceLocallyPaused.readCurrentAndObserve { (paused) in self.pauseCallButtons.forEach { @@ -64,7 +65,7 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi } } } - self.grid.reloadData() + self.reloadData() } } @@ -132,16 +133,20 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi grid.dataSource = self grid.delegate = self grid.register(VoipGridParticipantCell.self, forCellWithReuseIdentifier: "VoipGridParticipantCell") - grid.backgroundColor = VoipTheme.voipBackgroundColor.get() + grid.backgroundColor = .clear grid.isScrollEnabled = false - addSubview(grid) - grid.matchParentSideBorders().alignUnder(view:headerView,withMargin: ActiveCallView.center_view_margin_top).alignParentBottom().done() - + 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 - grid.onClick { + gridContainer.onClick { ControlsViewModel.shared.toggleFullScreen() } @@ -149,17 +154,18 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi if (self.isHidden) { return } - self.grid.removeConstraints().done() + self.gridContainer.removeConstraints().done() if (fullScreen == true) { - self.grid.removeFromSuperview() - PhoneMainView.instance().mainViewController.view?.addSubview(self.grid) - self.grid.matchParentDimmensions().center().done() - self.grid.reloadData() // Cauz of the frames + self.gridContainer.removeFromSuperview() + PhoneMainView.instance().mainViewController.view?.addSubview(self.gridContainer) + self.gridContainer.matchParentDimmensions().center().done() } else { - self.grid.removeFromSuperview() - self.addSubview(self.grid) - self.grid.matchParentSideBorders().alignUnder(view:headerView,withMargin: ActiveCallView.center_view_margin_top).alignParentBottom().done() - self.grid.reloadData() + 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() } } } @@ -167,6 +173,19 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi // UICollectionView related delegates + func reloadData() { + 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() + } + Log.e("cdes > \(width)") + } + } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return inter_cell } @@ -193,44 +212,43 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi 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, + layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { guard let participantsCount = conferenceViewModel?.conferenceParticipantDevices.value?.count else { return .zero } - var cellSize : CGSize = .zero - let availableSize = collectionView.frame.size - - if (participantsCount == 1) { - cellSize = availableSize - } else if (participantsCount == 2) { - cellSize = CGSize(width:availableSize.width, height:availableSize.height/2) - cellSize.height -= inter_cell/2 - } else if (participantsCount == 3) { - cellSize = CGSize(width:availableSize.width, height:availableSize.height/3) - cellSize.height -= 2*inter_cell/3 - } else if (participantsCount == 4) { - cellSize = CGSize(width:availableSize.width/2, height:availableSize.height/2) - cellSize.height -= inter_cell/2 - cellSize.width -= inter_cell/2 - } else if (participantsCount == 5) { - if (indexPath.row == 4) { // last (local) participant takes full width (under discussion) - cellSize = CGSize(width:availableSize.width, height:availableSize.height/3) - } else { - cellSize = CGSize(width:availableSize.width/2, height:availableSize.height/3) - cellSize.width -= inter_cell/2 - } - cellSize.height -= 2*inter_cell/3 - } else { - cellSize = CGSize(width:availableSize.width/2, height:availableSize.height/CGFloat((participantsCount/2))) - cellSize.height -= 2*inter_cell/3 - cellSize.width -= inter_cell/2 - } return cellSize - } required init?(coder: NSCoder) { From f452f010bbf4de9fae58a938a56d6a549f67b706 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Tue, 22 Mar 2022 10:31:29 +0100 Subject: [PATCH 07/75] ConferenceViewModel updates (aligned with Android) --- .../Extensions/IOS/OptionalExtensions.swift | 2 +- Classes/Swift/Voip/Models/CallData.swift | 2 +- .../Voip/Models/ConferenceViewModel.swift | 326 ++++++++++++------ .../ActiveCallOrConferenceView.swift | 1 - .../Conference/VoipConferenceGridView.swift | 3 +- 5 files changed, 223 insertions(+), 111 deletions(-) diff --git a/Classes/Swift/Extensions/IOS/OptionalExtensions.swift b/Classes/Swift/Extensions/IOS/OptionalExtensions.swift index ddb0ad754..345b367d7 100644 --- a/Classes/Swift/Extensions/IOS/OptionalExtensions.swift +++ b/Classes/Swift/Extensions/IOS/OptionalExtensions.swift @@ -19,7 +19,7 @@ extension Optional { - var logable: Any { + var orNil: Any { switch self { case .none: return "|⭕️" diff --git a/Classes/Swift/Voip/Models/CallData.swift b/Classes/Swift/Voip/Models/CallData.swift index fb69db380..7d97fdec4 100644 --- a/Classes/Swift/Voip/Models/CallData.swift +++ b/Classes/Swift/Voip/Models/CallData.swift @@ -119,7 +119,7 @@ class CallData { let localAddress = try?Factory.Instance.createAddress(addr: localSipUri), let remoteSipAddress = try?Factory.Instance.createAddress(addr: remoteSipUri) else { - Log.e("[Call] Failed to get either local \(localSipUri.logable) or remote \(remoteSipUri.logable) SIP address!") + Log.e("[Call] Failed to get either local \(localSipUri.orNil) or remote \(remoteSipUri.orNil) SIP address!") return } do { diff --git a/Classes/Swift/Voip/Models/ConferenceViewModel.swift b/Classes/Swift/Voip/Models/ConferenceViewModel.swift index 40f9df8bf..4a1aeb2c1 100644 --- a/Classes/Swift/Voip/Models/ConferenceViewModel.swift +++ b/Classes/Swift/Voip/Models/ConferenceViewModel.swift @@ -32,69 +32,104 @@ class ConferenceViewModel { 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 participantAdminStatusChangedEvent = MutableLiveData() + let maxParticipantsForMosaicLayout = ConfigManager.instance().lpConfigIntForKey(key: "max_conf_part_mosaic_layout",defaultValue: 6) + let speakingParticipant = MutableLiveData() private var conferenceDelegate : ConferenceDelegateStub? private var coreDelegate : CoreDelegateStub? 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 + 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 + } + }, + onParticipantRemoved: {(conference: Conference, participant: Participant) in + Log.i("[Conference] \(conference) \(participant) Participant removed") + self.updateParticipantsList(conference) + }, + 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 + Log.i("[Conference] onParticipantDeviceJoined Entered conference") + self.isConferenceLocallyPaused.value = true + }, + onParticipantDeviceJoined: { (conference: Conference, device: ParticipantDevice) in + Log.i("[Conference] onParticipantDeviceJoined Entered conference") + self.isConferenceLocallyPaused.value = false + }, + onStateChanged: { (conference: Conference, state: Conference.State) in + Log.i("[Conference] State changed: \(state)") + self.isVideoConference.value = conference.currentParams?.isVideoEnabled + 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") + } + + } } - }, onParticipantRemoved: {(conference: Conference, participant: Participant) in - Log.i("[Conference] \(conference) \(participant) Participant removed") - self.updateParticipantsList(conference) - }, onParticipantDeviceAdded: {(conference: Conference, participantDevice: ParticipantDevice) in - Log.i("[Conference] \(conference) Participant device \(participantDevice) added") - self.updateParticipantsDevicesList(conference) - }, onParticipantDeviceRemoved: { (conference: Conference, participantDevice: ParticipantDevice) in - Log.i("[Conference] \(conference) Participant device \(participantDevice) removed") - self.updateParticipantsDevicesList(conference) - }, onParticipantAdminStatusChanged: { (conference: Conference, participant: Participant) in - Log.i("[Conference] \(conference) Participant admin status changed") - self.isMeAdmin.value = conference.me?.isAdmin - self.updateParticipantsList(conference) - }, onParticipantDeviceLeft: { (conference: Conference, device: ParticipantDevice) in - Log.i("[Conference] onParticipantDeviceJoined Entered conference") - self.isConferenceLocallyPaused.value = true - }, onParticipantDeviceJoined: { (conference: Conference, device: ParticipantDevice) in - Log.i("[Conference] onParticipantDeviceJoined Entered conference") - self.isConferenceLocallyPaused.value = false - }, onSubjectChanged: { (conference: Conference, subject: String) in - self.subject.value = subject - } ) coreDelegate = CoreDelegateStub( onConferenceStateChanged: { (core, conference, state) in Log.i("[Conference] \(conference) Conference state changed: \(state)") - self.isVideoConference.value = conference.currentParams?.videoEnabled == true - if (state == Conference.State.Instantiated) { + self.conferenceCreationPending.value = true self.initConference(conference) - } else if (state == Conference.State.Created) { - self.initConference(conference) - self.configureConference(conference) - } else if (state == Conference.State.Terminated || state == Conference.State.TerminationFailed) { - self.terminateConference(conference) + } - - let layout = conference.layout == .Legacy ? .Grid : conference.layout - self.conferenceDisplayMode.value = layout - Log.i("[Conference] \(conference) Conference current layout is: \(layout)") } ) @@ -105,28 +140,47 @@ class ConferenceViewModel { subject.value = VoipTexts.conference_default_title if let conference = core.conference != nil ? core.conference : core.currentCall?.conference { - Log.i("[Conference] Found an existing conference: \(conference)") - initConference(conference) - configureConference(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 + conferenceExists.value = true self.conference.value = conference conference.addDelegate(delegate: self.conferenceDelegate!) - isRecording.value = conference.isRecording - } - - func terminateConference(_ conference: Conference) { - conferenceExists.value = false - isVideoConference.value = false - self.conferenceParticipants.value?.forEach{ $0.destroy()} - self.conferenceParticipantDevices.value?.forEach{ $0.destroy()} - conferenceParticipants.value = [] - conferenceParticipantDevices.value = [] + isRecording.value = conference.isRecording + updateConferenceLayout(conference: conference) } func configureConference(_ conference: Conference) { @@ -136,7 +190,7 @@ class ConferenceViewModel { isConferenceLocallyPaused.value = !conference.isIn self.isMeAdmin.value = conference.me?.isAdmin == true isVideoConference.value = conference.currentParams?.videoEnabled == true - + self.subject.value = conference.subject.isEmpty ? ( conference.me?.isFocus == true ? ( VoipTexts.conference_local_title @@ -149,44 +203,55 @@ class ConferenceViewModel { } - - func pauseConference() { - Log.i("[Conference] Leaving conference with address \(conference) temporarily") - conference.value?.leave() - } - - func resumeConference() { - Log.i("[Conference] entering conference with address \(conference)") - conference.value?.enter() - } - - func togglePlayPause () { - if (isConferenceLocallyPaused.value == true) { - resumeConference() - isConferenceLocallyPaused.value = false - } else { - pauseConference() - isConferenceLocallyPaused.value = true - } - } - - func toggleRecording() { - guard let conference = conference.value else { - Log.e("[Conference] Failed to find conference!") + func addCallsToConference() { + Log.i("[Conference] Trying to merge all calls into existing conference") + guard let conf = conference.value else { return } - /* frogtrust has is own recording method - if (conference.isRecording == true) { - conference.stopRecording() - } else { - let path = AppManager.recordingFilePathFromCall(address: conference.conferenceAddress?.asStringUriOnly() ?? "") - Log.i("[Conference] Starting recording \(conference) in file \(path)") - conference.startRecording(path: path) - }*/ - - isRecording.value = conference.isRecording + 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: ConferenceLayout) { + Log.i("[Conference] Trying to change conference layout to $layout") + if let conference = conference.value { + conference.layout = layout + updateConferenceLayout(conference: conference) + } else { + Log.e("[Conference] Conference is null in ConferenceViewModel") + } + } + + private func updateConferenceLayout(conference: Conference) { + conferenceDisplayMode.value = conference.layout == .Legacy ? .Grid : conference.layout + let list = sortDevicesDataList(devices: conferenceParticipantDevices.value!) + conferenceParticipantDevices.value = list + Log.i("[Conference] Conference current layout is: \(conference.layout)") + } + + + + 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] = [] @@ -232,6 +297,68 @@ class ConferenceViewModel { 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") @@ -252,7 +379,7 @@ class ConferenceViewModel { 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()) will be removed from conference") + Log.w("[Conference Participants] Participant \((participant.address?.asStringUriOnly()).orNil) will be removed from conference") try conference.removeParticipant(participant: participant) } } @@ -261,19 +388,6 @@ class ConferenceViewModel { } } - 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) - } - } - } - - } @objc class ConferenceViewModelBridge : NSObject { diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index 630b1cb01..adb32ae1f 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -311,7 +311,6 @@ import linphonesw } func goToChat() { - let core = Core.get() guard let chatRoom = CallsViewModel.shared.currentCallData.value??.chatRoom else { diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift index c99d5f240..e9dd7883f 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift @@ -182,7 +182,6 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi if (width > 0) { self.grid.removeConstraints().width(width).height(height).center().done() } - Log.e("cdes > \(width)") } } @@ -244,7 +243,7 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - guard let participantsCount = conferenceViewModel?.conferenceParticipantDevices.value?.count else { + guard let _ = conferenceViewModel?.conferenceParticipantDevices.value?.count else { return .zero } From e4da04b46474323ade6b4bdc4fd2cfbf4ff57620 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Tue, 22 Mar 2022 12:20:54 +0100 Subject: [PATCH 08/75] Replace outgoing call view for conference joining by spinner --- Classes/DevicesListView.m | 2 +- Classes/LinphoneManager.m | 2 +- Classes/PhoneMainView.m | 9 +++++--- Classes/Swift/CallManager.swift | 22 ++++++++++++++----- .../views/ConferenceWaitingRoomFragment.swift | 2 +- Classes/Swift/ProviderDelegate.swift | 8 +++++-- .../{Models => ViewModels}/CallData.swift | 2 +- .../CallStatisticsData.swift | 0 .../CallsViewModel.swift | 1 + .../ConferenceParticipantData.swift | 0 .../ConferenceParticipantDeviceData.swift | 0 .../ConferenceViewModel.swift | 0 .../ControlsViewModel.swift | 0 .../ActiveCallOrConferenceView.swift | 13 +++++++++++ 14 files changed, 46 insertions(+), 15 deletions(-) rename Classes/Swift/Voip/{Models => ViewModels}/CallData.swift (98%) rename Classes/Swift/Voip/{Models => ViewModels}/CallStatisticsData.swift (100%) rename Classes/Swift/Voip/{Models => ViewModels}/CallsViewModel.swift (99%) rename Classes/Swift/Voip/{Models => ViewModels}/ConferenceParticipantData.swift (100%) rename Classes/Swift/Voip/{Models => ViewModels}/ConferenceParticipantDeviceData.swift (100%) rename Classes/Swift/Voip/{Models => ViewModels}/ConferenceViewModel.swift (100%) rename Classes/Swift/Voip/{Models => ViewModels}/ControlsViewModel.swift (100%) diff --git a/Classes/DevicesListView.m b/Classes/DevicesListView.m index 60e005dc9..5238673f2 100644 --- a/Classes/DevicesListView.m +++ b/Classes/DevicesListView.m @@ -151,7 +151,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/LinphoneManager.m b/Classes/LinphoneManager.m index afcd795e0..a6821ad70 100644 --- a/Classes/LinphoneManager.m +++ b/Classes/LinphoneManager.m @@ -1862,7 +1862,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/PhoneMainView.m b/Classes/PhoneMainView.m index 49f6aa3f8..1d3393bd6 100644 --- a/Classes/PhoneMainView.m +++ b/Classes/PhoneMainView.m @@ -378,9 +378,12 @@ static RootViewManager *rootViewManagerInstance = nil; case LinphoneCallOutgoingEarlyMedia: case LinphoneCallOutgoingProgress: case LinphoneCallOutgoingRinging: { - OutgoingCallView *v = VIEW(OutgoingCallView); - [self changeCurrentView:OutgoingCallView.compositeViewDescription]; - [v setCallWithCall:call]; + CallAppData *data = [CallManager getAppDataWithCall:call]; + if (!data.isConference) { + OutgoingCallView *v = VIEW(OutgoingCallView); + [self changeCurrentView:OutgoingCallView.compositeViewDescription]; + [v setCallWithCall:call]; + } break; } case LinphoneCallPausedByRemote: diff --git a/Classes/Swift/CallManager.swift b/Classes/Swift/CallManager.swift index ef52ebe34..7d3a0eec4 100644 --- a/Classes/Swift/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 + } /* @@ -207,7 +209,7 @@ 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 @@ -221,27 +223,27 @@ import AVFoundation 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) { + 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) + 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().core!.createCallParams(call: nil) @@ -270,6 +272,13 @@ import AVFoundation if (isSas) { lcallParams.mediaEncryption = .ZRTP } + if (isConference) { + lcallParams.videoEnabled = true + lcallParams.videoDirection = isVideo ? .SendRecv : .RecvOnly + } else { + lcallParams.videoEnabled = isVideo + } + let call = CallManager.instance().core!.inviteAddressWithParams(addr: addr, params: lcallParams) if (call != nil) { // The LinphoneCallAppData object should be set on call creation with callback @@ -280,6 +289,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) } diff --git a/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift b/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift index 690338032..ea3ba9852 100644 --- a/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift +++ b/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift @@ -84,7 +84,7 @@ import linphonesw } start.onClick { - self.conferenceUrl.map{ CallManager.instance().startCall(addr: $0, isSas: false) } + self.conferenceUrl.map{ CallManager.instance().startCall(addr: $0, isSas: false, isVideo: true, isConference: true) } } diff --git a/Classes/Swift/ProviderDelegate.swift b/Classes/Swift/ProviderDelegate.swift index 5f4ecd512..f3e1f4e96 100644 --- a/Classes/Swift/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 } } @@ -252,7 +256,7 @@ extension ProviderDelegate: CXProviderDelegate { } CallManager.instance().core?.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() diff --git a/Classes/Swift/Voip/Models/CallData.swift b/Classes/Swift/Voip/ViewModels/CallData.swift similarity index 98% rename from Classes/Swift/Voip/Models/CallData.swift rename to Classes/Swift/Voip/ViewModels/CallData.swift index 7d97fdec4..988c79729 100644 --- a/Classes/Swift/Voip/Models/CallData.swift +++ b/Classes/Swift/Voip/ViewModels/CallData.swift @@ -88,7 +88,7 @@ class CallData { isRemotelyPaused.value = isCallRemotelyPaused() canBePaused.value = canCallBePaused() let conference = call.conference - isInRemoteConference.value = conference != nil + isInRemoteConference.value = conference != nil || CallManager.getAppData(call: call.getCobject!)?.isConference == true if (conference != nil) { remoteConferenceSubject.value = conference?.subject != nil && (conference?.subject.count)! > 0 ? conference!.subject : VoipTexts.conference_default_title } diff --git a/Classes/Swift/Voip/Models/CallStatisticsData.swift b/Classes/Swift/Voip/ViewModels/CallStatisticsData.swift similarity index 100% rename from Classes/Swift/Voip/Models/CallStatisticsData.swift rename to Classes/Swift/Voip/ViewModels/CallStatisticsData.swift diff --git a/Classes/Swift/Voip/Models/CallsViewModel.swift b/Classes/Swift/Voip/ViewModels/CallsViewModel.swift similarity index 99% rename from Classes/Swift/Voip/Models/CallsViewModel.swift rename to Classes/Swift/Voip/ViewModels/CallsViewModel.swift index b68707700..294491b9e 100644 --- a/Classes/Swift/Voip/Models/CallsViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/CallsViewModel.swift @@ -33,6 +33,7 @@ class CallsViewModel { let callConnectedEvent = MutableLiveData() let callUpdateEvent = MutableLiveData() let noMoreCallEvent = MutableLiveData(false) + var core : Core { get { Core.get() } } static let shared = CallsViewModel() diff --git a/Classes/Swift/Voip/Models/ConferenceParticipantData.swift b/Classes/Swift/Voip/ViewModels/ConferenceParticipantData.swift similarity index 100% rename from Classes/Swift/Voip/Models/ConferenceParticipantData.swift rename to Classes/Swift/Voip/ViewModels/ConferenceParticipantData.swift diff --git a/Classes/Swift/Voip/Models/ConferenceParticipantDeviceData.swift b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift similarity index 100% rename from Classes/Swift/Voip/Models/ConferenceParticipantDeviceData.swift rename to Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift diff --git a/Classes/Swift/Voip/Models/ConferenceViewModel.swift b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift similarity index 100% rename from Classes/Swift/Voip/Models/ConferenceViewModel.swift rename to Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift diff --git a/Classes/Swift/Voip/Models/ControlsViewModel.swift b/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift similarity index 100% rename from Classes/Swift/Voip/Models/ControlsViewModel.swift rename to Classes/Swift/Voip/ViewModels/ControlsViewModel.swift diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index adb32ae1f..e75519c6f 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -33,6 +33,8 @@ import linphonesw var currentCallView : ActiveCallView? = nil var conferenceGridView: VoipConferenceGridView? = nil var conferenceActiveSpeakerView: VoipConferenceActiveSpeakerView? = nil + let conferenceJoinSpinner = RotatingSpinner() + let extraButtonsView = VoipExtraButtonsView() var numpadView : NumpadView? = nil @@ -128,6 +130,17 @@ import linphonesw } } + 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!) From ce93af25669f7aac609328a64df20ca1543ab173 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Tue, 22 Mar 2022 15:17:08 +0100 Subject: [PATCH 09/75] SDK update --- Podfile | 2 +- linphone.xcodeproj/project.pbxproj | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Podfile b/Podfile index 908001766..c6dff2350 100644 --- a/Podfile +++ b/Podfile @@ -5,7 +5,7 @@ source "https://github.com/CocoaPods/Specs.git" def all_pods if ENV['PODFILE_PATH'].nil? - pod 'linphone-sdk', '5.2.0-alpha.44+75de6a92' + pod 'linphone-sdk', '5.2.0-alpha.61+5e27e59a' else pod 'linphone-sdk', :path => ENV['PODFILE_PATH'] # local sdk end diff --git a/linphone.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj index a9f83623c..610162cf7 100644 --- a/linphone.xcodeproj/project.pbxproj +++ b/linphone.xcodeproj/project.pbxproj @@ -3340,7 +3340,7 @@ C67C97B7274FD76B0074A0D8 /* AudioRouteUtils.swift */, C6710F5A2722A9FB00ED888F /* Theme */, C6710F4D2722900A00ED888F /* Widgets */, - C65A5D3127216C3E005BA038 /* Models */, + C65A5D3127216C3E005BA038 /* ViewModels */, C65A5D2E27216B4C005BA038 /* Views */, ); path = Voip; @@ -3356,7 +3356,7 @@ path = Views; sourceTree = ""; }; - C65A5D3127216C3E005BA038 /* Models */ = { + C65A5D3127216C3E005BA038 /* ViewModels */ = { isa = PBXGroup; children = ( C65A5D3E27216E3A005BA038 /* CallData.swift */, @@ -3367,7 +3367,7 @@ C6F2D4EE27392D960071BA52 /* CallsViewModel.swift */, C6C98CD427453ED700059B55 /* ConferenceViewModel.swift */, ); - path = Models; + path = ViewModels; sourceTree = ""; }; C65A5D3427216CAB005BA038 /* ViewModel */ = { @@ -5646,7 +5646,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.44+75de6a92\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.61+5e27e59a\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5773,7 +5773,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.44+75de6a92\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.61+5e27e59a\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5899,7 +5899,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.44+75de6a92\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.61+5e27e59a\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -6024,7 +6024,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.44+75de6a92\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.61+5e27e59a\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; From 64d08445b65bdfcf8691f8a925a6878e0560c0dd Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Tue, 22 Mar 2022 17:24:43 +0100 Subject: [PATCH 10/75] Conference Waiting room view model + outgoing phase for call conference to remain on waiting room --- .../ConferenceWaitingRoomViewModel.swift | 40 +++++++++++++++++++ .../views/ConferenceWaitingRoomFragment.swift | 25 +++++++++++- linphone.xcodeproj/project.pbxproj | 12 ++++-- 3 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 Classes/Swift/Conference/models/ConferenceWaitingRoomViewModel.swift diff --git a/Classes/Swift/Conference/models/ConferenceWaitingRoomViewModel.swift b/Classes/Swift/Conference/models/ConferenceWaitingRoomViewModel.swift new file mode 100644 index 000000000..bf428bf79 --- /dev/null +++ b/Classes/Swift/Conference/models/ConferenceWaitingRoomViewModel.swift @@ -0,0 +1,40 @@ +/* + * 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 { + + var core : Core { get { Core.get() } } + static let shared = ConferenceWaitingRoomViewModel() + + let joinWithVideo = MutableLiveData() + let layout = MutableLiveData() + let joinInProgress = MutableLiveData(false) + + init () { + + } + + + +} diff --git a/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift b/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift index ea3ba9852..5a6857164 100644 --- a/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift +++ b/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift @@ -41,7 +41,8 @@ import linphonesw 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() @@ -80,13 +81,33 @@ import linphonesw 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.shared.joinInProgress.value = false PhoneMainView.instance().popView(self.compositeViewDescription()) } - + start.onClick { + ConferenceWaitingRoomViewModel.shared.joinInProgress.value = true self.conferenceUrl.map{ CallManager.instance().startCall(addr: $0, isSas: false, isVideo: true, isConference: true) } } + ConferenceWaitingRoomViewModel.shared.joinInProgress.readCurrentAndObserve { joining in + self.start.isEnabled = joining != true + self.localVideo.isHidden = joining == true + if (joining == true) { + self.view.addSubview(self.conferenceJoinSpinner) + self.conferenceJoinSpinner.square(IncomingOutgoingCommonView.spinner_size).center().done() + self.conferenceJoinSpinner.startRotation() + } else { + self.conferenceJoinSpinner.stopRotation() + self.conferenceJoinSpinner.removeFromSuperview() + } + } + // localVideo view localVideo.layer.cornerRadius = center_view_corner_radius diff --git a/linphone.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj index 610162cf7..32681ee96 100644 --- a/linphone.xcodeproj/project.pbxproj +++ b/linphone.xcodeproj/project.pbxproj @@ -609,7 +609,7 @@ 63E27A321C4FECD000D332AE /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 63E27A311C4FECD000D332AE /* LaunchScreen.xib */; }; 63E27A521C50EDB000D332AE /* hold.mkv in Resources */ = {isa = PBXBuildFile; fileRef = 63E27A511C50EB2700D332AE /* hold.mkv */; }; 63E59A3F1ADE70D900646FB3 /* InAppProductsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 63E59A3E1ADE70D900646FB3 /* InAppProductsManager.m */; }; - 63E802DB1C625AEF000D5509 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 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 */; }; 63FB30351A680E73008CA393 /* UIRoundedImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 63FB30341A680E73008CA393 /* UIRoundedImageView.m */; }; @@ -683,6 +683,7 @@ C65A5D3B27216CC0005BA038 /* MutableLiveData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65A5D3727216CC0005BA038 /* MutableLiveData.swift */; }; C65A5D3F27216E3A005BA038 /* CallData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65A5D3E27216E3A005BA038 /* CallData.swift */; }; C65A5D45272196AE005BA038 /* OptionalExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65A5D44272196AE005BA038 /* OptionalExtensions.swift */; }; + C662D0A927EA2C5F00C02D4A /* ConferenceWaitingRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C662D0A827EA2C5F00C02D4A /* ConferenceWaitingRoomViewModel.swift */; }; 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 */; }; @@ -1852,6 +1853,7 @@ C65A5D3727216CC0005BA038 /* MutableLiveData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MutableLiveData.swift; sourceTree = ""; }; C65A5D3E27216E3A005BA038 /* CallData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallData.swift; sourceTree = ""; }; C65A5D44272196AE005BA038 /* OptionalExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalExtensions.swift; sourceTree = ""; }; + C662D0A827EA2C5F00C02D4A /* ConferenceWaitingRoomViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConferenceWaitingRoomViewModel.swift; sourceTree = ""; }; C66B03BC26E8EB1A009B5EDC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/UIChatReplyBubbleView.xib; sourceTree = ""; }; C66B03C126E8EB82009B5EDC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/UIChatReplyBubbleView.strings; sourceTree = ""; }; C66B03C326E8EB87009B5EDC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/UIChatReplyBubbleView.strings; sourceTree = ""; }; @@ -2524,7 +2526,7 @@ path = LinphoneUI; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA = { + 29B97314FDCFA39411CA2CEA /* CustomTemplate */ = { isa = PBXGroup; children = ( 8C23BCB71D82AAC3005F19BB /* linphone.entitlements */, @@ -3587,6 +3589,7 @@ isa = PBXGroup; children = ( C6EA2F662754DC45008E60F8 /* ConferenceSchedulingViewModel.swift */, + C662D0A827EA2C5F00C02D4A /* ConferenceWaitingRoomViewModel.swift */, C6AF9217275E13790087ACDE /* ScheduledConferencesViewModel.swift */, ); path = models; @@ -3933,7 +3936,7 @@ fr, hu, ); - mainGroup = 29B97314FDCFA39411CA2CEA; + mainGroup = 29B97314FDCFA39411CA2CEA /* CustomTemplate */; productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; projectRoot = ""; @@ -3999,7 +4002,7 @@ 633FEEE01D3CD55A0014B822 /* numpad_8_over~ipad@2x.png in Resources */, C6710FA42722B20000ED888F /* voip_single_contact_avatar.png in Resources */, 633FEDDC1D3CD5590014B822 /* call_start_body_disabled~ipad.png in Resources */, - 63E802DB1C625AEF000D5509 /* BuildFile in Resources */, + 63E802DB1C625AEF000D5509 /* (null) in Resources */, 633FEE2E1D3CD5590014B822 /* color_F.png in Resources */, C6710FA72722B20000ED888F /* voip_call_more.png in Resources */, 633FEDC51D3CD5590014B822 /* call_hangup_disabled@2x.png in Resources */, @@ -4917,6 +4920,7 @@ D3F83F8E15822ABE00336684 /* PhoneMainView.m in Sources */, C6A1BB3E26E882D000540D50 /* UIChatReplyBubbleView.m in Sources */, 6377AC801BDE4069007F7625 /* UIBackToCallButton.m in Sources */, + C662D0A927EA2C5F00C02D4A /* ConferenceWaitingRoomViewModel.swift in Sources */, C6D09F43274288D4003C2173 /* PayloadType.swift in Sources */, 6308F9C51BF0DD6600D1234B /* XMLRPCHelper.m in Sources */, C6EA2F5F2754CFB0008E60F8 /* TimestampUtils.swift in Sources */, From 1e45f7d58c83e640ef7df8b9982cfe99e9a7d50b Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Tue, 22 Mar 2022 18:00:53 +0100 Subject: [PATCH 11/75] Prevent crash when toggling speaker out of call --- Classes/Swift/Voip/AudioRouteUtils.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Classes/Swift/Voip/AudioRouteUtils.swift b/Classes/Swift/Voip/AudioRouteUtils.swift index 91944d7e9..4f0158ad7 100644 --- a/Classes/Swift/Voip/AudioRouteUtils.swift +++ b/Classes/Swift/Voip/AudioRouteUtils.swift @@ -90,16 +90,16 @@ import linphonesw } static private func routeAudioTo( call: Call?, types: [AudioDeviceType]) { - let currentCall = call != nil ? call : core.currentCall != nil ? core.currentCall : core.calls[0] - 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) - } + 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 From b4d6b8c3518fbfcd112e74ebac4867d1ed7a3376 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Wed, 23 Mar 2022 09:13:00 +0100 Subject: [PATCH 12/75] Fix camera switch in swift --- .../Swift/Extensions/LinphoneCore/CoreExtensions.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift index 5de3e26d7..96d56dd10 100644 --- a/Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift +++ b/Classes/Swift/Extensions/LinphoneCore/CoreExtensions.swift @@ -30,18 +30,14 @@ extension Core { } func toggleCamera() { - - UICamSwitch.switchCamera() - /* Not working Log.i("[Core] Current camera device is \(videoDevice)") - + var switched = false videoDevicesList.forEach { - if ($0 != videoDevice && $0 != "StaticImage: Static picture") { + if (!switched && $0 != videoDevice && $0 != "StaticImage: Static picture") { Log.i("[Core] New camera device will be \($0)") try?setVideodevice(newValue: $0) - return + switched = true } } - */ } } From 5adf3d8a3fb486945ef5e180e158977c098f0f6d Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Wed, 23 Mar 2022 10:25:23 +0100 Subject: [PATCH 13/75] - Waiting room preferences - Asymetric video - Refactorisation - Conference layout picker in waiting room --- Classes/Swift/CallManager.swift | 2 +- .../Conference/{data => Data}/Duration.swift | 0 .../ScheduledConferenceData.swift | 0 .../{data => Data}/TimeZoneData.swift | 0 .../ConferenceSchedulingViewModel.swift | 0 .../ConferenceWaitingRoomViewModel.swift | 75 +++++++++++++++ .../ScheduledConferencesViewModel.swift | 0 .../ConferenceHistoryDetailsView.swift | 0 .../ConferenceSchedulingSummaryView.swift | 0 .../ConferenceSchedulingView.swift | 0 .../ConferenceWaitingRoomFragment.swift | 66 +++++++++++-- .../{views => Views}/ICSBubbleView.swift | 0 .../ScheduledConferencesCell.swift | 0 .../ScheduledConferencesView.swift | 0 .../ConferenceWaitingRoomViewModel.swift | 40 -------- .../Extensions/IOS/UIVIewExtensions.swift | 7 ++ .../LinphoneCore/ConferenceExtensions.swift | 1 + Classes/Swift/Voip/Theme/VoipTexts.swift | 2 + Classes/Swift/Voip/Theme/VoipTheme.swift | 10 +- Classes/Swift/Voip/ViewModels/CallData.swift | 6 +- .../Voip/ViewModels/CallsViewModel.swift | 2 +- .../Voip/ViewModels/ConferenceViewModel.swift | 13 +++ .../Voip/ViewModels/ControlsViewModel.swift | 9 +- .../ActiveCallOrConferenceView.swift | 8 +- .../OutgoingCallView.swift | 2 +- .../ConferenceLayoutPickerView.swift | 87 ++++++++++++++++++ .../Voip/Views/Fragments/ControlsView.swift | 34 +++---- .../images/voip_conference_audio_only.png | Bin 0 -> 1260 bytes linphone.xcodeproj/project.pbxproj | 28 ++++-- 29 files changed, 302 insertions(+), 90 deletions(-) rename Classes/Swift/Conference/{data => Data}/Duration.swift (100%) rename Classes/Swift/Conference/{data => Data}/ScheduledConferenceData.swift (100%) rename Classes/Swift/Conference/{data => Data}/TimeZoneData.swift (100%) rename Classes/Swift/Conference/{models => ViewModels}/ConferenceSchedulingViewModel.swift (100%) create mode 100644 Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift rename Classes/Swift/Conference/{models => ViewModels}/ScheduledConferencesViewModel.swift (100%) rename Classes/Swift/Conference/{views => Views}/ConferenceHistoryDetailsView.swift (100%) rename Classes/Swift/Conference/{views => Views}/ConferenceSchedulingSummaryView.swift (100%) rename Classes/Swift/Conference/{views => Views}/ConferenceSchedulingView.swift (100%) rename Classes/Swift/Conference/{views => Views}/ConferenceWaitingRoomFragment.swift (63%) rename Classes/Swift/Conference/{views => Views}/ICSBubbleView.swift (100%) rename Classes/Swift/Conference/{views => Views}/ScheduledConferencesCell.swift (100%) rename Classes/Swift/Conference/{views => Views}/ScheduledConferencesView.swift (100%) delete mode 100644 Classes/Swift/Conference/models/ConferenceWaitingRoomViewModel.swift create mode 100644 Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift create mode 100644 Resources/images/voip_conference_audio_only.png diff --git a/Classes/Swift/CallManager.swift b/Classes/Swift/CallManager.swift index 7d3a0eec4..a99fb228b 100644 --- a/Classes/Swift/CallManager.swift +++ b/Classes/Swift/CallManager.swift @@ -274,7 +274,7 @@ import AVFoundation } if (isConference) { lcallParams.videoEnabled = true - lcallParams.videoDirection = isVideo ? .SendRecv : .RecvOnly + lcallParams.videoDirection = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true ? .SendRecv : .RecvOnly } else { lcallParams.videoEnabled = isVideo } diff --git a/Classes/Swift/Conference/data/Duration.swift b/Classes/Swift/Conference/Data/Duration.swift similarity index 100% rename from Classes/Swift/Conference/data/Duration.swift rename to Classes/Swift/Conference/Data/Duration.swift diff --git a/Classes/Swift/Conference/data/ScheduledConferenceData.swift b/Classes/Swift/Conference/Data/ScheduledConferenceData.swift similarity index 100% rename from Classes/Swift/Conference/data/ScheduledConferenceData.swift rename to Classes/Swift/Conference/Data/ScheduledConferenceData.swift diff --git a/Classes/Swift/Conference/data/TimeZoneData.swift b/Classes/Swift/Conference/Data/TimeZoneData.swift similarity index 100% rename from Classes/Swift/Conference/data/TimeZoneData.swift rename to Classes/Swift/Conference/Data/TimeZoneData.swift diff --git a/Classes/Swift/Conference/models/ConferenceSchedulingViewModel.swift b/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift similarity index 100% rename from Classes/Swift/Conference/models/ConferenceSchedulingViewModel.swift rename to Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift diff --git a/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift b/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift new file mode 100644 index 000000000..246f5f905 --- /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 = .Grid + joinInProgress.value = false + isMicrophoneMuted.value = !micAuthorized() + isMuteMicrophoneEnabled.value = true + isSpeakerSelected.value = true + isVideoEnabled.value = true + 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/models/ScheduledConferencesViewModel.swift b/Classes/Swift/Conference/ViewModels/ScheduledConferencesViewModel.swift similarity index 100% rename from Classes/Swift/Conference/models/ScheduledConferencesViewModel.swift rename to Classes/Swift/Conference/ViewModels/ScheduledConferencesViewModel.swift diff --git a/Classes/Swift/Conference/views/ConferenceHistoryDetailsView.swift b/Classes/Swift/Conference/Views/ConferenceHistoryDetailsView.swift similarity index 100% rename from Classes/Swift/Conference/views/ConferenceHistoryDetailsView.swift rename to Classes/Swift/Conference/Views/ConferenceHistoryDetailsView.swift diff --git a/Classes/Swift/Conference/views/ConferenceSchedulingSummaryView.swift b/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift similarity index 100% rename from Classes/Swift/Conference/views/ConferenceSchedulingSummaryView.swift rename to Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift diff --git a/Classes/Swift/Conference/views/ConferenceSchedulingView.swift b/Classes/Swift/Conference/Views/ConferenceSchedulingView.swift similarity index 100% rename from Classes/Swift/Conference/views/ConferenceSchedulingView.swift rename to Classes/Swift/Conference/Views/ConferenceSchedulingView.swift diff --git a/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift similarity index 63% rename from Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift rename to Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift index 5a6857164..c2d1d6f12 100644 --- a/Classes/Swift/Conference/views/ConferenceWaitingRoomFragment.swift +++ b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift @@ -38,6 +38,8 @@ import linphonesw 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) @@ -46,7 +48,6 @@ import linphonesw 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 } @@ -63,10 +64,38 @@ import linphonesw } // Controls - let controlsView = ControlsView(showVideo: true) + 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 .Legacy: icon = "voip_conference_audio_only"; 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 @@ -86,25 +115,29 @@ import linphonesw CallManager.instance().terminateCall(call: call.getCobject) } } - ConferenceWaitingRoomViewModel.shared.joinInProgress.value = false + ConferenceWaitingRoomViewModel.sharedModel.joinInProgress.value = false PhoneMainView.instance().popView(self.compositeViewDescription()) } start.onClick { - ConferenceWaitingRoomViewModel.shared.joinInProgress.value = true - self.conferenceUrl.map{ CallManager.instance().startCall(addr: $0, isSas: false, isVideo: true, isConference: true) } + ConferenceWaitingRoomViewModel.sharedModel.joinInProgress.value = true + self.conferenceUrl.map{ CallManager.instance().startCall(addr: $0, isSas: false, isVideo: ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value!, isConference: true) } } - ConferenceWaitingRoomViewModel.shared.joinInProgress.readCurrentAndObserve { joining in + ConferenceWaitingRoomViewModel.sharedModel.joinInProgress.readCurrentAndObserve { joining in self.start.isEnabled = joining != true - self.localVideo.isHidden = 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 } } @@ -126,11 +159,22 @@ import linphonesw 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() - ControlsViewModel.shared.audioRoutesSelected.readCurrentAndObserve { (audioRoutesSelected) in + ConferenceWaitingRoomViewModel.sharedModel.audioRoutesSelected.readCurrentAndObserve { (audioRoutesSelected) in self.audioRoutesView!.isHidden = audioRoutesSelected != true } audioRoutesView!.alignAbove(view:controlsView,withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done() @@ -140,15 +184,17 @@ import linphonesw override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(true) - ControlsViewModel.shared.audioRoutesSelected.value = false + ConferenceWaitingRoomViewModel.sharedModel.audioRoutesSelected.value = false + ConferenceWaitingRoomViewModel.sharedModel.reset() Core.get().nativePreviewWindow = localVideo - Core.get().videoPreviewEnabled = true + 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) } diff --git a/Classes/Swift/Conference/views/ICSBubbleView.swift b/Classes/Swift/Conference/Views/ICSBubbleView.swift similarity index 100% rename from Classes/Swift/Conference/views/ICSBubbleView.swift rename to Classes/Swift/Conference/Views/ICSBubbleView.swift diff --git a/Classes/Swift/Conference/views/ScheduledConferencesCell.swift b/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift similarity index 100% rename from Classes/Swift/Conference/views/ScheduledConferencesCell.swift rename to Classes/Swift/Conference/Views/ScheduledConferencesCell.swift diff --git a/Classes/Swift/Conference/views/ScheduledConferencesView.swift b/Classes/Swift/Conference/Views/ScheduledConferencesView.swift similarity index 100% rename from Classes/Swift/Conference/views/ScheduledConferencesView.swift rename to Classes/Swift/Conference/Views/ScheduledConferencesView.swift diff --git a/Classes/Swift/Conference/models/ConferenceWaitingRoomViewModel.swift b/Classes/Swift/Conference/models/ConferenceWaitingRoomViewModel.swift deleted file mode 100644 index bf428bf79..000000000 --- a/Classes/Swift/Conference/models/ConferenceWaitingRoomViewModel.swift +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { - - var core : Core { get { Core.get() } } - static let shared = ConferenceWaitingRoomViewModel() - - let joinWithVideo = MutableLiveData() - let layout = MutableLiveData() - let joinInProgress = MutableLiveData(false) - - init () { - - } - - - -} diff --git a/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift b/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift index 46008f213..03ccbff0a 100644 --- a/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift +++ b/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift @@ -285,6 +285,13 @@ extension UIView { 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 diff --git a/Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift index 483f3bc16..986c44007 100644 --- a/Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift +++ b/Classes/Swift/Extensions/LinphoneCore/ConferenceExtensions.swift @@ -29,5 +29,6 @@ extension Conference : CustomStringConvertible { } return "" } + } diff --git a/Classes/Swift/Voip/Theme/VoipTexts.swift b/Classes/Swift/Voip/Theme/VoipTexts.swift index f8e1d2649..60e5bdafd 100644 --- a/Classes/Swift/Voip/Theme/VoipTexts.swift +++ b/Classes/Swift/Voip/Theme/VoipTexts.swift @@ -95,6 +95,8 @@ import UIKit static let conference_display_no_active_speaker = NSLocalizedString("No active speaker",comment:"") static let conference_waiting_room_start_call = NSLocalizedString("Start",comment:"") static let conference_waiting_room_cancel_call = NSLocalizedString("Cancel",comment:"") + static let conference_waiting_room_video_disabled = NSLocalizedString("Video is currently disabled",comment:"") + static let conference_scheduled = NSLocalizedString("Conferences",comment:"") static let conference_too_many_participants_for_mosaic_layout = NSLocalizedString("You can't change conference layout as there is too many participants",comment:"") static let conference_participant_paused = NSLocalizedString("(paused)",comment:"") diff --git a/Classes/Swift/Voip/Theme/VoipTheme.swift b/Classes/Swift/Voip/Theme/VoipTheme.swift index 72d6ae29e..47ddd4ca6 100644 --- a/Classes/Swift/Voip/Theme/VoipTheme.swift +++ b/Classes/Swift/Voip/Theme/VoipTheme.swift @@ -123,7 +123,8 @@ class VoipTheme { // Names & values replicated from Android 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) @@ -203,7 +204,6 @@ class VoipTheme { // Names & values replicated from Android // 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: [ @@ -268,6 +268,12 @@ class VoipTheme { // Names & values replicated from Android ], 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:[ diff --git a/Classes/Swift/Voip/ViewModels/CallData.swift b/Classes/Swift/Voip/ViewModels/CallData.swift index 988c79729..a15fa2270 100644 --- a/Classes/Swift/Voip/ViewModels/CallData.swift +++ b/Classes/Swift/Voip/ViewModels/CallData.swift @@ -88,7 +88,7 @@ class CallData { isRemotelyPaused.value = isCallRemotelyPaused() canBePaused.value = canCallBePaused() let conference = call.conference - isInRemoteConference.value = conference != nil || CallManager.getAppData(call: call.getCobject!)?.isConference == true + isInRemoteConference.value = conference != nil || isCallingAConference() if (conference != nil) { remoteConferenceSubject.value = conference?.subject != nil && (conference?.subject.count)! > 0 ? conference!.subject : VoipTexts.conference_default_title } @@ -199,4 +199,8 @@ class CallData { isPaused.value = isCallPaused() } + func isCallingAConference() -> Bool { + return CallManager.getAppData(call: call.getCobject!)?.isConference == true + } + } diff --git a/Classes/Swift/Voip/ViewModels/CallsViewModel.swift b/Classes/Swift/Voip/ViewModels/CallsViewModel.swift index 294491b9e..284868eb0 100644 --- a/Classes/Swift/Voip/ViewModels/CallsViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/CallsViewModel.swift @@ -185,6 +185,6 @@ class CallsViewModel { updateUnreadChatCount() } - + } diff --git a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift index 4a1aeb2c1..345e9f552 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift @@ -181,6 +181,19 @@ class ConferenceViewModel { conference.addDelegate(delegate: self.conferenceDelegate!) isRecording.value = conference.isRecording updateConferenceLayout(conference: conference) + + if let call = core.currentCall, CallManager.getAppData(call: call.getCobject!)?.isConference == true { // Apply waiting room preference + if (ConferenceWaitingRoomViewModel.sharedModel.isSpeakerSelected.value == true) { + ControlsViewModel.shared.forceSpeakerAudioRoute() + } else { + ControlsViewModel.shared.forceEarpieceAudioRoute() + ControlsViewModel.shared.updateUI() + } + Core.get().micEnabled = ConferenceWaitingRoomViewModel.sharedModel.isMicrophoneMuted.value != true + conference.layout = ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! + updateConferenceLayout(conference: conference) + } + } func configureConference(_ conference: Conference) { diff --git a/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift b/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift index 5ee71fd59..35d9f9036 100644 --- a/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift @@ -137,10 +137,9 @@ class ControlsViewModel { func toggleVideo() { if let conference = core.conference, conference.isIn { - if let params = try?core.createConferenceParams(conference:conference) { - let videoEnabled = conference.currentParams?.videoEnabled == true - params.videoEnabled = !videoEnabled - _ = conference.updateParams(params: params) + if let currentCall = core.currentCall, let params = try?core.createCallParams(call: currentCall) { + params.videoDirection = params.videoDirection == MediaDirection.RecvOnly ? MediaDirection.SendRecv : MediaDirection.RecvOnly + try?currentCall.update(params: params) } } else if let currentCall = core.currentCall { let state = currentCall.state @@ -159,7 +158,7 @@ class ControlsViewModel { } - private func updateUI() { + func updateUI() { updateVideoAvailable() updateVideoEnabled() updateMicState() diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index e75519c6f..8583bef83 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -65,7 +65,7 @@ import linphonesw // Controls - let controlsView = ControlsView(showVideo: true) + let controlsView = ControlsView(showVideo: true, controlsViewModel: ControlsViewModel.shared) view.addSubview(controlsView) controlsView.alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done() @@ -315,7 +315,11 @@ import linphonesw if (data?.isOutgoing.value == true || data?.isIncoming.value == true) { PhoneMainView.instance().popView(self.compositeViewDescription()) } else { - PhoneMainView.instance().changeCurrentView(self.compositeViewDescription()) + if (data!.isCallingAConference()) { + PhoneMainView.instance().pop(toView: self.compositeViewDescription()) + } else { + PhoneMainView.instance().changeCurrentView(self.compositeViewDescription()) + } } } else { PhoneMainView.instance().changeCurrentView(self.compositeViewDescription()) diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift index 31286af27..613f05eb4 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/OutgoingCallView.swift @@ -47,7 +47,7 @@ import linphonesw cancelCall.alignParentLeft(withMargin:SharedLayoutConstants.margin_call_view_side_controls_buttons).alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).done() // Controls - let controlsView = ControlsView(showVideo: false) + let controlsView = ControlsView(showVideo: false, controlsViewModel: ControlsViewModel.shared) view.addSubview(controlsView) controlsView.alignParentBottom(withMargin:SharedLayoutConstants.buttons_bottom_margin).centerX().done() diff --git a/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift new file mode 100644 index 000000000..b9c010cc0 --- /dev/null +++ b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.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 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 = .Legacy + 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 == .Legacy + } + + 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 index 58c285aa5..b091a049c 100644 --- a/Classes/Swift/Voip/Views/Fragments/ControlsView.swift +++ b/Classes/Swift/Voip/Views/Fragments/ControlsView.swift @@ -25,7 +25,7 @@ class ControlsView: UIStackView { // Layout constants static let controls_button_spacing = 5.0 - init (showVideo:Bool) { + init (showVideo:Bool, controlsViewModel:ControlsViewModel) { super.init(frame: .zero) axis = .horizontal distribution = .equalSpacing @@ -34,35 +34,35 @@ class ControlsView: UIStackView { // Mute let mute = CallControlButton(buttonTheme: VoipTheme.call_mute, onClickAction: { - ControlsViewModel.shared.toggleMuteMicrophone() + controlsViewModel.toggleMuteMicrophone() }) addArrangedSubview(mute) - ControlsViewModel.shared.isMicrophoneMuted.readCurrentAndObserve { (muted) in + controlsViewModel.isMicrophoneMuted.readCurrentAndObserve { (muted) in mute.isSelected = muted == true } - ControlsViewModel.shared.isMuteMicrophoneEnabled.readCurrentAndObserve { (enabled) in + controlsViewModel.isMuteMicrophoneEnabled.readCurrentAndObserve { (enabled) in mute.isEnabled = enabled == true } // Speaker let speaker = CallControlButton(buttonTheme: VoipTheme.call_speaker, onClickAction: { - ControlsViewModel.shared.toggleSpeaker() + controlsViewModel.toggleSpeaker() }) addArrangedSubview(speaker) - ControlsViewModel.shared.isSpeakerSelected.readCurrentAndObserve { (selected) in + controlsViewModel.isSpeakerSelected.readCurrentAndObserve { (selected) in speaker.isSelected = selected == true } // Audio routes let routes = CallControlButton(buttonTheme: VoipTheme.call_audio_route, onClickAction: { - ControlsViewModel.shared.toggleRoutesMenu() + controlsViewModel.toggleRoutesMenu() }) addArrangedSubview(routes) - ControlsViewModel.shared.audioRoutesSelected.readCurrentAndObserve { (selected) in + controlsViewModel.audioRoutesSelected.readCurrentAndObserve { (selected) in routes.isSelected = selected == true } - ControlsViewModel.shared.audioRoutesEnabled.readCurrentAndObserve { (routesEnabled) in + controlsViewModel.audioRoutesEnabled.readCurrentAndObserve { (routesEnabled) in speaker.isHidden = routesEnabled == true routes.isHidden = !speaker.isHidden } @@ -71,11 +71,11 @@ class ControlsView: UIStackView { if (showVideo) { let video = CallControlButton(buttonTheme: VoipTheme.call_video, onClickAction: { if AVCaptureDevice.authorizationStatus(for: .video) == .authorized { - ControlsViewModel.shared.toggleVideo() + controlsViewModel.toggleVideo() } else { AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in if granted { - ControlsViewModel.shared.toggleVideo() + controlsViewModel.toggleVideo() } else { VoipDialog(message:VoipTexts.camera_required_for_video).show() } @@ -83,15 +83,15 @@ class ControlsView: UIStackView { } }) addArrangedSubview(video) - video.showActivityIndicatorDataSource = ControlsViewModel.shared.isVideoUpdateInProgress - ControlsViewModel.shared.isVideoEnabled.readCurrentAndObserve { (selected) in + video.showActivityIndicatorDataSource = controlsViewModel.isVideoUpdateInProgress + controlsViewModel.isVideoEnabled.readCurrentAndObserve { (selected) in video.isSelected = selected == true } - ControlsViewModel.shared.isVideoAvailable.readCurrentAndObserve { (available) in - video.isEnabled = available == true && ControlsViewModel.shared.isVideoUpdateInProgress.value != true + controlsViewModel.isVideoAvailable.readCurrentAndObserve { (available) in + video.isEnabled = available == true && controlsViewModel.isVideoUpdateInProgress.value != true } - ControlsViewModel.shared.isVideoUpdateInProgress.readCurrentAndObserve { (updateInProgress) in - video.isEnabled = updateInProgress != true && ControlsViewModel.shared.isVideoAvailable.value == true + controlsViewModel.isVideoUpdateInProgress.readCurrentAndObserve { (updateInProgress) in + video.isEnabled = updateInProgress != true && controlsViewModel.isVideoAvailable.value == true } } diff --git a/Resources/images/voip_conference_audio_only.png b/Resources/images/voip_conference_audio_only.png new file mode 100644 index 0000000000000000000000000000000000000000..fd57a3f27e6395ad6cbf7ed19ea01cc010864ba9 GIT binary patch literal 1260 zcmVEX>4Tx04R}tkv&MmKp2MKri!9f2Rn#3WT?7W5EXIMDionYs1;guFuC*(nlvOS zE{=k0!NH%!s)LKOt`4q(Aov5~=;Wm6A|-y86k5c1aCZ;yeecWNcYshUG0kcl2Q=L_ zlku3C&a8?ZuMp6UURpk71x=7pPYq=lj@k>L)<(8MxA${&EeN`6RvC z)FMYf-!^b@-PDvl;Bp5Tcrs*DcBLRqA(sQ*&*+;nK>sb!wdVD$agNgmAVs}O+yDoM zz(}65*L~jI+1j^%Ya0Fg0T}OcgK~Phpa1{>24YJ`L;z<1l>oV$G+_|{000SaNLh0L z01ejw01ejxLMWSf00007bV*G`2j&6`4g&@7(F_;>00QVqL_t(|+U?xWOB7)o$MMg1 zW^MP7WCev1MhFSsN`fFdgaD3kkHlgz`+F`t18ALyVD9awg8(k^mgSKZtD*+by1bX7+(SHFvl9N}l`!{^ z|DZ9Rth8OX1%g+Zp}v_?cyo@ob?&u{)np(qG9#&HfmtCZJ31;tu9*`^kEl#Hx1Npr zJw{_ZSt%;dRrHpKnLW|k84XO3T-H7UplbcV44m=(t;0190CxO7QX0tVzywjTB83NL zMEo8^p`Khb4~BfoA_id)Vh{!)24N6l5C$OzAqHU(@^27Plg%G1koEf;BM?unxhO%( zBJL9a3mcQgeZL2BY-0!F!kATtOE+| z64Aq%dw0f*@pp^%nR+JO{{=v*lC^`Rx4EfvQ}@vY{je4bH&-k&h=<10J)cBy z^Ve`ypjQ~g>nKCL=R@jNuQ^ub*l^$IW(wllueAB`>t@{486@Box%{DR3dammY$8*6 zao#VK;~n=951FONMcKCcQh?ndxIOc!-W-E?Xky~V8i4EELum5^5$LyqjasnGAYKMh z^kL((iq{b*{J6CL!2%!}OIDjQ-Ji3wv!(t6W<*3pL_|bHL_|bHL_|bHL_|bHf%X$c W+0(d@F1m350000 Date: Fri, 25 Mar 2022 07:44:33 +0100 Subject: [PATCH 14/75] Set content mode for video windows to scaleAspectFill --- .../Swift/Conference/Views/ConferenceWaitingRoomFragment.swift | 2 +- .../Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift index c2d1d6f12..f3da22735 100644 --- a/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift +++ b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift @@ -145,7 +145,7 @@ import linphonesw // localVideo view localVideo.layer.cornerRadius = center_view_corner_radius localVideo.clipsToBounds = true - localVideo.contentMode = .scaleAspectFit + 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() diff --git a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift index 960dd961d..d0d3a02d6 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift @@ -92,7 +92,7 @@ class ConferenceParticipantDeviceData { func setVideoView(view:UIView) { Log.i("[Conference Participant Device] Setting textureView \(view) for participant \(participantDevice)") - view.contentMode = .scaleAspectFit + view.contentMode = .scaleAspectFill if (isMe) { core.usePreviewWindow(yesno: false) core.nativePreviewWindow = view From 4fb127c135ac384f9fbf0e654187933d51c2cdbe Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 25 Mar 2022 18:49:48 +0100 Subject: [PATCH 15/75] Fix history details --- .../Views/ConferenceHistoryDetailsView.swift | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Classes/Swift/Conference/Views/ConferenceHistoryDetailsView.swift b/Classes/Swift/Conference/Views/ConferenceHistoryDetailsView.swift index 2c38a8c47..4ca70b470 100644 --- a/Classes/Swift/Conference/Views/ConferenceHistoryDetailsView.swift +++ b/Classes/Swift/Conference/Views/ConferenceHistoryDetailsView.swift @@ -27,7 +27,7 @@ import linphonesw let participantsListTableView = UITableView() let conectionsListTableView = UITableView() - let participantsLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_participants_list) + let participantsLabel = StyledLabel(VoipTheme.conference_scheduling_font, " "+VoipTexts.conference_schedule_participants_list) let datePicker = StyledDatePicker(pickerMode: .date, readOnly:true) let timePicker = StyledDatePicker(pickerMode: .time, readOnly:true) @@ -67,7 +67,6 @@ import linphonesw contentView.addSubview(schedulingStack) schedulingStack.alignParentTop(withMargin: 2*form_margin).matchParentSideBorders(insetedByDx: form_margin).done() - let scheduleForm = UIView() schedulingStack.addArrangedSubview(scheduleForm) scheduleForm.matchParentSideBorders().done() @@ -93,15 +92,13 @@ import linphonesw let timeLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_time) rightColumn.addSubview(timeLabel) - timeLabel.alignParentLeft().alignUnder(view: datePicker,withMargin: form_margin).done() + 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() // Participants @@ -120,10 +117,9 @@ import linphonesw } participantsListTableView.separatorStyle = .singleLine participantsListTableView.separatorColor = VoipTheme.light_grey_color - - - - // Goto chat + + // Goto chat - v2 + /* let chatButton = FormButton(title: VoipTexts.conference_go_to_chat.uppercased(), backgroundStateColors: VoipTheme.primary_colors_background) contentView.addSubview(chatButton) chatButton.onClick { @@ -132,13 +128,18 @@ import linphonesw } 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) { - // TODO when available : create view model from the conference that should be retreivable via call log + let log = CallLog.getSwiftObject(cObject: callLog) + if let conferenceInfo = log.conferenceInfo { + self.conferenceData = ScheduledConferenceData(conferenceInfo: conferenceInfo) + } + //self.callLog = log } From 5f7b4f3bcd78ce893c3a5b52415e339b27159fd8 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Thu, 31 Mar 2022 06:15:50 +0200 Subject: [PATCH 16/75] Clear mutable observers to avoid display issues when cell are recycled --- .../Views/Fragments/Conference/VoipGridParticipantCell.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift index 79ebb474c..4b7a7508b 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift @@ -42,11 +42,13 @@ class VoipGridParticipantCell: UICollectionViewCell { 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 } + data.videoEnabled.clearObservers() data.videoEnabled.readCurrentAndObserve { (videoEnabled) in self.updateBackground() if (videoEnabled == true) { @@ -67,6 +69,7 @@ class VoipGridParticipantCell: UICollectionViewCell { self.displayName.text = displayName } } + data.activeSpeaker.clearObservers() data.activeSpeaker.readCurrentAndObserve { (active) in if (active == true) { self.layer.borderWidth = 2 From 0ceb1f9043982565f6948fc3b5645584db903378 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 15 Apr 2022 11:40:37 +0200 Subject: [PATCH 17/75] Fix conference pausing on remote when pausing locally --- .../Swift/Voip/ViewModels/ConferenceViewModel.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift index 345e9f552..afcd9002b 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift @@ -86,12 +86,16 @@ class ConferenceViewModel { } }, onParticipantDeviceLeft: { (conference: Conference, device: ParticipantDevice) in - Log.i("[Conference] onParticipantDeviceJoined Entered conference") - self.isConferenceLocallyPaused.value = true + if (conference.isMe(uri: device.address!)) { + Log.i("[Conference] Left conference") + self.isConferenceLocallyPaused.value = true + } }, onParticipantDeviceJoined: { (conference: Conference, device: ParticipantDevice) in - Log.i("[Conference] onParticipantDeviceJoined Entered conference") - self.isConferenceLocallyPaused.value = false + 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)") From 5c0092351fd89a012a3d89dadf267df1fe73d233 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 15 Apr 2022 11:42:07 +0200 Subject: [PATCH 18/75] Renew VFS key if unable to find it --- Classes/Swift/VFSUtil.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Classes/Swift/VFSUtil.swift b/Classes/Swift/VFSUtil.swift index 5047fd21c..6dc09d5ae 100644 --- a/Classes/Swift/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) From 1d2d6f76055781a52a125e2c21f5bfcb369bdc00 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Mon, 25 Apr 2022 17:44:06 +0200 Subject: [PATCH 19/75] Use callkit to Pause/Resume --- Classes/Swift/Voip/ViewModels/CallData.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Classes/Swift/Voip/ViewModels/CallData.swift b/Classes/Swift/Voip/ViewModels/CallData.swift index a15fa2270..14dbb09ea 100644 --- a/Classes/Swift/Voip/ViewModels/CallData.swift +++ b/Classes/Swift/Voip/ViewModels/CallData.swift @@ -192,9 +192,14 @@ class CallData { func togglePause() { if (isCallPaused()) { - try?call.resume() + CallsViewModel.shared.callsData.value?.forEach { + if ($0.canCallBePaused()) { + CallManager.instance().setHeld(call: $0.call, hold: true) + } + } + CallManager.instance().setHeld(call: call, hold: false) } else { - try?call.pause() + CallManager.instance().setHeld(call: call, hold: true) } isPaused.value = isCallPaused() } From 08ce19533b40bdab16704a9f2df6232c094ed69f Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 29 Apr 2022 11:12:19 +0200 Subject: [PATCH 20/75] Default recording device to device microphone when in a call --- Classes/Swift/Voip/AudioRouteUtils.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Swift/Voip/AudioRouteUtils.swift b/Classes/Swift/Voip/AudioRouteUtils.swift index 4f0158ad7..7f888a7af 100644 --- a/Classes/Swift/Voip/AudioRouteUtils.swift +++ b/Classes/Swift/Voip/AudioRouteUtils.swift @@ -85,7 +85,7 @@ import linphonesw 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: break + default: applyAudioRouteChange(call:call,types: [AudioDeviceType.Microphone], output:false) } } From 64a290ac4a2a991df72881119776f68a76ae7610 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Mon, 9 May 2022 16:11:11 +0200 Subject: [PATCH 21/75] SDK update & Layout API adjustement on Video Conference --- .../Views/ConferenceWaitingRoomFragment.swift | 2 +- .../Voip/ViewModels/ConferenceViewModel.swift | 24 +++--- ...ipConferenceDisplayModeSelectionView.swift | 4 +- .../ConferenceLayoutPickerView.swift | 4 +- Podfile | 2 +- linphone.xcodeproj/project.pbxproj | 79 +++++++++++++------ .../xcshareddata/xcschemes/linphone.xcscheme | 4 +- 7 files changed, 79 insertions(+), 40 deletions(-) diff --git a/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift index f3da22735..4962b173a 100644 --- a/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift +++ b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift @@ -80,7 +80,7 @@ import linphonesw switch (layout!) { case .Grid: icon = "voip_conference_mosaic"; break case .ActiveSpeaker: icon = "voip_conference_active_speaker"; break - case .Legacy: icon = "voip_conference_audio_only"; break + // Todo audio only case .Legacy: icon = "voip_conference_audio_only"; break } layoutPicker.applyTintedIcons(tintedIcons: [UIButton.State.normal.rawValue : TintableIcon(name: icon ,tintColor: LightDarkColor(.white,.white))]) } diff --git a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift index afcd9002b..bdd6f229b 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift @@ -194,7 +194,7 @@ class ConferenceViewModel { ControlsViewModel.shared.updateUI() } Core.get().micEnabled = ConferenceWaitingRoomViewModel.sharedModel.isMicrophoneMuted.value != true - conference.layout = ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! + changeLayout(layout: ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value!) updateConferenceLayout(conference: conference) } @@ -239,19 +239,25 @@ class ConferenceViewModel { func changeLayout(layout: ConferenceLayout) { Log.i("[Conference] Trying to change conference layout to $layout") - if let conference = conference.value { - conference.layout = layout - updateConferenceLayout(conference: conference) + if let conference = conference.value, let call = conference.call, let params = try?call.core?.createCallParams(call: call) { + params.videoEnabled = true // TODO AUdioLonly layout != ConferenceDisplayMode.AUDIO_ONLY + params.conferenceVideoLayout = layout + try?call.update(params: params) + conferenceDisplayMode.value = layout + let list = sortDevicesDataList(devices: conferenceParticipantDevices.value!) + conferenceParticipantDevices.value = list } else { - Log.e("[Conference] Conference is null in ConferenceViewModel") + Log.e("[Conference] Conference or Call Or Call Params is null in ConferenceViewModel") } } private func updateConferenceLayout(conference: Conference) { - conferenceDisplayMode.value = conference.layout == .Legacy ? .Grid : conference.layout - let list = sortDevicesDataList(devices: conferenceParticipantDevices.value!) - conferenceParticipantDevices.value = list - Log.i("[Conference] Conference current layout is: \(conference.layout)") + if let call = conference.call, let params = call.params { + conferenceDisplayMode.value = params.conferenceVideoLayout + let list = sortDevicesDataList(devices: conferenceParticipantDevices.value!) + conferenceParticipantDevices.value = list + Log.i("[Conference] Conference current layout is: \(conferenceDisplayMode.value)") + } } diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift index 42a694bbd..f4de96559 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift @@ -54,14 +54,14 @@ import linphonesw cell.selectionStyle = .none if (indexPath.row == 0) { cell.setOption(title: VoipTexts.conference_display_mode_mosaic, onSelectAction: { - ConferenceViewModel.shared.conference.value?.layout = .Grid + 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.conference.value?.layout = .ActiveSpeaker + 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 diff --git a/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift index b9c010cc0..42807ee37 100644 --- a/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift +++ b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift @@ -57,7 +57,7 @@ class ConferenceLayoutPickerView: UIStackView { 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 = .Legacy + ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value = .Grid ConferenceWaitingRoomViewModel.sharedModel.showLayoutPicker.value = false }) audioOnly.applyTintedIcons(tintedIcons: [UIButton.State.normal.rawValue : TintableIcon(name: "voip_conference_audio_only" ,tintColor: LightDarkColor(.white,.white))]) @@ -66,7 +66,7 @@ class ConferenceLayoutPickerView: UIStackView { ConferenceWaitingRoomViewModel.sharedModel.joinLayout.readCurrentAndObserve { layout in grid.isSelected = layout == .Grid activeSpeaker.isSelected = layout == .ActiveSpeaker - audioOnly.isSelected = layout == .Legacy + audioOnly.isSelected = layout == .Grid } let padding2 = UIView() diff --git a/Podfile b/Podfile index c6dff2350..f8a427711 100644 --- a/Podfile +++ b/Podfile @@ -5,7 +5,7 @@ source "https://github.com/CocoaPods/Specs.git" def all_pods if ENV['PODFILE_PATH'].nil? - pod 'linphone-sdk', '5.2.0-alpha.61+5e27e59a' + pod 'linphone-sdk', '5.2.0-alpha.149+074802f6' else pod 'linphone-sdk', :path => ENV['PODFILE_PATH'] # local sdk end diff --git a/linphone.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj index aa112396e..519181441 100644 --- a/linphone.xcodeproj/project.pbxproj +++ b/linphone.xcodeproj/project.pbxproj @@ -609,7 +609,7 @@ 63E27A321C4FECD000D332AE /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 63E27A311C4FECD000D332AE /* LaunchScreen.xib */; }; 63E27A521C50EDB000D332AE /* hold.mkv in Resources */ = {isa = PBXBuildFile; fileRef = 63E27A511C50EB2700D332AE /* hold.mkv */; }; 63E59A3F1ADE70D900646FB3 /* InAppProductsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 63E59A3E1ADE70D900646FB3 /* InAppProductsManager.m */; }; - 63E802DB1C625AEF000D5509 /* (null) in Resources */ = {isa = PBXBuildFile; }; + 63E802DB1C625AEF000D5509 /* BuildFile 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 */; }; 63FB30351A680E73008CA393 /* UIRoundedImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 63FB30341A680E73008CA393 /* UIRoundedImageView.m */; }; @@ -2530,7 +2530,7 @@ path = LinphoneUI; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA /* CustomTemplate */ = { + 29B97314FDCFA39411CA2CEA = { isa = PBXGroup; children = ( 8C23BCB71D82AAC3005F19BB /* linphone.entitlements */, @@ -3942,7 +3942,7 @@ fr, hu, ); - mainGroup = 29B97314FDCFA39411CA2CEA /* CustomTemplate */; + mainGroup = 29B97314FDCFA39411CA2CEA; productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; projectRoot = ""; @@ -4009,7 +4009,7 @@ 633FEEE01D3CD55A0014B822 /* numpad_8_over~ipad@2x.png in Resources */, C6710FA42722B20000ED888F /* voip_single_contact_avatar.png in Resources */, 633FEDDC1D3CD5590014B822 /* call_start_body_disabled~ipad.png in Resources */, - 63E802DB1C625AEF000D5509 /* (null) in Resources */, + 63E802DB1C625AEF000D5509 /* BuildFile in Resources */, 633FEE2E1D3CD5590014B822 /* color_F.png in Resources */, C6710FA72722B20000ED888F /* voip_call_more.png in Resources */, 633FEDC51D3CD5590014B822 /* call_hangup_disabled@2x.png in Resources */, @@ -4814,25 +4814,58 @@ "${BUILT_PRODUCTS_DIR}/IQKeyboardManager/IQKeyboardManager.framework", "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework", "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/bctoolbox-ios.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/bctoolbox-tester.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/bctoolbox.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/belcard.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/belle-sip.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/belr.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/lime.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/limetester.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/linphone.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/linphonetester.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/mediastreamer2.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/msamr.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/mscodec2.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/msopenh264.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/mssilk.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/mswebrtc.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/msx264.framework", - "${PODS_ROOT}/linphone-sdk/linphone-sdk/apple-darwin/Frameworks/ortp.framework", "${BUILT_PRODUCTS_DIR}/linphone-sdk/linphonesw.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/bctoolbox-ios.framework/bctoolbox-ios", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/bctoolbox-tester.framework/bctoolbox-tester", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/bctoolbox.framework/bctoolbox", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/belcard.framework/belcard", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/belle-sip.framework/belle-sip", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/belr.framework/belr", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/lime.framework/lime", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/limetester.framework/limetester", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/linphone.framework/linphone", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/linphonetester.framework/linphonetester", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/mediastreamer2.framework/mediastreamer2", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/msamr.framework/msamr", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/mscodec2.framework/mscodec2", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/msopenh264.framework/msopenh264", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/mssilk.framework/mssilk", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/mswebrtc.framework/mswebrtc", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/msx264.framework/msx264", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/ortp.framework/ortp", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/bctoolbox-ios.framework/bctoolbox-ios", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/bctoolbox-tester.framework/bctoolbox-tester", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/bctoolbox.framework/bctoolbox", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/belcard.framework/belcard", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/belle-sip.framework/belle-sip", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/belr.framework/belr", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/lime.framework/lime", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/limetester.framework/limetester", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/linphone.framework/linphone", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/linphonetester.framework/linphonetester", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/mediastreamer2.framework/mediastreamer2", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/msamr.framework/msamr", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/mscodec2.framework/mscodec2", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/msopenh264.framework/msopenh264", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/mssilk.framework/mssilk", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/mswebrtc.framework/mswebrtc", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/msx264.framework/msx264", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/ortp.framework/ortp", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/bctoolbox.framework/bctoolbox", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/belcard.framework/belcard", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/belle-sip.framework/belle-sip", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/belr.framework/belr", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/lime.framework/lime", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/linphone.framework/linphone", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/mediastreamer2.framework/mediastreamer2", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/msamr.framework/msamr", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/mscodec2.framework/mscodec2", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/msopenh264.framework/msopenh264", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/mssilk.framework/mssilk", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/mswebrtc.framework/mswebrtc", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/msx264.framework/msx264", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/app-extension/ortp.framework/ortp", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/basic-frameworks/bctoolbox-ios.framework/bctoolbox-ios", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( @@ -4840,6 +4873,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IQKeyboardManager.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/linphonesw.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bctoolbox-ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bctoolbox-tester.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bctoolbox.framework", @@ -4858,7 +4892,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/mswebrtc.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/msx264.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ortp.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/linphonesw.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/linphone.xcodeproj/xcshareddata/xcschemes/linphone.xcscheme b/linphone.xcodeproj/xcshareddata/xcschemes/linphone.xcscheme index cff3f0053..a28d28254 100644 --- a/linphone.xcodeproj/xcshareddata/xcschemes/linphone.xcscheme +++ b/linphone.xcodeproj/xcshareddata/xcschemes/linphone.xcscheme @@ -65,8 +65,8 @@ Date: Mon, 9 May 2022 16:30:22 +0200 Subject: [PATCH 22/75] Fix video toggling whilst in conf --- .../Voip/ViewModels/ControlsViewModel.swift | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift b/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift index 35d9f9036..fe9d9cfdd 100644 --- a/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift @@ -136,22 +136,24 @@ class ControlsViewModel { } func toggleVideo() { - if let conference = core.conference, conference.isIn { - if let currentCall = core.currentCall, let params = try?core.createCallParams(call: currentCall) { - params.videoDirection = params.videoDirection == MediaDirection.RecvOnly ? MediaDirection.SendRecv : MediaDirection.RecvOnly - try?currentCall.update(params: params) - } - } else if let currentCall = core.currentCall { - 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() + 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() + } } } } From cc472ef0f93c3fed21b1230a96e372af5a63bcb9 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Mon, 9 May 2022 17:56:09 +0200 Subject: [PATCH 23/75] Disable AudioOnly layout chooser for now --- .../Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift index 42807ee37..c95241313 100644 --- a/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift +++ b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift @@ -66,7 +66,7 @@ class ConferenceLayoutPickerView: UIStackView { ConferenceWaitingRoomViewModel.sharedModel.joinLayout.readCurrentAndObserve { layout in grid.isSelected = layout == .Grid activeSpeaker.isSelected = layout == .ActiveSpeaker - audioOnly.isSelected = layout == .Grid + audioOnly.isSelected = false // Todo when doing auioonly layout == .Grid } let padding2 = UIView() From c844b36a47042bbbf89b5b94e58706e2befc9cea Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Mon, 9 May 2022 18:04:00 +0200 Subject: [PATCH 24/75] Update video button state in conference --- Classes/Swift/Voip/ViewModels/ControlsViewModel.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift b/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift index fe9d9cfdd..0a6dea9bc 100644 --- a/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/ControlsViewModel.swift @@ -208,10 +208,10 @@ class ControlsViewModel { } func isVideoCallOrConferenceActive() -> Bool { - if let conference = core.conference, conference.isIn { - return conference.currentParams?.videoEnabled == true + if let currentCall = core.currentCall, let params = currentCall.params { + return params.videoEnabled && (currentCall.conference == nil || params.videoDirection == MediaDirection.SendRecv) } else { - return core.currentCall?.currentParams?.videoEnabled == true + return false } } From 3bdab389f181b0e1f40f8fe62d1b548eaf38ea1f Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Mon, 9 May 2022 18:58:34 +0200 Subject: [PATCH 25/75] Update video window visibility --- .../Views/Fragments/Conference/VoipGridParticipantCell.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift index 4b7a7508b..fe26dfdfe 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift @@ -47,14 +47,18 @@ class VoipGridParticipantCell: UICollectionViewCell { 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() From cf9cfe7b35a7316f1dca8840adf271ae001e3cf5 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Tue, 10 May 2022 10:07:23 +0200 Subject: [PATCH 26/75] Various update on video conference --- Classes/LinphoneManager.m | 7 +++++++ Classes/Swift/CallManager.swift | 1 + .../Swift/Extensions/IOS/UIVIewExtensions.swift | 8 ++++++++ .../VoipActiveSpeakerParticipantCell.swift | 16 ++++++++++++++++ .../Conference/VoipGridParticipantCell.swift | 2 +- Resources/assistant_external_sip.rc | 1 + Resources/assistant_linphone_create.rc | 2 ++ Resources/assistant_linphone_existing.rc | 2 ++ Resources/linphonerc | 1 + Resources/linphonerc-factory | 1 + 10 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Classes/LinphoneManager.m b/Classes/LinphoneManager.m index a6821ad70..f0032fdfe 100644 --- a/Classes/LinphoneManager.m +++ b/Classes/LinphoneManager.m @@ -495,6 +495,11 @@ static int check_should_migrate_images(void *data, int argc, char **argv, char * 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; @@ -1197,6 +1202,8 @@ static void linphone_iphone_is_composing_received(LinphoneCore *lc, LinphoneChat [NSNotificationCenter.defaultCenter postNotificationName:kLinphoneCoreUpdate object:LinphoneManager.instance userInfo:dict]; + + } static BOOL libStarted = FALSE; diff --git a/Classes/Swift/CallManager.swift b/Classes/Swift/CallManager.swift index a99fb228b..444fc8617 100644 --- a/Classes/Swift/CallManager.swift +++ b/Classes/Swift/CallManager.swift @@ -275,6 +275,7 @@ import AVFoundation if (isConference) { lcallParams.videoEnabled = true lcallParams.videoDirection = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true ? .SendRecv : .RecvOnly + lcallParams.conferenceVideoLayout = ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! } else { lcallParams.videoEnabled = isVideo } diff --git a/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift b/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift index 03ccbff0a..edbb118be 100644 --- a/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift +++ b/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift @@ -39,6 +39,14 @@ extension UIView { 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) diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift index 02c2f384f..4e74b6b5b 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift @@ -42,17 +42,23 @@ class VoipActiveSpeakerParticipantCell: UICollectionViewCell { 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() @@ -63,6 +69,14 @@ class VoipActiveSpeakerParticipantCell: UICollectionViewCell { self.displayName.text = displayName } } + data.activeSpeaker.clearObservers() + data.activeSpeaker.readCurrentAndObserve { (active) in + if (active == true) { + self.layer.borderWidth = 2 + } else { + self.layer.borderWidth = 0 + } + } } } } @@ -85,6 +99,7 @@ class VoipActiveSpeakerParticipantCell: UICollectionViewCell { super.init(frame:.zero) layer.cornerRadius = corner_radius clipsToBounds = true + layer.borderColor = VoipTheme.primary_color.cgColor contentView.addSubview(videoView) videoView.matchParentDimmensions().done() @@ -113,6 +128,7 @@ class VoipActiveSpeakerParticipantCell: UICollectionViewCell { pauseLabel.toRightOf(displayName).alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done() contentView.matchParentDimmensions().done() + makeHeightMatchWidth().done() } required init?(coder: NSCoder) { diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift index fe26dfdfe..82aa1e10f 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift @@ -84,7 +84,7 @@ class VoipGridParticipantCell: UICollectionViewCell { } } } - + func updateBackground() { if let data = participantData { if (data.isInConference.value != true) { 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 434a19a72..6b8ddec91 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
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/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 94f461b92..999887c72 100644 --- a/Resources/linphonerc-factory +++ b/Resources/linphonerc-factory @@ -36,6 +36,7 @@ linphone_specs=groupchat,lime version_check_url_root=https://download.linphone.org/releases prefer_basic_chat_room=-1 + [sound] eq_location=mic From 88713cf1db03dda95b60857c4b33bc1b50f41bc9 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Tue, 10 May 2022 16:10:20 +0200 Subject: [PATCH 27/75] Update log message --- Classes/Swift/Voip/AudioRouteUtils.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Swift/Voip/AudioRouteUtils.swift b/Classes/Swift/Voip/AudioRouteUtils.swift index 7f888a7af..0e1c5cc68 100644 --- a/Classes/Swift/Voip/AudioRouteUtils.swift +++ b/Classes/Swift/Voip/AudioRouteUtils.swift @@ -39,7 +39,7 @@ import linphonesw var found = false core.audioDevices.forEach { (audioDevice) in - Log.i("[Audio Route Helper] cdes [\(audioDevice.deviceName)] [\(audioDevice.type)] [\(audioDevice.capabilities)] ") + Log.i("[Audio Route Helper] registered coe audio devices are : [\(audioDevice.deviceName)] [\(audioDevice.type)] [\(audioDevice.capabilities)] ") } core.audioDevices.forEach { (audioDevice) in From 2264103eeec9adcd8c369be431265b0d74128861 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Wed, 11 May 2022 09:49:48 +0200 Subject: [PATCH 28/75] Fix active spearker detection method with API --- .../Voip/ViewModels/ConferenceViewModel.swift | 1 + .../VoipConferenceActiveSpeakerView.swift | 42 ++++++++----------- .../Conference/VoipConferenceGridView.swift | 9 ++++ 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift index bdd6f229b..8b57549d3 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift @@ -60,6 +60,7 @@ class ConferenceViewModel { 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 diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift index a1683ee9e..f9d6b6756 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift @@ -46,7 +46,6 @@ class VoipConferenceActiveSpeakerView: UIView, UICollectionViewDataSource, UICol 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 activeSpeakerMonitorTimer : Timer? = nil var grid : UICollectionView @@ -60,7 +59,7 @@ class VoipConferenceActiveSpeakerView: UIView, UICollectionViewDataSource, UICol duration.conference = model.conference.value self.remotelyRecording.isRemotelyRecorded = model.isRemotelyRecorded model.conferenceParticipantDevices.readCurrentAndObserve { (_) in - self.grid.reloadData() + self.reloadData() } model.isConferenceLocallyPaused.readCurrentAndObserve { (paused) in self.pauseCallButtons.forEach { @@ -73,34 +72,29 @@ class VoipConferenceActiveSpeakerView: UIView, UICollectionViewDataSource, UICol } } Core.get().nativeVideoWindow = self.activeSpeakerVideoView - activeSpeakerMonitorTimer?.invalidate() - activeSpeakerMonitorTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in - var thereIsAnActiveSpeaker = false - model.conferenceParticipantDevices.value?.forEach { (data) in - if (data.activeSpeaker.value == true) { - thereIsAnActiveSpeaker = true - data.participantDevice.address.map { - self.activeSpeakerAvatar.isHidden = false - self.activeSpeakerAvatar.fillFromAddress(address: $0) - self.activeSpeakerDisplayName.text = $0.addressBookEnhancedDisplayName() - } - self.activeSpeakerVideoView.isHidden = data.videoEnabled.value != true - return - } - } - if (!thereIsAnActiveSpeaker) { - self.activeSpeakerAvatar.isHidden = true - self.activeSpeakerVideoView.isHidden = true - self.activeSpeakerDisplayName.text = VoipTexts.conference_display_no_active_speaker + 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 } - } else { - activeSpeakerMonitorTimer?.invalidate() } - self.grid.reloadData() + self.reloadData() } } + + func reloadData() { + if (self.isHidden || conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .ActiveSpeaker) { + return + } + self.grid.reloadData() + } init() { diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift index e9dd7883f..ac0773418 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift @@ -174,6 +174,9 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi // UICollectionView related delegates func reloadData() { + if (self.isHidden || conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .Grid) { + return + } computeCellSize() self.grid.reloadData() DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -185,6 +188,12 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi } } + func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + let gcell = (cell as! VoipGridParticipantCell) + gcell.participantData?.participantDevice.nativeVideoWindowId = nil + gcell.participantData?.clearObservers() + } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return inter_cell } From 8c8825790d437c2b797387cd0aa11044edfb4435 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Wed, 11 May 2022 09:50:12 +0200 Subject: [PATCH 29/75] SDK 5.2.0-alpha.153+9c69668a --- Podfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile b/Podfile index f8a427711..9eac562db 100644 --- a/Podfile +++ b/Podfile @@ -5,7 +5,7 @@ source "https://github.com/CocoaPods/Specs.git" def all_pods if ENV['PODFILE_PATH'].nil? - pod 'linphone-sdk', '5.2.0-alpha.149+074802f6' + pod 'linphone-sdk', '5.2.0-alpha.153+9c69668a' else pod 'linphone-sdk', :path => ENV['PODFILE_PATH'] # local sdk end From e0af3af18b2f9af26e481feb76123f5266b99da1 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Wed, 11 May 2022 10:36:20 +0200 Subject: [PATCH 30/75] Various conference adjustments --- .../ConferenceParticipantDeviceData.swift | 2 +- .../VoipConferenceActiveSpeakerView.swift | 15 ++++++++++++--- .../Conference/VoipConferenceGridView.swift | 4 ++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift index d0d3a02d6..0affb76b7 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift @@ -97,7 +97,7 @@ class ConferenceParticipantDeviceData { core.usePreviewWindow(yesno: false) core.nativePreviewWindow = view } else { - participantDevice.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque()) + participantDevice.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passUnretained(view).toOpaque()) } } } diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift index f9d6b6756..09d1d4c9b 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift @@ -90,9 +90,6 @@ class VoipConferenceActiveSpeakerView: UIView, UICollectionViewDataSource, UICol } func reloadData() { - if (self.isHidden || conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .ActiveSpeaker) { - return - } self.grid.reloadData() } @@ -212,6 +209,9 @@ class VoipConferenceActiveSpeakerView: UIView, UICollectionViewDataSource, UICol 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() + } } } @@ -219,6 +219,12 @@ class VoipConferenceActiveSpeakerView: UIView, UICollectionViewDataSource, UICol // UICollectionView related delegates + func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { + let gcell = (cell as! VoipActiveSpeakerParticipantCell) + gcell.participantData?.participantDevice.nativeVideoWindowId = nil + gcell.participantData?.clearObservers() + } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return inter_cell } @@ -230,6 +236,9 @@ class VoipConferenceActiveSpeakerView: UIView, UICollectionViewDataSource, UICol } 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 } diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift index ac0773418..b4c93bfee 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift @@ -175,6 +175,7 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi func reloadData() { if (self.isHidden || conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .Grid) { + self.grid.reloadData() return } computeCellSize() @@ -205,6 +206,9 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + if (self.isHidden || conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .Grid) { + return 0 + } guard let participantsCount = conferenceViewModel?.conferenceParticipantDevices.value?.count else { return .zero } From e9a960ec303600637ae42f92cd0eb8b066c0ea37 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Wed, 11 May 2022 10:37:36 +0200 Subject: [PATCH 31/75] SDK upadte + 4.7 build2 --- linphone.xcodeproj/project.pbxproj | 72 +++++++++++++++--------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/linphone.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj index 519181441..92c00876f 100644 --- a/linphone.xcodeproj/project.pbxproj +++ b/linphone.xcodeproj/project.pbxproj @@ -5652,7 +5652,7 @@ CODE_SIGN_STYLE = Automatic; COMPRESS_PNG_FILES = NO; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -5684,14 +5684,14 @@ "$(inherited)", ); LINK_WITH_STANDARD_LIBRARIES = YES; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; OTHER_CFLAGS = ( "-DBCTBX_LOG_DOMAIN=\\\"ios\\\"", "-DCHECK_VERSION_UPDATE=FALSE", "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.61+5e27e59a\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.153+9c69668a\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5782,7 +5782,7 @@ CODE_SIGN_STYLE = Automatic; COMPRESS_PNG_FILES = NO; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -5811,14 +5811,14 @@ "$(inherited)", ); LINK_WITH_STANDARD_LIBRARIES = YES; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; OTHER_CFLAGS = ( "-DBCTBX_LOG_DOMAIN=\\\"ios\\\"", "-DCHECK_VERSION_UPDATE=FALSE", "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.61+5e27e59a\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.153+9c69668a\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5908,7 +5908,7 @@ CODE_SIGN_STYLE = Automatic; COMPRESS_PNG_FILES = NO; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -5937,14 +5937,14 @@ "$(inherited)", ); LINK_WITH_STANDARD_LIBRARIES = YES; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; OTHER_CFLAGS = ( "-DBCTBX_LOG_DOMAIN=\\\"ios\\\"", "-DCHECK_VERSION_UPDATE=FALSE", "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.61+5e27e59a\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.153+9c69668a\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -6033,7 +6033,7 @@ CODE_SIGN_STYLE = Automatic; COMPRESS_PNG_FILES = NO; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6062,14 +6062,14 @@ "$(inherited)", ); LINK_WITH_STANDARD_LIBRARIES = YES; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; OTHER_CFLAGS = ( "-DBCTBX_LOG_DOMAIN=\\\"ios\\\"", "-DCHECK_VERSION_UPDATE=FALSE", "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.61+5e27e59a\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.153+9c69668a\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -6111,7 +6111,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6123,7 +6123,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; INFOPLIST_FILE = linphoneExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.linphoneExtension; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6154,7 +6154,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6165,7 +6165,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; INFOPLIST_FILE = linphoneExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.linphoneExtension; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6196,7 +6196,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6207,7 +6207,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; INFOPLIST_FILE = linphoneExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.linphoneExtension; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6239,7 +6239,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6250,7 +6250,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; INFOPLIST_FILE = linphoneExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 9.0; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.linphoneExtension; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6333,7 +6333,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6350,7 +6350,7 @@ INFOPLIST_FILE = "$(SRCROOT)/msgNotificationService/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited)"; @@ -6389,7 +6389,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6402,7 +6402,7 @@ INFOPLIST_FILE = "$(SRCROOT)/msgNotificationService/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited)"; @@ -6441,7 +6441,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6454,7 +6454,7 @@ INFOPLIST_FILE = "$(SRCROOT)/msgNotificationService/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited)"; @@ -6493,7 +6493,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6506,7 +6506,7 @@ INFOPLIST_FILE = "$(SRCROOT)/msgNotificationService/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited)"; @@ -6545,7 +6545,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6562,7 +6562,7 @@ INFOPLIST_FILE = msgNotificationContent/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited)"; @@ -6601,7 +6601,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6614,7 +6614,7 @@ INFOPLIST_FILE = msgNotificationContent/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited)"; @@ -6653,7 +6653,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6666,7 +6666,7 @@ INFOPLIST_FILE = msgNotificationContent/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited)"; @@ -6705,7 +6705,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = Z2V957B3D6; ENABLE_BITCODE = NO; @@ -6718,7 +6718,7 @@ INFOPLIST_FILE = msgNotificationContent/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 4.4.0; + MARKETING_VERSION = 4.7.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited)"; From d0d3f2e403d5512795765589a77aa6869b34528e Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Wed, 11 May 2022 14:48:14 +0200 Subject: [PATCH 32/75] Update video window of conference participants upon stream availabililty --- .../ConferenceParticipantDeviceData.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift index 0affb76b7..700394c60 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift @@ -28,6 +28,8 @@ class ConferenceParticipantDeviceData { let videoEnabled = MutableLiveData() let activeSpeaker = MutableLiveData() let isInConference = MutableLiveData() + private var videoView: UIView? = nil + var core : Core { get { Core.get() } } private var participantDeviceDelegate : ParticipantDeviceDelegate? @@ -42,6 +44,7 @@ class ConferenceParticipantDeviceData { }, onConferenceJoined: { (participantDevice) in Log.i("[Conference Participant Device] Participant \(participantDevice) has joined the conference") self.isInConference.value = true + self.setVideoView(view: self.videoView) }, onConferenceLeft: { (participantDevice) in Log.i("[Conference Participant Device] Participant \(participantDevice) has left the conference") self.isInConference.value = false @@ -55,6 +58,9 @@ class ConferenceParticipantDeviceData { if (streamType == StreamType.Video) { Log.i("[Conference Participant Device] Participant [\(participantDevice.address?.asStringUriOnly())] video availability changed to \(available)") self.videoEnabled.value = available + if (available) { + self.setVideoView(view: self.videoView) + } } } @@ -90,14 +96,15 @@ class ConferenceParticipantDeviceData { return isMe && Core.get().showSwitchCameraButton() } - func setVideoView(view:UIView) { + func setVideoView(view:UIView?) { + self.videoView = view Log.i("[Conference Participant Device] Setting textureView \(view) for participant \(participantDevice)") - view.contentMode = .scaleAspectFill + view?.contentMode = .scaleAspectFill if (isMe) { core.usePreviewWindow(yesno: false) core.nativePreviewWindow = view } else { - participantDevice.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passUnretained(view).toOpaque()) + participantDevice.nativeVideoWindowId = view != nil ? UnsafeMutableRawPointer(Unmanaged.passUnretained(view!).toOpaque()) : nil } } } From 137eb6de294b26996c0684212c9be24b8432cbc4 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Wed, 11 May 2022 15:14:58 +0200 Subject: [PATCH 33/75] Set appropriate visibility of Conference layout --- .../ActiveCallOrConferenceView.swift | 5 +++-- .../Conference/VoipConferenceActiveSpeakerView.swift | 5 ----- .../Fragments/Conference/VoipConferenceGridView.swift | 7 +------ 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index 8583bef83..152242a88 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -122,8 +122,9 @@ import linphonesw self.currentCallView!.isHidden = true self.extraButtonsView.isHidden = true self.conferencePausedView?.isHidden = true - self.conferenceGridView!.isHidden = false - self.conferenceActiveSpeakerView?.isHidden = true + let conferenceMode = ConferenceViewModel.shared.conferenceDisplayMode.value + self.conferenceGridView!.isHidden = conferenceMode != .Grid + self.conferenceActiveSpeakerView?.isHidden = conferenceMode != .ActiveSpeaker self.conferenceGridView?.conferenceViewModel = ConferenceViewModel.shared } else { self.conferenceGridView?.isHidden = true diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift index 09d1d4c9b..723d3f007 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift @@ -219,11 +219,6 @@ class VoipConferenceActiveSpeakerView: UIView, UICollectionViewDataSource, UICol // UICollectionView related delegates - func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - let gcell = (cell as! VoipActiveSpeakerParticipantCell) - gcell.participantData?.participantDevice.nativeVideoWindowId = nil - gcell.participantData?.clearObservers() - } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return inter_cell diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift index b4c93bfee..d2394bec1 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift @@ -151,7 +151,7 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi } ControlsViewModel.shared.fullScreenMode.observe { (fullScreen) in - if (self.isHidden) { + if (self.isHidden || self.conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .Grid) { return } self.gridContainer.removeConstraints().done() @@ -189,11 +189,6 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi } } - func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { - let gcell = (cell as! VoipGridParticipantCell) - gcell.participantData?.participantDevice.nativeVideoWindowId = nil - gcell.participantData?.clearObservers() - } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return inter_cell From 9581d88f0947c1665c820ee6b347d69cd76d872b Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Thu, 12 May 2022 14:19:52 +0200 Subject: [PATCH 34/75] Fix Conference invitation sometime not displaying in chat thread --- Classes/LinphoneUI/UIChatBubbleTextCell.h | 1 + Classes/LinphoneUI/UIChatBubbleTextCell.m | 15 +++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Classes/LinphoneUI/UIChatBubbleTextCell.h b/Classes/LinphoneUI/UIChatBubbleTextCell.h index 941d44364..604af74d5 100644 --- a/Classes/LinphoneUI/UIChatBubbleTextCell.h +++ b/Classes/LinphoneUI/UIChatBubbleTextCell.h @@ -69,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 da123c580..973461edc 100644 --- a/Classes/LinphoneUI/UIChatBubbleTextCell.m +++ b/Classes/LinphoneUI/UIChatBubbleTextCell.m @@ -30,7 +30,6 @@ @implementation UIChatBubbleTextCell -ICSBubbleView *icsBubbleView; #pragma mark - Lifecycle Functions @@ -47,10 +46,10 @@ ICSBubbleView *icsBubbleView; UIView *sub = ((UIView *)[arrayOfViews objectAtIndex:arrayOfViews.count - 1]); [self setFrame:CGRectMake(0, 0, sub.frame.size.width, sub.frame.size.height)]; [self addSubview:sub]; - icsBubbleView = [[ICSBubbleView alloc] init]; - icsBubbleView.frame = CGRectMake(_messageText.frame.origin.x, _messageText.frame.origin.y+25, CONFERENCE_INVITATION_WIDTH-80, CONFERENCE_INVITATION_HEIGHT-20); - [self.innerView addSubview:icsBubbleView]; - [icsBubbleView setLayoutConstraintsWithView:self.backgroundColorImage]; + 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]; } } @@ -287,11 +286,11 @@ ICSBubbleView *icsBubbleView; // ICS for conference invitations if ([ICSBubbleView isConferenceInvitationMessageWithCmessage:self.message]) { - [icsBubbleView setFromChatMessageWithCmessage:self.message]; - icsBubbleView.hidden = false; + [(ICSBubbleView*)self.icsBubbleView setFromChatMessageWithCmessage:self.message]; + self.icsBubbleView.hidden = false; _messageText.hidden = true; } else { - icsBubbleView.hidden = true; + self.icsBubbleView.hidden = true; _messageText.hidden = false; } From 1c8fdb192451e4624eadc21fcd82165430714988 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Thu, 12 May 2022 20:35:40 +0200 Subject: [PATCH 35/75] Remove observers before reloading grid/as participants of a conference --- .../Fragments/Conference/VoipConferenceActiveSpeakerView.swift | 3 +++ .../Views/Fragments/Conference/VoipConferenceGridView.swift | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift index 723d3f007..aa4cbc813 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceActiveSpeakerView.swift @@ -90,6 +90,9 @@ class VoipConferenceActiveSpeakerView: UIView, UICollectionViewDataSource, UICol } func reloadData() { + conferenceViewModel?.conferenceParticipantDevices.value?.forEach { + $0.clearObservers() + } self.grid.reloadData() } diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift index d2394bec1..f8d863e5f 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift @@ -174,6 +174,9 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi // UICollectionView related delegates func reloadData() { + conferenceViewModel?.conferenceParticipantDevices.value?.forEach { + $0.clearObservers() + } if (self.isHidden || conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .Grid) { self.grid.reloadData() return From 0ee549f5e2eb0a83ccb73a777d7edbf02620a52c Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 13 May 2022 09:20:01 +0200 Subject: [PATCH 36/75] Added more debug info --- .../Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift index 700394c60..1a0e066d5 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift @@ -98,7 +98,7 @@ class ConferenceParticipantDeviceData { func setVideoView(view:UIView?) { self.videoView = view - Log.i("[Conference Participant Device] Setting textureView \(view) for participant \(participantDevice)") + Log.i("[Conference Participant Device] Setting textureView \(view) for participant \(participantDevice.address?.asStringUriOnly())") view?.contentMode = .scaleAspectFill if (isMe) { core.usePreviewWindow(yesno: false) From 176cfd8b4bbb8d782e14e3f8701e84262cbf781c Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 13 May 2022 11:20:58 +0200 Subject: [PATCH 37/75] Fix multiple instance of call view Added further debug details --- Classes/Base.lproj/AssistantViewScreens.xib | 4 ++-- Classes/LinphoneAppDelegate.m | 2 +- Classes/LinphoneUI/UICompositeView.h | 1 - Classes/LinphoneUI/UICompositeView.m | 7 ------- .../ConferenceParticipantDeviceData.swift | 20 +++++++------------ .../ActiveCallOrConferenceView.swift | 1 - .../Conference/VoipConferenceGridView.swift | 1 + 7 files changed, 11 insertions(+), 25 deletions(-) diff --git a/Classes/Base.lproj/AssistantViewScreens.xib b/Classes/Base.lproj/AssistantViewScreens.xib index 847755544..8426a4403 100644 --- a/Classes/Base.lproj/AssistantViewScreens.xib +++ b/Classes/Base.lproj/AssistantViewScreens.xib @@ -1,9 +1,9 @@ - + - + diff --git a/Classes/LinphoneAppDelegate.m b/Classes/LinphoneAppDelegate.m index 1d4c152c3..ef2219282 100644 --- a/Classes/LinphoneAppDelegate.m +++ b/Classes/LinphoneAppDelegate.m @@ -321,7 +321,7 @@ return NO; } - VIEW(ActiveCallOrConferenceView); // to get created and all observers added + [PhoneMainView.instance.mainViewController getCachedController:ActiveCallOrConferenceView.compositeViewDescription.name]; // This will create the single instance of the ActiveCallOrConferenceView including listeneres return YES; } diff --git a/Classes/LinphoneUI/UICompositeView.h b/Classes/LinphoneUI/UICompositeView.h index e91b21fdb..b7c4739cb 100644 --- a/Classes/LinphoneUI/UICompositeView.h +++ b/Classes/LinphoneUI/UICompositeView.h @@ -85,7 +85,6 @@ - (UIInterfaceOrientation)currentOrientation; - (void)clearCache:(NSArray *)exclude; - (IBAction)onRightSwipe:(id)sender; -- (void) removeCallViewFromCache; @end diff --git a/Classes/LinphoneUI/UICompositeView.m b/Classes/LinphoneUI/UICompositeView.m index 08baa4963..3df38c997 100644 --- a/Classes/LinphoneUI/UICompositeView.m +++ b/Classes/LinphoneUI/UICompositeView.m @@ -305,13 +305,6 @@ return nil; } --(void) removeCallViewFromCache { - for (NSString *key in [viewControllerCache allKeys]) { - if ([key isEqualToString:ActiveCallOrConferenceView.compositeViewDescription.name]) { - [viewControllerCache removeObjectForKey:key]; - } - } -} - (void)clearCache:(NSArray *)exclude { for (NSString *key in [viewControllerCache allKeys]) { diff --git a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift index 1a0e066d5..4180a406e 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift @@ -28,7 +28,6 @@ class ConferenceParticipantDeviceData { let videoEnabled = MutableLiveData() let activeSpeaker = MutableLiveData() let isInConference = MutableLiveData() - private var videoView: UIView? = nil var core : Core { get { Core.get() } } @@ -39,17 +38,16 @@ class ConferenceParticipantDeviceData { self.participantDevice = participantDevice participantDeviceDelegate = ParticipantDeviceDelegateStub( onIsSpeakingChanged: { (participantDevice, isSpeaking) in - Log.i("[Conference Participant Device] Participant \(participantDevice ) isspeaking = \(isSpeaking)") + Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) isspeaking = \(isSpeaking)") self.activeSpeaker.value = isSpeaking }, onConferenceJoined: { (participantDevice) in - Log.i("[Conference Participant Device] Participant \(participantDevice) has joined the conference") + Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) has joined the conference") self.isInConference.value = true - self.setVideoView(view: self.videoView) }, onConferenceLeft: { (participantDevice) in - Log.i("[Conference Participant Device] Participant \(participantDevice) has left the conference") + 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) video stream direction changed: \(direction)") + 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)") @@ -58,9 +56,6 @@ class ConferenceParticipantDeviceData { if (streamType == StreamType.Video) { Log.i("[Conference Participant Device] Participant [\(participantDevice.address?.asStringUriOnly())] video availability changed to \(available)") self.videoEnabled.value = available - if (available) { - self.setVideoView(view: self.videoView) - } } } @@ -96,15 +91,14 @@ class ConferenceParticipantDeviceData { return isMe && Core.get().showSwitchCameraButton() } - func setVideoView(view:UIView?) { - self.videoView = view + func setVideoView(view:UIView) { Log.i("[Conference Participant Device] Setting textureView \(view) for participant \(participantDevice.address?.asStringUriOnly())") - view?.contentMode = .scaleAspectFill + view.contentMode = .scaleAspectFill if (isMe) { core.usePreviewWindow(yesno: false) core.nativePreviewWindow = view } else { - participantDevice.nativeVideoWindowId = view != nil ? UnsafeMutableRawPointer(Unmanaged.passUnretained(view!).toOpaque()) : nil + participantDevice.nativeVideoWindowId = UnsafeMutableRawPointer(Unmanaged.passRetained(view).toOpaque()) } } } diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index 152242a88..5cce8a48a 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -310,7 +310,6 @@ import linphonesw func updateNavigation() { if (Core.get().callsNb == 0) { PhoneMainView.instance().popView(self.compositeViewDescription()) - PhoneMainView.instance().mainViewController.removeCallFromCache() } else { if let data = CallsViewModel.shared.currentCallData.value { if (data?.isOutgoing.value == true || data?.isIncoming.value == true) { diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift index f8d863e5f..37ccb0416 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift @@ -74,6 +74,7 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() layout.minimumInteritemSpacing = 0 layout.minimumLineSpacing = 0 + layout.estimatedItemSize = .zero grid = UICollectionView(frame:.zero, collectionViewLayout: layout) super.init(frame: .zero) From bd1ea4a5e3650b240eec1c3c1005c2eb6535f2dd Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 13 May 2022 16:17:16 +0200 Subject: [PATCH 38/75] My Conferences view --- .../Data/ScheduledConferenceData.swift | 2 +- .../ScheduledConferencesViewModel.swift | 4 +- .../Views/ScheduledConferencesCell.swift | 158 ++++++++++++------ .../Views/ScheduledConferencesView.swift | 38 +++-- .../Extensions/IOS/UIVIewExtensions.swift | 6 + Classes/Swift/Util/TimestampUtils.swift | 7 + Classes/Swift/Voip/Theme/VoipTexts.swift | 3 +- Classes/Swift/Voip/Theme/VoipTheme.swift | 5 + Classes/Swift/Voip/VoipDialog.swift | 13 +- 9 files changed, 169 insertions(+), 67 deletions(-) diff --git a/Classes/Swift/Conference/Data/ScheduledConferenceData.swift b/Classes/Swift/Conference/Data/ScheduledConferenceData.swift index 0115c250c..e8f66aad2 100644 --- a/Classes/Swift/Conference/Data/ScheduledConferenceData.swift +++ b/Classes/Swift/Conference/Data/ScheduledConferenceData.swift @@ -55,7 +55,7 @@ class ScheduledConferenceData { durationFormatter.unitsStyle = .positional durationFormatter.allowedUnits = [.minute, .second ] durationFormatter.zeroFormattingBehavior = [ .pad ] - duration.value = durationFormatter.string(from: TimeInterval(conferenceInfo.duration)) + duration.value = conferenceInfo.duration > 0 ? durationFormatter.string(from: TimeInterval(conferenceInfo.duration)) : nil organizer.value = conferenceInfo.organizer?.addressBookEnhancedDisplayName() diff --git a/Classes/Swift/Conference/ViewModels/ScheduledConferencesViewModel.swift b/Classes/Swift/Conference/ViewModels/ScheduledConferencesViewModel.swift index 72449a192..005651614 100644 --- a/Classes/Swift/Conference/ViewModels/ScheduledConferencesViewModel.swift +++ b/Classes/Swift/Conference/ViewModels/ScheduledConferencesViewModel.swift @@ -48,7 +48,9 @@ class ScheduledConferencesViewModel { func computeConferenceInfoList() { conferences.value!.removeAll() - core.futureConferenceInformationList.forEach { conferenceInfo in // Sorted in the sdk + 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)).forEach { conferenceInfo in conferences.value!.append(ScheduledConferenceData(conferenceInfo: conferenceInfo)) } diff --git a/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift b/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift index 0b5f21a87..f95d11325 100644 --- a/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift +++ b/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift @@ -26,42 +26,58 @@ 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_scheduling_font, VoipTexts.conference_description_title) - let descriptionValue = StyledLabel(VoipTheme.conference_scheduling_font) - let urlTitle = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_address_title) - let urlAndCopy = UIView() + 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(buttonTheme: VoipTheme.scheduled_conference_action("voip_copy")) - let joinEditDelete = UIView() + 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(buttonTheme: VoipTheme.scheduled_conference_action("voip_delete")) - let editConf = CallControlButton(buttonTheme: VoipTheme.scheduled_conference_action("voip_edit")) - + 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) ( \(data.duration) )" - timeDuration.addIndicatorIcon(iconName: "conference_schedule_time_default", trailing: false) + 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.isHidden = expanded != true - self.urlAndCopy.isHidden = expanded != true - self.joinEditDelete.isHidden = expanded != true + 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.addIndicatorIcon(iconName: "conference_schedule_participants_default", trailing: false) + 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() + } + } } } @@ -75,48 +91,90 @@ class ScheduledConferencesCell: UITableViewCell { contentView.backgroundColor = VoipTheme.header_background_color contentView.layer.borderColor = VoipTheme.primary_color.cgColor - let rows = UIStackView() - rows.axis = .vertical - rows.addArrangedSubview(timeDuration) - rows.addArrangedSubview(subject) + + contentView.addSubview(clockIcon) + clockIcon.alignParentTop(withMargin: 15).square(15).alignParentLeft(withMargin: 10).done() - let participantsAndInfos = UIView() - participantsAndInfos.addSubview(participants) - participants.alignParentLeft().done() - participantsAndInfos.addSubview(infoConf) - infoConf.toRightOf(participants).done() - rows.addArrangedSubview(participantsAndInfos) - infoConf.applyTintedIcons(tintedIcons: VoipTheme.conference_info_button) + 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 { self.conferenceData?.toggleExpand() + self.owningTableView?.reloadData() } - - rows.addArrangedSubview(descriptionTitle) - rows.addArrangedSubview(descriptionValue) + 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) - rows.addArrangedSubview(urlTitle) - urlAndCopy.addSubview(urlValue) + + 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.addSubview(copyLink) - copyLink.toLeftOf(urlValue).done() - rows.addArrangedSubview(urlAndCopy) - - joinEditDelete.addSubview(joinConf) - joinEditDelete.addSubview(editConf) - joinEditDelete.addSubview(deleteConf) - deleteConf.alignParentRight().done() - editConf.toLeftOf(deleteConf).done() - joinConf.toLeftOf(deleteConf).done() - - joinConf.onClick { - /* - ConferenceWaitingRoomFragment *view = VIEW(ConferenceWaitingRoomFragment); - [PhoneMainView.instance changeCurrentView:ConferenceWaitingRoomFragment.compositeViewDescription]; - [view setDetailsWithSubject:@"Sujet de la conférence" url:@"toto"]; - return; - */ + 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 { + // TODO + } + + 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() + }, 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) { diff --git a/Classes/Swift/Conference/Views/ScheduledConferencesView.swift b/Classes/Swift/Conference/Views/ScheduledConferencesView.swift index f8139e375..1974404b4 100644 --- a/Classes/Swift/Conference/Views/ScheduledConferencesView.swift +++ b/Classes/Swift/Conference/Views/ScheduledConferencesView.swift @@ -22,7 +22,7 @@ import UIKit import Foundation import linphonesw -@objc class ScheduledConferencesView: BackNextNavigationView, UICompositeViewDelegate, UITableViewDataSource { +@objc class ScheduledConferencesView: BackNextNavigationView, UICompositeViewDelegate, UITableViewDataSource, UITableViewDelegate { let conferenceListView = UITableView() let noConference = StyledLabel(VoipTheme.empty_list_font,VoipTexts.conference_no_schedule) @@ -37,17 +37,20 @@ import linphonesw backAction: { PhoneMainView.instance().popView(self.compositeViewDescription()) },nextAction: { + PhoneMainView.instance().changeCurrentView(ConferenceSchedulingView.compositeDescription) }, - nextActionEnableCondition: MutableLiveData(false), + nextActionEnableCondition: MutableLiveData(true), title:VoipTexts.conference_scheduled) - super.nextButton.isHidden = true + + super.nextButton.applyTintedIcons(tintedIcons: VoipTheme.conference_create_button) - - contentView.addSubview(conferenceListView) - conferenceListView.isScrollEnabled = false + 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 } @@ -56,7 +59,6 @@ import linphonesw view.addSubview(noConference) noConference.center().done() - } @@ -66,21 +68,22 @@ import linphonesw super.viewWillAppear(animated) self.conferenceListView.reloadData() self.conferenceListView.removeConstraints().done() - self.conferenceListView.matchParentSideBorders().alignUnder(view: super.topBar,withMargin: self.form_margin).alignParentBottom().done() + self.conferenceListView.matchParentSideBorders(insetedByDx: 10).alignUnder(view: super.topBar,withMargin: self.form_margin).alignParentBottom().done() noConference.isHidden = !ScheduledConferencesViewModel.shared.daySplitted.isEmpty } // TableView datasource delegate func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - let daysArray = Array(ScheduledConferencesViewModel.shared.daySplitted.keys) + let daysArray = Array(ScheduledConferencesViewModel.shared.daySplitted.keys.sorted().reversed()) let day = daysArray[section] - return TimestampUtils.dateToString(date: day) + 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 { @@ -88,19 +91,30 @@ import linphonesw } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let daysArray = Array(ScheduledConferencesViewModel.shared.daySplitted.keys) + 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) + 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/Swift/Extensions/IOS/UIVIewExtensions.swift b/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift index edbb118be..7d974d818 100644 --- a/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift +++ b/Classes/Swift/Extensions/IOS/UIVIewExtensions.swift @@ -308,6 +308,12 @@ extension UIView { 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 diff --git a/Classes/Swift/Util/TimestampUtils.swift b/Classes/Swift/Util/TimestampUtils.swift index 64946c34c..d217ee350 100644 --- a/Classes/Swift/Util/TimestampUtils.swift +++ b/Classes/Swift/Util/TimestampUtils.swift @@ -53,6 +53,13 @@ class TimestampUtils { 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 diff --git a/Classes/Swift/Voip/Theme/VoipTexts.swift b/Classes/Swift/Voip/Theme/VoipTexts.swift index 60e5bdafd..8d4ba6618 100644 --- a/Classes/Swift/Voip/Theme/VoipTexts.swift +++ b/Classes/Swift/Voip/Theme/VoipTexts.swift @@ -104,7 +104,8 @@ import UIKit static let conference_schedule_organizer = NSLocalizedString("Organizer:",comment:"") static let conference_go_to_chat = NSLocalizedString("Conference's chat room",comment:"") static let conference_creation_failed = NSLocalizedString("Failed to create conference",comment:"") - + static let conference_info_confirm_removal = NSLocalizedString("Do you really want to delete this conference?",comment:"") + static let conference_info_confirm_removal_delete = NSLocalizedString("Delete",comment:"") // Call Stats diff --git a/Classes/Swift/Voip/Theme/VoipTheme.swift b/Classes/Swift/Voip/Theme/VoipTheme.swift index 47ddd4ca6..28cfe24ba 100644 --- a/Classes/Swift/Voip/Theme/VoipTheme.swift +++ b/Classes/Swift/Voip/Theme/VoipTheme.swift @@ -388,6 +388,11 @@ class VoipTheme { // Names & values replicated from Android 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)), + ] + } diff --git a/Classes/Swift/Voip/VoipDialog.swift b/Classes/Swift/Voip/VoipDialog.swift index 7ece3a8d8..2ed619103 100644 --- a/Classes/Swift/Voip/VoipDialog.swift +++ b/Classes/Swift/Voip/VoipDialog.swift @@ -86,11 +86,11 @@ class VoipDialog : UIView{ } func show() { - rootVC()?.view.addSubview(self) + VoipDialog.rootVC()?.view.addSubview(self) matchParentDimmensions().done() } - private func rootVC() -> UIViewController? { + private static func rootVC() -> UIViewController? { return UIApplication.getTopMostViewController() } @@ -98,6 +98,15 @@ class VoipDialog : UIView{ super.init(coder: coder) } + static func toast(message:String, timeout:CGFloat = 1.5) { + let alertDisapperTimeInSeconds = 2.0 + let alert = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet) + rootVC()?.present(alert, animated: true) + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + alertDisapperTimeInSeconds) { + alert.dismiss(animated: true) + } + } + } struct ButtonAttributes { From f396cdf5a4c98ebf27e3821c1523de57a1daa4cb Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Sun, 15 May 2022 21:54:39 +0200 Subject: [PATCH 39/75] Fix account not persisted after login into linphon --- Classes/AssistantView.m | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Classes/AssistantView.m b/Classes/AssistantView.m index b6b5b65db..7033f3673 100644 --- a/Classes/AssistantView.m +++ b/Classes/AssistantView.m @@ -526,6 +526,9 @@ static UICompositeViewDescription *compositeDescription = nil; [self displayAssistantConfigurationError]; [LinphoneManager.instance migrationPerAccount]; + + linphone_config_sync(LinphoneManager.instance.configDb); + } - (void)displayAssistantConfigurationError { From deb4fbb71e464bbe59b52e265fff1dbfa31e2098 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Sun, 15 May 2022 21:55:19 +0200 Subject: [PATCH 40/75] Preserve Call VC Single Instance in case of cache cleaning --- Classes/LinphoneUI/UICompositeView.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Classes/LinphoneUI/UICompositeView.m b/Classes/LinphoneUI/UICompositeView.m index 3df38c997..f7ed463b5 100644 --- a/Classes/LinphoneUI/UICompositeView.m +++ b/Classes/LinphoneUI/UICompositeView.m @@ -307,11 +307,13 @@ - (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) { From 1f2aa259be8008f5b6659cab1bd5645a567d8cf7 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Sun, 15 May 2022 21:55:40 +0200 Subject: [PATCH 41/75] Various conference adjustments --- .../ConferenceSchedulingViewModel.swift | 9 ++- .../ConferenceSchedulingSummaryView.swift | 68 +++++++++++-------- .../Views/ConferenceSchedulingView.swift | 41 +++++++---- .../Views/ScheduledConferencesCell.swift | 4 +- .../Views/ScheduledConferencesView.swift | 4 +- Classes/Swift/Voip/Theme/VoipTheme.swift | 1 + .../Voip/Widgets/StyledValuePicker.swift | 17 +++-- 7 files changed, 88 insertions(+), 56 deletions(-) diff --git a/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift b/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift index 458106791..57a9d015b 100644 --- a/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift +++ b/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift @@ -215,7 +215,7 @@ class ConferenceSchedulingViewModel { if (scheduleForLater.value == true) { let timestamp = getConferenceStartTimestamp() conferenceInfo.dateTime = time_t(timestamp) - scheduledDuration.value.map {conferenceInfo.duration = UInt($0) } + scheduledDuration.value.map { conferenceInfo.duration = UInt(ConferenceSchedulingViewModel.durationList[$0].value) } } conferenceScheduler?.info = conferenceInfo // Will trigger the conference creation automatically @@ -232,8 +232,11 @@ class ConferenceSchedulingViewModel { private func getConferenceStartTimestamp() -> Double { - return scheduleForLater.value == true ? scheduledDate.value!.timeIntervalSince1970 + scheduledTime.value!.timeIntervalSince1970 : Date().timeIntervalSince1970 - + 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 } diff --git a/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift b/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift index cabdebde2..8807c571c 100644 --- a/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift +++ b/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift @@ -27,8 +27,14 @@ import SVProgressHUD let CONFERENCE_CREATION_TIME_OUT_SEC = 15.0 - let viewModel = ConferenceSchedulingViewModel.shared 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) + 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 } @@ -41,7 +47,7 @@ import SVProgressHUD self.goBackParticipantsListSelection() },nextAction: { }, - nextActionEnableCondition: viewModel.continueEnabled, + nextActionEnableCondition: ConferenceSchedulingViewModel.shared.continueEnabled, title:VoipTexts.conference_schedule_summary) super.nextButton.isHidden = true @@ -53,12 +59,12 @@ import SVProgressHUD encryptedIcon.contentMode = .scaleAspectFit contentView.addSubview(encryptedIcon) encryptedIcon.height(form_input_height).alignParentTop().alignParentTop().alignParentRight(withMargin: form_margin).alignHorizontalCenterWith(subjectLabel).done() - viewModel.isEncrypted.readCurrentAndObserve { (encrypt) in + ConferenceSchedulingViewModel.shared.isEncrypted.readCurrentAndObserve { (encrypt) in encryptedIcon.isHidden = encrypt != true } - let subjectInput = StyledTextView(VoipTheme.conference_scheduling_font, placeHolder:VoipTexts.conference_schedule_subject_hint, liveValue: viewModel.subject, readOnly: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() @@ -72,7 +78,7 @@ import SVProgressHUD let scheduleForm = UIView() schedulingStack.addArrangedSubview(scheduleForm) scheduleForm.matchParentSideBorders().done() - viewModel.scheduleForLater.readCurrentAndObserve { (forLater) in scheduleForm.isHidden = forLater != true } + ConferenceSchedulingViewModel.shared.scheduleForLater.readCurrentAndObserve { (forLater) in scheduleForm.isHidden = forLater != true } // Left column (Date & Time) let leftColumn = UIView() @@ -83,7 +89,6 @@ import SVProgressHUD leftColumn.addSubview(dateLabel) dateLabel.alignParentLeft().alignParentTop(withMargin: form_margin).done() - let datePicker = StyledDatePicker(liveValue: viewModel.scheduledDate,pickerMode: .date, readOnly:true) leftColumn.addSubview(datePicker) datePicker.alignParentLeft().alignUnder(view: dateLabel,withMargin: form_margin).matchParentSideBorders().done() @@ -91,7 +96,6 @@ import SVProgressHUD leftColumn.addSubview(timeLabel) timeLabel.alignParentLeft().alignUnder(view: datePicker,withMargin: form_margin).done() - let timePicker = StyledDatePicker(liveValue: viewModel.scheduledTime,pickerMode: .time, readOnly:true) leftColumn.addSubview(timePicker) timePicker.alignParentLeft().alignUnder(view: timeLabel,withMargin: form_margin).matchParentSideBorders().done() @@ -107,7 +111,6 @@ import SVProgressHUD rightColumn.addSubview(durationLabel) durationLabel.alignParentLeft().alignParentTop(withMargin: form_margin).done() - let durationValue = StyledValuePicker(liveIndex: viewModel.scheduledDuration,options: ConferenceSchedulingViewModel.durationList.map({ (duration: Duration) -> String in duration.display}), readOnly:true) rightColumn.addSubview(durationValue) durationValue.alignParentLeft().alignUnder(view: durationLabel,withMargin: form_margin).matchParentSideBorders().done() @@ -115,7 +118,6 @@ import SVProgressHUD rightColumn.addSubview(timeZoneLabel) timeZoneLabel.alignParentLeft().alignUnder(view: durationValue,withMargin: form_margin).done() - let timeZoneValue = StyledValuePicker(liveIndex: viewModel.scheduledTimeZone,options: ConferenceSchedulingViewModel.timeZones.map({ (tzd: TimeZoneData) -> String in tzd.descWithOffset()}), readOnly:true) rightColumn.addSubview(timeZoneValue) timeZoneValue.alignParentLeft().alignUnder(view: timeZoneLabel,withMargin: form_margin).matchParentSideBorders().done() @@ -126,7 +128,6 @@ import SVProgressHUD scheduleForm.addSubview(descriptionLabel) descriptionLabel.alignUnder(view: leftColumn,withMargin: form_margin).alignUnder(view: rightColumn,withMargin: form_margin).matchParentSideBorders(insetedByDx: form_margin).done() - let descriptionInput = StyledTextView(VoipTheme.conference_scheduling_font, placeHolder:VoipTexts.conference_schedule_description_hint,liveValue: viewModel.description, readOnly:true) descriptionInput.textContainer.maximumNumberOfLines = 5 descriptionInput.textContainer.lineBreakMode = .byWordWrapping scheduleForm.addSubview(descriptionInput) @@ -138,7 +139,7 @@ import SVProgressHUD 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() - viewModel.sendInviteViaChat.readCurrentAndObserve { (sendChat) in + ConferenceSchedulingViewModel.shared.sendInviteViaChat.readCurrentAndObserve { (sendChat) in viaChatLabel.isHidden = sendChat != true } @@ -161,7 +162,7 @@ import SVProgressHUD participantsListTableView.separatorStyle = .singleLine participantsListTableView.separatorColor = VoipTheme.light_grey_color - viewModel.selectedAddresses.readCurrentAndObserve { (addresses) in + ConferenceSchedulingViewModel.shared.selectedAddresses.readCurrentAndObserve { (addresses) in self.participantsListTableView.reloadData() self.participantsListTableView.removeConstraints().done() self.participantsListTableView.matchParentSideBorders().alignUnder(view: participantsLabel,withMargin: self.form_margin).done() @@ -171,12 +172,12 @@ import SVProgressHUD // Create / Schedule let createButton = FormButton(backgroundStateColors: VoipTheme.primary_colors_background) contentView.addSubview(createButton) - viewModel.scheduleForLater.readCurrentAndObserve { _ in - createButton.title = self.viewModel.scheduleForLater.value == true ? VoipTexts.conference_schedule.uppercased() : VoipTexts.conference_schedule_create.uppercased() + ConferenceSchedulingViewModel.shared.scheduleForLater.readCurrentAndObserve { _ in + createButton.title = ConferenceSchedulingViewModel.shared.scheduleForLater.value == true ? VoipTexts.conference_schedule.uppercased() : VoipTexts.conference_schedule_create.uppercased() createButton.addSidePadding() } - self.viewModel.conferenceCreationInProgress.observe { progress in + ConferenceSchedulingViewModel.shared.conferenceCreationInProgress.observe { progress in if (progress == true) { SVProgressHUD.show() } else { @@ -186,9 +187,9 @@ import SVProgressHUD var enableCreationTimeOut = false - viewModel.conferenceCreationCompletedEvent.observe { pair in + ConferenceSchedulingViewModel.shared.conferenceCreationCompletedEvent.observe { pair in enableCreationTimeOut = false - if (self.viewModel.scheduleForLater.value == true) { + if (ConferenceSchedulingViewModel.shared.scheduleForLater.value == true) { PhoneMainView.instance().pop(toView:ScheduledConferencesView.compositeDescription) } else { let view: ConferenceWaitingRoomFragment = self.VIEW(ConferenceWaitingRoomFragment.compositeViewDescription()); @@ -196,34 +197,43 @@ import SVProgressHUD view.setDetails(subject: pair!.second!, url: pair!.first!) } } - viewModel.onErrorEvent.observe { error in + ConferenceSchedulingViewModel.shared.onErrorEvent.observe { error in VoipDialog.init(message: error!).show() } createButton.onClick { enableCreationTimeOut = true - self.viewModel.createConference() + ConferenceSchedulingViewModel.shared.createConference() DispatchQueue.main.asyncAfter(deadline: .now() + self.CONFERENCE_CREATION_TIME_OUT_SEC) { if (enableCreationTimeOut) { enableCreationTimeOut = false - self.viewModel.conferenceCreationInProgress.value = false - self.viewModel.onErrorEvent.value = VoipTexts.call_error_server_timeout + ConferenceSchedulingViewModel.shared.conferenceCreationInProgress.value = false + ConferenceSchedulingViewModel.shared.onErrorEvent.value = VoipTexts.call_error_server_timeout } } } - viewModel.scheduleForLater.readCurrentAndObserve { _ in - createButton.title = self.viewModel.scheduleForLater.value == true ? VoipTexts.conference_schedule.uppercased() : VoipTexts.conference_schedule_create.uppercased() + ConferenceSchedulingViewModel.shared.scheduleForLater.readCurrentAndObserve { _ in + createButton.title = ConferenceSchedulingViewModel.shared.scheduleForLater.value == true ? VoipTexts.conference_schedule.uppercased() : VoipTexts.conference_schedule_create.uppercased() 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 + super.viewWillAppear(animated) + } func goBackParticipantsListSelection() { let view: ChatConversationCreateView = VIEW(ChatConversationCreateView.compositeViewDescription()) - let addresses = viewModel.selectedAddresses.value!.map { (address) in String(address.asStringUriOnly()) } + 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 @@ -233,17 +243,17 @@ import SVProgressHUD // Objc - bridge, as can't access easily to the view model. @objc func setParticipants(addresses:[String]) { - viewModel.selectedAddresses.value = [] + ConferenceSchedulingViewModel.shared.selectedAddresses.value = [] return addresses.forEach { (address) in if let address = try?Factory.Instance.createAddress(addr: address) { - viewModel.selectedAddresses.value?.append(address) + ConferenceSchedulingViewModel.shared.selectedAddresses.value?.append(address) } } } // TableView datasource delegate func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - guard let participants = viewModel.selectedAddresses.value else { + guard let participants = ConferenceSchedulingViewModel.shared.selectedAddresses.value else { return 0 } return participants.count @@ -251,12 +261,12 @@ import SVProgressHUD func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell:VoipParticipantCell = tableView.dequeueReusableCell(withIdentifier: "VoipParticipantCellSSchedule") as! VoipParticipantCell - guard let participant = viewModel.selectedAddresses.value?[indexPath.row] else { + guard let participant = ConferenceSchedulingViewModel.shared.selectedAddresses.value?[indexPath.row] else { return cell } cell.selectionStyle = .none cell.scheduleConfParticipantAddress = participant - cell.limeBadge.isHidden = viewModel.isEncrypted.value != true + 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 index 372ee752c..27f0934eb 100644 --- a/Classes/Swift/Conference/Views/ConferenceSchedulingView.swift +++ b/Classes/Swift/Conference/Views/ConferenceSchedulingView.swift @@ -24,12 +24,18 @@ import linphonesw @objc class ConferenceSchedulingView: BackNextNavigationView, UICompositeViewDelegate { - let viewModel = ConferenceSchedulingViewModel.shared 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( @@ -38,7 +44,7 @@ import linphonesw },nextAction: { self.gotoParticipantsListSelection() }, - nextActionEnableCondition: viewModel.continueEnabled, + nextActionEnableCondition: ConferenceSchedulingViewModel.shared.continueEnabled, title:VoipTexts.conference_schedule_title) let subjectLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_subject_title) @@ -46,7 +52,7 @@ import linphonesw contentView.addSubview(subjectLabel) subjectLabel.alignParentLeft(withMargin: form_margin).alignParentTop().done() - let subjectInput = StyledTextView(VoipTheme.conference_scheduling_font, placeHolder:VoipTexts.conference_schedule_subject_hint, liveValue: viewModel.subject,maxLines:1) + 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() @@ -60,7 +66,7 @@ import linphonesw schedulingStack.addArrangedSubview(scheduleForLater) scheduleForLater.matchParentSideBorders().height(schdule_for_later_height).done() - let laterSwitch = StyledSwitch(liveValue: viewModel.scheduleForLater) + let laterSwitch = StyledSwitch(liveValue: ConferenceSchedulingViewModel.shared.scheduleForLater) scheduleForLater.addSubview(laterSwitch) laterSwitch.alignParentTop(withMargin: form_margin*2.5).alignParentLeft(withMargin: form_margin).centerY().done() @@ -72,7 +78,7 @@ import linphonesw let scheduleForm = UIView() schedulingStack.addArrangedSubview(scheduleForm) scheduleForm.matchParentSideBorders().done() - viewModel.scheduleForLater.readCurrentAndObserve { (forLater) in scheduleForm.isHidden = forLater != true } + ConferenceSchedulingViewModel.shared.scheduleForLater.readCurrentAndObserve { (forLater) in scheduleForm.isHidden = forLater != true } // Left column (Date & Time) let leftColumn = UIView() @@ -84,7 +90,6 @@ import linphonesw leftColumn.addSubview(dateLabel) dateLabel.alignParentLeft().alignParentTop(withMargin: form_margin).done() - let datePicker = StyledDatePicker(liveValue: viewModel.scheduledDate,pickerMode: .date) leftColumn.addSubview(datePicker) datePicker.alignParentLeft().alignUnder(view: dateLabel,withMargin: form_margin).matchParentSideBorders().done() @@ -93,7 +98,6 @@ import linphonesw leftColumn.addSubview(timeLabel) timeLabel.alignParentLeft().alignUnder(view: datePicker,withMargin: form_margin).done() - let timePicker = StyledDatePicker(liveValue: viewModel.scheduledTime,pickerMode: .time) leftColumn.addSubview(timePicker) timePicker.alignParentLeft().alignUnder(view: timeLabel,withMargin: form_margin).matchParentSideBorders().done() @@ -109,7 +113,6 @@ import linphonesw rightColumn.addSubview(durationLabel) durationLabel.alignParentLeft().alignParentTop(withMargin: form_margin).done() - let durationValue = StyledValuePicker(liveIndex: viewModel.scheduledDuration,options: ConferenceSchedulingViewModel.durationList.map({ (duration: Duration) -> String in duration.display})) rightColumn.addSubview(durationValue) durationValue.alignParentLeft().alignUnder(view: durationLabel,withMargin: form_margin).matchParentSideBorders().done() @@ -117,7 +120,6 @@ import linphonesw rightColumn.addSubview(timeZoneLabel) timeZoneLabel.alignParentLeft().alignUnder(view: durationValue,withMargin: form_margin).done() - let timeZoneValue = StyledValuePicker(liveIndex: viewModel.scheduledTimeZone,options: ConferenceSchedulingViewModel.timeZones.map({ (tzd: TimeZoneData) -> String in tzd.descWithOffset()})) rightColumn.addSubview(timeZoneValue) timeZoneValue.alignParentLeft().alignUnder(view: timeZoneLabel,withMargin: form_margin).matchParentSideBorders().done() @@ -128,7 +130,6 @@ import linphonesw scheduleForm.addSubview(descriptionLabel) descriptionLabel.alignUnder(view: leftColumn,withMargin: form_margin).alignUnder(view: rightColumn,withMargin: form_margin).matchParentSideBorders(insetedByDx: form_margin).done() - let descriptionInput = StyledTextView(VoipTheme.conference_scheduling_font, placeHolder:VoipTexts.conference_schedule_description_hint,liveValue: viewModel.description) descriptionInput.textContainer.maximumNumberOfLines = 5 descriptionInput.textContainer.lineBreakMode = .byWordWrapping scheduleForm.addSubview(descriptionInput) @@ -137,7 +138,7 @@ import linphonesw scheduleForm.wrapContentY().done() // Sending methods - let viaChatSwitch = StyledCheckBox(liveValue: viewModel.sendInviteViaChat) + let viaChatSwitch = StyledCheckBox(liveValue: ConferenceSchedulingViewModel.shared.sendInviteViaChat) contentView.addSubview(viaChatSwitch) viaChatSwitch.alignParentLeft(withMargin: form_margin).alignUnder(view: schedulingStack,withMargin: 2*form_margin).done() @@ -145,7 +146,7 @@ import linphonesw contentView.addSubview(viaChatLabel) viaChatLabel.toRightOf(viaChatSwitch,withLeftMargin: form_margin).alignUnder(view: schedulingStack,withMargin: 2*form_margin).alignHorizontalCenterWith(viaChatSwitch).done() - let viaMailSwitch = StyledCheckBox(liveValue: viewModel.sendInviteViaEmail) + let viaMailSwitch = StyledCheckBox(liveValue: ConferenceSchedulingViewModel.shared.sendInviteViaEmail) contentView.addSubview(viaMailSwitch) viaMailSwitch.alignParentLeft(withMargin: form_margin).alignUnder(view: viaChatSwitch,withMargin: 2*form_margin).done() @@ -167,7 +168,7 @@ import linphonesw unencryptedIcon.contentMode = .scaleAspectFit encryptCombo.addArrangedSubview(unencryptedIcon) - let encryptSwitch = StyledSwitch(liveValue: viewModel.isEncrypted) + let encryptSwitch = StyledSwitch(liveValue: ConferenceSchedulingViewModel.shared.isEncrypted) encryptCombo.addArrangedSubview(encryptSwitch) encryptSwitch.centerY().alignParentTop(withMargin: form_margin).done() @@ -188,9 +189,19 @@ import linphonesw } + + 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 = viewModel.selectedAddresses.value!.map { (address) in String(address.asStringUriOnly()) } + 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 @@ -201,6 +212,6 @@ import linphonesw } @objc func resetViewModel() { - viewModel.reset() + ConferenceSchedulingViewModel.shared.reset() } } diff --git a/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift b/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift index f95d11325..7f8a98350 100644 --- a/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift +++ b/Classes/Swift/Conference/Views/ScheduledConferencesCell.swift @@ -107,7 +107,8 @@ class ScheduledConferencesCell: UITableViewCell { contentView.addSubview(participantsIcon) participantsIcon.alignUnder(view: subject,withMargin: 15).square(15).alignParentLeft(withMargin: 10).done() - infoConf.onClick { + //infoConf.onClick { + contentView.onClick { self.conferenceData?.toggleExpand() self.owningTableView?.reloadData() } @@ -161,6 +162,7 @@ class ScheduledConferencesCell: UITableViewCell { joinEditDelete.addArrangedSubview(editConf) editConf.onClick { // TODO + VoipDialog.toast(message: "not available yet") } joinEditDelete.addArrangedSubview(deleteConf) diff --git a/Classes/Swift/Conference/Views/ScheduledConferencesView.swift b/Classes/Swift/Conference/Views/ScheduledConferencesView.swift index 1974404b4..87b604451 100644 --- a/Classes/Swift/Conference/Views/ScheduledConferencesView.swift +++ b/Classes/Swift/Conference/Views/ScheduledConferencesView.swift @@ -37,9 +37,10 @@ import linphonesw backAction: { PhoneMainView.instance().popView(self.compositeViewDescription()) },nextAction: { + ConferenceSchedulingViewModel.shared.reset() PhoneMainView.instance().changeCurrentView(ConferenceSchedulingView.compositeDescription) }, - nextActionEnableCondition: MutableLiveData(true), + nextActionEnableCondition: MutableLiveData(), title:VoipTexts.conference_scheduled) super.nextButton.applyTintedIcons(tintedIcons: VoipTheme.conference_create_button) @@ -70,6 +71,7 @@ import linphonesw 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 diff --git a/Classes/Swift/Voip/Theme/VoipTheme.swift b/Classes/Swift/Voip/Theme/VoipTheme.swift index 28cfe24ba..afe311f6f 100644 --- a/Classes/Swift/Voip/Theme/VoipTheme.swift +++ b/Classes/Swift/Voip/Theme/VoipTheme.swift @@ -391,6 +391,7 @@ class VoipTheme { // Names & values replicated from Android 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/Widgets/StyledValuePicker.swift b/Classes/Swift/Voip/Widgets/StyledValuePicker.swift index f8ca9d3c6..e13a9117e 100644 --- a/Classes/Swift/Voip/Widgets/StyledValuePicker.swift +++ b/Classes/Swift/Voip/Widgets/StyledValuePicker.swift @@ -27,16 +27,20 @@ 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 @@ -65,7 +69,6 @@ class StyledValuePicker: UIView { DropDown.appearance().selectionBackgroundColor = VoipTheme.light_grey_color DropDown.appearance().cellHeight = form_input_height - let dropDown = DropDown() dropDown.anchorView = self dropDown.bottomOffset = CGPoint(x: 0, y:(dropDown.anchorView?.plainView.bounds.height)!) dropDown.dataSource = options @@ -80,21 +83,21 @@ class StyledValuePicker: UIView { } onClick { - dropDown.show() + self.dropDown.show() } height(form_input_height).done() liveIndex.readCurrentAndObserve { (value) in - dropDown.selectRow(value!) + self.dropDown.selectRow(value!) } isUserInteractionEnabled = !readOnly - - } - - + func setIndex(index: Int) { + self.dropDown.selectRow(index) + formattedLabel.text = " "+options[index] + } } From f33ed2f7f93da1aa88c3a337d9b2b4ff6087768d Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Sun, 15 May 2022 21:56:04 +0200 Subject: [PATCH 42/75] Accomodate SDK update --- linphone.xcodeproj/project.pbxproj | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/linphone.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj index 92c00876f..99ed4acce 100644 --- a/linphone.xcodeproj/project.pbxproj +++ b/linphone.xcodeproj/project.pbxproj @@ -609,7 +609,7 @@ 63E27A321C4FECD000D332AE /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 63E27A311C4FECD000D332AE /* LaunchScreen.xib */; }; 63E27A521C50EDB000D332AE /* hold.mkv in Resources */ = {isa = PBXBuildFile; fileRef = 63E27A511C50EB2700D332AE /* hold.mkv */; }; 63E59A3F1ADE70D900646FB3 /* InAppProductsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 63E59A3E1ADE70D900646FB3 /* InAppProductsManager.m */; }; - 63E802DB1C625AEF000D5509 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; + 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 */; }; 63FB30351A680E73008CA393 /* UIRoundedImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 63FB30341A680E73008CA393 /* UIRoundedImageView.m */; }; @@ -2530,7 +2530,7 @@ path = LinphoneUI; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA = { + 29B97314FDCFA39411CA2CEA /* CustomTemplate */ = { isa = PBXGroup; children = ( 8C23BCB71D82AAC3005F19BB /* linphone.entitlements */, @@ -3942,7 +3942,7 @@ fr, hu, ); - mainGroup = 29B97314FDCFA39411CA2CEA; + mainGroup = 29B97314FDCFA39411CA2CEA /* CustomTemplate */; productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; projectRoot = ""; @@ -4009,7 +4009,7 @@ 633FEEE01D3CD55A0014B822 /* numpad_8_over~ipad@2x.png in Resources */, C6710FA42722B20000ED888F /* voip_single_contact_avatar.png in Resources */, 633FEDDC1D3CD5590014B822 /* call_start_body_disabled~ipad.png in Resources */, - 63E802DB1C625AEF000D5509 /* BuildFile in Resources */, + 63E802DB1C625AEF000D5509 /* (null) in Resources */, 633FEE2E1D3CD5590014B822 /* color_F.png in Resources */, C6710FA72722B20000ED888F /* voip_call_more.png in Resources */, 633FEDC51D3CD5590014B822 /* call_hangup_disabled@2x.png in Resources */, @@ -4816,13 +4816,11 @@ "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework", "${BUILT_PRODUCTS_DIR}/linphone-sdk/linphonesw.framework", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/bctoolbox-ios.framework/bctoolbox-ios", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/bctoolbox-tester.framework/bctoolbox-tester", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/bctoolbox.framework/bctoolbox", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/belcard.framework/belcard", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/belle-sip.framework/belle-sip", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/belr.framework/belr", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/lime.framework/lime", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/limetester.framework/limetester", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/linphone.framework/linphone", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/linphonetester.framework/linphonetester", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/mediastreamer2.framework/mediastreamer2", @@ -4834,13 +4832,11 @@ "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/msx264.framework/msx264", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/ortp.framework/ortp", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/bctoolbox-ios.framework/bctoolbox-ios", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/bctoolbox-tester.framework/bctoolbox-tester", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/bctoolbox.framework/bctoolbox", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/belcard.framework/belcard", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/belle-sip.framework/belle-sip", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/belr.framework/belr", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/lime.framework/lime", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/limetester.framework/limetester", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/linphone.framework/linphone", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/linphonetester.framework/linphonetester", "${PODS_XCFRAMEWORKS_BUILD_DIR}/linphone-sdk/all-frameworks/mediastreamer2.framework/mediastreamer2", @@ -4875,13 +4871,11 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SnapKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/linphonesw.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bctoolbox-ios.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bctoolbox-tester.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/bctoolbox.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/belcard.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/belle-sip.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/belr.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/lime.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/limetester.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/linphone.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/linphonetester.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/mediastreamer2.framework", @@ -5691,7 +5685,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.153+9c69668a\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.156+f3dd1fe\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5818,7 +5812,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.153+9c69668a\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.156+f3dd1fe\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5944,7 +5938,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.153+9c69668a\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.156+f3dd1fe\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -6069,7 +6063,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.153+9c69668a\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.156+f3dd1fe\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; From 1a9c6060c2df498b950414f40040c1339d7c8d8e Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 20 May 2022 09:21:25 +0200 Subject: [PATCH 43/75] Stop voice recording in progress if any before accepting a call --- Classes/ChatConversationView.h | 2 ++ Classes/Swift/CallManager.swift | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/Classes/ChatConversationView.h b/Classes/ChatConversationView.h index 7d8fd00dc..b186a849c 100644 --- a/Classes/ChatConversationView.h +++ b/Classes/ChatConversationView.h @@ -159,4 +159,6 @@ -(void) initiateReplyViewForMessage:(LinphoneChatMessage *)message; +-(void) stopVoiceRecording; + @end diff --git a/Classes/Swift/CallManager.swift b/Classes/Swift/CallManager.swift index 444fc8617..cf13510b3 100644 --- a/Classes/Swift/CallManager.swift +++ b/Classes/Swift/CallManager.swift @@ -202,6 +202,10 @@ 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() + } try call.acceptWithParams(params: callParams) } catch { Log.directLog(BCTBX_LOG_ERROR, text: "accept call failed \(error)") From 8c66c4f96acb8d86ecd02b9b741708009fa3a92c Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Thu, 9 Jun 2022 11:47:20 +0200 Subject: [PATCH 44/75] - Updated scheduling/group call tunnel - Synchronized ConferenceViewModel with Android --- .../ConferenceSchedulingViewModel.swift | 20 ++--- .../ConferenceSchedulingSummaryView.swift | 11 +-- .../Views/ConferenceSchedulingView.swift | 20 +++-- Classes/Swift/Voip/Theme/VoipTexts.swift | 11 ++- Classes/Swift/Voip/ViewModels/CallData.swift | 2 +- .../Voip/ViewModels/ConferenceViewModel.swift | 90 +++++++++++-------- 6 files changed, 88 insertions(+), 66 deletions(-) diff --git a/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift b/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift index 57a9d015b..642b319eb 100644 --- a/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift +++ b/Classes/Swift/Conference/ViewModels/ConferenceSchedulingViewModel.swift @@ -167,6 +167,9 @@ class ConferenceSchedulingViewModel { }.first continueEnabled.value = false + + selectedAddresses.value = [] + } @@ -190,23 +193,11 @@ class ConferenceSchedulingViewModel { do { conferenceCreationInProgress.value = true - guard let localAddress = core.defaultAccount?.params?.identityAddress else { + guard let localAccount = core.defaultAccount, let localAddress = localAccount.params?.identityAddress else { Log.e("[Conference Creation] Couldn't get local address from default account!") return } - - /* - // TODO: Temporary workaround for chat room, to be removed once we can get matching chat room from conference - let chatRoomParams = try core.createDefaultChatRoomParams() - chatRoomParams.backend = ChatRoomBackend.FlexisipChat - chatRoomParams.groupEnabled = true - chatRoomParams.subject = subject.value! - let chatRoom = try core.createChatRoom(params: chatRoomParams, localAddr: localAddress, participants: selectedAddresses.value!) - Log.i("[Conference Creation] Creating chat room with same subject [\(subject.value)] & participants as for conference") - chatRoom.addDelegate(delegate: chatRooomDelegate!) - // END OF TODO -*/ - + let conferenceInfo = try Factory.Instance.createConferenceInfo() conferenceInfo.organizer = localAddress subject.value.map { conferenceInfo.subject = $0} @@ -217,6 +208,7 @@ class ConferenceSchedulingViewModel { 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 } catch { diff --git a/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift b/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift index 8807c571c..906cc0934 100644 --- a/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift +++ b/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift @@ -140,7 +140,7 @@ import SVProgressHUD 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 + viaChatLabel.isHidden = sendChat != true || ConferenceSchedulingViewModel.shared.scheduleForLater.value != true } // Participants @@ -173,8 +173,8 @@ import SVProgressHUD let createButton = FormButton(backgroundStateColors: VoipTheme.primary_colors_background) contentView.addSubview(createButton) ConferenceSchedulingViewModel.shared.scheduleForLater.readCurrentAndObserve { _ in - createButton.title = ConferenceSchedulingViewModel.shared.scheduleForLater.value == true ? VoipTexts.conference_schedule.uppercased() : VoipTexts.conference_schedule_create.uppercased() - createButton.addSidePadding() + createButton.title = ConferenceSchedulingViewModel.shared.scheduleForLater.value == true ? VoipTexts.conference_schedule_start.uppercased() : VoipTexts.conference_group_call_create.uppercased() + createButton.addSidePadding() } ConferenceSchedulingViewModel.shared.conferenceCreationInProgress.observe { progress in @@ -211,8 +211,9 @@ import SVProgressHUD } } } - ConferenceSchedulingViewModel.shared.scheduleForLater.readCurrentAndObserve { _ in - createButton.title = ConferenceSchedulingViewModel.shared.scheduleForLater.value == true ? VoipTexts.conference_schedule.uppercased() : VoipTexts.conference_schedule_create.uppercased() + ConferenceSchedulingViewModel.shared.scheduleForLater.readCurrentAndObserve { (later) in + 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 createButton.addSidePadding() } diff --git a/Classes/Swift/Conference/Views/ConferenceSchedulingView.swift b/Classes/Swift/Conference/Views/ConferenceSchedulingView.swift index 27f0934eb..c2e85d1ef 100644 --- a/Classes/Swift/Conference/Views/ConferenceSchedulingView.swift +++ b/Classes/Swift/Conference/Views/ConferenceSchedulingView.swift @@ -45,7 +45,7 @@ import linphonesw self.gotoParticipantsListSelection() }, nextActionEnableCondition: ConferenceSchedulingViewModel.shared.continueEnabled, - title:VoipTexts.conference_schedule_title) + title:VoipTexts.conference_group_call_title) let subjectLabel = StyledLabel(VoipTheme.conference_scheduling_font, VoipTexts.conference_schedule_subject_title) subjectLabel.addIndicatorIcon(iconName: "voip_mandatory") @@ -78,7 +78,6 @@ import linphonesw 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() @@ -146,6 +145,8 @@ import linphonesw 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() @@ -153,7 +154,6 @@ import linphonesw 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) @@ -175,17 +175,27 @@ import linphonesw 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: encryptCombo,withMargin: 4*form_margin).centerX().matchParentSideBorders().done() + 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 + } + } diff --git a/Classes/Swift/Voip/Theme/VoipTexts.swift b/Classes/Swift/Voip/Theme/VoipTexts.swift index 8d4ba6618..9a8ef3428 100644 --- a/Classes/Swift/Voip/Theme/VoipTexts.swift +++ b/Classes/Swift/Voip/Theme/VoipTexts.swift @@ -59,7 +59,8 @@ import UIKit static let call_remotely_paused_title = NSLocalizedString("Call has been paused by remote.",comment:"") // Conference - static let conference_schedule_title = NSLocalizedString("Start a conference",comment:"") + static let conference_schedule_title = NSLocalizedString("Schedule a meeting",comment:"") + static let conference_group_call_title = NSLocalizedString("Start a group call",comment:"") static let conference_schedule_later = NSLocalizedString("Do you want to schedule this conference for later?",comment:"") static let conference_schedule_mandatory_field = NSLocalizedString("Mandatory",comment:"") static let conference_schedule_subject_title = NSLocalizedString("Subject",comment:"") @@ -77,15 +78,17 @@ import UIKit static let conference_schedule_send_invite_chat_summary = NSLocalizedString("Invite will be sent out from my \(appName) account",comment:"") static let conference_schedule_participants_list = NSLocalizedString("Participants list",comment:"") static let conference_schedule_summary = NSLocalizedString("Conference info",comment:"") - static let conference_schedule_create = NSLocalizedString("Create conference",comment:"") + static let conference_schedule_start = NSLocalizedString("Schedule meeting",comment:"") + static let conference_group_call_create = NSLocalizedString("Start group call",comment:"") + static let conference_schedule = NSLocalizedString("Schedule conference",comment:"") static let conference_schedule_address_copied_to_clipboard = NSLocalizedString("Conference address copied into clipboard",comment:"") static let conference_schedule_creation_failure = NSLocalizedString("Failed to create conference!",comment:"") static let conference_schedule_info_not_sent_to_participant = NSLocalizedString("Failed to send conference info to a participant",comment:"") static let conference_paused_title = NSLocalizedString("You are currently out of the conference.",comment:"") static let conference_paused_subtitle = NSLocalizedString("Click on play button to join it back.",comment:"") - static let conference_default_title = NSLocalizedString("Remote conference",comment:"") - static let conference_local_title = NSLocalizedString("Local conference",comment:"") + static let conference_default_title = NSLocalizedString("Remote group call",comment:"") + static let conference_local_title = NSLocalizedString("Local group call",comment:"") static let conference_invite_title = NSLocalizedString("Conference invite:",comment:"") static let conference_description_title = NSLocalizedString("Description:",comment:"") static let conference_invite_join = NSLocalizedString("Join",comment:"") diff --git a/Classes/Swift/Voip/ViewModels/CallData.swift b/Classes/Swift/Voip/ViewModels/CallData.swift index 14dbb09ea..7affaea99 100644 --- a/Classes/Swift/Voip/ViewModels/CallData.swift +++ b/Classes/Swift/Voip/ViewModels/CallData.swift @@ -90,7 +90,7 @@ class CallData { let conference = call.conference isInRemoteConference.value = conference != nil || isCallingAConference() if (conference != nil) { - remoteConferenceSubject.value = conference?.subject != nil && (conference?.subject.count)! > 0 ? conference!.subject : VoipTexts.conference_default_title + remoteConferenceSubject.value = ConferenceViewModel.getConferenceSubject(conference: conference!) } isOutgoing.value = isOutGoing() isIncoming.value = isInComing() diff --git a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift index 8b57549d3..cf52a8964 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceViewModel.swift @@ -37,17 +37,21 @@ class ConferenceViewModel { let conferenceCreationPending = MutableLiveData() let conferenceParticipants = MutableLiveData<[ConferenceParticipantData]>() let conferenceParticipantDevices = MutableLiveData<[ConferenceParticipantDeviceData]>() - let conferenceDisplayMode = MutableLiveData() + let conferenceDisplayMode = MutableLiveData() let isRecording = MutableLiveData() let isRemotelyRecorded = MutableLiveData() - let participantAdminStatusChangedEvent = 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? @@ -66,6 +70,9 @@ class ConferenceViewModel { 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") @@ -100,7 +107,7 @@ class ConferenceViewModel { }, onStateChanged: { (conference: Conference, state: Conference.State) in Log.i("[Conference] State changed: \(state)") - self.isVideoConference.value = conference.currentParams?.isVideoEnabled + self.isVideoConference.value = conference.currentParams?.videoEnabled if (state == .Created) { self.configureConference(conference) self.conferenceCreationPending.value = false @@ -141,8 +148,6 @@ class ConferenceViewModel { Core.get().addDelegate(delegate: coreDelegate!) conferenceParticipants.value = [] conferenceParticipantDevices.value = [] - conferenceDisplayMode.value = .Grid - subject.value = VoipTexts.conference_default_title if let conference = core.conference != nil ? core.conference : core.currentCall?.conference { Log.i("[Conference] Found an existing conference: \(conference) in state \(conference.state)") @@ -182,42 +187,30 @@ class ConferenceViewModel { 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) - - if let call = core.currentCall, CallManager.getAppData(call: call.getCobject!)?.isConference == true { // Apply waiting room preference - if (ConferenceWaitingRoomViewModel.sharedModel.isSpeakerSelected.value == true) { - ControlsViewModel.shared.forceSpeakerAudioRoute() - } else { - ControlsViewModel.shared.forceEarpieceAudioRoute() - ControlsViewModel.shared.updateUI() - } - Core.get().micEnabled = ConferenceWaitingRoomViewModel.sharedModel.isMicrophoneMuted.value != true - changeLayout(layout: ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value!) - 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 - self.subject.value = conference.subject.isEmpty ? ( - conference.me?.isFocus == true ? ( - VoipTexts.conference_local_title - ) : ( - VoipTexts.conference_default_title - ) - ) : ( - conference.subject - ) + subject.value = ConferenceViewModel.getConferenceSubject(conference: conference) + updateConferenceLayout(conference: conference) + } @@ -238,12 +231,13 @@ class ConferenceViewModel { } - func changeLayout(layout: ConferenceLayout) { + 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 = true // TODO AUdioLonly layout != ConferenceDisplayMode.AUDIO_ONLY - params.conferenceVideoLayout = layout + 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 @@ -254,7 +248,7 @@ class ConferenceViewModel { private func updateConferenceLayout(conference: Conference) { if let call = conference.call, let params = call.params { - conferenceDisplayMode.value = params.conferenceVideoLayout + 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)") @@ -275,7 +269,7 @@ class ConferenceViewModel { conferenceParticipantDevices.value = [] } - + private func updateParticipantsList(_ conference: Conference) { self.conferenceParticipants.value?.forEach{ $0.destroy()} var participants :[ConferenceParticipantData] = [] @@ -370,7 +364,7 @@ class ConferenceViewModel { return devices } - + func togglePlayPause () { if (isConferenceLocallyPaused.value == true) { resumeConference() @@ -412,6 +406,25 @@ class ConferenceViewModel { } } + 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 { @@ -425,12 +438,15 @@ class ConferenceViewModel { } - - - enum FlexDirection { case ROW case ROW_REVERSE case COLUMN case COLUMN_REVERSE } + +enum ConferenceDisplayMode { + case Grid + case ActiveSpeaker + case AudioOnly +} From 698bb7882a7420bef479192d024972a086aac635 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 10 Jun 2022 11:07:58 +0200 Subject: [PATCH 45/75] First/Last to join conference Support simulator for play pause Removed unused code (chat room access in conference) --- Classes/Swift/CallManager.swift | 9 ++++ .../ConferenceSchedulingSummaryView.swift | 6 +-- Classes/Swift/Voip/Theme/VoipTexts.swift | 7 ++- Classes/Swift/Voip/ViewModels/CallData.swift | 54 ------------------- .../ActiveCallOrConferenceView.swift | 15 +++++- 5 files changed, 30 insertions(+), 61 deletions(-) diff --git a/Classes/Swift/CallManager.swift b/Classes/Swift/CallManager.swift index cf13510b3..322574a9d 100644 --- a/Classes/Swift/CallManager.swift +++ b/Classes/Swift/CallManager.swift @@ -349,6 +349,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) { @@ -358,6 +366,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) { diff --git a/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift b/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift index 906cc0934..270cb05b8 100644 --- a/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift +++ b/Classes/Swift/Conference/Views/ConferenceSchedulingSummaryView.swift @@ -140,7 +140,7 @@ import SVProgressHUD 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 + viaChatLabel.isHidden = sendChat != true || ConferenceSchedulingViewModel.shared.scheduleForLater.value != true } // Participants @@ -191,10 +191,6 @@ import SVProgressHUD enableCreationTimeOut = false if (ConferenceSchedulingViewModel.shared.scheduleForLater.value == true) { PhoneMainView.instance().pop(toView:ScheduledConferencesView.compositeDescription) - } else { - let view: ConferenceWaitingRoomFragment = self.VIEW(ConferenceWaitingRoomFragment.compositeViewDescription()); - PhoneMainView.instance().pop(toView:view.compositeViewDescription()) - view.setDetails(subject: pair!.second!, url: pair!.first!) } } ConferenceSchedulingViewModel.shared.onErrorEvent.observe { error in diff --git a/Classes/Swift/Voip/Theme/VoipTexts.swift b/Classes/Swift/Voip/Theme/VoipTexts.swift index 9a8ef3428..bad54b4ab 100644 --- a/Classes/Swift/Voip/Theme/VoipTexts.swift +++ b/Classes/Swift/Voip/Theme/VoipTexts.swift @@ -37,7 +37,7 @@ import UIKit static let call_error_incompatible_media_params = NSLocalizedString("Incompatible media parameters",comment:"") static let call_error_network_unreachable = NSLocalizedString("Network is unreachable",comment:"") static let call_error_io_error = NSLocalizedString("Service unavailable or network error",comment:"") - static let call_error_server_timeout = NSLocalizedString("Server timeout",comment:"") + static let call_error_server_timeout = NSLocalizedString("Server error, please try again later.",comment:"") static let call_error_temporarily_unavailable = NSLocalizedString("Temporarily unavailable",comment:"") static let call_error_generic = NSLocalizedString("Error: %s",comment:"") static let call_video_update_requested_dialog = NSLocalizedString("Correspondent would like to turn the video on",comment:"") @@ -109,6 +109,11 @@ import UIKit static let conference_creation_failed = NSLocalizedString("Failed to create conference",comment:"") static let conference_info_confirm_removal = NSLocalizedString("Do you really want to delete this conference?",comment:"") static let conference_info_confirm_removal_delete = NSLocalizedString("Delete",comment:"") + static let conference_last_user = NSLocalizedString("All other participants have left the group call",comment:"") + static let conference_first_to_join = NSLocalizedString("You're the first to join the group call",comment:"") + + + // Call Stats diff --git a/Classes/Swift/Voip/ViewModels/CallData.swift b/Classes/Swift/Voip/ViewModels/CallData.swift index 7affaea99..b83c9742f 100644 --- a/Classes/Swift/Voip/ViewModels/CallData.swift +++ b/Classes/Swift/Voip/ViewModels/CallData.swift @@ -59,7 +59,6 @@ class CallData { ) call.addDelegate(delegate: callDelegate!) update() - initChatRoom() } @@ -104,59 +103,6 @@ class CallData { callState.value = call.state } - private func initChatRoom() { - - return // V1 work around - - let localSipUri = Core.get().defaultAccount?.params?.identityAddress?.asStringUriOnly() - let remoteSipUri = call.remoteAddress?.asStringUriOnly() - let conference = call.conference - - - guard - let localSipUri = Core.get().defaultAccount?.params?.identityAddress?.asStringUriOnly(), - let remoteSipUri = call.remoteAddress?.asStringUriOnly(), - let localAddress = try?Factory.Instance.createAddress(addr: localSipUri), - let remoteSipAddress = try?Factory.Instance.createAddress(addr: remoteSipUri) - else { - Log.e("[Call] Failed to get either local \(localSipUri.orNil) or remote \(remoteSipUri.orNil) SIP address!") - return - } - do { - if let conferenceInfo = Core.get().findConferenceInformationFromUri(uri: call.remoteAddress!), let params = try?Core.get().createDefaultChatRoomParams() { - params.subject = conferenceInfo.subject - params.backend = ChatRoomBackend.FlexisipChat - params.groupEnabled = true - chatRoom = Core.get().searchChatRoom(params: params, localAddr: localAddress, remoteAddr: nil, participants: conferenceInfo.participants) - } else { - chatRoom = Core.get().searchChatRoom(params: nil, localAddr: localAddress, remoteAddr: remoteSipAddress, participants: []) - if (chatRoom == nil) { - chatRoom = Core.get().searchChatRoom(params: nil, localAddr: localAddress, remoteAddr: nil, participants: [remoteSipAddress]) - } - } - - if (chatRoom == nil) { - let chatRoomParams = try Core.get().createDefaultChatRoomParams() - if let conferenceInfo = Core.get().findConferenceInformationFromUri(uri: call.remoteAddress!) { - Log.w("[Call] Failed to find existing chat room with same subject & participants, creating it") - chatRoomParams.backend = ChatRoomBackend.FlexisipChat - chatRoomParams.groupEnabled = true - chatRoomParams.subject = conferenceInfo.subject - chatRoom = try?Core.get().createChatRoom(params: chatRoomParams, localAddr: localAddress, participants: conferenceInfo.participants) - } else { - Log.w("[Call] Failed to find existing chat room with same participants, creating it") - // TODO: configure chat room params - chatRoom = try?Core.get().createChatRoom(params: chatRoomParams, localAddr: localAddress, participants: [remoteSipAddress]) - } - } - - if (chatRoom == nil) { - Log.e("[Call] Failed to create a chat room for local address \(localSipUri) and remote address \(remoteSipUri)!") - } - } catch { - Log.e("[Call] Exception caught initiating a chat room for local address \(localSipUri) and remote address \(remoteSipUri) Error : \(error)!") - } - } func sendDTMF(dtmf:String) { enteredDTMF.value = enteredDTMF.value! + dtmf diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index 5cce8a48a..44add5d0b 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -276,7 +276,20 @@ import linphonesw 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) + } + } + } override func viewWillAppear(_ animated: Bool) { From 8c9772ae4b6ea8cf85c07407377c0ba78eb494dc Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 10 Jun 2022 11:16:19 +0200 Subject: [PATCH 46/75] Match Record button style with Pause button style --- Classes/Swift/Voip/Theme/VoipTheme.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Classes/Swift/Voip/Theme/VoipTheme.swift b/Classes/Swift/Voip/Theme/VoipTheme.swift index afe311f6f..eace5a9a9 100644 --- a/Classes/Swift/Voip/Theme/VoipTheme.swift +++ b/Classes/Swift/Voip/Theme/VoipTheme.swift @@ -213,11 +213,9 @@ class VoipTheme { // Names & values replicated from Android static let call_record = ButtonTheme( tintableStateIcons:[ - UIButton.State.normal.rawValue : TintableIcon(name: "voip_call_record",tintColor: LightDarkColor(voip_gray_blue_color,voip_gray_blue_color)), - UIButton.State.selected.rawValue : TintableIcon(name: "voip_call_record",tintColor: LightDarkColor(.white,.white)), - UIButton.State.highlighted.rawValue : TintableIcon(name: "voip_call_record",tintColor: LightDarkColor(primary_color,primary_color)), + UIButton.State.normal.rawValue : TintableIcon(name: "voip_call_record",tintColor: LightDarkColor(.white,.white)), ], - backgroundStateColors: button_call_recording_background) + backgroundStateColors: button_toggle_background) static let call_pause = ButtonTheme( tintableStateIcons:[ From 12e623e176b388f7859296b94d9791fdd6192c30 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 10 Jun 2022 11:26:47 +0200 Subject: [PATCH 47/75] Change burger icon in call's list actions --- Resources/images/voip_call_list_menu.png | Bin 750 -> 1895 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Resources/images/voip_call_list_menu.png b/Resources/images/voip_call_list_menu.png index fdd82bea690184cd11af45f15cdccb4bf2a5e7b2..b4e2ff3bfe4c743a755296729ce33507ca0e58b6 100644 GIT binary patch delta 1808 zcmV+r2k-dq1?LWsJ%2zP#a~m4B2@=F6miJVI$01E5htxe5h|2gp;ZTyOTVB=Lz3d+ zD7Y3J{8+3yxH#+T;3^1$A0UoSPKqv4;{TFDix>}%`|nZq35PxSJG=1A!OY{)htIyFjyU+uz5w-8=#O&%l+|_E#Ig^e5@{ zwiY=82DX8V>$WEE0hc>K_(_)x$&vgtg+c*%KcjET0fV1k&00009a7bBm z000fw000fw0YWI7cmMzZ2XskIMF-{u7!Eow)naTF000GJNkl7ip9LMqB ze|ER!RZMBcmmmf_nHZF$Z6XCF?a2rU8aNmeHIb@dF(g%JOKmsOEpH*=r68cec(ef$ zn-~(amn*g=wkrv0ynqU#A`~bhinQIC$3q(`l$LgPFJ``xj&Xy)cd@1ZkePKNe>Kt^t+^PGy802^v2k3N@xRHHqhB z+9_%7T@L+GPHS75YdBZtj6+x;a*-m;C>ee#VCDuyX zGQnBfOj=UBxZ-lEBiT)gobn_|Z*FLw>v`^bNq>_v9HKAc`H%0~RDUY$S$fn-@*-|y z7G;v8{IrX#CrM@~RaWkg#R@O?NV+%6p?V8reY3)T@h|0WE#~)7sE~74XPlLEEeAvWIWy}2LZL!#_wKcQlG?H^ zrhn|S#>U2e3I%yiO_Bz(7$O}g*g}%rDVHR5cP1|uFRGA9OER3E>i0F*9VST%byKP% z**ye}oqp5Rx{`G0{e z{9tp<8B*k#a!FD{LqpW$wPjEyNqW$WxFai8e9FU$6l&0={`^&PnxFAB@APERqd&Yx zk`!w2;?2{ltk(`E3dI^qs;Ws81*V+5@jDR9D>@Lbz7*y>Lq9{Mo#kwUi`YOPn`MMP z058bd4N4~k6e{ciILp<31kQ3d7Jr5MzuMhgd(lkCb1+n@rMdPpg@PpeF#D@K`-mJV zlqBiG$$1AQoyg*r!SM?x=TRsMHK?_9rSGL9OC@Ep20G^yywKXZ(kDp@HN2MEy8c)5 z{HG_lo=TbLm$fu){FwwP)ChYv)t@em^*`mz)<`-t#-V5Aw7Mv-zlL;pO;xpU53*SBwkdtlL)*8%^EYWxsIkjb+w_V_+axQz%b^#YT=m{W-6y0rjMt}c za`skD{%)BiB%3mhFHAMr@_&-D@`6-n@)-G1sM}_yNewc+o537kT2fZ-r#h4Eq{t~x zl2lcbcq}lTkQB>sh_pq#1$*A#@Lkxm(j=c?LTzw=5Ws;=nz3IptPI+Muz>gH(1nr;>8tLO?o!7$w_awBsP9Qk`(HuR7bMAcu_@Q(lZ+W{W)i{`j+O} z9VAGh?o_JwlCtuYOf{vF3dWw4?34m~sinE*5Xt@LN|y*mYG`PPUQSP19yl#Cxe`ea y4Sn>ROkE~xn;E@vltnm`Ap#s50001BO#cF2c8lFjXbc+w0000*o ztDLtuYo!Wn^vPcs&gsibT&EgD0t;A#1Q7}<*gy$3Vzld|SV+-%+{ZuQ`XzEHKY#c=TPr^??j?oeK>Lg1d<+4hU7%idobO}DshQiya5gl zf#CvWulc;Yqjhfo-f7J52RBc0kFr2gD*ylh24YJ`L}vg100000`9r&Z00009a7bBm z000fw000fw0YWI7cmMzZ2XskIMF-^x5D6$2aSSyJ0002$Nkl;&%dI5QpF$WqK@P{A6mNrt|4XD}x0000 Date: Fri, 10 Jun 2022 11:32:04 +0200 Subject: [PATCH 48/75] Add call padding --- .../Swift/Voip/Views/Fragments/CallsList/CallsListView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Swift/Voip/Views/Fragments/CallsList/CallsListView.swift b/Classes/Swift/Voip/Views/Fragments/CallsList/CallsListView.swift index 33fdcf922..57324d918 100644 --- a/Classes/Swift/Voip/Views/Fragments/CallsList/CallsListView.swift +++ b/Classes/Swift/Voip/Views/Fragments/CallsList/CallsListView.swift @@ -39,7 +39,7 @@ import linphonesw super.init(title: VoipTexts.call_action_calls_list) // New Call - let newCall = CallControlButton(width: buttons_size,height: buttons_size, buttonTheme: VoipTheme.call_add, onClickAction: { + 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 From e8274983cdb45619b95f597fd4126fb84002f25d Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 10 Jun 2022 11:48:16 +0200 Subject: [PATCH 49/75] Join conference from history --- Classes/HistoryListTableView.m | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Classes/HistoryListTableView.m b/Classes/HistoryListTableView.m index 97f50fbce..2876d4a1a 100644 --- a/Classes/HistoryListTableView.m +++ b/Classes/HistoryListTableView.m @@ -22,6 +22,8 @@ #import "LinphoneManager.h" #import "PhoneMainView.h" #import "Utils.h" +#import "linphoneapp-Swift.h" + @implementation HistoryListTableView @@ -264,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]; + } } } } From 64d33a67fbcea974f9e37641c13d1d6fe95ca001 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 10 Jun 2022 12:21:53 +0200 Subject: [PATCH 50/75] Pause label remove from active speaker stickers, et add spacing on grid ones --- .../ViewModels/ConferenceWaitingRoomViewModel.swift | 2 +- .../Conference/VoipActiveSpeakerParticipantCell.swift | 5 +++-- .../Views/Fragments/Conference/VoipGridParticipantCell.swift | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift b/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift index 246f5f905..4b00f24dd 100644 --- a/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift +++ b/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift @@ -39,7 +39,7 @@ class ConferenceWaitingRoomViewModel: ControlsViewModel { } func reset() { - joinLayout.value = .Grid + joinLayout.value = .ActiveSpeaker // TODO add setting joinInProgress.value = false isMicrophoneMuted.value = !micAuthorized() isMuteMicrophoneEnabled.value = true diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift index 4e74b6b5b..707a3baf1 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipActiveSpeakerParticipantCell.swift @@ -124,8 +124,9 @@ class VoipActiveSpeakerParticipantCell: UICollectionViewCell { contentView.addSubview(displayName) displayName.matchParentSideBorders(insetedByDx:ActiveCallView.bottom_displayname_margin_left).alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done() - contentView.addSubview(pauseLabel) - pauseLabel.toRightOf(displayName).alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done() + // Paused label commented out (Android 10.06.2022) + // contentView.addSubview(pauseLabel) + //pauseLabel.toRightOf(displayName).alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done() contentView.matchParentDimmensions().done() makeHeightMatchWidth().done() diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift index 82aa1e10f..ffa83dd39 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipGridParticipantCell.swift @@ -30,6 +30,7 @@ class VoipGridParticipantCell: UICollectionViewCell { 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 let videoView = UIView() @@ -129,7 +130,7 @@ class VoipGridParticipantCell: UICollectionViewCell { displayName.alignParentLeft(withMargin:ActiveCallView.bottom_displayname_margin_left).alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done() contentView.addSubview(pauseLabel) - pauseLabel.toRightOf(displayName).alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done() + pauseLabel.toRightOf(displayName,withLeftMargin: pause_label_left_margin).alignParentBottom(withMargin:ActiveCallView.bottom_displayname_margin_bottom).done() contentView.matchParentDimmensions().done() } From ecad9c86b353418ea3f700287e4367b70b581a6d Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Fri, 10 Jun 2022 16:50:34 +0200 Subject: [PATCH 51/75] Incoming calls from Conference serveur handling - (CallKit) --- Classes/Swift/CallManager.swift | 40 +++++++++++- Classes/Swift/Voip/Theme/VoipTexts.swift | 4 +- Classes/Swift/Voip/ViewModels/CallData.swift | 64 ++++++++++++++++--- .../ActiveCallOrConferenceView.swift | 2 +- 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/Classes/Swift/CallManager.swift b/Classes/Swift/CallManager.swift index 322574a9d..c12826d17 100644 --- a/Classes/Swift/CallManager.swift +++ b/Classes/Swift/CallManager.swift @@ -206,6 +206,15 @@ import AVFoundation 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)") @@ -430,6 +439,11 @@ import AVFoundation CallManager.instance().endCallkit = false } } + + 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) { let callLog = call.callLog @@ -452,9 +466,31 @@ import AVFoundation 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. diff --git a/Classes/Swift/Voip/Theme/VoipTexts.swift b/Classes/Swift/Voip/Theme/VoipTexts.swift index bad54b4ab..303d1aa2e 100644 --- a/Classes/Swift/Voip/Theme/VoipTexts.swift +++ b/Classes/Swift/Voip/Theme/VoipTexts.swift @@ -111,10 +111,10 @@ import UIKit static let conference_info_confirm_removal_delete = NSLocalizedString("Delete",comment:"") static let conference_last_user = NSLocalizedString("All other participants have left the group call",comment:"") static let conference_first_to_join = NSLocalizedString("You're the first to join the group call",comment:"") + static let conference_incoming_title = NSLocalizedString("Incoming group call",comment:"") + static let conference_participants_title = NSLocalizedString("%d participants",comment:"") - - // Call Stats static let call_stats_audio = "Audio" diff --git a/Classes/Swift/Voip/ViewModels/CallData.swift b/Classes/Swift/Voip/ViewModels/CallData.swift index b83c9742f..c70a5cc8b 100644 --- a/Classes/Swift/Voip/ViewModels/CallData.swift +++ b/Classes/Swift/Voip/ViewModels/CallData.swift @@ -32,6 +32,11 @@ class CallData { 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() @@ -58,6 +63,14 @@ class CallData { } ) 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() } @@ -86,13 +99,12 @@ class CallData { isPaused.value = isCallPaused() isRemotelyPaused.value = isCallRemotelyPaused() canBePaused.value = canCallBePaused() - let conference = call.conference - isInRemoteConference.value = conference != nil || isCallingAConference() - if (conference != nil) { - remoteConferenceSubject.value = ConferenceViewModel.getConferenceSubject(conference: conference!) - } + + updateConferenceInfo() + isOutgoing.value = isOutGoing() isIncoming.value = isInComing() + if (call.mediaInProgress()) { DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) { self.update() @@ -103,6 +115,43 @@ class CallData { 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 @@ -150,8 +199,5 @@ class CallData { isPaused.value = isCallPaused() } - func isCallingAConference() -> Bool { - return CallManager.getAppData(call: call.getCobject!)?.isConference == true - } - + } diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index 44add5d0b..edba6f0e9 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -328,7 +328,7 @@ import linphonesw if (data?.isOutgoing.value == true || data?.isIncoming.value == true) { PhoneMainView.instance().popView(self.compositeViewDescription()) } else { - if (data!.isCallingAConference()) { + if (data!.isInRemoteConference.value == true) { PhoneMainView.instance().pop(toView: self.compositeViewDescription()) } else { PhoneMainView.instance().changeCurrentView(self.compositeViewDescription()) From 876f22da36bf0c9b5408487ae626643e1134809e Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Mon, 13 Jun 2022 18:45:59 +0200 Subject: [PATCH 52/75] Audio only layout & assorted fixes --- Classes/Swift/CallManager.swift | 10 +- .../ConferenceWaitingRoomViewModel.swift | 6 +- .../Views/ConferenceWaitingRoomFragment.swift | 5 +- .../LinphoneCore/AddressExtensions.swift | 4 +- Classes/Swift/Voip/Theme/VoipTexts.swift | 2 +- Classes/Swift/Voip/Theme/VoipTheme.swift | 4 +- .../ConferenceParticipantDeviceData.swift | 26 ++- .../ActiveCallOrConferenceView.swift | 50 ++-- .../VoipAudioOnlyParticipantCell.swift | 110 +++++++++ .../VoipConferenceAudioOnlyView.swift | 216 ++++++++++++++++++ ...ipConferenceDisplayModeSelectionView.swift | 16 +- .../Conference/VoipConferenceGridView.swift | 8 +- .../ConferenceLayoutPickerView.swift | 5 +- 13 files changed, 418 insertions(+), 44 deletions(-) create mode 100644 Classes/Swift/Voip/Views/Fragments/Conference/VoipAudioOnlyParticipantCell.swift create mode 100644 Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceAudioOnlyView.swift diff --git a/Classes/Swift/CallManager.swift b/Classes/Swift/CallManager.swift index c12826d17..9c3a5b1c4 100644 --- a/Classes/Swift/CallManager.swift +++ b/Classes/Swift/CallManager.swift @@ -286,9 +286,13 @@ import AVFoundation lcallParams.mediaEncryption = .ZRTP } if (isConference) { - lcallParams.videoEnabled = true - lcallParams.videoDirection = ConferenceWaitingRoomViewModel.sharedModel.isVideoEnabled.value == true ? .SendRecv : .RecvOnly - lcallParams.conferenceVideoLayout = ConferenceWaitingRoomViewModel.sharedModel.joinLayout.value! + 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 } diff --git a/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift b/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift index 4b00f24dd..14b34a5eb 100644 --- a/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift +++ b/Classes/Swift/Conference/ViewModels/ConferenceWaitingRoomViewModel.swift @@ -28,7 +28,7 @@ class ConferenceWaitingRoomViewModel: ControlsViewModel { static let sharedModel = ConferenceWaitingRoomViewModel() - let joinLayout = MutableLiveData() + let joinLayout = MutableLiveData() let joinInProgress = MutableLiveData(false) let showLayoutPicker = MutableLiveData() @@ -39,12 +39,12 @@ class ConferenceWaitingRoomViewModel: ControlsViewModel { } func reset() { - joinLayout.value = .ActiveSpeaker // TODO add setting + joinLayout.value = Core.get().defaultConferenceLayout == .Grid ? .Grid : .ActiveSpeaker joinInProgress.value = false isMicrophoneMuted.value = !micAuthorized() isMuteMicrophoneEnabled.value = true isSpeakerSelected.value = true - isVideoEnabled.value = true + isVideoEnabled.value = false isVideoAvailable.value = core.videoCaptureEnabled showLayoutPicker.value = false } diff --git a/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift index 4962b173a..a7c6f0de9 100644 --- a/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift +++ b/Classes/Swift/Conference/Views/ConferenceWaitingRoomFragment.swift @@ -80,7 +80,10 @@ import linphonesw switch (layout!) { case .Grid: icon = "voip_conference_mosaic"; break case .ActiveSpeaker: icon = "voip_conference_active_speaker"; break - // Todo audio only case .Legacy: icon = "voip_conference_audio_only"; 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))]) } diff --git a/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift b/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift index d4aff4ef7..39bd741f8 100644 --- a/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift +++ b/Classes/Swift/Extensions/LinphoneCore/AddressExtensions.swift @@ -39,9 +39,7 @@ extension Address { } func addressBookEnhancedDisplayName() -> String? { - if (username == Core.get().defaultAccount?.contactAddress?.username) { - return VoipTexts.me - } else if let contact = FastAddressBook.getContactWith(getCobject) { + if let contact = FastAddressBook.getContactWith(getCobject) { return contact.displayName } else if (!displayName.isEmpty) { return displayName diff --git a/Classes/Swift/Voip/Theme/VoipTexts.swift b/Classes/Swift/Voip/Theme/VoipTexts.swift index 303d1aa2e..da85fae6c 100644 --- a/Classes/Swift/Voip/Theme/VoipTexts.swift +++ b/Classes/Swift/Voip/Theme/VoipTexts.swift @@ -23,7 +23,6 @@ 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 - static let me = NSLocalizedString("Me",comment:"") // Calls static let call_incoming_title = NSLocalizedString("Incoming Call",comment:"") @@ -95,6 +94,7 @@ import UIKit static let conference_invite_participants_count = NSLocalizedString("%d participants",comment:"") static let conference_display_mode_mosaic = NSLocalizedString("Mosaic mode",comment:"") static let conference_display_mode_active_speaker = NSLocalizedString("Active speaker mode",comment:"") + static let conference_display_mode_audio_only = NSLocalizedString("Audio only mode",comment:"") static let conference_display_no_active_speaker = NSLocalizedString("No active speaker",comment:"") static let conference_waiting_room_start_call = NSLocalizedString("Start",comment:"") static let conference_waiting_room_cancel_call = NSLocalizedString("Cancel",comment:"") diff --git a/Classes/Swift/Voip/Theme/VoipTheme.swift b/Classes/Swift/Voip/Theme/VoipTheme.swift index eace5a9a9..029ef1e62 100644 --- a/Classes/Swift/Voip/Theme/VoipTheme.swift +++ b/Classes/Swift/Voip/Theme/VoipTheme.swift @@ -27,7 +27,7 @@ class VoipTheme { // Names & values replicated from Android static let voip_light_gray = UIColor(hex:"#D0D8DE") static let voip_dark_gray = UIColor(hex:"#4B5964") static let voip_gray = UIColor(hex:"#96A5B1") - static let voip_gray_background = UIColor(hex:"#D8D8D8") + 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") @@ -115,6 +115,8 @@ class VoipTheme { // Names & values replicated from Android 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) diff --git a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift index 4180a406e..948622017 100644 --- a/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift +++ b/Classes/Swift/Voip/ViewModels/ConferenceParticipantDeviceData.swift @@ -27,10 +27,12 @@ class ConferenceParticipantDeviceData { 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) { @@ -40,30 +42,38 @@ class ConferenceParticipantDeviceData { onIsSpeakingChanged: { (participantDevice, isSpeaking) in Log.i("[Conference Participant Device] Participant \(participantDevice.address?.asStringUriOnly()) isspeaking = \(isSpeaking)") self.activeSpeaker.value = isSpeaking - }, onConferenceJoined: { (participantDevice) in + }, + 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 + }, + 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 + }, + 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 + }, + 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) diff --git a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift index edba6f0e9..5363943e9 100644 --- a/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift +++ b/Classes/Swift/Voip/Views/CompositeViewControllers/ActiveCallOrConferenceView.swift @@ -33,6 +33,8 @@ import linphonesw var currentCallView : ActiveCallView? = nil var conferenceGridView: VoipConferenceGridView? = nil var conferenceActiveSpeakerView: VoipConferenceActiveSpeakerView? = nil + var conferenceAudioOnlyView: VoipConferenceAudioOnlyView? = nil + let conferenceJoinSpinner = RotatingSpinner() @@ -116,18 +118,17 @@ import linphonesw fullScreenMutableContainerView.addSubview(conferenceGridView!) conferenceGridView?.matchParentDimmensions().done() conferenceGridView?.isHidden = true - ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { (isInConference) in + ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { (exists) in self.updateNavigation() - if (isInConference == true) { + if (exists == true) { self.currentCallView!.isHidden = true self.extraButtonsView.isHidden = true self.conferencePausedView?.isHidden = true - let conferenceMode = ConferenceViewModel.shared.conferenceDisplayMode.value - self.conferenceGridView!.isHidden = conferenceMode != .Grid - self.conferenceActiveSpeakerView?.isHidden = conferenceMode != .ActiveSpeaker - self.conferenceGridView?.conferenceViewModel = ConferenceViewModel.shared + self.displaySelectedConferenceLayout() } else { self.conferenceGridView?.isHidden = true + self.conferenceActiveSpeakerView?.isHidden = true + self.conferenceActiveSpeakerView?.isHidden = true } } @@ -148,22 +149,20 @@ import linphonesw conferenceActiveSpeakerView?.matchParentDimmensions().done() conferenceActiveSpeakerView?.isHidden = true - // Conference mode switching + + // 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.conferenceGridView!.isHidden = conferenceMode != .Grid - self.conferenceActiveSpeakerView!.isHidden = conferenceMode != .ActiveSpeaker - self.conferenceActiveSpeakerView?.conferenceViewModel = ConferenceViewModel.shared - } else { - self.conferenceActiveSpeakerView?.isHidden = true + self.displaySelectedConferenceLayout() } } - ConferenceViewModel.shared.conferenceExists.readCurrentAndObserve { (isInConference) in - self.updateNavigation() - } - // Calls List ControlsViewModel.shared.goToCallsListEvent.observe { (_) in self.dismissableView = CallsListView() @@ -189,9 +188,8 @@ import linphonesw self.view.addSubview(self.dismissableView!) self.dismissableView?.matchParentDimmensions().done() let activeDisplayMode = ConferenceViewModel.shared.conferenceDisplayMode.value! - let indexPath = IndexPath(row: activeDisplayMode == .Grid ? 0 : 1, section: 0) + 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 @@ -292,6 +290,22 @@ import linphonesw } + 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() 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..133a297f5 --- /dev/null +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipAudioOnlyParticipantCell.swift @@ -0,0 +1,110 @@ +/* + * 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.0 + 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 = UIImageView(image: UIImage(named: "voip_micro_off")?.tinted(with: .white)) + + 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.layer.cornerRadius = VoipAudioOnlyParticipantCell.avatar_size/2 + muted.clipsToBounds = true + muted.backgroundColor = VoipTheme.voip_dark_gray + muted.size(w: VoipAudioOnlyParticipantCell.mute_size, h: VoipAudioOnlyParticipantCell.mute_size).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/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 index f4de96559..b9317b81b 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceDisplayModeSelectionView.swift @@ -46,7 +46,7 @@ import linphonesw // TableView datasource delegate func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return 2 + return 3 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -66,6 +66,14 @@ import linphonesw }, 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 @@ -78,9 +86,15 @@ import linphonesw cell.isSelected = true if (indexPath.row == 0) { tableView.deselectRow(at: IndexPath(row: 1, section: 0), animated: false) + tableView.deselectRow(at: IndexPath(row: 2, section: 0), animated: false) } if (indexPath.row == 1) { tableView.deselectRow(at: IndexPath(row: 0, section: 0), animated: false) + tableView.deselectRow(at: IndexPath(row: 2, section: 0), animated: false) + } + if (indexPath.row == 2) { + tableView.deselectRow(at: IndexPath(row: 0, section: 0), animated: false) + tableView.deselectRow(at: IndexPath(row: 1, section: 0), animated: false) } } diff --git a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift index 37ccb0416..dcccdf8c7 100644 --- a/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift +++ b/Classes/Swift/Voip/Views/Fragments/Conference/VoipConferenceGridView.swift @@ -46,19 +46,23 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi 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 @@ -178,7 +182,7 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi conferenceViewModel?.conferenceParticipantDevices.value?.forEach { $0.clearObservers() } - if (self.isHidden || conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .Grid) { + if (self.isHidden) { self.grid.reloadData() return } @@ -205,7 +209,7 @@ class VoipConferenceGridView: UIView, UICollectionViewDataSource, UICollectionVi } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - if (self.isHidden || conferenceViewModel?.conference.value?.call?.params?.conferenceVideoLayout != .Grid) { + if (self.isHidden) { return 0 } guard let participantsCount = conferenceViewModel?.conferenceParticipantDevices.value?.count else { diff --git a/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift index c95241313..dfa77a918 100644 --- a/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift +++ b/Classes/Swift/Voip/Views/Fragments/ConferenceLayoutPickerView.swift @@ -57,7 +57,7 @@ class ConferenceLayoutPickerView: UIStackView { 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 = .Grid + 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))]) @@ -66,13 +66,12 @@ class ConferenceLayoutPickerView: UIStackView { ConferenceWaitingRoomViewModel.sharedModel.joinLayout.readCurrentAndObserve { layout in grid.isSelected = layout == .Grid activeSpeaker.isSelected = layout == .ActiveSpeaker - audioOnly.isSelected = false // Todo when doing auioonly layout == .Grid + 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() From 4e7db0c33e0b0e46b0531bd7e185a6bdd0a40e4d Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Mon, 13 Jun 2022 19:19:00 +0200 Subject: [PATCH 53/75] Update gitlabCI to use macmini-m1-xcode13 runner --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c9a545775..d24b53500 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,7 +7,7 @@ variables: job-ios: stage: build - tags: [ "macosx-xcode12" ] + tags: [ "macmini-m1-xcode13" ] script: - pod install --repo-update From 770c9252a2ebad12298383c397ce88933c8068a0 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Mon, 13 Jun 2022 19:22:39 +0200 Subject: [PATCH 54/75] xcodebuild fix on m1 chips --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d24b53500..3fbfa5902 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,8 +12,8 @@ job-ios: 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: From 18910129b513bd153ead6d48b428bae2bfd4197d Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Mon, 13 Jun 2022 19:58:08 +0200 Subject: [PATCH 55/75] Update project.pbxproj for new classes --- linphone.xcodeproj/project.pbxproj | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/linphone.xcodeproj/project.pbxproj b/linphone.xcodeproj/project.pbxproj index 99ed4acce..57a87656c 100644 --- a/linphone.xcodeproj/project.pbxproj +++ b/linphone.xcodeproj/project.pbxproj @@ -827,6 +827,8 @@ C6D1E42427595988008EB388 /* security_toggle_icon_grey.png in Resources */ = {isa = PBXBuildFile; fileRef = C6D1E42227595987008EB388 /* security_toggle_icon_grey.png */; }; C6D1E42527595988008EB388 /* security_toggle_icon_green.png in Resources */ = {isa = PBXBuildFile; fileRef = C6D1E42327595988008EB388 /* security_toggle_icon_green.png */; }; C6D1EC4A274D212B0091881C /* UICamSwitch.m in Sources */ = {isa = PBXBuildFile; fileRef = C6D1EC49274D212B0091881C /* UICamSwitch.m */; }; + C6D446C928572876005C67B7 /* VoipConferenceAudioOnlyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D446C828572876005C67B7 /* VoipConferenceAudioOnlyView.swift */; }; + C6D446CB28572897005C67B7 /* VoipAudioOnlyParticipantCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D446CA28572897005C67B7 /* VoipAudioOnlyParticipantCell.swift */; }; C6D52B45274648E500904660 /* VoipGridParticipantCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D52B44274648E500904660 /* VoipGridParticipantCell.swift */; }; C6DA657C261C950C0020CB43 /* VFSUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DA657B261C950C0020CB43 /* VFSUtil.swift */; }; C6DB1DE22757E35F00A22704 /* StyledTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6DB1DE12757E35F00A22704 /* StyledTextView.swift */; }; @@ -2001,6 +2003,8 @@ C6D1E42327595988008EB388 /* security_toggle_icon_green.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = security_toggle_icon_green.png; sourceTree = ""; }; C6D1EC48274D212A0091881C /* UICamSwitch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UICamSwitch.h; sourceTree = ""; }; C6D1EC49274D212B0091881C /* UICamSwitch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UICamSwitch.m; sourceTree = ""; }; + C6D446C828572876005C67B7 /* VoipConferenceAudioOnlyView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipConferenceAudioOnlyView.swift; sourceTree = ""; }; + C6D446CA28572897005C67B7 /* VoipAudioOnlyParticipantCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipAudioOnlyParticipantCell.swift; sourceTree = ""; }; C6D52B44274648E500904660 /* VoipGridParticipantCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoipGridParticipantCell.swift; sourceTree = ""; }; C6DA657B261C950C0020CB43 /* VFSUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VFSUtil.swift; sourceTree = ""; }; C6DB1DE12757E35F00A22704 /* StyledTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyledTextView.swift; sourceTree = ""; }; @@ -3547,6 +3551,8 @@ C6D52B4727481D3F00904660 /* Conference */ = { isa = PBXGroup; children = ( + C6D446C828572876005C67B7 /* VoipConferenceAudioOnlyView.swift */, + C6D446CA28572897005C67B7 /* VoipAudioOnlyParticipantCell.swift */, C6C98CE0274568F700059B55 /* VoipConferenceGridView.swift */, C6D52B44274648E500904660 /* VoipGridParticipantCell.swift */, C6B04D66274BD61200F70559 /* VoipConferenceActiveSpeakerView.swift */, @@ -4996,6 +5002,7 @@ C6C98CCE27438A3F00059B55 /* DismissableView.swift in Sources */, D37DC6C11594AE1800B2A5EB /* LinphoneCoreSettingsStore.m in Sources */, 63CD4B4F1A5AAC8C00B84282 /* DTAlertView.m in Sources */, + C6D446C928572876005C67B7 /* VoipConferenceAudioOnlyView.swift in Sources */, D3EA53FD159850E80037DC6B /* LinphoneManager.m in Sources */, C60D265C272AA0BD006238BB /* UIImageExtensions.swift in Sources */, C6D09F3D273EE467003C2173 /* BouncingCounter.swift in Sources */, @@ -5109,6 +5116,7 @@ D306459E1611EC2A00BB571E /* UILoadingImageView.m in Sources */, 6381DA7D1C1AD5EA00DF3BBD /* UIBouncingView.m in Sources */, C6C98CDD274547C500059B55 /* ParticipantExtensions.swift in Sources */, + C6D446CB28572897005C67B7 /* VoipAudioOnlyParticipantCell.swift in Sources */, D37E3ECD1619C27A0087659A /* CAAnimation+Blocks.m in Sources */, C6F2D505273BB3BB0071BA52 /* UIApplication+Extension.swift in Sources */, C65A5D3F27216E3A005BA038 /* CallData.swift in Sources */, @@ -5685,7 +5693,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.156+f3dd1fe\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.199+1355eaf\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5812,7 +5820,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.156+f3dd1fe\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.199+1355eaf\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -5938,7 +5946,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.156+f3dd1fe\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.199+1355eaf\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; @@ -6063,7 +6071,7 @@ "-DENABLE_QRCODE=TRUE", "-DENABLE_SMS_INVITE=TRUE", "$(inherited)", - "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.156+f3dd1fe\\\"", + "-DLINPHONE_SDK_VERSION=\\\"5.2.0-alpha.199+1355eaf\\\"", ); OTHER_SWIFT_FLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone; From 2f48008050601b463f7741e523602b2dae1fcfc2 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Tue, 14 Jun 2022 11:15:28 +0200 Subject: [PATCH 56/75] Group call from group chat --- Classes/Base.lproj/ChatConversationView.xib | 21 +------ Classes/ChatConversationView.h | 1 - Classes/ChatConversationView.m | 59 ++++++++++++++++--- .../Base.lproj/UIConfirmationDialog.xib | 10 +++- Classes/LinphoneUI/UIConfirmationDialog.h | 2 + Classes/LinphoneUI/UIConfirmationDialog.m | 7 +++ .../ConferenceSchedulingViewModel.swift | 14 ++--- Classes/Swift/Voip/Theme/VoipTexts.swift | 4 +- Classes/Swift/Voip/Theme/VoipTheme.swift | 4 +- .../Voip/ViewModels/ConferenceViewModel.swift | 25 +++++++- Resources/fr.lproj/Localizable.strings | 2 +- 11 files changed, 106 insertions(+), 43 deletions(-) diff --git a/Classes/Base.lproj/ChatConversationView.xib b/Classes/Base.lproj/ChatConversationView.xib index 446b483da..11869f94a 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 @@ -