From ddab27e292bacbd131d55bc7bf01a7e0ac8ba893 Mon Sep 17 00:00:00 2001 From: Alex Bumbu Date: Wed, 29 Jun 2022 18:57:51 +0300 Subject: [PATCH] fix(ios, pip): update view hierarchy to present the rn view with view controller --- ios/sdk/sdk.xcodeproj/project.pbxproj | 26 +++- ios/sdk/src/ExternalAPI.h | 2 + ios/sdk/src/ExternalAPI.m | 48 +------ ios/sdk/src/JitsiMeet.m | 14 +- ios/sdk/src/JitsiMeetRenderingView.h | 30 ++++ ios/sdk/src/JitsiMeetRenderingView.m | 103 ++++++++++++++ ios/sdk/src/JitsiMeetView+Private.h | 16 ++- ios/sdk/src/JitsiMeetView+Private.m | 25 ++++ ios/sdk/src/JitsiMeetView.m | 190 +++++++++----------------- ios/sdk/src/JitsiMeetViewController.h | 38 ++++++ ios/sdk/src/JitsiMeetViewController.m | 127 +++++++++++++++++ 11 files changed, 436 insertions(+), 183 deletions(-) create mode 100644 ios/sdk/src/JitsiMeetRenderingView.h create mode 100644 ios/sdk/src/JitsiMeetRenderingView.m create mode 100644 ios/sdk/src/JitsiMeetView+Private.m create mode 100644 ios/sdk/src/JitsiMeetViewController.h create mode 100644 ios/sdk/src/JitsiMeetViewController.m diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj index d493653309..8711641f77 100644 --- a/ios/sdk/sdk.xcodeproj/project.pbxproj +++ b/ios/sdk/sdk.xcodeproj/project.pbxproj @@ -24,8 +24,14 @@ 0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */ = {isa = PBXBuildFile; fileRef = 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4E51B76425E5345E0038575A /* ScheenshareEventEmiter.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E51B76225E5345E0038575A /* ScheenshareEventEmiter.h */; }; 4E51B76525E5345E0038575A /* ScheenshareEventEmiter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E51B76325E5345E0038575A /* ScheenshareEventEmiter.m */; }; + 4EBA6E61286072E300B31882 /* JitsiMeetViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EBA6E5F286072E300B31882 /* JitsiMeetViewController.h */; }; + 4EBA6E62286072E300B31882 /* JitsiMeetViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA6E60286072E300B31882 /* JitsiMeetViewController.m */; }; + 4EBA6E652860B1E800B31882 /* JitsiMeetRenderingView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EBA6E632860B1E800B31882 /* JitsiMeetRenderingView.h */; }; + 4EBA6E662860B1E800B31882 /* JitsiMeetRenderingView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EBA6E642860B1E800B31882 /* JitsiMeetRenderingView.m */; }; 4ED4FFF32721B9B90074E620 /* JitsiAudioSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 4ED4FFF12721B9B90074E620 /* JitsiAudioSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4ED4FFF42721B9B90074E620 /* JitsiAudioSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 4ED4FFF22721B9B90074E620 /* JitsiAudioSession.m */; }; + 4EEC9630286C73A2008705FA /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 4EEC962E286C73A2008705FA /* JitsiMeetView+Private.h */; }; + 4EEC9631286C73A2008705FA /* JitsiMeetView+Private.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC962F286C73A2008705FA /* JitsiMeetView+Private.m */; }; 6F08DF7D4458EE3CF3F36F6D /* libPods-JitsiMeetSDK.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E4376CA6886DE68FD7A4294B /* libPods-JitsiMeetSDK.a */; }; A4A934E9212F3ADB001E9388 /* Dropbox.m in Sources */ = {isa = PBXBuildFile; fileRef = A4A934E8212F3ADB001E9388 /* Dropbox.m */; }; C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; }; @@ -79,9 +85,15 @@ 0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4E51B76225E5345E0038575A /* ScheenshareEventEmiter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ScheenshareEventEmiter.h; sourceTree = ""; }; 4E51B76325E5345E0038575A /* ScheenshareEventEmiter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScheenshareEventEmiter.m; sourceTree = ""; }; + 4EBA6E5F286072E300B31882 /* JitsiMeetViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetViewController.h; sourceTree = ""; }; + 4EBA6E60286072E300B31882 /* JitsiMeetViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetViewController.m; sourceTree = ""; }; + 4EBA6E632860B1E800B31882 /* JitsiMeetRenderingView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetRenderingView.h; sourceTree = ""; }; + 4EBA6E642860B1E800B31882 /* JitsiMeetRenderingView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetRenderingView.m; sourceTree = ""; }; 4ED4FFF12721B9B90074E620 /* JitsiAudioSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiAudioSession.h; sourceTree = ""; }; 4ED4FFF22721B9B90074E620 /* JitsiAudioSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiAudioSession.m; sourceTree = ""; }; 4ED4FFF52721BAE10074E620 /* JitsiAudioSession+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiAudioSession+Private.h"; sourceTree = ""; }; + 4EEC962E286C73A2008705FA /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = ""; }; + 4EEC962F286C73A2008705FA /* JitsiMeetView+Private.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "JitsiMeetView+Private.m"; sourceTree = ""; }; 891FE43DAD30BC8976683100 /* Pods-JitsiMeetSDK.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeetSDK.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeetSDK/Pods-JitsiMeetSDK.release.xcconfig"; sourceTree = ""; }; 98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = ""; }; 9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = ""; }; @@ -94,7 +106,6 @@ C69EFA0B209A0F660027712B /* JMCallKitListener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JMCallKitListener.swift; sourceTree = ""; }; C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = ""; }; C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = ""; }; - C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = ""; }; C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExternalAPI.h; sourceTree = ""; }; C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InfoPlistUtil.h; sourceTree = ""; }; C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InfoPlistUtil.m; sourceTree = ""; }; @@ -194,12 +205,17 @@ DE65AACB2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h */, DE81A2DD2317ED5400AE1940 /* JitsiMeetBaseLogHandler.m */, 0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */, + 4EEC962E286C73A2008705FA /* JitsiMeetView+Private.h */, + 4EEC962F286C73A2008705FA /* JitsiMeetView+Private.m */, 0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */, + 4EBA6E632860B1E800B31882 /* JitsiMeetRenderingView.h */, + 4EBA6E642860B1E800B31882 /* JitsiMeetRenderingView.m */, + 4EBA6E5F286072E300B31882 /* JitsiMeetViewController.h */, + 4EBA6E60286072E300B31882 /* JitsiMeetViewController.m */, DE81A2D72316AC7600AE1940 /* LogBridge.m */, DE65AAC92317FFCD00290BEC /* LogUtils.h */, DEAFA777229EAD3B0033A7FA /* RNRootView.h */, DEAFA778229EAD520033A7FA /* RNRootView.m */, - C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */, 0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */, DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */, C6A3426B204F127900E062DD /* picture-in-picture */, @@ -284,11 +300,14 @@ DEA9F284258A5D9900D4CD74 /* JitsiMeetSDK.h in Headers */, 4E51B76425E5345E0038575A /* ScheenshareEventEmiter.h in Headers */, DE65AACC2318028300290BEC /* JitsiMeetBaseLogHandler+Private.h in Headers */, + 4EBA6E652860B1E800B31882 /* JitsiMeetRenderingView.h in Headers */, 0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */, 4ED4FFF32721B9B90074E620 /* JitsiAudioSession.h in Headers */, + 4EEC9630286C73A2008705FA /* JitsiMeetView+Private.h in Headers */, 0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */, DE81A2D42316AC4D00AE1940 /* JitsiMeetLogger.h in Headers */, DE65AACA2317FFCD00290BEC /* LogUtils.h in Headers */, + 4EBA6E61286072E300B31882 /* JitsiMeetViewController.h in Headers */, DEAD3226220C497000E93636 /* JitsiMeetConferenceOptions.h in Headers */, C81E9AB925AC5AD800B134D9 /* ExternalAPI.h in Headers */, C8AFD27F2462C613000293D2 /* InfoPlistUtil.h in Headers */, @@ -449,6 +468,7 @@ files = ( 0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */, DE81A2DF2317ED5400AE1940 /* JitsiMeetBaseLogHandler.m in Sources */, + 4EBA6E662860B1E800B31882 /* JitsiMeetRenderingView.m in Sources */, 4ED4FFF42721B9B90074E620 /* JitsiAudioSession.m in Sources */, 0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */, DE81A2D92316AC7600AE1940 /* LogBridge.m in Sources */, @@ -465,9 +485,11 @@ 0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */, C69EFA0C209A0F660027712B /* JMCallKitEmitter.swift in Sources */, DEFE535621FB2E8300011A3A /* ReactUtils.m in Sources */, + 4EEC9631286C73A2008705FA /* JitsiMeetView+Private.m in Sources */, C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */, 4E51B76525E5345E0038575A /* ScheenshareEventEmiter.m in Sources */, A4A934E9212F3ADB001E9388 /* Dropbox.m in Sources */, + 4EBA6E62286072E300B31882 /* JitsiMeetViewController.m in Sources */, C69EFA0D209A0F660027712B /* JMCallKitProxy.swift in Sources */, DE81A2D52316AC4D00AE1940 /* JitsiMeetLogger.m in Sources */, C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */, diff --git a/ios/sdk/src/ExternalAPI.h b/ios/sdk/src/ExternalAPI.h index feb49f3979..b27d51dc81 100644 --- a/ios/sdk/src/ExternalAPI.h +++ b/ios/sdk/src/ExternalAPI.h @@ -16,6 +16,8 @@ #import #import +static NSString * const sendEventNotificationName = @"org.jitsi.meet.SendEvent"; + @interface ExternalAPI : RCTEventEmitter - (void)sendHangUp; diff --git a/ios/sdk/src/ExternalAPI.m b/ios/sdk/src/ExternalAPI.m index aa287af4e5..4b1a9d91a6 100644 --- a/ios/sdk/src/ExternalAPI.m +++ b/ios/sdk/src/ExternalAPI.m @@ -15,7 +15,6 @@ */ #import "ExternalAPI.h" -#import "JitsiMeetView+Private.h" // Events static NSString * const hangUpAction = @"org.jitsi.meet.HANG_UP"; @@ -91,31 +90,14 @@ RCT_EXPORT_MODULE(); RCT_EXPORT_METHOD(sendEvent:(NSString *)name data:(NSDictionary *)data scope:(NSString *)scope) { - // The JavaScript App needs to provide uniquely identifying information to - // the native ExternalAPI module so that the latter may match the former - // to the native JitsiMeetView which hosts it. - JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:scope]; - - if (!view) { - return; - } - - id delegate = view.delegate; - - if (!delegate) { - return; - } - if ([name isEqual: @"PARTICIPANTS_INFO_RETRIEVED"]) { [self onParticipantsInfoRetrieved: data]; return; } - - SEL sel = NSSelectorFromString([self methodNameFromEventName:name]); - - if (sel && [delegate respondsToSelector:sel]) { - [delegate performSelector:sel withObject:data]; - } + + [[NSNotificationCenter defaultCenter] postNotificationName:sendEventNotificationName + object:nil + userInfo:@{@"name": name, @"data": data}]; } - (void) onParticipantsInfoRetrieved:(NSDictionary *)data { @@ -127,28 +109,6 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name [participantInfoCompletionHandlers removeObjectForKey:completionHandlerId]; } -/** - * Converts a specific event name i.e. redux action type description to a - * method name. - * - * @param eventName The event name to convert to a method name. - * @return A method name constructed from the specified `eventName`. - */ -- (NSString *)methodNameFromEventName:(NSString *)eventName { - NSMutableString *methodName - = [NSMutableString stringWithCapacity:eventName.length]; - - for (NSString *c in [eventName componentsSeparatedByString:@"_"]) { - if (c.length) { - [methodName appendString: - methodName.length ? c.capitalizedString : c.lowercaseString]; - } - } - [methodName appendString:@":"]; - - return methodName; -} - - (void)sendHangUp { [self sendEventWithName:hangUpAction body:nil]; } diff --git a/ios/sdk/src/JitsiMeet.m b/ios/sdk/src/JitsiMeet.m index 06c46e82f4..6af035478d 100644 --- a/ios/sdk/src/JitsiMeet.m +++ b/ios/sdk/src/JitsiMeet.m @@ -15,6 +15,8 @@ */ #import +#import +#import #import "Dropbox.h" #import "JitsiMeet+Private.h" @@ -25,9 +27,6 @@ #import "RNSplashScreen.h" #import "ScheenshareEventEmiter.h" -#import -#import - @implementation JitsiMeet { RCTBridgeWrapper *_bridgeWrapper; NSDictionary *_launchOptions; @@ -87,8 +86,12 @@ restorationHandler:(void (^)(NSArray> *))restorationHandler { JitsiMeetConferenceOptions *options = [self optionsFromUserActivity:userActivity]; + if (options) { + [JitsiMeetView updateProps:[options asProps]]; + return true; + } - return options && [JitsiMeetView setPropsInViews:[options asProps]]; + return false; } - (BOOL)application:(UIApplication *)app @@ -112,8 +115,9 @@ JitsiMeetConferenceOptions *conferenceOptions = [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) { builder.room = [url absoluteString]; }]; + [JitsiMeetView updateProps:[conferenceOptions asProps]]; - return [JitsiMeetView setPropsInViews:[conferenceOptions asProps]]; + return true; } #pragma mark - Utility methods diff --git a/ios/sdk/src/JitsiMeetRenderingView.h b/ios/sdk/src/JitsiMeetRenderingView.h new file mode 100644 index 0000000000..2c918aa685 --- /dev/null +++ b/ios/sdk/src/JitsiMeetRenderingView.h @@ -0,0 +1,30 @@ +/* + * Copyright @ 2022-present 8x8, Inc. + * + * 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 +#import "JitsiMeetViewDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface JitsiMeetRenderingView : UIView + +@property (nonatomic, assign) BOOL isPiPEnabled; + +- (void)setProps:(NSDictionary *_Nonnull)newProps; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/sdk/src/JitsiMeetRenderingView.m b/ios/sdk/src/JitsiMeetRenderingView.m new file mode 100644 index 0000000000..25b7efa151 --- /dev/null +++ b/ios/sdk/src/JitsiMeetRenderingView.m @@ -0,0 +1,103 @@ +/* + * Copyright @ 2022-present 8x8, Inc. + * + * 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. + */ +#include + +#import "JitsiMeetRenderingView.h" +#import "ReactUtils.h" +#import "RNRootView.h" +#import "JitsiMeet+Private.h" + +/** + * Backwards compatibility: turn the boolean prop into a feature flag. + */ +static NSString *const PiPEnabledFeatureFlag = @"pip.enabled"; + +@interface JitsiMeetRenderingView () + +/** + * The unique identifier of this `JitsiMeetView` within the process for the + * purposes of `ExternalAPI`. The name scope was inspired by postis which we + * use on Web for the similar purposes of the iframe-based external API. + */ +@property (nonatomic, strong) NSString *externalAPIScope; + +@end + +@implementation JitsiMeetRenderingView { + /** + * React Native view where the entire content will be rendered. + */ + RNRootView *rootView; +} + +- (instancetype)init { + self = [super init]; + if (self) { + // Hook this JitsiMeetView into ExternalAPI. + self.externalAPIScope = [NSUUID UUID].UUIDString; + } + + return self; +} + +/** + * Passes the given props to the React Native application. The props which we pass + * are a combination of 3 different sources: + * + * - JitsiMeet.defaultConferenceOptions + * - This function's parameters + * - Some extras which are added by this function + */ +- (void)setProps:(NSDictionary *_Nonnull)newProps { + NSMutableDictionary *props = mergeProps([[JitsiMeet sharedInstance] getDefaultProps], newProps); + + // Set the PiP flag if it wasn't manually set. + NSMutableDictionary *featureFlags = props[@"flags"]; + // TODO: temporary implementation + if (featureFlags[PiPEnabledFeatureFlag] == nil) { + featureFlags[PiPEnabledFeatureFlag] = @(self.isPiPEnabled); + } + + props[@"externalAPIScope"] = self.externalAPIScope; + + // This method is supposed to be imperative i.e. a second + // invocation with one and the same URL is expected to join the respective + // conference again if the first invocation was followed by leaving the + // conference. However, React and, respectively, + // appProperties/initialProperties are declarative expressions i.e. one and + // the same URL will not trigger an automatic re-render in the JavaScript + // source code. The workaround implemented below introduces imperativeness + // in React Component props by defining a unique value per invocation. + props[@"timestamp"] = @(mach_absolute_time()); + + if (rootView) { + // Update props with the new URL. + rootView.appProperties = props; + } else { + RCTBridge *bridge = [[JitsiMeet sharedInstance] getReactBridge]; + rootView = [[RNRootView alloc] initWithBridge:bridge + moduleName:@"App" + initialProperties:props]; + rootView.backgroundColor = self.backgroundColor; + + // Add rootView as a subview which completely covers this one. + [rootView setFrame:[self bounds]]; + rootView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:rootView]; + } +} + +@end diff --git a/ios/sdk/src/JitsiMeetView+Private.h b/ios/sdk/src/JitsiMeetView+Private.h index d17ac0eb76..83e2b8d3f4 100644 --- a/ios/sdk/src/JitsiMeetView+Private.h +++ b/ios/sdk/src/JitsiMeetView+Private.h @@ -1,6 +1,5 @@ /* - * Copyright @ 2018-present 8x8, Inc. - * Copyright @ 2017-2018 Atlassian Pty Ltd + * Copyright @ 2022-present 8x8, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +14,16 @@ * limitations under the License. */ -#import "JitsiMeetView.h" +#import -@interface JitsiMeetView () +NS_ASSUME_NONNULL_BEGIN -+ (instancetype _Nullable)viewForExternalAPIScope:(NSString *_Nonnull)externalAPIScope; -+ (BOOL)setPropsInViews:(NSDictionary *_Nonnull)newProps; +static NSString * const updateViewPropsNotificationName = @"org.jitsi.meet.UpdateViewProps"; + +@interface JitsiMeetView (Private) + ++ (void)updateProps:(NSDictionary *_Nonnull)newProps; @end + +NS_ASSUME_NONNULL_END diff --git a/ios/sdk/src/JitsiMeetView+Private.m b/ios/sdk/src/JitsiMeetView+Private.m new file mode 100644 index 0000000000..bec01f416d --- /dev/null +++ b/ios/sdk/src/JitsiMeetView+Private.m @@ -0,0 +1,25 @@ +/* + * Copyright @ 2022-present 8x8, Inc. + * + * 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 "JitsiMeetView+Private.h" + +@implementation JitsiMeetView (Private) + ++ (void)updateProps:(NSDictionary *_Nonnull)newProps { + [[NSNotificationCenter defaultCenter] postNotificationName:updateViewPropsNotificationName object:nil userInfo:@{@"props": newProps}]; +} + +@end diff --git a/ios/sdk/src/JitsiMeetView.m b/ios/sdk/src/JitsiMeetView.m index 5eb3aafeec..8992b9602b 100644 --- a/ios/sdk/src/JitsiMeetView.m +++ b/ios/sdk/src/JitsiMeetView.m @@ -20,43 +20,22 @@ #import "ExternalAPI.h" #import "JitsiMeet+Private.h" #import "JitsiMeetConferenceOptions+Private.h" -#import "JitsiMeetView+Private.h" +#import "JitsiMeetView.h" +#import "JitsiMeetViewController.h" #import "ReactUtils.h" #import "RNRootView.h" +@interface JitsiMeetView () -/** - * Backwards compatibility: turn the boolean prop into a feature flag. - */ -static NSString *const PiPEnabledFeatureFlag = @"pip.enabled"; +@property (nonatomic, strong) JitsiMeetViewController *jitsiMeetViewController; +@property (nonatomic, strong) UINavigationController *navController; +@property (nonatomic, readonly) BOOL isPiPEnabled; +@end -@implementation JitsiMeetView { - /** - * The unique identifier of this `JitsiMeetView` within the process for the - * purposes of `ExternalAPI`. The name scope was inspired by postis which we - * use on Web for the similar purposes of the iframe-based external API. - */ - NSString *externalAPIScope; +@implementation JitsiMeetView - /** - * React Native view where the entire content will be rendered. - */ - RNRootView *rootView; -} - -/** - * The `JitsiMeetView`s associated with their `ExternalAPI` scopes (i.e. unique - * identifiers within the process). - */ -static NSMapTable *views; -/** - * This gets called automagically when the program starts. - */ -__attribute__((constructor)) -static void initializeViewsMap() { - views = [NSMapTable strongToWeakObjectsMapTable]; -} +@dynamic isPiPEnabled; #pragma mark Initializers @@ -87,6 +66,10 @@ static void initializeViewsMap() { return self; } +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + /** * Internal initialization: * @@ -94,70 +77,57 @@ static void initializeViewsMap() { * - initializes the external API scope */ - (void)initWithXXX { - // Hook this JitsiMeetView into ExternalAPI. - externalAPIScope = [NSUUID UUID].UUIDString; - [views setObject:self forKey:externalAPIScope]; - - // Set a background color which is in accord with the JavaScript and Android - // parts of the application and causes less perceived visual flicker than - // the default background color. - self.backgroundColor - = [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1]; + self.jitsiMeetViewController = [[JitsiMeetViewController alloc] init]; + self.jitsiMeetViewController.view.frame = [self bounds]; + [self addSubview:self.jitsiMeetViewController.view]; + + [self registerObservers]; } #pragma mark API - (void)join:(JitsiMeetConferenceOptions *)options { - [self setProps:options == nil ? @{} : [options asProps]]; + [self.jitsiMeetViewController join:options withPiP:self.isPiPEnabled]; } - (void)leave { - [self setProps:@{}]; + [self.jitsiMeetViewController leave]; } - (void)hangUp { - ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; - [externalAPI sendHangUp]; + [self.jitsiMeetViewController hangUp]; } - (void)setAudioMuted:(BOOL)muted { - ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; - [externalAPI sendSetAudioMuted:muted]; + [self.jitsiMeetViewController setAudioMuted:muted]; } - (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to { - ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; - [externalAPI sendEndpointTextMessage:message :to]; + [self.jitsiMeetViewController sendEndpointTextMessage:message :to]; } - (void)toggleScreenShare:(BOOL)enabled { - ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; - [externalAPI toggleScreenShare:enabled]; + [self.jitsiMeetViewController toggleScreenShare:enabled]; } - (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler { - ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; - [externalAPI retrieveParticipantsInfo:completionHandler]; + [self.jitsiMeetViewController retrieveParticipantsInfo:completionHandler]; } - (void)openChat:(NSString*)to { - ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; - [externalAPI openChat:to]; + [self.jitsiMeetViewController openChat:to]; } - (void)closeChat { - ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; - [externalAPI closeChat]; + [self.jitsiMeetViewController closeChat]; } - (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to { - ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; - [externalAPI sendChatMessage:message :to]; + [self.jitsiMeetViewController sendChatMessage:message :to]; } - (void)setVideoMuted:(BOOL)muted { - ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; - [externalAPI sendSetVideoMuted:muted]; + [self.jitsiMeetViewController setVideoMuted:muted]; } - (void)setClosedCaptionsEnabled:(BOOL)enabled { @@ -165,79 +135,47 @@ static void initializeViewsMap() { [externalAPI sendSetClosedCaptionsEnabled:enabled]; } -#pragma mark Private methods +#pragma mark Private + +- (BOOL)isPiPEnabled { + return self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)]; +} + +- (void)registerObservers { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleSendEventNotification:) name:sendEventNotificationName object:nil]; +} + +- (void)handleSendEventNotification:(NSNotification *)notification { + NSString *eventName = notification.userInfo[@"name"]; + NSString *eventData = notification.userInfo[@"data"]; + + SEL sel = NSSelectorFromString([self methodNameFromEventName:eventName]); + + if (sel && [self.delegate respondsToSelector:sel]) { + [self.delegate performSelector:sel withObject:eventData]; + } +} /** - * Passes the given props to the React Native application. The props which we pass - * are a combination of 3 different sources: + * Converts a specific event name i.e. redux action type description to a + * method name. * - * - JitsiMeet.defaultConferenceOptions - * - This function's parameters - * - Some extras which are added by this function + * @param eventName The event name to convert to a method name. + * @return A method name constructed from the specified `eventName`. */ -- (void)setProps:(NSDictionary *_Nonnull)newProps { - NSMutableDictionary *props = mergeProps([[JitsiMeet sharedInstance] getDefaultProps], newProps); +- (NSString *)methodNameFromEventName:(NSString *)eventName { + NSMutableString *methodName + = [NSMutableString stringWithCapacity:eventName.length]; - // Set the PiP flag if it wasn't manually set. - NSMutableDictionary *featureFlags = props[@"flags"]; - if (featureFlags[PiPEnabledFeatureFlag] == nil) { - featureFlags[PiPEnabledFeatureFlag] - = [NSNumber numberWithBool: - self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)]]; - } + for (NSString *c in [eventName componentsSeparatedByString:@"_"]) { + if (c.length) { + [methodName appendString: + methodName.length ? c.capitalizedString : c.lowercaseString]; + } + } + [methodName appendString:@":"]; - props[@"externalAPIScope"] = externalAPIScope; - - // This method is supposed to be imperative i.e. a second - // invocation with one and the same URL is expected to join the respective - // conference again if the first invocation was followed by leaving the - // conference. However, React and, respectively, - // appProperties/initialProperties are declarative expressions i.e. one and - // the same URL will not trigger an automatic re-render in the JavaScript - // source code. The workaround implemented below introduces imperativeness - // in React Component props by defining a unique value per invocation. - props[@"timestamp"] = @(mach_absolute_time()); - - if (rootView) { - // Update props with the new URL. - rootView.appProperties = props; - } else { - RCTBridge *bridge = [[JitsiMeet sharedInstance] getReactBridge]; - rootView - = [[RNRootView alloc] initWithBridge:bridge - moduleName:@"App" - initialProperties:props]; - rootView.backgroundColor = self.backgroundColor; - - // Add rootView as a subview which completely covers this one. - [rootView setFrame:[self bounds]]; - rootView.autoresizingMask - = UIViewAutoresizingFlexibleWidth - | UIViewAutoresizingFlexibleHeight; - [self addSubview:rootView]; - } -} - -+ (BOOL)setPropsInViews:(NSDictionary *_Nonnull)newProps { - BOOL handled = NO; - - if (views) { - for (NSString *externalAPIScope in views) { - JitsiMeetView *view - = [self viewForExternalAPIScope:externalAPIScope]; - - if (view) { - [view setProps:newProps]; - handled = YES; - } - } - } - - return handled; -} - -+ (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope { - return [views objectForKey:externalAPIScope]; + return methodName; } @end diff --git a/ios/sdk/src/JitsiMeetViewController.h b/ios/sdk/src/JitsiMeetViewController.h new file mode 100644 index 0000000000..cc469ab314 --- /dev/null +++ b/ios/sdk/src/JitsiMeetViewController.h @@ -0,0 +1,38 @@ +/* + * Copyright @ 2022-present 8x8, Inc. + * + * 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 +#import "JitsiMeetConferenceOptions.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface JitsiMeetViewController : UIViewController + +- (void)join:(JitsiMeetConferenceOptions *)options withPiP:(BOOL)enablePiP; +- (void)leave; +- (void)hangUp; +- (void)setAudioMuted:(BOOL)muted; +- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to; +- (void)toggleScreenShare:(BOOL)enabled; +- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler; +- (void)openChat:(NSString*)to; +- (void)closeChat; +- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to; +- (void)setVideoMuted:(BOOL)muted; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/sdk/src/JitsiMeetViewController.m b/ios/sdk/src/JitsiMeetViewController.m new file mode 100644 index 0000000000..78df28522b --- /dev/null +++ b/ios/sdk/src/JitsiMeetViewController.m @@ -0,0 +1,127 @@ +/* + * Copyright @ 2022-present 8x8, Inc. + * + * 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 "JitsiMeetViewController.h" +#import "JitsiMeet+Private.h" +#import "JitsiMeetConferenceOptions+Private.h" +#import "JitsiMeetRenderingView.h" +#import "JitsiMeetView+Private.h" + +@interface JitsiMeetViewController () + +@property (strong, nonatomic) JitsiMeetRenderingView *view; + +@end + +@implementation JitsiMeetViewController + +@dynamic view; + +- (instancetype)init { + self = [super init]; + if (self) { + [self registerObservers]; + } + + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)loadView { + [super loadView]; + + self.view = [[JitsiMeetRenderingView alloc] init]; + self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + // Set a background color which is in accord with the JavaScript and Android + // parts of the application and causes less perceived visual flicker than + // the default background color. + self.view.backgroundColor = [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1]; +} + +- (void)join:(JitsiMeetConferenceOptions *)options withPiP:(BOOL)enablePiP { + self.view.isPiPEnabled = enablePiP; + [self.view setProps:options == nil ? @{} : [options asProps]]; +} + +- (void)leave { + [self.view setProps:@{}]; +} + +- (void)hangUp { + ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; + [externalAPI sendHangUp]; +} + +- (void)setAudioMuted:(BOOL)muted { + ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; + [externalAPI sendSetAudioMuted:muted]; +} + +- (void)sendEndpointTextMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to { + ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; + [externalAPI sendEndpointTextMessage:message :to]; +} + +- (void)toggleScreenShare:(BOOL)enabled { + ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; + [externalAPI toggleScreenShare:enabled]; +} + +- (void)retrieveParticipantsInfo:(void (^ _Nonnull)(NSArray * _Nullable))completionHandler { + ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; + [externalAPI retrieveParticipantsInfo:completionHandler]; +} + +- (void)openChat:(NSString*)to { + ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; + [externalAPI openChat:to]; +} + +- (void)closeChat { + ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; + [externalAPI closeChat]; +} + +- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to { + ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; + [externalAPI sendChatMessage:message :to]; +} + +- (void)setVideoMuted:(BOOL)muted { + ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI]; + [externalAPI sendSetVideoMuted:muted]; +} + +#pragma mark Private + +- (void)registerObservers { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleUpdateViewPropsNotification:) name:updateViewPropsNotificationName object:nil]; +} + +- (void)handleUpdateViewPropsNotification:(NSNotification *)notification { + NSDictionary *props = [notification.userInfo objectForKey:@"props"]; + [self.view setProps:props]; +} + +@end