浏览代码

[iOS] Allow multiple JitsiMeetViews

j8
Lyubo Marinov 8 年前
父节点
当前提交
10e5e0fdf5

+ 1
- 1
android/sdk/src/main/java/org/jitsi/meet/sdk/JitsiMeetActivity.java 查看文件

19
 import android.content.Intent;
19
 import android.content.Intent;
20
 import android.net.Uri;
20
 import android.net.Uri;
21
 import android.os.Build;
21
 import android.os.Build;
22
+import android.os.Bundle;
22
 import android.provider.Settings;
23
 import android.provider.Settings;
23
 import android.support.annotation.Nullable;
24
 import android.support.annotation.Nullable;
24
 import android.support.v7.app.AppCompatActivity;
25
 import android.support.v7.app.AppCompatActivity;
25
-import android.os.Bundle;
26
 
26
 
27
 import java.net.URL;
27
 import java.net.URL;
28
 
28
 

+ 20
- 5
ios/sdk/src/ExternalAPI.m 查看文件

16
 
16
 
17
 #import "RCTBridgeModule.h"
17
 #import "RCTBridgeModule.h"
18
 
18
 
19
-#import "JitsiMeetView.h"
19
+#import "JitsiMeetView+Private.h"
20
 
20
 
21
 @interface ExternalAPI : NSObject<RCTBridgeModule>
21
 @interface ExternalAPI : NSObject<RCTBridgeModule>
22
+
22
 @end
23
 @end
23
 
24
 
24
 @implementation ExternalAPI
25
 @implementation ExternalAPI
31
  * - name: name of the event.
32
  * - name: name of the event.
32
  * - data: dictionary (JSON object in JS) with data associated with the event.
33
  * - data: dictionary (JSON object in JS) with data associated with the event.
33
  */
34
  */
