forked from mirrors/linphone-iphone
Bulk refactorisation + new ConferenceViewModel from Android + Bulk fixes
This commit is contained in:
parent
0afc2036d6
commit
69a885df4f
127 changed files with 1636 additions and 574 deletions
|
|
@ -1,13 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15702" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19455" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="HistoryListView">
|
||||
<connections>
|
||||
<outlet property="allButton" destination="4" id="27"/>
|
||||
<outlet property="conferenceButton" destination="VWZ-Nd-W2s" id="wy8-oW-FqP"/>
|
||||
<outlet property="missedButton" destination="5" id="28"/>
|
||||
<outlet property="selectedButtonImage" destination="o8E-gw-vhI" id="hNf-FA-7aQ"/>
|
||||
<outlet property="tableController" destination="18" id="26"/>
|
||||
|
|
@ -29,7 +33,7 @@
|
|||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="38" userLabel="switchView">
|
||||
<rect key="frame" x="0.0" y="0.0" width="150" height="66"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="225" height="66"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="4" userLabel="allButton" customClass="UIInterfaceStyleButton">
|
||||
|
|
@ -49,7 +53,7 @@
|
|||
</button>
|
||||
<button opaque="NO" contentMode="bottom" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5" userLabel="missedButton" customClass="UIInterfaceStyleButton">
|
||||
<rect key="frame" x="75" y="0.0" width="75" height="66"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" heightSizable="YES" flexibleMaxY="YES"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" heightSizable="YES" flexibleMaxY="YES"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Missed contacts filter"/>
|
||||
<state key="normal" image="history_missed_default.png">
|
||||
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
|
|
@ -61,6 +65,21 @@
|
|||
<action selector="onMissedClick:" destination="-1" eventType="touchUpInside" id="30"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleAspectFit" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="VWZ-Nd-W2s" userLabel="conferenceButton" customClass="UIInterfaceStyleButton">
|
||||
<rect key="frame" x="150" y="0.0" width="75" height="66"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" heightSizable="YES" flexibleMaxY="YES"/>
|
||||
<accessibility key="accessibilityConfiguration" label="Missed contacts filter"/>
|
||||
<inset key="imageEdgeInsets" minX="10" minY="5" maxX="10" maxY="5"/>
|
||||
<state key="normal" image="voip_conference_new.png">
|
||||
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</state>
|
||||
<state key="disabled" image="history_missed_disabled.png"/>
|
||||
<state key="selected" image="voip_conference_new_selected.png"/>
|
||||
<state key="highlighted" backgroundImage="color_E.png"/>
|
||||
<connections>
|
||||
<action selector="onConferenceClick:" destination="-1" eventType="touchUpInside" id="9hJ-lr-NB7"/>
|
||||
</connections>
|
||||
</button>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="color_A.png" translatesAutoresizingMaskIntoConstraints="NO" id="o8E-gw-vhI" userLabel="selectedButtonImage">
|
||||
<rect key="frame" x="0.0" y="63" width="75" height="3"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES"/>
|
||||
|
|
@ -127,12 +146,12 @@
|
|||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
||||
</view>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" alwaysBounceVertical="YES" style="plain" separatorStyle="default" allowsSelectionDuringEditing="YES" allowsMultipleSelectionDuringEditing="YES" rowHeight="44" sectionHeaderHeight="35" sectionFooterHeight="1" translatesAutoresizingMaskIntoConstraints="NO" id="17" userLabel="tableView">
|
||||
<rect key="frame" x="0.0" y="66" width="375" height="493"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<inset key="scrollIndicatorInsets" minX="0.0" minY="0.0" maxX="0.0" maxY="10"/>
|
||||
<color key="separatorColor" red="0.67030966281890869" green="0.71867996454238892" blue="0.75078284740447998" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<connections>
|
||||
|
|
@ -143,7 +162,7 @@
|
|||
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="No call in your history" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xtr-Fp-60Z" userLabel="emptyTableLabel">
|
||||
<rect key="frame" x="0.0" y="66" width="375" height="493"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
|
|
@ -184,5 +203,13 @@
|
|||
<image name="history_missed_selected.png" width="52.799999237060547" height="52.799999237060547"/>
|
||||
<image name="select_all_default.png" width="43.200000762939453" height="43.200000762939453"/>
|
||||
<image name="select_all_disabled.png" width="43.200000762939453" height="43.200000762939453"/>
|
||||
<image name="voip_conference_new.png" width="97.599998474121094" height="97.599998474121094"/>
|
||||
<image name="voip_conference_new_selected.png" width="97.599998474121094" height="97.599998474121094"/>
|
||||
<systemColor name="secondarySystemBackgroundColor">
|
||||
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -24,5 +24,6 @@
|
|||
@interface UICamSwitch : UIIconButton
|
||||
|
||||
@property(nonatomic, weak) IBOutlet UIView *preview;
|
||||
+ (void) switchCamera;
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <UIDocumentPickerDelegate, UITableViewDataSource,UITableViewDelegate>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -85,5 +85,7 @@
|
|||
- (UIInterfaceOrientation)currentOrientation;
|
||||
- (void)clearCache:(NSArray *)exclude;
|
||||
- (IBAction)onRightSwipe:(id)sender;
|
||||
- (void) removeCallViewFromCache;
|
||||
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface VideoZoomHandler : NSObject {
|
||||
float zoomLevel, cx, cy;
|
||||
UIView* videoView;
|
||||
}
|
||||
|
||||
- (void) setup: (UIView*) videoView;
|
||||
- (void) resetZoom;
|
||||
|
||||
@end
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:^() {
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -36,6 +36,7 @@ class ScheduledConferenceData {
|
|||
let organizer = MutableLiveData<String>()
|
||||
let participantsShort = MutableLiveData<String>()
|
||||
let participantsExpanded = MutableLiveData<String>()
|
||||
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() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@
|
|||
import Foundation
|
||||
import linphonesw
|
||||
|
||||
|
||||
class ConferenceSchedulingViewModel {
|
||||
|
||||
let core = Core.get()
|
||||
|
|
@ -45,54 +44,78 @@ class ConferenceSchedulingViewModel {
|
|||
|
||||
let sendInviteViaChat = MutableLiveData<Bool>()
|
||||
let sendInviteViaEmail = MutableLiveData<Bool>()
|
||||
|
||||
|
||||
let address = MutableLiveData<Address>()
|
||||
|
||||
let conferenceCreationInProgress = MutableLiveData<Bool>()
|
||||
|
||||
let conferenceCreationCompletedEvent: MutableLiveData<Bool> = MediatorLiveData()
|
||||
let conferenceCreationCompletedEvent: MutableLiveData<Pair<String?,String?>> = MutableLiveData()
|
||||
let onErrorEvent = MutableLiveData<String>()
|
||||
|
||||
let continueEnabled: MediatorLiveData<Bool> = MediatorLiveData()
|
||||
let continueEnabled: MutableLiveData<Bool> = 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
|
||||
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
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))!
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
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<String>()
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
138
Classes/Swift/Conference/views/ICSBubbleView.swift
Normal file
138
Classes/Swift/Conference/views/ICSBubbleView.swift
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
126
Classes/Swift/Conference/views/ScheduledConferencesCell.swift
Normal file
126
Classes/Swift/Conference/views/ScheduledConferencesCell.swift
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
}
|
||||
108
Classes/Swift/Conference/views/ScheduledConferencesView.swift
Normal file
108
Classes/Swift/Conference/views/ScheduledConferencesView.swift
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
33
Classes/Swift/Util/Pair.swift
Normal file
33
Classes/Swift/Util/Pair.swift
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
class Pair <T1,T2> {
|
||||
var first:T1
|
||||
var second:T2
|
||||
|
||||
init(_ first:T1, _ second:T2) {
|
||||
self.first = first
|
||||
self.second = second
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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)) {
|
||||
|
|
@ -27,60 +27,36 @@ class ConferenceViewModel {
|
|||
let core = Core.get()
|
||||
static let shared = ConferenceViewModel()
|
||||
|
||||
|
||||
let isConferencePaused = MutableLiveData<Bool>()
|
||||
let canResumeConference = MutableLiveData<Bool>()
|
||||
|
||||
let isMeConferenceFocus = MutableLiveData<Bool>()
|
||||
let conferenceExists = MutableLiveData<Bool>()
|
||||
let subject = MutableLiveData<String>()
|
||||
let isConferenceLocallyPaused = MutableLiveData<Bool>()
|
||||
let isVideoConference = MutableLiveData<Bool>()
|
||||
let isMeAdmin = MutableLiveData<Bool>()
|
||||
|
||||
let conferenceAddress = MutableLiveData<Address>()
|
||||
|
||||
|
||||
let conference = MutableLiveData<Conference>()
|
||||
let conferenceParticipants = MutableLiveData<[ConferenceParticipantData]>()
|
||||
let conferenceParticipantDevices = MutableLiveData<[ConferenceParticipantDeviceData]>()
|
||||
let conferenceDisplayMode = MutableLiveData<ConferenceLayout>()
|
||||
|
||||
let isInConference = MutableLiveData<Bool>()
|
||||
|
||||
let isVideoConference = MutableLiveData<Bool>()
|
||||
|
||||
let isRecording = MutableLiveData<Bool>()
|
||||
let isRemotelyRecorded = MutableLiveData<Bool>()
|
||||
|
||||
let subject = MutableLiveData<String>()
|
||||
|
||||
let conference = MutableLiveData<Conference>()
|
||||
|
||||
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
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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)),
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ class RemotelyRecordingView: UIView {
|
|||
var isRemotelyRecorded: MutableLiveData<Bool>? = nil {
|
||||
didSet {
|
||||
isRemotelyRecorded?.readCurrentAndObserve(onChange: { (isRemotelyRecording) in
|
||||
self.isHidden = !(isRemotelyRecording == true)
|
||||
self.isHidden = isRemotelyRecording != true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue