[iOS] Add support for Siri shortcuts

This is mostly implemented in the app, with the needed support in the SDK. Since
the app needs to donate intents and deal with creating NSUserActivity objects it
doesn't feel right to do this in a library. Instead, we donate the intents from
the app, but the SDK is ready to extract conference URLs from any intent which
was registered as a conference activity.

This also opens the door for eventually adding Handoff support.
This commit is contained in:
Saúl Ibarra Corretgé 2018-10-18 15:54:26 +02:00 committed by Zoltan Bettenbuk
parent 4898f81596
commit 889644f7bd
9 changed files with 89 additions and 3 deletions

View File

@ -7,5 +7,7 @@
<string>applinks:beta.meet.jit.si</string> <string>applinks:beta.meet.jit.si</string>
<string>applinks:meet.jit.si</string> <string>applinks:meet.jit.si</string>
</array> </array>
<key>com.apple.developer.siri</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@ -43,6 +43,7 @@
0B412F1D1EDEE6E800B1A0A6 /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = "<group>"; }; 0B412F1D1EDEE6E800B1A0A6 /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = "<group>"; };
0B412F1E1EDEE6E800B1A0A6 /* ViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; }; 0B412F1E1EDEE6E800B1A0A6 /* ViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; };
0B412F201EDEE95300B1A0A6 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; 0B412F201EDEE95300B1A0A6 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
0BBD021F212EB69D00CCB19F /* Types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Types.h; sourceTree = "<group>"; };
0BD6B4361EF82A6B00D1F4CD /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = "../../node_modules/react-native-webrtc/ios/WebRTC.framework"; sourceTree = "<group>"; }; 0BD6B4361EF82A6B00D1F4CD /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = "../../node_modules/react-native-webrtc/ios/WebRTC.framework"; sourceTree = "<group>"; };
13B07F961A680F5B00A75B9A /* jitsi-meet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "jitsi-meet.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07F961A680F5B00A75B9A /* jitsi-meet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "jitsi-meet.app"; sourceTree = BUILT_PRODUCTS_DIR; };
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
@ -96,6 +97,7 @@
13B07FB11A68108700A75B9A /* LaunchScreen.xib */, 13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
13B07FB71A68108700A75B9A /* main.m */, 13B07FB71A68108700A75B9A /* main.m */,
0B412F201EDEE95300B1A0A6 /* Main.storyboard */, 0B412F201EDEE95300B1A0A6 /* Main.storyboard */,
0BBD021F212EB69D00CCB19F /* Types.h */,
0B412F1D1EDEE6E800B1A0A6 /* ViewController.h */, 0B412F1D1EDEE6E800B1A0A6 /* ViewController.h */,
0B412F1E1EDEE6E800B1A0A6 /* ViewController.m */, 0B412F1E1EDEE6E800B1A0A6 /* ViewController.m */,
); );
@ -173,6 +175,9 @@
com.apple.SafariKeychain = { com.apple.SafariKeychain = {
enabled = 1; enabled = 1;
}; };
com.apple.Siri = {
enabled = 1;
};
}; };
}; };
}; };

View File

@ -17,6 +17,7 @@
#import "AppDelegate.h" #import "AppDelegate.h"
#import "FIRUtilities.h" #import "FIRUtilities.h"
#import "Types.h"
#import <JitsiMeet/JitsiMeet.h> #import <JitsiMeet/JitsiMeet.h>
@ -37,6 +38,10 @@
[Fabric with:@[[Crashlytics class]]]; [Fabric with:@[[Crashlytics class]]];
} }
// Set the conference activity type defined in this application.
// This cannot be defined by the SDK.
JitsiMeetView.conferenceActivityType = JitsiMeetConferenceActivityType;
return [JitsiMeetView application:application return [JitsiMeetView application:application
didFinishLaunchingWithOptions:launchOptions]; didFinishLaunchingWithOptions:launchOptions];
} }

View File

@ -97,5 +97,9 @@
<false/> <false/>
<key>firebase_crashlytics_collection_enabled</key> <key>firebase_crashlytics_collection_enabled</key>
<string>false</string> <string>false</string>
<key>NSUserActivityTypes</key>
<array>
<string>org.jitsi.JitsiMeet.ios.conference</string>
</array>
</dict> </dict>
</plist> </plist>

7
ios/app/src/Types.h Normal file
View File

@ -0,0 +1,7 @@
#import <Foundation/Foundation.h>
// This must match what's defined in the NSUserActivityTypes array in the
// Info.plist file.
static NSString *const JitsiMeetConferenceActivityType
= @"org.jitsi.JitsiMeet.ios.conference";

View File

