Compare commits

...

74 Commits

Author SHA1 Message Date
damencho
f77976b742 Notify for detecting suspend. 2019-06-26 17:10:34 +01:00
Saúl Ibarra Corretgé
9e95e7cd97 rn: bump SDK version to 2.2.0 2019-06-26 12:04:17 +02:00
Leonard Kim
9d94257e79 ref(api): move conference join notification to middleware 2019-06-26 10:47:18 +02:00
Leonard Kim
13cfd61c83 fix(index): set reload link as attribute, not raw html 2019-06-25 12:48:31 -07:00
Leonard Kim
fa818bc386 feat(screenshare): allow auto-pin remote only 2019-06-25 11:21:23 +01:00
Saúl Ibarra Corretgé
a73a642c64 rn: fix CallKit failure screen flash when kicked
Pretend we have left instead of triggering a call failure. The user was already
told in the dialog.
2019-06-25 10:25:33 +02:00
Leonard Kim
94b3f6410d ref(api): move local participant name change 2019-06-24 13:15:02 -07:00
Leonard Kim
3d30f6e9cd ref(api): move filmstrip display notification 2019-06-24 13:15:02 -07:00
Leonard Kim
40c16f0bac ref(api): move tile view subscriber to api dir 2019-06-24 13:15:02 -07:00
Leonard Kim
a1db63a8c2 ref(api): move feedbackSubmitted notification to api middleware 2019-06-24 13:15:02 -07:00
Leonard Kim
59a9c2d947 fix(notifications): show notifications after reducers update to get name
Otherwise the participant will not have been added yet
to state so the participant name will not display in
the notification.
2019-06-24 10:51:17 -07:00
damencho
96f013c549 Fixes few undefined errors on standby. 2019-06-24 12:06:27 +01:00
damencho
742905e05a Removes duplicated text. 2019-06-24 12:06:27 +01:00
damencho
bde44a94e8 Changes display name prompt to non modal, so people can mute/unmute. 2019-06-24 12:06:27 +01:00
Saúl Ibarra Corretgé
1786bfadce android: make some activity attributes private
They are only meant to be used internally. This reduces the risk of SDK users
sending garbage when we expect a specific type.
2019-06-21 21:48:46 +02:00
Saúl Ibarra Corretgé
b2e840636a sounds: lower log severity
When native SDK users end a meeting the view gets disposed and detached from
React, and then the entire app gets destroyed and these errors get printed at
the error level, throwing some people off.
2019-06-21 21:48:46 +02:00
Saúl Ibarra Corretgé
ddaa22048f android: make sure we left the current meeting when the activity is destroyed
If the user swipes the activity from the recents list there is no other chance,
onDestroy gets called directly.
2019-06-21 21:48:46 +02:00
Saúl Ibarra Corretgé
3e77890387 android: fix not calling super in onNewIntent 2019-06-21 21:48:46 +02:00
Leonard Kim
1e39c12963 fix(suboptimal): allow checks for chromium based browsers 2019-06-21 09:55:43 -07:00
Saúl Ibarra Corretgé
243fdba80f android: fix exception when sending events without URL
ENTER_PIP_MODE, for example, does not have it.
2019-06-21 15:42:10 +02:00
Дамян Минков
08c4933c1b Fixes inviting more than one participant (#4352)
* Fixes inviting more than one participant.

* Shows a notification when participants are invited.

* Adds support for both .id and .user_id props for people query results.
2019-06-21 13:17:47 +01:00
Leonard Kim
d5e0dea469 fix(suboptimal): ignore mobile browsers in whitelists/blacklists 2019-06-21 11:01:40 +01:00
paweldomas
033aa0dd6e fix(base/conference/reducer): check if room is defined
This was hit on a corner case when ConnectionService will deny
the request to start the call. I am not sure, but it could have been
that the conference object has been disposed or closed or something
else, but the fact is that 'conference.room' was not defined and things
crashed. It is not safe to access conference's private field 'room'. It
is true JitsiConference doesn't follow the practice of marking this
field as private with the underscore '_', but it is not a public field.
2019-06-20 13:19:04 -05:00
Saúl Ibarra Corretgé
803870ef8f android: add a uncaught exception handler
Its main task is to cleanup conferences (specially the connection services
stuff) to make sure the system is left in a working state even when the
unexpected happens.
2019-06-20 17:03:24 +02:00
virtuacoplenny
bf67a4a675 Configurable (sub)optimal and unsupported browsers (#4351)
* Configurable (sub)optimal and unsupported browsers

* maybe this helps somehow
2019-06-20 14:02:15 +01:00
Bettenbuk Zoltan
ee2036a2a7 feat: show on stage function 2019-06-20 11:05:34 +02:00
Bettenbuk Zoltan
4c3ed190f3 feat: add ability to toggle toolbox from tile view 2019-06-20 11:05:34 +02:00
Hristo Terezov
a91b49c2c1 feat(reload): on offer / answer error. 2019-06-19 05:25:43 -07:00
Saúl Ibarra Corretgé
186ba70cb7 participants: skip local participant join notification
Also, use the helper function to get the display name for a participant.
2019-06-19 13:15:17 +02:00
Leonard Kim
12c18657d5 fix(welcome-page): make Tabs more resilient to bad props
In case wrong props are passed in, such as pointing to
an index that does not contain a value in the tabs
array.
2019-06-18 17:55:21 -07:00
Leonard Kim
9f8e7d4050 chore(deps): bump lib to a89d673 for caps version fixes 2019-06-18 17:24:12 -07:00
Leonard Kim
4cea7018f5 fix(tile-view): back to single click to pin
Undoes the logic added in:
ebcde745ef
2019-06-18 15:14:20 -07:00
Saúl Ibarra Corretgé
54a9b9199e rn: adapt to API changes in google signin 2019-06-18 14:51:44 +02:00
Saúl Ibarra Corretgé
4591b36c3e android: handle onActivityResult Activity lifecycle method
It may be called in the Activity instead of in the Fragment. Handle both.
2019-06-18 14:51:44 +02:00
Saúl Ibarra Corretgé
db862b5b3b misc: add Android debug builds to .gitignore 2019-06-18 14:51:44 +02:00
Saúl Ibarra Corretgé
70b864f00b deps: react-native-google-signin@2.0.0 2019-06-18 14:51:44 +02:00
Saúl Ibarra Corretgé
73b6a7a134 android: fix React packager not working on debug builds on a device 2019-06-18 14:51:44 +02:00
Saúl Ibarra Corretgé
8b5b112c6a android: don't default to making libre builds 2019-06-18 14:51:44 +02:00
Leonard Kim
2f7f9f24c4 fix(display-name): dismiss prompt if name gets set 2019-06-17 15:10:53 +01:00
Leonard Kim
1197c26529 fix(always-on-top): enforce circle avatar 2019-06-17 16:08:39 +02:00
Дамян Минков
6eb66b639e Notify for kick and mute (#4328)
* Updates kick showing who kicked us.

* Notify participants that someone was kicked.

* Shows notification to user who is remotely muted.

* Updates the notification type.

* Muted by notification for mobile.

* Moves code to react and adds the kick notifications to mobile.

* Updates lib-jitsi-meet.
2019-06-17 16:00:09 +02:00
Saúl Ibarra Corretgé
fa88db6897 ios: update CocoaPods to 1.7.1 2019-06-14 13:24:14 +02:00
Дамян Минков
64eb4b5609 Updates start silent, turning on startWithAudioMuted and few UI tweaks. (#4314)
* Updates start silent, turning on startWithAudioMuted and few UI tweaks.

Disabled mic unmute button and removes remote participants volume slider.

* Adds analytics for start silent.

* Removes extra semi colon.

* Updates lib-jitsi-meet and updates meeting info text.
2019-06-14 12:16:08 +01:00
Emil Ivov
ef2455caea Merge pull request #4333 from saghul/display-name-copy
lang: update display name request copy
2019-06-14 11:13:22 +01:00
Saúl Ibarra Corretgé
ca11cbf6cc calendar-sync: fix loading calendar entries
Checking if the calendar support in the reducer is not only useless but wrong,
since we don't have access to the entire store (the calendar support is checked
in the base/config feature). If calendar support is not enabled the actions
being reduced won't be dispatched anyway, so no harm is done by removing the
check.
2019-06-14 11:32:39 +02:00
Saúl Ibarra Corretgé
f15a2aea68 lang: update display name request copy 2019-06-14 11:03:32 +02:00
paweldomas
e6c3d7ded7 chore(deps): bump LJM version to increase frozen video timeout
Update LJM to 25ca5cbd2a93150e74f029075bc7ac734ed97a2f
2019-06-13 12:32:25 -05:00
Hristo Terezov
2861198251 ref(no-data-from-source): logic. 2019-06-13 09:52:10 -07:00
Hristo Terezov
226c0bb084 ref(track-error): Remove NO_DATA_FROM_SOURCE 2019-06-13 09:52:10 -07:00
Saúl Ibarra Corretgé
30c0bfc108 rn: disable vertical scrollbars on bottom sheets 2019-06-13 10:07:55 +02:00
Leonard Kim
df50e7fa69 fix(large-video): make blurred background fit whole screen 2019-06-12 20:27:04 -07:00
Saúl Ibarra Corretgé
f85ac3ef91 rn: fix video unmuting when disabling audio-only
When the video unmute button disabled audio-only, also unmute video. This fixes
a weird case in which the user need to "unmute twice" if they were muted beofre
they enabled audio-only mode. That's ok if the audio-only button was used, but
not if the video-unmute button was used, since the expectation is to have video,
of course.
2019-06-12 18:54:35 +02:00
Saúl Ibarra Corretgé
e33b334307 rn: add SDK API to set user information
At the moment it includes:

- display name
- email
- avatar URL

This information is used *only* if no token was specified.
2019-06-11 17:27:16 +00:00
Bettenbuk Zoltan
ce6f7308ad fix: avoid keyboard with search results list 2019-06-11 19:14:15 +02:00
Bettenbuk Zoltan
f66478fa34 feat: don't auto-stage bot type participants 2019-06-11 17:55:44 +02:00
Bettenbuk Zoltan
bf99051885 fix: invite search result sorting 2019-06-11 17:55:44 +02:00
Bettenbuk Zoltan
7234ca69c8 feat: invite field autofocus 2019-06-11 17:55:44 +02:00
Bettenbuk Zoltan
ae965877f3 fix: invite list needs double tap to select 2019-06-11 17:55:44 +02:00
Leonard Kim
ae3b70eb13 feat(api): notify of password required 2019-06-11 08:12:37 -07:00
Saúl Ibarra Corretgé
97e0303065 feature-flags: add flag for enabling calendar integration 2019-06-11 13:37:50 +00:00
Saúl Ibarra Corretgé
35ffbe1720 feature-flags: add flag for enabling chat 2019-06-11 13:37:50 +00:00
Saúl Ibarra Corretgé
f7b92f65ca ios: add feature flag to enable recording on iOS 2019-06-11 13:37:50 +00:00
Saúl Ibarra Corretgé
cf7b10d53d feature-flags: initial implementation
The welcomePageEnabled and pictureInPictureEnabled props on mobile have been
converted to feature flags.
2019-06-11 13:37:50 +00:00
Saúl Ibarra Corretgé
d798f93614 deps: check-in the diff when npm 6.9.0 is used 2019-06-11 13:08:23 +00:00
Saúl Ibarra Corretgé
4ddfcaf584 toolbox: disable audio-mode when video-unmuting
If audio-only was engaged, use the video (un)mute button to disengage it. This
should simplify the flow for getting back to video.
2019-06-11 13:08:23 +00:00
Saúl Ibarra Corretgé
431a221c63 ios: be resilient to invalid UUIDs 2019-06-11 10:04:15 +00:00
Mihai Damian
477826089c ios: Added property for setting PiP initial position 2019-06-11 09:25:23 +00:00
Hristo Terezov
a46369cf22 chore(package.json): Update lib-jitsi-meet 2019-06-10 06:53:36 -07:00
Hristo Terezov
651791b8df feat(deeplinking): Pass state to openDesktopApp. 2019-06-10 04:14:41 -07:00
virtuacoplenny
09cc738219 fix(recording): do not prompt hidden participants for name (#4309)
* fix(recording): do not prompt hidden participants for name

* squash: update package.json
2019-06-10 11:27:08 +01:00
Leonard Kim
6d8ec4d147 fix(unsupported): stop recommending safari 2019-06-07 07:36:20 -07:00
Leonard Kim
d65a068fdb feat(deep-linking): hide web button on unsupported browsers 2019-06-07 07:36:20 -07:00
Leonard Kim
cf23045f8d feat(unsupported): provide custom blacklist for branding 2019-06-07 07:36:20 -07:00
Leonard Kim
e47d2d13ce fix(deep-linking): deep link first, then show unsupported
Re-structure the custom routing to split between
platforms instead of between intended route features.
This made it easier for me to understand where to
do the checks for unsupported browser after deep-linking
had been checked.
2019-06-07 07:36:20 -07:00
142 changed files with 2194 additions and 771 deletions

1
.gitignore vendored
View File

@@ -73,6 +73,7 @@ buck-out/
# Build artifacts
*.jsbundle
*.framework
android/app/debug
android/app/release
# precommit-hook

View File

@@ -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)) {

View File

@@ -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

View File

@@ -18,4 +18,4 @@
# org.gradle.parallel=true
appVersion=19.2.0
sdkVersion=2.1.0
sdkVersion=2.2.0

View File

@@ -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"

View File

@@ -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

View File

@@ -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.
*

View File

@@ -73,6 +73,7 @@ class ExternalAPIModule
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) {

View File

@@ -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,13 @@ public class JitsiMeetActivity extends FragmentActivity
}
}
@Override
public void onDestroy() {
leave();
super.onDestroy();
}
@Override
public void finish() {
leave();
@@ -143,6 +150,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 +162,8 @@ public class JitsiMeetActivity extends FragmentActivity
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
JitsiMeetConferenceOptions options;
if ((options = getConferenceOptions(intent)) != null) {

View File

@@ -54,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.
@@ -63,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.
@@ -78,14 +82,16 @@ public class JitsiMeetConferenceOptions implements Parcelable {
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();
}
/**\
@@ -186,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;
}
@@ -204,10 +234,11 @@ public class JitsiMeetConferenceOptions implements Parcelable {
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;
}
@@ -221,30 +252,30 @@ public class JitsiMeetConferenceOptions implements Parcelable {
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) {
@@ -278,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);
@@ -305,10 +340,11 @@ public class JitsiMeetConferenceOptions implements Parcelable {
dest.writeString(subject);
dest.writeString(token);
dest.writeBundle(colorScheme);
dest.writeBundle(featureFlags);
dest.writeBundle(userInfo.asBundle());
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

View File

@@ -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

View File

@@ -0,0 +1,58 @@
/*
* 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);
// Terminate all conferences
for (BaseReactView view: BaseReactView.getViews()) {
if (view instanceof JitsiMeetView) {
((JitsiMeetView) view).leave();
}
}
// Abort all ConnectionService ongoing calls
if (AudioModeModule.useConnectionService()) {
ConnectionService.abortConnections();
}
if (defaultUncaughtExceptionHandler != null) {
defaultUncaughtExceptionHandler.uncaughtException(t, e);
}
}
}

View File

@@ -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;
}
}

View File

@@ -181,7 +181,7 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener> {
* by/associated with the specified {@code eventName}.
*/
private void maybeSetViewURL(String eventName, ReadableMap eventData) {
String url = eventData.getString("url");
String url = eventData.hasKey("url") ? eventData.getString("url") : null;
switch(eventName) {
case "CONFERENCE_WILL_JOIN":

View File

@@ -182,5 +182,8 @@ class ReactInstanceManagerHolder {
if (devSettings != null) {
devSettings.setBundleDeltasEnabled(false);
}
// Register our uncaught exception handler.
JitsiMeetUncaughtExceptionHandler.register();
}
}

View File

@@ -16,6 +16,7 @@ import * as JitsiMeetConferenceEvents from './ConferenceEvents';
import {
createDeviceChangedEvent,
createStartSilentEvent,
createScreenSharingEvent,
createStreamSwitchDelayEvent,
createTrackMutedEvent,
@@ -78,13 +79,14 @@ import {
import { showNotification } from './react/features/notifications';
import {
dominantSpeakerChanged,
getAvatarURLByParticipantId,
getLocalParticipant,
getNormalizedDisplayName,
getParticipantById,
localParticipantConnectionStatusChanged,
localParticipantRoleChanged,
participantConnectionStatusChanged,
participantKicked,
participantMutedUs,
participantPresenceChanged,
participantRoleChanged,
participantUpdated
@@ -102,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';
@@ -375,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;
@@ -403,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
@@ -729,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))
@@ -736,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 ]) => {
@@ -793,6 +791,7 @@ export default {
}
if (config.startSilent) {
sendAnalytics(createStartSilentEvent());
APP.store.dispatch(showNotification({
descriptionKey: 'notify.startSilentDescription',
titleKey: 'notify.startSilentTitle'
@@ -1855,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);
});
@@ -1955,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());
@@ -1989,6 +1998,8 @@ export default {
this.localAudio.dispose();
this.localAudio = null;
}
APP.API.notifySuspendDetected();
});
APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => {
@@ -2257,7 +2268,8 @@ export default {
APP.keyboardshortcut.init();
if (config.requireDisplayName
&& !APP.conference.getLocalDisplayName()) {
&& !APP.conference.getLocalDisplayName()
&& !this._room.isHidden()) {
APP.UI.promptDisplayName();
}
@@ -2267,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())
}
);
},
/**
@@ -2742,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);
}

View File

@@ -25,6 +25,9 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-enlarge:before {
content: "\e90a";
}
.icon-signal_cellular_0:before {
content: "\e901";
}

View File

@@ -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 {

View File

@@ -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 {
@@ -546,13 +541,17 @@
#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%;
}
}
}

View File

@@ -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');
@@ -389,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
{
@@ -430,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.

Binary file not shown.

View File

@@ -37,6 +37,7 @@
<glyph unicode="&#xe907;" glyph-name="signal_cellular_2" d="M86 86l852 852v-852h-852z" />
<glyph unicode="&#xe908;" 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="&#xe909;" 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="&#xe90a;" 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="&#xe90b;" glyph-name="full-screen" d="M598 810h212v-212h-84v128h-128v84zM726 298v128h84v-212h-212v84h128zM214 598v212h212v-84h-128v-128h-84zM298 426v-128h128v-84h-212v212h84z" />
<glyph unicode="&#xe90c;" 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="&#xe90d;" 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

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -93,12 +93,15 @@
+ "font-size:small;"
+ "cursor: pointer'>" + showMoreText + "</a>"
+ "&nbsp;&nbsp;&nbsp;"
+ "<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

View File

@@ -167,7 +167,27 @@ var interfaceConfig = {
*
* @type {boolean}
*/
RECENT_LIST_ENABLED: true
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. 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: 'remote-only'
/**
* How many columns the tile view can expand to. The respected range is
@@ -195,12 +215,6 @@ var interfaceConfig = {
*/
// ANDROID_APP_PACKAGE: 'org.jitsi.meet',
/**
* A UX mode where the last screen share participant is automatically
* pinned. Note: this mode is experimental and subject to breakage.
*/
// AUTO_PIN_LATEST_SCREEN_SHARE: false,
/**
* Override the behavior of some notifications to remain displayed until
* explicitly dismissed through a user action. The value is how long, in

View File

@@ -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

View File

@@ -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];

View File

@@ -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

View File

@@ -42,6 +42,8 @@
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 */; };
@@ -94,6 +96,9 @@
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>"; };
@@ -174,6 +179,9 @@
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 */,
@@ -259,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 */,
@@ -484,6 +493,7 @@
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 */,

