Merge pull request #1449 from vector-im/jitsi_widget

Integrating Jitsi into mobile apps
This commit is contained in:
manuroe 2017-08-18 13:26:30 +02:00 committed by GitHub
commit c21c40031a
45 changed files with 112200 additions and 78 deletions

View file

@ -17,6 +17,12 @@
32185B311F20FA2B00752141 /* LanguagePickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 32185B301F20FA2B00752141 /* LanguagePickerViewController.m */; };
322806A01F0F64C4008C53D7 /* RoomMembershipExpandedBubbleCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 3228069E1F0F64C4008C53D7 /* RoomMembershipExpandedBubbleCell.m */; };
322806A11F0F64C4008C53D7 /* RoomMembershipExpandedBubbleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3228069F1F0F64C4008C53D7 /* RoomMembershipExpandedBubbleCell.xib */; };
3233F7311F31F4BF006ACA81 /* JitsiViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3233F72F1F31F4BF006ACA81 /* JitsiViewController.m */; };
3233F7321F31F4BF006ACA81 /* JitsiViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3233F7301F31F4BF006ACA81 /* JitsiViewController.xib */; };
3233F73C1F3306A7006ACA81 /* WidgetManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3233F73B1F3306A7006ACA81 /* WidgetManager.m */; };
3233F73F1F331F05006ACA81 /* Widget.m in Sources */ = {isa = PBXBuildFile; fileRef = 3233F73E1F331F05006ACA81 /* Widget.m */; };
3233F7461F3497E2006ACA81 /* JitsiMeet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3233F7441F3497DA006ACA81 /* JitsiMeet.framework */; };
3233F7471F3497E2006ACA81 /* JitsiMeet.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3233F7441F3497DA006ACA81 /* JitsiMeet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
32471CDC1F1373A100BDF50A /* RoomMembershipCollapsedWithPaginationTitleBubbleCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 32471CDA1F1373A100BDF50A /* RoomMembershipCollapsedWithPaginationTitleBubbleCell.m */; };
32471CDD1F1373A100BDF50A /* RoomMembershipCollapsedWithPaginationTitleBubbleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 32471CDB1F1373A100BDF50A /* RoomMembershipCollapsedWithPaginationTitleBubbleCell.xib */; };
32471CE11F13AC1500BDF50A /* RoomMembershipExpandedWithPaginationTitleBubbleCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 32471CDF1F13AC1500BDF50A /* RoomMembershipExpandedWithPaginationTitleBubbleCell.m */; };
@ -500,6 +506,20 @@
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
3233F7481F3497E2006ACA81 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
3233F7471F3497E2006ACA81 /* JitsiMeet.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1129C74A281B080432B1A1A1 /* Pods-Riot.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Riot.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Riot/Pods-Riot.debug.xcconfig"; sourceTree = "<group>"; };
24B5103C1EFA7083004C6AD2 /* ReadReceiptsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReadReceiptsViewController.h; sourceTree = "<group>"; };
@ -518,6 +538,14 @@
3228069D1F0F64C4008C53D7 /* RoomMembershipExpandedBubbleCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomMembershipExpandedBubbleCell.h; sourceTree = "<group>"; };
3228069E1F0F64C4008C53D7 /* RoomMembershipExpandedBubbleCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomMembershipExpandedBubbleCell.m; sourceTree = "<group>"; };
3228069F1F0F64C4008C53D7 /* RoomMembershipExpandedBubbleCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomMembershipExpandedBubbleCell.xib; sourceTree = "<group>"; };
3233F72E1F31F4BF006ACA81 /* JitsiViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JitsiViewController.h; sourceTree = "<group>"; };
3233F72F1F31F4BF006ACA81 /* JitsiViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JitsiViewController.m; sourceTree = "<group>"; };
3233F7301F31F4BF006ACA81 /* JitsiViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = JitsiViewController.xib; sourceTree = "<group>"; };
3233F73A1F3306A6006ACA81 /* WidgetManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WidgetManager.h; sourceTree = "<group>"; };
3233F73B1F3306A7006ACA81 /* WidgetManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WidgetManager.m; sourceTree = "<group>"; };
3233F73D1F331F05006ACA81 /* Widget.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Widget.h; sourceTree = "<group>"; };
3233F73E1F331F05006ACA81 /* Widget.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Widget.m; sourceTree = "<group>"; };
3233F7441F3497DA006ACA81 /* JitsiMeet.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = JitsiMeet.framework; sourceTree = "<group>"; };
32471CD91F1373A100BDF50A /* RoomMembershipCollapsedWithPaginationTitleBubbleCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RoomMembershipCollapsedWithPaginationTitleBubbleCell.h; sourceTree = "<group>"; };
32471CDA1F1373A100BDF50A /* RoomMembershipCollapsedWithPaginationTitleBubbleCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RoomMembershipCollapsedWithPaginationTitleBubbleCell.m; sourceTree = "<group>"; };
32471CDB1F1373A100BDF50A /* RoomMembershipCollapsedWithPaginationTitleBubbleCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = RoomMembershipCollapsedWithPaginationTitleBubbleCell.xib; sourceTree = "<group>"; };
@ -1131,6 +1159,7 @@
buildActionMask = 2147483647;
files = (
E2EAC1A4FBD6FE5228584591 /* libPods-Riot.a in Frameworks */,
3233F7461F3497E2006ACA81 /* JitsiMeet.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1157,6 +1186,43 @@
path = Directory;
sourceTree = "<group>";
};
3233F7291F31F3B4006ACA81 /* libs */ = {
isa = PBXGroup;
children = (
3233F7431F3497DA006ACA81 /* jitsi-meet */,
);
path = libs;
sourceTree = "<group>";
};
3233F72D1F31F47E006ACA81 /* Widgets */ = {
isa = PBXGroup;
children = (
3233F72E1F31F4BF006ACA81 /* JitsiViewController.h */,
3233F72F1F31F4BF006ACA81 /* JitsiViewController.m */,
3233F7301F31F4BF006ACA81 /* JitsiViewController.xib */,
);
path = Widgets;
sourceTree = "<group>";
};
3233F7391F33065F006ACA81 /* Widgets */ = {
isa = PBXGroup;
children = (
3233F73A1F3306A6006ACA81 /* WidgetManager.h */,
3233F73B1F3306A7006ACA81 /* WidgetManager.m */,
3233F73D1F331F05006ACA81 /* Widget.h */,
3233F73E1F331F05006ACA81 /* Widget.m */,
);
path = Widgets;
sourceTree = "<group>";
};
3233F7431F3497DA006ACA81 /* jitsi-meet */ = {
isa = PBXGroup;
children = (
3233F7441F3497DA006ACA81 /* JitsiMeet.framework */,
);
path = "jitsi-meet";
sourceTree = "<group>";
};
327382A71F276AD200356143 /* de.lproj */ = {
isa = PBXGroup;
children = (
@ -1241,6 +1307,7 @@
F083BC0F1E7009EC00A9B29C /* Utils */,
F083BC191E7009EC00A9B29C /* ViewController */,
F083BC571E7009EC00A9B29C /* Views */,
3233F7291F31F3B4006ACA81 /* libs */,
F083BB0C1E7009EC00A9B29C /* AppDelegate.h */,
F083BB0D1E7009EC00A9B29C /* AppDelegate.m */,
F083BBE21E7009EC00A9B29C /* Main.storyboard */,
@ -1608,6 +1675,7 @@
F083BC0F1E7009EC00A9B29C /* Utils */ = {
isa = PBXGroup;
children = (
3233F7391F33065F006ACA81 /* Widgets */,
F083BC101E7009EC00A9B29C /* AvatarGenerator.h */,
F083BC111E7009EC00A9B29C /* AvatarGenerator.m */,
F083BC121E7009EC00A9B29C /* EventFormatter.h */,
@ -1623,6 +1691,7 @@
F083BC191E7009EC00A9B29C /* ViewController */ = {
isa = PBXGroup;
children = (
3233F72D1F31F47E006ACA81 /* Widgets */,
F0B4CBA81F41E71A008E99C5 /* RiotNavigationController.h */,
F0B4CBA91F41E71A008E99C5 /* RiotNavigationController.m */,
F0131DE31F2200D600CBF707 /* RiotSplitViewController.h */,
@ -2105,6 +2174,7 @@
F094A9A01B78D8F000B1FBBF /* Resources */,
44C35695CFA4F9799C449367 /* [CP] Copy Pods Resources */,
381DA4CC07D2104BFA23E45A /* [CP] Embed Pods Frameworks */,
3233F7481F3497E2006ACA81 /* Embed Frameworks */,
);
buildRules = (
);
@ -2224,6 +2294,7 @@
F083BD3D1E7009ED00A9B29C /* call_hangup_icon@2x.png in Resources */,
F083BE551E7009ED00A9B29C /* RoomOutgoingEncryptedTextMsgWithoutSenderNameBubbleCell.xib in Resources */,
32471CDD1F1373A100BDF50A /* RoomMembershipCollapsedWithPaginationTitleBubbleCell.xib in Resources */,
3233F7321F31F4BF006ACA81 /* JitsiViewController.xib in Resources */,
F083BD301E7009ED00A9B29C /* bubbles_bg_landscape.png in Resources */,
F083BDA41E7009ED00A9B29C /* notifications.png in Resources */,
F083BE0F1E7009ED00A9B29C /* ContactsTableViewController.xib in Resources */,
@ -2631,6 +2702,7 @@
F083BE661E7009ED00A9B29C /* RoomIncomingTextMsgWithPaginationTitleBubbleCell.m in Sources */,
F083BE141E7009ED00A9B29C /* HomeViewController.m in Sources */,
F083BDFB1E7009ED00A9B29C /* RoomSearchDataSource.m in Sources */,
3233F73C1F3306A7006ACA81 /* WidgetManager.m in Sources */,
F083BE281E7009ED00A9B29C /* StartChatViewController.m in Sources */,
F083BE841E7009ED00A9B29C /* RoomIdOrAliasTableViewCell.m in Sources */,
F083BD1D1E7009ED00A9B29C /* RageShakeManager.m in Sources */,
@ -2660,6 +2732,7 @@
F083BE2F1E7009ED00A9B29C /* ContactTableViewCell.m in Sources */,
F083BE901E7009ED00A9B29C /* RoomTitleView.m in Sources */,
F083BE6E1E7009ED00A9B29C /* RoomOutgoingAttachmentWithPaginationTitleBubbleCell.m in Sources */,
3233F7311F31F4BF006ACA81 /* JitsiViewController.m in Sources */,
F083BE2A1E7009ED00A9B29C /* UsersDevicesViewController.m in Sources */,
F083BE2D1E7009ED00A9B29C /* ForgotPasswordInputsView.m in Sources */,
F083BE7E1E7009ED00A9B29C /* InviteRecentTableViewCell.m in Sources */,
@ -2683,6 +2756,7 @@
F083BE401E7009ED00A9B29C /* RoomIncomingEncryptedTextMsgBubbleCell.m in Sources */,
32AE61E91F0CE099007255F4 /* RoomMembershipWithPaginationTitleBubbleCell.m in Sources */,
F083BDFD1E7009ED00A9B29C /* PublicRoomsDirectoryDataSource.m in Sources */,
3233F73F1F331F05006ACA81 /* Widget.m in Sources */,
F083BE191E7009ED00A9B29C /* RecentsViewController.m in Sources */,
F0B4CBA51F418D0B008E99C5 /* WebViewViewController.m in Sources */,
F083BE351E7009ED00A9B29C /* MediaAlbumTableCell.m in Sources */,
@ -2964,6 +3038,11 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
DEVELOPMENT_TEAM = 7J4U792NQT;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Riot/libs",
"$(PROJECT_DIR)/Riot/libs/jitsi-meet",
);
INFOPLIST_FILE = Riot/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@ -2981,6 +3060,11 @@
CODE_SIGN_ENTITLEMENTS = Riot/Riot.entitlements;
DEVELOPMENT_TEAM = 7J4U792NQT;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Riot/libs",
"$(PROJECT_DIR)/Riot/libs/jitsi-meet",
);
INFOPLIST_FILE = Riot/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";

View file

@ -24,6 +24,7 @@
#import "GAIDictionaryBuilder.h"
#import "MasterTabBarController.h"
#import "JitsiViewController.h"
#import "RageShakeManager.h"
@ -40,7 +41,7 @@ extern NSString *const kAppDelegateDidTapStatusBarNotification;
*/
extern NSString *const kAppDelegateNetworkStatusDidChangeNotification;
@interface AppDelegate : UIResponder <UIApplicationDelegate, MXKCallViewControllerDelegate, UISplitViewControllerDelegate, UINavigationControllerDelegate>
@interface AppDelegate : UIResponder <UIApplicationDelegate, MXKCallViewControllerDelegate, UISplitViewControllerDelegate, UINavigationControllerDelegate, JitsiViewControllerDelegate>
{
BOOL isAPNSRegistered;
@ -133,6 +134,21 @@ extern NSString *const kAppDelegateNetworkStatusDidChangeNotification;
*/
- (BOOL)handleUniversalLinkFragment:(NSString*)fragment;
#pragma mark - Jitsi call
/**
Open the Jitsi view controller from a widget.
@param jitsiWidget the jitsi widget.
@param video to indicate voice or video call.
*/
- (void)displayJitsiViewControllerWithWidget:(Widget*)jitsiWidget andVideo:(BOOL)video;
/**
The current Jitsi view controller being displayed.
*/
@property (nonatomic, readonly) JitsiViewController *jitsiViewController;
#pragma mark - Call status handling
/**

View file

@ -34,6 +34,8 @@
#import "MatrixSDK/MatrixSDK.h"
#import "Tools.h"
#import "MXRoom+Riot.h"
#import "WidgetManager.h"
#import "AFNetworkReachabilityManager.h"
@ -1684,6 +1686,9 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
// Update home data sources
[_masterTabBarController addMatrixSession:mxSession];
// Register the session to the widgets manager
[[WidgetManager sharedManager] addMatrixSession:mxSession];
[mxSessionArray addObject:mxSession];
@ -1698,6 +1703,9 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
// Update home data sources
[_masterTabBarController removeMatrixSession:mxSession];
// Update the widgets manager
[[WidgetManager sharedManager] removeMatrixSession:mxSession];
// If any, disable the no VoIP support workaround
[self disableNoVoIPOnMatrixSession:mxSession];
@ -1792,7 +1800,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
matrixCallObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXCallManagerNewCall object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
// Ignore the call if a call is already in progress
if (!currentCallViewController)
if (!currentCallViewController && !_jitsiViewController)
{
MXCall *mxCall = (MXCall*)notif.object;
@ -2489,6 +2497,89 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
}
}
#pragma mark - Jitsi call
- (void)displayJitsiViewControllerWithWidget:(Widget*)jitsiWidget andVideo:(BOOL)video
{
if (!_jitsiViewController && !currentCallViewController)
{
_jitsiViewController = [JitsiViewController jitsiViewController];
if ([_jitsiViewController openWidget:jitsiWidget withVideo:video])
{
_jitsiViewController.delegate = self;
[self presentJitsiViewController:nil];
}
else
{
_jitsiViewController = nil;
NSError *error = [NSError errorWithDomain:@""
code:0
userInfo:@{
NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"call_jitsi_error", @"Vector", nil)
}];
[self showErrorAsAlert:error];
}
}
else
{
NSError *error = [NSError errorWithDomain:@""
code:0
userInfo:@{
NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"call_already_displayed", @"Vector", nil)
}];
[self showErrorAsAlert:error];
}
}
- (void)presentJitsiViewController:(void (^)())completion
{
[self removeCallStatusBar];
if (_jitsiViewController)
{
if (self.window.rootViewController.presentedViewController)
{
[self.window.rootViewController.presentedViewController presentViewController:_jitsiViewController animated:YES completion:completion];
}
else
{
[self.window.rootViewController presentViewController:_jitsiViewController animated:YES completion:completion];
}
}
}
- (void)jitsiViewController:(JitsiViewController *)jitsiViewController dismissViewJitsiController:(void (^)())completion
{
if (jitsiViewController == _jitsiViewController)
{
[_jitsiViewController dismissViewControllerAnimated:YES completion:completion];
_jitsiViewController = nil;
[self removeCallStatusBar];
}
}
- (void)jitsiViewController:(JitsiViewController *)jitsiViewController goBackToApp:(void (^)())completion
{
if (jitsiViewController == _jitsiViewController)
{
[_jitsiViewController dismissViewControllerAnimated:YES completion:^{
MXRoom *room = [_jitsiViewController.widget.mxSession roomWithRoomId:_jitsiViewController.widget.roomId];
NSString *btnTitle = [NSString stringWithFormat:NSLocalizedStringFromTable(@"active_call_details", @"Vector", nil), room.riotDisplayname];
[self addCallStatusBar:btnTitle];
if (completion)
{
completion();
}
}];
}
}
#pragma mark - Call status handling
- (void)addCallStatusBar:(NSString*)buttonTitle
@ -2517,7 +2608,7 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
}
[_callStatusBarButton setBackgroundColor:kRiotColorGreen];
[_callStatusBarButton addTarget:self action:@selector(presentCallViewController) forControlEvents:UIControlEventTouchUpInside];
[_callStatusBarButton addTarget:self action:@selector(onCallStatusBarButtonPressed) forControlEvents:UIControlEventTouchUpInside];
// Place button into the new window
[_callStatusBarButton setTranslatesAutoresizingMaskIntoConstraints:NO];
@ -2570,9 +2661,16 @@ NSString *const kAppDelegateNetworkStatusDidChangeNotification = @"kAppDelegateN
}
}
- (void)presentCallViewController
- (void)onCallStatusBarButtonPressed
{
[self presentCallViewController:nil];
if (currentCallViewController)
{
[self presentCallViewController:nil];
}
else if (_jitsiViewController)
{
[self presentJitsiViewController:nil];
}
}
- (void)presentCallViewController:(void (^)())completion

View file

@ -231,6 +231,9 @@
"room_unsent_messages_notification" = "Messages not sent. %@ or %@ now?";
"room_unsent_messages_unknown_devices_notification" = "Message not sent due to unknown devices being present. %@ or %@ now?";
"room_ongoing_conference_call" = "Ongoing conference call. Join as %@ or %@.";
"room_ongoing_conference_call_with_close" = "Ongoing conference call. Join as %@ or %@. %@ it.";
"room_ongoing_conference_call_close" = "Close";
"room_conference_call_no_power" = "You need permission to manage conference call in this room";
"room_prompt_resend" = "Resend all";
"room_prompt_cancel" = "cancel all";
"room_resend_unsent_messages" = "Resend unsent messages";
@ -343,6 +346,7 @@
"settings_labs_e2e_encryption" = "End-to-End Encryption";
"settings_labs_e2e_encryption_prompt_message" = "To finish setting up encryption you must log in again.";
"settings_labs_create_conference_with_jitsi" = "Create conference calls with jitsi";
"settings_version" = "Version %@";
"settings_olm_version" = "Olm Version %@";
@ -468,6 +472,8 @@
// Call
"call_incoming_voice_prompt" = "Incoming voice call from %@";
"call_incoming_video_prompt" = "Incoming video call from %@";
"call_already_displayed" = "There is already a call in progress.";
"call_jitsi_error" = "Failed to join the conference call.";
// No VoIP support
"no_voip_title" = "Incoming call";
@ -492,3 +498,7 @@
"bug_report_progress_uploading" = "Uploading report";
"bug_report_send" = "Send";
// Widget
"widget_no_power_to_manage" = "You need permission to manage widgets in this room";
"widget_creation_failure" = "Widget creation has failed";

View file

@ -17,6 +17,8 @@
#import <MatrixKit/MatrixKit.h>
#import "WidgetManager.h"
/**
The data source for `RoomViewController` in Vector.
*/
@ -32,4 +34,11 @@
*/
@property(nonatomic) BOOL markTimelineInitialEvent;
/**
Check if there is an active jitsi widget in the room and return it.
@return a widget representating the active jitsi conference in the room. Else, nil.
*/
- (Widget *)jitsiWidget;
@end

View file

@ -425,4 +425,14 @@
_selectedEventId = selectedEventId;
}
- (Widget *)jitsiWidget
{
Widget *jitsiWidget;
// Note: Manage only one jitsi widget at a time for the moment
jitsiWidget = [[WidgetManager sharedManager] widgetsOfTypes:@[kWidgetTypeJitsi] inRoom:self.room].firstObject;
return jitsiWidget;
}
@end

View file

@ -59,6 +59,10 @@ extern UIStatusBarStyle kRiotDesignStatusBarStyle;
extern UIBarStyle kRiotDesignSearchBarStyle;
extern UIColor *kRiotDesignSearchBarTintColor;
// Flag to enable the display of jitsi conference widget
// TODO: Disable it before release
// TODO: Remove it once Riot web and android are ready
#define USE_JITSI_WIDGET
/**
`RiotDesignValues` class manages the Riot design parameters

View file

@ -0,0 +1,85 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
/**
The `Widget` class represents scalar widget information.
*/
@interface Widget : NSObject
/**
The widget id.
*/
@property (nonatomic, readonly, nonnull) NSString *widgetId;
/**
The widget type.
Some types are defined in `WidgetManager.h`.
Nil if the widget is no more active in the room.
*/
@property (nonatomic, readonly, nullable) NSString *type;
/**
The widget url.
Widgets are basically opened in a webview. `url` is the url the webview must use
to render the widget.
*/
@property (nonatomic, readonly, nullable) NSString *url;
/**
The widget name.
*/
@property (nonatomic, readonly, nullable) NSString *name;
/**
The widget additional data.
*/
@property (nonatomic, readonly, nullable) NSDictionary *data;
/**
The widget event that is at the origin of the widget.
*/
@property (nonatomic, readonly, nonnull) MXEvent *widgetEvent;
/**
The Matrix session where the widget is.
*/
@property (nonatomic, readonly, nonnull) MXSession *mxSession;
/**
Indicate if the widget is still active.
*/
@property (nonatomic, readonly) BOOL isActive;
/**
The room id of the widget.
*/
@property (nonatomic, readonly, nonnull) NSString *roomId;
/**
Create a Widget instance from a widget event.
@param widgetEvent the state event representing a widget.
@return the newly created instance.
*/
- (instancetype _Nullable )initWithWidgetEvent:(MXEvent* _Nonnull)widgetEvent inMatrixSession:(MXSession* _Nonnull)mxSession;
@end

View file

@ -0,0 +1,69 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "Widget.h"
#import "WidgetManager.h"
@implementation Widget
- (instancetype)initWithWidgetEvent:(MXEvent *)widgetEvent inMatrixSession:(MXSession*)mxSession
{
if (![widgetEvent.type isEqualToString:kWidgetEventTypeString])
{
// The Widget class works only with scalar, aka "im.vector.modular.widgets", widgets
return nil;
}
self = [super init];
if (self)
{
_widgetId = widgetEvent.stateKey;
_widgetEvent = widgetEvent;
_mxSession = mxSession;
MXJSONModelSetString(_type, widgetEvent.content[@"type"]);
MXJSONModelSetString(_url, widgetEvent.content[@"url"]);
MXJSONModelSetString(_name, widgetEvent.content[@"name"]);
MXJSONModelSetDictionary(_data, widgetEvent.content[@"data"]);
// Format the url string with user data
_url = [_url stringByReplacingOccurrencesOfString:@"$matrix_user_id" withString:mxSession.myUser.userId];
_url = [_url stringByReplacingOccurrencesOfString:@"$matrix_display_name"
withString:mxSession.myUser.displayname ? mxSession.myUser.displayname : mxSession.myUser.userId];
_url = [_url stringByReplacingOccurrencesOfString:@"$matrix_avatar_url"
withString:mxSession.myUser.avatarUrl ? mxSession.myUser.avatarUrl : @""];
}
return self;
}
- (BOOL)isActive
{
return (_type != nil && _url != nil);
}
- (NSString *)roomId
{
return _widgetEvent.roomId;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<Widget: %p> id: %@ - type: %@ - name: %@ - url: %@", self, _widgetId, _type, _name, _url];
}
@end

View file

@ -0,0 +1,142 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <MatrixSDK/MatrixSDK.h>
#import "Widget.h"
/**
The type of matrix event used for scalar widgets.
*/
FOUNDATION_EXPORT NSString *const kWidgetEventTypeString;
/**
Known types widgets.
*/
FOUNDATION_EXPORT NSString *const kWidgetTypeJitsi;
/**
Posted when a widget has been created, updated or disabled.
The notification object is the `Widget` instance.
*/
FOUNDATION_EXPORT NSString *const kWidgetManagerDidUpdateWidgetNotification;
/**
`WidgetManager` NSError domain and codes.
*/
FOUNDATION_EXPORT NSString *const WidgetManagerErrorDomain;
typedef enum : NSUInteger
{
WidgetManagerErrorCodeNotEnoughPower,
WidgetManagerErrorCodeCreationFailed
}
WidgetManagerErrorCode;
/**
The `WidgetManager` helps to handle scalar widgets.
*/
@interface WidgetManager : NSObject
/**
Returns the shared widget manager.
@return the shared widget manager.
*/
+ (instancetype)sharedManager;
/**
List all active widgets in a room.
@param room the room to check.
@return a list of widgets.
*/
- (NSArray<Widget*> *)widgetsInRoom:(MXRoom*)room;
/**
List all active widgets of a given type in a room.
@param widgetType the types of widget to search.
@param room the room to check.
@return a list of widgets.
*/
- (NSArray<Widget*> *)widgetsOfTypes:(NSArray<NSString*>*)widgetTypes inRoom:(MXRoom*)room;
/**
Add a scalar widget to a room.
@param widgetId the id of the widget.
@param widgetContent the widget content.
@param room the room to create the widget to.
@param success A block object called when the operation succeeds. It provides the newly added widget.
@param failure A block object called when the operation fails.
@return a MXHTTPOperation instance.
*/
- (MXHTTPOperation *)createWidget:(NSString*)widgetId
withContent:(NSDictionary<NSString*, NSObject*>*)widgetContent
inRoom:(MXRoom*)room
success:(void (^)(Widget *widget))success
failure:(void (^)(NSError *error))failure;
/**
Add a jitsi conference widget to a room.
@param room the room to create the widget to.
@param video the conference type
@param success A block object called when the operation succeeds. It provides the newly added widget.
@param failure A block object called when the operation fails.
@return a MXHTTPOperation instance.
*/
- (MXHTTPOperation *)createJitsiWidgetInRoom:(MXRoom*)room
withVideo:(BOOL)video
success:(void (^)(Widget *jitsiWidget))success
failure:(void (^)(NSError *error))failure;
/**
Close/Disable a widget in a room.
@param widgetId the id of the widget to close.
@param room the room the widget is in.
@param success A block object called when the operation succeeds.
@param failure A block object called when the operation fails.
@return a MXHTTPOperation instance.
*/
- (MXHTTPOperation *)closeWidget:(NSString*)widgetId inRoom:(MXRoom*)room
success:(void (^)())success
failure:(void (^)(NSError *error))failure;
/**
Add/remove matrix session.
Registering session allows to generate `kWidgetManagerDidUpdateWidgetNotification` notifications.
@param mxSession the session to add/remove.
*/
- (void)addMatrixSession:(MXSession*)mxSession;
- (void)removeMatrixSession:(MXSession*)mxSession;
@end

View file

@ -0,0 +1,329 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "WidgetManager.h"
#pragma mark - Contants
NSString *const kWidgetEventTypeString = @"im.vector.modular.widgets";
NSString *const kWidgetTypeJitsi = @"jitsi";
NSString *const kWidgetManagerDidUpdateWidgetNotification = @"kWidgetManagerDidUpdateWidgetNotification";
NSString *const WidgetManagerErrorDomain = @"WidgetManagerErrorDomain";
@interface WidgetManager ()
{
// MXSession kind of hash -> Listener for matrix events for widgets.
// There is one per matrix session
NSMutableDictionary<NSString*, id> *widgetEventListener;
// Success blocks of widgets being created
// MXSession kind of hash -> (Widget id -> `createWidget:` success block).
NSMutableDictionary<NSString*,
NSMutableDictionary<NSString*, void (^)(Widget *widget)>*> *successBlockForWidgetCreation;
// Failure blocks of widgets being created
// MXSession kind of hash -> (Widget id -> `createWidget:` failure block).
NSMutableDictionary<NSString*,
NSMutableDictionary<NSString*, void (^)(NSError *error)>*> *failureBlockForWidgetCreation;
}
@end
@implementation WidgetManager
+ (instancetype)sharedManager
{
static WidgetManager *sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[WidgetManager alloc] init];
});
return sharedManager;
}
- (instancetype)init
{
self = [super init];
if (self)
{
widgetEventListener = [NSMutableDictionary dictionary];
successBlockForWidgetCreation = [NSMutableDictionary dictionary];
failureBlockForWidgetCreation = [NSMutableDictionary dictionary];
}
return self;
}
- (NSArray<Widget *> *)widgetsInRoom:(MXRoom *)room
{
return [self widgetsOfTypes:nil inRoom:room];
}
- (NSArray<Widget *> *)widgetsOfTypes:(NSArray<NSString *> *)widgetTypes inRoom:(MXRoom *)room
{
// Widget id -> widget
NSMutableDictionary <NSString*, Widget *> *widgets = [NSMutableDictionary dictionary];
// Get all im.vector.modular.widgets state events in the room
NSMutableArray<MXEvent*> *widgetEvents = [NSMutableArray arrayWithArray:[room.state stateEventsWithType:kWidgetEventTypeString]];
// There can be several im.vector.modular.widgets state events for a same widget but
// only the last one must be considered.
// Order widgetEvents with the last event first
[widgetEvents sortUsingComparator:^NSComparisonResult(MXEvent *event1, MXEvent *event2) {
NSComparisonResult result = NSOrderedAscending;
if (event2.originServerTs > event1.originServerTs)
{
result = NSOrderedDescending;
}
else if (event2.originServerTs == event1.originServerTs)
{
result = NSOrderedSame;
}
return result;
}];
// Create each widget from its lastest im.vector.modular.widgets state event
for (MXEvent *widgetEvent in widgetEvents)
{
// Filter widget types if required
if (widgetTypes)
{
NSString *widgetType;
MXJSONModelSetString(widgetType, widgetEvent.content[@"type"]);
if (widgetType && NSNotFound == [widgetTypes indexOfObject:widgetType])
{
continue;
}
}
// widgetEvent.stateKey = widget id
if (!widgets[widgetEvent.stateKey])
{
Widget *widget = [[Widget alloc] initWithWidgetEvent:widgetEvent inMatrixSession:room.mxSession];
if (widget)
{
widgets[widget.widgetId] = widget;
}
}
}
// Return active widgets only
NSMutableArray<Widget *> *activeWidgets = [NSMutableArray array];
for (Widget *widget in widgets.allValues)
{
if (widget.isActive)
{
[activeWidgets addObject:widget];
}
}
return activeWidgets;
}
- (MXHTTPOperation *)createWidget:(NSString*)widgetId
withContent:(NSDictionary<NSString*, NSObject*>*)widgetContent
inRoom:(MXRoom*)room
success:(void (^)(Widget *widget))success
failure:(void (^)(NSError *error))failure
{
NSError *permissionError = [self checkWidgetPermissionInRoom:room];
if (permissionError)
{
if (failure)
{
failure(permissionError);
}
return nil;
}
// Send a state event with the widget data
// TODO: This API will be shortly replaced by a pure scalar API
return [room sendStateEventOfType:kWidgetEventTypeString
content:widgetContent
stateKey:widgetId
success:nil failure:failure];
}
- (MXHTTPOperation *)createJitsiWidgetInRoom:(MXRoom*)room
withVideo:(BOOL)video
success:(void (^)(Widget *jitsiWidget))success
failure:(void (^)(NSError *error))failure
{
// Build data for a jitsi widget
NSString *widgetId = [NSString stringWithFormat:@"%@_%@_%@", kWidgetTypeJitsi, room.mxSession.myUser.userId, @((uint64_t)([[NSDate date] timeIntervalSince1970] * 1000))];
// Create a random enough jitsi conference id
// Note: the jitsi server automatically creates conference when the conference
// id does not exist yet
NSString *widgetSessionId = [[[[NSProcessInfo processInfo] globallyUniqueString] substringToIndex:7] lowercaseString];
NSString *confId = [room.roomId substringWithRange:NSMakeRange(1, [room.roomId rangeOfString:@":"].location - 1)];
confId = [confId stringByAppendingString:widgetSessionId];
// TODO: This url may come from scalar API
// Note: this url can be used as is inside a web container (like iframe for Riot-web)
// Riot-iOS does not directly use it but extracts params from it (see `[JitsiViewController openWidget:withVideo:]`)
NSString *url = [NSString stringWithFormat:@"https://scalar-staging.riot.im/scalar/api/widgets/jitsi.html?confId=%@&isAudioConf=%@&displayName=$matrix_display_name&avatarUrl=$matrix_avatar_url&email=$matrix_user_id@", confId, video ? @"false" : @"true"];
NSString *hash = [NSString stringWithFormat:@"%p", room.mxSession];
successBlockForWidgetCreation[hash][widgetId] = success;
failureBlockForWidgetCreation[hash][widgetId] = failure;
return [self createWidget:widgetId
withContent:@{
@"url": url,
@"type": kWidgetTypeJitsi,
@"data": @{
@"widgetSessionId": widgetSessionId
}
}
inRoom:room
success:success
failure:failure];
}
- (MXHTTPOperation *)closeWidget:(NSString *)widgetId inRoom:(MXRoom *)room success:(void (^)())success failure:(void (^)(NSError *))failure
{
NSError *permissionError = [self checkWidgetPermissionInRoom:room];
if (permissionError)
{
if (failure)
{
failure(permissionError);
}
return nil;
}
// Send a state event with an empty content to disable the widget
// TODO: This API will be shortly replaced by a pure scalar API
return [room sendStateEventOfType:kWidgetEventTypeString
content:@{}
stateKey:widgetId
success:^(NSString *eventId)
{
if (success)
{
success();
}
} failure:failure];
}
/**
Check user's power for widgets management in a room.
@param room the room to check.
@return an NSError if the user cannot act on widgets in this room. Else, nil.
*/
- (NSError *)checkWidgetPermissionInRoom:(MXRoom *)room
{
NSError *error;
// Check user's power in the room
MXRoomPowerLevels *powerLevels = room.state.powerLevels;
NSInteger oneSelfPowerLevel = [powerLevels powerLevelOfUserWithUserID:room.mxSession.myUser.userId];
// The user must be able to send state events to manage widgets
if (oneSelfPowerLevel < powerLevels.stateDefault)
{
error = [NSError errorWithDomain:WidgetManagerErrorDomain
code:WidgetManagerErrorCodeNotEnoughPower
userInfo:@{
NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"widget_no_power_to_manage", @"Vector", nil)
}];
}
return error;
}
- (void)addMatrixSession:(MXSession *)mxSession
{
__weak __typeof__(self) weakSelf = self;
NSString *hash = [NSString stringWithFormat:@"%p", mxSession];
id listener = [mxSession listenToEventsOfTypes:@[kWidgetEventTypeString] onEvent:^(MXEvent *event, MXTimelineDirection direction, id customObject) {
typeof(self) self = weakSelf;
if (self && direction == MXTimelineDirectionForwards)
{
// stateKey = widgetId
NSString *widgetId = event.stateKey;
NSLog(@"[WidgetManager] New widget detected: %@ in %@", widgetId, event.roomId);
Widget *widget = [[Widget alloc] initWithWidgetEvent:event inMatrixSession:mxSession];
if (widget)
{
// If it is a widget we have just created, indicate its creation is complete
if (self->successBlockForWidgetCreation[hash][widgetId])
{
self->successBlockForWidgetCreation[hash][widgetId](widget);
}
// Broadcast the generic notification
[[NSNotificationCenter defaultCenter] postNotificationName:kWidgetManagerDidUpdateWidgetNotification object:widget];
}
else
{
NSLog(@"[WidgetManager] Cannot decode new widget - event: %@", event);
if (self->failureBlockForWidgetCreation[hash][widgetId])
{
// If it is a widget we have just created, indicate its creation has failed somehow
NSError *error = [NSError errorWithDomain:WidgetManagerErrorDomain
code:WidgetManagerErrorCodeCreationFailed
userInfo:@{
NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"widget_creation_failure", @"Vector", nil)
}];
self->failureBlockForWidgetCreation[hash][widgetId](error);
}
}
[self->successBlockForWidgetCreation[hash] removeObjectForKey:widgetId];
[self->failureBlockForWidgetCreation[hash] removeObjectForKey:widgetId];
}
}];
widgetEventListener[hash] = listener;
successBlockForWidgetCreation[hash] = [NSMutableDictionary dictionary];
failureBlockForWidgetCreation[hash] = [NSMutableDictionary dictionary];
}
- (void)removeMatrixSession:(MXSession *)mxSession
{
// mxSession.myUser.userId and mxSession.matrixRestClient.credentials.userId may be nil here
// So, use a kind of hash value instead
NSString *hash = [NSString stringWithFormat:@"%p", mxSession];
id listener = widgetEventListener[hash];
[mxSession removeListener:listener];
[widgetEventListener removeObjectForKey:hash];
[successBlockForWidgetCreation removeObjectForKey:hash];
[failureBlockForWidgetCreation removeObjectForKey:hash];
}
@end