34
-RCT_EXPORT_METHOD(sendEvent:(NSString*)name data:(NSDictionary *) data) {
35
-    JitsiMeetView *view = [JitsiMeetView getInstance];
36
-    id delegate = view != nil ? view.delegate : nil;
35
+RCT_EXPORT_METHOD(sendEvent:(NSString *)name
36
+                       data:(NSDictionary *)data
37
+                      scope:(NSString *)scope) {
38
+    // The JavaScript App needs to provide uniquely identifying information to
39
+    // the native ExternalAPI module so that the latter may match the former
40
+    // to the native JitsiMeetView which hosts it.
41
+    JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:scope];
42
+
43
+    if (!view) {
44
+        return;
45
+    }
46
+
47
+    id delegate = view.delegate;
37
 
48
 
38
-    if (delegate == nil) {
49
+    if (!delegate) {
39
         return;
50
         return;
40
     }
51
     }
41
 
52
 
42
     if ([name isEqualToString:@"CONFERENCE_FAILED"]
53
     if ([name isEqualToString:@"CONFERENCE_FAILED"]
43
             && [delegate respondsToSelector:@selector(conferenceFailed:)]) {
54
             && [delegate respondsToSelector:@selector(conferenceFailed:)]) {
44
         [delegate conferenceFailed:data];
55
         [delegate conferenceFailed:data];
56
+
45
     } else if ([name isEqualToString:@"CONFERENCE_JOINED"]
57
     } else if ([name isEqualToString:@"CONFERENCE_JOINED"]
46
             && [delegate respondsToSelector:@selector(conferenceJoined:)]) {
58
             && [delegate respondsToSelector:@selector(conferenceJoined:)]) {
47
         [delegate conferenceJoined:data];
59
         [delegate conferenceJoined:data];
60
+
48
     } else if ([name isEqualToString:@"CONFERENCE_LEFT"]
61
     } else if ([name isEqualToString:@"CONFERENCE_LEFT"]
49
             && [delegate respondsToSelector:@selector(conferenceLeft:)]) {
62
             && [delegate respondsToSelector:@selector(conferenceLeft:)]) {
50
         [delegate conferenceLeft:data];
63
         [delegate conferenceLeft:data];
64
+
51
     } else if ([name isEqualToString:@"CONFERENCE_WILL_JOIN"]
65
     } else if ([name isEqualToString:@"CONFERENCE_WILL_JOIN"]
52
             && [delegate respondsToSelector:@selector(conferenceWillJoin:)]) {
66
             && [delegate respondsToSelector:@selector(conferenceWillJoin:)]) {
53
         [delegate conferenceWillJoin:data];
67
         [delegate conferenceWillJoin:data];
68
+
54
     } else if ([name isEqualToString:@"CONFERENCE_WILL_LEAVE"]
69
     } else if ([name isEqualToString:@"CONFERENCE_WILL_LEAVE"]
55
             && [delegate respondsToSelector:@selector(conferenceWillLeave:)]) {
70
             && [delegate respondsToSelector:@selector(conferenceWillLeave:)]) {
56
         [delegate conferenceWillLeave:data];
71
         [delegate conferenceWillLeave:data];

+ 23
- 0
ios/sdk/src/JitsiMeetView+Private.h 查看文件

1
+/*
2
+ * Copyright @ 2017-present Atlassian Pty Ltd
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *     http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+#import "JitsiMeetView.h"
18
+
19
+@interface JitsiMeetView ()
20
+
21
++ (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope;
22
+
23
+@end

+ 0
- 2
ios/sdk/src/JitsiMeetView.h 查看文件

34
   sourceApplication:(NSString *)sourceApplication
34
   sourceApplication:(NSString *)sourceApplication
35
          annotation:(id)annotation;
35
          annotation:(id)annotation;
36
 
36
 
37
-+ (instancetype)getInstance;
38
-
39
 - (void)loadURL:(nullable NSURL *)url;
37
 - (void)loadURL:(nullable NSURL *)url;
40
 
38
 
41
 @end
39
 @end

+ 90
- 84
ios/sdk/src/JitsiMeetView.m 查看文件

20
 #import <React/RCTLinkingManager.h>
20
 #import <React/RCTLinkingManager.h>
21
 #import <React/RCTRootView.h>
21
 #import <React/RCTRootView.h>
22
 
22
 
23
-#import "JitsiMeetView.h"
23
+#import "JitsiMeetView+Private.h"
24
 #import "RCTBridgeWrapper.h"
24
 #import "RCTBridgeWrapper.h"
25
 
25
 
26
 /**
26
 /**
46
     }
46
     }
47
 };
47
 };
48
 
48
 
49
+/**
50
+ * Helper function to dynamically load custom fonts. The UIAppFonts key in the
51
+ * plist file doesn't work for frameworks, so fonts have to be manually loaded.
52
+ */
53
+void loadCustomFonts(Class clazz) {
54
+    NSBundle *bundle = [NSBundle bundleForClass:clazz];
55
+    NSArray *fonts = [bundle objectForInfoDictionaryKey:@"JitsiMeetFonts"];
56
+
57
+    for (NSString *item in fonts) {
58
+        NSString *fontName = [item stringByDeletingPathExtension];
59
+        NSString *fontExt = [item pathExtension];
60
+        NSString *fontPath = [bundle pathForResource:fontName ofType:fontExt];
61
+        NSData *inData = [NSData dataWithContentsOfFile:fontPath];
62
+        CFErrorRef error;
63
+        CGDataProviderRef provider
64
+            = CGDataProviderCreateWithCFData((__bridge CFDataRef)inData);
65
+        CGFontRef font = CGFontCreateWithDataProvider(provider);
66
+
67
+        if (!CTFontManagerRegisterGraphicsFont(font, &error)) {
68
+            CFStringRef errorDescription = CFErrorCopyDescription(error);
69
+
70
+            NSLog(@"Failed to load font: %@", errorDescription);
71
+            CFRelease(errorDescription);
72
+        }
73
+        CFRelease(font);
74
+        CFRelease(provider);
75
+    }
76
+}
77
+
78
+/**
79
+ * Helper function to register a fatal error handler for React. Our handler
80
+ * won't kill the process, it will swallow JS errors and print stack traces
81
+ * instead.
82
+ */
83
+void registerFatalErrorHandler() {
84
+#if !DEBUG
85
+    // In the Release configuration, React Native will (intentionally) raise
86
+    // an unhandled NSException for an unhandled JavaScript error. This will
87
+    // effectively kill the application. In accord with the Web, do not kill
88
+    // the application.
89
+    if (!RCTGetFatalHandler()) {
90
+        RCTSetFatalHandler(_RCTFatal);
91
+    }
92
+#endif
93
+}
94
+
49
 @interface JitsiMeetView() {
95
 @interface JitsiMeetView() {
96
+    /**
97
+     * The unique identifier of this {@code JitsiMeetView} within the process
98
+     * for the purposes of {@link ExternalAPI}. The name scope was inspired by
99
+     * postis which we use on Web for the similar purposes of the iframe-based
100
+     * external API.
101
+     */
102
+    NSString *externalAPIScope;
103
+
50
     RCTRootView *rootView;
104
     RCTRootView *rootView;
51
 }
105
 }
52
 
106
 
55
 @implementation JitsiMeetView
109
 @implementation JitsiMeetView
56
 
110
 
57
 static RCTBridgeWrapper *bridgeWrapper;
111
 static RCTBridgeWrapper *bridgeWrapper;
58
-static JitsiMeetView *instance;
112
+
113
+/**
114
+ * The {@code JitsiMeetView}s associated with their {@code ExternalAPI} scopes
115
+ * (i.e. unique identifiers within the process).
116
+ */
117
+static NSMapTable<NSString *, JitsiMeetView *> *views;
59
 
118
 
60
 #pragma mark Linking delegate helpers
119
 #pragma mark Linking delegate helpers
61
 // https://facebook.github.io/react-native/docs/linking.html
120
 // https://facebook.github.io/react-native/docs/linking.html
79
                                annotation:annotation];
138
                                annotation:annotation];
80
 }