View File

@@ -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>

View File

@@ -16,6 +16,9 @@
#import <Foundation/Foundation.h>
#import "JitsiMeetUserInfo.h"
@interface JitsiMeetConferenceOptionsBuilder : NSObject
/**
@@ -41,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.
@@ -55,6 +63,14 @@
*/
@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
@@ -66,6 +82,7 @@
@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;
@@ -73,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;

View File

@@ -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;
@@ -38,17 +45,26 @@
_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 {
@@ -76,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
@@ -97,17 +116,13 @@
return _videoMuted;
}
- (NSNumber *)getWelcomePageEnabled {
return _welcomePageEnabled;
}
@end
@implementation JitsiMeetConferenceOptions {
NSNumber *_audioOnly;
NSNumber *_audioMuted;
NSNumber *_videoMuted;
NSNumber *_welcomePageEnabled;
NSDictionary *_featureFlags;
}
@dynamic audioOnly;
@@ -130,7 +145,9 @@
}
- (BOOL)welcomePageEnabled {
return _welcomePageEnabled && [_welcomePageEnabled boolValue];
NSNumber *n = _featureFlags[WelcomePageEnabledFeatureFlag];
return n != nil ? [n boolValue] : NO;
}
#pragma mark - Internal initializer
@@ -148,7 +165,9 @@
_audioMuted = [builder getAudioMuted];
_videoMuted = [builder getVideoMuted];
_welcomePageEnabled = [builder getWelcomePageEnabled];
_featureFlags = [NSDictionary dictionaryWithDictionary:builder.featureFlags];
_userInfo = builder.userInfo;
}
return self;
@@ -167,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);
@@ -208,6 +225,10 @@
urlProps[@"jwt"] = _token;
}
if (_token == nil && _userInfo != nil) {
props[@"userInfo"] = [self.userInfo asDict];
}
urlProps[@"config"] = config;
props[@"url"] = urlProps;

