Compare commits

...

24 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé dcedb6334e fix crash 2017-09-28 21:21:16 +02:00
paweldomas da5f1081f2 ongoing complication 2017-09-28 13:48:38 -05:00
paweldomas 2ee44578c8 fix(watchos.middleware): transient NULL conference URL
There was NULL conference URL emitted when selecting new conference from
the list while still in a conference.

(cherry picked from commit 4d6970c)
2017-09-28 13:48:38 -05:00
Saúl Ibarra Corretgé 53697f9d7e wire up mute button 2017-09-28 18:29:16 +02:00
Saúl Ibarra Corretgé 0ffeef3ef4 icons! 2017-09-28 16:58:31 +02:00
paweldomas 2cb2de917e fix(watchos.middleware): use mic muted from base/media
(cherry picked from commit 1b0ba54)
2017-09-28 09:30:50 -05:00
paweldomas f048a89a13 fix(watchos.middleware): do not send NULL URL on join from watch 2017-09-28 09:22:12 -05:00
Saúl Ibarra Corretgé c0e00dab4c use the main thread, luke 2017-09-28 14:12:03 +02:00
Saúl Ibarra Corretgé aef73cc85f show in-call controller when joining on the phone, dismiss on hangup 2017-09-28 13:43:07 +02:00
Saúl Ibarra Corretgé d183d41fdf join from the watch! 2017-09-28 13:04:56 +02:00
Saúl Ibarra Corretgé b4f236b433 fix modifying immutable state 2017-09-28 13:04:36 +02:00
Saúl Ibarra Corretgé dcb32d2792 populate meetings list with entries from context 2017-09-28 10:32:08 +02:00
paweldomas ea4cd420fa feat(watchos): emit app context
(cherry picked from commit 6ad851b)
2017-09-27 20:27:45 -05:00
Saúl Ibarra Corretgé 0ef4be6e9e checkpoint: tables + modal 2017-09-28 00:47:41 +02:00
paweldomas 72d1144f47 feat(recent): store URLs in window.localStorage
(cherry picked from commit 06c5aeb)
2017-09-27 15:49:43 -05:00
paweldomas 2d1f2ed8bd feat: add recent URL
Store up to 25 recent URLs with timestamps

(cherry picked from commit 29b99a4)
2017-09-27 15:49:33 -05:00
Saúl Ibarra Corretgé 94a26a33c6 WIP: buttons! 2017-09-27 22:42:26 +02:00
Saúl Ibarra Corretgé 93aa8c5883 WIP2 2017-09-27 16:51:05 +02:00
Saúl Ibarra Corretgé 367bc693f7 WIP 2017-09-27 15:05:59 +02:00
Saúl Ibarra Corretgé f2df6fb657 watchos: add watchOS app structure 2017-09-27 11:02:50 +02:00
Saúl Ibarra Corretgé c6c7f08fb2 [iOS] Add initial CallKit support
This commit adds initial support for CallKit on supported platforms: iOS >= 10.

Since the call flow in Jitsi Meet is basically making outgoing calls, only
outgoing call support is currently handled via CallKit.

Features:
 - "Green bar" when in a call.
 - Native CallKit view when tapping on the call label on the lock screen.
 - Support for audio muting from the native CallKit view.
 - Support for recent calls (audio-only calls logged as Audio calls, others show
   as Video calls).
 - Call display name is room name.
 - Graceful downgrade on systems without CallKit support.

Limitations:
 - Native CallKit view cannot be shown for audio-only calls (this is a CallKit
   limitaion).
 - The video button in the CallKit view will start a new video call to the same
   room, and terminate the previous one.
 - No support for call hold.
2017-09-26 23:39:30 -05:00
Saúl Ibarra Corretgé eee01c314c [RN] Don't hardcode app name when sharing a room 2017-09-26 23:39:30 -05:00
Saúl Ibarra Corretgé 836710924e [RN] Add AppInfo module
It provides access to the app's display name and version.
2017-09-26 23:39:30 -05:00
Saúl Ibarra Corretgé 205dc5f0c3 [RN] Simplify handling of universal / deeps links
Handle them directly on the application and call view.loadURL instead of having
a static method which needs to figure out what view to load the URL on.

On iOS, update the deep link method delegate since we don't support iOS 8.

In addition, remove linking handling from the JS side, so now changing props of
the root component is The One And Only Way to change the URL at runtime.
2017-09-26 23:39:30 -05:00
66 changed files with 2626 additions and 208 deletions

6
.babelrc Normal file
View File

@ -0,0 +1,6 @@
{
"presets": [
"react-native",
"react-native-stage-0"
]
}

View File

@ -208,14 +208,6 @@ Helper method which should be called from the activity's `onResume` method.
This is a static method.
#### onNewIntent(intent)
Helper method for integrating the *deep linking* functionality. If your app's
activity is launched in "singleTask" mode this method should be called from the
activity's `onNewIntent` method.
This is a static method.
#### JitsiMeetViewListener
`JitsiMeetViewListener` provides an interface apps can implement to listen to

View File

@ -0,0 +1,56 @@
package org.jitsi.meet.sdk;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import java.util.HashMap;
import java.util.Map;
class AppInfoModule extends ReactContextBaseJavaModule {
/**
* React Native module name.
*/
private static final String MODULE_NAME = "AppInfo";
public AppInfoModule(ReactApplicationContext reactContext) {
super(reactContext);
}
/**
* Gets a mapping with the constants this module is exporting.
*
* @return a {@link Map} mapping the constants to be exported with their
* values.
*/
@Override
public Map<String, Object> getConstants() {
Map<String, Object> constants = new HashMap<>();
Context context = getReactApplicationContext();
PackageManager pm = context.getPackageManager();
ApplicationInfo appInfo;
PackageInfo packageInfo;
try {
appInfo = pm.getApplicationInfo(context.getPackageName(), 0);
packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
constants.put("name", "");
constants.put("version", "");
return constants;
}
constants.put("name", pm.getApplicationLabel(appInfo));
constants.put("version", packageInfo.versionName);
return constants;
}
@Override
public String getName() {
return MODULE_NAME;
}
}

View File