139
 }
81
 
140
 
82
-#pragma mark initializers
141
+#pragma mark Initializers
83
 
142
 
84
 - (instancetype)initWithCoder:(NSCoder *)coder {
143
 - (instancetype)initWithCoder:(NSCoder *)coder {
85
     self = [super initWithCoder:coder];
144
     self = [super initWithCoder:coder];
86
     if (self) {
145
     if (self) {
87
-        [self initialize];
146
+        [self initWithXXX];
88
     }
147
     }
89
 
148
 
90
     return self;
149
     return self;
93
 - (instancetype)initWithFrame:(CGRect)frame {
152
 - (instancetype)initWithFrame:(CGRect)frame {
94
     self = [super initWithFrame:frame];
153
     self = [super initWithFrame:frame];
95
     if (self) {
154
     if (self) {
96
-        [self initialize];
155
+        [self initWithXXX];
97
     }
156
     }
98
 
157
 
99
     return self;
158
     return self;
101
 
160
 
102
 #pragma mark API
161
 #pragma mark API
103
 
162
 
104
-/*
163
+/**
105
  * Loads the given URL and joins the specified conference. If the specified URL
164
  * Loads the given URL and joins the specified conference. If the specified URL
106
  * is null, the welcome page is shown.
165
  * is null, the welcome page is shown.
107
  */
166
  */
108
 - (void)loadURL:(NSURL *)url {
167
 - (void)loadURL:(NSURL *)url {
109
     NSMutableDictionary *props = [[NSMutableDictionary alloc] init];
168
     NSMutableDictionary *props = [[NSMutableDictionary alloc] init];
110
 
169
 
170
+    // externalAPIScope
171
+    [props setObject:externalAPIScope forKey:@"externalAPIScope"];
111
     // url
172
     // url
112
     if (url) {
173
     if (url) {
113
         [props setObject:url.absoluteString forKey:@"url"];
174
         [props setObject:url.absoluteString forKey:@"url"];
131
     }
192
     }
132
 }
193
 }
133
 
194
 
134
-#pragma mark private methods
195
+#pragma mark Private methods
135
 
196
 
136
-+ (instancetype)getInstance {
137
-    return instance;
197
++ (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope {
198
+    return [views objectForKey:externalAPIScope];
138
 }
199
 }
139
 
200
 
140
-/*
201
+/**
141
  * Internal initialization:
202
  * Internal initialization:
142
  *
203
  *
143
- *   - sets the backgroudn color
144
- *   - creates the React bridge
145
- *   - loads the necessary custom fonts
146
- *   - registers a custom fatal error error handler for React
204
+ * - sets the backgroudn color
205
+ * - creates the React bridge
206
+ * - loads the necessary custom fonts
207
+ * - registers a custom fatal error error handler for React
147
  */
208
  */
148
-- (void)initialize {
149
-    static dispatch_once_t onceToken;
150
-
151
-    /*
152
-     * TODO: Only allow a single instance for now. All React Native modules are
153
-     * kinda singletons so global state would be broken since we have a single
154
-     * bridge. Once we have that sorted out multiple instances of JitsiMeetView
155
-     * will be allowed.
156
-     */
157
-    if (instance != nil) {
158
-        @throw [NSException
159
-            exceptionWithName:@"RuntimeError"
160
-                       reason:@"Only a single instance is currently allowed"
161
-                     userInfo:nil];
162
-    }
163
-    instance = self;
209
+- (void)initWithXXX {
210
+    static dispatch_once_t dispatchOncePredicate;
164
 
211
 
165
-    dispatch_once(&onceToken, ^{
166
-        // Set a background color which is in accord with the JavaScript and
167
-        // Android parts of the application and causes less perceived visual
168
-        // flicker than the default background color.
169
-        self.backgroundColor
170
-            = [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1];
171
-
172
-        // Initialize the React bridge.
212
+    dispatch_once(&dispatchOncePredicate, ^{
213
+        // Initialize the static state of JitsiMeetView.
173
         bridgeWrapper = [[RCTBridgeWrapper alloc] init];
214
         bridgeWrapper = [[RCTBridgeWrapper alloc] init];
215
+        views = [NSMapTable strongToWeakObjectsMapTable];
174
 
216
 
175
         // Dynamically load custom bundled fonts.
217
         // Dynamically load custom bundled fonts.
176
-        [self loadCustomFonts];
218
+        loadCustomFonts(self.class);
177
 
219
 
178
         // Register a fatal error handler for React.
220
         // Register a fatal error handler for React.
179
-        [self registerFatalErrorHandler];
221
+        registerFatalErrorHandler();
180
     });
222
     });
181
-}
182
-
183
-/*
184
- * Helper function to dynamically load custom fonts. The UIAppFonts key in the
185
- * plist file doesn't work for frameworks, so fonts have to be manually loaded.
186
- */
187
-- (void)loadCustomFonts {
188
-    NSBundle *bundle = [NSBundle bundleForClass:self.class];
189
-    NSArray *fonts = [bundle objectForInfoDictionaryKey:@"JitsiMeetFonts"];
190
-
191
-    for (NSString *item in fonts) {
192
-        NSString *fontName = [item stringByDeletingPathExtension];
193
-        NSString *fontExt = [item pathExtension];
194
-        NSString *fontPath = [bundle pathForResource:fontName ofType:fontExt];
195
-        NSData *inData = [NSData dataWithContentsOfFile:fontPath];
196
-        CFErrorRef error;
197
-        CGDataProviderRef provider
198
-            = CGDataProviderCreateWithCFData((__bridge CFDataRef)inData);
199
-        CGFontRef font = CGFontCreateWithDataProvider(provider);
200
 
223
 
201
-        if (!CTFontManagerRegisterGraphicsFont(font, &error)) {
202
-            CFStringRef errorDescription = CFErrorCopyDescription(error);
203
-
204
-            NSLog(@"Failed to load font: %@", errorDescription);
205
-            CFRelease(errorDescription);
206
-        }
207
-        CFRelease(font);
208
-        CFRelease(provider);
209
-    }
210
-}
224
+    // Hook this JitsiMeetView into ExternalAPI.
225
+    externalAPIScope = [NSUUID UUID].UUIDString;
226
+    [views setObject:self forKey:externalAPIScope];
211
 
227
 
212
-/*
213
- * Helper function to register a fatal error handler for React. Our handler
214
- * won't kill the process, it will swallow JS errors and print stack traces
215
- * instead.
216
- */
217
-- (void)registerFatalErrorHandler {
218
-#if !DEBUG
219
-    // In the Release configuration, React Native will (intentionally) raise
220
-    // an unhandled NSException for an unhandled JavaScript error. This will
221
-    // effectively kill the application. In accord with the Web, do not kill
222
-    // the application.
223
-    if (!RCTGetFatalHandler()) {
224
-        RCTSetFatalHandler(_RCTFatal);
225
-    }
226
-#endif
228
+    // Set a background color which is in accord with the JavaScript and
229
+    // Android parts of the application and causes less perceived visual
230
+    // flicker than the default background color.
231
+    self.backgroundColor
232
+        = [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1];
227
 }
233
 }
228
 
234
 
229
 @end
235
 @end

+ 22
- 18
ios/sdk/src/JitsiMeetViewDelegate.h 查看文件

18
 
18
 
19
 @optional
19
 @optional
20
 
20
 
21
-/*
22
- * Called when a joining a conference was unsuccessful or when there was an error
23
- * while in a conference.
21
+/**
22
+ * Called when a joining a conference was unsuccessful or when there was an
23
+ * error while in a conference.
24
  *
24
  *
25
- * The `data` dictionary contains an "error" key describing the error and a "url"
26
- * key with the conference URL.
25
+ * The {@code data} dictionary contains an "error" key describing the error and
26
+ * a {@code url} key with the conference URL.
27
  */
27
  */
28
-- (void) conferenceFailed:(NSDictionary *) data;
28
+- (void) conferenceFailed:(NSDictionary *)data;
29
 
29
 
30
-/*
30
+/**
31
  * Called when a conference was joined.
31
  * Called when a conference was joined.
32
  *
32
  *
33
- * The `data` dictionary contains a "url" key with the conference URL.
33
+ * The {@code data} dictionary contains a {@code url} key with the conference
34
+ * URL.
34
  */
35
  */
35
-- (void) conferenceJoined:(NSDictionary *) data;
36
+- (void) conferenceJoined:(NSDictionary *)data;
36
 
37
 
37
-/*
38
+/**
38
  * Called when a conference was left.
39
  * Called when a conference was left.
39
  *
40
  *
40
- * The `data` dictionary contains a "url" key with the conference URL.
41
+ * The {@code data} dictionary contains a {@code url} key with the conference
42
+ * URL.
41
  */
43
  */
42
-- (void) conferenceLeft:(NSDictionary *) data;
44
+- (void) conferenceLeft:(NSDictionary *)data;
43
 
45
 
44
-/*
46
+/**
45
  * Called before a conference is joined.
47
  * Called before a conference is joined.
46
  *
48
  *
47
- * The `data` dictionary contains a "url" key with the conference URL.
49
+ * The {@code data} dictionary contains a {@code url} key with the conference
50
+ * URL.
48
  */
51
  */
49
-- (void) conferenceWillJoin:(NSDictionary *) data;
52
+- (void) conferenceWillJoin:(NSDictionary *)data;
50
 
53
 
51
-/*
54
+/**
52
  * Called before a conference is left.
55
  * Called before a conference is left.
53
  *
56
  *
54
- * The `data` dictionary contains a "url" key with the conference URL.
57
+ * The {@code data} dictionary contains a {@code url} key with the conference
58
+ * URL.
55
  */
59
  */
56
-- (void) conferenceWillLeave:(NSDictionary *) data;
60
+- (void) conferenceWillLeave:(NSDictionary *)data;
57
 
61
 
58
 @end
62
 @end

+ 3
- 3
ios/sdk/src/RCTBridgeWrapper.m 查看文件

16
 
16
 
17
 #include "RCTBridgeWrapper.h"
17
 #include "RCTBridgeWrapper.h"
18
 
18
 
19
-/*
19
+/**
20
  * Wrapper around RCTBridge which also implements the RCTBridgeDelegate methods,
20
  * Wrapper around RCTBridge which also implements the RCTBridgeDelegate methods,
21
  * allowing us to specify where the bundles are loaded from.
21
  * allowing us to specify where the bundles are loaded from.
22
  */
22
  */
56
 
56
 
57
 - (NSString *)guessPackagerHost {
57
 - (NSString *)guessPackagerHost {
58
     static NSString *ipGuess;
58
     static NSString *ipGuess;
59
-    static dispatch_once_t onceToken;
59
+    static dispatch_once_t dispatchOncePredicate;
60
 
60
 
61
-    dispatch_once(&onceToken, ^{
61
+    dispatch_once(&dispatchOncePredicate, ^{
62
         NSString *ipPath
62
         NSString *ipPath
63
             = [[NSBundle bundleForClass:self.class] pathForResource:@"ip"
63
             = [[NSBundle bundleForClass:self.class] pathForResource:@"ip"
64
                                                              ofType:@"txt"];
64
                                                              ofType:@"txt"];

+ 32
- 5
react/features/mobile/external-api/middleware.js 查看文件

2
 
2
 
3
 import { NativeModules } from 'react-native';
3
 import { NativeModules } from 'react-native';
4
 
4
 
5
+import { Platform } from '../../base/react';
5
 import {
6
 import {
6
     CONFERENCE_FAILED,
7
     CONFERENCE_FAILED,
7
     CONFERENCE_JOINED,
8
     CONFERENCE_JOINED,
41
             // of locationURL at the time of CONFERENCE_WILL_LEAVE and
42
             // of locationURL at the time of CONFERENCE_WILL_LEAVE and
42
             // CONFERENCE_LEFT will not be the value with which the
43
             // CONFERENCE_LEFT will not be the value with which the
43
             // JitsiConference instance being left.)
44
             // JitsiConference instance being left.)
44
-            const { locationURL }
45
-                = store.getState()['features/base/connection'];
45
+            const state = store.getState();
46
+            const { locationURL } = state['features/base/connection'];
46
 
47
 
47
             if (!locationURL) {
48
             if (!locationURL) {
48
                 // The (redux) action cannot be fully converted to an (external
49
                 // The (redux) action cannot be fully converted to an (external
63
             name = name.slice(7, -1);
64
             name = name.slice(7, -1);
64
         }
65
         }
65
 
66
 
66
-        _sendEvent(name, data);
67
+        // The polyfill es6-symbol that we use does not appear to comply with
68
+        // the Symbol standard and, merely, adds @@ at the beginning of the
69
+        // description.
70
+        if (name.startsWith('@@')) {
71
+            name = name.slice(2);
72
+        }
73
+
74
+        _sendEvent(store, name, data);
67
         break;
75
         break;
68
     }
76
     }
69
     }
77
     }
76
  * apps may listen to such events via the mechanisms provided by the (native)
84
  * apps may listen to such events via the mechanisms provided by the (native)
77
  * mobile Jitsi Meet SDK.
85
  * mobile Jitsi Meet SDK.
78
  *
86
  *
87
+ * @param {Object} store - The redux store associated with the need to send the
88
+ * specified event.
79
  * @param {string} name - The name of the event to send.
89
  * @param {string} name - The name of the event to send.
80
  * @param {Object} data - The details/specifics of the event to send determined
90
  * @param {Object} data - The details/specifics of the event to send determined
81
  * by/associated with the specified {@code name}.
91
  * by/associated with the specified {@code name}.
82
  * @private
92
  * @private
83
  * @returns {void}
93
  * @returns {void}
84
  */
94
  */
85
-function _sendEvent(name: string, data: Object) {
86
-    NativeModules.ExternalAPI.sendEvent(name, data);
95
+function _sendEvent(store: Object, name: string, data: Object) {
96
+    // The JavaScript App needs to provide uniquely identifying information
97
+    // to the native ExternalAPI module so that the latter may match the former
98
+    // to the native JitsiMeetView which hosts it.
99
+    const state = store.getState();
100
+    const { app } = state['features/app'];
101
+
102
+    if (app) {
103
+        const { externalAPIScope } = app.props;
104
+
105
+        // TODO Lift the restriction on the JitsiMeetView instance count on
106
+        // Android as well.
107
+        if (externalAPIScope) {
108
+            NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope);
109
+        } else if (Platform.OS === 'android') {
110
+            NativeModules.ExternalAPI.sendEvent(name, data);
111
+            console.warn(name);
112
+        }
113
+    }
87
 }
114
 }

正在加载...
取消
保存