View 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

View 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

View 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

View File

@@ -24,6 +24,12 @@
#import "RNRootView.h"
/**
* Backwards compatibility: turn the boolean prop into a feature flag.
*/
static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
@implementation JitsiMeetView {
/**
* The unique identifier of this `JitsiMeetView` within the process for the
@@ -122,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

View File

@@ -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];

View File

@@ -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

View File

@@ -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! Whats 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,10 +472,16 @@
"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!",
@@ -635,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",
@@ -760,6 +767,7 @@
"mute": "Member is muted",
"muted": "Muted",
"remoteControl": "Remote control",
"show": "Show on stage",
"videomute": "Member has stopped the camera"
},
"welcomepage": {

View File

@@ -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);
},
@@ -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.
@@ -627,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.

View File

@@ -33,6 +33,7 @@ const commands = {
displayName: 'display-name',
email: 'email',
hangup: 'video-hangup',
password: 'password',
subject: 'subject',
submitFeedback: 'submit-feedback',
toggleAudio: 'toggle-audio',
@@ -63,6 +64,7 @@ const events = {
'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',
@@ -71,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'
};
@@ -535,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}

View File

@@ -99,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
@@ -731,24 +720,6 @@ UI.showExtensionInlineInstallationDialog = function(callback) {
});
};
/**
* 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);
};

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}
/**
@@ -859,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.
@@ -882,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();
@@ -934,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.

View File

@@ -637,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];
@@ -985,7 +985,7 @@ const VideoLayout = {
} else if (currentId) {
const currentSmallVideo = this.getSmallVideo(currentId);
currentSmallVideo.updateView();
currentSmallVideo && currentSmallVideo.updateView();
}
},

53
package-lock.json generated
View File

@@ -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#0ee30bf12a549d10bb5d559e19bd557c3ed179eb",
"from": "github:jitsi/lib-jitsi-meet#0ee30bf12a549d10bb5d559e19bd557c3ed179eb",
"version": "github:jitsi/lib-jitsi-meet#ee1854c1b12a229f70f351acaebe6686f38f6a85",
"from": "github:jitsi/lib-jitsi-meet#ee1854c1b12a229f70f351acaebe6686f38f6a85",
"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",

View File

@@ -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#0ee30bf12a549d10bb5d559e19bd557c3ed179eb",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#ee1854c1b12a229f70f351acaebe6686f38f6a85",
"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",

View File

@@ -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.
*
@@ -551,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.
*

View File

@@ -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 || {}));
});
}

View File

@@ -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);
});
}

View File

@@ -14,6 +14,8 @@ import {
dominantSpeakerChanged,
getNormalizedDisplayName,
participantConnectionStatusChanged,
participantKicked,
participantMutedUs,
participantPresenceChanged,
participantRoleChanged,
participantUpdated
@@ -89,7 +91,11 @@ function _addConferenceListeners(conference, dispatch) {
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,
@@ -129,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,
@@ -432,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
};
}

View File

@@ -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,
@@ -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 => {

View File

@@ -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,

View File

@@ -28,14 +28,12 @@ const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
microphone: {
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.micConstraintFailedError',
[JitsiTrackErrors.GENERAL]: 'dialog.micUnknownError',
[JitsiTrackErrors.NO_DATA_FROM_SOURCE]: 'dialog.micNotSendingData',
[JitsiTrackErrors.NOT_FOUND]: 'dialog.micNotFoundError',
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError'
},
camera: {
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.cameraConstraintFailedError',
[JitsiTrackErrors.GENERAL]: 'dialog.cameraUnknownError',
[JitsiTrackErrors.NO_DATA_FROM_SOURCE]: 'dialog.cameraNotSendingData',
[JitsiTrackErrors.NOT_FOUND]: 'dialog.cameraNotFoundError',
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.cameraPermissionDeniedError',
[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]: 'dialog.cameraUnsupportedResolutionError'

View File

@@ -61,7 +61,9 @@ class BottomSheet extends PureComponent<Props> {
styles.sheetItemContainer,
_styles.sheet
] }>
<ScrollView bounces = { false }>
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false }>
{ this._getWrappedContent() }
</ScrollView>
</View>

View File

@@ -0,0 +1,109 @@
// @flow
import JitsiMeetJS from '../lib-jitsi-meet';
import { Platform } from '../react';
const { browser } = JitsiMeetJS.util;
const DEFAULT_OPTIMAL_BROWSERS = [
'chrome',
'electron',
'firefox',
'nwjs'
];
const DEFAULT_UNSUPPORTED_BROWSERS = [];
const browserNameToCheck = {
chrome: browser.isChrome.bind(browser),
chromium: browser.isChromiumBased.bind(browser),
edge: browser.isEdge.bind(browser),
electron: browser.isElectron.bind(browser),
firefox: browser.isFirefox.bind(browser),
nwjs: browser.isNWJS.bind(browser),
opera: browser.isOpera.bind(browser),
safari: browser.isSafari.bind(browser)
};
declare var interfaceConfig: Object;
/**
* Returns whether or not jitsi is optimized and targeted for the provided
* browser name.
*
* @param {string} browserName - The name of the browser to check.
* @returns {boolean}
*/
export function isBrowsersOptimal(browserName: string) {
return (interfaceConfig.OPTIMAL_BROWSERS || DEFAULT_OPTIMAL_BROWSERS)
.includes(browserName);
}
/**
* Returns whether or not the current browser or the list of passed in browsers
* is considered suboptimal. Suboptimal means it is a supported browser but has
* not been explicitly listed as being optimal, possibly due to functionality
* issues.
*
* @param {Array<string>} [browsers] - A list of browser names to check. Will
* default to a whitelist.
* @returns {boolean}
*/
export function isSuboptimalBrowser() {
const optimalBrowsers
= interfaceConfig.OPTIMAL_BROWSERS || DEFAULT_OPTIMAL_BROWSERS;
return !_isCurrentBrowserInList(optimalBrowsers) && isSupportedBrowser();
}
/**
* Returns whether or not the current browser should allow the app to display.
* A supported browser is assumed to be able to support WebRtc.
*
* @returns {boolean}
*/
export function isSupportedBrowser() {
if (navigator.product === 'ReactNative') {
return false;
}
// Blacklists apply to desktop browsers only right now.
if (!_isMobileBrowser() && _isCurrentBrowserInList(
interfaceConfig.UNSUPPORTED_BROWSERS || DEFAULT_UNSUPPORTED_BROWSERS
)) {
return false;
}
// We are intentionally allow mobile browsers because:
// - the WelcomePage is mobile ready;
// - if the URL points to a conference then deep-linking will take
// care of it.
return _isMobileBrowser() || JitsiMeetJS.isWebRtcSupported();
}
/**
* Runs various browser checks to know if the current browser is found within
* the list.
*
* @param {Array<string>} list - Browser names to check. The names should be
* keys in {@link browserNameToCheck}.
* @private
* @returns {boolean}
*/
function _isCurrentBrowserInList(list) {
return Boolean(list.find(browserName => {
const checkFunction = browserNameToCheck[browserName];
return checkFunction ? checkFunction.call(browser) : false;
}));
}
/**
* Returns whether or not the current environment is a mobile device.
*
* @private
* @returns {boolean}
*/
function _isMobileBrowser() {
return Platform.OS === 'android' || Platform.OS === 'ios';
}

View File

@@ -0,0 +1 @@
export * from './environment';

View File

@@ -0,0 +1,10 @@
/**
* The type of Redux action which updates the feature flags.
*
* {
* type: UPDATE_FLAGS,
* flags: Object
* }
*
*/
export const UPDATE_FLAGS = 'UPDATE_FLAGS';

View File

@@ -0,0 +1,19 @@
// @flow
import { UPDATE_FLAGS } from './actionTypes';
/**
* Updates the current features flags with the given ones. They will be merged.
*
* @param {Object} flags - The new flags object.
* @returns {{
* type: UPDATE_FLAGS,
* flags: Object
* }}
*/
export function updateFlags(flags: Object) {
return {
type: UPDATE_FLAGS,
flags
};
}

View File

@@ -0,0 +1,31 @@
// @flow
/**
* Flag indicating if calendar integration should be enabled.
* Default: enabled (true) on Android, auto-detected on iOS.
*/
export const CALENDAR_ENABLED = 'calendar.enabled';
/**
* Flag indicating if chat should be enabled.
* Default: enabled (true).
*/
export const CHAT_ENABLED = 'chat.enabled';
/**
* Flag indicating if recording should be enabled in iOS.
* Default: disabled (false).
*/
export const IOS_RECORDING_ENABLED = 'ios.recording.enabled';
/**
* Flag indicating if Picture-in-Picture should be enabled.
* Default: auto-detected.
*/
export const PIP_ENABLED = 'pip.enabled';
/**
* Flag indicating if the welcome page should be enabled.
* Default: disabled (false).
*/
export const WELCOME_PAGE_ENABLED = 'welcomepage.enabled';

View File

@@ -0,0 +1,32 @@
// @flow
import { getAppProp } from '../app';
import { toState } from '../redux';
/**
* Gets the value of a specific feature flag.
*
* @param {Function|Object} stateful - The redux store or {@code getState}
* function.
* @param {string} flag - The name of the React {@code Component} prop of
* the currently mounted {@code App} to get.
* @param {*} defaultValue - A default value for the flag, in case it's not defined.
* @returns {*} The value of the specified React {@code Compoennt} prop of the
* currently mounted {@code App}.
*/
export function getFeatureFlag(stateful: Function | Object, flag: string, defaultValue: any) {
const state = toState(stateful)['features/base/flags'];
if (state) {
const value = state[flag];
if (typeof value !== 'undefined') {
return value;
}
}
// Maybe the value hasn't made it to the redux store yet, check the app props.
const flags = getAppProp(stateful, 'flags') || {};
return flags[flag] || defaultValue;
}

View File

@@ -0,0 +1,6 @@
export * from './actions';
export * from './actionTypes';
export * from './constants';
export * from './functions';
import './reducer';

View File

@@ -0,0 +1,33 @@
// @flow
import _ from 'lodash';
import { ReducerRegistry } from '../redux';
import { UPDATE_FLAGS } from './actionTypes';
/**
* Default state value for the feature flags.
*/
const DEFAULT_STATE = {};
/**
* Reduces redux actions which handle feature flags.
*
* @param {State} state - The current redux state.
* @param {Action} action - The redux action to reduce.
* @param {string} action.type - The type of the redux action to reduce.
* @returns {State} The next redux state that is the result of reducing the
* specified action.
*/
ReducerRegistry.register('features/base/flags', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case UPDATE_FLAGS: {
const newState = _.merge({}, state, action.flags);
return _.isEqual(state, newState) ? state : newState;
}
}
return state;
});