View file

@ -49,6 +49,8 @@
#import "ReadReceiptsViewController.h"
#import "JitsiViewController.h"
#import "RoomEmptyBubbleCell.h"
#import "RoomIncomingTextMsgBubbleCell.h"
@ -98,6 +100,7 @@
#import "AvatarGenerator.h"
#import "Tools.h"
#import "WidgetManager.h"
#import "GBDeviceInfo_iOS.h"
@ -164,6 +167,9 @@
id kMXCallStateDidChangeObserver;
id kMXCallManagerConferenceStartedObserver;
id kMXCallManagerConferenceFinishedObserver;
// Observers to manage widgets
id kMXKWidgetManagerDidUpdateWidgetObserver;
// Observer kMXRoomSummaryDidChangeNotification to keep updated the missed discussion count
id mxRoomSummaryDidChangeObserver;
@ -468,6 +474,7 @@
[self listenTypingNotifications];
[self listenCallNotifications];
[self listenWidgetNotifications];
if (self.showExpandedHeader)
{
@ -515,7 +522,8 @@
}
[self removeCallNotificationsListeners];
[self removeWidgetNotificationsListeners];
// Re-enable the read marker display, and disable its update.
self.roomDataSource.showReadMarker = YES;
self.updateRoomReadMarker = NO;
@ -1077,7 +1085,8 @@
}
[self removeCallNotificationsListeners];
[self removeWidgetNotificationsListeners];
if (previewHeader || (self.expandedHeaderContainer.isHidden == NO))
{
// Here [destroy] is called before [viewWillDisappear:]
@ -1222,9 +1231,11 @@
userPictureView.clipsToBounds = YES;
}
// Show the hangup button if there is an active call in the current room
// Show the hangup button if there is an active call or an active jitsi
// conference call in the current room
MXCall *callInRoom = [self.roomDataSource.mxSession.callManager callInRoom:self.roomDataSource.roomId];
if (callInRoom && callInRoom.state != MXCallStateEnded)
if ((callInRoom && callInRoom.state != MXCallStateEnded)
|| [[AppDelegate theDelegate].jitsiViewController.widget.roomId isEqualToString:self.roomDataSource.roomId])
{
roomInputToolbarView.activeCall = YES;
}
@ -2648,72 +2659,126 @@
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView placeCallWithVideo:(BOOL)video
{
// Conference call is not supported in encrypted rooms
if (self.roomDataSource.room.state.isEncrypted && self.roomDataSource.room.state.joinedMembers.count > 2)
{
[currentAlert dismissViewControllerAnimated:NO completion:nil];
__weak __typeof(self) weakSelf = self;
currentAlert = [UIAlertController alertControllerWithTitle:[NSBundle mxk_localizedStringForKey:@"room_no_conference_call_in_encrypted_rooms"] message:nil preferredStyle:UIAlertControllerStyleAlert];
[currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
}
}]];
[currentAlert mxk_setAccessibilityIdentifier:@"RoomVCCallAlert"];
[self presentViewController:currentAlert animated:YES completion:nil];
}
// In case of conference call, check that the user has enough power level
else if (self.roomDataSource.room.state.joinedMembers.count > 2 &&
![MXCallManager canPlaceConferenceCallInRoom:self.roomDataSource.room])
{
[currentAlert dismissViewControllerAnimated:NO completion:nil];
__weak __typeof(self) weakSelf = self;
currentAlert = [UIAlertController alertControllerWithTitle:[NSBundle mxk_localizedStringForKey:@"room_no_power_to_create_conference_call"] message:nil preferredStyle:UIAlertControllerStyleAlert];
[currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
}
}]];
[currentAlert mxk_setAccessibilityIdentifier:@"RoomVCCallAlert"];
[self presentViewController:currentAlert animated:YES completion:nil];
}
else
{
NSString *appDisplayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
// Check app permissions before placing the call
[MXKTools checkAccessForCall:video
manualChangeMessageForAudio:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"microphone_access_not_granted_for_call"], appDisplayName]
manualChangeMessageForVideo:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"camera_access_not_granted_for_call"], appDisplayName]
showPopUpInViewController:self completionHandler:^(BOOL granted) {
__weak __typeof(self) weakSelf = self;
NSString *appDisplayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
// Check app permissions first
[MXKTools checkAccessForCall:video
manualChangeMessageForAudio:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"microphone_access_not_granted_for_call"], appDisplayName]
manualChangeMessageForVideo:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"camera_access_not_granted_for_call"], appDisplayName]
showPopUpInViewController:self completionHandler:^(BOOL granted) {
if (weakSelf)
{
typeof(self) self = weakSelf;
if (granted)
{
[self.roomDataSource.room placeCallWithVideo:video success:nil failure:nil];
[self roomInputToolbarView:toolbarView placeCallWithVideo2:video];
}
else
{
NSLog(@"RoomViewController: Warning: The application does not have the perssion to place the call");
}
}];
}
}];
}
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView placeCallWithVideo2:(BOOL)video
{
__weak __typeof(self) weakSelf = self;
#ifdef USE_JITSI_WIDGET
// If there is already a jitsi widget, join it
Widget *jitsiWidget = [customizedRoomDataSource jitsiWidget];
if (jitsiWidget)
{
[[AppDelegate theDelegate] displayJitsiViewControllerWithWidget:jitsiWidget andVideo:video];
}
// If enabled, create the conf using jitsi widget and open it directly
else if ([[NSUserDefaults standardUserDefaults] boolForKey:@"createConferenceCallsWithJitsi"]
&& self.roomDataSource.room.state.joinedMembers.count > 2)
{
[self startActivityIndicator];
[[WidgetManager sharedManager] createJitsiWidgetInRoom:self.roomDataSource.room
withVideo:video
success:^(Widget *jitsiWidget)
{
if (weakSelf)
{
typeof(self) self = weakSelf;
[self stopActivityIndicator];
[[AppDelegate theDelegate] displayJitsiViewControllerWithWidget:jitsiWidget andVideo:video];
}
}
failure:^(NSError *error)
{
if (weakSelf)
{
typeof(self) self = weakSelf;
[self stopActivityIndicator];
[self showJitsiErrorAsAlert:error];
}
}];
}
else
#endif
// Classic conference call is not supported in encrypted rooms
if (self.roomDataSource.room.state.isEncrypted && self.roomDataSource.room.state.joinedMembers.count > 2)
{
[currentAlert dismissViewControllerAnimated:NO completion:nil];
currentAlert = [UIAlertController alertControllerWithTitle:[NSBundle mxk_localizedStringForKey:@"room_no_conference_call_in_encrypted_rooms"] message:nil preferredStyle:UIAlertControllerStyleAlert];
[currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
}
}]];
[currentAlert mxk_setAccessibilityIdentifier:@"RoomVCCallAlert"];
[self presentViewController:currentAlert animated:YES completion:nil];
}
// In case of conference call, check that the user has enough power level
else if (self.roomDataSource.room.state.joinedMembers.count > 2 &&
![MXCallManager canPlaceConferenceCallInRoom:self.roomDataSource.room])
{
[currentAlert dismissViewControllerAnimated:NO completion:nil];
currentAlert = [UIAlertController alertControllerWithTitle:[NSBundle mxk_localizedStringForKey:@"room_no_power_to_create_conference_call"] message:nil preferredStyle:UIAlertControllerStyleAlert];
[currentAlert addAction:[UIAlertAction actionWithTitle:[NSBundle mxk_localizedStringForKey:@"ok"]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
if (weakSelf)
{
typeof(self) self = weakSelf;
self->currentAlert = nil;
}
}]];
[currentAlert mxk_setAccessibilityIdentifier:@"RoomVCCallAlert"];
[self presentViewController:currentAlert animated:YES completion:nil];
}
// Classic 1:1 or group call can be done
else
{
[self.roomDataSource.room placeCallWithVideo:video success:nil failure:nil];
}
}
@ -2724,6 +2789,13 @@
{
[callInRoom hangup];
}
else if ([[AppDelegate theDelegate].jitsiViewController.widget.roomId isEqualToString:self.roomDataSource.roomId])
{
[[AppDelegate theDelegate].jitsiViewController hangup];
}
[self refreshActivitiesViewDisplay];
[self refreshRoomInputToolbar];
}
- (void)roomInputToolbarView:(MXKRoomInputToolbarView*)toolbarView heightDidChanged:(CGFloat)height completion:(void (^)(BOOL finished))completion
@ -3302,6 +3374,49 @@
}];
}
#pragma mark - Widget notifications management
- (void)removeWidgetNotificationsListeners
{
if (kMXKWidgetManagerDidUpdateWidgetObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:kMXKWidgetManagerDidUpdateWidgetObserver];
kMXKWidgetManagerDidUpdateWidgetObserver = nil;
}
}
- (void)listenWidgetNotifications
{
kMXKWidgetManagerDidUpdateWidgetObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kWidgetManagerDidUpdateWidgetNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
Widget *widget = notif.object;
if (widget.mxSession == self.roomDataSource.mxSession
&& [widget.roomId isEqualToString:customizedRoomDataSource.roomId])
{
// Jitsi conference widget existence is shown in the bottom bar
// Update the bar
[self refreshActivitiesViewDisplay];
[self refreshRoomInputToolbar];
}
}];
}
- (void)showJitsiErrorAsAlert:(NSError*)error
{
// Customise the error for permission issues
if ([error.domain isEqualToString:WidgetManagerErrorDomain] && error.code == WidgetManagerErrorCodeNotEnoughPower)
{
error = [NSError errorWithDomain:error.domain
code:error.code
userInfo:@{
NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"room_conference_call_no_power", @"Vector", nil)
}];
}
// Alert user
[[AppDelegate theDelegate] showErrorAsAlert:error];
}
#pragma mark - Unreachable Network Handling
- (void)refreshActivitiesViewDisplay
@ -3309,12 +3424,14 @@
if ([self.activitiesView isKindOfClass:RoomActivitiesView.class])
{
RoomActivitiesView *roomActivitiesView = (RoomActivitiesView*)self.activitiesView;
// Reset gesture recognizers
while (roomActivitiesView.gestureRecognizers.count)
{
[roomActivitiesView removeGestureRecognizer:roomActivitiesView.gestureRecognizers[0]];
}
Widget *jitsiWidget = [customizedRoomDataSource jitsiWidget];
if ([AppDelegate theDelegate].isOffline)
{
@ -3342,9 +3459,80 @@
{
[customizedRoomDataSource.room placeCallWithVideo:video success:nil failure:nil];
}
} onClosePressed:nil];
}
}
#ifdef USE_JITSI_WIDGET
else if (jitsiWidget)
{
// The room has an active jitsi widget
// Show it in the banner if the user is not already in
AppDelegate *appDelegate = [AppDelegate theDelegate];
if ([appDelegate.jitsiViewController.widget.widgetId isEqualToString:jitsiWidget.widgetId])
{
if ([self checkUnsentMessages] == NO)
{
[self refreshTypingNotification];
}
}
else
{
[roomActivitiesView displayOngoingConferenceCall:^(BOOL video) {
NSLog(@"[RoomVC] onOngoingConferenceCallPressed (jitsi)");
__weak __typeof(self) weakSelf = self;
NSString *appDisplayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
// Check app permissions first
[MXKTools checkAccessForCall:video
manualChangeMessageForAudio:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"microphone_access_not_granted_for_call"], appDisplayName]
manualChangeMessageForVideo:[NSString stringWithFormat:[NSBundle mxk_localizedStringForKey:@"camera_access_not_granted_for_call"], appDisplayName]
showPopUpInViewController:self completionHandler:^(BOOL granted) {
if (weakSelf)
{
if (granted)
{
// Present the Jitsi view controller
[appDelegate displayJitsiViewControllerWithWidget:jitsiWidget andVideo:video];
}
else
{
NSLog(@"[RoomVC] onOngoingConferenceCallPressed: Warning: The application does not have the perssion to join the call");
}
}
}];
} onClosePressed:^{
[self startActivityIndicator];
// Close the widget
__weak __typeof(self) weakSelf = self;
[[WidgetManager sharedManager] closeWidget:jitsiWidget.widgetId inRoom:self.roomDataSource.room success:^{
if (weakSelf)
{
typeof(self) self = weakSelf;
[self stopActivityIndicator];
// The banner will automatically leave thanks to kWidgetManagerDidUpdateWidgetNotification
}
} failure:^(NSError *error) {
if (weakSelf)
{
typeof(self) self = weakSelf;
[self showJitsiErrorAsAlert:error];
[self stopActivityIndicator];
}
}];
}];
}
}
#endif
else if ([self checkUnsentMessages] == NO)
{
// Show "scroll to bottom" icon when the most recent message is not visible,

View file

@ -98,7 +98,10 @@ enum
enum
{
LABS_CRYPTO_INDEX = 0,
#ifdef USE_JITSI_WIDGET
LABS_MATRIX_APPS_INDEX = 0,
#endif
LABS_CRYPTO_INDEX,
LABS_COUNT
};
@ -1913,6 +1916,21 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)();
}
else if (section == SETTINGS_SECTION_LABS_INDEX)
{
#ifdef USE_JITSI_WIDGET
if (row == LABS_MATRIX_APPS_INDEX)
{
MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
labelAndSwitchCell.mxkLabel.text = NSLocalizedStringFromTable(@"settings_labs_create_conference_with_jitsi", @"Vector", nil);
labelAndSwitchCell.mxkSwitch.on = [[NSUserDefaults standardUserDefaults] boolForKey:@"createConferenceCallsWithJitsi"];
[labelAndSwitchCell.mxkSwitch removeTarget:self action:nil forControlEvents:UIControlEventTouchUpInside];
[labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleJitsiForConference:) forControlEvents:UIControlEventTouchUpInside];
cell = labelAndSwitchCell;
}
else
#endif
if (row == LABS_CRYPTO_INDEX)
{
MXSession* session = [[AppDelegate theDelegate].mxSessions objectAtIndex:0];
@ -2628,6 +2646,19 @@ typedef void (^blockSettingsViewController_onReadyToDestroy)();
}
}
- (void)toggleJitsiForConference:(id)sender
{
if (sender && [sender isKindOfClass:UISwitch.class])
{
UISwitch *switchButton = (UISwitch*)sender;
[[NSUserDefaults standardUserDefaults] setBool:switchButton.isOn forKey:@"createConferenceCallsWithJitsi"];
[[NSUserDefaults standardUserDefaults] synchronize];
[self.tableView reloadData];
}
}
- (void)toggleLabsEndToEndEncryption:(id)sender
{
if (sender && [sender isKindOfClass:UISwitch.class])

View file

@ -0,0 +1,104 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import <MatrixKit/MatrixKit.h>
#import <JitsiMeet/JitsiMeet.h>
#import "WidgetManager.h"
@protocol JitsiViewControllerDelegate;
/**
The `JitsiViewController` is a VC for specifically handling a jitsi widget using the
jitsi-meet iOS SDK instead of displaying it in a webview like other scalar widgets.
https://github.com/jitsi/jitsi-meet/tree/master/ios
*/
@interface JitsiViewController : MXKViewController <JitsiMeetViewDelegate>
/**
Returns the `UINib` object initialized for a `JitsiViewController`.
@return The initialized `UINib` object or `nil` if there were errors during initialization
or the nib file could not be located.
*/
+ (UINib *)nib;
/**
Creates and returns a new `JitsiViewController` object.
@discussion This is the designated initializer for programmatic instantiation.
@return An initialized `JitsiViewController` object if successful, `nil` otherwise.
*/
+ (instancetype)jitsiViewController;
/**
Make jitsi-meet iOS SDK open the jitsi conference indicated by a jitsi widget.
@param widget the jitsi widget.
@param video to indicate voice or video call.
@return YES if the operation is successful.
*/
- (BOOL)openWidget:(Widget*)widget withVideo:(BOOL)video;
/**
Hang up the jitsi conference call in progress.
*/
- (void)hangup;
/**
The jitsi widget displayed by this `JitsiViewController`.
*/
@property (nonatomic, readonly) Widget *widget;
/**
The delegate for the view controller.
*/
@property (nonatomic) id<JitsiViewControllerDelegate> delegate;
#pragma mark - Xib attributes
// The jitsi-meet SDK view
@property (weak, nonatomic) IBOutlet JitsiMeetView *jitsiMeetView;
@property (weak, nonatomic) IBOutlet UIButton *backToAppButton;
@end
/**
Delegate for `JitsiViewController` object
*/
@protocol JitsiViewControllerDelegate <NSObject>
/**
Tells the delegate to dismiss the jitsi view controller.
@param jitsiViewController the jitsi view controller.
@param completion the block to execute at the end of the operation.
*/
- (void)jitsiViewController:(JitsiViewController *)jitsiViewController dismissViewJitsiController:(void (^)())completion;
/**
Tells the delegate to put the jitsi view controller in background.
@param jitsiViewController the jitsi view controller.
@param completion the block to execute at the end of the operation.
*/
- (void)jitsiViewController:(JitsiViewController *)jitsiViewController goBackToApp:(void (^)())completion;
@end

View file

@ -0,0 +1,147 @@
/*
Copyright 2017 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "JitsiViewController.h"
static const NSString *kJitsiServerUrl = @"https://jitsi.riot.im/";
@interface JitsiViewController ()
{
NSString *jitsiUrl;
BOOL video;
}
@end
@implementation JitsiViewController
#pragma mark - Class methods
+ (UINib *)nib
{
return [UINib nibWithNibName:NSStringFromClass(self.class)
bundle:[NSBundle bundleForClass:self.class]];
}
+ (instancetype)jitsiViewController
{
JitsiViewController *jitsiViewController = [[[self class] alloc] initWithNibName:NSStringFromClass(self.class)
bundle:[NSBundle bundleForClass:self.class]];
return jitsiViewController;
}
- (BOOL)openWidget:(Widget*)widget withVideo:(BOOL)aVideo
{
video = aVideo;
_widget = widget;
// Extract the jitsi conference id from the widget url
NSString *confId;
NSURL *url = [NSURL URLWithString:_widget.url];
NSURLComponents *components = [[NSURLComponents new] initWithURL:url resolvingAgainstBaseURL:NO];
NSArray *queryItems = [components queryItems];
for (NSURLQueryItem *item in queryItems)
{
if ([item.name isEqualToString:@"confId"])
{
confId = item.value;
break;
}
}
// And build from it the url to use in jitsi-meet sdk
if (confId)
{
jitsiUrl = [NSString stringWithFormat:@"%@%@", kJitsiServerUrl, confId];
}
if (!jitsiUrl)
{
NSLog(@"[JitsiVC] Failed to load widget: %@", widget);
}
return (jitsiUrl != nil);
}
- (void)hangup
{
jitsiUrl = nil;
// It would have been nicer to ask JitsiMeetView but there is no api.
// Dismissing the view controller and releasing it does the job for the moment
if (_delegate)
{
[_delegate jitsiViewController:self dismissViewJitsiController:nil];
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.jitsiMeetView.delegate = self;
// Pass the URL to jitsi-meet sdk
[self.jitsiMeetView loadURLObject: @{
@"url": jitsiUrl,
@"configOverwrite": @{
@"startWithVideoMuted": @(!video)
}
}];
// TODO: Set up user info but it is not yet available in the jitsi-meet iOS SDK
// See https://github.com/jitsi/jitsi-meet/issues/1880
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}
#pragma mark - Actions
- (IBAction)onBackToAppButtonPressed:(id)sender
{
if (_delegate)
{
[_delegate jitsiViewController:self goBackToApp:nil];
}
}
#pragma mark - JitsiMeetViewDelegate
- (void)conferenceFailed:(NSDictionary *)data
{
NSLog(@"[JitsiViewController] conferenceFailed - data: %@", data);
}
- (void)conferenceLeft:(NSDictionary *)data
{
// The conference is over. Let the delegate close this view controller.
if (_delegate)
{
[_delegate jitsiViewController:self dismissViewJitsiController:nil];
}
else
{
// Do it ourself
[self dismissViewControllerAnimated:YES completion:nil];
}
}
@end

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="12121" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="JitsiViewController">
<connections>
<outlet property="backToAppButton" destination="8tr-Cb-ue8" id="aUj-co-7JA"/>
<outlet property="jitsiMeetView" destination="7hL-Cs-mak" id="7kR-Te-Klw"/>
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7hL-Cs-mak" customClass="JitsiMeetView">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
<button opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenHighlighted="NO" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8tr-Cb-ue8">
<rect key="frame" x="10" y="5" width="44" height="44"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<accessibility key="accessibilityConfiguration" identifier="CallVCBackToAppButton"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="3id-6Q-PUF"/>
<constraint firstAttribute="width" constant="44" id="JGj-Jz-SbU"/>
</constraints>
<fontDescription key="fontDescription" name="Helvetica-Bold" family="Helvetica" pointSize="13"/>
<color key="tintColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<inset key="titleEdgeInsets" minX="-69" minY="61" maxX="0.0" maxY="0.0"/>
<state key="normal" image="back_icon.png">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<state key="highlighted">
<color key="titleColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="onBackToAppButtonPressed:" destination="-1" eventType="touchUpInside" id="2wo-nB-Rwd"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="7hL-Cs-mak" secondAttribute="trailing" id="8eH-2r-pjD"/>
<constraint firstAttribute="bottom" secondItem="7hL-Cs-mak" secondAttribute="bottom" id="BAo-6X-ovC"/>
<constraint firstItem="8tr-Cb-ue8" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" constant="5" id="FPS-wy-gK6"/>
<constraint firstItem="8tr-Cb-ue8" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" constant="10" id="Xca-R4-1cu"/>
<constraint firstItem="7hL-Cs-mak" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="s46-Fx-tT8"/>
<constraint firstItem="7hL-Cs-mak" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="x3v-Xl-cKi"/>
</constraints>
</view>
</objects>
<resources>
<image name="back_icon.png" width="12" height="23"/>
</resources>
</document>

View file

@ -63,9 +63,10 @@
@param ongoingConferenceCallPressed the block called when the user clicks on the banner.
video is YES if the user chose to join the conf in video mode.
@param ongoingConferenceCallClosePressed the block called when the user clicks on the banner close button.
nil means do not display a close button.
*/
- (void)displayOngoingConferenceCall:(void (^)(BOOL video))ongoingConferenceCallPressed;
- (void)displayOngoingConferenceCall:(void (^)(BOOL video))ongoingConferenceCallPressed onClosePressed:(void (^)(void))ongoingConferenceCallClosePressed;
/**
Display a "scroll to bottom" icon.

View file

@ -211,17 +211,31 @@
[self checkHeight:YES];
}
- (void)displayOngoingConferenceCall:(void (^)(BOOL))onOngoingConferenceCallPressed
- (void)displayOngoingConferenceCall:(void (^)(BOOL))onOngoingConferenceCallPressed onClosePressed:(void (^)(void))onOngoingConferenceCallClosePressed
{
[self reset];
objc_setAssociatedObject(self.messageTextView, "onOngoingConferenceCallPressed", [onOngoingConferenceCallPressed copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// Build the string to display in the banner
NSString *onGoingConferenceCall =
[NSString stringWithFormat:NSLocalizedStringFromTable(@"room_ongoing_conference_call", @"Vector", nil),
NSLocalizedStringFromTable(@"voice", @"Vector", nil),
NSLocalizedStringFromTable(@"video", @"Vector", nil)];
NSString *onGoingConferenceCall;
if (!onOngoingConferenceCallClosePressed)
{
onGoingConferenceCall = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_ongoing_conference_call", @"Vector", nil),
NSLocalizedStringFromTable(@"voice", @"Vector", nil),
NSLocalizedStringFromTable(@"video", @"Vector", nil)];
}
else
{
// Display the banner with a "Close it" string
objc_setAssociatedObject(self.messageTextView, "onOngoingConferenceCallClosePressed", [onOngoingConferenceCallClosePressed copy], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
onGoingConferenceCall = [NSString stringWithFormat:NSLocalizedStringFromTable(@"room_ongoing_conference_call_with_close", @"Vector", nil),
NSLocalizedStringFromTable(@"voice", @"Vector", nil),
NSLocalizedStringFromTable(@"video", @"Vector", nil),
NSLocalizedStringFromTable(@"room_ongoing_conference_call_close", @"Vector", nil)];
}
NSMutableAttributedString *onGoingConferenceCallAttibutedString = [[NSMutableAttributedString alloc] initWithString:onGoingConferenceCall];
@ -235,6 +249,14 @@
[onGoingConferenceCallAttibutedString addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:videoRange];
[onGoingConferenceCallAttibutedString addAttribute:NSLinkAttributeName value:@"onOngoingConferenceCallWithVideoPressed" range:videoRange];
// Add a link on the "Close" string
if (onOngoingConferenceCallClosePressed)
{
NSRange closeRange = [onGoingConferenceCall rangeOfString:NSLocalizedStringFromTable(@"room_ongoing_conference_call_close", @"Vector", nil)];
[onGoingConferenceCallAttibutedString addAttribute:NSUnderlineStyleAttributeName value:@(NSUnderlineStyleSingle) range:closeRange];
[onGoingConferenceCallAttibutedString addAttribute:NSLinkAttributeName value:@"onOngoingConferenceCallClosePressed" range:closeRange];
}
// Display the string in white on pink red
NSRange wholeString = NSMakeRange(0, onGoingConferenceCallAttibutedString.length);
[onGoingConferenceCallAttibutedString addAttribute:NSForegroundColorAttributeName value:kRiotPrimaryBgColor range:wholeString];
@ -382,7 +404,16 @@
return NO;
}
return YES;
else if ([[URL absoluteString] isEqualToString:@"onOngoingConferenceCallClosePressed"])
{
void (^onOngoingConferenceCallClosePressed)(BOOL) = objc_getAssociatedObject(self.messageTextView, "onOngoingConferenceCallClosePressed");
if (onOngoingConferenceCallClosePressed)
{
onOngoingConferenceCallClosePressed(YES);
}
return NO;
} return YES;
}
#pragma mark - UIGestureRecognizerDelegate

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,18 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <JitsiMeet/JitsiMeetView.h>
#import <JitsiMeet/JitsiMeetViewDelegate.h>

View file

@ -0,0 +1,46 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "JitsiMeetViewDelegate.h"
@interface JitsiMeetView : UIView
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
@property (nonatomic) BOOL welcomePageEnabled;
+ (BOOL)application:(UIApplication *_Nonnull)application
didFinishLaunchingWithOptions:(NSDictionary *_Nonnull)launchOptions;
+ (BOOL)application:(UIApplication * _Nonnull)application
continueUserActivity:(NSUserActivity * _Nonnull)userActivity
restorationHandler:(void (^ _Nullable)(NSArray * _Nullable))restorationHandler;
+ (BOOL)application:(UIApplication * _Nonnull)application
openURL:(NSURL * _Nonnull)URL
sourceApplication:(NSString * _Nullable)sourceApplication
annotation:(id _Nullable)annotation;
- (void)loadURL:(NSURL * _Nullable)url;
- (void)loadURLObject:(NSDictionary * _Nullable)urlObject;
- (void)loadURLString:(NSString * _Nullable)urlString;
@end

View file

@ -0,0 +1,62 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@protocol JitsiMeetViewDelegate <NSObject>
@optional
/**
* Called when a joining a conference was unsuccessful or when there was an
* error while in a conference.
*
* The {@code data} dictionary contains an "error" key describing the error and
* a {@code url} key with the conference URL.
*/
- (void) conferenceFailed:(NSDictionary *)data;
/**
* Called when a conference was joined.
*
* The {@code data} dictionary contains a {@code url} key with the conference
* URL.
*/
- (void) conferenceJoined:(NSDictionary *)data;
/**
* Called when a conference was left.
*
* The {@code data} dictionary contains a {@code url} key with the conference
* URL.
*/
- (void) conferenceLeft:(NSDictionary *)data;
/**
* Called before a conference is joined.
*
* The {@code data} dictionary contains a {@code url} key with the conference
* URL.
*/
- (void) conferenceWillJoin:(NSDictionary *)data;
/**
* Called before a conference is left.
*
* The {@code data} dictionary contains a {@code url} key with the conference
* URL.
*/
- (void) conferenceWillLeave:(NSDictionary *)data;
@end

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>16F73</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>JitsiMeet</string>
<key>CFBundleIdentifier</key>
<string>org.jitsi.JitsiMeetSDK.ios</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>JitsiMeet</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>14E8301</string>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>DTPlatformVersion</key>
<string>10.3</string>
<key>DTSDKBuild</key>
<string>14E8301</string>
<key>DTSDKName</key>
<string>iphoneos10.3</string>
<key>DTXcode</key>
<string>0833</string>
<key>DTXcodeBuild</key>
<string>8E3004b</string>
<key>JitsiMeetFonts</key>
<array>
<string>FontAwesome.ttf</string>
<string>jitsi.ttf</string>
</array>
<key>MinimumOSVersion</key>
<string>10.3</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>192.168.2.9.xip.io</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>localhost</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,6 @@
framework module JitsiMeet {
umbrella header "JitsiMeet.h"
export *
module * { export * }
}

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
192.168.2.9.xip.io

Binary file not shown.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
®t»Ηη<>†ΩxV<78><56>ήGPƒmHΰ

View file

@ -0,0 +1,10 @@
jitsi-meet (https://github.com/jitsi/jitsi-meet) does not provide yet a pod for
iOS SDK (https://github.com/jitsi/jitsi-meet/issues/1854). So, the framework is
built as described below and it is temporarly added to the Xcode project.
How to build JitsiMeet.framework:
- clone https://github.com/jitsi/jitsi-meet
- build jitsi-meet following instruction at https://github.com/jitsi/jitsi-meet#building-the-sources
- build it specifically for iOS using https://github.com/jitsi/jitsi-meet/blob/master/doc/mobile.md#ios
Then, copy the generated JitsiMeet.framework here.

View file

@ -1342,6 +1342,16 @@
regulations.
</pre>
</li>
<li>
<b>jitsi-meet</b> (<a
href="https://github.com/jitsi/jitsi-meet">https://github.com/jitsi/jitsi-meet</a>)
<br/><br/>Jitsi Meet - Secure, Simple and Scalable Video Conferences.
<br/><br/>Copyright @ 2017-present Atlassian Pty Ltd.
<br/><br/>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:
<br/><br/><a
href="https://www.apache.org/licenses/LICENSE-2.0">https://www.apache.org/licenses/LICENSE-2.0</a>
<br/><br/>Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
</li>
</ul>
</body>
</html>