Bulk refactorisation + new ConferenceViewModel from Android + Bulk fixes

This commit is contained in:
Christophe Deschamps 2022-01-12 10:28:22 +01:00
parent 0afc2036d6
commit 69a885df4f
127 changed files with 1636 additions and 574 deletions

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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];

View file

@ -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;

View file

@ -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);

View file

@ -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

View file

@ -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);

View file

@ -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;

View file

@ -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

View file

@ -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"]);

View file

@ -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;
}

View file

@ -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];

View file

@ -24,5 +24,6 @@
@interface UICamSwitch : UIIconButton
@property(nonatomic, weak) IBOutlet UIView *preview;
+ (void) switchCamera;
@end

View file

@ -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);
}
}
}

View file

@ -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>

View file

@ -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;

View file

@ -85,5 +85,7 @@
- (UIInterfaceOrientation)currentOrientation;
- (void)clearCache:(NSArray *)exclude;
- (IBAction)onRightSwipe:(id)sender;
- (void) removeCallViewFromCache;
@end

View file

@ -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;

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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:^() {

View file

@ -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:

View file

@ -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() {
}
}

View file

@ -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

View file

@ -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))!
}
}

View file

@ -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
}
}

View file

@ -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()
}

View file

@ -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())

View 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 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
}
}

View 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()
}
}

View 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")
}
}

View 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
}
}

View file

@ -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))
}
}
}
}

View file

@ -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()

View file

@ -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

View file

@ -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)
}
}*/
}
}

View file

@ -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()

View 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
}
}

View file

@ -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)

View file

@ -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) {

View file

@ -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)) {

View file

@ -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

View file

@ -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) {

View file

@ -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

View file

@ -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)),
]
}

View file

@ -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)
}

View file

@ -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
})

View file

@ -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()

View file

@ -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()

View file

@ -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) {

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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()

View file

@ -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())
}
}

View file

@ -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
})
}
}

View file

@ -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