Compare commits
128 Commits
android-sd
...
android-sd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c506ae5fd | ||
|
|
54e8d2c1a8 | ||
|
|
d27a94b3b5 | ||
|
|
e1710eaa38 | ||
|
|
f4467206d7 | ||
|
|
97e8b31cee | ||
|
|
55218de779 | ||
|
|
714e0e045d | ||
|
|
0bc369afb4 | ||
|
|
f71ec55170 | ||
|
|
760885437a | ||
|
|
f77976b742 | ||
|
|
9e95e7cd97 | ||
|
|
9d94257e79 | ||
|
|
13cfd61c83 | ||
|
|
fa818bc386 | ||
|
|
a73a642c64 | ||
|
|
94b3f6410d | ||
|
|
3d30f6e9cd | ||
|
|
40c16f0bac | ||
|
|
a1db63a8c2 | ||
|
|
59a9c2d947 | ||
|
|
fc897b9bac | ||
|
|
96f013c549 | ||
|
|
742905e05a | ||
|
|
bde44a94e8 | ||
|
|
1786bfadce | ||
|
|
b2e840636a | ||
|
|
ddaa22048f | ||
|
|
3e77890387 | ||
|
|
1e39c12963 | ||
|
|
243fdba80f | ||
|
|
08c4933c1b | ||
|
|
d5e0dea469 | ||
|
|
033aa0dd6e | ||
|
|
803870ef8f | ||
|
|
bf67a4a675 | ||
|
|
ee2036a2a7 | ||
|
|
4c3ed190f3 | ||
|
|
a91b49c2c1 | ||
|
|
186ba70cb7 | ||
|
|
12c18657d5 | ||
|
|
9f8e7d4050 | ||
|
|
4cea7018f5 | ||
|
|
54a9b9199e | ||
|
|
4591b36c3e | ||
|
|
db862b5b3b | ||
|
|
70b864f00b | ||
|
|
73b6a7a134 | ||
|
|
8b5b112c6a | ||
|
|
2f7f9f24c4 | ||
|
|
1197c26529 | ||
|
|
6eb66b639e | ||
|
|
fa88db6897 | ||
|
|
64eb4b5609 | ||
|
|
ef2455caea | ||
|
|
ca11cbf6cc | ||
|
|
f15a2aea68 | ||
|
|
e6c3d7ded7 | ||
|
|
2861198251 | ||
|
|
226c0bb084 | ||
|
|
30c0bfc108 | ||
|
|
df50e7fa69 | ||
|
|
f85ac3ef91 | ||
|
|
e33b334307 | ||
|
|
ce6f7308ad | ||
|
|
f66478fa34 | ||
|
|
bf99051885 | ||
|
|
7234ca69c8 | ||
|
|
ae965877f3 | ||
|
|
ae3b70eb13 | ||
|
|
97e0303065 | ||
|
|
35ffbe1720 | ||
|
|
f7b92f65ca | ||
|
|
cf7b10d53d | ||
|
|
d798f93614 | ||
|
|
4ddfcaf584 | ||
|
|
431a221c63 | ||
|
|
477826089c | ||
|
|
a46369cf22 | ||
|
|
651791b8df | ||
|
|
09cc738219 | ||
|
|
6d8ec4d147 | ||
|
|
d65a068fdb | ||
|
|
cf23045f8d | ||
|
|
e47d2d13ce | ||
|
|
07b7f03aa7 | ||
|
|
7ce44f85ca | ||
|
|
41e0d782ce | ||
|
|
2a8fafdd36 | ||
|
|
faee1c139e | ||
|
|
eb644987ce | ||
|
|
de60a70daf | ||
|
|
2904dfa794 | ||
|
|
d51cf7c581 | ||
|
|
f25e6c6a5d | ||
|
|
5fb9422513 | ||
|
|
d01cfc8466 | ||
|
|
fa3888991f | ||
|
|
ded355a807 | ||
|
|
b655c8d54a | ||
|
|
42a6e6faaf | ||
|
|
c7954c284d | ||
|
|
251da1861a | ||
|
|
9712804040 | ||
|
|
fecbef0aff | ||
|
|
d65b71b584 | ||
|
|
579d291bca | ||
|
|
871026f4ba | ||
|
|
9a8a070c62 | ||
|
|
7cf4c7bd78 | ||
|
|
72a1def571 | ||
|
|
0dad99c3b7 | ||
|
|
840c0190c4 | ||
|
|
e0fdeea69b | ||
|
|
e3612929f8 | ||
|
|
70921bb6ef | ||
|
|
371ca4eef1 | ||
|
|
54fdb7066f | ||
|
|
85bcb0c757 | ||
|
|
d387cbe5bd | ||
|
|
1bc28e4904 | ||
|
|
cb3419ba2a | ||
|
|
a2f8e156da | ||
|
|
a4cf79c161 | ||
|
|
9352517705 | ||
|
|
47d5163c52 | ||
|
|
def22b01bb |
1
.gitignore
vendored
@@ -73,6 +73,7 @@ buck-out/
|
||||
# Build artifacts
|
||||
*.jsbundle
|
||||
*.framework
|
||||
android/app/debug
|
||||
android/app/release
|
||||
|
||||
# precommit-hook
|
||||
|
||||
@@ -28,6 +28,7 @@ import android.view.KeyEvent;
|
||||
import org.jitsi.meet.sdk.JitsiMeet;
|
||||
import org.jitsi.meet.sdk.JitsiMeetActivity;
|
||||
import org.jitsi.meet.sdk.JitsiMeetConferenceOptions;
|
||||
import org.jitsi.meet.sdk.JitsiMeetUserInfo;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.MalformedURLException;
|
||||
@@ -106,7 +107,7 @@ public class MainActivity extends JitsiMeetActivity {
|
||||
//
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE
|
||||
&& canRequestOverlayPermission()) {
|
||||
if (Settings.canDrawOverlays(this)) {
|
||||
|
||||
BIN
android/app/src/main/res/drawable-hdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 659 B |
BIN
android/app/src/main/res/drawable-mdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 379 B |
BIN
android/app/src/main/res/drawable-xhdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 960 B |
BIN
android/app/src/main/res/drawable-xxhdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/ic_notification.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
@@ -169,7 +169,7 @@ ext {
|
||||
glideVersion = "4.7.1" // keep in sync with react-native-fast-image
|
||||
|
||||
// Libre build
|
||||
libreBuild = (System.env.LIBRE_BUILD ?: "true").toBoolean()
|
||||
libreBuild = (System.env.LIBRE_BUILD ?: "false").toBoolean()
|
||||
}
|
||||
|
||||
// If Android SDK is not installed, accept its license so that it
|
||||
|
||||
@@ -18,4 +18,4 @@
|
||||
# org.gradle.parallel=true
|
||||
|
||||
appVersion=19.2.0
|
||||
sdkVersion=2.1.0
|
||||
sdkVersion=2.2.2
|
||||
|
||||
@@ -10,6 +10,7 @@ MVN_HTTP=0
|
||||
DEFAULT_SDK_VERSION=$(grep sdkVersion ${THIS_DIR}/../gradle.properties | cut -d"=" -f2)
|
||||
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
|
||||
RN_VERSION=$(jq -r '.dependencies."react-native"' ${THIS_DIR}/../../package.json)
|
||||
DO_GIT_TAG=${GIT_TAG:-0}
|
||||
|
||||
if [[ $THE_MVN_REPO == http* ]]; then
|
||||
MVN_HTTP=1
|
||||
@@ -64,13 +65,11 @@ pushd ${THIS_DIR}/../
|
||||
./gradlew clean assembleRelease publish
|
||||
popd
|
||||
|
||||
if [[ $MVN_HTTP == 0 ]]; then
|
||||
if [[ $DO_GIT_TAG == 1 ]]; then
|
||||
# The artifacts are now on the Maven repo, commit them
|
||||
pushd ${MVN_REPO_PATH}
|
||||
if [[ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" == "true" ]]; then
|
||||
git add -A .
|
||||
git commit -m "Jitsi Meet SDK + dependencies: ${SDK_VERSION}"
|
||||
fi
|
||||
git add -A .
|
||||
git commit -m "Jitsi Meet SDK + dependencies: ${SDK_VERSION}"
|
||||
popd
|
||||
|
||||
# Tag the release
|
||||
|
||||
@@ -14,6 +14,7 @@ if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then
|
||||
exit 2
|
||||
fi
|
||||
else
|
||||
adb reverse tcp:8081 tcp:8081
|
||||
CMD="${THIS_DIR}/../../node_modules/react-native/scripts/launchPackager.command"
|
||||
if [[ `uname` == "Darwin" ]]; then
|
||||
open -g "${CMD}" || echo "Can't start packager automatically"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
|
||||
<uses-feature
|
||||
android:glEsVersion="0x00020000"
|
||||
@@ -44,6 +45,8 @@
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -52,6 +52,17 @@ class AmplitudeModule
|
||||
Amplitude.getInstance(instanceName).initialize(getCurrentActivity(), apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user ID for an Amplitude instance.
|
||||
*
|
||||
* @param instanceName The name of the Amplitude instance.
|
||||
* @param userId The new value for the user ID.
|
||||
*/
|
||||
@ReactMethod
|
||||
public void setUserId(String instanceName, String userId) {
|
||||
Amplitude.getInstance(instanceName).setUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user properties for an Amplitude instance.
|
||||
*
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.facebook.react.bridge.ReadableMap;
|
||||
import com.rnimmersive.RNImmersiveModule;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -77,6 +78,15 @@ public abstract class BaseReactView<ListenerT>
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all registered React views.
|
||||
*
|
||||
* @return An {@link ArrayList} containing all views currently held by React.
|
||||
*/
|
||||
static ArrayList<BaseReactView> getViews() {
|
||||
return new ArrayList<>(views);
|
||||
}
|
||||
|
||||
/**
|
||||
* The unique identifier of this {@code BaseReactView} within the process
|
||||
* for the purposes of {@link ExternalAPIModule}. The name scope was
|
||||
|
||||
@@ -63,6 +63,16 @@ public class ConnectionService extends android.telecom.ConnectionService {
|
||||
static private final HashMap<String, Promise> startCallPromises
|
||||
= new HashMap<>();
|
||||
|
||||
/**
|
||||
* Aborts all ongoing connections. This is a last resort mechanism which forces all resources to
|
||||
* be freed on the system in case of fatal error.
|
||||
*/
|
||||
static void abortConnections() {
|
||||
for (ConnectionImpl connection: getConnections()) {
|
||||
connection.onAbort();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds {@link ConnectionImpl} to the list.
|
||||
*
|
||||
|
||||
@@ -67,12 +67,16 @@ class ExternalAPIModule
|
||||
*/
|
||||
@ReactMethod
|
||||
public void sendEvent(String name, ReadableMap data, String scope) {
|
||||
// Keep track of the current ongoing conference.
|
||||
OngoingConferenceTracker.getInstance().onExternalAPIEvent(name, data);
|
||||
|
||||
// The JavaScript App needs to provide uniquely identifying information
|
||||
// to the native ExternalAPI module so that the latter may match the
|
||||
// former to the native BaseReactView which hosts it.
|
||||
BaseReactView view = BaseReactView.findViewByExternalAPIScope(scope);
|
||||
|
||||
if (view != null) {
|
||||
Log.d(TAG, "Sending event: " + name + " with data: " + data);
|
||||
try {
|
||||
view.onExternalAPIEvent(name, data);
|
||||
} catch(Exception e) {
|
||||
|
||||
@@ -36,6 +36,15 @@ public class JitsiMeet {
|
||||
defaultConferenceOptions = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current conference URL as a string.
|
||||
*
|
||||
* @return the current conference URL.
|
||||
*/
|
||||
public static String getCurrentConference() {
|
||||
return OngoingConferenceTracker.getInstance().getCurrentConference();
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the default conference options as a {@link Bundle}.
|
||||
*
|
||||
|
||||
@@ -38,8 +38,8 @@ public class JitsiMeetActivity extends FragmentActivity
|
||||
|
||||
protected static final String TAG = JitsiMeetActivity.class.getSimpleName();
|
||||
|
||||
public static final String ACTION_JITSI_MEET_CONFERENCE = "org.jitsi.meet.CONFERENCE";
|
||||
public static final String JITSI_MEET_CONFERENCE_OPTIONS = "JitsiMeetConferenceOptions";
|
||||
private static final String ACTION_JITSI_MEET_CONFERENCE = "org.jitsi.meet.CONFERENCE";
|
||||
private static final String JITSI_MEET_CONFERENCE_OPTIONS = "JitsiMeetConferenceOptions";
|
||||
|
||||
// Helpers for starting the activity
|
||||
//
|
||||
@@ -71,6 +71,24 @@ public class JitsiMeetActivity extends FragmentActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
// Here we are trying to handle the following corner case: an application using the SDK
|
||||
// is using this Activity for displaying meetings, but there is another "main" Activity
|
||||
// with other content. If this Activity is "swiped out" from the recent list we will get
|
||||
// Activity#onDestroy() called without warning. At this point we can try to leave the
|
||||
// current meeting, but when our view is detached from React the JS <-> Native bridge won't
|
||||
// be operational so the external API won't be able to notify the native side that the
|
||||
// conference terminated. Thus, try our best to clean up.
|
||||
leave();
|
||||
if (AudioModeModule.useConnectionService()) {
|
||||
ConnectionService.abortConnections();
|
||||
}
|
||||
JitsiMeetOngoingConferenceService.abort(this);
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
leave();
|
||||
@@ -143,6 +161,11 @@ public class JitsiMeetActivity extends FragmentActivity
|
||||
// Activity lifecycle methods
|
||||
//
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
JitsiMeetActivityDelegate.onActivityResult(this, requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
JitsiMeetActivityDelegate.onBackPressed();
|
||||
@@ -150,6 +173,8 @@ public class JitsiMeetActivity extends FragmentActivity
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
|
||||
JitsiMeetConferenceOptions options;
|
||||
|
||||
if ((options = getConferenceOptions(intent)) != null) {
|
||||
@@ -184,6 +209,8 @@ public class JitsiMeetActivity extends FragmentActivity
|
||||
@Override
|
||||
public void onConferenceJoined(Map<String, Object> data) {
|
||||
Log.d(TAG, "Conference joined: " + data);
|
||||
// Launch the service for the ongoing notification.
|
||||
JitsiMeetOngoingConferenceService.launch(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -40,6 +40,10 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
* Room name.
|
||||
*/
|
||||
private String room;
|
||||
/**
|
||||
* Conference subject.
|
||||
*/
|
||||
private String subject;
|
||||
/**
|
||||
* JWT token used for authentication.
|
||||
*/
|
||||
@@ -50,6 +54,11 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
*/
|
||||
private Bundle colorScheme;
|
||||
|
||||
/**
|
||||
* Feature flags. See: https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/flags/constants.js
|
||||
*/
|
||||
private Bundle featureFlags;
|
||||
|
||||
/**
|
||||
* Set to {@code true} to join the conference with audio / video muted or to start in audio
|
||||
* only mode respectively.
|
||||
@@ -59,10 +68,9 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
private Boolean videoMuted;
|
||||
|
||||
/**
|
||||
* Set to {@code true} to enable the welcome page. Typically SDK users won't need this enabled
|
||||
* since the host application decides which meeting to join.
|
||||
* USer information, to be used when no token is specified.
|
||||
*/
|
||||
private Boolean welcomePageEnabled;
|
||||
private JitsiMeetUserInfo userInfo;
|
||||
|
||||
/**
|
||||
* Class used to build the immutable {@link JitsiMeetConferenceOptions} object.
|
||||
@@ -70,17 +78,20 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
public static class Builder {
|
||||
private URL serverURL;
|
||||
private String room;
|
||||
private String subject;
|
||||
private String token;
|
||||
|
||||
private Bundle colorScheme;
|
||||
private Bundle featureFlags;
|
||||
|
||||
private Boolean audioMuted;
|
||||
private Boolean audioOnly;
|
||||
private Boolean videoMuted;
|
||||
|
||||
private Boolean welcomePageEnabled;
|
||||
private JitsiMeetUserInfo userInfo;
|
||||
|
||||
public Builder() {
|
||||
featureFlags = new Bundle();
|
||||
}
|
||||
|
||||
/**\
|
||||
@@ -105,6 +116,17 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the conference subject.
|
||||
* @param subject - Subject for the conference.
|
||||
* @return - The {@link Builder} object itself so the method calls can be chained.
|
||||
*/
|
||||
public Builder setSubject(String subject) {
|
||||
this.subject = subject;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the JWT token to be used for authentication when joining a conference.
|
||||
* @param token - The JWT token to be used for authentication.
|
||||
@@ -170,7 +192,31 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
* @return - The {@link Builder} object itself so the method calls can be chained.
|
||||
*/
|
||||
public Builder setWelcomePageEnabled(boolean enabled) {
|
||||
this.welcomePageEnabled = enabled;
|
||||
this.featureFlags.putBoolean("welcomepage.enabled", enabled);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setFeatureFlag(String flag, boolean value) {
|
||||
this.featureFlags.putBoolean(flag, value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setFeatureFlag(String flag, String value) {
|
||||
this.featureFlags.putString(flag, value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setFeatureFlag(String flag, int value) {
|
||||
this.featureFlags.putInt(flag, value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUserInfo(JitsiMeetUserInfo userInfo) {
|
||||
this.userInfo = userInfo;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -185,12 +231,14 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
|
||||
options.serverURL = this.serverURL;
|
||||
options.room = this.room;
|
||||
options.subject = this.subject;
|
||||
options.token = this.token;
|
||||
options.colorScheme = this.colorScheme;
|
||||
options.featureFlags = this.featureFlags;
|
||||
options.audioMuted = this.audioMuted;
|
||||
options.audioOnly = this.audioOnly;
|
||||
options.videoMuted = this.videoMuted;
|
||||
options.welcomePageEnabled = this.welcomePageEnabled;
|
||||
options.userInfo = this.userInfo;
|
||||
|
||||
return options;
|
||||
}
|
||||
@@ -201,32 +249,33 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
|
||||
private JitsiMeetConferenceOptions(Parcel in) {
|
||||
room = in.readString();
|
||||
subject = in.readString();
|
||||
token = in.readString();
|
||||
colorScheme = in.readBundle();
|
||||
featureFlags = in.readBundle();
|
||||
userInfo = new JitsiMeetUserInfo(in.readBundle());
|
||||
byte tmpAudioMuted = in.readByte();
|
||||
audioMuted = tmpAudioMuted == 0 ? null : tmpAudioMuted == 1;
|
||||
byte tmpAudioOnly = in.readByte();
|
||||
audioOnly = tmpAudioOnly == 0 ? null : tmpAudioOnly == 1;
|
||||
byte tmpVideoMuted = in.readByte();
|
||||
videoMuted = tmpVideoMuted == 0 ? null : tmpVideoMuted == 1;
|
||||
byte tmpWelcomePageEnabled = in.readByte();
|
||||
welcomePageEnabled = tmpWelcomePageEnabled == 0 ? null : tmpWelcomePageEnabled == 1;
|
||||
}
|
||||
|
||||
Bundle asProps() {
|
||||
Bundle props = new Bundle();
|
||||
|
||||
// Android always has the PiP flag set by default.
|
||||
if (!featureFlags.containsKey("pip.enabled")) {
|
||||
featureFlags.putBoolean("pip.enabled", true);
|
||||
}
|
||||
|
||||
props.putBundle("flags", featureFlags);
|
||||
|
||||
if (colorScheme != null) {
|
||||
props.putBundle("colorScheme", colorScheme);
|
||||
}
|
||||
|
||||
if (welcomePageEnabled != null) {
|
||||
props.putBoolean("welcomePageEnabled", welcomePageEnabled);
|
||||
}
|
||||
|
||||
// TODO: get rid of this.
|
||||
props.putBoolean("pictureInPictureEnabled", true);
|
||||
|
||||
Bundle config = new Bundle();
|
||||
|
||||
if (audioMuted != null) {
|
||||
@@ -238,6 +287,9 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
if (videoMuted != null) {
|
||||
config.putBoolean("startWithVideoMuted", videoMuted);
|
||||
}
|
||||
if (subject != null) {
|
||||
config.putString("subject", subject);
|
||||
}
|
||||
|
||||
Bundle urlProps = new Bundle();
|
||||
|
||||
@@ -257,6 +309,10 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
urlProps.putString("jwt", token);
|
||||
}
|
||||
|
||||
if (token == null && userInfo != null) {
|
||||
props.putBundle("userInfo", userInfo.asBundle());
|
||||
}
|
||||
|
||||
urlProps.putBundle("config", config);
|
||||
props.putBundle("url", urlProps);
|
||||
|
||||
@@ -281,12 +337,14 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(room);
|
||||
dest.writeString(subject);
|
||||
dest.writeString(token);
|
||||
dest.writeBundle(colorScheme);
|
||||
dest.writeBundle(featureFlags);
|
||||
dest.writeBundle(userInfo != null ? userInfo.asBundle() : new Bundle());
|
||||
dest.writeByte((byte) (audioMuted == null ? 0 : audioMuted ? 1 : 2));
|
||||
dest.writeByte((byte) (audioOnly == null ? 0 : audioOnly ? 1 : 2));
|
||||
dest.writeByte((byte) (videoMuted == null ? 0 : videoMuted ? 1 : 2));
|
||||
dest.writeByte((byte) (welcomePageEnabled == null ? 0 : welcomePageEnabled ? 1 : 2));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,8 +26,6 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* Base {@link Fragment} for applications integrating Jitsi Meet at a higher level. It
|
||||
* contains all the required wiring between the {@code JitsiMeetView} and
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
|
||||
/**
|
||||
* This class implements an Android {@link Service}, a foreground one specifically, and it's
|
||||
* responsible for presenting an ongoing notification when a conference is in progress.
|
||||
* The service will help keep the app running while in the background.
|
||||
*
|
||||
* See: https://developer.android.com/guide/components/services
|
||||
*/
|
||||
public class JitsiMeetOngoingConferenceService extends Service
|
||||
implements OngoingConferenceTracker.OngoingConferenceListener {
|
||||
private static final String TAG = JitsiMeetOngoingConferenceService.class.getSimpleName();
|
||||
|
||||
static final class Actions {
|
||||
static final String START = TAG + ":START";
|
||||
static final String HANGUP = TAG + ":HANGUP";
|
||||
}
|
||||
|
||||
static void launch(Context context) {
|
||||
OngoingNotification.createOngoingConferenceNotificationChannel();
|
||||
|
||||
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
|
||||
intent.setAction(Actions.START);
|
||||
|
||||
ComponentName componentName = context.startService(intent);
|
||||
if (componentName == null) {
|
||||
Log.w(TAG, "Ongoing conference service not started");
|
||||
}
|
||||
}
|
||||
|
||||
static void abort(Context context) {
|
||||
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
|
||||
context.stopService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
OngoingConferenceTracker.getInstance().addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
OngoingConferenceTracker.getInstance().removeListener(this);
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
final String action = intent.getAction();
|
||||
if (action.equals(Actions.START)) {
|
||||
Notification notification = OngoingNotification.buildOngoingConferenceNotification();
|
||||
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
|
||||
Log.i(TAG, "Service started");
|
||||
} else if (action.equals(Actions.HANGUP)) {
|
||||
Log.i(TAG, "Hangup requested");
|
||||
// Abort all ongoing calls
|
||||
if (AudioModeModule.useConnectionService()) {
|
||||
ConnectionService.abortConnections();
|
||||
}
|
||||
stopSelf();
|
||||
} else {
|
||||
Log.w(TAG, "Unknown action received: " + action);
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCurrentConferenceChanged(String conferenceUrl) {
|
||||
if (conferenceUrl == null) {
|
||||
stopSelf();
|
||||
Log.i(TAG, "Service stopped");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright @ 2018-present 8x8, Inc.
|
||||
* Copyright @ 2017-2018 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.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
class JitsiMeetUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
|
||||
private final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;
|
||||
|
||||
public static void register() {
|
||||
Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
|
||||
JitsiMeetUncaughtExceptionHandler uncaughtExceptionHandler
|
||||
= new JitsiMeetUncaughtExceptionHandler(defaultUncaughtExceptionHandler);
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
|
||||
}
|
||||
|
||||
private JitsiMeetUncaughtExceptionHandler(Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler) {
|
||||
this.defaultUncaughtExceptionHandler = defaultUncaughtExceptionHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
Log.e(this.getClass().getSimpleName(), "FATAL ERROR", e);
|
||||
|
||||
// Abort all ConnectionService ongoing calls
|
||||
if (AudioModeModule.useConnectionService()) {
|
||||
ConnectionService.abortConnections();
|
||||
}
|
||||
|
||||
if (defaultUncaughtExceptionHandler != null) {
|
||||
defaultUncaughtExceptionHandler.uncaughtException(t, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
/**
|
||||
* This class represents user information to be passed to {@link JitsiMeetConferenceOptions} for
|
||||
* identifying a user.
|
||||
*/
|
||||
public class JitsiMeetUserInfo {
|
||||
/**
|
||||
* User's display name.
|
||||
*/
|
||||
private String displayName;
|
||||
|
||||
/**
|
||||
* User's email address.
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* User's avatar URL.
|
||||
*/
|
||||
private URL avatar;
|
||||
|
||||
public JitsiMeetUserInfo() {}
|
||||
|
||||
public JitsiMeetUserInfo(Bundle b) {
|
||||
super();
|
||||
|
||||
if (b.containsKey("displayName")) {
|
||||
displayName = b.getString("displayName");
|
||||
}
|
||||
|
||||
if (b.containsKey("email")) {
|
||||
email = b.getString("email");
|
||||
}
|
||||
|
||||
if (b.containsKey("avatarURL")) {
|
||||
String avatarURL = b.getString("avatarURL");
|
||||
try {
|
||||
avatar = new URL(avatarURL);
|
||||
} catch (MalformedURLException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public URL getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public void setAvatar(URL avatar) {
|
||||
this.avatar = avatar;
|
||||
}
|
||||
|
||||
Bundle asBundle() {
|
||||
Bundle b = new Bundle();
|
||||
|
||||
if (displayName != null) {
|
||||
b.putString("displayName", displayName);
|
||||
}
|
||||
|
||||
if (email != null) {
|
||||
b.putString("email", email);
|
||||
}
|
||||
|
||||
if (avatar != null) {
|
||||
b.putString("avatarURL", avatar.toString());
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,8 @@ import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener> {
|
||||
public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
|
||||
implements OngoingConferenceTracker.OngoingConferenceListener {
|
||||
|
||||
/**
|
||||
* The {@code Method}s of {@code JitsiMeetViewListener} by event name i.e.
|
||||
@@ -106,6 +107,14 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener> {
|
||||
if (!(context instanceof JitsiMeetActivityInterface)) {
|
||||
throw new RuntimeException("Enclosing Activity must implement JitsiMeetActivityInterface");
|
||||
}
|
||||
|
||||
OngoingConferenceTracker.getInstance().addListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
OngoingConferenceTracker.getInstance().removeListener(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,27 +182,17 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener> {
|
||||
}
|
||||
|
||||
/**
|
||||
* The internal processing for the URL of the current conference set on the
|
||||
* associated {@link JitsiMeetView}.
|
||||
*
|
||||
* @param eventName the name of the external API event to be processed
|
||||
* @param eventData the details/specifics of the event to process determined
|
||||
* by/associated with the specified {@code eventName}.
|
||||
* Handler for {@link OngoingConferenceTracker} events.
|
||||
* @param conferenceUrl
|
||||
*/
|
||||
private void maybeSetViewURL(String eventName, ReadableMap eventData) {
|
||||
String url = eventData.getString("url");
|
||||
|
||||
switch(eventName) {
|
||||
case "CONFERENCE_WILL_JOIN":
|
||||
this.url = url;
|
||||
break;
|
||||
|
||||
case "CONFERENCE_TERMINATED":
|
||||
if (url != null && url.equals(this.url)) {
|
||||
this.url = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@Override
|
||||
public void onCurrentConferenceChanged(String conferenceUrl) {
|
||||
// This property was introduced in order to address
|
||||
// an exception in the Picture-in-Picture functionality which arose
|
||||
// because of delays related to bridging between JavaScript and Java. To
|
||||
// reduce these delays do not wait for the call to be transferred to the
|
||||
// UI thread.
|
||||
this.url = conferenceUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,13 +204,6 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener> {
|
||||
*/
|
||||
@Override
|
||||
protected void onExternalAPIEvent(String name, ReadableMap data) {
|
||||
// XXX The JitsiMeetView property URL was introduced in order to address
|
||||
// an exception in the Picture-in-Picture functionality which arose
|
||||
// because of delays related to bridging between JavaScript and Java. To
|
||||
// reduce these delays do not wait for the call to be transferred to the
|
||||
// UI thread.
|
||||
maybeSetViewURL(name, data);
|
||||
|
||||
onExternalAPIEvent(LISTENER_METHODS, name, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
||||
|
||||
/**
|
||||
* Helper class to keep track of what the current conference is.
|
||||
*/
|
||||
class OngoingConferenceTracker {
|
||||
private static final OngoingConferenceTracker instance = new OngoingConferenceTracker();
|
||||
|
||||
private static final String CONFERENCE_WILL_JOIN = "CONFERENCE_WILL_JOIN";
|
||||
private static final String CONFERENCE_TERMINATED = "CONFERENCE_TERMINATED";
|
||||
|
||||
private final Collection<OngoingConferenceListener> listeners =
|
||||
Collections.synchronizedSet(new HashSet<OngoingConferenceListener>());
|
||||
private String currentConference;
|
||||
|
||||
public OngoingConferenceTracker() {
|
||||
}
|
||||
|
||||
public static OngoingConferenceTracker getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current active conference URL.
|
||||
*
|
||||
* @return - The current conference URL as a String.
|
||||
*/
|
||||
synchronized String getCurrentConference() {
|
||||
return currentConference;
|
||||
}
|
||||
|
||||
synchronized void onExternalAPIEvent(String name, ReadableMap data) {
|
||||
if (!data.hasKey("url")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String url = data.getString("url");
|
||||
if (url == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(name) {
|
||||
case CONFERENCE_WILL_JOIN:
|
||||
currentConference = url;
|
||||
updateListeners();
|
||||
break;
|
||||
|
||||
case CONFERENCE_TERMINATED:
|
||||
if (url.equals(currentConference)) {
|
||||
currentConference = null;
|
||||
updateListeners();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void addListener(OngoingConferenceListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
void removeListener(OngoingConferenceListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
private void updateListeners() {
|
||||
synchronized (listeners) {
|
||||
for (OngoingConferenceListener listener : listeners) {
|
||||
listener.onCurrentConferenceChanged(currentConference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface OngoingConferenceListener {
|
||||
void onCurrentConferenceChanged(String conferenceUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
|
||||
/**
|
||||
* Helper class for creating the ongoing notification which is used with
|
||||
* {@link JitsiMeetOngoingConferenceService}. It allows the user to easily get back to the app
|
||||
* and to hangup from within the notification itself.
|
||||
*/
|
||||
class OngoingNotification {
|
||||
private static final String TAG = OngoingNotification.class.getSimpleName();
|
||||
|
||||
private static final String CHANNEL_ID = "JitsiNotificationChannel";
|
||||
private static final String CHANNEL_NAME = "Ongoing Conference Notifications";
|
||||
|
||||
static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
|
||||
|
||||
|
||||
static void createOngoingConferenceNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = ReactInstanceManagerHolder.getCurrentActivity();
|
||||
if (context == null) {
|
||||
Log.w(TAG, "Cannot create notification channel: no current context");
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationManager notificationManager
|
||||
= (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
NotificationChannel channel
|
||||
= notificationManager.getNotificationChannel(CHANNEL_ID);
|
||||
if (channel != null) {
|
||||
// The channel was already created, no need to do it again.
|
||||
return;
|
||||
}
|
||||
|
||||
channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel.enableLights(false);
|
||||
channel.enableVibration(false);
|
||||
channel.setShowBadge(false);
|
||||
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
static Notification buildOngoingConferenceNotification() {
|
||||
Context context = ReactInstanceManagerHolder.getCurrentActivity();
|
||||
if (context == null) {
|
||||
Log.w(TAG, "Cannot create notification: no current context");
|
||||
return null;
|
||||
}
|
||||
|
||||
Intent notificationIntent = new Intent(context, context.getClass());
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
|
||||
|
||||
NotificationCompat.Builder builder;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
builder = new NotificationCompat.Builder(context, CHANNEL_ID);
|
||||
} else {
|
||||
builder = new NotificationCompat.Builder(context);
|
||||
}
|
||||
|
||||
builder
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.setContentTitle(context.getString(R.string.ongoing_notification_title))
|
||||
.setContentText(context.getString(R.string.ongoing_notification_text))
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.setAutoCancel(false)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setUsesChronometer(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));
|
||||
|
||||
// Add a "hang-up" action only if we are using ConnectionService.
|
||||
if (AudioModeModule.useConnectionService()) {
|
||||
Intent hangupIntent = new Intent(context, JitsiMeetOngoingConferenceService.class);
|
||||
hangupIntent.setAction(JitsiMeetOngoingConferenceService.Actions.HANGUP);
|
||||
PendingIntent hangupPendingIntent
|
||||
= PendingIntent.getService(context, 0, hangupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
NotificationCompat.Action hangupAction = new NotificationCompat.Action(0, "Hang up", hangupPendingIntent);
|
||||
|
||||
builder.addAction(hangupAction);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
@@ -66,7 +67,7 @@ class ReactInstanceManagerHolder {
|
||||
}
|
||||
|
||||
try {
|
||||
Class<?> amplitudeModuleClass = Class.forName("AmplitudeModule");
|
||||
Class<?> amplitudeModuleClass = Class.forName("org.jitsi.meet.sdk.AmplitudeModule");
|
||||
Constructor constructor = amplitudeModuleClass.getConstructor(ReactApplicationContext.class);
|
||||
nativeModules.add((NativeModule)constructor.newInstance(reactContext));
|
||||
} catch (Exception e) {
|
||||
@@ -120,6 +121,18 @@ class ReactInstanceManagerHolder {
|
||||
? reactContext.getNativeModule(nativeModuleClass) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current {@link Activity} linked to React Native.
|
||||
*
|
||||
* @return An activity attached to React Native.
|
||||
*/
|
||||
static Activity getCurrentActivity() {
|
||||
ReactContext reactContext
|
||||
= reactInstanceManager != null
|
||||
? reactInstanceManager.getCurrentReactContext() : null;
|
||||
return reactContext != null ? reactContext.getCurrentActivity() : null;
|
||||
}
|
||||
|
||||
static ReactInstanceManager getReactInstanceManager() {
|
||||
return reactInstanceManager;
|
||||
}
|
||||
@@ -182,5 +195,8 @@ class ReactInstanceManagerHolder {
|
||||
if (devSettings != null) {
|
||||
devSettings.setBundleDeltasEnabled(false);
|
||||
}
|
||||
|
||||
// Register our uncaught exception handler.
|
||||
JitsiMeetUncaughtExceptionHandler.register();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<resources>
|
||||
<string name="app_name">Jitsi Meet SDK</string>
|
||||
<string name="dropbox_app_key"></string>
|
||||
<string name="ongoing_notification_title">Ongoing meeting</string>
|
||||
<string name="ongoing_notification_text">You are currently in a meeting. Tap to return to it.</string>
|
||||
</resources>
|
||||
|
||||
@@ -16,6 +16,7 @@ import * as JitsiMeetConferenceEvents from './ConferenceEvents';
|
||||
|
||||
import {
|
||||
createDeviceChangedEvent,
|
||||
createStartSilentEvent,
|
||||
createScreenSharingEvent,
|
||||
createStreamSwitchDelayEvent,
|
||||
createTrackMutedEvent,
|
||||
@@ -51,6 +52,8 @@ import {
|
||||
import {
|
||||
checkAndNotifyForNewDevice,
|
||||
getAvailableDevices,
|
||||
notifyCameraError,
|
||||
notifyMicError,
|
||||
setAudioOutputDeviceId,
|
||||
updateDeviceList
|
||||
} from './react/features/base/devices';
|
||||
@@ -76,13 +79,14 @@ import {
|
||||
import { showNotification } from './react/features/notifications';
|
||||
import {
|
||||
dominantSpeakerChanged,
|
||||
getAvatarURLByParticipantId,
|
||||
getLocalParticipant,
|
||||
getNormalizedDisplayName,
|
||||
getParticipantById,
|
||||
localParticipantConnectionStatusChanged,
|
||||
localParticipantRoleChanged,
|
||||
participantConnectionStatusChanged,
|
||||
participantKicked,
|
||||
participantMutedUs,
|
||||
participantPresenceChanged,
|
||||
participantRoleChanged,
|
||||
participantUpdated
|
||||
@@ -100,6 +104,7 @@ import {
|
||||
getLocationContextRoot,
|
||||
getJitsiMeetGlobalNS
|
||||
} from './react/features/base/util';
|
||||
import { notifyKickedOut } from './react/features/conference';
|
||||
import { addMessage } from './react/features/chat';
|
||||
import { showDesktopPicker } from './react/features/desktop-picker';
|
||||
import { appendSuffix } from './react/features/display-name';
|
||||
@@ -373,13 +378,6 @@ class ConferenceConnector {
|
||||
APP.UI.notifyGracefulShutdown();
|
||||
break;
|
||||
|
||||
case JitsiConferenceErrors.JINGLE_FATAL_ERROR: {
|
||||
const [ error ] = params;
|
||||
|
||||
APP.UI.notifyInternalError(error);
|
||||
break;
|
||||
}
|
||||
|
||||
case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
|
||||
const [ reason ] = params;
|
||||
|
||||
@@ -401,6 +399,7 @@ class ConferenceConnector {
|
||||
|
||||
case JitsiConferenceErrors.FOCUS_LEFT:
|
||||
case JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
|
||||
case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
|
||||
APP.store.dispatch(conferenceWillLeave(room));
|
||||
|
||||
// FIXME the conference should be stopped by the library and not by
|
||||
@@ -486,10 +485,13 @@ class ConferenceConnector {
|
||||
* call in hangup() to resolve when all operations are finished.
|
||||
*/
|
||||
function disconnect() {
|
||||
connection.disconnect();
|
||||
APP.API.notifyConferenceLeft(APP.conference.roomName);
|
||||
const onDisconnected = () => {
|
||||
APP.API.notifyConferenceLeft(APP.conference.roomName);
|
||||
|
||||
return Promise.resolve();
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
return connection.disconnect().then(onDisconnected, onDisconnected);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -694,13 +696,14 @@ export default {
|
||||
// If both requests for 'audio' + 'video' and 'audio'
|
||||
// only failed, we assume that there are some problems
|
||||
// with user's microphone and show corresponding dialog.
|
||||
APP.UI.showMicErrorNotification(audioOnlyError);
|
||||
APP.UI.showCameraErrorNotification(videoOnlyError);
|
||||
APP.store.dispatch(notifyMicError(audioOnlyError));
|
||||
APP.store.dispatch(notifyCameraError(videoOnlyError));
|
||||
} else {
|
||||
// If request for 'audio' + 'video' failed, but request
|
||||
// for 'audio' only was OK, we assume that we had
|
||||
// problems with camera and show corresponding dialog.
|
||||
APP.UI.showCameraErrorNotification(audioAndVideoError);
|
||||
APP.store.dispatch(
|
||||
notifyCameraError(audioAndVideoError));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -723,6 +726,7 @@ export default {
|
||||
// based on preferred devices, loose label matching can be done in
|
||||
// cases where the exact ID match is no longer available, such as
|
||||
// when the camera device has switched USB ports.
|
||||
// when in startSilent mode we want to start with audio muted
|
||||
this._initDeviceList()
|
||||
.catch(error => logger.warn(
|
||||
'initial device list initialization failed', error))
|
||||
@@ -730,7 +734,7 @@ export default {
|
||||
options.roomName, {
|
||||
startAudioOnly: config.startAudioOnly,
|
||||
startScreenSharing: config.startScreenSharing,
|
||||
startWithAudioMuted: config.startWithAudioMuted,
|
||||
startWithAudioMuted: config.startWithAudioMuted || config.startSilent,
|
||||
startWithVideoMuted: config.startWithVideoMuted
|
||||
}))
|
||||
.then(([ tracks, con ]) => {
|
||||
@@ -786,6 +790,14 @@ export default {
|
||||
this.recorder = new Recorder();
|
||||
}
|
||||
|
||||
if (config.startSilent) {
|
||||
sendAnalytics(createStartSilentEvent());
|
||||
APP.store.dispatch(showNotification({
|
||||
descriptionKey: 'notify.startSilentDescription',
|
||||
titleKey: 'notify.startSilentTitle'
|
||||
}));
|
||||
}
|
||||
|
||||
// XXX The API will take care of disconnecting from the XMPP
|
||||
// server (and, thus, leaving the room) on unload.
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -839,7 +851,7 @@ export default {
|
||||
|
||||
if (!this.localAudio && !mute) {
|
||||
const maybeShowErrorDialog = error => {
|
||||
showUI && APP.UI.showMicErrorNotification(error);
|
||||
showUI && APP.store.dispatch(notifyMicError(error));
|
||||
};
|
||||
|
||||
createLocalTracksF({ devices: [ 'audio' ] }, false)
|
||||
@@ -902,7 +914,7 @@ export default {
|
||||
|
||||
if (!this.localVideo && !mute) {
|
||||
const maybeShowErrorDialog = error => {
|
||||
showUI && APP.UI.showCameraErrorNotification(error);
|
||||
showUI && APP.store.dispatch(notifyCameraError(error));
|
||||
};
|
||||
|
||||
// Try to create local video if there wasn't any.
|
||||
@@ -1842,6 +1854,12 @@ export default {
|
||||
APP.UI.setAudioLevel(id, newLvl);
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (_, participantThatMutedUs) => {
|
||||
if (participantThatMutedUs) {
|
||||
APP.store.dispatch(participantMutedUs(participantThatMutedUs));
|
||||
}
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.TALK_WHILE_MUTED, () => {
|
||||
APP.UI.showToolbar(6000);
|
||||
});
|
||||
@@ -1942,13 +1960,17 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.KICKED, () => {
|
||||
room.on(JitsiConferenceEvents.KICKED, participant => {
|
||||
APP.UI.hideStats();
|
||||
APP.UI.notifyKicked();
|
||||
APP.store.dispatch(notifyKickedOut(participant));
|
||||
|
||||
// FIXME close
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
|
||||
APP.store.dispatch(participantKicked(kicker, kicked));
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.SUSPEND_DETECTED, () => {
|
||||
APP.store.dispatch(suspendDetected());
|
||||
|
||||
@@ -1976,6 +1998,8 @@ export default {
|
||||
this.localAudio.dispose();
|
||||
this.localAudio = null;
|
||||
}
|
||||
|
||||
APP.API.notifySuspendDetected();
|
||||
});
|
||||
|
||||
APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => {
|
||||
@@ -2109,7 +2133,7 @@ export default {
|
||||
this._updateVideoDeviceId();
|
||||
})
|
||||
.catch(err => {
|
||||
APP.UI.showCameraErrorNotification(err);
|
||||
APP.store.dispatch(notifyCameraError(err));
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -2142,7 +2166,7 @@ export default {
|
||||
this._updateAudioDeviceId();
|
||||
})
|
||||
.catch(err => {
|
||||
APP.UI.showMicErrorNotification(err);
|
||||
APP.store.dispatch(notifyMicError(err));
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -2244,7 +2268,8 @@ export default {
|
||||
APP.keyboardshortcut.init();
|
||||
|
||||
if (config.requireDisplayName
|
||||
&& !APP.conference.getLocalDisplayName()) {
|
||||
&& !APP.conference.getLocalDisplayName()
|
||||
&& !this._room.isHidden()) {
|
||||
APP.UI.promptDisplayName();
|
||||
}
|
||||
|
||||
@@ -2254,18 +2279,6 @@ export default {
|
||||
= APP.store.getState()['features/base/settings'].displayName;
|
||||
|
||||
APP.UI.changeDisplayName('localVideoContainer', displayName);
|
||||
APP.API.notifyConferenceJoined(
|
||||
this.roomName,
|
||||
this._room.myUserId(),
|
||||
{
|
||||
displayName,
|
||||
formattedDisplayName: appendSuffix(
|
||||
displayName,
|
||||
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME),
|
||||
avatarURL: getAvatarURLByParticipantId(
|
||||
APP.store.getState(), this._room.myUserId())
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -2600,8 +2613,7 @@ export default {
|
||||
leaveRoomAndDisconnect() {
|
||||
APP.store.dispatch(conferenceWillLeave(room));
|
||||
|
||||
return room.leave()
|
||||
.then(disconnect, disconnect);
|
||||
return room.leave().then(disconnect, disconnect);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -2730,14 +2742,6 @@ export default {
|
||||
displayName: formattedNickname
|
||||
}));
|
||||
|
||||
APP.API.notifyDisplayNameChanged(id, {
|
||||
displayName: formattedNickname,
|
||||
formattedDisplayName:
|
||||
appendSuffix(
|
||||
formattedNickname,
|
||||
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME)
|
||||
});
|
||||
|
||||
if (room) {
|
||||
APP.UI.changeDisplayName(id, formattedNickname);
|
||||
}
|
||||
|
||||
@@ -90,6 +90,10 @@ var config = {
|
||||
// applied locally. FIXME: having these 2 options is confusing.
|
||||
// startWithAudioMuted: false,
|
||||
|
||||
// Enabling it (with #params) will disable local audio output of remote
|
||||
// participants and to enable it back a reload is needed.
|
||||
// startSilent: false
|
||||
|
||||
// Video
|
||||
|
||||
// Sets the preferred resolution (height) for local video. Defaults to 720.
|
||||
@@ -414,6 +418,10 @@ var config = {
|
||||
// use only.
|
||||
// _desktopSharingSourceDevice: 'sample-id-or-label'
|
||||
|
||||
// A property to disable the right click context menu for localVideo
|
||||
// the menu has option to flip the locally seen video for local presentations
|
||||
// disableLocalVideoFlip: false
|
||||
|
||||
// List of undocumented settings used in jitsi-meet
|
||||
/**
|
||||
_immediateReloadThreshold
|
||||
@@ -428,7 +436,6 @@ var config = {
|
||||
dialOutCodesUrl
|
||||
disableRemoteControl
|
||||
displayJids
|
||||
enableLocalVideoFlip
|
||||
etherpad_base
|
||||
externalConnectUrl
|
||||
firefox_fake_device
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-enlarge:before {
|
||||
content: "\e90a";
|
||||
}
|
||||
.icon-signal_cellular_0:before {
|
||||
content: "\e901";
|
||||
}
|
||||
|
||||
@@ -116,9 +116,9 @@
|
||||
}
|
||||
}
|
||||
i.disabled, .disabled i {
|
||||
cursor: initial;
|
||||
color: #fff;
|
||||
background-color: #a4b8d1;
|
||||
cursor: initial !important;
|
||||
color: #fff !important;
|
||||
background-color: #a4b8d1 !important;
|
||||
}
|
||||
|
||||
.icon-mic-disabled, .icon-microphone, .icon-camera-disabled, .icon-camera {
|
||||
|
||||
@@ -23,14 +23,9 @@
|
||||
top: 0;
|
||||
width: 100%;
|
||||
|
||||
&.fit-full-height #largeVideoBackground {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.fit-full-width #largeVideoBackground {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
#largeVideoBackground {
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
#largeVideoBackgroundContainer {
|
||||
@@ -514,6 +509,7 @@
|
||||
}
|
||||
#dominantSpeakerAvatar {
|
||||
background-color: #000000;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dynamic-shadow {
|
||||
@@ -525,24 +521,37 @@
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.userAvatar {
|
||||
.avatar-container {
|
||||
@include maxSize(60px);
|
||||
@include absoluteAligning();
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 50%;
|
||||
overflow: hidden;
|
||||
width: auto;
|
||||
|
||||
.userAvatar {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#videoNotAvailableScreen {
|
||||
text-align: center;
|
||||
#avatarContainer {
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
height: 50vh;
|
||||
display:inline-block;
|
||||
margin-top: 25vh;
|
||||
overflow: hidden;
|
||||
width: 50vh;
|
||||
|
||||
#avatar {
|
||||
border-radius: 50%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
/**
|
||||
* Let the avatar grow with the tile.
|
||||
*/
|
||||
.userAvatar {
|
||||
.avatar-container {
|
||||
max-height: initial;
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
25
doc/api.md
@@ -192,6 +192,11 @@ The `command` parameter is String object with the name of the command. The follo
|
||||
api.executeCommand('displayName', 'New Nickname');
|
||||
```
|
||||
|
||||
* **password** - Sets the password for the room. This command requires one argument - the password name to be set.
|
||||
```javascript
|
||||
api.executeCommand('password', 'The Password');
|
||||
```
|
||||
|
||||
* **subject** - Sets the subject of the conference. This command requires one argument - the new subject to be set.
|
||||
```javascript
|
||||
api.executeCommand('subject', 'New Conference Subject');
|
||||
@@ -264,6 +269,14 @@ The `event` parameter is a String object with the name of the event.
|
||||
The `listener` parameter is a Function object with one argument that will be notified when the event occurs with data related to the event.
|
||||
|
||||
The following events are currently supported:
|
||||
* **cameraError** - event notifications about Jitsi-Meet having failed to access the camera. The listener will receive an object with the following structure:
|
||||
```javascript
|
||||
{
|
||||
type: string, // A constant representing the overall type of the error.
|
||||
message: string // Additional information about the error.
|
||||
}
|
||||
```
|
||||
|
||||
* **avatarChanged** - event notifications about avatar
|
||||
changes. The listener will receive an object with the following structure:
|
||||
```javascript
|
||||
@@ -287,6 +300,14 @@ changes. The listener will receive an object with the following structure:
|
||||
}
|
||||
```
|
||||
|
||||
* **micError** - event notifications about Jitsi-Meet having failed to access the mic. The listener will receive an object with the following structure:
|
||||
```javascript
|
||||
{
|
||||
type: string, // A constant representing the overall type of the error.
|
||||
message: string // Additional information about the error.
|
||||
}
|
||||
```
|
||||
|
||||
* **screenSharingStatusChanged** - receives event notifications about turning on/off the local user screen sharing. The listener will receive object with the following structure:
|
||||
```javascript
|
||||
{
|
||||
@@ -373,6 +394,8 @@ changes. The listener will receive an object with the following structure:
|
||||
}
|
||||
```
|
||||
|
||||
* **passwordRequired** - event notifications fired when failing to join a room because it has a password.
|
||||
|
||||
* **videoConferenceJoined** - event notifications fired when the local user has joined the video conference. The listener will receive an object with the following structure:
|
||||
```javascript
|
||||
{
|
||||
@@ -414,6 +437,8 @@ The listener will receive an object with the following structure:
|
||||
}
|
||||
```
|
||||
|
||||
* **suspendDetected** - event notifications about detecting suspend event in host computer.
|
||||
|
||||
You can also add multiple event listeners by using `addEventListeners`.
|
||||
This method requires one argument of type Object. The object argument must
|
||||
have the names of the events as keys and the listeners of the events as values.
|
||||
|
||||
BIN
fonts/jitsi.eot
@@ -37,6 +37,7 @@
|
||||
<glyph unicode="" glyph-name="signal_cellular_2" d="M86 86l852 852v-852h-852z" />
|
||||
<glyph unicode="" glyph-name="share-doc" d="M554 640h236l-236 234v-234zM682 426v86h-340v-86h340zM682 256v86h-340v-86h340zM598 938l256-256v-512c0-46-40-84-86-84h-512c-46 0-86 38-86 84l2 684c0 46 38 84 84 84h342z" />
|
||||
<glyph unicode="" glyph-name="ninja" d="M330.667 469.333c-0.427 14.933 6.4 29.44 17.92 39.253 32-6.827 61.867-20.053 88.747-39.253 0-29.013-23.893-52.907-53.333-52.907s-52.907 23.467-53.333 52.907zM586.667 469.333c26.88 18.773 56.747 32 88.747 38.827 11.52-9.813 18.347-24.32 17.92-38.827 0-29.867-23.893-53.76-53.333-53.76s-53.333 23.893-53.333 53.76v0zM512 640c-118.187 1.707-234.667-27.733-338.347-85.333l-2.987-42.667c0-52.48 12.373-104.107 35.84-151.040 101.12 15.36 203.093 23.040 305.493 23.040s204.373-7.68 305.493-23.040c23.467 46.933 35.84 98.56 35.84 151.040l-2.987 42.667c-103.68 57.6-220.16 87.040-338.347 85.333zM512 938.667c235.641 0 426.667-191.025 426.667-426.667s-191.025-426.667-426.667-426.667c-235.641 0-426.667 191.025-426.667 426.667s191.025 426.667 426.667 426.667z" />
|
||||
<glyph unicode="" glyph-name="enlarge" d="M896 212v600h-768v-600h768zM896 896q34 0 60-26t26-60v-596q0-34-26-60t-60-26h-768q-34 0-60 26t-26 60v596q0 34 26 60t60 26h768zM598 342l-86-108-86 108h172zM256 598v-172l-106 86zM768 598l106-86-106-86v172zM512 790l86-108h-172z" />
|
||||
<glyph unicode="" glyph-name="full-screen" d="M598 810h212v-212h-84v128h-128v84zM726 298v128h84v-212h-212v84h128zM214 598v212h212v-84h-128v-128h-84zM298 426v-128h128v-84h-212v212h84z" />
|
||||
<glyph unicode="" glyph-name="exit-full-screen" d="M682 682h128v-84h-212v212h84v-128zM598 214v212h212v-84h-128v-128h-84zM342 682v128h84v-212h-212v84h128zM214 342v84h212v-212h-84v128h-128z" />
|
||||
<glyph unicode="" glyph-name="security" d="M768 170v428h-512v-428h512zM768 682c46 0 86-38 86-84v-428c0-46-40-84-86-84h-512c-46 0-86 38-86 84v428c0 46 40 84 86 84h388v86c0 72-60 132-132 132s-132-60-132-132h-82c0 118 96 214 214 214s214-96 214-214v-86h42zM512 298c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86z" />
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.woff
@@ -93,12 +93,15 @@
|
||||
+ "font-size:small;"
|
||||
+ "cursor: pointer'>" + showMoreText + "</a>"
|
||||
+ " "
|
||||
+ "<a href='" + href + "' style='"
|
||||
+ "<a id ='reloadLink' style='"
|
||||
+ "text-decoration: underline;"
|
||||
+ "font-size:small;"
|
||||
+ "'>reload now</a>"
|
||||
+ "</div>";
|
||||
|
||||
var reloadLink = document.getElementById('reloadLink');
|
||||
reloadLink.setAttribute('href', href);
|
||||
|
||||
var showMoreElem = document.getElementById("showMore");
|
||||
showMoreElem.addEventListener('click', function () {
|
||||
var moreInfoElem
|
||||
|
||||
@@ -169,11 +169,25 @@ var interfaceConfig = {
|
||||
*/
|
||||
RECENT_LIST_ENABLED: true,
|
||||
|
||||
// Names of browsers which should show a warning stating the current browser
|
||||
// has a suboptimal experience. Browsers which are not listed as optimal or
|
||||
// unsupported are considered suboptimal. Valid values are:
|
||||
// chrome, chromium, edge, electron, firefox, nwjs, opera, safari
|
||||
OPTIMAL_BROWSERS: [ 'chrome', 'chromium', 'firefox', 'nwjs', 'electron' ],
|
||||
|
||||
// Browsers, in addition to those which do not fully support WebRTC, that
|
||||
// are not supported and should show the unsupported browser page.
|
||||
UNSUPPORTED_BROWSERS: [],
|
||||
|
||||
/**
|
||||
* A UX mode where the last screen share participant is automatically
|
||||
* pinned. Note: this mode is experimental and subject to breakage.
|
||||
* pinned. Valid values are the string "remote-only" so remote participants
|
||||
* get pinned but not local, otherwise any truthy value for all participants,
|
||||
* and any falsy value to disable the feature.
|
||||
*
|
||||
* Note: this mode is experimental and subject to breakage.
|
||||
*/
|
||||
AUTO_PIN_LATEST_SCREEN_SHARE: true
|
||||
AUTO_PIN_LATEST_SCREEN_SHARE: 'remote-only'
|
||||
|
||||
/**
|
||||
* How many columns the tile view can expand to. The respected range is
|
||||
|
||||
@@ -150,8 +150,8 @@ PODS:
|
||||
- React/RCTBlob
|
||||
- RNCAsyncStorage (1.3.4):
|
||||
- React
|
||||
- RNGoogleSignin (1.0.2):
|
||||
- GoogleSignIn
|
||||
- RNGoogleSignin (2.0.0):
|
||||
- GoogleSignIn (~> 4.4.0)
|
||||
- React
|
||||
- RNSound (0.10.12):
|
||||
- React/Core
|
||||
@@ -291,7 +291,7 @@ SPEC CHECKSUMS:
|
||||
react-native-webrtc: 90a847d19deb2d7323fef8cc89ca12b8995fbc90
|
||||
react-native-webview: a95842e3f351a6d2c8bc8bcc9eab689c7e7e5ad4
|
||||
RNCAsyncStorage: 8e31405a9f12fbf42c2bb330e4560bfd79c18323
|
||||
RNGoogleSignin: 361174d9a3090d295b06257162b560d8efc8a6ed
|
||||
RNGoogleSignin: d030c6c6591db24c3cee649f64c7babf0a1699a0
|
||||
RNSound: e157320f503bdd4f4ee6d8542e948d54f90c3c3a
|
||||
RNVectorIcons: d819334932bcda3332deb3d2c8ea4d069e0b98f9
|
||||
RNWatch: 09738b339eceb66e4d80a2371633ca5fb380fa42
|
||||
@@ -300,4 +300,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: b55338cc43312051ed83f8d9c6aadbd8c9402e6a
|
||||
|
||||
COCOAPODS: 1.6.1
|
||||
COCOAPODS: 1.7.2
|
||||
|
||||
@@ -737,7 +737,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = FC967L3QRG;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"../../node_modules/react-native-webrtc/ios",
|
||||
|
||||
@@ -46,6 +46,12 @@
|
||||
jitsiMeet.defaultConferenceOptions = [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) {
|
||||
builder.serverURL = [NSURL URLWithString:@"https://meet.jit.si"];
|
||||
builder.welcomePageEnabled = YES;
|
||||
|
||||
// Apple rejected our app because they claim requiring a
|
||||
// Dropbox account for recording is not acceptable.
|
||||
#if DEBUG
|
||||
[builder setFeatureFlag:@"ios.recording.enabled" withBoolean:YES];
|
||||
#endif
|
||||
}];
|
||||
|
||||
[jitsiMeet application:application didFinishLaunchingWithOptions:launchOptions];
|
||||
|
||||
@@ -42,11 +42,11 @@
|
||||
|
||||
- (void)_onJitsiMeetViewDelegateEvent:(NSString *)name
|
||||
withData:(NSDictionary *)data {
|
||||
#if DEBUG
|
||||
NSLog(
|
||||
@"[%s:%d] JitsiMeetViewDelegate %@ %@",
|
||||
__FILE__, __LINE__, name, data);
|
||||
|
||||
#if DEBUG
|
||||
NSAssert(
|
||||
[NSThread isMainThread],
|
||||
@"JitsiMeetViewDelegate %@ method invoked on a non-main thread",
|
||||
@@ -96,4 +96,10 @@
|
||||
[self _onJitsiMeetViewDelegateEvent:@"CONFERENCE_WILL_JOIN" withData:data];
|
||||
}
|
||||
|
||||
#if 0
|
||||
- (void)enterPictureInPicture:(NSDictionary *)data {
|
||||
[self _onJitsiMeetViewDelegateEvent:@"ENTER_PICTURE_IN_PICTURE" withData:data];
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<objects>
|
||||
<controller title="Meetings" id="AgC-eL-Hgc" customClass="InterfaceController" customModule="JitsiMeetCompanion" customModuleProvider="target">
|
||||
<items>
|
||||
<label alignment="left" textAlignment="left" numberOfLines="0" id="OQN-sx-tDt"/>
|
||||
<table alignment="left" id="gpO-ql-Xsr">
|
||||
<items>
|
||||
<tableRow identifier="MeetingRowType" id="GGl-av-xeJ" customClass="MeetingRowController" customModule="JitsiMeetCompanion_Extension">
|
||||
@@ -39,6 +40,7 @@
|
||||
</table>
|
||||
</items>
|
||||
<connections>
|
||||
<outlet property="infoLabel" destination="OQN-sx-tDt" id="Juv-tb-SNj"/>
|
||||
<outlet property="table" destination="gpO-ql-Xsr" id="aVV-iZ-z3l"/>
|
||||
</connections>
|
||||
</controller>
|
||||
|
||||
@@ -16,17 +16,31 @@
|
||||
*/
|
||||
|
||||
import WatchKit
|
||||
import WatchConnectivity
|
||||
import Foundation
|
||||
|
||||
|
||||
class InterfaceController: WKInterfaceController {
|
||||
|
||||
@IBOutlet var infoLabel: WKInterfaceLabel!
|
||||
@IBOutlet var table: WKInterfaceTable!
|
||||
|
||||
override func didAppear(){
|
||||
self.updateUI(ExtensionDelegate.currentJitsiMeetContext)
|
||||
}
|
||||
|
||||
func updateUI(_ newContext:JitsiMeetContext) {
|
||||
if let recentURLsArray = newContext.recentURLs {
|
||||
updateRecents(withRecents: recentURLsArray, currentContext: newContext)
|
||||
}
|
||||
if (newContext.recentURLs == nil || newContext.recentURLs!.count == 0) {
|
||||
infoLabel.setText("There are no recent meetings. Please use the app on the phone to start a new call.")
|
||||
|
||||
table.setHidden(true)
|
||||
infoLabel.setHidden(false)
|
||||
} else {
|
||||
updateRecents(withRecents: newContext.recentURLs!, currentContext: newContext)
|
||||
|
||||
table.setHidden(false)
|
||||
infoLabel.setHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRecents(withRecents recents: NSArray, currentContext: JitsiMeetContext) {
|
||||
@@ -68,7 +82,7 @@ class InterfaceController: WKInterfaceController {
|
||||
override func contextForSegue(withIdentifier segueIdentifier: String, in table: WKInterfaceTable, rowIndex: Int) -> Any? {
|
||||
let controller = table.rowController(at: rowIndex) as! MeetingRowController
|
||||
let currentContext = ExtensionDelegate.currentJitsiMeetContext
|
||||
|
||||
|
||||
// Copy the current context and add the joinConferenceURL to trigger the command when the in-call screen is displayed
|
||||
let actionContext = JitsiMeetContext(jmContext: currentContext)
|
||||
actionContext.joinConferenceURL = controller.roomUrl
|
||||
|
||||
@@ -7,6 +7,7 @@ PROJECT_REPO=$(realpath ${THIS_DIR}/../..)
|
||||
RELEASE_REPO=$(realpath ${THIS_DIR}/../../../jitsi-meet-ios-sdk-releases)
|
||||
DEFAULT_SDK_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${THIS_DIR}/../sdk/src/Info.plist)
|
||||
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
|
||||
DO_GIT_TAG=${GIT_TAG:-0}
|
||||
|
||||
|
||||
echo "Releasing Jitsi Meet SDK ${SDK_VERSION}"
|
||||
@@ -25,7 +26,9 @@ popd
|
||||
pushd ${PROJECT_REPO}
|
||||
rm -rf ios/sdk/JitsiMeet.framework
|
||||
xcodebuild -workspace ios/jitsi-meet.xcworkspace -scheme JitsiMeet -destination='generic/platform=iOS' -configuration Release archive
|
||||
git tag ios-sdk-${SDK_VERSION}
|
||||
if [[ $DO_GIT_TAG == 1 ]]; then
|
||||
git tag ios-sdk-${SDK_VERSION}
|
||||
fi
|
||||
popd
|
||||
|
||||
pushd ${RELEASE_REPO}
|
||||
@@ -39,9 +42,11 @@ xcrun bitcode_strip -r Frameworks/JitsiMeet.framework/JitsiMeet -o Frameworks/Ji
|
||||
xcrun bitcode_strip -r Frameworks/WebRTC.framework/WebRTC -o Frameworks/WebRTC.framework/WebRTC
|
||||
|
||||
# Add all files to git
|
||||
git add -A .
|
||||
git commit -m "${SDK_VERSION}"
|
||||
git tag ${SDK_VERSION}
|
||||
if [[ $DO_GIT_TAG == 1 ]]; then
|
||||
git add -A .
|
||||
git commit -m "${SDK_VERSION}"
|
||||
git tag ${SDK_VERSION}
|
||||
fi
|
||||
|
||||
popd
|
||||
|
||||
|
||||
@@ -42,8 +42,11 @@
|
||||
C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA0B209A0F660027712B /* JMCallKitListener.swift */; };
|
||||
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
|
||||
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
|
||||
DE762DB422AFDE76000DEBD6 /* JitsiMeetUserInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = DE762DB322AFDE76000DEBD6 /* JitsiMeetUserInfo.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
DE762DB622AFDE8D000DEBD6 /* JitsiMeetUserInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = DE762DB522AFDE8D000DEBD6 /* JitsiMeetUserInfo.m */; };
|
||||
DEAD3226220C497000E93636 /* JitsiMeetConferenceOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = DEAD3224220C497000E93636 /* JitsiMeetConferenceOptions.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
DEAD3227220C497000E93636 /* JitsiMeetConferenceOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */; };
|
||||
DEAFA779229EAD520033A7FA /* RNRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = DEAFA778229EAD520033A7FA /* RNRootView.m */; };
|
||||
DEFC743F21B178FA00E4DD96 /* LocaleDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */; };
|
||||
DEFE535421FB1BF800011A3A /* JitsiMeet.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE535321FB1BF800011A3A /* JitsiMeet.m */; };
|
||||
DEFE535621FB2E8300011A3A /* ReactUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE535521FB2E8300011A3A /* ReactUtils.m */; };
|
||||
@@ -93,9 +96,14 @@
|
||||
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
|
||||
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
|
||||
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
|
||||
DE762DB322AFDE76000DEBD6 /* JitsiMeetUserInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetUserInfo.h; sourceTree = "<group>"; };
|
||||
DE762DB522AFDE8D000DEBD6 /* JitsiMeetUserInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetUserInfo.m; sourceTree = "<group>"; };
|
||||
DE762DB722AFE166000DEBD6 /* JitsiMeetUserInfo+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetUserInfo+Private.h"; sourceTree = "<group>"; };
|
||||
DEAD3224220C497000E93636 /* JitsiMeetConferenceOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetConferenceOptions.h; sourceTree = "<group>"; };
|
||||
DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetConferenceOptions.m; sourceTree = "<group>"; };
|
||||
DEAD3228220C734300E93636 /* JitsiMeetConferenceOptions+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetConferenceOptions+Private.h"; sourceTree = "<group>"; };
|
||||
DEAFA777229EAD3B0033A7FA /* RNRootView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNRootView.h; sourceTree = "<group>"; };
|
||||
DEAFA778229EAD520033A7FA /* RNRootView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNRootView.m; sourceTree = "<group>"; };
|
||||
DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocaleDetector.m; sourceTree = "<group>"; };
|
||||
DEFE535321FB1BF800011A3A /* JitsiMeet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeet.m; sourceTree = "<group>"; };
|
||||
DEFE535521FB2E8300011A3A /* ReactUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactUtils.m; sourceTree = "<group>"; };
|
||||
@@ -171,8 +179,13 @@
|
||||
DEAD3224220C497000E93636 /* JitsiMeetConferenceOptions.h */,
|
||||
DEAD3228220C734300E93636 /* JitsiMeetConferenceOptions+Private.h */,
|
||||
DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */,
|
||||
DE762DB322AFDE76000DEBD6 /* JitsiMeetUserInfo.h */,
|
||||
DE762DB722AFE166000DEBD6 /* JitsiMeetUserInfo+Private.h */,
|
||||
DE762DB522AFDE8D000DEBD6 /* JitsiMeetUserInfo.m */,
|
||||
0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */,
|
||||
0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */,
|
||||
DEAFA777229EAD3B0033A7FA /* RNRootView.h */,
|
||||
DEAFA778229EAD520033A7FA /* RNRootView.m */,
|
||||
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */,
|
||||
0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */,
|
||||
DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */,
|
||||
@@ -254,6 +267,7 @@
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DE762DB422AFDE76000DEBD6 /* JitsiMeetUserInfo.h in Headers */,
|
||||
0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */,
|
||||
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */,
|
||||
0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */,
|
||||
@@ -478,6 +492,8 @@
|
||||
files = (
|
||||
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
|
||||
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
|
||||
DEAFA779229EAD520033A7FA /* RNRootView.m in Sources */,
|
||||
DE762DB622AFDE8D000DEBD6 /* JitsiMeetUserInfo.m in Sources */,
|
||||
DEAD3227220C497000E93636 /* JitsiMeetConferenceOptions.m in Sources */,
|
||||
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
|
||||
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,
|
||||
@@ -634,7 +650,7 @@
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = src/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.1.0</string>
|
||||
<string>2.2.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <React/RCTBridge.h>
|
||||
|
||||
#import "JitsiMeet.h"
|
||||
|
||||
@interface JitsiMeet ()
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "JitsiMeetUserInfo.h"
|
||||
|
||||
|
||||
@interface JitsiMeetConferenceOptionsBuilder : NSObject
|
||||
|
||||
/**
|
||||
@@ -26,6 +29,10 @@
|
||||
* Room name.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *room;
|
||||
/**
|
||||
* Conference subject.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *subject;
|
||||
/**
|
||||
* JWT token used for authentication.
|
||||
*/
|
||||
@@ -37,6 +44,11 @@
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSDictionary *colorScheme;
|
||||
|
||||
/**
|
||||
* Feature flags. See: https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/flags/constants.js
|
||||
*/
|
||||
@property (nonatomic, readonly, nonnull) NSDictionary *featureFlags;
|
||||
|
||||
/**
|
||||
* Set to YES to join the conference with audio / video muted or to start in audio
|
||||
* only mode respectively.
|
||||
@@ -51,15 +63,26 @@
|
||||
*/
|
||||
@property (nonatomic) BOOL welcomePageEnabled;
|
||||
|
||||
/**
|
||||
* Information about the local user. It will be used in absence of a token.
|
||||
*/
|
||||
@property (nonatomic, nullable) JitsiMeetUserInfo *userInfo;
|
||||
|
||||
- (void)setFeatureFlag:(NSString *_Nonnull)flag withBoolean:(BOOL)value;
|
||||
- (void)setFeatureFlag:(NSString *_Nonnull)flag withValue:(id _Nonnull)value;
|
||||
|
||||
@end
|
||||
|
||||
@interface JitsiMeetConferenceOptions : NSObject
|
||||
|
||||
@property (nonatomic, copy, nullable, readonly) NSURL *serverURL;
|
||||
|
||||
@property (nonatomic, copy, nullable, readonly) NSString *room;
|
||||
@property (nonatomic, copy, nullable, readonly) NSString *subject;
|
||||
@property (nonatomic, copy, nullable, readonly) NSString *token;
|
||||
|
||||
@property (nonatomic, copy, nullable) NSDictionary *colorScheme;
|
||||
@property (nonatomic, readonly, nonnull) NSDictionary *featureFlags;
|
||||
|
||||
@property (nonatomic, readonly) BOOL audioOnly;
|
||||
@property (nonatomic, readonly) BOOL audioMuted;
|
||||
@@ -67,6 +90,8 @@
|
||||
|
||||
@property (nonatomic, readonly) BOOL welcomePageEnabled;
|
||||
|
||||
@property (nonatomic, nullable) JitsiMeetUserInfo *userInfo;
|
||||
|
||||
+ (instancetype _Nonnull)fromBuilder:(void (^_Nonnull)(JitsiMeetConferenceOptionsBuilder *_Nonnull))initBlock;
|
||||
- (instancetype _Nonnull)init NS_UNAVAILABLE;
|
||||
|
||||
|
||||
@@ -17,12 +17,19 @@
|
||||
#import <React/RCTUtils.h>
|
||||
|
||||
#import "JitsiMeetConferenceOptions+Private.h"
|
||||
#import "JitsiMeetUserInfo+Private.h"
|
||||
|
||||
/**
|
||||
* Backwards compatibility: turn the boolean property into a feature flag.
|
||||
*/
|
||||
static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
|
||||
|
||||
|
||||
@implementation JitsiMeetConferenceOptionsBuilder {
|
||||
NSNumber *_audioOnly;
|
||||
NSNumber *_audioMuted;
|
||||
NSNumber *_videoMuted;
|
||||
NSNumber *_welcomePageEnabled;
|
||||
NSMutableDictionary *_featureFlags;
|
||||
}
|
||||
|
||||
@dynamic audioOnly;
|
||||
@@ -34,20 +41,30 @@
|
||||
if (self = [super init]) {
|
||||
_serverURL = nil;
|
||||
_room = nil;
|
||||
_subject = nil;
|
||||
_token = nil;
|
||||
|
||||
_colorScheme = nil;
|
||||
_featureFlags = [[NSMutableDictionary alloc] init];
|
||||
|
||||
_audioOnly = nil;
|
||||
_audioMuted = nil;
|
||||
_videoMuted = nil;
|
||||
|
||||
_welcomePageEnabled = nil;
|
||||
_userInfo = nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setFeatureFlag:(NSString *)flag withBoolean:(BOOL)value {
|
||||
[self setFeatureFlag:flag withValue:[NSNumber numberWithBool:value]];
|
||||
}
|
||||
|
||||
- (void)setFeatureFlag:(NSString *)flag withValue:(id)value {
|
||||
_featureFlags[flag] = value;
|
||||
}
|
||||
|
||||
#pragma mark - Dynamic properties
|
||||
|
||||
- (void)setAudioOnly:(BOOL)audioOnly {
|
||||
@@ -75,11 +92,14 @@
|
||||
}
|
||||
|
||||
- (void)setWelcomePageEnabled:(BOOL)welcomePageEnabled {
|
||||
_welcomePageEnabled = [NSNumber numberWithBool:welcomePageEnabled];
|
||||
[self setFeatureFlag:WelcomePageEnabledFeatureFlag
|
||||
withBoolean:welcomePageEnabled];
|
||||
}
|
||||
|
||||
- (BOOL)welcomePageEnabled {
|
||||
return _welcomePageEnabled && [_welcomePageEnabled boolValue];
|
||||
NSNumber *n = _featureFlags[WelcomePageEnabledFeatureFlag];
|
||||
|
||||
return n != nil ? [n boolValue] : NO;
|
||||
}
|
||||
|
||||
#pragma mark - Private API
|
||||
@@ -96,17 +116,13 @@
|
||||
return _videoMuted;
|
||||
}
|
||||
|
||||
- (NSNumber *)getWelcomePageEnabled {
|
||||
return _welcomePageEnabled;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation JitsiMeetConferenceOptions {
|
||||
NSNumber *_audioOnly;
|
||||
NSNumber *_audioMuted;
|
||||
NSNumber *_videoMuted;
|
||||
NSNumber *_welcomePageEnabled;
|
||||
NSDictionary *_featureFlags;
|
||||
}
|
||||
|
||||
@dynamic audioOnly;
|
||||
@@ -129,7 +145,9 @@
|
||||
}
|
||||
|
||||
- (BOOL)welcomePageEnabled {
|
||||
return _welcomePageEnabled && [_welcomePageEnabled boolValue];
|
||||
NSNumber *n = _featureFlags[WelcomePageEnabledFeatureFlag];
|
||||
|
||||
return n != nil ? [n boolValue] : NO;
|
||||
}
|
||||
|
||||
#pragma mark - Internal initializer
|
||||
@@ -138,6 +156,7 @@
|
||||
if (self = [super init]) {
|
||||
_serverURL = builder.serverURL;
|
||||
_room = builder.room;
|
||||
_subject = builder.subject;
|
||||
_token = builder.token;
|
||||
|
||||
_colorScheme = builder.colorScheme;
|
||||
@@ -146,7 +165,9 @@
|
||||
_audioMuted = [builder getAudioMuted];
|
||||
_videoMuted = [builder getVideoMuted];
|
||||
|
||||
_welcomePageEnabled = [builder getWelcomePageEnabled];
|
||||
_featureFlags = [NSDictionary dictionaryWithDictionary:builder.featureFlags];
|
||||
|
||||
_userInfo = builder.userInfo;
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -165,14 +186,12 @@
|
||||
- (NSDictionary *)asProps {
|
||||
NSMutableDictionary *props = [[NSMutableDictionary alloc] init];
|
||||
|
||||
props[@"flags"] = [NSMutableDictionary dictionaryWithDictionary:_featureFlags];
|
||||
|
||||
if (_colorScheme != nil) {
|
||||
props[@"colorScheme"] = self.colorScheme;
|
||||
}
|
||||
|
||||
if (_welcomePageEnabled != nil) {
|
||||
props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
|
||||
}
|
||||
|
||||
NSMutableDictionary *config = [[NSMutableDictionary alloc] init];
|
||||
if (_audioOnly != nil) {
|
||||
config[@"startAudioOnly"] = @(self.audioOnly);
|
||||
@@ -183,6 +202,9 @@
|
||||
if (_videoMuted != nil) {
|
||||
config[@"startWithVideoMuted"] = @(self.videoMuted);
|
||||
}
|
||||
if (_subject != nil) {
|
||||
config[@"subject"] = self.subject;
|
||||
}
|
||||
|
||||
NSMutableDictionary *urlProps = [[NSMutableDictionary alloc] init];
|
||||
|
||||
@@ -203,6 +225,10 @@
|
||||
urlProps[@"jwt"] = _token;
|
||||
}
|
||||
|
||||
if (_token == nil && _userInfo != nil) {
|
||||
props[@"userInfo"] = [self.userInfo asDict];
|
||||
}
|
||||
|
||||
urlProps[@"config"] = config;
|
||||
props[@"url"] = urlProps;
|
||||
|
||||
|
||||
23
ios/sdk/src/JitsiMeetUserInfo+Private.h
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "JitsiMeetUserInfo.h"
|
||||
|
||||
@interface JitsiMeetUserInfo ()
|
||||
|
||||
- (NSMutableDictionary *)asDict;
|
||||
|
||||
@end
|
||||
38
ios/sdk/src/JitsiMeetUserInfo.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface JitsiMeetUserInfo : NSObject
|
||||
|
||||
/**
|
||||
* User display name.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *displayName;
|
||||
/**
|
||||
* User e-mail.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *email;
|
||||
/**
|
||||
* URL for the user avatar.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSURL *avatar;
|
||||
|
||||
- (instancetype _Nullable)initWithDisplayName:(NSString *_Nullable)displayName
|
||||
andEmail:(NSString *_Nullable)email
|
||||
andAvatar:(NSURL *_Nullable) avatar;
|
||||
|
||||
@end
|
||||
55
ios/sdk/src/JitsiMeetUserInfo.m
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "JitsiMeetUserInfo+Private.h"
|
||||
|
||||
@implementation JitsiMeetUserInfo
|
||||
|
||||
- (instancetype)initWithDisplayName:(NSString *)displayName
|
||||
andEmail:(NSString *)email
|
||||
andAvatar:(NSURL *_Nullable) avatar {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.displayName = displayName;
|
||||
self.email = email;
|
||||
self.avatar = avatar;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSDictionary *)asDict {
|
||||
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
|
||||
|
||||
if (self.displayName != nil) {
|
||||
dict[@"displayName"] = self.displayName;
|
||||
}
|
||||
|
||||
if (self.email != nil) {
|
||||
dict[@"email"] = self.email;
|
||||
}
|
||||
|
||||
if (self.avatar != nil) {
|
||||
NSString *avatarURL = [self.avatar absoluteString];
|
||||
if (avatarURL != nil) {
|
||||
dict[@"avatarURL"] = avatarURL;
|
||||
}
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -17,12 +17,17 @@
|
||||
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
#import <React/RCTRootView.h>
|
||||
|
||||
#import "JitsiMeet+Private.h"
|
||||
#import "JitsiMeetConferenceOptions+Private.h"
|
||||
#import "JitsiMeetView+Private.h"
|
||||
#import "ReactUtils.h"
|
||||
#import "RNRootView.h"
|
||||
|
||||
|
||||
/**
|
||||
* Backwards compatibility: turn the boolean prop into a feature flag.
|
||||
*/
|
||||
static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
|
||||
|
||||
|
||||
@implementation JitsiMeetView {
|
||||
@@ -36,7 +41,7 @@
|
||||
/**
|
||||
* React Native view where the entire content will be rendered.
|
||||
*/
|
||||
RCTRootView *rootView;
|
||||
RNRootView *rootView;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,11 +128,15 @@ static void initializeViewsMap() {
|
||||
- (void)setProps:(NSDictionary *_Nonnull)newProps {
|
||||
NSMutableDictionary *props = mergeProps([[JitsiMeet sharedInstance] getDefaultProps], newProps);
|
||||
|
||||
props[@"externalAPIScope"] = externalAPIScope;
|
||||
// Set the PiP flag if it wasn't manually set.
|
||||
NSMutableDictionary *featureFlags = props[@"flags"];
|
||||
if (featureFlags[PiPEnabledFeatureFlag] == nil) {
|
||||
featureFlags[PiPEnabledFeatureFlag]
|
||||
= [NSNumber numberWithBool:
|
||||
self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)]];
|
||||
}
|
||||
|
||||
// TODO: put this in some 'flags' field
|
||||
props[@"pictureInPictureEnabled"]
|
||||
= @(self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)]);
|
||||
props[@"externalAPIScope"] = externalAPIScope;
|
||||
|
||||
// This method is supposed to be imperative i.e. a second
|
||||
// invocation with one and the same URL is expected to join the respective
|
||||
@@ -145,9 +154,9 @@ static void initializeViewsMap() {
|
||||
} else {
|
||||
RCTBridge *bridge = [[JitsiMeet sharedInstance] getReactBridge];
|
||||
rootView
|
||||
= [[RCTRootView alloc] initWithBridge:bridge
|
||||
moduleName:@"App"
|
||||
initialProperties:props];
|
||||
= [[RNRootView alloc] initWithBridge:bridge
|
||||
moduleName:@"App"
|
||||
initialProperties:props];
|
||||
rootView.backgroundColor = self.backgroundColor;
|
||||
|
||||
// Add rootView as a subview which completely covers this one.
|
||||
|
||||
20
ios/sdk/src/RNRootView.h
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <React/RCTRootView.h>
|
||||
|
||||
@interface RNRootView : RCTRootView
|
||||
@end
|
||||
45
ios/sdk/src/RNRootView.m
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <React/RCTRootContentView.h>
|
||||
#import <React/RCTLog.h>
|
||||
|
||||
#import "RNRootView.h"
|
||||
|
||||
@implementation RNRootView
|
||||
|
||||
// Monkey-patch RCTRootView.runApplication to avoid logging initial props.
|
||||
- (void)runApplication:(RCTBridge *)bridge
|
||||
{
|
||||
NSString *moduleName = [self valueForKey:@"_moduleName"] ?: @"";
|
||||
RCTRootContentView *_contentView = [self valueForKey:@"_contentView"];
|
||||
NSNumber *reactTag = [_contentView valueForKey:@"reactTag"];
|
||||
|
||||
NSDictionary *appParameters = @{
|
||||
@"rootTag": reactTag,
|
||||
@"initialProps": self.appProperties ?: @{},
|
||||
};
|
||||
#if DEBUG
|
||||
RCTLogInfo(@"Running application %@ (%@)", moduleName, appParameters);
|
||||
#endif
|
||||
|
||||
[bridge enqueueJSCall:@"AppRegistry"
|
||||
method:@"runApplication"
|
||||
args:@[moduleName, appParameters]
|
||||
completion:NULL];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -33,6 +33,10 @@ RCT_EXPORT_METHOD(init:(NSString*)instanceName API_KEY:(NSString*)apiKey) {
|
||||
[[Amplitude instanceWithName:instanceName] initializeApiKey:apiKey];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(setUserId:(NSString*)instanceName userId: (NSString *) userId) {
|
||||
[[Amplitude instanceWithName:instanceName] setUserId:userId];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(setUserProperties:(NSString*)instanceName userPropsString:(NSDictionary*)userProps) {
|
||||
if (userProps != nil) {
|
||||
[[Amplitude instanceWithName:instanceName] setUserProperties:userProps];
|
||||
|
||||
@@ -74,6 +74,12 @@ RCT_EXPORT_METHOD(endCall:(NSString *)callUUID
|
||||
#endif
|
||||
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
|
||||
if (!callUUID_) {
|
||||
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
|
||||
return;
|
||||
}
|
||||
|
||||
CXEndCallAction *action
|
||||
= [[CXEndCallAction alloc] initWithCallUUID:callUUID_];
|
||||
[self requestTransaction:[[CXTransaction alloc] initWithAction:action]
|
||||
@@ -91,6 +97,12 @@ RCT_EXPORT_METHOD(setMuted:(NSString *)callUUID
|
||||
#endif
|
||||
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
|
||||
if (!callUUID_) {
|
||||
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
|
||||
return;
|
||||
}
|
||||
|
||||
CXSetMutedCallAction *action
|
||||
= [[CXSetMutedCallAction alloc] initWithCallUUID:callUUID_ muted:muted];
|
||||
[self requestTransaction:[[CXTransaction alloc] initWithAction:action]
|
||||
@@ -123,6 +135,13 @@ RCT_EXPORT_METHOD(startCall:(NSString *)callUUID
|
||||
NSLog(@"[RNCallKit][startCall] callUUID = %@", callUUID);
|
||||
#endif
|
||||
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
|
||||
if (!callUUID_) {
|
||||
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't start a new call if there's an active call for the specified
|
||||
// callUUID. JitsiMeetView was configured for an incoming call.
|
||||
if ([JMCallKitProxy hasActiveCallForUUID:callUUID]) {
|
||||
@@ -132,7 +151,6 @@ RCT_EXPORT_METHOD(startCall:(NSString *)callUUID
|
||||
|
||||
CXHandle *handle_
|
||||
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
CXStartCallAction *action
|
||||
= [[CXStartCallAction alloc] initWithCallUUID:callUUID_
|
||||
handle:handle_];
|
||||
@@ -146,6 +164,12 @@ RCT_EXPORT_METHOD(reportCallFailed:(NSString *)callUUID
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
|
||||
if (!callUUID_) {
|
||||
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
|
||||
return;
|
||||
}
|
||||
|
||||
[JMCallKitProxy reportCallWith:callUUID_
|
||||
endedAt:nil
|
||||
reason:CXCallEndedReasonFailed];
|
||||
@@ -157,6 +181,12 @@ RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)callUUID
|
||||
resolve:(RCTPromiseResolveBlock)resolve
|
||||
reject:(RCTPromiseRejectBlock)reject) {
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
|
||||
if (!callUUID_) {
|
||||
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
|
||||
return;
|
||||
}
|
||||
|
||||
[JMCallKitProxy reportOutgoingCallWith:callUUID_
|
||||
connectedAt:nil];
|
||||
resolve(nil);
|
||||
@@ -175,6 +205,12 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
|
||||
#endif
|
||||
|
||||
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
|
||||
|
||||
if (!callUUID_) {
|
||||
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *displayName = options[@"displayName"];
|
||||
BOOL hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];
|
||||
|
||||
|
||||
@@ -37,6 +37,15 @@ public class PiPViewCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
public enum Position {
|
||||
case lowerRightCorner
|
||||
case upperRightCorner
|
||||
case lowerLeftCorner
|
||||
case upperLeftCorner
|
||||
}
|
||||
|
||||
public var initialPositionInSuperview = Position.lowerRightCorner
|
||||
|
||||
/// The size ratio of the view when in PiP mode
|
||||
public var pipSizeRatio: CGFloat = {
|
||||
let deviceIdiom = UIScreen.main.traitCollection.userInterfaceIdiom
|
||||
@@ -205,9 +214,21 @@ public class PiPViewCoordinator {
|
||||
let adjustedBounds = bounds.inset(by: dragBoundInsets)
|
||||
let size = CGSize(width: bounds.size.width * pipSizeRatio,
|
||||
height: bounds.size.height * pipSizeRatio)
|
||||
let x: CGFloat = adjustedBounds.maxX - size.width
|
||||
let y: CGFloat = adjustedBounds.maxY - size.height
|
||||
return CGRect(x: x, y: y, width: size.width, height: size.height)
|
||||
let origin = initialPositionFor(pipSize: size, bounds: adjustedBounds)
|
||||
return CGRect(x: origin.x, y: origin.y, width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
private func initialPositionFor(pipSize size: CGSize, bounds: CGRect) -> CGPoint {
|
||||
switch initialPositionInSuperview {
|
||||
case .lowerLeftCorner:
|
||||
return CGPoint(x: bounds.minX, y: bounds.maxY - size.height)
|
||||
case .lowerRightCorner:
|
||||
return CGPoint(x: bounds.maxX - size.width, y: bounds.maxY - size.height)
|
||||
case .upperLeftCorner:
|
||||
return CGPoint(x: bounds.minX, y: bounds.minY)
|
||||
case .upperRightCorner:
|
||||
return CGPoint(x: bounds.maxX - size.width, y: bounds.minY)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Animation helpers
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
{
|
||||
"en": "Anglés",
|
||||
"af": "Afrikaans",
|
||||
"az": "Azèri",
|
||||
"bg": "Bulgar",
|
||||
"cs": "Chèc",
|
||||
"de": "Aleman",
|
||||
"el": "Grèc",
|
||||
"eo": "Esperanto",
|
||||
"es": "Castelhan",
|
||||
"fr": "Francés",
|
||||
"hy": "Armenian",
|
||||
"it": "Italian",
|
||||
"ja": "Japonés",
|
||||
"ko": "Corean",
|
||||
"nb": "Norvegian Bokmål",
|
||||
"oc": "Occitan",
|
||||
"pl": "Polonés",
|
||||
"ptBR": "Portugués (Brasil)",
|
||||
@@ -14,7 +22,6 @@
|
||||
"sl": "Eslovèn",
|
||||
"sv": "Suedés",
|
||||
"tr": "Turc",
|
||||
"zhCN": "Chinés (China)",
|
||||
"nb": "Norvegian Bokmål",
|
||||
"eo": "Esperanto"
|
||||
"vi": "Vietnamian",
|
||||
"zhCN": "Chinés (China)"
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"en": "Inglês",
|
||||
"af": "Africâner",
|
||||
"az": "Azerbaijanês",
|
||||
"bg": "Búlgaro",
|
||||
"cs": "Checo",
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
{
|
||||
"en": "Tiếng Anh",
|
||||
"af": "Tiếng Afrika",
|
||||
"az": "Tiếng Azecbaizan",
|
||||
"bg": "Tiếng Bulgaria",
|
||||
"cs": "Tiếng Séc",
|
||||
"de": "Tiếng Đức",
|
||||
"el": "Tiếng Nhật",
|
||||
"eo": "Tiếng Esperanto",
|
||||
"es": "Tiếng Tây Ban Nha",
|
||||
"fr": "Tiếng Pháp",
|
||||
"hy": "Tiếng Acmenia",
|
||||
"it": "Tiếng Ý",
|
||||
"ja": "Tiếng Nhật",
|
||||
"ko": "Tiếng Hàn",
|
||||
"nb": "Tiếng Na Uy",
|
||||
"oc": "Tiếng Occitan",
|
||||
"pl": "Tiếng Ba Lan",
|
||||
"ptBR": "Tiếng Bồ Đào Nha (Brazil)",
|
||||
@@ -14,7 +22,6 @@
|
||||
"sl": "Tiếng Slovenia",
|
||||
"sv": "Tiếng Thụy Điển",
|
||||
"tr": "Tiếng Thổ Nhĩ Kỳ",
|
||||
"zhCN": "Tiếng Hoa (Trung Quốc)",
|
||||
"nb": "Tiếng Na Uy",
|
||||
"eo": "Tiếng Esperanto"
|
||||
"vi": "Tiếng Việt",
|
||||
"zhCN": "Tiếng Hoa (Trung Quốc)"
|
||||
}
|
||||
1185
lang/main-oc.json
1260
lang/main-ptBR.json
1204
lang/main-vi.json
@@ -121,6 +121,7 @@
|
||||
"deepLinking": {
|
||||
"appNotInstalled": "You need the __app__ mobile app to join this meeting on your phone.",
|
||||
"description": "Nothing happened? We tried launching your meeting in the __app__ desktop app. Try again or launch it in the __app__ web app.",
|
||||
"descriptionWithoutWeb": "Nothing happened? We tried launching your meeting in the __app__ desktop app.",
|
||||
"downloadApp": "Download the app",
|
||||
"launchWebButton": "Launch in web",
|
||||
"openApp": "Continue to the app",
|
||||
@@ -176,10 +177,10 @@
|
||||
"defaultError": "There was some kind of error",
|
||||
"detectext": "Error when trying to detect desktopsharing extension.",
|
||||
"dismiss": "Dismiss",
|
||||
"displayNameRequired": "Display name is required",
|
||||
"displayNameRequired": "Hi! What’s your name?",
|
||||
"done": "Done",
|
||||
"doNotShowMessageAgain": "Don't show this message again",
|
||||
"enterDisplayName": "Please enter your display name",
|
||||
"enterDisplayName": "Please enter your name here",
|
||||
"error": "Error",
|
||||
"externalInstallationMsg": "You need to install our desktop sharing extension.",
|
||||
"externalInstallationTitle": "Extension required",
|
||||
@@ -196,11 +197,11 @@
|
||||
"internalError": "Oops! Something went wrong. The following error occurred: __error__",
|
||||
"internalErrorTitle": "Internal error",
|
||||
"joinAgain": "Join again",
|
||||
"kickMessage": "Ouch! You have been kicked out of the meet!",
|
||||
"kickMessage": "You can contact __participantDisplayName__ for more details.",
|
||||
"kickParticipantButton": "Kick",
|
||||
"kickParticipantDialog": "Are you sure you want to kick this participant?",
|
||||
"kickParticipantTitle": "Kick this member?",
|
||||
"kickTitle": "Kicked from meeting",
|
||||
"kickTitle": "Ouch! __participantDisplayName__ kicked you out of the meeting",
|
||||
"liveStreaming": "Live Streaming",
|
||||
"liveStreamingDisabledForGuestTooltip": "Guests can't start live streaming.",
|
||||
"liveStreamingDisabledTooltip": "Start live stream disabled.",
|
||||
@@ -213,8 +214,8 @@
|
||||
"maxUsersLimitReachedTitle": "Maximum members limit reached",
|
||||
"micConstraintFailedError": "Your microphone does not satisfy some of the required constraints.",
|
||||
"micNotFoundError": "Microphone was not found.",
|
||||
"micNotSendingData": "We are unable to access your microphone. Please select another device from the settings menu or try to reload the application.",
|
||||
"micNotSendingDataTitle": "Unable to access microphone",
|
||||
"micNotSendingData": "Go to your computer's settings to unmute your mic and adjust its level",
|
||||
"micNotSendingDataTitle": "Your mic is muted by your system settings",
|
||||
"micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
|
||||
"micUnknownError": "Cannot use microphone for an unknown reason.",
|
||||
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
|
||||
@@ -355,12 +356,11 @@
|
||||
"dialInTollFree": "Toll Free",
|
||||
"genericError": "Whoops, something went wrong.",
|
||||
"inviteLiveStream": "To view the live stream of this meeting, click this link: __url__",
|
||||
"invitePhone": "One tap audio Dial In: __number__,,__conferenceID__#",
|
||||
"invitePhoneAlternatives": "Looking for a different dial in number? Please see: __url__",
|
||||
"invitePhone": "To join by phone instead, tap this: __number__,,__conferenceID__#\n",
|
||||
"invitePhoneAlternatives": "Looking for a different dial-in number?\nSee meeting dial-in numbers: __url__\n\n\nIf also dialing-in through a room phone, join without connecting to audio: __silentUrl__",
|
||||
"inviteURLFirstPartGeneral": "You are invited to join a meeting.",
|
||||
"inviteURLFirstPartPersonal": "__name__ is inviting you to a meeting.",
|
||||
"inviteURLSecondPart": "\n__moreInfo__\nJoin meeting: __url__\n",
|
||||
"inviteURLMoreInfo": "Meeting ID: __conferenceID__#\n",
|
||||
"inviteURLFirstPartPersonal": "__name__ is inviting you to a meeting.\n",
|
||||
"inviteURLSecondPart": "\nJoin the meeting:\n__url__\n",
|
||||
"liveStreamURL": "Live stream:",
|
||||
"moreNumbers": "More numbers",
|
||||
"noNumbers": "No dial-in numbers.",
|
||||
@@ -472,12 +472,20 @@
|
||||
"focus": "Conference focus",
|
||||
"focusFail": "__component__ not available - retry in __ms__ sec",
|
||||
"grantedTo": "Moderator rights granted to __to__!",
|
||||
"invitedOneMember": "__name__ has been invited",
|
||||
"invitedThreePlusMembers": "__name__ and __count__ others have been invited",
|
||||
"invitedTwoMembers": "__first__ and __second__ have been invited",
|
||||
"kickParticipant": "__kicked__ was kicked by __kicker__",
|
||||
"me": "Me",
|
||||
"moderator": "Moderator rights granted!",
|
||||
"muted": "You have started the conversation muted.",
|
||||
"mutedTitle": "You're muted!",
|
||||
"mutedRemotelyTitle": "You have been muted by __participantDisplayName__!",
|
||||
"mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
|
||||
"raisedHand": "__name__ would like to speak.",
|
||||
"somebody": "Somebody",
|
||||
"startSilentTitle": "You joined with no audio output!",
|
||||
"startSilentDescription": "Rejoin the meeting to enable audio",
|
||||
"suboptimalExperienceDescription": "Eer... we are afraid your experience with __appName__ isn't going to be that great here. We are looking for ways to improve this but, until then, please try using one of the <a href='static/recommendedBrowsers.html' target='_blank'>fully supported browsers</a>.",
|
||||
"suboptimalExperienceTitle": "Browser Warning",
|
||||
"newDeviceCameraTitle": "New camera detected",
|
||||
@@ -609,7 +617,7 @@
|
||||
"accessibilityLabel": {
|
||||
"audioOnly": "Toggle audio only",
|
||||
"audioRoute": "Select the sound device",
|
||||
"callQuality": "Manage call quality",
|
||||
"callQuality": "Manage video quality",
|
||||
"cc": "Toggle subtitles",
|
||||
"chat": "Toggle chat window",
|
||||
"document": "Toggle shared document",
|
||||
@@ -633,6 +641,7 @@
|
||||
"shareRoom": "Invite someone",
|
||||
"shareYourScreen": "Toggle screenshare",
|
||||
"shortcuts": "Toggle shortcuts",
|
||||
"show": "Show on stage",
|
||||
"speakerStats": "Toggle speaker statistics",
|
||||
"tileView": "Toggle tile view",
|
||||
"toggleCamera": "Toggle camera",
|
||||
@@ -644,7 +653,7 @@
|
||||
"audioOnlyOn": "Enable audio only mode",
|
||||
"audioRoute": "Select the sound device",
|
||||
"authenticate": "Authenticate",
|
||||
"callQuality": "Manage call quality",
|
||||
"callQuality": "Manage video quality",
|
||||
"cameraDisabled": "Camera is not available",
|
||||
"chat": "Open / Close chat",
|
||||
"closeChat": "Close chat",
|
||||
@@ -730,7 +739,7 @@
|
||||
"videoStatus": {
|
||||
"audioOnly": "AUD",
|
||||
"audioOnlyExpanded": "You are in audio only mode. This mode saves bandwidth but you won't see videos of others.",
|
||||
"callQuality": "Call Quality",
|
||||
"callQuality": "Video Quality",
|
||||
"hd": "HD",
|
||||
"hdTooltip": "Viewing high definition video",
|
||||
"highDefinition": "High definition",
|
||||
@@ -743,7 +752,7 @@
|
||||
"onlyAudioAvailable": "Only audio is available",
|
||||
"onlyAudioSupported": "We only support audio in this browser.",
|
||||
"p2pEnabled": "Peer to Peer Enabled",
|
||||
"p2pVideoQualityDescription": "In peer to peer mode, received call quality can only be toggled between high and audio only. Other settings will not be honored until peer to peer is exited.",
|
||||
"p2pVideoQualityDescription": "In peer to peer mode, received video quality can only be toggled between high and audio only. Other settings will not be honored until peer to peer is exited.",
|
||||
"qualityButtonTip": "Change received video quality",
|
||||
"recHighDefinitionOnly": "Will prefer high definition.",
|
||||
"sd": "SD",
|
||||
@@ -758,6 +767,7 @@
|
||||
"mute": "Member is muted",
|
||||
"muted": "Muted",
|
||||
"remoteControl": "Remote control",
|
||||
"show": "Show on stage",
|
||||
"videomute": "Member has stopped the camera"
|
||||
},
|
||||
"welcomepage": {
|
||||
@@ -781,6 +791,7 @@
|
||||
"recentList": "Recent",
|
||||
"recentListDelete": "Delete",
|
||||
"recentListEmpty": "Your recent list is currently empty. Chat with your team and you will find all your recent meetings here.",
|
||||
"reducedUIText": "Welcome to __app__!",
|
||||
"roomname": "Enter room name",
|
||||
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
|
||||
"sendFeedback": "Send feedback",
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
createApiEvent,
|
||||
sendAnalytics
|
||||
} from '../../react/features/analytics';
|
||||
import { setSubject } from '../../react/features/base/conference';
|
||||
import { setPassword, setSubject } from '../../react/features/base/conference';
|
||||
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
|
||||
import { invite } from '../../react/features/invite';
|
||||
import { toggleTileView } from '../../react/features/video-layout';
|
||||
@@ -65,6 +65,28 @@ function initCommands() {
|
||||
sendAnalytics(createApiEvent('display.name.changed'));
|
||||
APP.conference.changeLocalDisplayName(displayName);
|
||||
},
|
||||
'password': password => {
|
||||
const { conference, passwordRequired }
|
||||
= APP.store.getState()['features/base/conference'];
|
||||
|
||||
if (passwordRequired) {
|
||||
sendAnalytics(createApiEvent('submit.password'));
|
||||
|
||||
APP.store.dispatch(setPassword(
|
||||
passwordRequired,
|
||||
passwordRequired.join,
|
||||
password
|
||||
));
|
||||
} else {
|
||||
sendAnalytics(createApiEvent('password.changed'));
|
||||
|
||||
APP.store.dispatch(setPassword(
|
||||
conference,
|
||||
conference.lock,
|
||||
password
|
||||
));
|
||||
}
|
||||
},
|
||||
'proxy-connection-event': event => {
|
||||
APP.conference.onProxyConnectionEvent(event);
|
||||
},
|
||||
@@ -103,9 +125,9 @@ function initCommands() {
|
||||
|
||||
APP.store.dispatch(toggleTileView());
|
||||
},
|
||||
'video-hangup': () => {
|
||||
'video-hangup': (showFeedbackDialog = true) => {
|
||||
sendAnalytics(createApiEvent('video.hangup'));
|
||||
APP.conference.hangup(true);
|
||||
APP.conference.hangup(showFeedbackDialog);
|
||||
},
|
||||
'email': email => {
|
||||
sendAnalytics(createApiEvent('email.changed'));
|
||||
@@ -487,6 +509,15 @@ class API {
|
||||
this._sendEvent({ name: 'video-ready-to-close' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that a suspend event in host computer.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
notifySuspendDetected() {
|
||||
this._sendEvent({ name: 'suspend-detected' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) for audio muted status
|
||||
* changed.
|
||||
@@ -559,6 +590,38 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application of an unexpected camera-related error having
|
||||
* occurred.
|
||||
*
|
||||
* @param {string} type - The type of the camera error.
|
||||
* @param {string} message - Additional information about the error.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyOnCameraError(type: string, message: string) {
|
||||
this._sendEvent({
|
||||
name: 'camera-error',
|
||||
type,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application of an unexpected mic-related error having
|
||||
* occurred.
|
||||
*
|
||||
* @param {string} type - The type of the mic error.
|
||||
* @param {string} message - Additional information about the error.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyOnMicError(type: string, message: string) {
|
||||
this._sendEvent({
|
||||
name: 'mic-error',
|
||||
type,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that conference feedback
|
||||
* has been submitted. Intended to be used in conjunction with the
|
||||
@@ -595,6 +658,16 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application of the current meeting requiring a password
|
||||
* to join.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyOnPasswordRequired() {
|
||||
this._sendEvent({ name: 'password-required' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that the screen sharing
|
||||
* has been turned on/off.
|
||||
|
||||
6
modules/API/external/external_api.js
vendored
@@ -33,6 +33,7 @@ const commands = {
|
||||
displayName: 'display-name',
|
||||
email: 'email',
|
||||
hangup: 'video-hangup',
|
||||
password: 'password',
|
||||
subject: 'subject',
|
||||
submitFeedback: 'submit-feedback',
|
||||
toggleAudio: 'toggle-audio',
|
||||
@@ -51,6 +52,7 @@ const events = {
|
||||
'avatar-changed': 'avatarChanged',
|
||||
'audio-availability-changed': 'audioAvailabilityChanged',
|
||||
'audio-mute-status-changed': 'audioMuteStatusChanged',
|
||||
'camera-error': 'cameraError',
|
||||
'device-list-changed': 'deviceListChanged',
|
||||
'display-name-change': 'displayNameChange',
|
||||
'email-change': 'emailChange',
|
||||
@@ -58,9 +60,11 @@ const events = {
|
||||
'feedback-prompt-displayed': 'feedbackPromptDisplayed',
|
||||
'filmstrip-display-changed': 'filmstripDisplayChanged',
|
||||
'incoming-message': 'incomingMessage',
|
||||
'mic-error': 'micError',
|
||||
'outgoing-message': 'outgoingMessage',
|
||||
'participant-joined': 'participantJoined',
|
||||
'participant-left': 'participantLeft',
|
||||
'password-required': 'passwordRequired',
|
||||
'proxy-connection-event': 'proxyConnectionEvent',
|
||||
'video-ready-to-close': 'readyToClose',
|
||||
'video-conference-joined': 'videoConferenceJoined',
|
||||
@@ -69,6 +73,7 @@ const events = {
|
||||
'video-mute-status-changed': 'videoMuteStatusChanged',
|
||||
'screen-sharing-status-changed': 'screenSharingStatusChanged',
|
||||
'subject-change': 'subjectChange',
|
||||
'suspend-detected': 'suspendDetected',
|
||||
'tile-view-changed': 'tileViewChanged'
|
||||
};
|
||||
|
||||
@@ -533,6 +538,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* {{
|
||||
* on: on //whether screen sharing is on
|
||||
* }}
|
||||
* {@code suspendDetected} - receives event notifications about detecting suspend event in host computer.
|
||||
* {@code readyToClose} - all hangup operations are completed and Jitsi Meet
|
||||
* is ready to be disposed.
|
||||
* @returns {void}
|
||||
|
||||
133
modules/UI/UI.js
@@ -13,16 +13,12 @@ import SharedVideoManager from './shared_video/SharedVideo';
|
||||
import VideoLayout from './videolayout/VideoLayout';
|
||||
import Filmstrip from './videolayout/Filmstrip';
|
||||
|
||||
import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet';
|
||||
import { getLocalParticipant } from '../../react/features/base/participants';
|
||||
import { toggleChat } from '../../react/features/chat';
|
||||
import { openDisplayNamePrompt } from '../../react/features/display-name';
|
||||
import { setEtherpadHasInitialzied } from '../../react/features/etherpad';
|
||||
import { setFilmstripVisible } from '../../react/features/filmstrip';
|
||||
import {
|
||||
setNotificationsEnabled,
|
||||
showWarningNotification
|
||||
} from '../../react/features/notifications';
|
||||
import { setNotificationsEnabled } from '../../react/features/notifications';
|
||||
import {
|
||||
dockToolbox,
|
||||
setToolboxEnabled,
|
||||
@@ -40,39 +36,6 @@ UI.eventEmitter = eventEmitter;
|
||||
let etherpadManager;
|
||||
let sharedVideoManager;
|
||||
|
||||
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
|
||||
microphone: {},
|
||||
camera: {}
|
||||
};
|
||||
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.camera[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]
|
||||
= 'dialog.cameraUnsupportedResolutionError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.GENERAL]
|
||||
= 'dialog.cameraUnknownError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.PERMISSION_DENIED]
|
||||
= 'dialog.cameraPermissionDeniedError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.NOT_FOUND]
|
||||
= 'dialog.cameraNotFoundError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.CONSTRAINT_FAILED]
|
||||
= 'dialog.cameraConstraintFailedError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.camera[JitsiTrackErrors.NO_DATA_FROM_SOURCE]
|
||||
= 'dialog.cameraNotSendingData';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[JitsiTrackErrors.GENERAL]
|
||||
= 'dialog.micUnknownError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.microphone[JitsiTrackErrors.PERMISSION_DENIED]
|
||||
= 'dialog.micPermissionDeniedError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[JitsiTrackErrors.NOT_FOUND]
|
||||
= 'dialog.micNotFoundError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.microphone[JitsiTrackErrors.CONSTRAINT_FAILED]
|
||||
= 'dialog.micConstraintFailedError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.microphone[JitsiTrackErrors.NO_DATA_FROM_SOURCE]
|
||||
= 'dialog.micNotSendingData';
|
||||
|
||||
const UIListeners = new Map([
|
||||
[
|
||||
UIEvents.ETHERPAD_CLICKED,
|
||||
@@ -136,17 +99,6 @@ UI.notifyReservationError = function(code, msg) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Notify user that he has been kicked from the server.
|
||||
*/
|
||||
UI.notifyKicked = function() {
|
||||
messageHandler.showError({
|
||||
hideErrorSupportLink: true,
|
||||
descriptionKey: 'dialog.kickMessage',
|
||||
titleKey: 'dialog.kickTitle'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Notify user that conference was destroyed.
|
||||
* @param reason {string} the reason text
|
||||
@@ -295,12 +247,6 @@ UI.addLocalStream = track => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removed remote stream from UI.
|
||||
* @param {JitsiTrack} track stream to remove
|
||||
*/
|
||||
UI.removeRemoteStream = track => VideoLayout.onRemoteStreamRemoved(track);
|
||||
|
||||
/**
|
||||
* Setup and show Etherpad.
|
||||
* @param {string} name etherpad id
|
||||
@@ -774,83 +720,6 @@ UI.showExtensionInlineInstallationDialog = function(callback) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a notifications about the passed in microphone error.
|
||||
*
|
||||
* @param {JitsiTrackError} micError - An error object related to using or
|
||||
* acquiring an audio stream.
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.showMicErrorNotification = function(micError) {
|
||||
if (!micError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { message, name } = micError;
|
||||
|
||||
const micJitsiTrackErrorMsg
|
||||
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name];
|
||||
const micErrorMsg = micJitsiTrackErrorMsg
|
||||
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.microphone[JitsiTrackErrors.GENERAL];
|
||||
const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message;
|
||||
|
||||
APP.store.dispatch(showWarningNotification({
|
||||
description: additionalMicErrorMsg,
|
||||
descriptionKey: micErrorMsg,
|
||||
titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
|
||||
? 'deviceError.microphonePermission'
|
||||
: 'deviceError.microphoneError'
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a notifications about the passed in camera error.
|
||||
*
|
||||
* @param {JitsiTrackError} cameraError - An error object related to using or
|
||||
* acquiring a video stream.
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.showCameraErrorNotification = function(cameraError) {
|
||||
if (!cameraError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { message, name } = cameraError;
|
||||
|
||||
const cameraJitsiTrackErrorMsg
|
||||
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name];
|
||||
const cameraErrorMsg = cameraJitsiTrackErrorMsg
|
||||
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.camera[JitsiTrackErrors.GENERAL];
|
||||
const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message;
|
||||
|
||||
APP.store.dispatch(showWarningNotification({
|
||||
description: additionalCameraErrorMsg,
|
||||
descriptionKey: cameraErrorMsg,
|
||||
titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
|
||||
? 'deviceError.cameraPermission' : 'deviceError.cameraError'
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows error dialog that informs the user that no data is received from the
|
||||
* device.
|
||||
*
|
||||
* @param {boolean} isAudioTrack - Whether or not the dialog is for an audio
|
||||
* track error.
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.showTrackNotWorkingDialog = function(isAudioTrack) {
|
||||
messageHandler.showError({
|
||||
descriptionKey: isAudioTrack
|
||||
? 'dialog.micNotSendingData' : 'dialog.cameraNotSendingData',
|
||||
titleKey: isAudioTrack
|
||||
? 'dialog.micNotSendingDataTitle'
|
||||
: 'dialog.cameraNotSendingDataTitle'
|
||||
});
|
||||
};
|
||||
|
||||
UI.updateDevicesAvailability = function(id, devices) {
|
||||
VideoLayout.setDeviceAvailabilityIcons(id, devices);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,6 @@ export default function SharedVideoThumb(participant, videoType, VideoLayout) {
|
||||
this.updateDisplayName();
|
||||
|
||||
this.container.onclick = this._onContainerClick;
|
||||
this.container.ondblclick = this._onContainerDoubleClick;
|
||||
}
|
||||
SharedVideoThumb.prototype = Object.create(SmallVideo.prototype);
|
||||
SharedVideoThumb.prototype.constructor = SharedVideoThumb;
|
||||
|
||||
@@ -361,6 +361,12 @@ const Filmstrip = {
|
||||
'min-width': `${local.thumbWidth}px`,
|
||||
width: `${local.thumbWidth}px`
|
||||
});
|
||||
|
||||
const avatarSize = local.thumbHeight / 2;
|
||||
|
||||
thumbs.localThumb.find('.avatar-container')
|
||||
.height(avatarSize)
|
||||
.width(avatarSize);
|
||||
}
|
||||
|
||||
if (thumbs.remoteThumbs) {
|
||||
@@ -371,6 +377,12 @@ const Filmstrip = {
|
||||
'min-width': `${remote.thumbWidth}px`,
|
||||
width: `${remote.thumbWidth}px`
|
||||
});
|
||||
|
||||
const avatarSize = remote.thumbHeight / 2;
|
||||
|
||||
thumbs.remoteThumbs.find('.avatar-container')
|
||||
.height(avatarSize)
|
||||
.width(avatarSize);
|
||||
}
|
||||
|
||||
const currentLayout = getCurrentLayout(APP.store.getState());
|
||||
|
||||
@@ -32,7 +32,7 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
|
||||
|
||||
this.localVideoId = null;
|
||||
this.bindHoverHandler();
|
||||
if (config.enableLocalVideoFlip) {
|
||||
if (!config.disableLocalVideoFlip) {
|
||||
this._buildContextMenu();
|
||||
}
|
||||
this.isLocal = true;
|
||||
@@ -62,7 +62,6 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
|
||||
this.updateIndicators();
|
||||
|
||||
this.container.onclick = this._onContainerClick;
|
||||
this.container.ondblclick = this._onContainerDoubleClick;
|
||||
}
|
||||
|
||||
LocalVideo.prototype = Object.create(SmallVideo.prototype);
|
||||
|
||||
@@ -89,7 +89,6 @@ function RemoteVideo(user, VideoLayout, emitter) {
|
||||
this._stopRemoteControl = this._stopRemoteControl.bind(this);
|
||||
|
||||
this.container.onclick = this._onContainerClick;
|
||||
this.container.ondblclick = this._onContainerDoubleClick;
|
||||
}
|
||||
|
||||
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
|
||||
@@ -165,7 +164,10 @@ RemoteVideo.prototype._generatePopupContent = function() {
|
||||
|
||||
const initialVolumeValue
|
||||
= this._audioStreamElement && this._audioStreamElement.volume;
|
||||
const onVolumeChange = this._setAudioVolume;
|
||||
|
||||
// hide volume when in silent mode
|
||||
const onVolumeChange = APP.store.getState()['features/base/config'].startSilent
|
||||
? undefined : this._setAudioVolume;
|
||||
const { isModerator } = APP.conference;
|
||||
const participantID = this.id;
|
||||
|
||||
@@ -357,6 +359,11 @@ RemoteVideo.prototype.removeRemoteStreamElement = function(stream) {
|
||||
logger.info(`${isVideo ? 'Video' : 'Audio'
|
||||
} removed ${this.id}`, select);
|
||||
|
||||
|
||||
if (stream === this.videoStream) {
|
||||
this.videoStream = null;
|
||||
}
|
||||
|
||||
this.updateView();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global $, APP, interfaceConfig */
|
||||
/* global $, APP, config, interfaceConfig */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React from 'react';
|
||||
@@ -144,7 +144,6 @@ function SmallVideo(VideoLayout) {
|
||||
this.updateView = this.updateView.bind(this);
|
||||
|
||||
this._onContainerClick = this._onContainerClick.bind(this);
|
||||
this._onContainerDoubleClick = this._onContainerDoubleClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,6 +198,8 @@ SmallVideo.createStreamElement = function(stream) {
|
||||
|
||||
if (isVideo) {
|
||||
element.setAttribute('muted', 'true');
|
||||
} else if (config.startSilent) {
|
||||
element.muted = true;
|
||||
}
|
||||
|
||||
element.autoplay = true;
|
||||
@@ -857,20 +858,6 @@ SmallVideo.prototype.updateIndicators = function() {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback invoked when the thumbnail is double clicked. Will pin the
|
||||
* participant if in tile view.
|
||||
*
|
||||
* @param {MouseEvent} event - The click event to intercept.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
SmallVideo.prototype._onContainerDoubleClick = function(event) {
|
||||
if (this._pinningRequiresDoubleClick() && this._shouldTriggerPin(event)) {
|
||||
APP.store.dispatch(pinParticipant(this.id));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback invoked when the thumbnail is clicked and potentially trigger
|
||||
* pinning of the participant.
|
||||
@@ -880,8 +867,7 @@ SmallVideo.prototype._onContainerDoubleClick = function(event) {
|
||||
* @returns {void}
|
||||
*/
|
||||
SmallVideo.prototype._onContainerClick = function(event) {
|
||||
const triggerPin = this._shouldTriggerPin(event)
|
||||
&& !this._pinningRequiresDoubleClick();
|
||||
const triggerPin = this._shouldTriggerPin(event);
|
||||
|
||||
if (event.stopPropagation && triggerPin) {
|
||||
event.stopPropagation();
|
||||
@@ -932,17 +918,6 @@ SmallVideo.prototype.togglePin = function() {
|
||||
APP.store.dispatch(pinParticipant(participantIdToPin));
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether or not clicking to pin the participant needs to be a double
|
||||
* click instead of a single click.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
SmallVideo.prototype._pinningRequiresDoubleClick = function() {
|
||||
return shouldDisplayTileView(APP.store.getState());
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the React element responsible for showing connection status, dominant
|
||||
* speaker, and raised hand icons.
|
||||
|
||||
@@ -348,10 +348,6 @@ const VideoLayout = {
|
||||
remoteVideo.removeRemoteStreamElement(stream);
|
||||
}
|
||||
|
||||
if (stream.isVideoTrack()) {
|
||||
this._updateLargeVideoIfDisplayed(id);
|
||||
}
|
||||
|
||||
this.updateMutedForNoTracks(id, stream.getType());
|
||||
},
|
||||
|
||||
@@ -641,7 +637,7 @@ const VideoLayout = {
|
||||
*/
|
||||
onVideoMute(id, value) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
localVideoThumbnail.setVideoMutedView(value);
|
||||
localVideoThumbnail && localVideoThumbnail.setVideoMutedView(value);
|
||||
} else {
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
@@ -989,7 +985,7 @@ const VideoLayout = {
|
||||
} else if (currentId) {
|
||||
const currentSmallVideo = this.getSmallVideo(currentId);
|
||||
|
||||
currentSmallVideo.updateView();
|
||||
currentSmallVideo && currentSmallVideo.updateView();
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/* global APP, JitsiMeetJS */
|
||||
|
||||
import { getAudioOutputDeviceId } from '../../react/features/base/devices';
|
||||
import {
|
||||
getAudioOutputDeviceId,
|
||||
notifyCameraError,
|
||||
notifyMicError
|
||||
} from '../../react/features/base/devices';
|
||||
import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
getUserSelectedMicDeviceId,
|
||||
@@ -176,11 +180,11 @@ export default {
|
||||
]))
|
||||
.then(tracks => {
|
||||
if (audioTrackError) {
|
||||
APP.UI.showMicErrorNotification(audioTrackError);
|
||||
APP.store.dispatch(notifyMicError(audioTrackError));
|
||||
}
|
||||
|
||||
if (videoTrackError) {
|
||||
APP.UI.showCameraErrorNotification(videoTrackError);
|
||||
APP.store.dispatch(notifyCameraError(videoTrackError));
|
||||
}
|
||||
|
||||
return tracks.filter(t => typeof t !== 'undefined');
|
||||
@@ -205,7 +209,7 @@ export default {
|
||||
})
|
||||
.catch(err => {
|
||||
audioTrackError = err;
|
||||
showError && APP.UI.showMicErrorNotification(err);
|
||||
showError && APP.store.disptach(notifyMicError(err));
|
||||
|
||||
return [];
|
||||
}));
|
||||
@@ -223,7 +227,7 @@ export default {
|
||||
})
|
||||
.catch(err => {
|
||||
videoTrackError = err;
|
||||
showError && APP.UI.showCameraErrorNotification(err);
|
||||
showError && APP.store.dispatch(notifyCameraError(err));
|
||||
|
||||
return [];
|
||||
}));
|
||||
|
||||
53
package-lock.json
generated
@@ -2840,7 +2840,7 @@
|
||||
"blueimp-md5": "^2.10.0",
|
||||
"json3": "^3.3.2",
|
||||
"lodash": "^4.17.4",
|
||||
"ua-parser-js": "github:amplitude/ua-parser-js#ed538f16f5c6ecd8357da989b617d4f156dcf35d"
|
||||
"ua-parser-js": "github:amplitude/ua-parser-js#ed538f1"
|
||||
},
|
||||
"dependencies": {
|
||||
"ua-parser-js": {
|
||||
@@ -6750,7 +6750,8 @@
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
@@ -6768,11 +6769,13 @@
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -6785,15 +6788,18 @@
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@@ -6896,7 +6902,8 @@
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@@ -6906,6 +6913,7 @@
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@@ -6918,17 +6926,20 @@
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.2.4",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.1",
|
||||
"yallist": "^3.0.0"
|
||||
@@ -6945,6 +6956,7 @@
|
||||
"mkdirp": {
|
||||
"version": "0.5.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "0.0.8"
|
||||
}
|
||||
@@ -7017,7 +7029,8 @@
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@@ -7027,6 +7040,7 @@
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -7102,7 +7116,8 @@
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.1",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@@ -7132,6 +7147,7 @@
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@@ -7149,6 +7165,7 @@
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
@@ -7187,11 +7204,13 @@
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.2",
|
||||
"bundled": true
|
||||
"bundled": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -8927,8 +8946,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#23503b0efd4b97989af7b10696ec9ae790aec7bb",
|
||||
"from": "github:jitsi/lib-jitsi-meet#23503b0efd4b97989af7b10696ec9ae790aec7bb",
|
||||
"version": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
|
||||
"from": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
|
||||
"requires": {
|
||||
"@jitsi/sdp-interop": "0.1.14",
|
||||
"@jitsi/sdp-simulcast": "0.2.1",
|
||||
@@ -12200,9 +12219,9 @@
|
||||
"integrity": "sha512-kEzgZxbbXYhy27u5GnhrKitn+XDBFAHSDUJdYC6llMi5cDPjgcqhOAQABj0K+ga5pn+/xPZLmD882rrUGiwVVA=="
|
||||
},
|
||||
"react-native-google-signin": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-google-signin/-/react-native-google-signin-1.0.2.tgz",
|
||||
"integrity": "sha512-4HPPSecI29gX0Pu7h2E7ZYXnKO4r+6eh5f+Unm67liE1RfvCQfOqoDliPbK96Mb/91VgHwqyxi0sUEC4j54/AQ=="
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-google-signin/-/react-native-google-signin-2.0.0.tgz",
|
||||
"integrity": "sha512-9loM4lcCIdbco5BnmNio7yGaXQKCpCaY1VRmYiTSvC5NjuSf6Ui6jZRee46p/YdaU4yRnS3u5Vct6Psrvr0HNg=="
|
||||
},
|
||||
"react-native-immersive": {
|
||||
"version": "2.0.0",
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"js-utils": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
|
||||
"jsrsasign": "8.0.12",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#23503b0efd4b97989af7b10696ec9ae790aec7bb",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.11",
|
||||
"moment": "2.19.4",
|
||||
@@ -68,7 +68,7 @@
|
||||
"react-native-calendar-events": "1.6.4",
|
||||
"react-native-callstats": "3.58.2",
|
||||
"react-native-fast-image": "5.1.1",
|
||||
"react-native-google-signin": "1.0.2",
|
||||
"react-native-google-signin": "2.0.0",
|
||||
"react-native-immersive": "2.0.0",
|
||||
"react-native-keep-awake": "4.0.0",
|
||||
"react-native-linear-gradient": "2.5.3",
|
||||
|
||||
@@ -271,6 +271,18 @@ export function createInviteDialogEvent(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an "offer/answer failure" event.
|
||||
*
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createOfferAnswerFailedEvent() {
|
||||
return {
|
||||
action: 'offer.answer.failure'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a "page reload" event.
|
||||
*
|
||||
@@ -372,6 +384,28 @@ export function createLiveStreamingDialogEvent(dialogName, buttonName) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event with the local tracks duration.
|
||||
*
|
||||
* @param {Object} duration - The object with the duration of the local tracks.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createLocalTracksDurationEvent(duration) {
|
||||
const { audio, video, conference } = duration;
|
||||
const { camera, desktop } = video;
|
||||
|
||||
return {
|
||||
action: 'local.tracks.durations',
|
||||
attributes: {
|
||||
audio: audio.value,
|
||||
camera: camera.value,
|
||||
conference: conference.value,
|
||||
desktop: desktop.value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that an action related to recording has
|
||||
* occured.
|
||||
@@ -529,6 +563,18 @@ export function createStartAudioOnlyEvent(audioOnly) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates the "start silent" configuration.
|
||||
*
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createStartSilentEvent() {
|
||||
return {
|
||||
action: 'start.silent'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates the "start muted" configuration.
|
||||
*
|
||||
|
||||
11
react/features/analytics/actionTypes.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that local media duration has changed.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_LOCAL_TRACKS_DURATION,
|
||||
* localTracksDuration: Object
|
||||
* }
|
||||
*/
|
||||
export const UPDATE_LOCAL_TRACKS_DURATION = 'UPDATE_LOCAL_TRACKS_DURATION';
|
||||
@@ -15,7 +15,7 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
const { amplitudeAPPKey, host } = options;
|
||||
const { amplitudeAPPKey, host, user } = options;
|
||||
|
||||
if (!amplitudeAPPKey) {
|
||||
throw new Error('Failed to initialize Amplitude handler, no APP key');
|
||||
@@ -28,6 +28,10 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
};
|
||||
|
||||
amplitude.getInstance(this._amplitudeOptions).init(amplitudeAPPKey);
|
||||
|
||||
if (user) {
|
||||
amplitude.getInstance(this._amplitudeOptions).setUserId(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,16 @@ class Amplitude {
|
||||
AmplitudeNative.init(this._instanceName, apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an identifier for the current user.
|
||||
*
|
||||
* @param {string} userId - The new user id.
|
||||
* @returns {void}
|
||||
*/
|
||||
setUserId(userId) {
|
||||
AmplitudeNative.setUserId(this._instanceName, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets user properties for the current user.
|
||||
*
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './AnalyticsEvents';
|
||||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
||||
@@ -1,8 +1,74 @@
|
||||
import { SET_ROOM } from '../base/conference';
|
||||
// @flow
|
||||
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
SET_ROOM
|
||||
} from '../base/conference';
|
||||
import { SET_CONFIG } from '../base/config';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import {
|
||||
getLocalAudioTrack,
|
||||
getLocalVideoTrack,
|
||||
TRACK_ADDED,
|
||||
TRACK_REMOVED,
|
||||
TRACK_UPDATED
|
||||
} from '../base/tracks';
|
||||
|
||||
import { initAnalytics, resetAnalytics } from './functions';
|
||||
import { UPDATE_LOCAL_TRACKS_DURATION } from './actionTypes';
|
||||
import { createLocalTracksDurationEvent } from './AnalyticsEvents';
|
||||
import { initAnalytics, resetAnalytics, sendAnalytics } from './functions';
|
||||
|
||||
/**
|
||||
* Calculates the duration of the local tracks.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object} - The local tracks duration.
|
||||
*/
|
||||
function calculateLocalTrackDuration(state) {
|
||||
const now = Date.now();
|
||||
const { localTracksDuration } = state['features/analytics'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { audio, video } = localTracksDuration;
|
||||
const { camera, desktop } = video;
|
||||
const tracks = state['features/base/tracks'];
|
||||
const audioTrack = getLocalAudioTrack(tracks);
|
||||
const videoTrack = getLocalVideoTrack(tracks);
|
||||
const newDuration = { ...localTracksDuration };
|
||||
|
||||
if (!audioTrack || audioTrack.muted || !conference) {
|
||||
newDuration.audio = {
|
||||
startedTime: -1,
|
||||
value: audio.value + (audio.startedTime === -1 ? 0 : now - audio.startedTime)
|
||||
};
|
||||
} else if (audio.startedTime === -1) {
|
||||
newDuration.audio.startedTime = now;
|
||||
}
|
||||
|
||||
if (!videoTrack || videoTrack.muted || !conference) {
|
||||
newDuration.video = {
|
||||
camera: {
|
||||
startedTime: -1,
|
||||
value: camera.value + (camera.startedTime === -1 ? 0 : now - camera.startedTime)
|
||||
},
|
||||
desktop: {
|
||||
startedTime: -1,
|
||||
value: desktop.value + (desktop.startedTime === -1 ? 0 : now - desktop.startedTime)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const { videoType } = videoTrack;
|
||||
|
||||
if (video[videoType].startedTime === -1) {
|
||||
newDuration.video[videoType].startedTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...localTracksDuration,
|
||||
...newDuration
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware which intercepts config actions to handle evaluating analytics
|
||||
@@ -12,25 +78,81 @@ import { initAnalytics, resetAnalytics } from './functions';
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case SET_CONFIG: {
|
||||
if (action.type === SET_CONFIG) {
|
||||
if (navigator.product === 'ReactNative') {
|
||||
// Reseting the analytics is currently not needed for web because
|
||||
// the user will be redirected to another page and new instance of
|
||||
// Analytics will be created and initialized.
|
||||
resetAnalytics();
|
||||
}
|
||||
}
|
||||
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED: {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_LOCAL_TRACKS_DURATION,
|
||||
localTracksDuration: {
|
||||
...calculateLocalTrackDuration(state),
|
||||
conference: {
|
||||
startedTime: Date.now(),
|
||||
value: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_WILL_LEAVE: {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const { localTracksDuration } = state['features/analytics'];
|
||||
const newLocalTracksDuration = {
|
||||
...calculateLocalTrackDuration(state),
|
||||
conference: {
|
||||
startedTime: -1,
|
||||
value: Date.now() - localTracksDuration.conference.startedTime
|
||||
}
|
||||
};
|
||||
|
||||
sendAnalytics(createLocalTracksDurationEvent(newLocalTracksDuration));
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_LOCAL_TRACKS_DURATION,
|
||||
localTracksDuration: newLocalTracksDuration
|
||||
});
|
||||
break;
|
||||
}
|
||||
case SET_ROOM: {
|
||||
const result = next(action);
|
||||
|
||||
initAnalytics(store);
|
||||
break;
|
||||
}
|
||||
case TRACK_ADDED:
|
||||
case TRACK_REMOVED:
|
||||
case TRACK_UPDATED: {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const { localTracksDuration } = state['features/analytics'];
|
||||
|
||||
return result;
|
||||
if (localTracksDuration.conference.startedTime === -1) {
|
||||
// We don't want to track the media duration if the conference is not joined yet because otherwise we won't
|
||||
// be able to compare them with the conference duration (from conference join to conference will leave).
|
||||
break;
|
||||
}
|
||||
dispatch({
|
||||
type: UPDATE_LOCAL_TRACKS_DURATION,
|
||||
localTracksDuration: {
|
||||
...localTracksDuration,
|
||||
...calculateLocalTrackDuration(state)
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
return result;
|
||||
});
|
||||
|
||||
51
react/features/analytics/reducer.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { UPDATE_LOCAL_TRACKS_DURATION } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Initial state.
|
||||
*/
|
||||
const DEFAULT_STATE = {
|
||||
localTracksDuration: {
|
||||
audio: {
|
||||
startedTime: -1,
|
||||
value: 0
|
||||
},
|
||||
video: {
|
||||
camera: {
|
||||
startedTime: -1,
|
||||
value: 0
|
||||
},
|
||||
desktop: {
|
||||
startedTime: -1,
|
||||
value: 0
|
||||
}
|
||||
},
|
||||
conference: {
|
||||
startedTime: -1,
|
||||
value: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for actions which changes the state of the analytics feature.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature features/analytics.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @returns {Object}
|
||||
*/
|
||||
ReducerRegistry.register('features/analytics', (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case UPDATE_LOCAL_TRACKS_DURATION:
|
||||
return {
|
||||
...state,
|
||||
localTracksDuration: action.localTracksDuration
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
setConfig,
|
||||
storeConfig
|
||||
} from '../base/config';
|
||||
import { setLocationURL } from '../base/connection';
|
||||
import { connect, disconnect, setLocationURL } from '../base/connection';
|
||||
import { loadConfig } from '../base/lib-jitsi-meet';
|
||||
import { createDesiredLocalTracks } from '../base/tracks';
|
||||
import { parseURIString, toURLString } from '../base/util';
|
||||
import { setFatalError } from '../overlay';
|
||||
|
||||
@@ -58,6 +59,12 @@ export function appNavigate(uri: ?string) {
|
||||
const { contextRoot, host, room } = location;
|
||||
const locationURL = new URL(location.toString());
|
||||
|
||||
// Disconnect from any current conference.
|
||||
// FIXME: unify with web.
|
||||
if (navigator.product === 'ReactNative') {
|
||||
dispatch(disconnect());
|
||||
}
|
||||
|
||||
dispatch(configWillLoad(locationURL, room));
|
||||
|
||||
let protocol = location.protocol.toLowerCase();
|
||||
@@ -74,25 +81,40 @@ export function appNavigate(uri: ?string) {
|
||||
|
||||
let config;
|
||||
|
||||
try {
|
||||
config = await loadConfig(url);
|
||||
dispatch(storeConfig(baseURL, config));
|
||||
} catch (error) {
|
||||
// Avoid (re)loading the config when there is no room.
|
||||
if (!room) {
|
||||
config = restoreConfig(baseURL);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
dispatch(loadConfigError(error, locationURL));
|
||||
if (!config) {
|
||||
try {
|
||||
config = await loadConfig(url);
|
||||
dispatch(storeConfig(baseURL, config));
|
||||
} catch (error) {
|
||||
config = restoreConfig(baseURL);
|
||||
|
||||
return;
|
||||
if (!config) {
|
||||
dispatch(loadConfigError(error, locationURL));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (getState()['features/base/config'].locationURL === locationURL) {
|
||||
dispatch(setLocationURL(locationURL));
|
||||
dispatch(setConfig(config));
|
||||
dispatch(setRoom(room));
|
||||
} else {
|
||||
if (getState()['features/base/config'].locationURL !== locationURL) {
|
||||
dispatch(loadConfigError(new Error('Config no longer needed!'), locationURL));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(setLocationURL(locationURL));
|
||||
dispatch(setConfig(config));
|
||||
dispatch(setRoom(room));
|
||||
|
||||
// FIXME: unify with web, currently the connection and track creation happens in conference.js.
|
||||
if (room && navigator.product === 'ReactNative') {
|
||||
dispatch(createDesiredLocalTracks());
|
||||
dispatch(connect());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import '../../analytics';
|
||||
import '../../authentication';
|
||||
import { setColorScheme } from '../../base/color-scheme';
|
||||
import { DialogContainer } from '../../base/dialog';
|
||||
import { updateFlags } from '../../base/flags';
|
||||
import '../../base/jwt';
|
||||
import { Platform } from '../../base/react';
|
||||
import {
|
||||
AspectRatioDetector,
|
||||
ReducedUIDetector
|
||||
} from '../../base/responsive-ui';
|
||||
import { updateSettings } from '../../base/settings';
|
||||
import '../../google-api';
|
||||
import '../../mobile/audio-mode';
|
||||
import '../../mobile/background';
|
||||
@@ -47,18 +49,14 @@ type Props = AbstractAppProps & {
|
||||
externalAPIScope: string,
|
||||
|
||||
/**
|
||||
* Whether Picture-in-Picture is enabled. If {@code true}, a toolbar button
|
||||
* is rendered in the {@link Conference} view to afford entering
|
||||
* Picture-in-Picture.
|
||||
* An object with the feature flags.
|
||||
*/
|
||||
pictureInPictureEnabled: boolean,
|
||||
flags: Object,
|
||||
|
||||
/**
|
||||
* Whether the Welcome page is enabled. If {@code true}, the Welcome page is
|
||||
* rendered when the {@link App} is not at a location (URL) identifying
|
||||
* a Jitsi Meet conference/room.
|
||||
* An object with user information (display name, email, avatar URL).
|
||||
*/
|
||||
welcomePageEnabled: boolean
|
||||
userInfo: ?Object
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -96,9 +94,12 @@ export class App extends AbstractApp {
|
||||
super.componentDidMount();
|
||||
|
||||
this._init.then(() => {
|
||||
// We set the color scheme early enough so then we avoid any
|
||||
// unnecessary re-renders.
|
||||
this.state.store.dispatch(setColorScheme(this.props.colorScheme));
|
||||
// We set these early enough so then we avoid any unnecessary re-renders.
|
||||
const { dispatch } = this.state.store;
|
||||
|
||||
dispatch(setColorScheme(this.props.colorScheme));
|
||||
dispatch(updateFlags(this.props.flags));
|
||||
dispatch(updateSettings(this.props.userInfo || {}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import React from 'react';
|
||||
import { DialogContainer } from '../../base/dialog';
|
||||
import '../../base/responsive-ui';
|
||||
import '../../chat';
|
||||
import '../../external-api';
|
||||
import '../../room-lock';
|
||||
import '../../video-layout';
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ import { generateRoomWithoutSeparator } from 'js-utils/random';
|
||||
import type { Component } from 'react';
|
||||
|
||||
import { isRoomValid } from '../base/conference';
|
||||
import JitsiMeetJS from '../base/lib-jitsi-meet';
|
||||
import { Platform } from '../base/react';
|
||||
import { toState } from '../base/redux';
|
||||
import { isSupportedBrowser } from '../base/environment';
|
||||
import { Conference } from '../conference';
|
||||
import { getDeepLinkingPage } from '../deep-linking';
|
||||
import { UnsupportedDesktopBrowser } from '../unsupported-browser';
|
||||
@@ -40,69 +39,109 @@ export type Route = {
|
||||
*/
|
||||
export function _getRouteToRender(stateful: Function | Object): Promise<Route> {
|
||||
const state = toState(stateful);
|
||||
const { room } = state['features/base/conference'];
|
||||
const isMobileApp = navigator.product === 'ReactNative';
|
||||
const isMobileBrowser
|
||||
= !isMobileApp && (Platform.OS === 'android' || Platform.OS === 'ios');
|
||||
const route: Route = {
|
||||
|
||||
if (navigator.product === 'ReactNative') {
|
||||
return _getMobileRoute(state);
|
||||
}
|
||||
|
||||
return _getWebConferenceRoute(state) || _getWebWelcomePageRoute(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code Route} to display on the React Native app.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Promise<Route>}
|
||||
*/
|
||||
function _getMobileRoute(state): Promise<Route> {
|
||||
const route = _getEmptyRoute();
|
||||
|
||||
if (isRoomValid(state['features/base/conference'].room)) {
|
||||
route.component = Conference;
|
||||
} else if (isWelcomePageAppEnabled(state)) {
|
||||
route.component = WelcomePage;
|
||||
} else {
|
||||
route.component = BlankPage;
|
||||
}
|
||||
|
||||
return Promise.resolve(route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code Route} to display when trying to access a conference if
|
||||
* a valid conference is being joined.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Promise<Route>|undefined}
|
||||
*/
|
||||
function _getWebConferenceRoute(state): ?Promise<Route> {
|
||||
if (!isRoomValid(state['features/base/conference'].room)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const route = _getEmptyRoute();
|
||||
|
||||
// Update the location if it doesn't match. This happens when a room is
|
||||
// joined from the welcome page. The reason for doing this instead of using
|
||||
// the history API is that we want to load the config.js which takes the
|
||||
// room into account.
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
if (window.location.href !== locationURL.href) {
|
||||
route.href = locationURL.href;
|
||||
|
||||
return Promise.resolve(route);
|
||||
}
|
||||
|
||||
return getDeepLinkingPage(state)
|
||||
.then(deepLinkComponent => {
|
||||
if (deepLinkComponent) {
|
||||
route.component = deepLinkComponent;
|
||||
} else if (isSupportedBrowser()) {
|
||||
route.component = Conference;
|
||||
} else {
|
||||
route.component = UnsupportedDesktopBrowser;
|
||||
}
|
||||
|
||||
return route;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@code Route} to display when trying to access the welcome page.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Promise<Route>}
|
||||
*/
|
||||
function _getWebWelcomePageRoute(state): Promise<Route> {
|
||||
const route = _getEmptyRoute();
|
||||
|
||||
if (isWelcomePageUserEnabled(state)) {
|
||||
if (isSupportedBrowser()) {
|
||||
route.component = WelcomePage;
|
||||
} else {
|
||||
route.component = UnsupportedDesktopBrowser;
|
||||
}
|
||||
} else {
|
||||
// Web: if the welcome page is disabled, go directly to a random room.
|
||||
|
||||
let href = window.location.href;
|
||||
|
||||
href.endsWith('/') || (href += '/');
|
||||
route.href = href + generateRoomWithoutSeparator();
|
||||
}
|
||||
|
||||
return Promise.resolve(route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default {@code Route}.
|
||||
*
|
||||
* @returns {Route}
|
||||
*/
|
||||
function _getEmptyRoute(): Route {
|
||||
return {
|
||||
component: BlankPage,
|
||||
href: undefined
|
||||
};
|
||||
|
||||
return new Promise(resolve => {
|
||||
// First, check if the current endpoint supports WebRTC. We are
|
||||
// intentionally not performing the check for mobile browsers because:
|
||||
// - the WelcomePage is mobile ready;
|
||||
// - if the URL points to a conference, getDeepLinkingPage will take
|
||||
// care of it.
|
||||
if (!isMobileBrowser && !JitsiMeetJS.isWebRtcSupported()) {
|
||||
route.component = UnsupportedDesktopBrowser;
|
||||
resolve(route);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRoomValid(room)) {
|
||||
if (isMobileApp) {
|
||||
route.component = Conference;
|
||||
resolve(route);
|
||||
} else {
|
||||
// Update the location if it doesn't match. This happens when a
|
||||
// room is joined from the welcome page. The reason for doing
|
||||
// this instead of using the history API is that we want to load
|
||||
// the config.js which takes the room into account.
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
// eslint-disable-next-line no-negated-condition
|
||||
if (window.location.href !== locationURL.href) {
|
||||
route.href = locationURL.href;
|
||||
resolve(route);
|
||||
} else {
|
||||
// Maybe show deep-linking, otherwise go to Conference.
|
||||
getDeepLinkingPage(state).then(component => {
|
||||
route.component = component || Conference;
|
||||
resolve(route);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isWelcomePageUserEnabled(state)) {
|
||||
// Web: if the welcome page is disabled, go directly to a random
|
||||
// room.
|
||||
|
||||
let href = window.location.href;
|
||||
|
||||
href.endsWith('/') || (href += '/');
|
||||
route.href = href + generateRoomWithoutSeparator();
|
||||
} else if (isWelcomePageAppEnabled(state)) {
|
||||
// Mobile: only go to the welcome page if enabled.
|
||||
|
||||
route.component = WelcomePage;
|
||||
}
|
||||
|
||||
resolve(route);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,12 +64,14 @@ class WaitForOwnerDialog extends Component<Props> {
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
cancelKey = 'dialog.Cancel'
|
||||
contentKey = {
|
||||
{
|
||||
key: 'dialog.WaitForHostMsgWOk',
|
||||
params: { room }
|
||||
}
|
||||
}
|
||||
okKey = 'dialog.Ok'
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onLogin } />
|
||||
);
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
dominantSpeakerChanged,
|
||||
getNormalizedDisplayName,
|
||||
participantConnectionStatusChanged,
|
||||
participantKicked,
|
||||
participantMutedUs,
|
||||
participantPresenceChanged,
|
||||
participantRoleChanged,
|
||||
participantUpdated
|
||||
@@ -84,10 +86,16 @@ function _addConferenceListeners(conference, dispatch) {
|
||||
conference.on(
|
||||
JitsiConferenceEvents.CONFERENCE_LEFT,
|
||||
(...args) => dispatch(conferenceLeft(conference, ...args)));
|
||||
conference.on(JitsiConferenceEvents.SUBJECT_CHANGED,
|
||||
(...args) => dispatch(conferenceSubjectChanged(...args)));
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.KICKED,
|
||||
() => dispatch(kickedOut(conference)));
|
||||
(...args) => dispatch(kickedOut(conference, ...args)));
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.PARTICIPANT_KICKED,
|
||||
(kicker, kicked) => dispatch(participantKicked(kicker, kicked)));
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.LOCK_STATE_CHANGED,
|
||||
@@ -127,6 +135,14 @@ function _addConferenceListeners(conference, dispatch) {
|
||||
JitsiConferenceEvents.TRACK_REMOVED,
|
||||
t => t && !t.isLocal() && dispatch(trackRemoved(t)));
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.TRACK_MUTE_CHANGED,
|
||||
(_, participantThatMutedUs) => {
|
||||
if (participantThatMutedUs) {
|
||||
dispatch(participantMutedUs(participantThatMutedUs));
|
||||
}
|
||||
});
|
||||
|
||||
// Dispatches into features/base/participants follow:
|
||||
conference.on(
|
||||
JitsiConferenceEvents.DISPLAY_NAME_CHANGED,
|
||||
@@ -430,15 +446,19 @@ export function dataChannelOpened() {
|
||||
*
|
||||
* @param {JitsiConference} conference - The {@link JitsiConference} instance
|
||||
* for which the event is being signaled.
|
||||
* @param {JitsiParticipant} participant - The {@link JitsiParticipant}
|
||||
* instance which initiated the kick event.
|
||||
* @returns {{
|
||||
* type: KICKED_OUT,
|
||||
* conference: JitsiConference
|
||||
* conference: JitsiConference,
|
||||
* participant: JitsiParticipant
|
||||
* }}
|
||||
*/
|
||||
export function kickedOut(conference: Object) {
|
||||
export function kickedOut(conference: Object, participant: Object) {
|
||||
return {
|
||||
type: KICKED_OUT,
|
||||
conference
|
||||
conference,
|
||||
participant
|
||||
};
|
||||
}
|
||||
|
||||
@@ -757,10 +777,6 @@ export function setSubject(subject: string = '') {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
|
||||
if (conference) {
|
||||
dispatch({
|
||||
type: SET_PENDING_SUBJECT_CHANGE,
|
||||
subject: undefined
|
||||
});
|
||||
conference.setSubject(subject);
|
||||
} else {
|
||||
dispatch({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
@@ -155,10 +157,10 @@ export function forEachConference(
|
||||
export function getConferenceName(stateful: Function | Object): string {
|
||||
const state = toState(stateful);
|
||||
const { callee } = state['features/base/jwt'];
|
||||
const { callDisplayName } = state['features/base/config'];
|
||||
const { pendingSubjectChange, room, subject } = state['features/base/conference'];
|
||||
|
||||
return state['features/base/config'].callDisplayName
|
||||
|| (callee && callee.name)
|
||||
|| state['features/base/conference'].room;
|
||||
return pendingSubjectChange || subject || callDisplayName || (callee && callee.name) || _.startCase(room);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,10 +6,12 @@ import {
|
||||
ACTION_UNPINNED,
|
||||
createAudioOnlyChangedEvent,
|
||||
createConnectionEvent,
|
||||
createOfferAnswerFailedEvent,
|
||||
createPinnedEvent,
|
||||
sendAnalytics
|
||||
} from '../../analytics';
|
||||
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection';
|
||||
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
|
||||
import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
@@ -24,7 +26,6 @@ import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
|
||||
|
||||
import {
|
||||
conferenceFailed,
|
||||
conferenceLeft,
|
||||
conferenceWillLeave,
|
||||
createConference,
|
||||
setLastN,
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
DATA_CHANNEL_OPENED,
|
||||
SET_AUDIO_ONLY,
|
||||
SET_LASTN,
|
||||
SET_PENDING_SUBJECT_CHANGE,
|
||||
SET_ROOM
|
||||
} from './actionTypes';
|
||||
import {
|
||||
@@ -152,6 +154,12 @@ StateListenerRegistry.register(
|
||||
function _conferenceFailed(store, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
const { conference, error } = action;
|
||||
|
||||
if (error.name === JitsiConferenceErrors.OFFER_ANSWER_FAILED) {
|
||||
sendAnalytics(createOfferAnswerFailedEvent());
|
||||
}
|
||||
|
||||
// FIXME: Workaround for the web version. Currently, the creation of the
|
||||
// conference is handled by /conference.js and appropriate failure handlers
|
||||
// are set there.
|
||||
@@ -165,8 +173,6 @@ function _conferenceFailed(store, next, action) {
|
||||
}
|
||||
|
||||
// XXX After next(action), it is clear whether the error is recoverable.
|
||||
const { conference, error } = action;
|
||||
|
||||
!error.recoverable
|
||||
&& conference
|
||||
&& conference.leave().catch(reason => {
|
||||
@@ -329,10 +335,17 @@ function _connectionFailed({ dispatch, getState }, next, action) {
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceSubjectChanged({ getState }, next, action) {
|
||||
function _conferenceSubjectChanged({ dispatch, getState }, next, action) {
|
||||
const result = next(action);
|
||||
const { subject } = getState()['features/base/conference'];
|
||||
|
||||
if (subject) {
|
||||
dispatch({
|
||||
type: SET_PENDING_SUBJECT_CHANGE,
|
||||
subject: undefined
|
||||
});
|
||||
}
|
||||
|
||||
typeof APP === 'object' && APP.API.notifySubjectChanged(subject);
|
||||
|
||||
return result;
|
||||
@@ -567,48 +580,30 @@ function _setReceiverVideoConstraint(conference, preferred, max) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature {@code base/conference} that the redix action
|
||||
* {@link SET_ROOM} is being dispatched within a specific redux store.
|
||||
* Notifies the feature base/conference that the action
|
||||
* {@code SET_ROOM} is being dispatched within a specific
|
||||
* redux store.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code SET_ROOM} which is being
|
||||
* dispatched in the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code SET_ROOM}
|
||||
* which is being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _setRoom({ dispatch, getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
// By the time SET_ROOM is dispatched, base/connection's locationURL and
|
||||
// base/conference's leaving should be the only conference-related sources
|
||||
// of truth.
|
||||
const state = getState();
|
||||
const { leaving } = state['features/base/conference'];
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
const dispatchConferenceLeft = new Set();
|
||||
const { subject } = state['features/base/config'];
|
||||
const { room } = action;
|
||||
|
||||
// Figure out which of the JitsiConferences referenced by base/conference
|
||||
// have not dispatched or are not likely to dispatch CONFERENCE_FAILED and
|
||||
// CONFERENCE_LEFT.
|
||||
forEachConference(state, (conference, url) => {
|
||||
if (conference !== leaving && url && url !== locationURL) {
|
||||
dispatchConferenceLeft.add(conference);
|
||||
}
|
||||
|
||||
return true; // All JitsiConference instances are to be examined.
|
||||
});
|
||||
|
||||
// Dispatch CONFERENCE_LEFT for the JitsiConferences referenced by
|
||||
// base/conference which have not dispatched or are not likely to dispatch
|
||||
// CONFERENCE_FAILED or CONFERENCE_LEFT.
|
||||
for (const conference of dispatchConferenceLeft) {
|
||||
dispatch(conferenceLeft(conference));
|
||||
if (room) {
|
||||
// Set the stored subject.
|
||||
dispatch(setSubject(subject));
|
||||
}
|
||||
|
||||
return result;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -213,7 +213,8 @@ function _conferenceJoined(state, { conference }) {
|
||||
// i.e. password-protected is private to lib-jitsi-meet. However, the
|
||||
// library does not fire LOCK_STATE_CHANGED upon joining a JitsiConference
|
||||
// with a password.
|
||||
const locked = conference.room.locked ? LOCKED_REMOTELY : undefined;
|
||||
// FIXME Technically JitsiConference.room is a private field.
|
||||
const locked = conference.room && conference.room.locked ? LOCKED_REMOTELY : undefined;
|
||||
|
||||
return assign(state, {
|
||||
authRequired: undefined,
|
||||
|
||||
@@ -21,6 +21,7 @@ const WHITELISTED_KEYS = [
|
||||
'_peerConnStatusOutOfLastNTimeout',
|
||||
'_peerConnStatusRtcMuteTimeout',
|
||||
'abTesting',
|
||||
'analytics.disabled',
|
||||
'autoRecord',
|
||||
'autoRecordToken',
|
||||
'avgRtpStatsN',
|
||||
@@ -101,7 +102,7 @@ const WHITELISTED_KEYS = [
|
||||
'enableDisplayNameInStats',
|
||||
'enableLayerSuspension',
|
||||
'enableLipSync',
|
||||
'enableLocalVideoFlip',
|
||||
'disableLocalVideoFlip',
|
||||
'enableRemb',
|
||||
'enableStatsID',
|
||||
'enableTalkWhileMuted',
|
||||
@@ -131,10 +132,12 @@ const WHITELISTED_KEYS = [
|
||||
'startAudioMuted',
|
||||
'startAudioOnly',
|
||||
'startBitrate',
|
||||
'startSilent',
|
||||
'startScreenSharing',
|
||||
'startVideoMuted',
|
||||
'startWithAudioMuted',
|
||||
'startWithVideoMuted',
|
||||
'subject',
|
||||
'testing',
|
||||
'useIPv6',
|
||||
'useNicks',
|
||||
|
||||
@@ -370,10 +370,7 @@ export function disconnect() {
|
||||
if (connection_) {
|
||||
promise = promise.then(() => connection_.disconnect());
|
||||
} else {
|
||||
// FIXME: We have no connection! Fake a disconnect. Because of how the current disconnec is implemented
|
||||
// (by doing the diconnect() in the Conference component unmount) we have lost the location URL already.
|
||||
// Oh well, at least send the event.
|
||||
promise.then(() => dispatch(_connectionDisconnected({}, '')));
|
||||
logger.info('No connection found while disconnecting.');
|
||||
}
|
||||
|
||||
return promise;
|
||||
|
||||