View File

@@ -70,6 +70,7 @@ export function isFatalJitsiConferenceError(error: Object | string) {
return (
error === JitsiConferenceErrors.FOCUS_DISCONNECTED
|| error === JitsiConferenceErrors.FOCUS_LEFT
|| error === JitsiConferenceErrors.OFFER_ANSWER_FAILED
|| error === JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE);
}

View File

@@ -1,5 +1,3 @@
/* global interfaceConfig */
import throttle from 'lodash/throttle';
import { set } from '../redux';
@@ -17,7 +15,11 @@ import {
PARTICIPANT_UPDATED,
PIN_PARTICIPANT
} from './actionTypes';
import { getLocalParticipant, getNormalizedDisplayName } from './functions';
import {
getLocalParticipant,
getNormalizedDisplayName,
getParticipantDisplayName
} from './functions';
/**
* Create an action for when dominant speaker changes.
@@ -382,6 +384,51 @@ export function participantUpdated(participant = {}) {
};
}
/**
* Action to signal that a participant has muted us.
*
* @param {JitsiParticipant} participant - Information about participant.
* @returns {Promise}
*/
export function participantMutedUs(participant) {
return (dispatch, getState) => {
if (!participant) {
return;
}
dispatch(showNotification({
descriptionKey: 'notify.mutedRemotelyDescription',
titleKey: 'notify.mutedRemotelyTitle',
titleArguments: {
participantDisplayName:
getParticipantDisplayName(getState, participant.getId())
}
}));
};
}
/**
* Action to signal that a participant had been kicked.
*
* @param {JitsiParticipant} kicker - Information about participant performing the kick.
* @param {JitsiParticipant} kicked - Information about participant that was kicked.
* @returns {Promise}
*/
export function participantKicked(kicker, kicked) {
return (dispatch, getState) => {
dispatch(showNotification({
titleArguments: {
kicked:
getParticipantDisplayName(getState, kicked.getId()),
kicker:
getParticipantDisplayName(getState, kicker.getId())
},
titleKey: 'notify.kickParticipant'
}, NOTIFICATION_TIMEOUT * 2)); // leave more time for this
};
}
/**
* Create an action which pins a conference participant.
*
@@ -468,8 +515,7 @@ const _throttledNotifyParticipantConnected = throttle(dispatch => {
* @returns {Function}
*/
export function showParticipantJoinedNotification(displayName) {
joinedParticipantsNames.push(
displayName || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME);
joinedParticipantsNames.push(displayName);
return dispatch => _throttledNotifyParticipantConnected(dispatch);
}

