From 69a885df4fc9a808e33aace800ea25321ee612e3 Mon Sep 17 00:00:00 2001 From: Christophe Deschamps Date: Wed, 12 Jan 2022 10:28:22 +0100 Subject: [PATCH] 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;