@ -14,8 +14,16 @@
* limitations under the License. * limitations under the License.
*/ */
#import <Availability.h>
#import <CoreSpotlight/CoreSpotlight.h>
#import <MobileCoreServices/MobileCoreServices.h>
#import "Types.h"
#import "ViewController.h" #import "ViewController.h"
// Needed for NSUserActivity suggestedInvocationPhrase
@import Intents;
/** /**
* The query to perform through JMAddPeopleController when the InviteButton is * The query to perform through JMAddPeopleController when the InviteButton is
* tapped in order to exercise the public API of the feature invite. If nil, the * tapped in order to exercise the public API of the feature invite. If nil, the
@ -33,11 +41,10 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil;
[super viewDidLoad]; [super viewDidLoad];
JitsiMeetView *view = (JitsiMeetView *) self.view; JitsiMeetView *view = (JitsiMeetView *) self.view;
view.delegate = self;
#ifdef DEBUG #ifdef DEBUG
view.delegate = self;
// inviteController // inviteController
JMInviteController *inviteController = view.inviteController; JMInviteController *inviteController = view.inviteController;
inviteController.delegate = self; inviteController.delegate = self;
@ -56,12 +63,13 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil;
[view loadURL:nil]; [view loadURL:nil];
} }
#if DEBUG
// JitsiMeetViewDelegate // JitsiMeetViewDelegate
- (void)_onJitsiMeetViewDelegateEvent:(NSString *)name - (void)_onJitsiMeetViewDelegateEvent:(NSString *)name
withData:(NSDictionary *)data { withData:(NSDictionary *)data {
#if DEBUG
NSLog( NSLog(
@"[%s:%d] JitsiMeetViewDelegate %@ %@", @"[%s:%d] JitsiMeetViewDelegate %@ %@",
__FILE__, __LINE__, name, data); __FILE__, __LINE__, name, data);
@ -70,6 +78,7 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil;
[NSThread isMainThread], [NSThread isMainThread],
@"JitsiMeetViewDelegate %@ method invoked on a non-main thread", @"JitsiMeetViewDelegate %@ method invoked on a non-main thread",
name); name);
#endif
} }
- (void)conferenceFailed:(NSDictionary *)data { - (void)conferenceFailed:(NSDictionary *)data {
@ -78,6 +87,36 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil;
- (void)conferenceJoined:(NSDictionary *)data { - (void)conferenceJoined:(NSDictionary *)data {
[self _onJitsiMeetViewDelegateEvent:@"CONFERENCE_JOINED" withData:data]; [self _onJitsiMeetViewDelegateEvent:@"CONFERENCE_JOINED" withData:data];
// Register a NSUserActivity for this conference so it can be invoked as a
// Siri shortcut. This is only supported in iOS >= 12.
#ifdef __IPHONE_12_0
if (@available(iOS 12.0, *)) {
NSUserActivity *userActivity
= [[NSUserActivity alloc] initWithActivityType:JitsiMeetConferenceActivityType];
NSString *urlStr = data[@"url"];
NSURL *url = [NSURL URLWithString:urlStr];
NSString *conference = [url.pathComponents lastObject];
userActivity.title = [NSString stringWithFormat:@"Join %@", conference];
userActivity.suggestedInvocationPhrase = @"Join my Jitsi meeting";
userActivity.userInfo = @{@"url": urlStr};
[userActivity setEligibleForSearch:YES];
[userActivity setEligibleForPrediction:YES];
[userActivity setPersistentIdentifier:urlStr];
// Subtitle
CSSearchableItemAttributeSet *attributes
= [[CSSearchableItemAttributeSet alloc] initWithItemContentType:(NSString *)kUTTypeItem];
attributes.contentDescription = urlStr;
userActivity.contentAttributeSet = attributes;
self.userActivity = userActivity;
[userActivity becomeCurrent];
}
#endif
} }
- (void)conferenceLeft:(NSDictionary *)data { - (void)conferenceLeft:(NSDictionary *)data {
@ -96,6 +135,8 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil;
[self _onJitsiMeetViewDelegateEvent:@"LOAD_CONFIG_ERROR" withData:data]; [self _onJitsiMeetViewDelegateEvent:@"LOAD_CONFIG_ERROR" withData:data];
} }
#if DEBUG
// JMInviteControllerDelegate // JMInviteControllerDelegate
- (void)beginAddPeople:(JMAddPeopleController *)addPeopleController { - (void)beginAddPeople:(JMAddPeopleController *)addPeopleController {

View File

@ -18,6 +18,7 @@
@interface JitsiMeetView () @interface JitsiMeetView ()
+ (NSDictionary *)conferenceURLFromUserActivity:(NSUserActivity *)userActivity;
+ (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope; + (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope;
@end @end

View File

@ -22,6 +22,8 @@
@interface JitsiMeetView : UIView @interface JitsiMeetView : UIView
@property (class, copy, nonatomic, nullable) NSString *conferenceActivityType;
@property (copy, nonatomic, nullable) NSURL *defaultURL; @property (copy, nonatomic, nullable) NSURL *defaultURL;
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate; @property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;

View File

@ -88,6 +88,8 @@ void registerFatalErrorHandler() {
@dynamic pictureInPictureEnabled; @dynamic pictureInPictureEnabled;
static NSString *_conferenceActivityType;
static RCTBridgeWrapper *bridgeWrapper; static RCTBridgeWrapper *bridgeWrapper;
/** /**
@ -277,6 +279,16 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
[self loadURLObject:urlString ? @{ @"url": urlString } : nil]; [self loadURLObject:urlString ? @{ @"url": urlString } : nil];
} }
#pragma conferenceActivityType getter / setter
+ (NSString *)conferenceActivityType {
return _conferenceActivityType;
}
+ (void) setConferenceActivityType:(NSString *)conferenceActivityType {
_conferenceActivityType = conferenceActivityType;
}
#pragma pictureInPictureEnabled getter / setter #pragma pictureInPictureEnabled getter / setter
- (void) setPictureInPictureEnabled:(BOOL)pictureInPictureEnabled { - (void) setPictureInPictureEnabled:(BOOL)pictureInPictureEnabled {
@ -358,6 +370,13 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
@"url": url @"url": url
}; };
} }
} else if (_conferenceActivityType && [activityType isEqualToString:_conferenceActivityType]) {
// App was started by continuing a registered NSUserActivity (SiriKit, Handoff, ...)
NSString *url;
if ((url = userActivity.userInfo[@"url"])) {
return @{ @"url" : url };
}
} }
return nil; return nil;