View File

@@ -118,13 +118,15 @@ MiddlewareRegistry.register(store => next => action => {
case PARTICIPANT_JOINED: {
_maybePlaySounds(store, action);
const { participant: { name } } = action;
const result = _participantJoinedOrUpdated(store, next, action);
if (name) {
store.dispatch(showParticipantJoinedNotification(name));
const { participant: p } = action;
if (!p.local) {
store.dispatch(showParticipantJoinedNotification(getParticipantDisplayName(store.getState, p.id)));
}
return _participantJoinedOrUpdated(store, next, action);
return result;
}
case PARTICIPANT_LEFT:

View File

@@ -1,4 +1,5 @@
export * from './actions';
export * from './actionTypes';
export * from './constants';
export * from './functions';

View File

@@ -96,10 +96,7 @@ function _addOrRemoveAudioElement(state, action) {
}));
}
} else {
const actionName
= isAddAction ? '_ADD_AUDIO_ELEMENT' : '_REMOVE_AUDIO_ELEMENT';
logger.error(`${actionName}: no sound for id: ${soundId}`);
logger.warn(`${action.type}: no sound for id: ${soundId}`);
}
return nextState;

View File

@@ -43,6 +43,16 @@ export const TRACK_CREATE_CANCELED = 'TRACK_CREATE_CANCELED';
*/
export const TRACK_CREATE_ERROR = 'TRACK_CREATE_ERROR';
/**
* The type of redux action dispatched when a track has triggered no data from source event.
*
* {
* type: TRACK_NO_DATA_FROM_SOURCE,
* track: Track
* }
*/
export const TRACK_NO_DATA_FROM_SOURCE = 'TRACK_NO_DATA_FROM_SOURCE';
/**
* The type of redux action dispatched when a track has been (locally or
* remotely) removed from the conference.

View File

@@ -3,6 +3,7 @@ import {
sendAnalytics
} from '../../analytics';
import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
import { showErrorNotification, showNotification } from '../../notifications';
import {
CAMERA_FACING_MODE,
MEDIA_TYPE,
@@ -17,11 +18,12 @@ import {
TRACK_ADDED,
TRACK_CREATE_CANCELED,
TRACK_CREATE_ERROR,
TRACK_NO_DATA_FROM_SOURCE,
TRACK_REMOVED,
TRACK_UPDATED,
TRACK_WILL_CREATE
} from './actionTypes';
import { createLocalTracksF, getLocalTrack, getLocalTracks } from './functions';
import { createLocalTracksF, getLocalTrack, getLocalTracks, getTrackByJitsiTrack } from './functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -189,6 +191,55 @@ export function destroyLocalTracks() {
};
}
/**
* Signals that the passed JitsiLocalTrack has triggered a no data from source event.
*
* @param {JitsiLocalTrack} track - The track.
* @returns {{
* type: TRACK_NO_DATA_FROM_SOURCE,
* track: Track
* }}
*/
export function noDataFromSource(track) {
return {
type: TRACK_NO_DATA_FROM_SOURCE,
track
};
}
/**
* Displays a no data from source video error if needed.
*
* @param {JitsiLocalTrack} jitsiTrack - The track.
* @returns {Function}
*/
export function showNoDataFromSourceVideoError(jitsiTrack) {
return (dispatch, getState) => {
let notificationInfo;
const track = getTrackByJitsiTrack(getState()['features/base/tracks'], jitsiTrack);
if (!track) {
return;
}
if (track.isReceivingData) {
notificationInfo = undefined;
} else {
const notificationAction = showErrorNotification({
descriptionKey: 'dialog.cameraNotSendingData',
titleKey: 'dialog.cameraNotSendingDataTitle'
});
dispatch(notificationAction);
notificationInfo = {
uid: notificationAction.uid
};
}
dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, notificationInfo));
};
}
/**
* Signals that the local participant is ending screensharing or beginning the
* screensharing flow.
@@ -288,7 +339,8 @@ export function trackAdded(track) {
// participantId
const local = track.isLocal();
let participantId;
const mediaType = track.getType();
let isReceivingData, noDataFromSourceNotificationInfo, participantId;
if (local) {
const participant = getLocalParticipant(getState);
@@ -296,18 +348,40 @@ export function trackAdded(track) {
if (participant) {
participantId = participant.id;
}
isReceivingData = track.isReceivingData();
track.on(JitsiTrackEvents.NO_DATA_FROM_SOURCE, () => dispatch(noDataFromSource({ jitsiTrack: track })));
if (!isReceivingData) {
if (mediaType === MEDIA_TYPE.AUDIO) {
const notificationAction = showNotification({
descriptionKey: 'dialog.micNotSendingData',
titleKey: 'dialog.micNotSendingDataTitle'
});
dispatch(notificationAction);
noDataFromSourceNotificationInfo = { uid: notificationAction.uid };
} else {
const timeout = setTimeout(() => dispatch(showNoDataFromSourceVideoError(track)), 5000);
noDataFromSourceNotificationInfo = { timeout };
}
}
} else {
participantId = track.getParticipantId();
isReceivingData = true;
}
return dispatch({
type: TRACK_ADDED,
track: {
jitsiTrack: track,
isReceivingData,
local,
mediaType: track.getType(),
mediaType,
mirror: _shouldMirror(track),
muted: track.isMuted(),
noDataFromSourceNotificationInfo,
participantId,
videoStarted: false,
videoType: track.videoType
@@ -336,6 +410,26 @@ export function trackMutedChanged(track) {
};
}
/**
* Create an action for when a track's no data from source notification information changes.
*
* @param {JitsiLocalTrack} track - JitsiTrack instance.
* @param {Object} noDataFromSourceNotificationInfo - Information about no data from source notification.
* @returns {{
* type: TRACK_UPDATED,
* track: Track
* }}
*/
export function trackNoDataFromSourceNotificationInfoChanged(track, noDataFromSourceNotificationInfo) {
return {
type: TRACK_UPDATED,
track: {
jitsiTrack: track,
noDataFromSourceNotificationInfo
}
};
}
/**
* Create an action for when a track has been signaled for removal from the
* conference.
@@ -349,6 +443,7 @@ export function trackMutedChanged(track) {
export function trackRemoved(track) {
track.removeAllListeners(JitsiTrackEvents.TRACK_MUTE_CHANGED);
track.removeAllListeners(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED);
track.removeAllListeners(JitsiTrackEvents.NO_DATA_FROM_SOURCE);
return {
type: TRACK_REMOVED,

View File

@@ -1,7 +1,6 @@
/* global APP */
import JitsiMeetJS, { JitsiTrackErrors, JitsiTrackEvents }
from '../lib-jitsi-meet';
import JitsiMeetJS, { JitsiTrackErrors } from '../lib-jitsi-meet';
import { MEDIA_TYPE } from '../media';
import {
getUserSelectedCameraDeviceId,
@@ -77,21 +76,6 @@ export function createLocalTracksF(
resolution
},
firePermissionPromptIsShownEvent)
.then(tracks => {
// TODO JitsiTrackEvents.NO_DATA_FROM_SOURCE should probably be
// dispatched in the redux store here and then
// APP.UI.showTrackNotWorkingDialog should be in a middleware
// somewhere else.
if (typeof APP !== 'undefined') {
tracks.forEach(track =>
track.on(
JitsiTrackEvents.NO_DATA_FROM_SOURCE,
APP.UI.showTrackNotWorkingDialog.bind(
null, track.isAudioTrack())));
}
return tracks;
})
.catch(err => {
logger.error('Failed to create local tracks', options.devices, err);

View File

@@ -9,15 +9,22 @@ import {
TOGGLE_CAMERA_FACING_MODE,
toggleCameraFacingMode
} from '../media';
import { hideNotification } from '../../notifications';
import { MiddlewareRegistry } from '../redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { createLocalTracksA } from './actions';
import {
createLocalTracksA,
showNoDataFromSourceVideoError,
trackNoDataFromSourceNotificationInfoChanged
} from './actions';
import {
TOGGLE_SCREENSHARING,
TRACK_NO_DATA_FROM_SOURCE,
TRACK_REMOVED,
TRACK_UPDATED
} from './actionTypes';
import { getLocalTrack, setTrackMuted } from './functions';
import { getLocalTrack, getTrackByJitsiTrack, setTrackMuted } from './functions';
declare var APP: Object;
@@ -31,6 +38,17 @@ declare var APP: Object;
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case TRACK_NO_DATA_FROM_SOURCE: {
const result = next(action);
_handleNoDataFromSourceErrors(store, action);
return result;
}
case TRACK_REMOVED: {
_removeNoDataFromSourceNotification(store, action.track);
break;
}
case SET_AUDIO_MUTED:
_setMuted(store, action, MEDIA_TYPE.AUDIO);
break;
@@ -121,6 +139,53 @@ MiddlewareRegistry.register(store => next => action => {
return next(action);
});
/**
* Handles no data from source errors.
*
* @param {Store} store - The redux store in which the specified action is
* dispatched.
* @param {Action} action - The redux action dispatched in the specified store.
* @private
* @returns {void}
*/
function _handleNoDataFromSourceErrors(store, action) {
const { getState, dispatch } = store;
const track = getTrackByJitsiTrack(getState()['features/base/tracks'], action.track.jitsiTrack);
if (!track || !track.local) {
return;
}
const { jitsiTrack } = track;
if (track.mediaType === MEDIA_TYPE.AUDIO && track.isReceivingData) {
_removeNoDataFromSourceNotification(store, action.track);
}
if (track.mediaType === MEDIA_TYPE.VIDEO) {
const { noDataFromSourceNotificationInfo = {} } = track;
if (track.isReceivingData) {
if (noDataFromSourceNotificationInfo.timeout) {
clearTimeout(noDataFromSourceNotificationInfo.timeout);
dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined));
}
// try to remove the notification if there is one.
_removeNoDataFromSourceNotification(store, action.track);
} else {
if (noDataFromSourceNotificationInfo.timeout) {
return;
}
const timeout = setTimeout(() => dispatch(showNoDataFromSourceVideoError(jitsiTrack)), 5000);
dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, { timeout }));
}
}
}
/**
* Gets the local track associated with a specific {@code MEDIA_TYPE} in a
* specific redux store.
@@ -149,6 +214,23 @@ function _getLocalTrack(
includePending));
}
/**
* Removes the no data from source notification associated with the JitsiTrack if displayed.
*
* @param {Store} store - The redux store.
* @param {Track} track - The redux action dispatched in the specified store.
* @returns {void}
*/
function _removeNoDataFromSourceNotification({ getState, dispatch }, track) {
const t = getTrackByJitsiTrack(getState()['features/base/tracks'], track.jitsiTrack);
const { jitsiTrack, noDataFromSourceNotificationInfo = {} } = t || {};
if (noDataFromSourceNotificationInfo && noDataFromSourceNotificationInfo.uid) {
dispatch(hideNotification(noDataFromSourceNotificationInfo.uid));
dispatch(trackNoDataFromSourceNotificationInfoChanged(jitsiTrack, undefined));
}
}
/**
* Mutes or unmutes a local track with a specific media type.
*

View File

@@ -5,6 +5,7 @@ import {
TRACK_ADDED,
TRACK_CREATE_CANCELED,
TRACK_CREATE_ERROR,
TRACK_NO_DATA_FROM_SOURCE,
TRACK_REMOVED,
TRACK_UPDATED,
TRACK_WILL_CREATE
@@ -75,6 +76,21 @@ function track(state, action) {
}
break;
}
case TRACK_NO_DATA_FROM_SOURCE: {
const t = action.track;
if (state.jitsiTrack === t.jitsiTrack) {
const isReceivingData = t.jitsiTrack.isReceivingData();
if (state.isReceivingData !== isReceivingData) {
return {
...state,
isReceivingData
};
}
}
break;
}
}
return state;
@@ -86,6 +102,7 @@ function track(state, action) {
ReducerRegistry.register('features/base/tracks', (state = [], action) => {
switch (action.type) {
case PARTICIPANT_ID_CHANGED:
case TRACK_NO_DATA_FROM_SOURCE:
case TRACK_UPDATED:
return state.map(t => track(t, action));

View File

@@ -30,17 +30,19 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
*/
export function bootstrapCalendarIntegration(): Function {
return (dispatch, getState) => {
const state = getState();
if (!isCalendarEnabled(state)) {
return Promise.reject();
}
const {
googleApiApplicationClientID
} = getState()['features/base/config'];
} = state['features/base/config'];
const {
integrationReady,
integrationType
} = getState()['features/calendar-sync'];
if (!isCalendarEnabled()) {
return Promise.reject();
}
} = state['features/calendar-sync'];
return Promise.resolve()
.then(() => {

View File

@@ -9,7 +9,6 @@ import { AbstractPage } from '../../base/react';
import { connect } from '../../base/redux';
import { refreshCalendar } from '../actions';
import { isCalendarEnabled } from '../functions';
import styles from './styles';
import CalendarListContent from './CalendarListContent';
@@ -138,6 +137,4 @@ function _mapStateToProps(state: Object) {
};
}
export default isCalendarEnabled()
? translate(connect(_mapStateToProps)(CalendarList))
: undefined;
export default translate(connect(_mapStateToProps)(CalendarList));

View File

@@ -14,7 +14,6 @@ import {
import { refreshCalendar } from '../actions';
import { ERRORS } from '../constants';
import { isCalendarEnabled } from '../functions';
import CalendarListContent from './CalendarListContent';
@@ -257,6 +256,4 @@ function _mapStateToProps(state) {
};
}
export default isCalendarEnabled()
? translate(connect(_mapStateToProps)(CalendarList))
: undefined;
export default translate(connect(_mapStateToProps)(CalendarList));

View File

@@ -13,7 +13,6 @@ import { NavigateSectionList } from '../../base/react';
import { connect } from '../../base/redux';
import { refreshCalendar, openUpdateCalendarEventDialog } from '../actions';
import { isCalendarEnabled } from '../functions';
/**
@@ -271,6 +270,4 @@ function _mapStateToProps(state: Object) {
};
}
export default isCalendarEnabled()
? translate(connect(_mapStateToProps)(CalendarListContent))
: undefined;
export default translate(connect(_mapStateToProps)(CalendarListContent));

View File

@@ -11,8 +11,6 @@ import {
import { MeetingsList } from '../../base/react';
import { connect } from '../../base/redux';
import { isCalendarEnabled } from '../functions';
import AddMeetingUrlButton from './AddMeetingUrlButton';
import JoinButton from './JoinButton';
@@ -172,6 +170,4 @@ function _mapStateToProps(state: Object) {
};
}
export default isCalendarEnabled()
? connect(_mapStateToProps)(CalendarListContent)
: undefined;
export default connect(_mapStateToProps)(CalendarListContent);

View File

@@ -10,7 +10,6 @@ import { getLocalizedDateFormatter, translate } from '../../base/i18n';
import { connect } from '../../base/redux';
import { ASPECT_RATIO_NARROW } from '../../base/responsive-ui';
import { isCalendarEnabled } from '../functions';
import styles from './styles';
const ALERT_MILLISECONDS = 5 * 60 * 1000;
@@ -294,6 +293,4 @@ function _mapStateToProps(state: Object) {
};
}
export default isCalendarEnabled()
? translate(connect(_mapStateToProps)(ConferenceNotification))
: undefined;
export default translate(connect(_mapStateToProps)(ConferenceNotification));

View File

@@ -4,6 +4,7 @@ import { NativeModules, Platform } from 'react-native';
import RNCalendarEvents from 'react-native-calendar-events';
import type { Store } from 'redux';
import { CALENDAR_ENABLED, getFeatureFlag } from '../base/flags';
import { getShareInfoText } from '../invite';
import { setCalendarAuthorization } from './actions';
@@ -54,12 +55,20 @@ export function addLinkToCalendarEntry(
* Determines whether the calendar feature is enabled by the app. For
* example, Apple through its App Store requires
* {@code NSCalendarsUsageDescription} in the app's Info.plist or App Store
* rejects the app.
* rejects the app. It could also be disabled with a feature flag.
*
* @param {Function|Object} stateful - The redux store or {@code getState}
* function.
* @returns {boolean} If the app has enabled the calendar feature, {@code true};
* otherwise, {@code false}.
*/
export function isCalendarEnabled() {
export function isCalendarEnabled(stateful: Function | Object) {
const flag = getFeatureFlag(stateful, CALENDAR_ENABLED);
if (typeof flag !== 'undefined') {
return flag;
}
const { calendarEnabled = true } = NativeModules.AppInfo;
return calendarEnabled;

View File

@@ -16,22 +16,26 @@ import {
import { _updateCalendarEntries } from './functions';
import { googleCalendarApi } from './web/googleCalendar';
import { microsoftCalendarApi } from './web/microsoftCalendar';
import { toState } from '../base/redux';
const logger = require('jitsi-meet-logger').getLogger(__filename);
declare var config: Object;
/**
* Determines whether the calendar feature is enabled by the web.
*
* @param {Function|Object} stateful - The redux store or {@code getState}
* function.
* @returns {boolean} If the app has enabled the calendar feature, {@code true};
* otherwise, {@code false}.
*/
export function isCalendarEnabled() {
return Boolean(
config.enableCalendarIntegration
&& (config.googleApiApplicationClientID
|| config.microsoftApiApplicationClientID));
export function isCalendarEnabled(stateful: Function | Object) {
const {
enableCalendarIntegration,
googleApiApplicationClientID,
microsoftApiApplicationClientID
} = toState(stateful)['features/base/config'] || {};
return Boolean(enableCalendarIntegration && (googleApiApplicationClientID || microsoftApiApplicationClientID));
}
/* eslint-disable no-unused-vars */

View File

@@ -9,51 +9,55 @@ import { setCalendarAuthorization } from './actions';
import { REFRESH_CALENDAR } from './actionTypes';
import { _fetchCalendarEntries, isCalendarEnabled } from './functions';
isCalendarEnabled()
&& MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case ADD_KNOWN_DOMAINS: {
// XXX Fetch new calendar entries only when an actual domain has
// become known.
const { getState } = store;
const oldValue = getState()['features/base/known-domains'];
const result = next(action);
const newValue = getState()['features/base/known-domains'];
equals(oldValue, newValue)
|| _fetchCalendarEntries(store, false, false);
return result;
}
case APP_STATE_CHANGED: {
const result = next(action);
_maybeClearAccessStatus(store, action);
return result;
}
case SET_CONFIG: {
const result = next(action);
_fetchCalendarEntries(store, false, false);
return result;
}
case REFRESH_CALENDAR: {
const result = next(action);
_fetchCalendarEntries(
store, action.isInteractive, action.forcePermission);
return result;
}
}
MiddlewareRegistry.register(store => next => action => {
const { getState } = store;
if (!isCalendarEnabled(getState)) {
return next(action);
});
}
switch (action.type) {
case ADD_KNOWN_DOMAINS: {
// XXX Fetch new calendar entries only when an actual domain has
// become known.
const oldValue = getState()['features/base/known-domains'];
const result = next(action);
const newValue = getState()['features/base/known-domains'];
equals(oldValue, newValue)
|| _fetchCalendarEntries(store, false, false);
return result;
}
case APP_STATE_CHANGED: {
const result = next(action);
_maybeClearAccessStatus(store, action);
return result;
}
case SET_CONFIG: {
const result = next(action);
_fetchCalendarEntries(store, false, false);
return result;
}
case REFRESH_CALENDAR: {
const result = next(action);
_fetchCalendarEntries(
store, action.isInteractive, action.forcePermission);
return result;
}
}
return next(action);
});
/**
* Clears the calendar access status when the app comes back from the

View File

@@ -13,7 +13,6 @@ import {
SET_CALENDAR_PROFILE_EMAIL,
SET_LOADING_CALENDAR_EVENTS
} from './actionTypes';
import { isCalendarEnabled } from './functions';
/**
* The default state of the calendar feature.
@@ -44,52 +43,50 @@ const STORE_NAME = 'features/calendar-sync';
* runtime value to see if we need to re-request the calendar permission from
* the user.
*/
isCalendarEnabled()
&& PersistenceRegistry.register(STORE_NAME, {
integrationType: true,
msAuthState: true
});
PersistenceRegistry.register(STORE_NAME, {
integrationType: true,
msAuthState: true
});
isCalendarEnabled()
&& ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case CLEAR_CALENDAR_INTEGRATION:
return DEFAULT_STATE;
ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
switch (action.type) {
case CLEAR_CALENDAR_INTEGRATION:
return DEFAULT_STATE;
case SET_CALENDAR_AUTH_STATE: {
if (!action.msAuthState) {
// received request to delete the state
return set(state, 'msAuthState', undefined);
}
return set(state, 'msAuthState', {
...state.msAuthState,
...action.msAuthState
});
case SET_CALENDAR_AUTH_STATE: {
if (!action.msAuthState) {
// received request to delete the state
return set(state, 'msAuthState', undefined);
}
case SET_CALENDAR_AUTHORIZATION:
return set(state, 'authorization', action.authorization);
return set(state, 'msAuthState', {
...state.msAuthState,
...action.msAuthState
});
}
case SET_CALENDAR_ERROR:
return set(state, 'error', action.error);
case SET_CALENDAR_AUTHORIZATION:
return set(state, 'authorization', action.authorization);
case SET_CALENDAR_EVENTS:
return set(state, 'events', action.events);
case SET_CALENDAR_ERROR:
return set(state, 'error', action.error);
case SET_CALENDAR_INTEGRATION:
return {
...state,
integrationReady: action.integrationReady,
integrationType: action.integrationType
};
case SET_CALENDAR_EVENTS:
return set(state, 'events', action.events);
case SET_CALENDAR_PROFILE_EMAIL:
return set(state, 'profileEmail', action.email);
case SET_CALENDAR_INTEGRATION:
return {
...state,
integrationReady: action.integrationReady,
integrationType: action.integrationType
};
case SET_LOADING_CALENDAR_EVENTS:
return set(state, 'isLoadingEvents', action.isLoadingEvents);
}
case SET_CALENDAR_PROFILE_EMAIL:
return set(state, 'profileEmail', action.email);
return state;
});
case SET_LOADING_CALENDAR_EVENTS:
return set(state, 'isLoadingEvents', action.isLoadingEvents);
}
return state;
});