@ -200,7 +200,12 @@ public class JitsiMeetActivity
*/
@Override
public void onNewIntent(Intent intent) {
JitsiMeetView.onNewIntent(intent);
Uri uri;
if (Intent.ACTION_VIEW.equals(intent.getAction())
&& (uri = intent.getData()) != null) {
view.loadURLString(uri.toString());
}
}
/**

View File

@ -19,8 +19,6 @@ package org.jitsi.meet.sdk;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -61,6 +59,7 @@ public class JitsiMeetView extends FrameLayout {
ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(
new AndroidSettingsModule(reactContext),
new AppInfoModule(reactContext),
new AudioModeModule(reactContext),
new ExternalAPIModule(reactContext),
new ProximityModule(reactContext)
@ -207,34 +206,6 @@ public class JitsiMeetView extends FrameLayout {
}
}
/**
* Activity lifecycle method which should be called from
* <tt>Activity.onNewIntent</tt> so we can do the required internal
* processing. Note that this is only needed if the activity's "launchMode"
* was set to "singleTask". This is required for deep linking to work once
* the application is already running.
*
* @param intent - <tt>Intent</tt> instance which was received.
*/
public static void onNewIntent(Intent intent) {
// XXX At least twice we received bug reports about malfunctioning
// loadURL in the Jitsi Meet SDK while the Jitsi Meet app seemed to
// functioning as expected in our testing. But that was to be expected
// because the app does not exercise loadURL. In order to increase the
// test coverage of loadURL, channel deep linking through loadURL.
Uri uri;
if (Intent.ACTION_VIEW.equals(intent.getAction())
&& (uri = intent.getData()) != null
&& loadURLStringInViews(uri.toString())) {
return;
}
if (reactInstanceManager != null) {
reactInstanceManager.onNewIntent(intent);
}
}
/**
* The unique identifier of this {@code JitsiMeetView} within the process
* for the purposes of {@link ExternalAPI}. The name scope was inspired by

View File

@ -25,6 +25,7 @@ target 'JitsiMeet' do
:path => '../node_modules/react-native-keep-awake'
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
pod 'RNVectorIcons', :path => '../node_modules/react-native-vector-icons'
pod 'RNWatch', :path => '../node_modules/react-native-watch-connectivity'
end
post_install do |installer|

View File

@ -91,34 +91,6 @@ Loads a specific URL which may identify a conference to join. If the specified
URL is `nil` and the Welcome page is enabled, the Welcome page is displayed
instead.
#### Universal / deep linking
In order to support Universal / deep linking, `JitsiMeetView` offers 2 class
methods that you app's delegate should call in order for the app to follow those
links.
```objc
- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler
{
return [JitsiMeetView application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation
{
return [JitsiMeetView application:application
openURL:url
sourceApplication:sourceApplication
annotation:annotation];
}
```
### JitsiMeetViewDelegate
This delegate is optional, and can be set on the `JitsiMeetView` instance using

View File

@ -11,14 +11,41 @@
0B26BE6F1EC5BC3C00EEFB41 /* JitsiMeet.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0B26BE6D1EC5BC3C00EEFB41 /* JitsiMeet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
0B412F1F1EDEE6E800B1A0A6 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B412F1E1EDEE6E800B1A0A6 /* ViewController.m */; };
0B412F211EDEE95300B1A0A6 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0B412F201EDEE95300B1A0A6 /* Main.storyboard */; };
0B5418471F7C5D8C00A2DD86 /* MeetingRowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B5418461F7C5D8C00A2DD86 /* MeetingRowController.swift */; };
0B7001701F7C51CC005944F4 /* InCallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B70016F1F7C51CC005944F4 /* InCallController.swift */; };
0BD6B4371EF82A6B00D1F4CD /* WebRTC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BD6B4361EF82A6B00D1F4CD /* WebRTC.framework */; };
0BD6B4381EF82A6B00D1F4CD /* WebRTC.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0BD6B4361EF82A6B00D1F4CD /* WebRTC.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
0BEA5C291F7B8F73000D0AB4 /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0BEA5C271F7B8F73000D0AB4 /* Interface.storyboard */; };
0BEA5C2B1F7B8F73000D0AB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0BEA5C2A1F7B8F73000D0AB4 /* Assets.xcassets */; };
0BEA5C321F7B8F73000D0AB4 /* JitsiMeetCompanion Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 0BEA5C311F7B8F73000D0AB4 /* JitsiMeetCompanion Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
0BEA5C371F7B8F73000D0AB4 /* InterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BEA5C361F7B8F73000D0AB4 /* InterfaceController.swift */; };
0BEA5C391F7B8F73000D0AB4 /* ExtensionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BEA5C381F7B8F73000D0AB4 /* ExtensionDelegate.swift */; };
0BEA5C3B1F7B8F73000D0AB4 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BEA5C3A1F7B8F73000D0AB4 /* ComplicationController.swift */; };
0BEA5C3D1F7B8F73000D0AB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0BEA5C3C1F7B8F73000D0AB4 /* Assets.xcassets */; };
0BEA5C411F7B8F73000D0AB4 /* JitsiMeetCompanion.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 0BEA5C251F7B8F73000D0AB4 /* JitsiMeetCompanion.app */; };
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
0BEA5C331F7B8F73000D0AB4 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 0BEA5C301F7B8F73000D0AB4;
remoteInfo = "JitsiMeetCompanion Extension";
};
0BEA5C3F1F7B8F73000D0AB4 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 0BEA5C241F7B8F73000D0AB4;
remoteInfo = JitsiMeetCompanion;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
0B26BE701EC5BC3C00EEFB41 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
@ -32,6 +59,28 @@
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
0BEA5C471F7B8F73000D0AB4 /* Embed App Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
0BEA5C321F7B8F73000D0AB4 /* JitsiMeetCompanion Extension.appex in Embed App Extensions */,
);
name = "Embed App Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
0BEA5C491F7B8F73000D0AB4 /* Embed Watch Content */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
dstSubfolderSpec = 16;
files = (
0BEA5C411F7B8F73000D0AB4 /* JitsiMeetCompanion.app in Embed Watch Content */,
);
name = "Embed Watch Content";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
@ -39,7 +88,19 @@
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>"; };
0B412F201EDEE95300B1A0A6 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
0B5418461F7C5D8C00A2DD86 /* MeetingRowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingRowController.swift; sourceTree = "<group>"; };
0B70016F1F7C51CC005944F4 /* InCallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InCallController.swift; sourceTree = "<group>"; };
0BD6B4361EF82A6B00D1F4CD /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = "../../node_modules/react-native-webrtc/ios/WebRTC.framework"; sourceTree = "<group>"; };
0BEA5C251F7B8F73000D0AB4 /* JitsiMeetCompanion.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = JitsiMeetCompanion.app; sourceTree = BUILT_PRODUCTS_DIR; };
0BEA5C281F7B8F73000D0AB4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = "<group>"; };
0BEA5C2A1F7B8F73000D0AB4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
0BEA5C2C1F7B8F73000D0AB4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0BEA5C311F7B8F73000D0AB4 /* JitsiMeetCompanion Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "JitsiMeetCompanion Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
0BEA5C361F7B8F73000D0AB4 /* InterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterfaceController.swift; sourceTree = "<group>"; };
0BEA5C381F7B8F73000D0AB4 /* ExtensionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionDelegate.swift; sourceTree = "<group>"; };
0BEA5C3A1F7B8F73000D0AB4 /* ComplicationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = "<group>"; };
0BEA5C3C1F7B8F73000D0AB4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
0BEA5C3E1F7B8F73000D0AB4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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>"; };
13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
@ -51,6 +112,13 @@
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
0BEA5C2E1F7B8F73000D0AB4 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -72,6 +140,32 @@
name = Frameworks;
sourceTree = "<group>";
};
0BEA5C261F7B8F73000D0AB4 /* Watch app */ = {
isa = PBXGroup;
children = (
0BEA5C271F7B8F73000D0AB4 /* Interface.storyboard */,
0BEA5C2A1F7B8F73000D0AB4 /* Assets.xcassets */,
0BEA5C2C1F7B8F73000D0AB4 /* Info.plist */,
);
name = "Watch app";
path = watchos/app;
sourceTree = "<group>";
};
0BEA5C351F7B8F73000D0AB4 /* WatchKit extension */ = {
isa = PBXGroup;
children = (
0BEA5C361F7B8F73000D0AB4 /* InterfaceController.swift */,
0BEA5C381F7B8F73000D0AB4 /* ExtensionDelegate.swift */,
0BEA5C3A1F7B8F73000D0AB4 /* ComplicationController.swift */,
0BEA5C3C1F7B8F73000D0AB4 /* Assets.xcassets */,
0BEA5C3E1F7B8F73000D0AB4 /* Info.plist */,
0B70016F1F7C51CC005944F4 /* InCallController.swift */,
0B5418461F7C5D8C00A2DD86 /* MeetingRowController.swift */,
);
name = "WatchKit extension";
path = watchos/extension;
sourceTree = "<group>";
};
13B07FAE1A68108700A75B9A /* src */ = {
isa = PBXGroup;
children = (
@ -95,6 +189,8 @@
0B26BE711EC5BC4D00EEFB41 /* Frameworks */,
83CBBA001A601CBA00E9B192 /* Products */,
13B07FAE1A68108700A75B9A /* src */,
0BEA5C261F7B8F73000D0AB4 /* Watch app */,
0BEA5C351F7B8F73000D0AB4 /* WatchKit extension */,
);
indentWidth = 2;
sourceTree = "<group>";
@ -104,6 +200,8 @@
isa = PBXGroup;
children = (
13B07F961A680F5B00A75B9A /* jitsi-meet.app */,
0BEA5C251F7B8F73000D0AB4 /* JitsiMeetCompanion.app */,
0BEA5C311F7B8F73000D0AB4 /* JitsiMeetCompanion Extension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -111,6 +209,40 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
0BEA5C241F7B8F73000D0AB4 /* JitsiMeetCompanion */ = {
isa = PBXNativeTarget;
buildConfigurationList = 0BEA5C481F7B8F73000D0AB4 /* Build configuration list for PBXNativeTarget "JitsiMeetCompanion" */;
buildPhases = (
0BEA5C231F7B8F73000D0AB4 /* Resources */,
0BEA5C471F7B8F73000D0AB4 /* Embed App Extensions */,
);
buildRules = (
);
dependencies = (
0BEA5C341F7B8F73000D0AB4 /* PBXTargetDependency */,
);
name = JitsiMeetCompanion;
productName = JitsiMeetCompanion;
productReference = 0BEA5C251F7B8F73000D0AB4 /* JitsiMeetCompanion.app */;
productType = "com.apple.product-type.application.watchapp2";
};
0BEA5C301F7B8F73000D0AB4 /* JitsiMeetCompanion Extension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 0BEA5C461F7B8F73000D0AB4 /* Build configuration list for PBXNativeTarget "JitsiMeetCompanion Extension" */;
buildPhases = (
0BEA5C2D1F7B8F73000D0AB4 /* Sources */,
0BEA5C2E1F7B8F73000D0AB4 /* Frameworks */,
0BEA5C2F1F7B8F73000D0AB4 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = "JitsiMeetCompanion Extension";
productName = "JitsiMeetCompanion Extension";
productReference = 0BEA5C311F7B8F73000D0AB4 /* JitsiMeetCompanion Extension.appex */;
productType = "com.apple.product-type.watchkit2-extension";
};
13B07F861A680F5B00A75B9A /* jitsi-meet */ = {
isa = PBXNativeTarget;
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "jitsi-meet" */;
@ -122,10 +254,12 @@
0B26BE701EC5BC3C00EEFB41 /* Embed Frameworks */,
B35383AD1DDA0083008F406A /* Adjust embedded framework architectures */,
0BB7DA181EC9E695007AAE98 /* Adjust ATS */,
0BEA5C491F7B8F73000D0AB4 /* Embed Watch Content */,
);
buildRules = (
);
dependencies = (
0BEA5C401F7B8F73000D0AB4 /* PBXTargetDependency */,
);
name = "jitsi-meet";
productName = "Jitsi Meet";
@ -138,9 +272,20 @@
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 0900;
LastUpgradeCheck = 0820;
ORGANIZATIONNAME = Facebook;
TargetAttributes = {
0BEA5C241F7B8F73000D0AB4 = {
CreatedOnToolsVersion = 9.0;
DevelopmentTeam = BQNXB4G3KQ;
ProvisioningStyle = Automatic;
};
0BEA5C301F7B8F73000D0AB4 = {
CreatedOnToolsVersion = 9.0;
DevelopmentTeam = BQNXB4G3KQ;
ProvisioningStyle = Automatic;
};
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = BQNXB4G3KQ;
SystemCapabilities = {
@ -165,11 +310,30 @@
projectRoot = "";
targets = (
13B07F861A680F5B00A75B9A /* jitsi-meet */,
0BEA5C241F7B8F73000D0AB4 /* JitsiMeetCompanion */,
0BEA5C301F7B8F73000D0AB4 /* JitsiMeetCompanion Extension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
0BEA5C231F7B8F73000D0AB4 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0BEA5C2B1F7B8F73000D0AB4 /* Assets.xcassets in Resources */,
0BEA5C291F7B8F73000D0AB4 /* Interface.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
0BEA5C2F1F7B8F73000D0AB4 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0BEA5C3D1F7B8F73000D0AB4 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
13B07F8E1A680F5B00A75B9A /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -228,6 +392,18 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
0BEA5C2D1F7B8F73000D0AB4 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0B7001701F7C51CC005944F4 /* InCallController.swift in Sources */,
0B5418471F7C5D8C00A2DD86 /* MeetingRowController.swift in Sources */,
0BEA5C391F7B8F73000D0AB4 /* ExtensionDelegate.swift in Sources */,
0BEA5C371F7B8F73000D0AB4 /* InterfaceController.swift in Sources */,
0BEA5C3B1F7B8F73000D0AB4 /* ComplicationController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
13B07F871A680F5B00A75B9A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -240,7 +416,28 @@
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
0BEA5C341F7B8F73000D0AB4 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 0BEA5C301F7B8F73000D0AB4 /* JitsiMeetCompanion Extension */;
targetProxy = 0BEA5C331F7B8F73000D0AB4 /* PBXContainerItemProxy */;
};
0BEA5C401F7B8F73000D0AB4 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 0BEA5C241F7B8F73000D0AB4 /* JitsiMeetCompanion */;
targetProxy = 0BEA5C3F1F7B8F73000D0AB4 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
0BEA5C271F7B8F73000D0AB4 /* Interface.storyboard */ = {
isa = PBXVariantGroup;
children = (
0BEA5C281F7B8F73000D0AB4 /* Base */,
);
name = Interface.storyboard;
sourceTree = "<group>";
};
13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = {
isa = PBXVariantGroup;
children = (
@ -252,6 +449,140 @@
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
0BEA5C421F7B8F73000D0AB4 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = BQNXB4G3KQ;
GCC_C_LANGUAGE_STANDARD = gnu11;
IBSC_MODULE = JitsiMeetCompanion_Extension;
INFOPLIST_FILE = watchos/app/Info.plist;
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeet.ios.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 4.0;
};
name = Debug;
};
0BEA5C431F7B8F73000D0AB4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = BQNXB4G3KQ;
GCC_C_LANGUAGE_STANDARD = gnu11;
IBSC_MODULE = JitsiMeetCompanion_Extension;
INFOPLIST_FILE = watchos/app/Info.plist;
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeet.ios.watchkitapp;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 4.0;
};
name = Release;
};
0BEA5C441F7B8F73000D0AB4 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = BQNXB4G3KQ;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = watchos/extension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeet.ios.watchkitapp.watchkitextension;
PRODUCT_NAME = "${TARGET_NAME}";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 4.0;
};
name = Debug;
};
0BEA5C451F7B8F73000D0AB4 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = BQNXB4G3KQ;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = watchos/extension/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeet.ios.watchkitapp.watchkitextension;
PRODUCT_NAME = "${TARGET_NAME}";
SDKROOT = watchos;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 4.0;
};
name = Release;
};
13B07F941A680F5B00A75B9A /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -409,6 +740,24 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
0BEA5C461F7B8F73000D0AB4 /* Build configuration list for PBXNativeTarget "JitsiMeetCompanion Extension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0BEA5C441F7B8F73000D0AB4 /* Debug */,
0BEA5C451F7B8F73000D0AB4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
0BEA5C481F7B8F73000D0AB4 /* Build configuration list for PBXNativeTarget "JitsiMeetCompanion" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0BEA5C421F7B8F73000D0AB4 /* Debug */,
0BEA5C431F7B8F73000D0AB4 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "jitsi-meet" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@ -16,8 +16,22 @@
#import "AppDelegate.h"
#include <Availability.h>
#import <Foundation/Foundation.h>
#import <JitsiMeet/JitsiMeet.h>
// Weakly load the Intents framework since it's not available on iOS 9.
@import Intents;
// Constant describing iOS 10.0.0
static const NSOperatingSystemVersion ios10 = {
.majorVersion = 10,
.minorVersion = 0,
.patchVersion = 0
};
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
@ -28,22 +42,68 @@
#pragma mark Linking delegate methods
- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler {
return [JitsiMeetView application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler {
JitsiMeetView *view = (JitsiMeetView *) self.window.rootViewController.view;
if (!view) {
return NO;
}
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
[view loadURL:userActivity.webpageURL];
return YES;
}
// Check for CallKit intents only on iOS >= 10
if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:ios10]) {
if ([userActivity.activityType isEqualToString:@"INStartAudioCallIntent"]
|| [userActivity.activityType isEqualToString:@"INStartVideoCallIntent"]) {
INInteraction *interaction = [userActivity interaction];
INIntent *intent = interaction.intent;
NSString *handle;
BOOL isAudio = NO;
if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
INStartAudioCallIntent *startCallIntent
= (INStartAudioCallIntent *)intent;
handle = startCallIntent.contacts.firstObject.personHandle.value;
isAudio = YES;
} else {
INStartVideoCallIntent *startCallIntent
= (INStartVideoCallIntent *)intent;
handle = startCallIntent.contacts.firstObject.personHandle.value;
}
if (!handle) {
return NO;
}
// Load the URL contained in the handle
[view loadURLObject:@{
@"url": handle,
@"configOverwrite": @{
@"startAudioOnly": @(isAudio)
}
}];
return YES;
}
}
return NO;
}
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {
return [JitsiMeetView application:application
openURL:url
sourceApplication:sourceApplication
annotation:annotation];
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
JitsiMeetView *view = (JitsiMeetView *) self.window.rootViewController.view;
[view loadURL:url];
return YES;
}
@end

