diff --git a/ios/app/app.entitlements b/ios/app/app.entitlements index 939bee422..accd78bb6 100644 --- a/ios/app/app.entitlements +++ b/ios/app/app.entitlements @@ -7,5 +7,7 @@ applinks:beta.meet.jit.si applinks:meet.jit.si + com.apple.developer.siri + diff --git a/ios/app/app.xcodeproj/project.pbxproj b/ios/app/app.xcodeproj/project.pbxproj index b73552d21..a2c85bf1d 100644 --- a/ios/app/app.xcodeproj/project.pbxproj +++ b/ios/app/app.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 0B412F1D1EDEE6E800B1A0A6 /* ViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 0B412F1E1EDEE6E800B1A0A6 /* ViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 0B412F201EDEE95300B1A0A6 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 0BBD021F212EB69D00CCB19F /* Types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Types.h; sourceTree = ""; }; 0BD6B4361EF82A6B00D1F4CD /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = "../../node_modules/react-native-webrtc/ios/WebRTC.framework"; sourceTree = ""; }; 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 = ""; }; @@ -96,6 +97,7 @@ 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, 13B07FB71A68108700A75B9A /* main.m */, 0B412F201EDEE95300B1A0A6 /* Main.storyboard */, + 0BBD021F212EB69D00CCB19F /* Types.h */, 0B412F1D1EDEE6E800B1A0A6 /* ViewController.h */, 0B412F1E1EDEE6E800B1A0A6 /* ViewController.m */, ); @@ -173,6 +175,9 @@ com.apple.SafariKeychain = { enabled = 1; }; + com.apple.Siri = { + enabled = 1; + }; }; }; }; diff --git a/ios/app/src/AppDelegate.m b/ios/app/src/AppDelegate.m index 3768cf44a..3de9b1417 100644 --- a/ios/app/src/AppDelegate.m +++ b/ios/app/src/AppDelegate.m @@ -17,6 +17,7 @@ #import "AppDelegate.h" #import "FIRUtilities.h" +#import "Types.h" #import @@ -37,6 +38,10 @@ [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 didFinishLaunchingWithOptions:launchOptions]; } diff --git a/ios/app/src/Info.plist b/ios/app/src/Info.plist index 335b12c90..b2a7c1960 100644 --- a/ios/app/src/Info.plist +++ b/ios/app/src/Info.plist @@ -97,5 +97,9 @@ firebase_crashlytics_collection_enabled false + NSUserActivityTypes + + org.jitsi.JitsiMeet.ios.conference + diff --git a/ios/app/src/Types.h b/ios/app/src/Types.h new file mode 100644 index 000000000..c44cd888e --- /dev/null +++ b/ios/app/src/Types.h @@ -0,0 +1,7 @@ + +#import + +// This must match what's defined in the NSUserActivityTypes array in the +// Info.plist file. +static NSString *const JitsiMeetConferenceActivityType + = @"org.jitsi.JitsiMeet.ios.conference"; diff --git a/ios/app/src/ViewController.m b/ios/app/src/ViewController.m index 601ea0be0..205e7e3bf 100644 --- a/ios/app/src/ViewController.m +++ b/ios/app/src/ViewController.m @@ -14,8 +14,16 @@ * limitations under the License. */ +#import +#import +#import + +#import "Types.h" #import "ViewController.h" +// Needed for NSUserActivity suggestedInvocationPhrase +@import Intents; + /** * 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 @@ -33,11 +41,10 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil; [super viewDidLoad]; JitsiMeetView *view = (JitsiMeetView *) self.view; + view.delegate = self; #ifdef DEBUG - view.delegate = self; - // inviteController JMInviteController *inviteController = view.inviteController; inviteController.delegate = self; @@ -56,12 +63,13 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil; [view loadURL:nil]; } -#if DEBUG + // JitsiMeetViewDelegate - (void)_onJitsiMeetViewDelegateEvent:(NSString *)name withData:(NSDictionary *)data { +#if DEBUG NSLog( @"[%s:%d] JitsiMeetViewDelegate %@ %@", __FILE__, __LINE__, name, data); @@ -70,6 +78,7 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil; [NSThread isMainThread], @"JitsiMeetViewDelegate %@ method invoked on a non-main thread", name); +#endif } - (void)conferenceFailed:(NSDictionary *)data { @@ -78,6 +87,36 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil; - (void)conferenceJoined:(NSDictionary *)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 { @@ -96,6 +135,8 @@ static NSString * const ADD_PEOPLE_CONTROLLER_QUERY = nil; [self _onJitsiMeetViewDelegateEvent:@"LOAD_CONFIG_ERROR" withData:data]; } +#if DEBUG + // JMInviteControllerDelegate - (void)beginAddPeople:(JMAddPeopleController *)addPeopleController { diff --git a/ios/sdk/src/JitsiMeetView+Private.h b/ios/sdk/src/JitsiMeetView+Private.h index cc167c8a0..b62501746 100644 --- a/ios/sdk/src/JitsiMeetView+Private.h +++ b/ios/sdk/src/JitsiMeetView+Private.h @@ -18,6 +18,7 @@ @interface JitsiMeetView () ++ (NSDictionary *)conferenceURLFromUserActivity:(NSUserActivity *)userActivity; + (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope; @end diff --git a/ios/sdk/src/JitsiMeetView.h b/ios/sdk/src/JitsiMeetView.h index 26c409c5f..f0a0d69ad 100644 --- a/ios/sdk/src/JitsiMeetView.h +++ b/ios/sdk/src/JitsiMeetView.h @@ -22,6 +22,8 @@ @interface JitsiMeetView : UIView +@property (class, copy, nonatomic, nullable) NSString *conferenceActivityType; + @property (copy, nonatomic, nullable) NSURL *defaultURL; @property (nonatomic, nullable, weak) id delegate; diff --git a/ios/sdk/src/JitsiMeetView.m b/ios/sdk/src/JitsiMeetView.m index 776fafa41..d13f4e65c 100644 --- a/ios/sdk/src/JitsiMeetView.m +++ b/ios/sdk/src/JitsiMeetView.m @@ -88,6 +88,8 @@ void registerFatalErrorHandler() { @dynamic pictureInPictureEnabled; +static NSString *_conferenceActivityType; + static RCTBridgeWrapper *bridgeWrapper; /** @@ -277,6 +279,16 @@ static NSMapTable *views; [self loadURLObject:urlString ? @{ @"url": urlString } : nil]; } +#pragma conferenceActivityType getter / setter + ++ (NSString *)conferenceActivityType { + return _conferenceActivityType; +} + ++ (void) setConferenceActivityType:(NSString *)conferenceActivityType { + _conferenceActivityType = conferenceActivityType; +} + #pragma pictureInPictureEnabled getter / setter - (void) setPictureInPictureEnabled:(BOOL)pictureInPictureEnabled { @@ -358,6 +370,13 @@ static NSMapTable *views; @"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;