View File

@@ -96,7 +96,7 @@ function _mapDispatchToProps(dispatch: Function) {
},
/**
* Displays a diaply name prompt.
* Displays a display name prompt.
*
* @param {Function} onPostSubmit - The function to invoke after a
* succesfulsetting of the display name.

View File

@@ -0,0 +1,31 @@
// @flow
import type { Dispatch } from 'redux';
import {
AlertDialog,
openDialog
} from '../base/dialog';
import { getParticipantDisplayName } from '../base/participants';
/**
* Notify that we've been kicked out of the conference.
*
* @param {JitsiParticipant} participant - The {@link JitsiParticipant}
* instance which initiated the kick event.
* @param {?Function} submit - The function to execute after submiting the dialog.
* @returns {Function}
*/
export function notifyKickedOut(participant: Object, submit: ?Function) {
return (dispatch: Dispatch<any>, getState: Function) => {
dispatch(openDialog(AlertDialog, {
contentKey: {
key: 'dialog.kickTitle',
params: {
participantDisplayName: getParticipantDisplayName(getState, participant.getId())
}
},
onSubmit: submit
}));
};
}

View File

@@ -0,0 +1,35 @@
// @flow
import type { Dispatch } from 'redux';
import {
NOTIFICATION_TYPE,
showNotification
} from '../notifications';
import { getParticipantDisplayName } from '../base/participants';
/**
* Notify that we've been kicked out of the conference.
*
* @param {JitsiParticipant} participant - The {@link JitsiParticipant}
* instance which initiated the kick event.
* @param {?Function} _ - Used only in native code.
* @returns {Function}
*/
export function notifyKickedOut(participant: Object, _: ?Function) { // eslint-disable-line no-unused-vars
return (dispatch: Dispatch<any>, getState: Function) => {
const args = {
participantDisplayName:
getParticipantDisplayName(getState, participant.getDisplayName())
};
dispatch(showNotification({
appearance: NOTIFICATION_TYPE.ERROR,
hideErrorSupportLink: true,
descriptionKey: 'dialog.kickMessage',
descriptionArguments: args,
titleKey: 'dialog.kickTitle',
titleArguments: args
}));
};
}