View File

@ -0,0 +1,75 @@
{
"images" : [
{
"size" : "24x24",
"idiom" : "watch",
"filename" : "Icon-24@2x.png",
"scale" : "2x",
"role" : "notificationCenter",
"subtype" : "38mm"
},
{
"size" : "27.5x27.5",
"idiom" : "watch",
"filename" : "Icon-27.5@2x.png",
"scale" : "2x",
"role" : "notificationCenter",
"subtype" : "42mm"
},
{
"size" : "29x29",
"idiom" : "watch",
"filename" : "Icon-29@2x.png",
"role" : "companionSettings",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "watch",
"filename" : "Icon-29@3x.png",
"role" : "companionSettings",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "watch",
"filename" : "Icon-40@2x.png",
"scale" : "2x",
"role" : "appLauncher",
"subtype" : "38mm"
},
{
"size" : "44x44",
"idiom" : "watch",
"filename" : "Icon-44@2x.png",
"scale" : "2x",
"role" : "longLook",
"subtype" : "42mm"
},
{
"size" : "86x86",
"idiom" : "watch",
"filename" : "Icon-86@2x.png",
"scale" : "2x",
"role" : "quickLook",
"subtype" : "38mm"
},
{
"size" : "98x98",
"idiom" : "watch",
"filename" : "Icon-98@2x.png",
"scale" : "2x",
"role" : "quickLook",
"subtype" : "42mm"
},
{
"idiom" : "watch-marketing",
"size" : "1024x1024",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "hangup@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "mute-off@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "mute-on@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder.WatchKit.Storyboard" version="3.0" toolsVersion="13196" targetRuntime="watchKit" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="AgC-eL-Hgc">
<device id="watch38" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="watchOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13173"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBWatchKitPlugin" version="13051"/>
</dependencies>
<scenes>
<!--Meetings-->
<scene sceneID="aou-V4-d1y">
<objects>
<controller title="Meetings" id="AgC-eL-Hgc" customClass="InterfaceController" customModule="JitsiMeetCompanion" customModuleProvider="target">
<items>
<table alignment="left" id="gpO-ql-Xsr">
<items>
<tableRow identifier="MeetingRowType" id="GGl-av-xeJ" customClass="MeetingRowController" customModule="JitsiMeetCompanion_Extension">
<group key="rootItem" width="1" height="0.0" alignment="left" layout="vertical" id="5XE-gq-qzG">
<items>
<label alignment="left" text="Label" id="Sij-up-N4p"/>
<label alignment="left" text="Label" id="V5K-sm-jEH">
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
<fontDescription key="font" style="UICTFontTextStyleFootnote"/>
</label>
</items>
<connections>
<segue destination="9RD-qP-1Z0" kind="push" id="Boa-6E-eZs"/>
</connections>
</group>
<connections>
<outlet property="roomLabel" destination="Sij-up-N4p" id="PdS-SO-ylc"/>
<outlet property="timeLabel" destination="V5K-sm-jEH" id="fWQ-kx-vE4"/>
</connections>
</tableRow>
</items>
</table>
</items>
<connections>
<outlet property="table" destination="gpO-ql-Xsr" id="aVV-iZ-z3l"/>
</connections>
</controller>
</objects>
<point key="canvasLocation" x="-99" y="117"/>
</scene>
<!--Meetings-->
<scene sceneID="ns4-Kh-qqU">
<objects>
<controller identifier="InCallController" title="Meetings" hidesWhenLoading="NO" id="9RD-qP-1Z0" customClass="InCallController" customModule="JitsiMeetCompanion" customModuleProvider="target">
<items>
<label alignment="center" text="Label" id="vFt-lL-SNY"/>
<timer alignment="center" textAlignment="center" previewedSeconds="0" id="W8S-uZ-MPm">
<color key="textColor" red="0.024725984125768763" green="1" blue="0.24241188365329402" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="font" style="UICTFontTextStyleHeadline"/>
</timer>
<group alignment="center" verticalAlignment="bottom" spacing="10" id="Hfk-a0-uWj">
<items>
<button width="60" height="60" alignment="left" verticalAlignment="bottom" backgroundImage="hangup" id="8jF-SI-UHz">
<connections>
<action selector="hangupClicked" destination="9RD-qP-1Z0" id="cXK-lw-tsd"/>
</connections>
</button>
<button width="60" height="60" alignment="right" verticalAlignment="bottom" backgroundImage="mute-off" id="LmN-FI-aQq">
<connections>
<action selector="muteClicked" destination="9RD-qP-1Z0" id="dJg-kV-cqH"/>
</connections>
</button>
</items>
</group>
</items>
<connections>
<outlet property="mutedButton" destination="LmN-FI-aQq" id="gfi-4T-gdN"/>
<outlet property="roomLabel" destination="vFt-lL-SNY" id="cYB-Tf-Efz"/>
<outlet property="timer" destination="W8S-uZ-MPm" id="r7T-j1-9VJ"/>
</connections>
</controller>
</objects>
<point key="canvasLocation" x="213" y="117"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,33 @@
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Jitsi Meet</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.9</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
<key>WKCompanionAppBundleIdentifier</key>
<string>org.jitsi.JitsiMeet.ios</string>
<key>WKWatchKitApp</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,18 @@
{
"images" : [
{
"idiom" : "watch",
"screenWidth" : "{130,145}",
"scale" : "2x"
},
{
"idiom" : "watch",
"screenWidth" : "{146,165}",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,31 @@
{
"assets" : [
{
"idiom" : "watch",
"filename" : "Circular.imageset",
"role" : "circular"
},
{
"idiom" : "watch",
"filename" : "Extra Large.imageset",
"role" : "extra-large"
},
{
"filename" : "jitsi.imageset"
},
{
"idiom" : "watch",
"filename" : "Modular.imageset",
"role" : "modular"
},
{
"idiom" : "watch",
"filename" : "Utilitarian.imageset",
"role" : "utilitarian"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,18 @@
{
"images" : [
{
"idiom" : "watch",
"screenWidth" : "{130,145}",
"scale" : "2x"
},
{
"idiom" : "watch",
"screenWidth" : "{146,165}",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,18 @@
{
"images" : [
{
"idiom" : "watch",
"screenWidth" : "{130,145}",
"scale" : "2x"
},
{
"idiom" : "watch",
"screenWidth" : "{146,165}",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,18 @@
{
"images" : [
{
"idiom" : "watch",
"screenWidth" : "{130,145}",
"scale" : "2x"
},
{
"idiom" : "watch",
"screenWidth" : "{146,165}",
"scale" : "2x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "jitsi@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,78 @@
/*
* 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 ClockKit
class ComplicationController: NSObject, CLKComplicationDataSource {
// MARK: - Timeline Configuration
func getSupportedTimeTravelDirections(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimeTravelDirections) -> Void) {
//handler([.forward, .backward])
handler([])
}
func getTimelineStartDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) {
handler(nil)
}
func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) {
handler(nil)
}
func getPrivacyBehavior(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) {
handler(.showOnLockScreen)
}
// MARK: - Timeline Population
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
// Call the handler with the current timeline entry
handler(nil)
}
func getTimelineEntries(for complication: CLKComplication, before date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
// Call the handler with the timeline entries prior to the given date
handler(nil)
}
func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
// Call the handler with the timeline entries after to the given date
handler(nil)
}
// MARK: - Placeholder Templates
func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
let imageProvider = CLKImageProvider(onePieceImage: UIImage(named: "Complication/jitsi")!)
if complication.family == .circularSmall {
let small = CLKComplicationTemplateCircularSmallRingImage()
small.imageProvider = imageProvider
// This method will be called once per supported complication, and the results will be cached
handler(small)
} else if complication.family == .utilitarianSmall {
let utilitarian = CLKComplicationTemplateUtilitarianSmallRingImage()
utilitarian.imageProvider = imageProvider
handler(utilitarian)
} else if complication.family == .modularSmall {
let modular = CLKComplicationTemplateModularSmallRingImage()
modular.imageProvider = imageProvider
handler(modular)
}
}
}

View File

@ -0,0 +1,112 @@
/*
* 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 WatchConnectivity
import WatchKit
class ExtensionDelegate: NSObject, WCSessionDelegate, WKExtensionDelegate {
func applicationDidFinishLaunching() {
// Start Watch Connectivity
if WCSession.isSupported() {
let session = WCSession.default
session.delegate = self
session.activate()
}
}
func applicationDidBecomeActive() {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillResignActive() {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, etc.
}
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
// Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
for task in backgroundTasks {
// Use a switch statement to check the task type
switch task {
case let backgroundTask as WKApplicationRefreshBackgroundTask:
// Be sure to complete the background task once youre done.
backgroundTask.setTaskCompletedWithSnapshot(false)
case let snapshotTask as WKSnapshotRefreshBackgroundTask:
// Snapshot tasks have a unique completion call, make sure to set your expiration date
snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
// Be sure to complete the connectivity task once youre done.
connectivityTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// Be sure to complete the URL session task once youre done.
urlSessionTask.setTaskCompletedWithSnapshot(false)
default:
// make sure to complete unhandled task types
task.setTaskCompletedWithSnapshot(false)
}
}
}
func session(_ session: WCSession, activationDidCompleteWith
activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print("WC Session activation failed with error: \(error.localizedDescription)")
return
}
print("WC Session activated with state: \(activationState.rawValue)")
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
print("WC SESSION DID RECEIVE APP CONTEXT");
let conferenceURL = applicationContext["conferenceURL"] as? NSString ?? "NULL";
print("CONFERENCE URL \(conferenceURL)");
let micMuted = applicationContext["micMuted"] as? NSNumber ?? 0;
print("MIC MUTED \(micMuted)");
// Update recent URLs
let recentURLs = applicationContext["recentURLs"];
if let recentURLsArray = recentURLs as? NSArray {
let controller = WKExtension.shared().rootInterfaceController as! InterfaceController
controller.updateRecents(withRecents: recentURLsArray)
}
// If the current controller is not the in-call controller and we have a
// conference URL, show the in-call controller
if let currentController = WKExtension.shared().visibleInterfaceController as? InterfaceController {
if conferenceURL != "NULL" {
let room = conferenceURL.components(separatedBy: "/").last
let context = ["room" : room, "roomUrl" : conferenceURL as String!, "skipJoin" : "true", "muted" : micMuted.boolValue.description ]
DispatchQueue.main.async {
currentController.pushController(withName: "InCallController", context: context)
}
}
} else if let controller = WKExtension.shared().visibleInterfaceController as? InCallController {
if conferenceURL == "NULL" {
DispatchQueue.main.async {
controller.popToRootController()
}
} else {
// Update muted state
controller.updateMutedButton(withMuted: micMuted.boolValue)
}
}
}
}

View File

@ -0,0 +1,84 @@
/*
* 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 WatchConnectivity
import WatchKit
import Foundation
class InCallController: WKInterfaceController {
@IBOutlet var mutedButton: WKInterfaceButton!
@IBOutlet var roomLabel: WKInterfaceLabel!
@IBOutlet var timer: WKInterfaceTimer!
var muted: Bool!
@IBAction func hangupClicked() {
sendMessage(["command": "hangup"])
popToRootController()
}
@IBAction func muteClicked() {
sendMessage(["command": "toggleMute"])
updateMutedButton(withMuted: !muted)
}
func sendMessage(_ message: [String : Any]) {
if WCSession.isSupported() {
let session = WCSession.default
session.sendMessage(message, replyHandler: nil, errorHandler: nil)
}
}
func updateMutedButton(withMuted isMuted: Bool) {
muted = isMuted
if isMuted {
mutedButton.setBackgroundImageNamed("mute-on.png")
} else {
mutedButton.setBackgroundImageNamed("mute-off.png")
}
}
override func awake(withContext context: Any?) {
super.awake(withContext: context)
if let data = context as? [String : String] {
if data["skipJoin"] != "yes" {
sendMessage(["command": "joinConference", "data" : data["roomUrl"]!])
}
roomLabel.setText(data["room"]!)
updateMutedButton(withMuted: data["muted"] == "true")
}
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didAppear() {
super.didAppear()
timer.start()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
}

View File

@ -0,0 +1,44 @@
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Jitsi Meet Companion Extension</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>1.9</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CLKComplicationPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ComplicationController</string>
<key>CLKComplicationSupportedFamilies</key>
<array>
<string>CLKComplicationFamilyModularSmall</string>
<string>CLKComplicationFamilyUtilitarianSmall</string>
<string>CLKComplicationFamilyCircularSmall</string>
</array>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>WKAppBundleIdentifier</key>
<string>org.jitsi.JitsiMeet.ios.watchkitapp</string>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.watchkit</string>
</dict>
<key>WKExtensionDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).ExtensionDelegate</string>
</dict>
</plist>

View File

@ -0,0 +1,69 @@
/*
* 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 WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
@IBOutlet var table: WKInterfaceTable!
func updateRecents(withRecents recents: NSArray) {
table.setNumberOfRows(recents.count, withRowType: "MeetingRowType")
for (index, entry) in recents.enumerated() {
// FIXME possible runtime exception
let entryDict = entry as! NSDictionary
let roomURL = entryDict["roomURL"] as! NSString
let timestamp = entryDict["timestamp"] as! NSNumber
// Prepare values
let room = roomURL.components(separatedBy: "/").last
let date = Date(timeIntervalSince1970: timestamp.doubleValue / 1000) // timestamp is taken with Date.now() in JS, which uses milliseconds
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone.current
dateFormatter.locale = NSLocale.current
dateFormatter.dateFormat = "HH:mm yyyy-MM-dd"
let strDate = dateFormatter.string(from: date)
// Update row controller
let controller = table.rowController(at: index) as! MeetingRowController
controller.room = room
controller.roomUrl = roomURL as String!
controller.roomLabel.setText(room)
controller.timeLabel.setText(strDate)
}
}
override func awake(withContext context: Any?) {
super.awake(withContext: context)
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
override func contextForSegue(withIdentifier segueIdentifier: String, in table: WKInterfaceTable, rowIndex: Int) -> Any? {
let controller = table.rowController(at: rowIndex) as! MeetingRowController
return ["room" : controller.room, "roomUrl" : controller.roomUrl, "muted" : "false"]
}
}

View File

@ -0,0 +1,25 @@
/*
* 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 WatchKit
class MeetingRowController: NSObject {
@IBOutlet var roomLabel: WKInterfaceLabel!
@IBOutlet var timeLabel: WKInterfaceLabel!
var room: String!
var roomUrl: String!
}

View File

@ -14,6 +14,10 @@
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */; };
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */; };
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */; };
0BB9AD771F5EC6CE001C08DB /* CallKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BB9AD761F5EC6CE001C08DB /* CallKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
0BB9AD791F5EC6D7001C08DB /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BB9AD781F5EC6D7001C08DB /* Intents.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */; };
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BB9AD7C1F60356D001C08DB /* AppInfo.m */; };
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495C1EC4B6C600B793EE /* AudioMode.m */; };
0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495D1EC4B6C600B793EE /* POSIX.m */; };
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BCA495E1EC4B6C600B793EE /* Proximity.m */; };
@ -31,6 +35,10 @@
0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBridgeWrapper.h; sourceTree = "<group>"; };
0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridgeWrapper.m; sourceTree = "<group>"; };
0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExternalAPI.m; sourceTree = "<group>"; };
0BB9AD761F5EC6CE001C08DB /* CallKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CallKit.framework; path = System/Library/Frameworks/CallKit.framework; sourceTree = SDKROOT; };
0BB9AD781F5EC6D7001C08DB /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; };
0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CallKit.m; sourceTree = "<group>"; };
0BB9AD7C1F60356D001C08DB /* AppInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppInfo.m; sourceTree = "<group>"; };
0BCA495C1EC4B6C600B793EE /* AudioMode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioMode.m; sourceTree = "<group>"; };
0BCA495D1EC4B6C600B793EE /* POSIX.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = POSIX.m; sourceTree = "<group>"; };
0BCA495E1EC4B6C600B793EE /* Proximity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Proximity.m; sourceTree = "<group>"; };
@ -48,6 +56,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
0BB9AD791F5EC6D7001C08DB /* Intents.framework in Frameworks */,
0BB9AD771F5EC6CE001C08DB /* CallKit.framework in Frameworks */,
0B93EF7B1EC608550030D24D /* CoreText.framework in Frameworks */,
0F65EECE1D95DA94561BB47E /* libPods-JitsiMeet.a in Frameworks */,
);
@ -87,6 +97,8 @@
isa = PBXGroup;
children = (
0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
0BB9AD7C1F60356D001C08DB /* AppInfo.m */,
0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */,
0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */,
0BD906E91EC0C00300C8C18E /* Info.plist */,
0BD906E81EC0C00300C8C18E /* JitsiMeet.h */,
@ -104,6 +116,8 @@
9C3C6FA2341729836589B856 /* Frameworks */ = {
isa = PBXGroup;
children = (
0BB9AD781F5EC6D7001C08DB /* Intents.framework */,
0BB9AD761F5EC6CE001C08DB /* CallKit.framework */,
0B93EF7A1EC608550030D24D /* CoreText.framework */,
0BCA49631EC4B76D00B793EE /* WebRTC.framework */,
03F2ADC957FF109849B7FCA1 /* libPods-JitsiMeet.a */,
@ -222,13 +236,16 @@
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-JitsiMeet-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n";
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
C7BC10B338C94EEB98048E64 /* [CP] Copy Pods Resources */ = {
@ -237,9 +254,21 @@
files = (
);
inputPaths = (
"${SRCROOT}/../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet-resources.sh",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Octicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@ -253,6 +282,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,
0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */,

42
ios/sdk/src/AppInfo.m Normal file
View File

@ -0,0 +1,42 @@
/*
* 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 "RCTBridgeModule.h"
#import "RCTLog.h"
#import <AVFoundation/AVFoundation.h>
@interface AppInfo : NSObject<RCTBridgeModule>
@end
@implementation AppInfo
RCT_EXPORT_MODULE();
- (NSDictionary *)constantsToExport {
NSString *name = [[[NSBundle mainBundle]infoDictionary]objectForKey :@"CFBundleDisplayName"];
NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"];
if (version == nil) {
version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
if (version == nil) {
version = @"";
}
}
return @{ @"name" : name, @"version" : version };
};
@end

334
ios/sdk/src/CallKit.m Normal file
View File

@ -0,0 +1,334 @@
//
// Based on RNCallKit
//
// Original license:
//
// Copyright (c) 2016, Ian Yu-Hsun Lin
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTEventEmitter.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTUtils.h>
// Weakly load CallKit, because it's not available on iOS 9.
@import CallKit;
// Events we will emit.
static NSString *const RNCallKitPerformAnswerCallAction = @"performAnswerCallAction";
static NSString *const RNCallKitPerformEndCallAction = @"performEndCallAction";
static NSString *const RNCallKitPerformSetMutedCallAction = @"performSetMutedCallAction";
static NSString *const RNCallKitProviderDidReset = @"providerDidReset";
@interface RNCallKit : RCTEventEmitter <CXProviderDelegate>
@end
@implementation RNCallKit
{
CXCallController *callKitCallController;
CXProvider *callKitProvider;
}
RCT_EXPORT_MODULE()
- (NSArray<NSString *> *)supportedEvents
{
return @[
RNCallKitPerformAnswerCallAction,
RNCallKitPerformEndCallAction,
RNCallKitPerformSetMutedCallAction,
RNCallKitProviderDidReset
];
}
// Configure CallKit
RCT_EXPORT_METHOD(setup:(NSDictionary *)options)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][setup] options = %@", options);
#endif
callKitCallController = [[CXCallController alloc] init];
if (callKitProvider) {
[callKitProvider invalidate];
}
callKitProvider = [[CXProvider alloc] initWithConfiguration:[self getProviderConfiguration: options]];
[callKitProvider setDelegate:self queue:nil];
}
#pragma mark - CXCallController call actions
// Display the incoming call to the user
RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
handle:(NSString *)handle
hasVideo:(BOOL)hasVideo
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][displayIncomingCall] uuidString = %@", uuidString);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
callUpdate.remoteHandle
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
callUpdate.supportsDTMF = NO;
callUpdate.supportsHolding = NO;
callUpdate.supportsGrouping = NO;
callUpdate.supportsUngrouping = NO;
callUpdate.hasVideo = hasVideo;
[callKitProvider reportNewIncomingCallWithUUID:uuid
update:callUpdate
completion:^(NSError * _Nullable error) {
if (error == nil) {
resolve(nil);
} else {
reject(nil, @"Error reporting new incoming call", error);
}
}];
}
// End call
RCT_EXPORT_METHOD(endCall:(NSString *)uuidString
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][endCall] uuidString = %@", uuidString);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXEndCallAction *action = [[CXEndCallAction alloc] initWithCallUUID:uuid];
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
[self requestTransaction:transaction resolve:resolve reject:reject];
}
// Mute / unmute (audio)
RCT_EXPORT_METHOD(setMuted:(NSString *)uuidString
muted:(BOOL) muted
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][setMuted] uuidString = %@", uuidString);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXSetMutedCallAction *action
= [[CXSetMutedCallAction alloc] initWithCallUUID:uuid muted:muted];
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
[self requestTransaction:transaction resolve:resolve reject:reject];
}
// Start outgoing call
RCT_EXPORT_METHOD(startCall:(NSString *)uuidString
handle:(NSString *)handle
video:(BOOL)video
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][startCall] uuidString = %@", uuidString);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXHandle *callHandle
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
CXStartCallAction *action
= [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle];
action.video = video;
CXTransaction *transaction = [[CXTransaction alloc] initWithAction:action];
[self requestTransaction:transaction resolve:resolve reject:reject];
}
// Indicate call failed
RCT_EXPORT_METHOD(reportCallFailed:(NSString *)uuidString
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
[callKitProvider reportCallWithUUID:uuid
endedAtDate:[NSDate date]
reason:CXCallEndedReasonFailed];
resolve(nil);
}
// Indicate outgoing call connected
RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)uuidString
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
[callKitProvider reportOutgoingCallWithUUID:uuid
connectedAtDate:[NSDate date]];
resolve(nil);
}
// Update call in case we have a display name or video capability changes
RCT_EXPORT_METHOD(updateCall:(NSString *)uuidString
options:(NSDictionary *)options
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
#ifdef DEBUG
NSLog(@"[RNCallKit][updateCall] uuidString = %@ options = %@", uuidString, options);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
CXCallUpdate *update = [[CXCallUpdate alloc] init];
if (options[@"displayName"]) {
update.localizedCallerName = options[@"displayName"];
}
if (options[@"hasVideo"]) {
update.hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
}
[callKitProvider reportCallWithUUID:uuid updated:update];
resolve(nil);
}
#pragma mark - Helper methods
- (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary* )settings
{
#ifdef DEBUG
NSLog(@"[RNCallKit][getProviderConfiguration]");
#endif
CXProviderConfiguration *providerConfiguration
= [[CXProviderConfiguration alloc] initWithLocalizedName:settings[@"appName"]];
providerConfiguration.supportsVideo = YES;
providerConfiguration.maximumCallGroups = 1;
providerConfiguration.maximumCallsPerCallGroup = 1;
providerConfiguration.supportedHandleTypes
= [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypeGeneric], nil];
if (settings[@"imageName"]) {
providerConfiguration.iconTemplateImageData
= UIImagePNGRepresentation([UIImage imageNamed:settings[@"imageName"]]);
}
if (settings[@"ringtoneSound"]) {
providerConfiguration.ringtoneSound = settings[@"ringtoneSound"];
}
return providerConfiguration;
}
- (void)requestTransaction:(CXTransaction *)transaction
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
#ifdef DEBUG
NSLog(@"[RNCallKit][requestTransaction] transaction = %@", transaction);
#endif
[callKitCallController requestTransaction:transaction completion:^(NSError * _Nullable error) {
if (error == nil) {
resolve(nil);
} else {
NSLog(@"[RNCallKit][requestTransaction] Error requesting transaction (%@): (%@)", transaction.actions, error);
reject(nil, @"Error processing CallKit transaction", error);
}
}];
}
#pragma mark - CXProviderDelegate
// Called when the provider has been reset. We should terminate all calls.
- (void)providerDidReset:(CXProvider *)provider {
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:providerDidReset]");
#endif
[self sendEventWithName:RNCallKitProviderDidReset body:nil];
}
// Answering incoming call
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performAnswerCallAction]");
#endif
[self sendEventWithName:RNCallKitPerformAnswerCallAction
body:@{ @"callUUID": action.callUUID.UUIDString }];
[action fulfill];
}
// Call ended, user request
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performEndCallAction]");
#endif
[self sendEventWithName:RNCallKitPerformEndCallAction
body:@{ @"callUUID": action.callUUID.UUIDString }];
[action fulfill];
}
// Handle audio mute from CallKit view
- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action {
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performSetMutedCallAction]");
#endif
[self sendEventWithName:RNCallKitPerformSetMutedCallAction
body:@{ @"callUUID": action.callUUID.UUIDString,
@"muted": [NSNumber numberWithBool:action.muted]}];
[action fulfill];
}
// Starting outgoing call
- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:performStartCallAction]");
#endif
[action fulfill];
// Update call info
CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
callUpdate.remoteHandle = action.handle;
callUpdate.supportsDTMF = NO;
callUpdate.supportsHolding = NO;
callUpdate.supportsGrouping = NO;
callUpdate.supportsUngrouping = NO;
callUpdate.hasVideo = action.isVideo;
[callKitProvider reportCallWithUUID:action.callUUID updated:callUpdate];
// Notify the system about the outgoing call
[callKitProvider reportOutgoingCallWithUUID:action.callUUID
startedConnectingAtDate:[NSDate date]];
}
// These just help with debugging
- (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didActivateAudioSession]");
#endif
}
- (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:didDeactivateAudioSession]");
#endif
}
- (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action
{
#ifdef DEBUG
NSLog(@"[RNCallKit][CXProviderDelegate][provider:timedOutPerformingAction]");
#endif
}
@end

View File

@ -28,15 +28,6 @@
+ (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;

View File

@ -18,7 +18,6 @@
#include <mach/mach_time.h>
#import <React/RCTAssert.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTRootView.h>
#import "JitsiMeetView+Private.h"
@ -133,48 +132,6 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
return YES;
}
#pragma mark Linking delegate helpers
// https://facebook.github.io/react-native/docs/linking.html
+ (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler
{
// XXX At least twice we received bug reports about malfunctioning loadURL
// in the Jitsi Meet SDK while the Jitsi Meet app seemed to functioning as
// expected in our testing. But that was to be expected because the app does
// not exercise loadURL. In order to increase the test coverage of loadURL,
// channel Universal linking through loadURL.
if ([userActivity.activityType
isEqualToString:NSUserActivityTypeBrowsingWeb]
&& [JitsiMeetView loadURLInViews:userActivity.webpageURL]) {
return YES;
}
return [RCTLinkingManager application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
+ (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {
// XXX At least twice we received bug reports about malfunctioning loadURL
// in the Jitsi Meet SDK while the Jitsi Meet app seemed to functioning as
// expected in our testing. But that was to be expected because the app does
// not exercise loadURL. In order to increase the test coverage of loadURL,
// channel Universal linking through loadURL.
if ([JitsiMeetView loadURLInViews:url]) {
return YES;
}
return [RCTLinkingManager application:application
openURL:url
sourceApplication:sourceApplication
annotation:annotation];
}
#pragma mark Initializers
- (instancetype)init {
@ -281,32 +238,6 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
#pragma mark Private methods
/**
* Loads a specific {@link NSURL} in all existing {@code JitsiMeetView}s.
*
* @param url - The {@code NSURL} to load in all existing
* {@code JitsiMeetView}s.
* @return {@code YES} if the specified {@code url} was submitted for loading in
* at least one {@code JitsiMeetView}; otherwise, {@code NO}.
*/
+ (BOOL)loadURLInViews:(NSURL *)url {
BOOL handled = NO;
if (views) {
for (NSString *externalAPIScope in views) {
JitsiMeetView *view
= [JitsiMeetView viewForExternalAPIScope:externalAPIScope];
if (view) {
[view loadURL:url];
handled = YES;
}
}
}
return handled;
}
+ (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope {
return [views objectForKey:externalAPIScope];
}

View File

@ -38,11 +38,11 @@
"i18next": "8.4.3",
"i18next-browser-languagedetector": "2.0.0",
"i18next-xhr-backend": "1.4.2",
"jQuery-Impromptu": "trentrichardson/jQuery-Impromptu#v6.0.0",
"jitsi-meet-logger": "jitsi/jitsi-meet-logger",
"jquery": "2.1.4",
"jquery-contextmenu": "2.4.5",
"jquery-i18next": "1.2.0",
"jQuery-Impromptu": "trentrichardson/jQuery-Impromptu#v6.0.0",
"jquery-ui": "1.10.5",
"jssha": "2.2.0",
"jwt-decode": "2.2.0",
@ -64,6 +64,7 @@
"react-native-locale-detector": "1.0.1",
"react-native-prompt": "1.0.0",
"react-native-vector-icons": "4.3.0",
"react-native-watch-connectivity": "saghul/react-native-watch-connectivity#podspec-module",
"react-native-webrtc": "jitsi/react-native-webrtc",
"react-redux": "5.0.6",
"redux": "3.7.2",
@ -73,6 +74,7 @@
"strophejs-plugins": "0.0.7",
"styled-components": "1.3.0",
"url-polyfill": "github/url-polyfill",
"uuid": "3.1.0",
"xmldom": "0.1.27"
},
"devDependencies": {
@ -82,6 +84,8 @@
"babel-polyfill": "6.26.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-react": "6.24.1",
"babel-preset-react-native": "4.0.0",
"babel-preset-react-native-stage-0": "1.0.1",
"babel-preset-stage-1": "6.24.1",
"clean-css": "3.4.25",
"css-loader": "0.28.5",

View File

@ -1,18 +1,21 @@
/* global __DEV__ */
import React from 'react';
import { Linking } from 'react-native';
import '../../analytics';
import '../../authentication';
import { Platform } from '../../base/react';
import '../../recent';
import '../../mobile/audio-mode';
import '../../mobile/background';
import '../../mobile/callkit';
import '../../mobile/external-api';
import '../../mobile/full-screen';
import '../../mobile/permissions';
import '../../mobile/proximity';
import '../../mobile/wake-lock';
import '../../mobile/watchos';
import { AbstractApp } from './AbstractApp';
@ -47,9 +50,6 @@ export class App extends AbstractApp {
constructor(props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._onLinkingURL = this._onLinkingURL.bind(this);
// In the Release configuration, React Native will (intentionally) throw
// an unhandled JavascriptException for an unhandled JavaScript error.
// This will effectively kill the application. In accord with the Web,
@ -57,34 +57,6 @@ export class App extends AbstractApp {
this._maybeDisableExceptionsManager();
}
/**
* Subscribe to notifications about activating URLs registered to be handled
* by this app.
*
* @inheritdoc
* @returns {void}
* @see https://facebook.github.io/react-native/docs/linking.html
*/
componentWillMount() {
super.componentWillMount();
Linking.addEventListener('url', this._onLinkingURL);
}
/**
* Unsubscribe from notifications about activating URLs registered to be
* handled by this app.
*
* @inheritdoc
* @returns {void}
* @see https://facebook.github.io/react-native/docs/linking.html
*/
componentWillUnmount() {
Linking.removeEventListener('url', this._onLinkingURL);
super.componentWillUnmount();
}
/**
* Attempts to disable the use of React Native
* {@link ExceptionsManager#handleException} on platforms and in
@ -122,20 +94,6 @@ export class App extends AbstractApp {
global.ErrorUtils.setGlobalHandler(newHandler);
}
}
/**
* Notified by React's Linking API that a specific URL registered to be
* handled by this App was activated.
*
* @param {Object} event - The details of the notification/event.
* @param {string} event.url - The URL registered to be handled by this App
* which was activated.
* @private
* @returns {void}
*/
_onLinkingURL({ url }) {
this._openURL(url);
}
}
/**

View File

@ -1,3 +1,13 @@
/**
* The type of Redux action which signals that {@link JitsiMeetJS.init()} was
* invoked and completed successfully.
*
* {
* type: LIB_LOAD_STORAGE_DONE
* }
*/
export const LIB_LOAD_STORAGE_DONE = Symbol('LIB_LOAD_STORAGE_DONE');
/**
* The type of Redux action which signals that {@link JitsiMeetJS} was disposed.
*

View File

@ -6,7 +6,10 @@ import { PARTICIPANT_LEFT } from '../participants';
import { MiddlewareRegistry } from '../redux';
import { disposeLib, initLib, setWebRTCReady } from './actions';
import { LIB_DID_INIT, LIB_INIT_ERROR } from './actionTypes';
import {
LIB_DID_INIT, LIB_INIT_ERROR,
LIB_LOAD_STORAGE_DONE
} from './actionTypes';
import { WEBRTC_NOT_READY, WEBRTC_NOT_SUPPORTED } from './constants';
/**
@ -23,6 +26,15 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case LIB_DID_INIT:
store.dispatch(setWebRTCReady(true));
if (window.localStorage.setLoadingCompleteCb) {
window.localStorage.setLoadingCompleteCb(() => {
store.dispatch({
type: LIB_LOAD_STORAGE_DONE
});
});
} else {
console.error('No window.localStorage.setLoadingCompleteCb');
}
break;
case LIB_INIT_ERROR:

View File

@ -33,6 +33,7 @@ export default class Storage {
if (typeof this._keyPrefix !== 'undefined') {
// Load all previously persisted data items from React Native's
// AsyncStorage.
console.info('LOAD STORAGE START');
AsyncStorage.getAllKeys().then((...getAllKeysCallbackArgs) => {
// XXX The keys argument of getAllKeys' callback may or may not
// be preceded by an error argument.
@ -61,11 +62,25 @@ export default class Storage {
this[key] = value;
}
}
console.info('LOAD STORAGE DONE');
this.loadingComplete = true;
if (typeof this._loadingCompleteCallback === 'function') {
console.info('LOAD STORAGE DONE - CALLING CALLBACK');
this._loadingCompleteCallback();
}
});
});
}
}
setLoadingCompleteCb(callback) {
this._loadingCompleteCallback = callback;
if (this.loadingComplete) {
callback();
}
}
/**
* Removes all keys from this storage.
*

View File

@ -0,0 +1,235 @@
import {
NativeModules,
NativeEventEmitter,
Platform
} from 'react-native';
const RNCallKit = NativeModules.RNCallKit;
/**
* Thin wrapper around Apple's CallKit functionality.
*
* In CallKit requests are performed via actions (either user or system started)
* and async events are reported via dedicated methods. This class exposes that
* functionality in the form of methods and events. One important thing to note
* is that even if an action is started by the system (because the user pressed
* the "end call" button in the CallKit view, for example) the event will be
* emitted in the same way as it would if the action originated from calling
* the "endCall" method in this class, for example.
*
* Emitted events:
* - performAnswerCallAction: The user pressed the answer button.
* - performEndCallAction: The call should be ended.
* - performSetMutedCallAction: The call muted state should change. The
* ancillary `data` object contains a `muted` attribute.
* - providerDidReset: The system has reset, all calls should be terminated.
* This event gets no associated data.
*
* All events get a `data` object with a `callUUID` property, unless stated
* otherwise.
*/
class CallKit extends NativeEventEmitter {
/**
* Initializes a new {@code CallKit} instance.
*/
constructor() {
super(RNCallKit);
this._setup = false;
}
/**
* Returns True if the current platform is supported, false otherwise. The
* supported platforms are: iOS >= 10.
*
* @private
* @returns {boolean}
*/
static isSupported() {
return Platform.OS === 'ios' && parseInt(Platform.Version, 10) >= 10;
}
/**
* Checks if CallKit was setup, and throws an exception in that case.
*
* @private
* @returns {void}
*/
_checkSetup() {
if (!this._setup) {
throw new Error('CallKit not initialized, call setup() first.');
}
}
/**
* Adds a listener for the given event.
*
* @param {string} event - Name of the event we are interested in.
* @param {Function} listener - Function which will be called when the
* desired event is emitted.
* @returns {void}
*/
addEventListener(event, listener) {
this._checkSetup();
if (!CallKit.isSupported()) {
return;
}
this.addListener(event, listener);
}
/**
* Notifies CallKit about an incoming call. This will display the system
* incoming call view.
*
* @param {string} uuid - Unique identifier for the call.
* @param {string} handle - Call handle in CallKit's terms. The room URL.
* @param {boolean} hasVideo - True if it's a video call, false otherwise.
* @returns {Promise}
*/
displayIncomingCall(uuid, handle, hasVideo = true) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.displayIncomingCall(uuid, handle, hasVideo);
}
/**
* Request CallKit to end the call.
*
* @param {string} uuid - Unique identifier for the call.
* @returns {Promise}
*/
endCall(uuid) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.endCall(uuid);
}
/**
* Removes a listener for the given event.
*
* @param {string} event - Name of the event we are no longer interested in.
* @param {Function} listener - Function which used to be called when the
* desired event was emitted.
* @returns {void}
*/
removeEventListener(event, listener) {
this._checkSetup();
if (!CallKit.isSupported()) {
return;
}
this.removeListener(event, listener);
}
/**
* Indicate CallKit that the outgoing call with the given UUID is now
* connected.
*
* @param {string} uuid - Unique identifier for the call.
* @returns {Promise}
*/
reportConnectedOutgoingCall(uuid) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.reportConnectedOutgoingCall(uuid);
}
/**
* Indicate CallKit that the call with the given UUID has failed.
*
* @param {string} uuid - Unique identifier for the call.
* @returns {Promise}
*/
reportCallFailed(uuid) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.reportCallFailed(uuid);
}
/**
* Tell CallKit about the audio muted state.
*
* @param {string} uuid - Unique identifier for the call.
* @param {boolean} muted - True if audio is muted, false otherwise.
* @returns {Promise}
*/
setMuted(uuid, muted) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.setMuted(uuid, muted);
}
/**
* Prepare / initialize CallKit. This method must be called before any
* other.
*
* @param {Object} options - Initialization options.
* @param {string} options.imageName - Image to be used in CallKit's
* application button..
* @param {string} options.ringtoneSound - Ringtone to be used for incoming
* calls.
* @returns {void}
*/
setup(options = {}) {
if (CallKit.isSupported()) {
options.appName = NativeModules.AppInfo.name;
RNCallKit.setup(options);
}
this._setup = true;
}
/**
* Indicate CallKit about a new outgoing call.
*
* @param {string} uuid - Unique identifier for the call.
* @param {string} handle - Call handle in CallKit's terms. The room URL in
* our case.
* @param {boolean} hasVideo - True if it's a video call, false otherwise.
* @returns {Promise}
*/
startCall(uuid, handle, hasVideo = true) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.startCall(uuid, handle, hasVideo);
}
/**
* Updates an ongoing call's parameters.
*
* @param {string} uuid - Unique identifier for the call.
* @param {Object} options - Object with properties which should be updated.
* @param {string} options.displayName - Display name for the caller.
* @param {boolean} options.hasVideo - True if the call has video, false
* otherwise.
* @returns {Promise}
*/
updateCall(uuid, options) {
this._checkSetup();
if (!CallKit.isSupported()) {
return Promise.resolve();
}
return RNCallKit.updateCall(uuid, options);
}
}
export default new CallKit();

View File

@ -0,0 +1,11 @@
/**
* The type of redux action to set the CallKit event listeners.
*
* {
* type: _SET_CALLKIT_LISTENERS,
* listeners: Map|null
* }
*
* @protected
*/
export const _SET_CALLKIT_LISTENERS = Symbol('_SET_CALLKIT_LISTENERS');

View File

@ -0,0 +1,2 @@
import './middleware';
import './reducer';

View File

@ -0,0 +1,194 @@
/* @flow */
import uuid from 'uuid';
import {
APP_WILL_MOUNT,
APP_WILL_UNMOUNT,
appNavigate
} from '../../app';
import {
CONFERENCE_FAILED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
CONFERENCE_JOINED
} from '../../base/conference';
import { getInviteURL } from '../../base/connection';
import {
SET_AUDIO_MUTED,
SET_VIDEO_MUTED,
isVideoMutedByAudioOnly,
setAudioMuted
} from '../../base/media';
import { MiddlewareRegistry, toState } from '../../base/redux';
import { _SET_CALLKIT_LISTENERS } from './actionTypes';
import CallKit from './CallKit';
/**
* Middleware that captures several system actions and hooks up CallKit.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const result = next(action);
switch (action.type) {
case _SET_CALLKIT_LISTENERS: {
const { listeners } = getState()['features/callkit'];
if (listeners) {
for (const [ event, listener ] of listeners) {
CallKit.removeEventListener(event, listener);
}
}
if (action.listeners) {
for (const [ event, listener ] of action.listeners) {
CallKit.addEventListener(event, listener);
}
}
break;
}
case APP_WILL_MOUNT: {
CallKit.setup(); // TODO: set app icon.
const listeners = new Map();
const callEndListener = data => {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID === data.callUUID) {
// We arrive here when a call is ended by the system, for
// for example when another incoming call is received and the
// user selects "End & Accept".
delete conference.callUUID;
dispatch(appNavigate(undefined));
}
};
listeners.set('performEndCallAction', callEndListener);
// Set the same listener for providerDidReset. According to the docs,
// when the system resets we should terminate all calls.
listeners.set('providerDidReset', callEndListener);
const setMutedListener = data => {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID === data.callUUID) {
// Break the loop. Audio can be muted both from the CallKit
// interface and from the Jitsi Meet interface. We must keep
// them in sync, but at some point the loop needs to be broken.
// We are doing it here, on the CallKit handler.
const { muted } = getState()['features/base/media'].audio;
if (muted !== data.muted) {
dispatch(setAudioMuted(Boolean(data.muted)));
}
}
};
listeners.set('performSetMutedCallAction', setMutedListener);
dispatch({
type: _SET_CALLKIT_LISTENERS,
listeners
});
break;
}
case APP_WILL_UNMOUNT:
dispatch({
type: _SET_CALLKIT_LISTENERS,
listeners: null
});
break;
case CONFERENCE_FAILED: {
const { callUUID } = action.conference;
if (callUUID) {
CallKit.reportCallFailed(callUUID);
}
break;
}
case CONFERENCE_LEFT: {
const { callUUID } = action.conference;
if (callUUID) {
CallKit.endCall(callUUID);
}
break;
}
case CONFERENCE_JOINED: {
const { callUUID } = action.conference;
if (callUUID) {
CallKit.reportConnectedOutgoingCall(callUUID);
}
break;
}
case CONFERENCE_WILL_JOIN: {
const conference = action.conference;
const url = getInviteURL(getState);
const hasVideo = !isVideoMutedByAudioOnly({ getState });
// When assigning the call UUID, do so in upper case, since iOS will
// return it upper cased.
conference.callUUID = uuid.v4().toUpperCase();
CallKit.startCall(conference.callUUID, url.toString(), hasVideo)
.then(() => {
const { room } = getState()['features/base/conference'];
CallKit.updateCall(conference.callUUID, { displayName: room });
});
break;
}
case SET_AUDIO_MUTED: {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID) {
CallKit.setMuted(conference.callUUID, action.muted);
}
break;
}
case SET_VIDEO_MUTED: {
const conference = getCurrentConference(getState);
if (conference && conference.callUUID) {
const hasVideo = !isVideoMutedByAudioOnly({ getState });
CallKit.updateCall(conference.callUUID, { hasVideo });
}
break;
}
}
return result;
});
/**
* Returns the currently active conference.
*
* @param {Function|Object} stateOrGetState - The redux state or redux's
* {@code getState} function.
* @returns {Conference|undefined}
*/
function getCurrentConference(stateOrGetState: Function | Object): ?Object {
const state = toState(stateOrGetState);
const { conference, joining } = state['features/base/conference'];
return conference || joining;
}

View File

@ -0,0 +1,17 @@
import { ReducerRegistry } from '../../base/redux';
import {
_SET_CALLKIT_LISTENERS
} from './actionTypes';
ReducerRegistry.register('features/callkit', (state = {}, action) => {
switch (action.type) {
case _SET_CALLKIT_LISTENERS:
return {
...state,
listeners: action.listeners
};
}
return state;
});

View File

@ -0,0 +1,23 @@
/**
* {
* type: ADD_RECENT_URL,
* recentURLs: Array
* }
*/
export const SET_RECENT_URLS = Symbol('SET_RECENT_URLS');
/**
* {
* type: SET_MIC_MUTED,
* micMuted: boolean
* }
*/
export const SET_MIC_MUTED = Symbol('SET_MIC_MUTED');
/**
* {
* type: SET_CONFERENCE_URL,
* conferenceURL: String?
* }
*/
export const SET_CONFERENCE_URL = Symbol('SET_CONFERENCE_URL');

View File

@ -0,0 +1,8 @@
import { SET_CONFERENCE_URL } from './actionTypes';
export function setConferenceURL(conferenceURL) {
return {
type: SET_CONFERENCE_URL,
conferenceURL
};
}

View File

@ -0,0 +1,2 @@
import './middleware';
import './reducer';

View File

@ -0,0 +1,202 @@
/* @flow */
import { Platform } from 'react-native';
import * as watch from 'react-native-watch-connectivity';
import { setConferenceURL } from './actions';
import {
SET_CONFERENCE_URL,
SET_MIC_MUTED,
SET_RECENT_URLS
} from './actionTypes';
import { ADD_RECENT_URL, LOADED_RECENT_URLS } from '../../recent';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT, appNavigate } from '../../app';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
JITSI_CONFERENCE_URL_KEY
} from '../../base/conference';
import { SET_AUDIO_MUTED, toggleAudioMuted } from '../../base/media';
import { MiddlewareRegistry } from '../../base/redux';
import { getInviteURL } from '../../base/connection';
/**
* Middleware that captures conference actions and sets the correct audio mode
* based on the type of conference. Audio-only conferences don't use the speaker
* by default, and video conferences do.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const result = next(action);
if (Platform.OS !== 'ios') {
return result;
}
switch (action.type) {
case APP_WILL_MOUNT: {
watch.subscribeToWatchState((err, watchState) => {
if (!err) {
console.log('watchState', watchState);
// FIXME that does not seem to help with the initial sync up
// if (watchState === 'Activated') {
// _updateApplicationContext(getState);
// }
} else {
console.log('ERROR getting watchState');
}
});
watch.subscribeToMessages((err, message) => {
if (err) {
console.log('ERROR getting watch message');
} else {
switch (message.command) {
case 'joinConference': {
const newConferenceURL = message.data;
const oldConferenceURL
= _getConferenceUrlFromBaseConf(getState);
console.info(`WATCH - JOIN URL: ${newConferenceURL}`);
if (oldConferenceURL === newConferenceURL) {
console.info('No need to navigate');
} else {
// Set conference URL early to avoid NULL being sent as
// part of other updates.
// FIXME check if we'd go back to NULL on join failure.
dispatch(setConferenceURL(newConferenceURL));
dispatch(appNavigate(newConferenceURL));
}
break;
}
case 'toggleMute':
console.info('WATCH - TOGGLE MUTED');
dispatch(toggleAudioMuted());
break;
case 'hangup':
console.info('WATCH - HANG UP');
dispatch(appNavigate(undefined));
break;
}
}
});
break;
}
case ADD_RECENT_URL:
case LOADED_RECENT_URLS: {
dispatch({
type: SET_RECENT_URLS,
recentURLs: getState()['features/recent'].entries
});
break;
}
case SET_AUDIO_MUTED: {
const { audio } = getState()['features/base/media'];
dispatch({
type: SET_MIC_MUTED,
micMuted: Boolean(audio.muted)
});
break;
}
case CONFERENCE_FAILED:
case CONFERENCE_WILL_LEAVE: {
const conferenceURL = _getConferenceUrlFromBaseConf(getState);
const watchConferenceURL = _getWatchConferenceURL(getState);
// This may not be a real failure
if (action.type === CONFERENCE_FAILED) {
const conference = getState()['features/base/conference'];
if (conference.authRequired || conference.passwordRequired) {
break;
}
}
// FIXME I have bad feelings about this logic, but it aims to fix
// problem with setting NULL temporarily when selecting new conference
// on the watch while still in the previous room. It will first emit
// CONFERENCE_WILL_LEVE, before joining the new room and we don't want
// to send NULL.
if (watchConferenceURL !== 'NULL'
&& watchConferenceURL !== conferenceURL) {
console.info(
'Ignored action',
action.type,
`possibly for the previous conference ?: ${conferenceURL}`);
} else if (action.type === CONFERENCE_WILL_LEAVE
&& conferenceURL === watchConferenceURL) {
dispatch(setConferenceURL('NULL'));
} else if (conferenceURL !== watchConferenceURL) {
dispatch(setConferenceURL(conferenceURL));
} else {
console.info(
'Did nothing on',
action.type,
conferenceURL,
watchConferenceURL);
}
break;
}
case CONFERENCE_WILL_JOIN:
case CONFERENCE_JOINED: {
// NOTE for some reason 'null' does not update context - must be string
const conferenceURL = _getConferenceUrlFromBaseConf(getState);
const oldConferenceURL = _getWatchConferenceURL(getState);
// NOTE Those updates are expensive!
if (conferenceURL !== oldConferenceURL) {
dispatch(setConferenceURL(conferenceURL));
}
break;
}
// Here list all actions that affect the watch OS application context.
// The reducer should form all those actions into our context structure.
case SET_CONFERENCE_URL:
case SET_MIC_MUTED:
case SET_RECENT_URLS: {
_updateApplicationContext(getState, action);
break;
}
case APP_WILL_UNMOUNT:
break;
}
return result;
});
function _getWatchConferenceURL(getState) {
const { conferenceURL } = getState()['features/mobile/watchos'];
return conferenceURL;
}
function _getConferenceUrlFromBaseConf(getState) {
// FIXME probably authRequired and paswordRequired should be included
// as well...
const { conference, joining, leaving }
= getState()['features/base/conference'];
const theConference = conference || joining || leaving;
const conferenceURLObj
= theConference && theConference[JITSI_CONFERENCE_URL_KEY];
// NOTE for some reason 'null' does not update context - must be string
return conferenceURLObj ? getInviteURL(conferenceURLObj) : 'NULL';
}
function _updateApplicationContext(getState, action) {
const context = getState()['features/mobile/watchos'];
console.info('UPDATING WATCH CONTEXT', context, action.type);
watch.updateApplicationContext(context);
}

View File

@ -0,0 +1,32 @@
import { ReducerRegistry, set } from '../../base/redux';
import {
SET_CONFERENCE_URL, SET_MIC_MUTED,
SET_RECENT_URLS
} from './actionTypes';
const INITIAL_STATE = {
// NOTE for some reason 'null' does not update context
conferenceURL: 'NULL',
micMuted: false,
recentURLs: []
};
/**
* Reduces the Redux actions of the feature features/recording.
*/
ReducerRegistry.register(
'features/mobile/watchos', (state = INITIAL_STATE, action) => {
switch (action.type) {
case SET_CONFERENCE_URL: {
return set(state, 'conferenceURL', action.conferenceURL);
}
case SET_MIC_MUTED: {
return set(state, 'micMuted', action.micMuted);
}
case SET_RECENT_URLS: {
return set(state, 'recentURLs', action.recentURLs);
}
default:
return state;
}
});

View File

@ -0,0 +1,18 @@
/**
* {
* type: ADD_RECENT_URL,
* roomURL: string,
* timestamp: Number
* }
*/
export const ADD_RECENT_URL = Symbol('ADD_RECENT_URL');
/**
*
* {
* type: LOADED_RECENT_URLS,
* entries: []
* }
*/
export const LOADED_RECENT_URLS = Symbol('LOADED_RECENT_URLS');

View File

@ -0,0 +1,4 @@
export * from './actionTypes';
import './middleware';
import './reducer';

View File

@ -0,0 +1,54 @@
/* @flow */
import { MiddlewareRegistry } from '../base/redux';
import { getInviteURL } from '../base/connection';
import { CONFERENCE_WILL_JOIN } from '../base/conference';
import { LIB_LOAD_STORAGE_DONE } from '../base/lib-jitsi-meet';
import {ADD_RECENT_URL, LOADED_RECENT_URLS} from './actionTypes';
/**
* Middleware that captures conference actions and sets the correct audio mode
* based on the type of conference. Audio-only conferences don't use the speaker
* by default, and video conferences do.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const result = next(action);
switch (action.type) {
case LIB_LOAD_STORAGE_DONE: {
// FIXME this appears to be called when a conference is left
let entries = [];
const recentURLs = window.localStorage.getItem('recentURLs');
if (recentURLs) {
console.info('FOUND STORED URLs', recentURLs);
try {
entries = JSON.parse(recentURLs);
} catch (error) {
console.error('Failed to parse recent URLS', error);
}
} else {
console.info('NO STORED URLs found');
}
dispatch({
type: LOADED_RECENT_URLS,
entries
});
break;
}
case CONFERENCE_WILL_JOIN: {
dispatch({
type: ADD_RECENT_URL,
roomURL: getInviteURL(getState),
timestamp: Date.now()
});
break;
}
}
return result;
});

View File

@ -0,0 +1,75 @@
import _ from 'lodash';
import { ReducerRegistry, set } from '../base/redux';
import { ADD_RECENT_URL, LOADED_RECENT_URLS } from './actionTypes';
const MAX_LENGTH = 25;
const INITIAL_STATE = {
entries: []
};
/**
* Reduces the Redux actions of the feature features/recording.
*/
ReducerRegistry.register('features/recent', (state = INITIAL_STATE, action) => {
switch (action.type) {
case LOADED_RECENT_URLS: {
// FIXME if that happens too late it may overwrite any recent URLs
const newState = set(state, 'entries', action.entries);
console.info('RECENT STATE ON LOAD: ', newState);
return newState;
}
case ADD_RECENT_URL: {
return _addRecentUrl(state, action);
}
default:
return state;
}
});
/**
* FIXME.
* @param state
* @param action
* @returns {Object}
* @private
*/
function _addRecentUrl(state, action) {
const { roomURL, timestamp } = action;
let existingIdx = -1;
const entries = _.cloneDeep(state.entries);
for (let i = 0; i < entries.length; i++) {
if (entries[i].roomURL === roomURL) {
existingIdx = i;
break;
}
}
if (existingIdx !== -1) {
console.info('DELETED ALREADY EXISTING', roomURL);
entries.splice(existingIdx, 1);
}
entries.unshift({
roomURL,
timestamp
});
if (entries.length > MAX_LENGTH) {
const removed = entries.pop();
console.info('SIZE LIMIT exceeded, removed:', removed);
}
const newState = set(state, 'entries', entries);
console.info('RECENT URLs', newState);
window.localStorage.setItem('recentURLs', JSON.stringify(entries));
return newState;
}

View File

@ -1,6 +1,6 @@
/* @flow */
import { Share } from 'react-native';
import { NativeModules, Share } from 'react-native';
import { MiddlewareRegistry } from '../base/redux';
@ -37,7 +37,7 @@ function _shareRoom(roomURL: string, dispatch: Function) {
// review before i18n was introduces in react/. However, I reviewed it
// afterwards. Translate the display/human-readable strings.
const message = `Click the following link to join the meeting: ${roomURL}`;
const title = 'Jitsi Meet Conference';
const title = `${NativeModules.AppInfo.name} Conference`;
const onFulfilled
= (shared: boolean) => dispatch(endShareRoom(roomURL, shared));