Compare commits

...

47 Commits

Author SHA1 Message Date
Aaron van Meerten
4ed9d5893b util updates 2019-09-09 08:42:00 -05:00
Aaron van Meerten
07b7f03aa7 Merge pull request #4308 from jitsi/prosody-token-allow-asap-server-override
allows override of asap key server in token utility
2019-06-06 14:26:25 -06:00
Aaron van Meerten
7ce44f85ca changed to using a setter for the asapKeyServer 2019-06-06 15:22:38 -05:00
Aaron van Meerten
41e0d782ce allows override of asap key server in token utility 2019-06-06 14:41:46 -05:00
Aaron van Meerten
2a8fafdd36 Merge pull request #4303 from jitsi/start-silent
Adds a config param startSilent to disable audio output.
2019-06-05 12:29:57 -06:00
damencho
faee1c139e Adds a config param startSilent to disable audio output. 2019-06-05 18:01:18 +01:00
virtuacoplenny
eb644987ce fix(settings): do not use non-set localStorage values (#4299)
If a value is not set in localStorage then null is
returned. null should not be converted to an empty
string (via _.escape) because that will then be
stored in localStorage as the user set preference
and will keep overriding any other values set
in localStorage for the displayname.
2019-06-04 17:42:48 -07:00
damencho
de60a70daf Commit from translate.jitsi.org by user damencho.: 555 of 625 strings translated (8 fuzzy). 2019-06-03 11:19:10 +00:00
damencho
2904dfa794 Commit from translate.jitsi.org by user damencho.: 584 of 625 strings translated (2 fuzzy). 2019-06-03 11:19:01 +00:00
damencho
d51cf7c581 Commit from translate.jitsi.org by user damencho.: 625 of 625 strings translated (0 fuzzy). 2019-06-03 10:58:25 +00:00
Hristo Terezov
f25e6c6a5d chore(package.json): Update lib-jitsi-meet version 2019-06-01 02:28:04 -07:00
Hristo Terezov
5fb9422513 feat(API): Add show feedback parameter to hangup 2019-06-01 02:28:04 -07:00
Hristo Terezov
d01cfc8466 fix(conference): API left event. 2019-06-01 02:28:04 -07:00
Saúl Ibarra Corretgé
fa3888991f rn: avoid logging initial props in release builds
They may contain sensitive information.
2019-05-31 11:49:36 +02:00
virtuacoplenny
ded355a807 fix(settings): use moderator check helper (#4292) 2019-05-30 14:10:40 -07:00
Leonard Kim
b655c8d54a fix(large-video): clear remote video stream on track removal
VideoLayout schedules a large video update by passing in
the video stream on the small video instance. When a stream
is removed, the UI is removed from the small video instance
but a reference to the stream is left. So when VideoLayout
schedules the large video update after a stream removal,
the old stream from the small video instance is re-used,
even though it has been removed.

This change also brings balance with RemoteVideo method
"addRemoteStreamElement" which sets the stream on the
small video instance, so now "removeRemoteStreamElement
unsets it.
2019-05-30 09:46:35 -07:00
Leonard Kim
42a6e6faaf ref(large-video): remove redundant call to update on stream removal 2019-05-30 09:46:35 -07:00
Leonard Kim
c7954c284d ref(large-video): move layout update on stream removal to layout middleware 2019-05-30 09:46:35 -07:00
virtuacoplenny
251da1861a feat(api): notify api of mic and camera errors (#4289)
- Use actions to notify the rest of the app that
  a mic or camera error has occurred
- Use middleware to respond to those notifications
  of errors by showing in-app notifications and
  notifying the external api
2019-05-29 14:17:07 -07:00
Hristo Terezov
9712804040 fix(Amplitude): user id 2019-05-29 09:53:31 -07:00
Hristo Terezov
fecbef0aff fix(AmplitudeModule): class name 2019-05-29 17:22:50 +02:00
Saúl Ibarra Corretgé
d65b71b584 rn: add ability to set the conference subject 2019-05-29 14:48:02 +02:00
Saúl Ibarra Corretgé
579d291bca config: add ability to pass the subject as a URL parameter 2019-05-29 14:48:02 +02:00
Saúl Ibarra Corretgé
871026f4ba conference: clear the pending subject after it has been set 2019-05-29 14:48:02 +02:00
Saúl Ibarra Corretgé
9a8a070c62 rn: show conference subject if set 2019-05-29 14:48:02 +02:00
Leonard Kim
7cf4c7bd78 Revert "feat(screenshare): enable auto-pin of latest and last screenshare"
This reverts commit f42d0411b1.

The UX provided by this feature flag in its current state is not
desired. Also, I noticed filmstrip sometimes failing to properly
update small video display mode on pin/unpin. The feature is
being left in for consumers of jitsi-meet to enable as needed.
2019-05-28 15:28:50 -07:00
Hristo Terezov
72a1def571 feat(config): whitelist config.analytics 2019-05-24 13:42:12 -07:00
damencho
0dad99c3b7 Enables local video flip menu by default. 2019-05-24 23:09:24 +03:00
Hristo Terezov
840c0190c4 fix(deep-linking): Don't rely on custom scheme 2019-05-24 12:51:14 -07:00
Leonard Kim
e0fdeea69b fix(large-video): do not stretch dominant speaker avatar 2019-05-24 12:00:35 -07:00
Leonard Kim
e3612929f8 fix(avatar): dynamically size avatar in dynamically sizable filmstrip 2019-05-24 12:00:35 -07:00
Hristo Terezov
70921bb6ef feat(analytics): local tracks duration event. 2019-05-24 10:09:25 -07:00
Saúl Ibarra Corretgé
371ca4eef1 ios: don't require bitcode for Debug builds 2019-05-24 14:11:08 +02:00
Bettenbuk Zoltan
54fdb7066f feat: scrollable bottom sheet 2019-05-24 14:06:17 +02:00
Bettenbuk Zoltan
85bcb0c757 fix: auth dialog button labels 2019-05-24 11:35:56 +02:00
Bettenbuk Zoltan
d387cbe5bd fix: iOS 10 bottom sheet style 2019-05-24 11:28:28 +02:00
paweldomas
1bc28e4904 watchos: display a message if the recent list is empty 2019-05-24 09:41:31 +02:00
Saúl Ibarra Corretgé
cb3419ba2a android: enter PiP mode when pressing back button
When in a conference, try to enter PiP when pressing the back button. If this is
not possible (because it's unsupported, not enabled, etc.) fall back to the
previous behavior of simply hanging up.
2019-05-23 16:00:12 +02:00
Saúl Ibarra Corretgé
a2f8e156da app: avoid loading config when going back to the welcome page 2019-05-23 15:16:31 +02:00
Saúl Ibarra Corretgé
a4cf79c161 rn: fix losing audio if call is hangup too quickly
This PR changes the logic for connecting / disconnecting conferences. Instead of
doing it in mount / unmount events from the Conference component, it moves the
logic to the appNavigatee action.

This fixes a regression introduced in 774c5ecd when trying to make sure the
conference terminated event is always sent.

By moving the logic to appNavigate we no longer depend on side-effects for
connecting / disconnecting, and the code should be more maintainable moving
forward.

An improvement to this is the concept of sessions, which, while not tackled
here, was taken into consideration.
2019-05-23 15:16:31 +02:00
Saúl Ibarra Corretgé
9352517705 ios: always log delegate method calls 2019-05-23 15:16:31 +02:00
Saúl Ibarra Corretgé
47d5163c52 rn: don't tag builds by default
People run these in their own checkout and will run into problems because
tagging will fail.
2019-05-23 12:07:04 +02:00
Bettenbuk Zoltan
def22b01bb fix: set explicit color for search field to avoid theme override 2019-05-23 12:06:50 +02:00
Saúl Ibarra Corretgé
9445cf99fd Revert "ios: remove no longer needed code"
This reverts commit 603d161788.
2019-05-22 18:10:35 +02:00
paweldomas
96b226de24 watchos: change the icons
Inverts the icons to follow more what's in the phone app instead of
CallKit.
2019-05-22 17:25:35 +02:00
Bettenbuk Zoltan
5101f69e4e feat: don’t render moderator icon if everyone is moderator 2019-05-22 14:08:52 +02:00
François Benaiteau
61b66e0edf doc: fix incorrect code examples for universal / deep linking 2019-05-22 14:08:37 +02:00
86 changed files with 3244 additions and 2005 deletions

View File

@@ -10,6 +10,7 @@ MVN_HTTP=0
DEFAULT_SDK_VERSION=$(grep sdkVersion ${THIS_DIR}/../gradle.properties | cut -d"=" -f2)
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
RN_VERSION=$(jq -r '.dependencies."react-native"' ${THIS_DIR}/../../package.json)
DO_GIT_TAG=${GIT_TAG:-0}
if [[ $THE_MVN_REPO == http* ]]; then
MVN_HTTP=1
@@ -64,13 +65,11 @@ pushd ${THIS_DIR}/../
./gradlew clean assembleRelease publish
popd
if [[ $MVN_HTTP == 0 ]]; then
if [[ $DO_GIT_TAG == 1 ]]; then
# The artifacts are now on the Maven repo, commit them
pushd ${MVN_REPO_PATH}
if [[ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" == "true" ]]; then
git add -A .
git commit -m "Jitsi Meet SDK + dependencies: ${SDK_VERSION}"
fi
git add -A .
git commit -m "Jitsi Meet SDK + dependencies: ${SDK_VERSION}"
popd
# Tag the release

View File

@@ -52,6 +52,17 @@ class AmplitudeModule
Amplitude.getInstance(instanceName).initialize(getCurrentActivity(), apiKey);
}
/**
* Sets the user ID for an Amplitude instance.
*
* @param instanceName The name of the Amplitude instance.
* @param userId The new value for the user ID.
*/
@ReactMethod
public void setUserId(String instanceName, String userId) {
Amplitude.getInstance(instanceName).setUserId(userId);
}
/**
* Sets the user properties for an Amplitude instance.
*

View File

@@ -40,6 +40,10 @@ public class JitsiMeetConferenceOptions implements Parcelable {
* Room name.
*/
private String room;
/**
* Conference subject.
*/
private String subject;
/**
* JWT token used for authentication.
*/
@@ -70,6 +74,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
public static class Builder {
private URL serverURL;
private String room;
private String subject;
private String token;
private Bundle colorScheme;
@@ -105,6 +110,17 @@ public class JitsiMeetConferenceOptions implements Parcelable {
return this;
}
/**
* Sets the conference subject.
* @param subject - Subject for the conference.
* @return - The {@link Builder} object itself so the method calls can be chained.
*/
public Builder setSubject(String subject) {
this.subject = subject;
return this;
}
/**
* Sets the JWT token to be used for authentication when joining a conference.
* @param token - The JWT token to be used for authentication.
@@ -185,6 +201,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
options.serverURL = this.serverURL;
options.room = this.room;
options.subject = this.subject;
options.token = this.token;
options.colorScheme = this.colorScheme;
options.audioMuted = this.audioMuted;
@@ -201,6 +218,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
private JitsiMeetConferenceOptions(Parcel in) {
room = in.readString();
subject = in.readString();
token = in.readString();
colorScheme = in.readBundle();
byte tmpAudioMuted = in.readByte();
@@ -238,6 +256,9 @@ public class JitsiMeetConferenceOptions implements Parcelable {
if (videoMuted != null) {
config.putBoolean("startWithVideoMuted", videoMuted);
}
if (subject != null) {
config.putString("subject", subject);
}
Bundle urlProps = new Bundle();
@@ -281,6 +302,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(room);
dest.writeString(subject);
dest.writeString(token);
dest.writeBundle(colorScheme);
dest.writeByte((byte) (audioMuted == null ? 0 : audioMuted ? 1 : 2));

View File

@@ -66,7 +66,7 @@ class ReactInstanceManagerHolder {
}
try {
Class<?> amplitudeModuleClass = Class.forName("AmplitudeModule");
Class<?> amplitudeModuleClass = Class.forName("org.jitsi.meet.sdk.AmplitudeModule");
Constructor constructor = amplitudeModuleClass.getConstructor(ReactApplicationContext.class);
nativeModules.add((NativeModule)constructor.newInstance(reactContext));
} catch (Exception e) {

View File

@@ -51,6 +51,8 @@ import {
import {
checkAndNotifyForNewDevice,
getAvailableDevices,
notifyCameraError,
notifyMicError,
setAudioOutputDeviceId,
updateDeviceList
} from './react/features/base/devices';
@@ -486,10 +488,13 @@ class ConferenceConnector {
* call in hangup() to resolve when all operations are finished.
*/
function disconnect() {
connection.disconnect();
APP.API.notifyConferenceLeft(APP.conference.roomName);
const onDisconnected = () => {
APP.API.notifyConferenceLeft(APP.conference.roomName);
return Promise.resolve();
return Promise.resolve();
};
return connection.disconnect().then(onDisconnected, onDisconnected);
}
/**
@@ -694,13 +699,14 @@ export default {
// If both requests for 'audio' + 'video' and 'audio'
// only failed, we assume that there are some problems
// with user's microphone and show corresponding dialog.
APP.UI.showMicErrorNotification(audioOnlyError);
APP.UI.showCameraErrorNotification(videoOnlyError);
APP.store.dispatch(notifyMicError(audioOnlyError));
APP.store.dispatch(notifyCameraError(videoOnlyError));
} else {
// If request for 'audio' + 'video' failed, but request
// for 'audio' only was OK, we assume that we had
// problems with camera and show corresponding dialog.
APP.UI.showCameraErrorNotification(audioAndVideoError);
APP.store.dispatch(
notifyCameraError(audioAndVideoError));
}
}
@@ -786,6 +792,13 @@ export default {
this.recorder = new Recorder();
}
if (config.startSilent) {
APP.store.dispatch(showNotification({
descriptionKey: 'notify.startSilentDescription',
titleKey: 'notify.startSilentTitle'
}));
}
// XXX The API will take care of disconnecting from the XMPP
// server (and, thus, leaving the room) on unload.
return new Promise((resolve, reject) => {
@@ -839,7 +852,7 @@ export default {
if (!this.localAudio && !mute) {
const maybeShowErrorDialog = error => {
showUI && APP.UI.showMicErrorNotification(error);
showUI && APP.store.dispatch(notifyMicError(error));
};
createLocalTracksF({ devices: [ 'audio' ] }, false)
@@ -902,7 +915,7 @@ export default {
if (!this.localVideo && !mute) {
const maybeShowErrorDialog = error => {
showUI && APP.UI.showCameraErrorNotification(error);
showUI && APP.store.dispatch(notifyCameraError(error));
};
// Try to create local video if there wasn't any.
@@ -2109,7 +2122,7 @@ export default {
this._updateVideoDeviceId();
})
.catch(err => {
APP.UI.showCameraErrorNotification(err);
APP.store.dispatch(notifyCameraError(err));
});
}
);
@@ -2142,7 +2155,7 @@ export default {
this._updateAudioDeviceId();
})
.catch(err => {
APP.UI.showMicErrorNotification(err);
APP.store.dispatch(notifyMicError(err));
});
}
);
@@ -2600,8 +2613,7 @@ export default {
leaveRoomAndDisconnect() {
APP.store.dispatch(conferenceWillLeave(room));
return room.leave()
.then(disconnect, disconnect);
return room.leave().then(disconnect, disconnect);
},
/**

View File

@@ -90,6 +90,10 @@ var config = {
// applied locally. FIXME: having these 2 options is confusing.
// startWithAudioMuted: false,
// Enabling it (with #params) will disable local audio output of remote
// participants and to enable it back a reload is needed.
// startSilent: false
// Video
// Sets the preferred resolution (height) for local video. Defaults to 720.
@@ -414,6 +418,10 @@ var config = {
// use only.
// _desktopSharingSourceDevice: 'sample-id-or-label'
// A property to disable the right click context menu for localVideo
// the menu has option to flip the locally seen video for local presentations
// disableLocalVideoFlip: false
// List of undocumented settings used in jitsi-meet
/**
_immediateReloadThreshold
@@ -428,7 +436,6 @@ var config = {
dialOutCodesUrl
disableRemoteControl
displayJids
enableLocalVideoFlip
etherpad_base
externalConnectUrl
firefox_fake_device

View File

@@ -514,6 +514,7 @@
}
#dominantSpeakerAvatar {
background-color: #000000;
object-fit: cover;
}
.dynamic-shadow {
@@ -525,12 +526,21 @@
transition: box-shadow 0.3s ease;
}
.userAvatar {
.avatar-container {
@include maxSize(60px);
@include absoluteAligning();
border-radius: 50%;
display: flex;
justify-content: center;
height: 50%;
overflow: hidden;
width: auto;
.userAvatar {
height: 100%;
object-fit: cover;
width: 100%;
}
}
#videoNotAvailableScreen {

View File

@@ -6,7 +6,7 @@
/**
* Let the avatar grow with the tile.
*/
.userAvatar {
.avatar-container {
max-height: initial;
max-width: initial;
}

View File

@@ -264,6 +264,14 @@ The `event` parameter is a String object with the name of the event.
The `listener` parameter is a Function object with one argument that will be notified when the event occurs with data related to the event.
The following events are currently supported:
* **cameraError** - event notifications about Jitsi-Meet having failed to access the camera. The listener will receive an object with the following structure:
```javascript
{
type: string, // A constant representing the overall type of the error.
message: string // Additional information about the error.
}
```
* **avatarChanged** - event notifications about avatar
changes. The listener will receive an object with the following structure:
```javascript
@@ -287,6 +295,14 @@ changes. The listener will receive an object with the following structure:
}
```
* **micError** - event notifications about Jitsi-Meet having failed to access the mic. The listener will receive an object with the following structure:
```javascript
{
type: string, // A constant representing the overall type of the error.
message: string // Additional information about the error.
}
```
* **screenSharingStatusChanged** - receives event notifications about turning on/off the local user screen sharing. The listener will receive object with the following structure:
```javascript
{

View File

@@ -167,13 +167,7 @@ var interfaceConfig = {
*
* @type {boolean}
*/
RECENT_LIST_ENABLED: true,
/**
* 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: true
RECENT_LIST_ENABLED: true
/**
* How many columns the tile view can expand to. The respected range is
@@ -201,6 +195,12 @@ 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

@@ -92,7 +92,7 @@ Leaves the currently active conference.
#### Universal / deep linking
In order to support Universal / deep linking, `JitsiMeetView` offers 2 class
In order to support Universal / deep linking, `JitsiMeet` offers 2 class
methods that you app's delegate should call in order for the app to follow those
links.
@@ -104,7 +104,7 @@ is useful when the host application uses other SDKs which also use linking.
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler
{
return [JitsiMeetView application:application
return [[JitsiMeet sharedInstance] application:application
continueUserActivity:userActivity
restorationHandler:restorationHandler];
}
@@ -117,7 +117,7 @@ And also one of the following:
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
return [JitsiMeetView application:app
return [[JitsiMeet sharedInstance] application:app
openURL:url
options: options];
}

View File

@@ -737,7 +737,7 @@
CURRENT_PROJECT_VERSION = 1;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = FC967L3QRG;
ENABLE_BITCODE = YES;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"../../node_modules/react-native-webrtc/ios",

View File

@@ -42,11 +42,11 @@
- (void)_onJitsiMeetViewDelegateEvent:(NSString *)name
withData:(NSDictionary *)data {
#if DEBUG
NSLog(
@"[%s:%d] JitsiMeetViewDelegate %@ %@",
__FILE__, __LINE__, name, data);
#if DEBUG
NSAssert(
[NSThread isMainThread],
@"JitsiMeetViewDelegate %@ method invoked on a non-main thread",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -14,6 +14,7 @@
<objects>
<controller title="Meetings" id="AgC-eL-Hgc" customClass="InterfaceController" customModule="JitsiMeetCompanion" customModuleProvider="target">
<items>
<label alignment="left" textAlignment="left" numberOfLines="0" id="OQN-sx-tDt"/>
<table alignment="left" id="gpO-ql-Xsr">
<items>
<tableRow identifier="MeetingRowType" id="GGl-av-xeJ" customClass="MeetingRowController" customModule="JitsiMeetCompanion_Extension">
@@ -39,6 +40,7 @@
</table>
</items>
<connections>
<outlet property="infoLabel" destination="OQN-sx-tDt" id="Juv-tb-SNj"/>
<outlet property="table" destination="gpO-ql-Xsr" id="aVV-iZ-z3l"/>
</connections>
</controller>

View File

@@ -16,17 +16,31 @@
*/
import WatchKit
import WatchConnectivity
import Foundation
class InterfaceController: WKInterfaceController {
@IBOutlet var infoLabel: WKInterfaceLabel!
@IBOutlet var table: WKInterfaceTable!
override func didAppear(){
self.updateUI(ExtensionDelegate.currentJitsiMeetContext)
}
func updateUI(_ newContext:JitsiMeetContext) {
if let recentURLsArray = newContext.recentURLs {
updateRecents(withRecents: recentURLsArray, currentContext: newContext)
}
if (newContext.recentURLs == nil || newContext.recentURLs!.count == 0) {
infoLabel.setText("There are no recent meetings. Please use the app on the phone to start a new call.")
table.setHidden(true)
infoLabel.setHidden(false)
} else {
updateRecents(withRecents: newContext.recentURLs!, currentContext: newContext)
table.setHidden(false)
infoLabel.setHidden(true)
}
}
private func updateRecents(withRecents recents: NSArray, currentContext: JitsiMeetContext) {
@@ -68,7 +82,7 @@ class InterfaceController: WKInterfaceController {
override func contextForSegue(withIdentifier segueIdentifier: String, in table: WKInterfaceTable, rowIndex: Int) -> Any? {
let controller = table.rowController(at: rowIndex) as! MeetingRowController
let currentContext = ExtensionDelegate.currentJitsiMeetContext
// Copy the current context and add the joinConferenceURL to trigger the command when the in-call screen is displayed
let actionContext = JitsiMeetContext(jmContext: currentContext)
actionContext.joinConferenceURL = controller.roomUrl

View File

@@ -7,6 +7,7 @@ PROJECT_REPO=$(realpath ${THIS_DIR}/../..)
RELEASE_REPO=$(realpath ${THIS_DIR}/../../../jitsi-meet-ios-sdk-releases)
DEFAULT_SDK_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${THIS_DIR}/../sdk/src/Info.plist)
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
DO_GIT_TAG=${GIT_TAG:-0}
echo "Releasing Jitsi Meet SDK ${SDK_VERSION}"
@@ -25,7 +26,9 @@ popd
pushd ${PROJECT_REPO}
rm -rf ios/sdk/JitsiMeet.framework
xcodebuild -workspace ios/jitsi-meet.xcworkspace -scheme JitsiMeet -destination='generic/platform=iOS' -configuration Release archive
git tag ios-sdk-${SDK_VERSION}
if [[ $DO_GIT_TAG == 1 ]]; then
git tag ios-sdk-${SDK_VERSION}
fi
popd
pushd ${RELEASE_REPO}
@@ -39,9 +42,11 @@ xcrun bitcode_strip -r Frameworks/JitsiMeet.framework/JitsiMeet -o Frameworks/Ji
xcrun bitcode_strip -r Frameworks/WebRTC.framework/WebRTC -o Frameworks/WebRTC.framework/WebRTC
# Add all files to git
git add -A .
git commit -m "${SDK_VERSION}"
git tag ${SDK_VERSION}
if [[ $DO_GIT_TAG == 1 ]]; then
git add -A .
git commit -m "${SDK_VERSION}"
git tag ${SDK_VERSION}
fi
popd

View File

@@ -44,6 +44,7 @@
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
DEAD3226220C497000E93636 /* JitsiMeetConferenceOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = DEAD3224220C497000E93636 /* JitsiMeetConferenceOptions.h */; settings = {ATTRIBUTES = (Public, ); }; };
DEAD3227220C497000E93636 /* JitsiMeetConferenceOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */; };
DEAFA779229EAD520033A7FA /* RNRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = DEAFA778229EAD520033A7FA /* RNRootView.m */; };
DEFC743F21B178FA00E4DD96 /* LocaleDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */; };
DEFE535421FB1BF800011A3A /* JitsiMeet.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE535321FB1BF800011A3A /* JitsiMeet.m */; };
DEFE535621FB2E8300011A3A /* ReactUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE535521FB2E8300011A3A /* ReactUtils.m */; };
@@ -96,6 +97,8 @@
DEAD3224220C497000E93636 /* JitsiMeetConferenceOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetConferenceOptions.h; sourceTree = "<group>"; };
DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetConferenceOptions.m; sourceTree = "<group>"; };
DEAD3228220C734300E93636 /* JitsiMeetConferenceOptions+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetConferenceOptions+Private.h"; sourceTree = "<group>"; };
DEAFA777229EAD3B0033A7FA /* RNRootView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNRootView.h; sourceTree = "<group>"; };
DEAFA778229EAD520033A7FA /* RNRootView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNRootView.m; sourceTree = "<group>"; };
DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocaleDetector.m; sourceTree = "<group>"; };
DEFE535321FB1BF800011A3A /* JitsiMeet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeet.m; sourceTree = "<group>"; };
DEFE535521FB2E8300011A3A /* ReactUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactUtils.m; sourceTree = "<group>"; };
@@ -173,6 +176,8 @@
DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */,
0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */,
0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */,
DEAFA777229EAD3B0033A7FA /* RNRootView.h */,
DEAFA778229EAD520033A7FA /* RNRootView.m */,
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */,
0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */,
DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */,
@@ -478,6 +483,7 @@
files = (
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
DEAFA779229EAD520033A7FA /* RNRootView.m in Sources */,
DEAD3227220C497000E93636 /* JitsiMeetConferenceOptions.m in Sources */,
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,
@@ -634,7 +640,7 @@
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
DYLIB_INSTALL_NAME_BASE = "@rpath";
ENABLE_BITCODE = YES;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = src/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";

View File

@@ -14,6 +14,8 @@
* limitations under the License.
*/
#import <React/RCTBridge.h>
#import "JitsiMeet.h"
@interface JitsiMeet ()

View File

@@ -26,6 +26,10 @@
* Room name.
*/
@property (nonatomic, copy, nullable) NSString *room;
/**
* Conference subject.
*/
@property (nonatomic, copy, nullable) NSString *subject;
/**
* JWT token used for authentication.
*/
@@ -56,7 +60,9 @@
@interface JitsiMeetConferenceOptions : NSObject
@property (nonatomic, copy, nullable, readonly) NSURL *serverURL;
@property (nonatomic, copy, nullable, readonly) NSString *room;
@property (nonatomic, copy, nullable, readonly) NSString *subject;
@property (nonatomic, copy, nullable, readonly) NSString *token;
@property (nonatomic, copy, nullable) NSDictionary *colorScheme;

View File

@@ -34,6 +34,7 @@
if (self = [super init]) {
_serverURL = nil;
_room = nil;
_subject = nil;
_token = nil;
_colorScheme = nil;
@@ -138,6 +139,7 @@
if (self = [super init]) {
_serverURL = builder.serverURL;
_room = builder.room;
_subject = builder.subject;
_token = builder.token;
_colorScheme = builder.colorScheme;
@@ -183,6 +185,9 @@
if (_videoMuted != nil) {
config[@"startWithVideoMuted"] = @(self.videoMuted);
}
if (_subject != nil) {
config[@"subject"] = self.subject;
}
NSMutableDictionary *urlProps = [[NSMutableDictionary alloc] init];

View File

@@ -17,12 +17,11 @@
#include <mach/mach_time.h>
#import <React/RCTRootView.h>
#import "JitsiMeet+Private.h"
#import "JitsiMeetConferenceOptions+Private.h"
#import "JitsiMeetView+Private.h"
#import "ReactUtils.h"
#import "RNRootView.h"
@implementation JitsiMeetView {
@@ -36,7 +35,7 @@
/**
* React Native view where the entire content will be rendered.
*/
RCTRootView *rootView;
RNRootView *rootView;
}
/**
@@ -145,9 +144,9 @@ static void initializeViewsMap() {
} else {
RCTBridge *bridge = [[JitsiMeet sharedInstance] getReactBridge];
rootView
= [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"App"
initialProperties:props];
= [[RNRootView alloc] initWithBridge:bridge
moduleName:@"App"
initialProperties:props];
rootView.backgroundColor = self.backgroundColor;
// Add rootView as a subview which completely covers this one.

20
ios/sdk/src/RNRootView.h Normal file
View File

@@ -0,0 +1,20 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <React/RCTRootView.h>
@interface RNRootView : RCTRootView
@end

45
ios/sdk/src/RNRootView.m Normal file
View File

@@ -0,0 +1,45 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <React/RCTRootContentView.h>
#import <React/RCTLog.h>
#import "RNRootView.h"
@implementation RNRootView
// Monkey-patch RCTRootView.runApplication to avoid logging initial props.
- (void)runApplication:(RCTBridge *)bridge
{
NSString *moduleName = [self valueForKey:@"_moduleName"] ?: @"";
RCTRootContentView *_contentView = [self valueForKey:@"_contentView"];
NSNumber *reactTag = [_contentView valueForKey:@"reactTag"];
NSDictionary *appParameters = @{
@"rootTag": reactTag,
@"initialProps": self.appProperties ?: @{},
};
#if DEBUG
RCTLogInfo(@"Running application %@ (%@)", moduleName, appParameters);
#endif
[bridge enqueueJSCall:@"AppRegistry"
method:@"runApplication"
args:@[moduleName, appParameters]
completion:NULL];
}
@end

View File

@@ -33,6 +33,10 @@ RCT_EXPORT_METHOD(init:(NSString*)instanceName API_KEY:(NSString*)apiKey) {
[[Amplitude instanceWithName:instanceName] initializeApiKey:apiKey];
}
RCT_EXPORT_METHOD(setUserId:(NSString*)instanceName userId: (NSString *) userId) {
[[Amplitude instanceWithName:instanceName] setUserId:userId];
}
RCT_EXPORT_METHOD(setUserProperties:(NSString*)instanceName userPropsString:(NSDictionary*)userProps) {
if (userProps != nil) {
[[Amplitude instanceWithName:instanceName] setUserProperties:userProps];

View File

@@ -21,6 +21,7 @@ import Foundation
internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
private let listeners = NSMutableArray()
private var pendingMuteActions = Set<UUID>()
internal override init() {}
@@ -36,6 +37,12 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
listeners.remove(listener)
}
// MARK: - Add mute action
func addMuteAction(_ actionUUID: UUID) {
pendingMuteActions.insert(actionUUID)
}
// MARK: - CXProviderDelegate
func providerDidReset(_ provider: CXProvider) {
@@ -43,6 +50,7 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
let listener = $0 as! JMCallKitListener
listener.providerDidReset?()
}
pendingMuteActions.removeAll()
}
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
@@ -64,9 +72,20 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
}
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
listeners.forEach {
let listener = $0 as! JMCallKitListener
listener.performSetMutedCall?(UUID: action.callUUID, isMuted: action.isMuted)
let uuid = pendingMuteActions.remove(action.uuid)
// Avoid mute actions ping-pong: if the mute action was caused by
// the JS side (we requested a transaction) don't call the delegate
// method. If it was called by the provder itself (when the user presses
// the mute button in the CallKit view) then call the delegate method.
//
// NOTE: don't try to be clever and remove this. Been there, done that.
// Won't work.
if (uuid == nil) {
listeners.forEach {
let listener = $0 as! JMCallKitListener
listener.performSetMutedCall?(UUID: action.callUUID, isMuted: action.isMuted)
}
}
action.fulfill()

View File

@@ -160,6 +160,14 @@ import Foundation
completion: @escaping (Error?) -> Swift.Void) {
guard enabled else { return }
// XXX keep track of muted actions to avoid "ping-pong"ing. See
// JMCallKitEmitter for details on the CXSetMutedCallAction handling.
for action in transaction.actions {
if (action as? CXSetMutedCallAction) != nil {
emitter.addMuteAction(action.uuid)
}
}
callController.request(transaction, completion: completion)
}
@@ -187,3 +195,4 @@ import Foundation
return update
}
}

View File

@@ -1,11 +1,19 @@
{
"en": "Anglés",
"af": "Afrikaans",
"az": "Azèri",
"bg": "Bulgar",
"cs": "Chèc",
"de": "Aleman",
"el": "Grèc",
"eo": "Esperanto",
"es": "Castelhan",
"fr": "Francés",
"hy": "Armenian",
"it": "Italian",
"ja": "Japonés",
"ko": "Corean",
"nb": "Norvegian Bokmål",
"oc": "Occitan",
"pl": "Polonés",
"ptBR": "Portugués (Brasil)",
@@ -14,7 +22,6 @@
"sl": "Eslovèn",
"sv": "Suedés",
"tr": "Turc",
"zhCN": "Chinés (China)",
"nb": "Norvegian Bokmål",
"eo": "Esperanto"
"vi": "Vietnamian",
"zhCN": "Chinés (China)"
}

View File

@@ -1,5 +1,6 @@
{
"en": "Inglês",
"af": "Africâner",
"az": "Azerbaijanês",
"bg": "Búlgaro",
"cs": "Checo",

View File

@@ -1,11 +1,19 @@
{
"en": "Tiếng Anh",
"af": "Tiếng Afrika",
"az": "Tiếng Azecbaizan",
"bg": "Tiếng Bulgaria",
"cs": "Tiếng Séc",
"de": "Tiếng Đức",
"el": "Tiếng Nhật",
"eo": "Tiếng Esperanto",
"es": "Tiếng Tây Ban Nha",
"fr": "Tiếng Pháp",
"hy": "Tiếng Acmenia",
"it": "Tiếng Ý",
"ja": "Tiếng Nhật",
"ko": "Tiếng Hàn",
"nb": "Tiếng Na Uy",
"oc": "Tiếng Occitan",
"pl": "Tiếng Ba Lan",
"ptBR": "Tiếng Bồ Đào Nha (Brazil)",
@@ -14,7 +22,6 @@
"sl": "Tiếng Slovenia",
"sv": "Tiếng Thụy Điển",
"tr": "Tiếng Thổ Nhĩ Kỳ",
"zhCN": "Tiếng Hoa (Trung Quốc)",
"nb": "Tiếng Na Uy",
"eo": "Tiếng Esperanto"
"vi": "Tiếng Việt",
"zhCN": "Tiếng Hoa (Trung Quốc)"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -478,6 +478,8 @@
"mutedTitle": "You're muted!",
"raisedHand": "__name__ would like to speak.",
"somebody": "Somebody",
"startSilentTitle": "You joined with no audio output!",
"startSilentDescription": "Rejoin the meeting to enable audio",
"suboptimalExperienceDescription": "Eer... we are afraid your experience with __appName__ isn't going to be that great here. We are looking for ways to improve this but, until then, please try using one of the <a href='static/recommendedBrowsers.html' target='_blank'>fully supported browsers</a>.",
"suboptimalExperienceTitle": "Browser Warning",
"newDeviceCameraTitle": "New camera detected",

View File

@@ -103,9 +103,9 @@ function initCommands() {
APP.store.dispatch(toggleTileView());
},
'video-hangup': () => {
'video-hangup': (showFeedbackDialog = true) => {
sendAnalytics(createApiEvent('video.hangup'));
APP.conference.hangup(true);
APP.conference.hangup(showFeedbackDialog);
},
'email': email => {
sendAnalytics(createApiEvent('email.changed'));
@@ -559,6 +559,38 @@ class API {
});
}
/**
* Notify external application of an unexpected camera-related error having
* occurred.
*
* @param {string} type - The type of the camera error.
* @param {string} message - Additional information about the error.
* @returns {void}
*/
notifyOnCameraError(type: string, message: string) {
this._sendEvent({
name: 'camera-error',
type,
message
});
}
/**
* Notify external application of an unexpected mic-related error having
* occurred.
*
* @param {string} type - The type of the mic error.
* @param {string} message - Additional information about the error.
* @returns {void}
*/
notifyOnMicError(type: string, message: string) {
this._sendEvent({
name: 'mic-error',
type,
message
});
}
/**
* Notify external application (if API is enabled) that conference feedback
* has been submitted. Intended to be used in conjunction with the

View File

@@ -51,6 +51,7 @@ const events = {
'avatar-changed': 'avatarChanged',
'audio-availability-changed': 'audioAvailabilityChanged',
'audio-mute-status-changed': 'audioMuteStatusChanged',
'camera-error': 'cameraError',
'device-list-changed': 'deviceListChanged',
'display-name-change': 'displayNameChange',
'email-change': 'emailChange',
@@ -58,6 +59,7 @@ const events = {
'feedback-prompt-displayed': 'feedbackPromptDisplayed',
'filmstrip-display-changed': 'filmstripDisplayChanged',
'incoming-message': 'incomingMessage',
'mic-error': 'micError',
'outgoing-message': 'outgoingMessage',
'participant-joined': 'participantJoined',
'participant-left': 'participantLeft',

View File

@@ -13,16 +13,12 @@ import SharedVideoManager from './shared_video/SharedVideo';
import VideoLayout from './videolayout/VideoLayout';
import Filmstrip from './videolayout/Filmstrip';
import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet';
import { getLocalParticipant } from '../../react/features/base/participants';
import { toggleChat } from '../../react/features/chat';
import { openDisplayNamePrompt } from '../../react/features/display-name';
import { setEtherpadHasInitialzied } from '../../react/features/etherpad';
import { setFilmstripVisible } from '../../react/features/filmstrip';
import {
setNotificationsEnabled,
showWarningNotification
} from '../../react/features/notifications';
import { setNotificationsEnabled } from '../../react/features/notifications';
import {
dockToolbox,
setToolboxEnabled,
@@ -40,39 +36,6 @@ UI.eventEmitter = eventEmitter;
let etherpadManager;
let sharedVideoManager;
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
microphone: {},
camera: {}
};
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
.camera[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]
= 'dialog.cameraUnsupportedResolutionError';
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.GENERAL]
= 'dialog.cameraUnknownError';
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.PERMISSION_DENIED]
= 'dialog.cameraPermissionDeniedError';
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.NOT_FOUND]
= 'dialog.cameraNotFoundError';
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.CONSTRAINT_FAILED]
= 'dialog.cameraConstraintFailedError';
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
.camera[JitsiTrackErrors.NO_DATA_FROM_SOURCE]
= 'dialog.cameraNotSendingData';
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[JitsiTrackErrors.GENERAL]
= 'dialog.micUnknownError';
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
.microphone[JitsiTrackErrors.PERMISSION_DENIED]
= 'dialog.micPermissionDeniedError';
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[JitsiTrackErrors.NOT_FOUND]
= 'dialog.micNotFoundError';
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
.microphone[JitsiTrackErrors.CONSTRAINT_FAILED]
= 'dialog.micConstraintFailedError';
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
.microphone[JitsiTrackErrors.NO_DATA_FROM_SOURCE]
= 'dialog.micNotSendingData';
const UIListeners = new Map([
[
UIEvents.ETHERPAD_CLICKED,
@@ -295,12 +258,6 @@ UI.addLocalStream = track => {
}
};
/**
* Removed remote stream from UI.
* @param {JitsiTrack} track stream to remove
*/
UI.removeRemoteStream = track => VideoLayout.onRemoteStreamRemoved(track);
/**
* Setup and show Etherpad.
* @param {string} name etherpad id
@@ -774,65 +731,6 @@ UI.showExtensionInlineInstallationDialog = function(callback) {
});
};
/**
* Shows a notifications about the passed in microphone error.
*
* @param {JitsiTrackError} micError - An error object related to using or
* acquiring an audio stream.
* @returns {void}
*/
UI.showMicErrorNotification = function(micError) {
if (!micError) {
return;
}
const { message, name } = micError;
const micJitsiTrackErrorMsg
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name];
const micErrorMsg = micJitsiTrackErrorMsg
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
.microphone[JitsiTrackErrors.GENERAL];
const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message;
APP.store.dispatch(showWarningNotification({
description: additionalMicErrorMsg,
descriptionKey: micErrorMsg,
titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
? 'deviceError.microphonePermission'
: 'deviceError.microphoneError'
}));
};
/**
* Shows a notifications about the passed in camera error.
*
* @param {JitsiTrackError} cameraError - An error object related to using or
* acquiring a video stream.
* @returns {void}
*/
UI.showCameraErrorNotification = function(cameraError) {
if (!cameraError) {
return;
}
const { message, name } = cameraError;
const cameraJitsiTrackErrorMsg
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name];
const cameraErrorMsg = cameraJitsiTrackErrorMsg
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
.camera[JitsiTrackErrors.GENERAL];
const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message;
APP.store.dispatch(showWarningNotification({
description: additionalCameraErrorMsg,
descriptionKey: cameraErrorMsg,
titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
? 'deviceError.cameraPermission' : 'deviceError.cameraError'
}));
};
/**
* Shows error dialog that informs the user that no data is received from the
* device.

View File

@@ -361,6 +361,12 @@ const Filmstrip = {
'min-width': `${local.thumbWidth}px`,
width: `${local.thumbWidth}px`
});
const avatarSize = local.thumbHeight / 2;
thumbs.localThumb.find('.avatar-container')
.height(avatarSize)
.width(avatarSize);
}
if (thumbs.remoteThumbs) {
@@ -371,6 +377,12 @@ const Filmstrip = {
'min-width': `${remote.thumbWidth}px`,
width: `${remote.thumbWidth}px`
});
const avatarSize = remote.thumbHeight / 2;
thumbs.remoteThumbs.find('.avatar-container')
.height(avatarSize)
.width(avatarSize);
}
const currentLayout = getCurrentLayout(APP.store.getState());

View File

@@ -32,7 +32,7 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
this.localVideoId = null;
this.bindHoverHandler();
if (config.enableLocalVideoFlip) {
if (!config.disableLocalVideoFlip) {
this._buildContextMenu();
}
this.isLocal = true;

View File

@@ -357,6 +357,11 @@ RemoteVideo.prototype.removeRemoteStreamElement = function(stream) {
logger.info(`${isVideo ? 'Video' : 'Audio'
} removed ${this.id}`, select);
if (stream === this.videoStream) {
this.videoStream = null;
}
this.updateView();
};

View File

@@ -1,4 +1,4 @@
/* global $, APP, interfaceConfig */
/* global $, APP, config, interfaceConfig */
/* eslint-disable no-unused-vars */
import React from 'react';
@@ -199,6 +199,8 @@ SmallVideo.createStreamElement = function(stream) {
if (isVideo) {
element.setAttribute('muted', 'true');
} else if (config.startSilent) {
element.muted = true;
}
element.autoplay = true;

View File

@@ -348,10 +348,6 @@ const VideoLayout = {
remoteVideo.removeRemoteStreamElement(stream);
}
if (stream.isVideoTrack()) {
this._updateLargeVideoIfDisplayed(id);
}
this.updateMutedForNoTracks(id, stream.getType());
},

View File

@@ -1,6 +1,10 @@
/* global APP, JitsiMeetJS */
import { getAudioOutputDeviceId } from '../../react/features/base/devices';
import {
getAudioOutputDeviceId,
notifyCameraError,
notifyMicError
} from '../../react/features/base/devices';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId,
@@ -176,11 +180,11 @@ export default {
]))
.then(tracks => {
if (audioTrackError) {
APP.UI.showMicErrorNotification(audioTrackError);
APP.store.dispatch(notifyMicError(audioTrackError));
}
if (videoTrackError) {
APP.UI.showCameraErrorNotification(videoTrackError);
APP.store.dispatch(notifyCameraError(videoTrackError));
}
return tracks.filter(t => typeof t !== 'undefined');
@@ -205,7 +209,7 @@ export default {
})
.catch(err => {
audioTrackError = err;
showError && APP.UI.showMicErrorNotification(err);
showError && APP.store.disptach(notifyMicError(err));
return [];
}));
@@ -223,7 +227,7 @@ export default {
})
.catch(err => {
videoTrackError = err;
showError && APP.UI.showCameraErrorNotification(err);
showError && APP.store.dispatch(notifyCameraError(err));
return [];
}));

4
package-lock.json generated
View File

@@ -8927,8 +8927,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#23503b0efd4b97989af7b10696ec9ae790aec7bb",
"from": "github:jitsi/lib-jitsi-meet#23503b0efd4b97989af7b10696ec9ae790aec7bb",
"version": "github:jitsi/lib-jitsi-meet#0ee30bf12a549d10bb5d559e19bd557c3ed179eb",
"from": "github:jitsi/lib-jitsi-meet#0ee30bf12a549d10bb5d559e19bd557c3ed179eb",
"requires": {
"@jitsi/sdp-interop": "0.1.14",
"@jitsi/sdp-simulcast": "0.2.1",

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#23503b0efd4b97989af7b10696ec9ae790aec7bb",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0ee30bf12a549d10bb5d559e19bd557c3ed179eb",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.11",
"moment": "2.19.4",

View File

@@ -372,6 +372,28 @@ export function createLiveStreamingDialogEvent(dialogName, buttonName) {
};
}
/**
* Creates an event with the local tracks duration.
*
* @param {Object} duration - The object with the duration of the local tracks.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createLocalTracksDurationEvent(duration) {
const { audio, video, conference } = duration;
const { camera, desktop } = video;
return {
action: 'local.tracks.durations',
attributes: {
audio: audio.value,
camera: camera.value,
conference: conference.value,
desktop: desktop.value
}
};
}
/**
* Creates an event which indicates that an action related to recording has
* occured.

View File

@@ -0,0 +1,11 @@
// @flow
/**
* The type of (redux) action which signals that local media duration has changed.
*
* {
* type: UPDATE_LOCAL_TRACKS_DURATION,
* localTracksDuration: Object
* }
*/
export const UPDATE_LOCAL_TRACKS_DURATION = 'UPDATE_LOCAL_TRACKS_DURATION';

View File

@@ -15,7 +15,7 @@ export default class AmplitudeHandler extends AbstractHandler {
constructor(options) {
super();
const { amplitudeAPPKey, host } = options;
const { amplitudeAPPKey, host, user } = options;
if (!amplitudeAPPKey) {
throw new Error('Failed to initialize Amplitude handler, no APP key');
@@ -28,6 +28,10 @@ export default class AmplitudeHandler extends AbstractHandler {
};
amplitude.getInstance(this._amplitudeOptions).init(amplitudeAPPKey);
if (user) {
amplitude.getInstance(this._amplitudeOptions).setUserId(user);
}
}
/**

View File

@@ -31,6 +31,16 @@ class Amplitude {
AmplitudeNative.init(this._instanceName, apiKey);
}
/**
* Sets an identifier for the current user.
*
* @param {string} userId - The new user id.
* @returns {void}
*/
setUserId(userId) {
AmplitudeNative.setUserId(this._instanceName, userId);
}
/**
* Sets user properties for the current user.
*

View File

@@ -2,3 +2,4 @@ export * from './AnalyticsEvents';
export * from './functions';
import './middleware';
import './reducer';

View File

@@ -1,8 +1,74 @@
import { SET_ROOM } from '../base/conference';
// @flow
import {
CONFERENCE_JOINED,
CONFERENCE_WILL_LEAVE,
SET_ROOM
} from '../base/conference';
import { SET_CONFIG } from '../base/config';
import { MiddlewareRegistry } from '../base/redux';
import {
getLocalAudioTrack,
getLocalVideoTrack,
TRACK_ADDED,
TRACK_REMOVED,
TRACK_UPDATED
} from '../base/tracks';
import { initAnalytics, resetAnalytics } from './functions';
import { UPDATE_LOCAL_TRACKS_DURATION } from './actionTypes';
import { createLocalTracksDurationEvent } from './AnalyticsEvents';
import { initAnalytics, resetAnalytics, sendAnalytics } from './functions';
/**
* Calculates the duration of the local tracks.
*
* @param {Object} state - The redux state.
* @returns {Object} - The local tracks duration.
*/
function calculateLocalTrackDuration(state) {
const now = Date.now();
const { localTracksDuration } = state['features/analytics'];
const { conference } = state['features/base/conference'];
const { audio, video } = localTracksDuration;
const { camera, desktop } = video;
const tracks = state['features/base/tracks'];
const audioTrack = getLocalAudioTrack(tracks);
const videoTrack = getLocalVideoTrack(tracks);
const newDuration = { ...localTracksDuration };
if (!audioTrack || audioTrack.muted || !conference) {
newDuration.audio = {
startedTime: -1,
value: audio.value + (audio.startedTime === -1 ? 0 : now - audio.startedTime)
};
} else if (audio.startedTime === -1) {
newDuration.audio.startedTime = now;
}
if (!videoTrack || videoTrack.muted || !conference) {
newDuration.video = {
camera: {
startedTime: -1,
value: camera.value + (camera.startedTime === -1 ? 0 : now - camera.startedTime)
},
desktop: {
startedTime: -1,
value: desktop.value + (desktop.startedTime === -1 ? 0 : now - desktop.startedTime)
}
};
} else {
const { videoType } = videoTrack;
if (video[videoType].startedTime === -1) {
newDuration.video[videoType].startedTime = now;
}
}
return {
...localTracksDuration,
...newDuration
};
}
/**
* Middleware which intercepts config actions to handle evaluating analytics
@@ -12,25 +78,81 @@ import { initAnalytics, resetAnalytics } from './functions';
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case SET_CONFIG: {
if (action.type === SET_CONFIG) {
if (navigator.product === 'ReactNative') {
// Reseting the analytics is currently not needed for web because
// the user will be redirected to another page and new instance of
// Analytics will be created and initialized.
resetAnalytics();
}
}
const result = next(action);
switch (action.type) {
case CONFERENCE_JOINED: {
const { dispatch, getState } = store;
const state = getState();
dispatch({
type: UPDATE_LOCAL_TRACKS_DURATION,
localTracksDuration: {
...calculateLocalTrackDuration(state),
conference: {
startedTime: Date.now(),
value: 0
}
}
});
break;
}
case CONFERENCE_WILL_LEAVE: {
const { dispatch, getState } = store;
const state = getState();
const { localTracksDuration } = state['features/analytics'];
const newLocalTracksDuration = {
...calculateLocalTrackDuration(state),
conference: {
startedTime: -1,
value: Date.now() - localTracksDuration.conference.startedTime
}
};
sendAnalytics(createLocalTracksDurationEvent(newLocalTracksDuration));
dispatch({
type: UPDATE_LOCAL_TRACKS_DURATION,
localTracksDuration: newLocalTracksDuration
});
break;
}
case SET_ROOM: {
const result = next(action);
initAnalytics(store);
break;
}
case TRACK_ADDED:
case TRACK_REMOVED:
case TRACK_UPDATED: {
const { dispatch, getState } = store;
const state = getState();
const { localTracksDuration } = state['features/analytics'];
return result;
if (localTracksDuration.conference.startedTime === -1) {
// We don't want to track the media duration if the conference is not joined yet because otherwise we won't
// be able to compare them with the conference duration (from conference join to conference will leave).
break;
}
dispatch({
type: UPDATE_LOCAL_TRACKS_DURATION,
localTracksDuration: {
...localTracksDuration,
...calculateLocalTrackDuration(state)
}
});
break;
}
}
return next(action);
return result;
});

View File

@@ -0,0 +1,51 @@
// @flow
import { ReducerRegistry } from '../base/redux';
import { UPDATE_LOCAL_TRACKS_DURATION } from './actionTypes';
/**
* Initial state.
*/
const DEFAULT_STATE = {
localTracksDuration: {
audio: {
startedTime: -1,
value: 0
},
video: {
camera: {
startedTime: -1,
value: 0
},
desktop: {
startedTime: -1,
value: 0
}
},
conference: {
startedTime: -1,
value: 0
}
}
};
/**
* Listen for actions which changes the state of the analytics feature.
*
* @param {Object} state - The Redux state of the feature features/analytics.
* @param {Object} action - Action object.
* @param {string} action.type - Type of action.
* @returns {Object}
*/
ReducerRegistry.register('features/analytics', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case UPDATE_LOCAL_TRACKS_DURATION:
return {
...state,
localTracksDuration: action.localTracksDuration
};
default:
return state;
}
});

View File

@@ -10,8 +10,9 @@ import {
setConfig,
storeConfig
} from '../base/config';
import { setLocationURL } from '../base/connection';
import { connect, disconnect, setLocationURL } from '../base/connection';
import { loadConfig } from '../base/lib-jitsi-meet';
import { createDesiredLocalTracks } from '../base/tracks';
import { parseURIString, toURLString } from '../base/util';
import { setFatalError } from '../overlay';
@@ -58,6 +59,12 @@ export function appNavigate(uri: ?string) {
const { contextRoot, host, room } = location;
const locationURL = new URL(location.toString());
// Disconnect from any current conference.
// FIXME: unify with web.
if (navigator.product === 'ReactNative') {
dispatch(disconnect());
}
dispatch(configWillLoad(locationURL, room));
let protocol = location.protocol.toLowerCase();
@@ -74,25 +81,40 @@ export function appNavigate(uri: ?string) {
let config;
try {
config = await loadConfig(url);
dispatch(storeConfig(baseURL, config));
} catch (error) {
// Avoid (re)loading the config when there is no room.
if (!room) {
config = restoreConfig(baseURL);
}
if (!config) {
dispatch(loadConfigError(error, locationURL));
if (!config) {
try {
config = await loadConfig(url);
dispatch(storeConfig(baseURL, config));
} catch (error) {
config = restoreConfig(baseURL);
return;
if (!config) {
dispatch(loadConfigError(error, locationURL));
return;
}
}
}
if (getState()['features/base/config'].locationURL === locationURL) {
dispatch(setLocationURL(locationURL));
dispatch(setConfig(config));
dispatch(setRoom(room));
} else {
if (getState()['features/base/config'].locationURL !== locationURL) {
dispatch(loadConfigError(new Error('Config no longer needed!'), locationURL));
return;
}
dispatch(setLocationURL(locationURL));
dispatch(setConfig(config));
dispatch(setRoom(room));
// FIXME: unify with web, currently the connection and track creation happens in conference.js.
if (room && navigator.product === 'ReactNative') {
dispatch(createDesiredLocalTracks());
dispatch(connect());
}
};
}

View File

@@ -6,6 +6,7 @@ import React from 'react';
import { DialogContainer } from '../../base/dialog';
import '../../base/responsive-ui';
import '../../chat';
import '../../external-api';
import '../../room-lock';
import '../../video-layout';

View File

@@ -64,12 +64,14 @@ class WaitForOwnerDialog extends Component<Props> {
return (
<ConfirmDialog
cancelKey = 'dialog.Cancel'
contentKey = {
{
key: 'dialog.WaitForHostMsgWOk',
params: { room }
}
}
okKey = 'dialog.Ok'
onCancel = { this._onCancel }
onSubmit = { this._onLogin } />
);

View File

@@ -84,6 +84,8 @@ function _addConferenceListeners(conference, dispatch) {
conference.on(
JitsiConferenceEvents.CONFERENCE_LEFT,
(...args) => dispatch(conferenceLeft(conference, ...args)));
conference.on(JitsiConferenceEvents.SUBJECT_CHANGED,
(...args) => dispatch(conferenceSubjectChanged(...args)));
conference.on(
JitsiConferenceEvents.KICKED,
@@ -757,10 +759,6 @@ export function setSubject(subject: string = '') {
const { conference } = getState()['features/base/conference'];
if (conference) {
dispatch({
type: SET_PENDING_SUBJECT_CHANGE,
subject: undefined
});
conference.setSubject(subject);
} else {
dispatch({

View File

@@ -1,5 +1,7 @@
// @flow
import _ from 'lodash';
import { JitsiTrackErrors } from '../lib-jitsi-meet';
import {
getLocalParticipant,
@@ -155,10 +157,10 @@ export function forEachConference(
export function getConferenceName(stateful: Function | Object): string {
const state = toState(stateful);
const { callee } = state['features/base/jwt'];
const { callDisplayName } = state['features/base/config'];
const { pendingSubjectChange, room, subject } = state['features/base/conference'];
return state['features/base/config'].callDisplayName
|| (callee && callee.name)
|| state['features/base/conference'].room;
return pendingSubjectChange || subject || callDisplayName || (callee && callee.name) || _.startCase(room);
}
/**

View File

@@ -24,7 +24,6 @@ import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
import {
conferenceFailed,
conferenceLeft,
conferenceWillLeave,
createConference,
setLastN,
@@ -38,6 +37,7 @@ import {
DATA_CHANNEL_OPENED,
SET_AUDIO_ONLY,
SET_LASTN,
SET_PENDING_SUBJECT_CHANGE,
SET_ROOM
} from './actionTypes';
import {
@@ -329,10 +329,17 @@ function _connectionFailed({ dispatch, getState }, next, action) {
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _conferenceSubjectChanged({ getState }, next, action) {
function _conferenceSubjectChanged({ dispatch, getState }, next, action) {
const result = next(action);
const { subject } = getState()['features/base/conference'];
if (subject) {
dispatch({
type: SET_PENDING_SUBJECT_CHANGE,
subject: undefined
});
}
typeof APP === 'object' && APP.API.notifySubjectChanged(subject);
return result;
@@ -567,48 +574,30 @@ function _setReceiverVideoConstraint(conference, preferred, max) {
}
/**
* Notifies the feature {@code base/conference} that the redix action
* {@link SET_ROOM} is being dispatched within a specific redux store.
* Notifies the feature base/conference that the action
* {@code SET_ROOM} is being dispatched within a specific
* redux store.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
* specified {@code action} to the specified {@code store}.
* @param {Action} action - The redux action {@code SET_ROOM} which is being
* dispatched in the specified {@code store}.
* @param {Action} action - The redux action {@code SET_ROOM}
* which is being dispatched in the specified {@code store}.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _setRoom({ dispatch, getState }, next, action) {
const result = next(action);
// By the time SET_ROOM is dispatched, base/connection's locationURL and
// base/conference's leaving should be the only conference-related sources
// of truth.
const state = getState();
const { leaving } = state['features/base/conference'];
const { locationURL } = state['features/base/connection'];
const dispatchConferenceLeft = new Set();
const { subject } = state['features/base/config'];
const { room } = action;
// Figure out which of the JitsiConferences referenced by base/conference
// have not dispatched or are not likely to dispatch CONFERENCE_FAILED and
// CONFERENCE_LEFT.
forEachConference(state, (conference, url) => {
if (conference !== leaving && url && url !== locationURL) {
dispatchConferenceLeft.add(conference);
}
return true; // All JitsiConference instances are to be examined.
});
// Dispatch CONFERENCE_LEFT for the JitsiConferences referenced by
// base/conference which have not dispatched or are not likely to dispatch
// CONFERENCE_FAILED or CONFERENCE_LEFT.
for (const conference of dispatchConferenceLeft) {
dispatch(conferenceLeft(conference));
if (room) {
// Set the stored subject.
dispatch(setSubject(subject));
}
return result;
return next(action);
}
/**

View File

@@ -21,6 +21,7 @@ const WHITELISTED_KEYS = [
'_peerConnStatusOutOfLastNTimeout',
'_peerConnStatusRtcMuteTimeout',
'abTesting',
'analytics.disabled',
'autoRecord',
'autoRecordToken',
'avgRtpStatsN',
@@ -101,7 +102,7 @@ const WHITELISTED_KEYS = [
'enableDisplayNameInStats',
'enableLayerSuspension',
'enableLipSync',
'enableLocalVideoFlip',
'disableLocalVideoFlip',
'enableRemb',
'enableStatsID',
'enableTalkWhileMuted',
@@ -131,10 +132,12 @@ const WHITELISTED_KEYS = [
'startAudioMuted',
'startAudioOnly',
'startBitrate',
'startSilent',
'startScreenSharing',
'startVideoMuted',
'startWithAudioMuted',
'startWithVideoMuted',
'subject',
'testing',
'useIPv6',
'useNicks',

View File

@@ -370,10 +370,7 @@ export function disconnect() {
if (connection_) {
promise = promise.then(() => connection_.disconnect());
} else {
// FIXME: We have no connection! Fake a disconnect. Because of how the current disconnec is implemented
// (by doing the diconnect() in the Conference component unmount) we have lost the location URL already.
// Oh well, at least send the event.
promise.then(() => dispatch(_connectionDisconnected({}, '')));
logger.info('No connection found while disconnecting.');
}
return promise;

View File

@@ -1,3 +1,25 @@
/**
* The type of Redux action which signals that an error occurred while obtaining
* a camera.
*
* {
* type: NOTIFY_CAMERA_ERROR,
* error: Object
* }
*/
export const NOTIFY_CAMERA_ERROR = 'NOTIFY_CAMERA_ERROR';
/**
* The type of Redux action which signals that an error occurred while obtaining
* a microphone.
*
* {
* type: NOTIFY_MIC_ERROR,
* error: Object
* }
*/
export const NOTIFY_MIC_ERROR = 'NOTIFY_MIC_ERROR';
/**
* The type of Redux action which signals that the currently used audio
* input device should be changed.

View File

@@ -7,6 +7,8 @@ import {
import {
ADD_PENDING_DEVICE_REQUEST,
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
NOTIFY_CAMERA_ERROR,
NOTIFY_MIC_ERROR,
REMOVE_PENDING_DEVICE_REQUESTS,
SET_AUDIO_INPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE,
@@ -148,6 +150,43 @@ export function getAvailableDevices() {
});
}
/**
* Signals that an error occurred while trying to obtain a track from a camera.
*
* @param {Object} error - The device error, as provided by lib-jitsi-meet.
* @param {string} error.name - The constant for the type of the error.
* @param {string} error.message - Optional additional information about the
* error.
* @returns {{
* type: NOTIFY_CAMERA_ERROR,
* error: Object
* }}
*/
export function notifyCameraError(error) {
return {
type: NOTIFY_CAMERA_ERROR,
error
};
}
/**
* Signals that an error occurred while trying to obtain a track from a mic.
*
* @param {Object} error - The device error, as provided by lib-jitsi-meet.
* @param {Object} error.name - The constant for the type of the error.
* @param {string} error.message - Optional additional information about the
* error.
* @returns {{
* type: NOTIFY_MIC_ERROR,
* error: Object
* }}
*/
export function notifyMicError(error) {
return {
type: NOTIFY_MIC_ERROR,
error
};
}
/**
* Remove all pending device requests.

View File

@@ -4,6 +4,7 @@ import { CONFERENCE_JOINED } from '../conference';
import { processExternalDeviceRequest } from '../../device-selection';
import { MiddlewareRegistry } from '../redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { JitsiTrackErrors } from '../lib-jitsi-meet';
import {
removePendingDeviceRequests,
@@ -12,15 +13,35 @@ import {
} from './actions';
import {
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
NOTIFY_CAMERA_ERROR,
NOTIFY_MIC_ERROR,
SET_AUDIO_INPUT_DEVICE,
SET_VIDEO_INPUT_DEVICE
} from './actionTypes';
import { showNotification } from '../../notifications';
import { showNotification, showWarningNotification } from '../../notifications';
import { updateSettings } from '../settings';
import { setAudioOutputDeviceId } from './functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
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'
}
};
/**
* Implements the middleware of the feature base/devices.
*
@@ -32,6 +53,53 @@ MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
case NOTIFY_CAMERA_ERROR: {
if (typeof APP !== 'object' || !action.error) {
break;
}
const { message, name } = action.error;
const cameraJitsiTrackErrorMsg
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name];
const cameraErrorMsg = cameraJitsiTrackErrorMsg
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
.camera[JitsiTrackErrors.GENERAL];
const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message;
store.dispatch(showWarningNotification({
description: additionalCameraErrorMsg,
descriptionKey: cameraErrorMsg,
titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
? 'deviceError.cameraPermission' : 'deviceError.cameraError'
}));
break;
}
case NOTIFY_MIC_ERROR: {
if (typeof APP !== 'object' || !action.error) {
break;
}
const { message, name } = action.error;
const micJitsiTrackErrorMsg
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name];
const micErrorMsg = micJitsiTrackErrorMsg
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
.microphone[JitsiTrackErrors.GENERAL];
const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message;
store.dispatch(showWarningNotification({
description: additionalMicErrorMsg,
descriptionKey: micErrorMsg,
titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
? 'deviceError.microphonePermission'
: 'deviceError.microphoneError'
}));
break;
}
case SET_AUDIO_INPUT_DEVICE:
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
break;

View File

@@ -1,7 +1,7 @@
// @flow
import React, { PureComponent, type Node } from 'react';
import { SafeAreaView, View } from 'react-native';
import { Platform, SafeAreaView, ScrollView, View } from 'react-native';
import { ColorSchemeRegistry } from '../../../color-scheme';
import { SlidingView } from '../../../react';
@@ -36,21 +36,6 @@ type Props = {
* A component emulating Android's BottomSheet.
*/
class BottomSheet extends PureComponent<Props> {
/**
* Assembles a style for the BottomSheet container.
*
* @private
* @returns {StyleType}
*/
_getContainerStyle() {
const { _styles } = this.props;
return {
...styles.container,
backgroundColor: _styles.sheet.backgroundColor
};
}
/**
* Implements React's {@link Component#render()}.
*
@@ -65,15 +50,46 @@ class BottomSheet extends PureComponent<Props> {
onHide = { this.props.onCancel }
position = 'bottom'
show = { true }>
<SafeAreaView
style = { this._getContainerStyle() }>
<View style = { _styles.sheet }>
{ this.props.children }
<View
pointerEvents = 'box-none'
style = { styles.sheetContainer }>
<View
pointerEvents = 'box-none'
style = { styles.sheetAreaCover } />
<View
style = { [
styles.sheetItemContainer,
_styles.sheet
] }>
<ScrollView bounces = { false }>
{ this._getWrappedContent() }
</ScrollView>
</View>
</SafeAreaView>
</View>
</SlidingView>
);
}
/**
* Wraps the content when needed (iOS 11 and above), or just returns the original children.
*
* @returns {React$Element}
*/
_getWrappedContent() {
if (Platform.OS === 'ios') {
const majorVersionIOS = parseInt(Platform.Version, 10);
if (majorVersionIOS > 10) {
return (
<SafeAreaView>
{ this.props.children }
</SafeAreaView>
);
}
}
return this.props.children;
}
}
/**

View File

@@ -27,17 +27,28 @@ export const PLACEHOLDER_COLOR = ColorPalette.lightGrey;
* been implemented as per the Material Design guidelines:
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
*/
export const bottomSheetStyles = createStyleSheet({
export const bottomSheetStyles = {
sheetAreaCover: {
backgroundColor: ColorPalette.transparent,
flex: 1
},
/**
* Style for the container of the sheet.
*/
container: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0
sheetContainer: {
alignItems: 'stretch',
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-end'
},
sheetItemContainer: {
flex: -1,
maxHeight: '60%',
paddingHorizontal: MD_ITEM_MARGIN_PADDING
}
});
};
export const brandedDialog = createStyleSheet({
@@ -137,10 +148,7 @@ ColorSchemeRegistry.register('BottomSheet', {
* Bottom sheet's base style.
*/
sheet: {
backgroundColor: schemeColor('background'),
flex: 1,
paddingHorizontal: MD_ITEM_MARGIN_PADDING,
paddingVertical: 8
backgroundColor: schemeColor('background')
},
/**

View File

@@ -43,7 +43,9 @@ export function createLocalTrack(type: string, deviceId: string) {
* otherwise.
*/
export function isAnalyticsEnabled(stateful: Function | Object) {
return !toState(stateful)['features/base/config'].disableThirdPartyRequests;
const { disableThirdPartyRequests, analytics = {} } = toState(stateful)['features/base/config'];
return !disableThirdPartyRequests && !analytics.disabled;
}
/**

View File

@@ -260,6 +260,25 @@ function _getAllParticipants(stateful) {
: toState(stateful)['features/base/participants'] || []);
}
/**
* Returns true if all of the meeting participants are moderators.
*
* @param {Object|Function} stateful -Object or function that can be resolved
* to the Redux state.
* @returns {boolean}
*/
export function isEveryoneModerator(stateful: Object | Function) {
const participants = _getAllParticipants(stateful);
for (const participant of participants) {
if (participant.role !== PARTICIPANT_ROLE.MODERATOR) {
return false;
}
}
return true;
}
/**
* Returns true if the current local participant is a moderator in the
* conference.

View File

@@ -119,12 +119,22 @@ function _initSettings(featureState) {
let settings = featureState;
// Old Settings.js values
// FIXME: Let's remove this after a predefined time (e.g. by July 2018) to
// avoid garbage in the source.
const displayName = _.escape(window.localStorage.getItem('displayname'));
const email = _.escape(window.localStorage.getItem('email'));
// FIXME: jibri uses old settings.js local storage values to set its display
// name and email. Provide another way for jibri to set these values, update
// jibri, and remove the old settings.js values.
const savedDisplayName = window.localStorage.getItem('displayname');
const savedEmail = window.localStorage.getItem('email');
let avatarID = _.escape(window.localStorage.getItem('avatarId'));
// The helper _.escape will convert null to an empty strings. The empty
// string will be saved in settings. On app re-load, because an empty string
// is a defined value, it will override any value found in local storage.
// The workaround is sidestepping _.escape when the value is not set in
// local storage.
const displayName
= savedDisplayName === null ? undefined : _.escape(savedDisplayName);
const email = savedEmail === null ? undefined : _.escape(savedEmail);
if (!avatarID) {
// if there is no avatar id, we generate a unique one and use it forever
avatarID = randomHexString(32);

View File

@@ -15,7 +15,6 @@ import UIEvents from '../../../../service/UI/UIEvents';
import { createLocalTracksA } from './actions';
import {
TOGGLE_SCREENSHARING,
TRACK_REMOVED,
TRACK_UPDATED
} from './actionTypes';
import { getLocalTrack, setTrackMuted } from './functions';
@@ -92,14 +91,6 @@ MiddlewareRegistry.register(store => next => action => {
}
break;
case TRACK_REMOVED:
// TODO Remove this middleware case once all UI interested in tracks
// being removed are converted to react and listening for store changes.
if (typeof APP !== 'undefined' && !action.track.local) {
APP.UI.removeRemoteStream(action.track.jitsiTrack);
}
break;
case TRACK_UPDATED:
// TODO Remove the following calls to APP.UI once components interested
// in track mute changes are moved into React and/or redux.

View File

@@ -1,20 +1,18 @@
// @flow
import React from 'react';
import { BackHandler, SafeAreaView, StatusBar, View } from 'react-native';
import { BackHandler, NativeModules, SafeAreaView, StatusBar, View } from 'react-native';
import { appNavigate } from '../../../app';
import { connect, disconnect } from '../../../base/connection';
import { getAppProp } from '../../../base/app';
import { getParticipantCount } from '../../../base/participants';
import { Container, LoadingIndicator, TintedView } from '../../../base/react';
import { connect as reactReduxConnect } from '../../../base/redux';
import { connect } from '../../../base/redux';
import {
isNarrowAspectRatio,
makeAspectRatioAware
} from '../../../base/responsive-ui';
import { TestConnectionInfo } from '../../../base/testing';
import { createDesiredLocalTracks } from '../../../base/tracks';
import { ConferenceNotification } from '../../../calendar-sync';
import { Chat } from '../../../chat';
import { DisplayNameLabel } from '../../../display-name';
@@ -66,40 +64,6 @@ type Props = AbstractProps & {
*/
_largeVideoParticipantId: string,
/**
* Current conference's full URL.
*
* @private
*/
_locationURL: URL,
/**
* The handler which dispatches the (redux) action connect.
*
* @private
* @returns {void}
*/
_onConnect: Function,
/**
* The handler which dispatches the (redux) action disconnect.
*
* @private
* @returns {void}
*/
_onDisconnect: Function,
/**
* Handles a hardware button press for back navigation. Leaves the
* associated {@code Conference}.
*
* @private
* @returns {boolean} As the associated conference is unconditionally left
* and exiting the app while it renders a {@code Conference} is undesired,
* {@code true} is always returned.
*/
_onHardwareBackPress: Function,
/**
* The number of participants in the conference.
*
@@ -107,6 +71,13 @@ type Props = AbstractProps & {
*/
_participantCount: number,
/**
* Whether Picture-in-Picture is enabled.
*
* @private
*/
_pictureInPictureEnabled: boolean,
/**
* The indicator which determines whether the UI is reduced (to accommodate
* smaller display areas).
@@ -138,7 +109,12 @@ type Props = AbstractProps & {
*
* @private
*/
_toolboxAlwaysVisible: boolean
_toolboxAlwaysVisible: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
/**
@@ -156,6 +132,8 @@ class Conference extends AbstractConference<Props, *> {
// Bind event handlers so they are only bound once per instance.
this._onClick = this._onClick.bind(this);
this._onHardwareBackPress = this._onHardwareBackPress.bind(this);
this._setToolboxVisible = this._setToolboxVisible.bind(this);
}
/**
@@ -166,17 +144,11 @@ class Conference extends AbstractConference<Props, *> {
* @returns {void}
*/
componentDidMount() {
this.props._onConnect();
BackHandler.addEventListener(
'hardwareBackPress',
this.props._onHardwareBackPress);
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
// Show the toolbox if we are the only participant; otherwise, the whole
// UI looks too unpopulated the LargeVideo visible.
const { _participantCount, _setToolboxVisible } = this.props;
_participantCount === 1 && _setToolboxVisible(true);
this.props._participantCount === 1 && this._setToolboxVisible(true);
}
/**
@@ -184,34 +156,23 @@ class Conference extends AbstractConference<Props, *> {
*
* @inheritdoc
*/
componentDidUpdate(pevProps: Props) {
componentDidUpdate(prevProps: Props) {
const {
_locationURL: oldLocationURL,
_participantCount: oldParticipantCount,
_room: oldRoom
} = pevProps;
_participantCount: oldParticipantCount
} = prevProps;
const {
_locationURL: newLocationURL,
_participantCount: newParticipantCount,
_room: newRoom,
_setToolboxVisible,
_toolboxVisible
} = this.props;
// If the location URL changes we need to reconnect.
oldLocationURL !== newLocationURL && newRoom && this.props._onDisconnect();
// Start the connection process when there is a (valid) room.
oldRoom !== newRoom && newRoom && this.props._onConnect();
if (oldParticipantCount === 1
&& newParticipantCount > 1
&& _toolboxVisible) {
_setToolboxVisible(false);
this._setToolboxVisible(false);
} else if (oldParticipantCount > 1
&& newParticipantCount === 1
&& !_toolboxVisible) {
_setToolboxVisible(true);
this._setToolboxVisible(true);
}
}
@@ -225,11 +186,7 @@ class Conference extends AbstractConference<Props, *> {
*/
componentWillUnmount() {
// Tear handling any hardware button presses for back navigation down.
BackHandler.removeEventListener(
'hardwareBackPress',
this.props._onHardwareBackPress);
this.props._onDisconnect();
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
}
/**
@@ -338,9 +295,33 @@ class Conference extends AbstractConference<Props, *> {
return;
}
const toolboxVisible = !this.props._toolboxVisible;
this._setToolboxVisible(!this.props._toolboxVisible);
}
this.props._setToolboxVisible(toolboxVisible);
_onHardwareBackPress: () => boolean;
/**
* Handles a hardware button press for back navigation. Enters Picture-in-Picture mode
* (if supported) or leaves the associated {@code Conference} otherwise.
*
* @returns {boolean} Exiting the app is undesired, so {@code true} is always returned.
*/
_onHardwareBackPress() {
let p;
if (this.props._pictureInPictureEnabled) {
const { PictureInPicture } = NativeModules;
p = PictureInPicture.enterPictureInPicture();
} else {
p = Promise.reject(new Error('PiP not enabled'));
}
p.catch(() => {
this.props.dispatch(appNavigate(undefined));
});
return true;
}
/**
@@ -388,70 +369,20 @@ class Conference extends AbstractConference<Props, *> {
}
);
}
}
/**
* Maps dispatching of some action to React component props.
*
* @param {Function} dispatch - Redux action dispatcher.
* @private
* @returns {{
* _onConnect: Function,
* _onDisconnect: Function,
* _onHardwareBackPress: Function,
* _setToolboxVisible: Function
* }}
*/
function _mapDispatchToProps(dispatch) {
return {
/**
* Dispatches actions to create the desired local tracks and for
* connecting to the conference.
*
* @private
* @returns {void}
*/
_onConnect() {
dispatch(createDesiredLocalTracks());
dispatch(connect());
},
_setToolboxVisible: (boolean) => void;
/**
* Dispatches an action disconnecting from the conference.
*
* @private
* @returns {void}
*/
_onDisconnect() {
dispatch(disconnect());
},
/**
* Handles a hardware button press for back navigation. Leaves the
* associated {@code Conference}.
*
* @returns {boolean} As the associated conference is unconditionally
* left and exiting the app while it renders a {@code Conference} is
* undesired, {@code true} is always returned.
*/
_onHardwareBackPress() {
dispatch(appNavigate(undefined));
return true;
},
/**
* Dispatches an action changing the visibility of the {@link Toolbox}.
*
* @private
* @param {boolean} visible - Pass {@code true} to show the
* {@code Toolbox} or {@code false} to hide it.
* @returns {void}
*/
_setToolboxVisible(visible) {
dispatch(setToolboxVisible(visible));
}
};
/**
* Dispatches an action changing the visibility of the {@link Toolbox}.
*
* @private
* @param {boolean} visible - Pass {@code true} to show the
* {@code Toolbox} or {@code false} to hide it.
* @returns {void}
*/
_setToolboxVisible(visible) {
this.props.dispatch(setToolboxVisible(visible));
}
}
/**
@@ -462,8 +393,7 @@ function _mapDispatchToProps(dispatch) {
* @returns {Props}
*/
function _mapStateToProps(state) {
const { connecting, connection, locationURL }
= state['features/base/connection'];
const { connecting, connection } = state['features/base/connection'];
const {
conference,
joining,
@@ -508,14 +438,6 @@ function _mapStateToProps(state) {
*/
_largeVideoParticipantId: state['features/large-video'].participantId,
/**
* Current conference's full URL.
*
* @private
* @type {URL}
*/
_locationURL: locationURL,
/**
* The number of participants in the conference.
*
@@ -524,6 +446,14 @@ function _mapStateToProps(state) {
*/
_participantCount: getParticipantCount(state),
/**
* Whether Picture-in-Picture is enabled.
*
* @private
* @type {boolean}
*/
_pictureInPictureEnabled: getAppProp(state, 'pictureInPictureEnabled'),
/**
* The indicator which determines whether the UI is reduced (to
* accommodate smaller display areas).
@@ -551,5 +481,4 @@ function _mapStateToProps(state) {
};
}
export default reactReduxConnect(_mapStateToProps, _mapDispatchToProps)(
makeAspectRatioAware(Conference));
export default connect(_mapStateToProps)(makeAspectRatioAware(Conference));

View File

@@ -1,6 +1,5 @@
// @flow
import _ from 'lodash';
import React, { Component } from 'react';
import { SafeAreaView, Text, View } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
@@ -82,7 +81,7 @@ class NavigationBar extends Component<Props> {
*/
function _mapStateToProps(state) {
return {
_meetingName: _.startCase(getConferenceName(state)),
_meetingName: getConferenceName(state),
_visible: isToolboxVisible(state)
};
}

View File

@@ -50,7 +50,6 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
super(props);
// Bind event handlers so they are only bound once per instance.
this._openDesktopApp = this._openDesktopApp.bind(this);
this._onLaunchWeb = this._onLaunchWeb.bind(this);
this._onTryAgain = this._onTryAgain.bind(this);
}
@@ -61,7 +60,6 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
* @inheritdoc
*/
componentDidMount() {
this._openDesktopApp();
sendAnalytics(
createDeepLinkingPageEvent(
'displayed', 'DeepLinkingDesktop', { isMobileBrowser: false }));
@@ -133,17 +131,6 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
);
}
_openDesktopApp: () => {}
/**
* Dispatches the <tt>openDesktopApp</tt> action.
*
* @returns {void}
*/
_openDesktopApp() {
this.props.dispatch(openDesktopApp());
}
_onTryAgain: () => {}
/**
@@ -155,7 +142,7 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
sendAnalytics(
createDeepLinkingPageEvent(
'clicked', 'tryAgainButton', { isMobileBrowser: false }));
this._openDesktopApp();
this.props.dispatch(openDesktopApp());
}
_onLaunchWeb: () => {}

View File

@@ -8,28 +8,7 @@ import {
DeepLinkingMobilePage,
NoMobileApp
} from './components';
import { _shouldShowDeepLinkingDesktopPage }
from './shouldShowDeepLinkingDesktopPage';
/**
* Promise that resolves when the window load event is received.
*
* @type {Promise<void>}
*/
const windowLoadedPromise = new Promise(resolve => {
/**
* Handler for the window load event.
*
* @returns {void}
*/
function onWindowLoad() {
resolve();
window.removeEventListener('load', onWindowLoad);
}
window.addEventListener('load', onWindowLoad);
});
import { _openDesktopApp } from './openDesktopApp';
/**
* Generates a deep linking URL based on the current window URL.
@@ -96,23 +75,17 @@ export function getDeepLinkingPage(state) {
return Promise.resolve();
}
return _shouldShowDeepLinkingDesktopPage().then(
return _openDesktopApp().then(
// eslint-disable-next-line no-confusing-arrow
show => show ? DeepLinkingDesktopPage : undefined);
result => result ? DeepLinkingDesktopPage : undefined);
}
/**
* Opens the desktop app.
*
* @returns {void}
* @returns {Promise<boolean>} - Resolves with true if the attempt to open the desktop app was successful and resolves
* with false otherwise.
*/
export function openDesktopApp() {
windowLoadedPromise.then(() => {
// If the code for opening the deep link is executed before the window
// load event, something with the internal chrome state goes wrong. The
// result is that no window load event is received which is the cause
// for some permission prompts to not be displayed. In our case the GUM
// prompt wasn't displayed which causes the GUM call to never finish.
window.location.href = generateDeepLinkingURL();
});
return _openDesktopApp();
}

View File

@@ -0,0 +1,9 @@
/**
* Opens the desktop app.
*
* @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 Promise.resolve(false);
}

View File

@@ -1,9 +0,0 @@
/**
* Resolves with <tt>true</tt> if the deep linking page should be shown and with
* <tt>false</tt> otherwise.
*
* @returns {Promise<boolean>}
*/
export function _shouldShowDeepLinkingDesktopPage() {
return Promise.resolve(false);
}

View File

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

View File

@@ -0,0 +1,30 @@
// @flow
import { NOTIFY_CAMERA_ERROR, NOTIFY_MIC_ERROR } from '../base/devices';
import { MiddlewareRegistry } from '../base/redux';
declare var APP: Object;
/**
* The middleware of the feature {@code external-api}.
*
* @returns {Function}
*/
MiddlewareRegistry.register((/* store */) => next => action => {
switch (action.type) {
case NOTIFY_CAMERA_ERROR:
if (action.error) {
APP.API.notifyOnCameraError(
action.error.name, action.error.message);
}
break;
case NOTIFY_MIC_ERROR:
if (action.error) {
APP.API.notifyOnMicError(action.error.name, action.error.message);
}
break;
}
return next(action);
});

View File

@@ -10,6 +10,7 @@ import { Audio, MEDIA_TYPE } from '../../../base/media';
import {
PARTICIPANT_ROLE,
ParticipantView,
isEveryoneModerator,
isLocalParticipantModerator,
pinParticipant
} from '../../../base/participants';
@@ -38,6 +39,11 @@ type Props = {
*/
_audioTrack: Object,
/**
* True if everone in the meeting is moderator.
*/
_isEveryoneModerator: boolean,
/**
* True if the local participant is a moderator.
*/
@@ -117,6 +123,7 @@ class Thumbnail extends Component<Props> {
render() {
const {
_audioTrack: audioTrack,
_isEveryoneModerator,
_isModerator,
_largeVideo: largeVideo,
_onClick,
@@ -172,7 +179,7 @@ class Thumbnail extends Component<Props> {
{ renderDisplayName && <DisplayNameLabel participantId = { participantId } /> }
{ participant.role === PARTICIPANT_ROLE.MODERATOR
{ !_isEveryoneModerator && participant.role === PARTICIPANT_ROLE.MODERATOR
&& <View style = { styles.moderatorIndicatorContainer }>
<ModeratorIndicator />
</View> }
@@ -275,6 +282,7 @@ function _mapStateToProps(state, ownProps) {
return {
_audioTrack: audioTrack,
_isEveryoneModerator: isEveryoneModerator(state),
_isModerator: isLocalParticipantModerator(state),
_largeVideo: largeVideo,
_styles: ColorSchemeRegistry.get(state, 'Thumbnail'),

View File

@@ -55,6 +55,7 @@ export default {
backgroundColor: 'rgb(240, 243, 247)',
borderBottomRightRadius: 10,
borderTopRightRadius: 10,
color: DARK_GREY,
flex: 1,
fontSize: 17,
paddingVertical: 7

View File

@@ -2,7 +2,10 @@
import { toState } from '../base/redux';
import { parseStandardURIString } from '../base/util';
import { i18next, DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n';
import { getLocalParticipant, PARTICIPANT_ROLE } from '../base/participants';
import {
getLocalParticipant,
isLocalParticipantModerator
} from '../base/participants';
declare var interfaceConfig: Object;
@@ -83,14 +86,12 @@ export function getMoreTabProps(stateful: Object | Function) {
} = state['features/base/conference'];
const followMeActive = Boolean(state['features/follow-me'].moderator);
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
const localParticipant = getLocalParticipant(state);
// The settings sections to display.
const showModeratorSettings = Boolean(
conference
&& configuredTabs.includes('moderator')
&& localParticipant.role === PARTICIPANT_ROLE.MODERATOR);
&& isLocalParticipantModerator(state));
return {
currentLanguage: language,

View File

@@ -1,5 +1,6 @@
// @flow
import _ from 'lodash';
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { appNavigate } from '../../app';
@@ -26,10 +27,33 @@ type Props = AbstractButtonProps & {
* @extends AbstractHangupButton
*/
class HangupButton extends AbstractHangupButton<Props, *> {
_hangup: Function;
accessibilityLabel = 'toolbar.accessibilityLabel.hangup';
label = 'toolbar.hangup';
tooltip = 'toolbar.hangup';
/**
* Initializes a new HangupButton instance.
*
* @param {Props} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this._hangup = _.once(() => {
sendAnalytics(createToolbarEvent('hangup'));
// FIXME: these should be unified.
if (navigator.product === 'ReactNative') {
this.props.dispatch(appNavigate(undefined));
} else {
this.props.dispatch(disconnect(true));
}
});
}
/**
* Helper function to perform the actual hangup action.
*
@@ -38,14 +62,7 @@ class HangupButton extends AbstractHangupButton<Props, *> {
* @returns {void}
*/
_doHangup() {
sendAnalytics(createToolbarEvent('hangup'));
// FIXME: these should be unified.
if (navigator.product === 'ReactNative') {
this.props.dispatch(appNavigate(undefined));
} else {
this.props.dispatch(disconnect(true));
}
this._hangup();
}
}

View File

@@ -12,7 +12,7 @@ import {
getParticipantById
} from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import { TRACK_ADDED } from '../base/tracks';
import { TRACK_ADDED, TRACK_REMOVED } from '../base/tracks';
import { SET_FILMSTRIP_VISIBLE } from '../filmstrip';
import './middleware.any';
@@ -83,6 +83,12 @@ MiddlewareRegistry.register(store => next => action => {
VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
}
break;
case TRACK_REMOVED:
if (!action.track.local) {
VideoLayout.onRemoteStreamRemoved(action.track.jitsiTrack);
}
break;
}

View File

@@ -16,6 +16,8 @@ import { AppRegistry } from 'react-native';
import { App } from './features/app';
import { IncomingCallApp } from './features/mobile/incoming-call';
declare var __DEV__;
/**
* The type of the React {@code Component} props of {@link Root}.
*/
@@ -49,6 +51,26 @@ class Root extends PureComponent<Props> {
}
}
// HORRIBLE HACK ALERT! React Native logs the initial props with `console.log`. Here we are quickly patching it
// to avoid logging potentially sensitive information.
if (!__DEV__) {
/* eslint-disable */
const __orig_console_log = console.log;
const __orig_appregistry_runapplication = AppRegistry.runApplication;
AppRegistry.runApplication = (...args) => {
// $FlowExpectedError
console.log = () => {};
__orig_appregistry_runapplication(...args);
// $FlowExpectedError
console.log = __orig_console_log;
};
/* eslint-enable */
}
// Register the main/root Component of JitsiMeetView.
AppRegistry.registerComponent('App', () => Root);

View File

@@ -100,6 +100,10 @@ function Util.new(module)
return self
end
function Util:set_asap_key_server(asapKeyServer)
self.asapKeyServer = asapKeyServer
end
--- Returns the public key by keyID
-- @param keyId the key ID to request
-- @return the public key (the content of requested resource) or nil
@@ -239,7 +243,6 @@ end
-- @param session the current session
-- @return false and error
function Util:process_and_verify_token(session)
if session.auth_token == nil then
if self.allowEmptyToken then
return true;

View File

@@ -39,6 +39,25 @@ local function room_jid_match_rewrite(room_jid)
return room_jid
end
-- Utility function to check and convert a room JID from real [foo]room1@muc.example.com to virtual room1@muc.foo.example.com
local function room_jid_match_rewrite_from_internal(room_jid)
local node, host, resource = jid.split(room_jid);
if host ~= muc_domain or not node then
module:log("debug", "No need to rewrite %s (not from the MUC host)", room_jid);
return room_jid;
end
local target_subdomain, target_node = node:match("^%[([^%]]+)%](.+)$");
if not (target_node and target_subdomain) then
module:log("debug", "Not rewriting... unexpected node format: %s", node);
return room_jid;
end
-- Ok, rewrite room_jid address to pretty format
local new_node, new_host, new_resource = target_node, muc_domain_prefix..".".. target_subdomain.."."..muc_domain_base, resource;
room_jid = jid.join(new_node, new_host, new_resource);
module:log("debug", "Rewrote to %s", room_jid);
return room_jid
end
--- Finds and returns room by its jid
-- @param room_jid the room jid to search in the muc component
@@ -194,5 +213,6 @@ return {
wrap_async_run = wrap_async_run;
async_handler_wrapper = async_handler_wrapper;
room_jid_match_rewrite = room_jid_match_rewrite;
room_jid_match_rewrite_from_internal = room_jid_match_rewrite_from_internal
update_presence_identity = update_presence_identity;
};