View File

@@ -4,7 +4,7 @@ import React from 'react';
import { BackHandler, NativeModules, SafeAreaView, StatusBar, View } from 'react-native';
import { appNavigate } from '../../../app';
import { getAppProp } from '../../../base/app';
import { PIP_ENABLED, getFeatureFlag } from '../../../base/flags';
import { getParticipantCount } from '../../../base/participants';
import { Container, LoadingIndicator, TintedView } from '../../../base/react';
import { connect } from '../../../base/redux';
@@ -13,7 +13,7 @@ import {
makeAspectRatioAware
} from '../../../base/responsive-ui';
import { TestConnectionInfo } from '../../../base/testing';
import { ConferenceNotification } from '../../../calendar-sync';
import { ConferenceNotification, isCalendarEnabled } from '../../../calendar-sync';
import { Chat } from '../../../chat';
import { DisplayNameLabel } from '../../../display-name';
import {
@@ -42,6 +42,13 @@ import type { AbstractProps } from '../AbstractConference';
*/
type Props = AbstractProps & {
/**
* Wherther the calendar feature is enabled or not.
*
* @private
*/
_calendarEnabled: boolean,
/**
* The indicator which determines that we are still connecting to the
* conference which includes establishing the XMPP connection and then
@@ -104,13 +111,6 @@ type Props = AbstractProps & {
*/
_toolboxVisible: boolean,
/**
* The indicator which determines whether the Toolbox is always visible.
*
* @private
*/
_toolboxAlwaysVisible: boolean,
/**
* The redux {@code dispatch} function.
*/
@@ -291,10 +291,6 @@ class Conference extends AbstractConference<Props, *> {
* @returns {void}
*/
_onClick() {
if (this.props._toolboxAlwaysVisible) {
return;
}
this._setToolboxVisible(!this.props._toolboxVisible);
}
@@ -331,10 +327,10 @@ class Conference extends AbstractConference<Props, *> {
* @returns {React$Node}
*/
_renderConferenceNotification() {
// XXX If the calendar feature is disabled on a platform, then we don't
// have its components exported so an undefined check is necessary.
const { _calendarEnabled, _reducedUI } = this.props;
return (
!this.props._reducedUI && ConferenceNotification
_calendarEnabled && !_reducedUI
? <ConferenceNotification />
: undefined);
}
@@ -400,7 +396,7 @@ function _mapStateToProps(state) {
leaving
} = state['features/base/conference'];
const { reducedUI } = state['features/base/responsive-ui'];
const { alwaysVisible, visible } = state['features/toolbox'];
const { visible } = state['features/toolbox'];
// XXX There is a window of time between the successful establishment of the
// XMPP connection and the subsequent commencement of joining the MUC during
@@ -417,6 +413,14 @@ function _mapStateToProps(state) {
return {
...abstractMapStateToProps(state),
/**
* Wherther the calendar feature is enabled or not.
*
* @private
* @type {boolean}
*/
_calendarEnabled: isCalendarEnabled(state),
/**
* The indicator which determines that we are still connecting to the
* conference which includes establishing the XMPP connection and then
@@ -452,7 +456,7 @@ function _mapStateToProps(state) {
* @private
* @type {boolean}
*/
_pictureInPictureEnabled: getAppProp(state, 'pictureInPictureEnabled'),
_pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),
/**
* The indicator which determines whether the UI is reduced (to
@@ -469,15 +473,7 @@ function _mapStateToProps(state) {
* @private
* @type {boolean}
*/
_toolboxVisible: visible,
/**
* The indicator which determines whether the Toolbox is always visible.
*
* @private
* @type {boolean}
*/
_toolboxAlwaysVisible: alwaysVisible
_toolboxVisible: visible
};
}

View File

@@ -1,5 +1,5 @@
import { translateToHTML } from '../base/i18n';
import { browser } from '../base/lib-jitsi-meet';
import { isSuboptimalBrowser } from '../base/environment';
import { toState } from '../base/redux';
import { getName } from '../app';
@@ -17,16 +17,7 @@ import { getOverlayToRender } from '../overlay';
* @returns {void}
*/
export function maybeShowSuboptimalExperienceNotification(dispatch, t) {
if (!browser.isChrome()
&& !browser.isFirefox()
&& !browser.isNWJS()
&& !browser.isElectron()
// Adding react native to the list of recommended browsers is not
// necessary for now because the function won't be executed at all
// in this case but I'm adding it for completeness.
&& !browser.isReactNative()
) {
if (isSuboptimalBrowser()) {
dispatch(
showWarningNotification(
{

View File

@@ -1,5 +1,5 @@
// @flow
export * from './actions';
export * from './components';
import './middleware';

View File

@@ -1,16 +1,15 @@
// @flow
import { notifyKickedOut } from './actions';
import { appNavigate } from '../app';
import {
CONFERENCE_JOINED,
KICKED_OUT,
VIDEO_QUALITY_LEVELS,
conferenceFailed,
conferenceLeft,
getCurrentConference,
setPreferredReceiverVideoQuality
} from '../base/conference';
import { hideDialog, isDialogOpen } from '../base/dialog';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { pinParticipant } from '../base/participants';
import { SET_REDUCED_UI } from '../base/responsive-ui';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
@@ -43,9 +42,14 @@ MiddlewareRegistry.register(store => next => action => {
case KICKED_OUT: {
const { dispatch } = store;
dispatch(
conferenceFailed(action.conference, JitsiConferenceEvents.KICKED));
dispatch(appNavigate(undefined));
dispatch(notifyKickedOut(
action.participant,
() => {
dispatch(conferenceLeft(action.conference));
dispatch(appNavigate(undefined));
}
));
break;
}
}

View File

@@ -7,6 +7,7 @@ import { connect } from '../../base/redux';
import type { Dispatch } from 'redux';
import { createDeepLinkingPageEvent, sendAnalytics } from '../../analytics';
import { isSupportedBrowser } from '../../base/environment';
import { translate } from '../../base/i18n';
import {
@@ -107,8 +108,12 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
</h1>
<p className = 'description'>
{
t(`${_TNS}.description`,
{ app: NATIVE_APP_NAME })
t(
`${_TNS}.${isSupportedBrowser()
? 'description'
: 'descriptionWithoutWeb'}`,
{ app: NATIVE_APP_NAME }
)
}
</p>
<div className = 'buttons'>
@@ -118,9 +123,12 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
onClick = { this._onTryAgain }>
{ t(`${_TNS}.tryAgainButton`) }
</Button>
<Button onClick = { this._onLaunchWeb }>
{ t(`${_TNS}.launchWebButton`) }
</Button>
{
isSupportedBrowser()
&& <Button onClick = { this._onLaunchWeb }>
{ t(`${_TNS}.launchWebButton`) }
</Button>
}
</ButtonGroup>
</div>
</div>

View File

@@ -75,7 +75,7 @@ export function getDeepLinkingPage(state) {
return Promise.resolve();
}
return _openDesktopApp().then(
return _openDesktopApp(state).then(
// eslint-disable-next-line no-confusing-arrow
result => result ? DeepLinkingDesktopPage : undefined);
}
@@ -83,9 +83,10 @@ export function getDeepLinkingPage(state) {
/**
* Opens the desktop app.
*
* @param {Object} state - Object containing current redux state.
* @returns {Promise<boolean>} - Resolves with true if the attempt to open the desktop app was successful and resolves
* with false otherwise.
*/
export function openDesktopApp() {
return _openDesktopApp();
export function openDesktopApp(state) {
return _openDesktopApp(state);
}

View File

@@ -15,7 +15,7 @@ import { openDesktopApp } from './functions';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case OPEN_DESKTOP_APP:
openDesktopApp();
openDesktopApp(store.getState());
break;
}

View File

@@ -1,9 +1,12 @@
// @flow
/**
* Opens the desktop app.
*
* @param {Object} state - Object containing current redux state.
* @returns {Promise<boolean>} - Resolves with true if the attempt to open the desktop app was successful and resolves
* with false otherwise.
*/
export function _openDesktopApp() {
export function _openDesktopApp(state: Object) { // eslint-disable-line no-unused-vars
return Promise.resolve(false);
}

View File

@@ -56,7 +56,7 @@ class DisplayNamePrompt extends AbstractDisplayNamePrompt<State> {
render() {
return (
<Dialog
isModal = { true }
isModal = { false }
onSubmit = { this._onSubmit }
titleKey = 'dialog.displayNameRequired'
width = 'small'>

View File

@@ -1,3 +1,5 @@
// @flow
/**
* Appends a suffix to the display name.
*
@@ -5,7 +7,7 @@
* @param {string} suffix - Suffix that will be appended.
* @returns {string} The formatted display name.
*/
export function appendSuffix(displayName, suffix) {
export function appendSuffix(displayName: string, suffix: string) {
return `${displayName || suffix || ''}${
displayName && suffix && displayName !== suffix ? ` (${suffix})` : ''}`;
}

Some files were not shown because too many files have changed in this diff Show More