Compare commits
139 Commits
1885
...
analytics_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20b597abda | ||
|
|
e68be6fa84 | ||
|
|
44d75b5253 | ||
|
|
440865f3ca | ||
|
|
e91c62142d | ||
|
|
1a892a689e | ||
|
|
04851b4baa | ||
|
|
beec60be47 | ||
|
|
36d40cdec9 | ||
|
|
23fea490aa | ||
|
|
1d60300016 | ||
|
|
5be3504fad | ||
|
|
bc9ef4421a | ||
|
|
4ffe668dd2 | ||
|
|
6536f82559 | ||
|
|
4464a11314 | ||
|
|
2855ea1500 | ||
|
|
258dc594dd | ||
|
|
a1476c68f1 | ||
|
|
3cc4d44376 | ||
|
|
bf163d221c | ||
|
|
7900b9c294 | ||
|
|
6a17d50423 | ||
|
|
9e7f8d0e16 | ||
|
|
3a99ef512e | ||
|
|
a14886031f | ||
|
|
ec881e0fd0 | ||
|
|
80989147ad | ||
|
|
3c31a60b32 | ||
|
|
db59b45076 | ||
|
|
0f0ff6788c | ||
|
|
47c07c2e76 | ||
|
|
896dcde2b2 | ||
|
|
a88409bbfa | ||
|
|
b8189a31ad | ||
|
|
e90d09a6d9 | ||
|
|
9fb49cb59b | ||
|
|
77ab05823d | ||
|
|
28ff188f96 | ||
|
|
bac191f96c | ||
|
|
e1a9487896 | ||
|
|
9e728e4b25 | ||
|
|
06d2c9fb7b | ||
|
|
63c862d925 | ||
|
|
a96a70869d | ||
|
|
ede5be119f | ||
|
|
b7c57d306a | ||
|
|
816eef1702 | ||
|
|
92eeba5392 | ||
|
|
2f3706bd37 | ||
|
|
e6f6884c36 | ||
|
|
ab5c2e9ded | ||
|
|
4f72225372 | ||
|
|
3af0976a43 | ||
|
|
96b1f0ca74 | ||
|
|
32ea2161eb | ||
|
|
c8ab1b9892 | ||
|
|
61e637a639 | ||
|
|
7d94d3fd1a | ||
|
|
88a58a057e | ||
|
|
4bb51516bb | ||
|
|
0805b9e99e | ||
|
|
82b27b45fe | ||
|
|
166fb1d13f | ||
|
|
ef9f145cb5 | ||
|
|
929bc8b8b9 | ||
|
|
d24d5d95dd | ||
|
|
9ba3a1c4ff | ||
|
|
1bcdbd1d96 | ||
|
|
8ada06cfe3 | ||
|
|
bf0be99366 | ||
|
|
653f1dae4c | ||
|
|
d91340166d | ||
|
|
d694e8df86 | ||
|
|
75a486ff96 | ||
|
|
48626ee71b | ||
|
|
b297aa3f3a | ||
|
|
dfc94ff144 | ||
|
|
1ffa7be4e1 | ||
|
|
d7cccacc12 | ||
|
|
96bde3ff44 | ||
|
|
b49c1c6ba2 | ||
|
|
e9dc9c47a9 | ||
|
|
3e055c1201 | ||
|
|
54388b6a0a | ||
|
|
0dff35c0db | ||
|
|
6c676f8d5f | ||
|
|
4e95dbf0e5 | ||
|
|
00b4176bf8 | ||
|
|
7836fd1990 | ||
|
|
92e765ea21 | ||
|
|
bce1610794 | ||
|
|
1f16233afa | ||
|
|
e804548b22 | ||
|
|
dc994173d7 | ||
|
|
283140d16a | ||
|
|
03155c63ae | ||
|
|
17fc28b020 | ||
|
|
607bef8d68 | ||
|
|
55f5ceb85a | ||
|
|
7be8e3e1e9 | ||
|
|
55c3f5ddff | ||
|
|
44b81b20e3 | ||
|
|
4097be1908 | ||
|
|
f6ef727573 | ||
|
|
1045cb56fe | ||
|
|
6c0ad4966e | ||
|
|
5d50792a56 | ||
|
|
0c16842e0d | ||
|
|
d9538845bc | ||
|
|
7f1579d96d | ||
|
|
3674694d12 | ||
|
|
3e518e8040 | ||
|
|
33c92a31bf | ||
|
|
5e2e7902ce | ||
|
|
f9585430bb | ||
|
|
19de32e206 | ||
|
|
a82bc1df64 | ||
|
|
c34e841710 | ||
|
|
a5c78be52c | ||
|
|
edbbaef26f | ||
|
|
bdd133309d | ||
|
|
71da05dc96 | ||
|
|
f1cbafb097 | ||
|
|
3db557e2c9 | ||
|
|
a8b3177e20 | ||
|
|
8e6f043586 | ||
|
|
5163472392 | ||
|
|
965c811025 | ||
|
|
adc2260b63 | ||
|
|
4ef84054dc | ||
|
|
7db1c9b8eb | ||
|
|
ae06a6ce41 | ||
|
|
0316450ee2 | ||
|
|
281305147b | ||
|
|
ef41e32af5 | ||
|
|
4ef8172f8d | ||
|
|
5106f9f958 | ||
|
|
a9d9dc6658 |
@@ -35,6 +35,11 @@ module.system=haste
|
||||
|
||||
experimental.strict_type_args=true
|
||||
|
||||
; FIXME: munge_underscores should be false but right now there are some errors
|
||||
; if we change the value to false
|
||||
; Treats class properties with underscore as private. Disabled because currently
|
||||
; for us "_" can mean protected too.
|
||||
; munge_underscores=false
|
||||
munge_underscores=true
|
||||
|
||||
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub'
|
||||
@@ -46,6 +51,7 @@ suppress_type=$FixMe
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(3[0-8]\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(3[0-8]\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy
|
||||
suppress_comment=\\(.\\|\n\\)*\\$FlowDisableNextLine
|
||||
|
||||
unsafe.enable_getters_and_setters=true
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ node_modules/
|
||||
# The following are checked by ESLint with the maximum configuration which
|
||||
# supersedes JSHint.
|
||||
flow-typed/
|
||||
modules/API/
|
||||
modules/remotecontrol/
|
||||
modules/transport/
|
||||
react/
|
||||
|
||||
# The following are checked by ESLint with the minimum configuration which does
|
||||
|
||||
@@ -3,13 +3,9 @@ We would love to have your help. Before you start working however, please read
|
||||
and follow this short guide.
|
||||
|
||||
# Reporting Issues
|
||||
Before you open an issue on GitHub, please discuss it on one of our
|
||||
[mailing lists](https://jitsi.org/Development/MailingLists) and wait for
|
||||
confirmation from one of the committers. Once you have that confirmation,
|
||||
please proceed to reporting the issue on GitHub, while providing as much
|
||||
information as possible. Mention the version of Jitsi Meet, Jicofo and JVB
|
||||
you are using, and explain (as detailed as you can) how the problem can
|
||||
be reproduced.
|
||||
Provide as much information as possible. Mention the version of Jitsi Meet,
|
||||
Jicofo and JVB you are using, and explain (as detailed as you can) how the
|
||||
problem can be reproduced.
|
||||
|
||||
# Code contributions
|
||||
Found a bug and know how to fix it? Great! Please read on.
|
||||
|
||||
2
Makefile
@@ -27,6 +27,8 @@ deploy-appbundle:
|
||||
cp \
|
||||
$(BUILD_DIR)/app.bundle.min.js \
|
||||
$(BUILD_DIR)/app.bundle.min.map \
|
||||
$(BUILD_DIR)/do_external_connect.min.js \
|
||||
$(BUILD_DIR)/do_external_connect.min.map \
|
||||
$(BUILD_DIR)/external_api.min.js \
|
||||
$(BUILD_DIR)/external_api.min.map \
|
||||
$(OUTPUT_DIR)/analytics.js \
|
||||
|
||||
@@ -19,14 +19,15 @@ You can download Debian/Ubuntu binaries:
|
||||
* [testing](https://download.jitsi.org/testing/) ([instructions](https://jitsi.org/Main/InstallJitsiMeetDebianTestingRepository))
|
||||
* [nightly](https://download.jitsi.org/unstable/) ([instructions](https://jitsi.org/Main/InstallJitsiMeetDebianNightlyRepository))
|
||||
|
||||
You can download source archives (produced by ```make source-package```):
|
||||
* [source builds](https://download.jitsi.org/jitsi-meet/src/)
|
||||
|
||||
You can get our mobile versions from here:
|
||||
* [Android](https://play.google.com/store/apps/details?id=org.jitsi.meet)
|
||||
* [iOS](https://itunes.apple.com/us/app/jitsi-meet/id1165103905)
|
||||
|
||||
## Building the sources
|
||||
|
||||
Jitsi Meet uses [Browserify](http://browserify.org). If you want to make changes in the code you need to [install Browserify](http://browserify.org/#install). Browserify requires [nodejs](http://nodejs.org).
|
||||
|
||||
On Debian/Ubuntu systems, the required packages can be installed with:
|
||||
```
|
||||
sudo apt-get install npm nodejs-legacy
|
||||
@@ -99,9 +100,6 @@ Jitsi Meet provides a very flexible way of embedding it in external applications
|
||||
Jitsi Meet is also available as a React Native application for Android and iOS.
|
||||
Instructions on how to build it can be found [here](doc/mobile.md).
|
||||
|
||||
## Discuss
|
||||
Please use the [Jitsi dev mailing list](http://lists.jitsi.org/pipermail/dev/) to discuss feature requests before opening an issue on Github.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Jitsi Meet started out as a sample conferencing application using Jitsi Videobridge. It was originally developed by then ESTOS' developer Philipp Hancke who then contributed it to the community where development continues with joint forces!
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<uses-feature android:name="android.hardware.camera.autofocus"/>
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="19"
|
||||
android:minSdkVersion="16"
|
||||
android:targetSdkVersion="23" />
|
||||
|
||||
<application
|
||||
|
||||
@@ -188,13 +188,24 @@ public class AudioModeModule extends ReactContextBaseJavaModule {
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (updateAudioRoute(mode)) {
|
||||
boolean success;
|
||||
|
||||
try {
|
||||
success = updateAudioRoute(mode);
|
||||
} catch (Throwable e) {
|
||||
success = false;
|
||||
Log.e(
|
||||
TAG,
|
||||
"Failed to update audio route for mode: " + mode,
|
||||
e);
|
||||
}
|
||||
if (success) {
|
||||
AudioModeModule.this.mode = mode;
|
||||
promise.resolve(null);
|
||||
} else {
|
||||
promise.reject(
|
||||
"setMode",
|
||||
"Failed to set the requested audio mode");
|
||||
"Failed to set audio mode to " + mode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
rootProject.name = 'jitsi-meet-react'
|
||||
rootProject.name = 'jitsi-meet'
|
||||
|
||||
include ':app'
|
||||
include ':react-native-background-timer'
|
||||
|
||||
2
app.js
@@ -18,7 +18,7 @@ window.toastr = require("toastr");
|
||||
import UI from "./modules/UI/UI";
|
||||
import settings from "./modules/settings/Settings";
|
||||
import conference from './conference';
|
||||
import API from './modules/API/API';
|
||||
import API from './modules/API';
|
||||
|
||||
import translation from "./modules/translation/translation";
|
||||
import remoteControl from "./modules/remotecontrol/RemoteControl";
|
||||
|
||||
168
conference.js
@@ -2,7 +2,6 @@
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import {openConnection} from './connection';
|
||||
import Invite from './modules/UI/invite/Invite';
|
||||
import ContactList from './modules/UI/side_pannels/contactlist/ContactList';
|
||||
|
||||
import AuthHandler from './modules/UI/authentication/AuthHandler';
|
||||
@@ -20,15 +19,15 @@ import analytics from './modules/analytics/analytics';
|
||||
|
||||
import EventEmitter from "events";
|
||||
|
||||
import { showDesktopSharingButton } from './react/features/toolbox';
|
||||
|
||||
import { getLocationContextRoot } from './react/features/app';
|
||||
import {
|
||||
AVATAR_ID_COMMAND,
|
||||
AVATAR_URL_COMMAND,
|
||||
conferenceFailed,
|
||||
conferenceJoined,
|
||||
conferenceLeft,
|
||||
EMAIL_COMMAND
|
||||
EMAIL_COMMAND,
|
||||
lockStateChanged
|
||||
} from './react/features/base/conference';
|
||||
import {
|
||||
updateDeviceList
|
||||
@@ -37,6 +36,7 @@ import {
|
||||
isFatalJitsiConnectionError
|
||||
} from './react/features/base/lib-jitsi-meet';
|
||||
import {
|
||||
localParticipantRoleChanged,
|
||||
participantJoined,
|
||||
participantLeft,
|
||||
participantRoleChanged,
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
mediaPermissionPromptVisibilityChanged,
|
||||
suspendDetected
|
||||
} from './react/features/overlay';
|
||||
import { showDesktopSharingButton } from './react/features/toolbox';
|
||||
|
||||
const ConnectionEvents = JitsiMeetJS.events.connection;
|
||||
const ConnectionErrors = JitsiMeetJS.errors.connection;
|
||||
@@ -272,9 +273,11 @@ function muteLocalVideo(muted) {
|
||||
function maybeRedirectToWelcomePage(options) {
|
||||
// if close page is enabled redirect to it, without further action
|
||||
if (config.enableClosePage) {
|
||||
const { isGuest } = APP.store.getState()['features/jwt'];
|
||||
|
||||
// save whether current user is guest or not, before navigating
|
||||
// to close page
|
||||
window.sessionStorage.setItem('guest', APP.tokenData.isGuest);
|
||||
window.sessionStorage.setItem('guest', isGuest);
|
||||
assignWindowLocationPathname('static/'
|
||||
+ (options.feedbackSubmitted ? "close.html" : "close2.html"));
|
||||
return;
|
||||
@@ -308,18 +311,11 @@ function assignWindowLocationPathname(pathname) {
|
||||
const windowLocation = window.location;
|
||||
|
||||
if (!pathname.startsWith('/')) {
|
||||
// XXX To support a deployment in a sub-directory, assume that the room
|
||||
// (name) is the last non-directory component of the path (name).
|
||||
let contextRoot = windowLocation.pathname;
|
||||
|
||||
contextRoot
|
||||
= contextRoot.substring(0, contextRoot.lastIndexOf('/') + 1);
|
||||
|
||||
// A pathname equal to ./ specifies the current directory. It will be
|
||||
// fine but pointless to include it because contextRoot is the current
|
||||
// directory.
|
||||
pathname.startsWith('./') && (pathname = pathname.substring(2));
|
||||
pathname = contextRoot + pathname;
|
||||
pathname = getLocationContextRoot(windowLocation) + pathname;
|
||||
}
|
||||
|
||||
windowLocation.pathname = pathname;
|
||||
@@ -372,10 +368,9 @@ function createLocalTracks(options, checkForPermissionPrompt) {
|
||||
}
|
||||
|
||||
class ConferenceConnector {
|
||||
constructor(resolve, reject, invite) {
|
||||
constructor(resolve, reject) {
|
||||
this._resolve = resolve;
|
||||
this._reject = reject;
|
||||
this._invite = invite;
|
||||
this.reconnectTimeout = null;
|
||||
room.on(ConferenceEvents.CONFERENCE_JOINED,
|
||||
this._handleConferenceJoined.bind(this));
|
||||
@@ -409,15 +404,15 @@ class ConferenceConnector {
|
||||
break;
|
||||
|
||||
// not enough rights to create conference
|
||||
case ConferenceErrors.AUTHENTICATION_REQUIRED:
|
||||
// schedule reconnect to check if someone else created the room
|
||||
this.reconnectTimeout = setTimeout(function () {
|
||||
room.join();
|
||||
}, 5000);
|
||||
case ConferenceErrors.AUTHENTICATION_REQUIRED: {
|
||||
// Schedule reconnect to check if someone else created the room.
|
||||
this.reconnectTimeout = setTimeout(() => room.join(), 5000);
|
||||
|
||||
// notify user that auth is required
|
||||
AuthHandler.requireAuth(
|
||||
room, this._invite.getRoomLocker().password);
|
||||
const { password }
|
||||
= APP.store.getState()['features/base/conference'];
|
||||
|
||||
AuthHandler.requireAuth(room, password);
|
||||
}
|
||||
break;
|
||||
|
||||
case ConferenceErrors.RESERVATION_ERROR:
|
||||
@@ -603,8 +598,8 @@ export default {
|
||||
|
||||
APP.store.dispatch(showDesktopSharingButton());
|
||||
|
||||
APP.remoteControl.init();
|
||||
this._createRoom(tracks);
|
||||
APP.remoteControl.init();
|
||||
|
||||
if (UIUtil.isButtonEnabled('contacts')
|
||||
&& !interfaceConfig.filmStripOnly) {
|
||||
@@ -629,8 +624,7 @@ export default {
|
||||
// XXX The API will take care of disconnecting from the XMPP
|
||||
// server (and, thus, leaving the room) on unload.
|
||||
return new Promise((resolve, reject) => {
|
||||
(new ConferenceConnector(
|
||||
resolve, reject, this.invite)).connect();
|
||||
(new ConferenceConnector(resolve, reject)).connect();
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -984,7 +978,6 @@ export default {
|
||||
room = connection.initJitsiConference(APP.conference.roomName,
|
||||
this._getConferenceOptions());
|
||||
this._setLocalAudioVideoStreams(localTracks);
|
||||
this.invite = new Invite(room);
|
||||
this._room = room; // FIXME do not use this
|
||||
|
||||
_setupLocalParticipantProperties();
|
||||
@@ -1058,15 +1051,6 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current local video track in use.
|
||||
*
|
||||
* @returns {JitsiLocalTrack}
|
||||
*/
|
||||
getLocalVideoTrack() {
|
||||
return room.getLocalVideoTrack();
|
||||
},
|
||||
|
||||
/**
|
||||
* Start using provided audio stream.
|
||||
* Stops previous audio stream.
|
||||
@@ -1097,12 +1081,41 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the current local audio track in use.
|
||||
* Triggers a tooltip to display when a feature was attempted to be used
|
||||
* while in audio only mode.
|
||||
*
|
||||
* @returns {JitsiLocalTrack}
|
||||
* @param {string} featureName - The name of the feature that attempted to
|
||||
* toggle.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
getLocalAudioTrack() {
|
||||
return room.getLocalAudioTrack();
|
||||
_displayAudioOnlyTooltip(featureName) {
|
||||
let tooltipElementId = null;
|
||||
|
||||
switch (featureName) {
|
||||
case 'screenShare':
|
||||
tooltipElementId = '#screenshareWhileAudioOnly';
|
||||
break;
|
||||
case 'videoMute':
|
||||
tooltipElementId = '#unmuteWhileAudioOnly';
|
||||
break;
|
||||
}
|
||||
|
||||
if (tooltipElementId) {
|
||||
APP.UI.showToolbar(6000);
|
||||
APP.UI.showCustomToolbarPopup(
|
||||
tooltipElementId, true, 5000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether or not the conference is currently in audio only mode.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isAudioOnly() {
|
||||
return Boolean(
|
||||
APP.store.getState()['features/base/conference'].audioOnly);
|
||||
},
|
||||
|
||||
videoSwitchInProgress: false,
|
||||
@@ -1116,6 +1129,11 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isAudioOnly()) {
|
||||
this._displayAudioOnlyTooltip('screenShare');
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoSwitchInProgress = true;
|
||||
let externalInstallation = false;
|
||||
|
||||
@@ -1277,16 +1295,26 @@ export default {
|
||||
APP.UI.onSharedVideoStop(id);
|
||||
});
|
||||
|
||||
room.on(ConferenceEvents.USER_STATUS_CHANGED, (id, status) => {
|
||||
let user = room.getParticipantById(id);
|
||||
if (user) {
|
||||
APP.UI.updateUserStatus(user, status);
|
||||
}
|
||||
});
|
||||
|
||||
room.on(ConferenceEvents.USER_ROLE_CHANGED, (id, role) => {
|
||||
APP.store.dispatch(participantRoleChanged(id, role));
|
||||
if (this.isLocalId(id)) {
|
||||
logger.info(`My role changed, new role: ${role}`);
|
||||
|
||||
APP.store.dispatch(localParticipantRoleChanged(role));
|
||||
|
||||
if (this.isModerator !== room.isModerator()) {
|
||||
this.isModerator = room.isModerator();
|
||||
APP.UI.updateLocalRole(room.isModerator());
|
||||
}
|
||||
} else {
|
||||
APP.store.dispatch(participantRoleChanged(id, role));
|
||||
|
||||
let user = room.getParticipantById(id);
|
||||
if (user) {
|
||||
APP.UI.updateUserRole(user);
|
||||
@@ -1415,6 +1443,10 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
APP.UI.addListener(
|
||||
UIEvents.VIDEO_UNMUTING_WHILE_AUDIO_ONLY,
|
||||
() => this._displayAudioOnlyTooltip('videoMute'));
|
||||
|
||||
APP.UI.addListener(UIEvents.PINNED_ENDPOINT,
|
||||
(smallVideo, isPinned) => {
|
||||
let smallVideoId = smallVideo.getId();
|
||||
@@ -1457,10 +1489,18 @@ export default {
|
||||
room.on(ConferenceEvents.DISPLAY_NAME_CHANGED, (id, displayName) => {
|
||||
const formattedDisplayName
|
||||
= displayName.substr(0, MAX_DISPLAY_NAME_LENGTH);
|
||||
APP.store.dispatch(participantUpdated({
|
||||
id,
|
||||
name: formattedDisplayName
|
||||
}));
|
||||
APP.API.notifyDisplayNameChanged(id, formattedDisplayName);
|
||||
APP.UI.changeDisplayName(id, formattedDisplayName);
|
||||
});
|
||||
|
||||
room.on(
|
||||
ConferenceEvents.LOCK_STATE_CHANGED,
|
||||
(...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
|
||||
|
||||
room.on(ConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
|
||||
(participant, name, oldValue, newValue) => {
|
||||
if (name === "raisedHand") {
|
||||
@@ -1519,7 +1559,14 @@ export default {
|
||||
});
|
||||
|
||||
APP.UI.addListener(UIEvents.AUDIO_MUTED, muteLocalAudio);
|
||||
APP.UI.addListener(UIEvents.VIDEO_MUTED, muteLocalVideo);
|
||||
APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => {
|
||||
if (this.isAudioOnly() && !muted) {
|
||||
this._displayAudioOnlyTooltip('videoMute');
|
||||
return;
|
||||
}
|
||||
|
||||
muteLocalVideo(muted);
|
||||
});
|
||||
|
||||
room.on(ConnectionQualityEvents.LOCAL_STATS_UPDATED,
|
||||
(stats) => {
|
||||
@@ -1610,10 +1657,6 @@ export default {
|
||||
});
|
||||
});
|
||||
|
||||
APP.UI.addListener(UIEvents.SIP_DIAL, (sipNumber) => {
|
||||
room.dial(sipNumber);
|
||||
});
|
||||
|
||||
APP.UI.addListener(UIEvents.RESOLUTION_CHANGED,
|
||||
(id, oldResolution, newResolution, delay) => {
|
||||
var logObject = {
|
||||
@@ -1668,6 +1711,14 @@ export default {
|
||||
micDeviceId: null
|
||||
})
|
||||
.then(([stream]) => {
|
||||
if (this.isAudioOnly()) {
|
||||
return stream.mute()
|
||||
.then(() => stream);
|
||||
}
|
||||
|
||||
return stream;
|
||||
})
|
||||
.then(stream => {
|
||||
this.useVideoStream(stream);
|
||||
logger.log('switched local video device');
|
||||
APP.settings.setCameraDeviceId(cameraDeviceId, true);
|
||||
@@ -1714,6 +1765,18 @@ export default {
|
||||
}
|
||||
);
|
||||
|
||||
APP.UI.addListener(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly => {
|
||||
muteLocalVideo(audioOnly);
|
||||
|
||||
// Immediately update the UI by having remote videos and the large
|
||||
// video update themselves instead of waiting for some other event
|
||||
// to cause the update, usually PARTICIPANT_CONN_STATUS_CHANGED.
|
||||
// There is no guarantee another event will trigger the update
|
||||
// immediately and in all situations, for example because a remote
|
||||
// participant is having connection trouble so no status changes.
|
||||
APP.UI.updateAllVideos();
|
||||
});
|
||||
|
||||
APP.UI.addListener(
|
||||
UIEvents.TOGGLE_SCREENSHARING, this.toggleScreenSharing.bind(this)
|
||||
);
|
||||
@@ -2064,9 +2127,16 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.store.dispatch(participantUpdated({
|
||||
id: this.getMyUserId(),
|
||||
local: true,
|
||||
name: formattedNickname
|
||||
}));
|
||||
|
||||
APP.settings.setDisplayName(formattedNickname);
|
||||
room.setDisplayName(formattedNickname);
|
||||
APP.UI.changeDisplayName(this.getMyUserId(),
|
||||
formattedNickname);
|
||||
if (room) {
|
||||
room.setDisplayName(formattedNickname);
|
||||
APP.UI.changeDisplayName(this.getMyUserId(), formattedNickname);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ var config = { // eslint-disable-line no-unused-vars
|
||||
// extension is required.
|
||||
desktopSharingFirefoxExtId: null,
|
||||
// Whether desktop sharing should be disabled on Firefox.
|
||||
desktopSharingFirefoxDisabled: true,
|
||||
desktopSharingFirefoxDisabled: false,
|
||||
// The maximum version of Firefox which requires a jidesha extension.
|
||||
// Example: if set to 41, we will require the extension for Firefox versions
|
||||
// up to and including 41. On Firefox 42 and higher, we will run without the
|
||||
@@ -65,7 +65,6 @@ var config = { // eslint-disable-line no-unused-vars
|
||||
//enableClosePage: false, // enabling the close page will ignore the welcome
|
||||
// page redirection when call is hangup
|
||||
disableSimulcast: false,
|
||||
logStats: false, // Enable logging of PeerConnection stats via the focus
|
||||
// requireDisplayName: true, // Forces the participants that doesn't have display name to enter it when they enter the room.
|
||||
// startAudioMuted: 10, // every participant after the Nth will start audio muted
|
||||
// startVideoMuted: 10, // every participant after the Nth will start video muted
|
||||
@@ -77,6 +76,8 @@ var config = { // eslint-disable-line no-unused-vars
|
||||
'During that time service will not be available. ' +
|
||||
'Apologise for inconvenience.',*/
|
||||
disableThirdPartyRequests: false,
|
||||
// The minumum value a video's height (or width, whichever is smaller) needs
|
||||
// to be in order to be considered high-definition.
|
||||
minHDHeight: 540,
|
||||
// If true - all users without token will be considered guests and all users
|
||||
// with token will be considered non-guests. Only guests will be allowed to
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* global APP, JitsiMeetJS, config */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import AuthHandler from './modules/UI/authentication/AuthHandler';
|
||||
import jitsiLocalStorage from './modules/util/JitsiLocalStorage';
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
|
||||
const ConnectionEvents = JitsiMeetJS.events.connection;
|
||||
const ConnectionErrors = JitsiMeetJS.errors.connection;
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Checks if we have data to use attach instead of connect. If we have the data
|
||||
@@ -61,22 +61,27 @@ function checkForAttachParametersAndConnect(id, password, connection) {
|
||||
* everything is ok, else error.
|
||||
*/
|
||||
function connect(id, password, roomName) {
|
||||
|
||||
let connectionConfig = Object.assign({}, config);
|
||||
const connectionConfig = Object.assign({}, config);
|
||||
const { issuer, jwt } = APP.store.getState()['features/jwt'];
|
||||
|
||||
connectionConfig.bosh += '?room=' + roomName;
|
||||
|
||||
let connection
|
||||
= new JitsiMeetJS.JitsiConnection(null, config.token, connectionConfig);
|
||||
= new JitsiMeetJS.JitsiConnection(
|
||||
null,
|
||||
jwt && issuer && issuer !== 'anonymous' ? jwt : undefined,
|
||||
connectionConfig);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
connection.addEventListener(
|
||||
ConnectionEvents.CONNECTION_ESTABLISHED, handleConnectionEstablished
|
||||
);
|
||||
ConnectionEvents.CONNECTION_ESTABLISHED,
|
||||
handleConnectionEstablished);
|
||||
connection.addEventListener(
|
||||
ConnectionEvents.CONNECTION_FAILED, handleConnectionFailed
|
||||
);
|
||||
ConnectionEvents.CONNECTION_FAILED,
|
||||
handleConnectionFailed);
|
||||
connection.addEventListener(
|
||||
ConnectionEvents.CONNECTION_FAILED, connectionFailedHandler);
|
||||
ConnectionEvents.CONNECTION_FAILED,
|
||||
connectionFailedHandler);
|
||||
|
||||
function connectionFailedHandler(error, errMsg) {
|
||||
APP.store.dispatch(connectionFailed(connection, error, errMsg));
|
||||
@@ -91,12 +96,10 @@ function connect(id, password, roomName) {
|
||||
function unsubscribe() {
|
||||
connection.removeEventListener(
|
||||
ConnectionEvents.CONNECTION_ESTABLISHED,
|
||||
handleConnectionEstablished
|
||||
);
|
||||
handleConnectionEstablished);
|
||||
connection.removeEventListener(
|
||||
ConnectionEvents.CONNECTION_FAILED,
|
||||
handleConnectionFailed
|
||||
);
|
||||
handleConnectionFailed);
|
||||
}
|
||||
|
||||
function handleConnectionEstablished() {
|
||||
@@ -129,7 +132,6 @@ function connect(id, password, roomName) {
|
||||
* @returns {Promise<JitsiConnection>}
|
||||
*/
|
||||
export function openConnection({id, password, retry, roomName}) {
|
||||
|
||||
let usernameOverride
|
||||
= jitsiLocalStorage.getItem("xmpp_username_override");
|
||||
let passwordOverride
|
||||
@@ -138,25 +140,20 @@ export function openConnection({id, password, retry, roomName}) {
|
||||
if (usernameOverride && usernameOverride.length > 0) {
|
||||
id = usernameOverride;
|
||||
}
|
||||
|
||||
if (passwordOverride && passwordOverride.length > 0) {
|
||||
password = passwordOverride;
|
||||
}
|
||||
|
||||
return connect(id, password, roomName).catch(function (err) {
|
||||
if (!retry) {
|
||||
throw err;
|
||||
}
|
||||
return connect(id, password, roomName).catch(err => {
|
||||
if (retry) {
|
||||
const { issuer, jwt } = APP.store.getState()['features/jwt'];
|
||||
|
||||
if (err === ConnectionErrors.PASSWORD_REQUIRED) {
|
||||
// do not retry if token is not valid
|
||||
if (config.token) {
|
||||
throw err;
|
||||
} else {
|
||||
if (err === ConnectionErrors.PASSWORD_REQUIRED
|
||||
&& (!jwt || issuer === 'anonymous')) {
|
||||
return AuthHandler.requestAuth(roomName, connect);
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
3
connection_optimization/.eslintrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
'extends': '../react/.eslintrc.js'
|
||||
};
|
||||
@@ -1,75 +1,82 @@
|
||||
/* global config, getRoomName, getConfigParamsFromUrl */
|
||||
/* global createConnectionExternally */
|
||||
/* global config, createConnectionExternally */
|
||||
|
||||
import getRoomName from '../react/features/base/config/getRoomName';
|
||||
import parseURLParams from '../react/features/base/config/parseURLParams';
|
||||
|
||||
/**
|
||||
* Implements extrnal connect using createConnectionExtenally function defined
|
||||
* in external_connect.js for Jitsi Meet. Parses the room name and token from
|
||||
* the url and executes createConnectionExtenally.
|
||||
* Implements external connect using createConnectionExternally function defined
|
||||
* in external_connect.js for Jitsi Meet. Parses the room name and JSON Web
|
||||
* Token (JWT) from the URL and executes createConnectionExternally.
|
||||
*
|
||||
* NOTE: If you are using lib-jitsi-meet without Jitsi Meet you should use this
|
||||
* file as reference only because the implementation is Jitsi Meet specific.
|
||||
* NOTE: If you are using lib-jitsi-meet without Jitsi Meet, you should use this
|
||||
* file as reference only because the implementation is Jitsi Meet-specific.
|
||||
*
|
||||
* NOTE: For optimal results this file should be included right after
|
||||
* exrnal_connect.js.
|
||||
* external_connect.js.
|
||||
*/
|
||||
|
||||
if (typeof createConnectionExternally === 'function') {
|
||||
// URL params have higher proirity than config params.
|
||||
let url
|
||||
= parseURLParams(window.location, true, 'hash')[
|
||||
'config.externalConnectUrl']
|
||||
|| config.externalConnectUrl;
|
||||
let roomName;
|
||||
|
||||
if (url && (roomName = getRoomName())) {
|
||||
url += `?room=${roomName}`;
|
||||
|
||||
const token = parseURLParams(window.location, true, 'search').jwt;
|
||||
|
||||
if (token) {
|
||||
url += `&token=${token}`;
|
||||
}
|
||||
|
||||
createConnectionExternally(
|
||||
url,
|
||||
connectionInfo => {
|
||||
// Sets that global variable to be used later by connect method
|
||||
// in connection.js.
|
||||
window.XMPPAttachInfo = {
|
||||
status: 'success',
|
||||
data: connectionInfo
|
||||
};
|
||||
checkForConnectHandlerAndConnect();
|
||||
},
|
||||
errorCallback);
|
||||
} else {
|
||||
errorCallback();
|
||||
}
|
||||
} else {
|
||||
errorCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes createConnectionExternally function.
|
||||
* Check if connect from connection.js was executed and executes the handler
|
||||
* that is going to finish the connect work.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
(function () {
|
||||
var hashParams = getConfigParamsFromUrl("hash", true);
|
||||
var searchParams = getConfigParamsFromUrl("search", true);
|
||||
function checkForConnectHandlerAndConnect() {
|
||||
window.APP
|
||||
&& window.APP.connect.status === 'ready'
|
||||
&& window.APP.connect.handler();
|
||||
}
|
||||
|
||||
//Url params have higher proirity than config params
|
||||
var url = config.externalConnectUrl;
|
||||
if(hashParams.hasOwnProperty('config.externalConnectUrl'))
|
||||
url = hashParams["config.externalConnectUrl"];
|
||||
/**
|
||||
* Implements a callback to be invoked if anything goes wrong.
|
||||
*
|
||||
* @param {Error} error - The specifics of what went wrong.
|
||||
* @returns {void}
|
||||
*/
|
||||
function errorCallback(error) {
|
||||
// The value of error is undefined if external connect is disabled.
|
||||
error && console.warn(error);
|
||||
|
||||
/**
|
||||
* Check if connect from connection.js was executed and executes the handler
|
||||
* that is going to finish the connect work.
|
||||
*/
|
||||
function checkForConnectHandlerAndConnect() {
|
||||
|
||||
if(window.APP && window.APP.connect.status === "ready") {
|
||||
window.APP.connect.handler();
|
||||
}
|
||||
}
|
||||
|
||||
function error_callback(error){
|
||||
if(error) //error=undefined if external connect is disabled.
|
||||
console.warn(error);
|
||||
// Sets that global variable to be used later by connect method in
|
||||
// connection.js
|
||||
window.XMPPAttachInfo = {
|
||||
status: "error"
|
||||
};
|
||||
checkForConnectHandlerAndConnect();
|
||||
}
|
||||
|
||||
if(!url || !window.createConnectionExternally) {
|
||||
error_callback();
|
||||
return;
|
||||
}
|
||||
var room_name = getRoomName();
|
||||
if(!room_name) {
|
||||
error_callback();
|
||||
return;
|
||||
}
|
||||
|
||||
url += "?room=" + room_name;
|
||||
|
||||
var token = hashParams["config.token"] || config.token ||
|
||||
searchParams.jwt;
|
||||
if(token)
|
||||
url += "&token=" + token;
|
||||
|
||||
createConnectionExternally(url, function(connectionInfo) {
|
||||
// Sets that global variable to be used later by connect method in
|
||||
// connection.js
|
||||
window.XMPPAttachInfo = {
|
||||
status: "success",
|
||||
data: connectionInfo
|
||||
};
|
||||
checkForConnectHandlerAndConnect();
|
||||
}, error_callback);
|
||||
})();
|
||||
// Sets that global variable to be used later by connect method in
|
||||
// connection.js.
|
||||
window.XMPPAttachInfo = {
|
||||
status: 'error'
|
||||
};
|
||||
checkForConnectHandlerAndConnect();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
* {
|
||||
/**
|
||||
* Safari will limit input in input elements to one character when user-select
|
||||
* none is applied. Other browsers already support selecting within inputs while
|
||||
* user-select is none. As such, disallow user-select except on inputs.
|
||||
*/
|
||||
*:not(input) {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
64
css/_dial-out.scss
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* The dialog content element.
|
||||
*/
|
||||
.dial-out-content {
|
||||
margin-top: 5px;
|
||||
|
||||
/**
|
||||
* The style of the flag icon.
|
||||
*/
|
||||
.dial-out-flag-icon {
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
/**
|
||||
* The style of the dial code element.
|
||||
*/
|
||||
.dial-out-code {
|
||||
padding-left: 25px !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* The dial-out dialog error element.
|
||||
*/
|
||||
.dial-out-error {
|
||||
color: $errorColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* The style of the dial input element.
|
||||
*/
|
||||
.dial-out-input {
|
||||
padding-left: 70px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-styling the default dropdown inside the dial-out-content.
|
||||
*/
|
||||
.dropdown {
|
||||
left: $formPadding;
|
||||
position: absolute !important;
|
||||
width: 65px
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-styling the default form-control inside the dial-out-content.
|
||||
*/
|
||||
.form-control {
|
||||
padding-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdown-trigger-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
35
css/_flag-icon.scss
Executable file
@@ -0,0 +1,35 @@
|
||||
.flag-icon-background {
|
||||
background-size: contain;
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.flag-icon {
|
||||
background-size: contain;
|
||||
background-position: 50%;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 1.33333333em;
|
||||
line-height: 1em;
|
||||
}
|
||||
.flag-icon:before {
|
||||
content: "\00a0";
|
||||
}
|
||||
.flag-icon-au {
|
||||
background-image: url(../images/countries/au.svg);
|
||||
}
|
||||
.flag-icon-ca {
|
||||
background-image: url(../images/countries/ca.svg);
|
||||
}
|
||||
.flag-icon-de {
|
||||
background-image: url(../images/countries/de.svg);
|
||||
}
|
||||
.flag-icon-gb {
|
||||
background-image: url(../images/countries/gb.svg);
|
||||
}
|
||||
.flag-icon-fr {
|
||||
background-image: url(../images/countries/fr.svg);
|
||||
}
|
||||
.flag-icon-us {
|
||||
background-image: url(../images/countries/us.svg);
|
||||
}
|
||||
@@ -26,116 +26,125 @@
|
||||
}
|
||||
|
||||
.icon-mic-camera-combined:before {
|
||||
content: "\e903";
|
||||
content: "\e903";
|
||||
}
|
||||
.icon-feedback:before {
|
||||
content: "\e91d";
|
||||
content: "\e91d";
|
||||
}
|
||||
.icon-toggle-filmstrip:before {
|
||||
content: "\e91c";
|
||||
content: "\e91c";
|
||||
}
|
||||
.icon-avatar:before {
|
||||
content: "\e901";
|
||||
content: "\e901";
|
||||
}
|
||||
.icon-hangup:before {
|
||||
content: "\e905";
|
||||
content: "\e905";
|
||||
}
|
||||
.icon-chat:before {
|
||||
content: "\e906";
|
||||
content: "\e906";
|
||||
}
|
||||
.icon-download:before {
|
||||
content: "\e902";
|
||||
content: "\e902";
|
||||
}
|
||||
.icon-edit:before {
|
||||
content: "\e907";
|
||||
content: "\e907";
|
||||
}
|
||||
.icon-share-doc:before {
|
||||
content: "\e908";
|
||||
}
|
||||
.icon-telephone:before {
|
||||
content: "\e909";
|
||||
content: "\e908";
|
||||
}
|
||||
.icon-kick:before {
|
||||
content: "\e904";
|
||||
content: "\e904";
|
||||
}
|
||||
.icon-menu-up:before {
|
||||
content: "\e91f";
|
||||
content: "\e91f";
|
||||
}
|
||||
.icon-menu-down:before {
|
||||
content: "\e920";
|
||||
content: "\e920";
|
||||
}
|
||||
.icon-full-screen:before {
|
||||
content: "\e90b";
|
||||
content: "\e90b";
|
||||
}
|
||||
.icon-exit-full-screen:before {
|
||||
content: "\e90c";
|
||||
content: "\e90c";
|
||||
}
|
||||
.icon-star-full:before {
|
||||
content: "\e90a";
|
||||
content: "\e90a";
|
||||
}
|
||||
.icon-security:before {
|
||||
content: "\e90d";
|
||||
content: "\e90d";
|
||||
}
|
||||
.icon-security-locked:before {
|
||||
content: "\e90e";
|
||||
content: "\e90e";
|
||||
}
|
||||
.icon-reload:before {
|
||||
content: "\e90f";
|
||||
content: "\e90f";
|
||||
}
|
||||
.icon-microphone:before {
|
||||
content: "\e910";
|
||||
content: "\e910";
|
||||
}
|
||||
.icon-mic-empty:before {
|
||||
content: "\e911";
|
||||
content: "\e911";
|
||||
}
|
||||
.icon-mic-disabled:before {
|
||||
content: "\e912";
|
||||
content: "\e912";
|
||||
}
|
||||
.icon-raised-hand:before {
|
||||
content: "\e91e";
|
||||
content: "\e91e";
|
||||
}
|
||||
.icon-contactList:before {
|
||||
content: "\e91b";
|
||||
content: "\e91b";
|
||||
}
|
||||
.icon-link:before {
|
||||
content: "\e913";
|
||||
content: "\e913";
|
||||
}
|
||||
.icon-shared-video:before {
|
||||
content: "\e914";
|
||||
content: "\e914";
|
||||
}
|
||||
.icon-settings:before {
|
||||
content: "\e915";
|
||||
content: "\e915";
|
||||
}
|
||||
.icon-star:before {
|
||||
content: "\e916";
|
||||
content: "\e916";
|
||||
}
|
||||
.icon-switch-camera:before {
|
||||
content: "\e921";
|
||||
content: "\e921";
|
||||
}
|
||||
.icon-share-desktop:before {
|
||||
content: "\e917";
|
||||
content: "\e917";
|
||||
}
|
||||
.icon-camera:before {
|
||||
content: "\e918";
|
||||
content: "\e918";
|
||||
}
|
||||
.icon-camera-disabled:before {
|
||||
content: "\e919";
|
||||
content: "\e919";
|
||||
}
|
||||
.icon-volume:before {
|
||||
content: "\e91a";
|
||||
content: "\e91a";
|
||||
}
|
||||
.icon-connection-lost:before {
|
||||
content: "\e900";
|
||||
content: "\e900";
|
||||
}
|
||||
.icon-connection:before {
|
||||
content: "\e61a";
|
||||
content: "\e61a";
|
||||
}
|
||||
.icon-recDisable:before {
|
||||
content: "\e613";
|
||||
content: "\e613";
|
||||
}
|
||||
.icon-recEnable:before {
|
||||
content: "\e614";
|
||||
content: "\e614";
|
||||
}
|
||||
.icon-presentation:before {
|
||||
content: "\e603";
|
||||
}
|
||||
content: "\e603";
|
||||
}
|
||||
.icon-dialpad:before {
|
||||
content: "\e925";
|
||||
}
|
||||
.icon-visibility:before {
|
||||
content: "\e923";
|
||||
}
|
||||
.icon-visibility-off:before {
|
||||
content: "\e924";
|
||||
}
|
||||
.icon-telephone:before {
|
||||
content: "\e0cd";
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
}
|
||||
|
||||
&__avatar-container {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
> img {
|
||||
height: 100%;
|
||||
|
||||
@@ -41,21 +41,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__text,
|
||||
&__slider {
|
||||
&__text {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&__slider {
|
||||
width: 50px;
|
||||
&__contents {
|
||||
display: flex;
|
||||
|
||||
/**
|
||||
* Positioning styles on the slider and its container are used to make
|
||||
* the container fit the popup width, by removing the slider from the
|
||||
* page flow, and then making the slider fit the container.
|
||||
*/
|
||||
.popupmenu__slider_container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.popupmenu__slider {
|
||||
bottom: 50%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
|
||||
|
||||
@@ -71,6 +71,10 @@
|
||||
&.icon-microphone {
|
||||
@extend .icon-mic-disabled;
|
||||
}
|
||||
|
||||
&.icon-visibility {
|
||||
@extend .icon-visibility-off;
|
||||
}
|
||||
}
|
||||
|
||||
&.unclickable {
|
||||
@@ -170,7 +174,7 @@
|
||||
width: $defaultToolbarSize;
|
||||
-webkit-transform: translateX(-100%);
|
||||
|
||||
.button.toggled:not(.icon-raised-hand) {
|
||||
.button.toggled:not(.icon-raised-hand):not(.button-active) {
|
||||
background: $toolbarSelectBackground;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -83,6 +83,9 @@ $rateStarSize: 34px;
|
||||
* Modals
|
||||
*/
|
||||
$modalButtonFontSize: 14px;
|
||||
$modalMockAKInputBackground: #fafbfc;
|
||||
$modalMockAKInputBorder: 1px solid #f4f5f7;
|
||||
$modalTextColor: #333;
|
||||
|
||||
/**
|
||||
* Notifications
|
||||
@@ -124,11 +127,11 @@ $toolbarZ: 400;
|
||||
$tooltipsZ: 401;
|
||||
$dropdownMaskZ: 900;
|
||||
$dropdownZ: 901;
|
||||
$overlayZ: 902;
|
||||
$jitsipopoverZ: 1010;
|
||||
$centeredVideoLabelZ: 1011;
|
||||
$notificationZ: 1012;
|
||||
$popoverZ: 1015;
|
||||
$overlayZ: 1016;
|
||||
|
||||
|
||||
/**
|
||||
@@ -146,6 +149,7 @@ $inputControlEmColor: #f29424;
|
||||
//buttons
|
||||
$linkFontColor: #489afe;
|
||||
$linkHoverFontColor: #287ade;
|
||||
$formPadding: 16px;
|
||||
|
||||
/**
|
||||
* Unsupported browser
|
||||
|
||||
@@ -115,6 +115,12 @@
|
||||
visibility: hidden;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
&.audio-only {
|
||||
.videoThumbnailProblemFilter {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#localVideoWrapper {
|
||||
@@ -489,14 +495,23 @@
|
||||
0px 0px 1px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.audio-only-label {
|
||||
display: flex;
|
||||
height: auto;
|
||||
justify-content: center;
|
||||
z-index: $centeredVideoLabelZ;
|
||||
}
|
||||
|
||||
.audio-only-label,
|
||||
.video-state-indicator {
|
||||
background: $videoStateIndicatorBackground;
|
||||
color: $videoStateIndicatorColor;
|
||||
cursor: default;
|
||||
font-size: 13px;
|
||||
height: 40px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
padding: 10px 5px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
@@ -505,13 +520,13 @@
|
||||
|
||||
#videoResolutionLabel,
|
||||
.centeredVideoLabel {
|
||||
display: none;
|
||||
z-index: $centeredVideoLabelZ;
|
||||
}
|
||||
|
||||
.centeredVideoLabel {
|
||||
bottom: 45%;
|
||||
border-radius: 2px;
|
||||
display: none;
|
||||
-webkit-transition: all 2s 2s linear;
|
||||
transition: all 2s 2s linear;
|
||||
|
||||
@@ -528,4 +543,58 @@
|
||||
|
||||
.moveToCorner + .moveToCorner {
|
||||
right: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.video-state-indicator-menu {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
right: -10px;
|
||||
top: 20px;
|
||||
|
||||
.video-state-indicator-menu-options {
|
||||
background: $popoverBg;
|
||||
border-radius: 3px;
|
||||
color: $popoverFontColor;
|
||||
margin-top: 20px;
|
||||
padding: 5px 0;
|
||||
position: relative;
|
||||
|
||||
div {
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
padding-right: 30px;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
|
||||
&.active {
|
||||
background: $toolbarToggleBackground;
|
||||
}
|
||||
&:hover:not(.active) {
|
||||
background: $popupMenuSelectedItemBackground;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-state-indicator-menu-options::after {
|
||||
content: " ";
|
||||
border-color: transparent transparent $popoverBg transparent;
|
||||
border-style: solid;
|
||||
border-width: 5px;
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.video-state-indicator:hover,
|
||||
.video-state-indicator *:hover {
|
||||
.video-state-indicator-menu {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.form-control {
|
||||
padding: 16px 0;
|
||||
padding: $formPadding 0;
|
||||
|
||||
&:first-child {
|
||||
padding-top: 0;
|
||||
|
||||
@@ -26,11 +26,13 @@
|
||||
|
||||
@import 'font';
|
||||
@import 'font-awesome';
|
||||
|
||||
/* Fonts END */
|
||||
|
||||
@import 'flag-icon';
|
||||
|
||||
/* Modules BEGIN */
|
||||
|
||||
@import 'dial-out';
|
||||
@import 'toastr';
|
||||
@import 'base';
|
||||
@import 'utils';
|
||||
|
||||
@@ -77,6 +77,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog-form {
|
||||
color: $modalTextColor;
|
||||
|
||||
.input-control {
|
||||
background: $modalMockAKInputBackground;
|
||||
border: $modalMockAKInputBorder;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
.modal-dialog-footer {
|
||||
font-size: $modalButtonFontSize;
|
||||
}
|
||||
|
||||
@@ -5,37 +5,81 @@
|
||||
font-size: 14px;
|
||||
|
||||
> div {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.device-selector-icon {
|
||||
align-self: center;
|
||||
color: inherit;
|
||||
font-size: 20px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
/* device-selector-trigger stylings attempt to mimic AtlasKit button */
|
||||
.device-selector-trigger {
|
||||
background-color: rgba(9, 30, 66, 0.04);
|
||||
border-radius: 3px;
|
||||
color: #505f79;
|
||||
display: flex;
|
||||
height: 2.3em;
|
||||
justify-content: space-between;
|
||||
line-height: 2.3em;
|
||||
overflow: hidden;
|
||||
padding: 0 8px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(9,30,66,.08);
|
||||
}
|
||||
}
|
||||
.device-selector-trigger-disabled {
|
||||
.device-selector-trigger {
|
||||
color: #a5adba;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.device-selector-trigger-text {
|
||||
overflow: hidden;
|
||||
margin-left: 8px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.device-selection-column-selectors,
|
||||
.device-selection-column-video {
|
||||
padding: 10px;
|
||||
.device-selection-column {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
.device-selection-column-selectors {
|
||||
width: 46%;
|
||||
}
|
||||
.device-selection-column-video {
|
||||
width: 49%;
|
||||
padding: 10px 0;
|
||||
|
||||
&.column-selectors {
|
||||
margin-left: 15px;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
&.column-video {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.device-selection-video-container {
|
||||
background: black;
|
||||
height: 156px;
|
||||
margin: 15px 0 5px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.video-input-preview {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
|
||||
.video-input-preview-muted {
|
||||
> video {
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.video-input-preview-error {
|
||||
color: $participantNameColor;
|
||||
display: none;
|
||||
left: 0;
|
||||
@@ -45,12 +89,16 @@
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
&.video-muted .video-input-preview-muted {
|
||||
display: block;
|
||||
&.video-preview-has-error {
|
||||
background: black;
|
||||
|
||||
.video-input-preview-error {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.video-input-preview-display {
|
||||
height: 100%;
|
||||
height: auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -58,8 +106,8 @@
|
||||
}
|
||||
|
||||
.audio-output-preview {
|
||||
text-align: right;
|
||||
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
a {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -4,4 +4,84 @@
|
||||
*/
|
||||
#inviteDialogRemovePassword {
|
||||
cursor: hand;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-dialog {
|
||||
.dial-in-numbers {
|
||||
.dial-in-numbers-conference-id {
|
||||
color: orange;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
/*
|
||||
* dial-in-numbers-copy styling is needed for the feature of copying
|
||||
* text to the clipboard. The styling keeps the element invisible
|
||||
* to the user but still programmatically selectable for copying.
|
||||
*/
|
||||
.dial-in-numbers-copy {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.dial-in-numbers-trigger {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.dial-in-numbers-trigger-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.is-disabled,
|
||||
.is-loading {
|
||||
.dial-in-numbers-trigger-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.inviteLink {
|
||||
color: $readOnlyInputColor;
|
||||
}
|
||||
|
||||
.lock-state {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.password-overview {
|
||||
margin-top: 10px;
|
||||
|
||||
.form-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.password-overview-status,
|
||||
.remove-password {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.password-overview-toggle-edit,
|
||||
.remove-password-link {
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.remove-password {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-password-current {
|
||||
color: $inputControlEmColor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
.speaker-stats {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
color: $auiDialogColor;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
|
||||
|
||||
6
debian/jitsi-meet-tokens.postinst
vendored
@@ -48,7 +48,9 @@ case "$1" in
|
||||
db_stop
|
||||
|
||||
if [ -f "$PROSODY_HOST_CONFIG" ] ; then
|
||||
if grep -q "plugin_paths" "$PROSODY_HOST_CONFIG"; then
|
||||
# search for --plugin_paths, if this is not enabled this is the
|
||||
# first time we install tokens package and needs a config change
|
||||
if grep -q "\-\-plugin_paths" "$PROSODY_HOST_CONFIG"; then
|
||||
# enable tokens in prosody host config
|
||||
sed -i 's/--plugin_paths/plugin_paths/g' $PROSODY_HOST_CONFIG
|
||||
sed -i 's/authentication = "anonymous"/authentication = "token"/g' $PROSODY_HOST_CONFIG
|
||||
@@ -70,8 +72,6 @@ case "$1" in
|
||||
echo "Use the following command, after this package has been installed and"
|
||||
echo "after every prosody-trunk upgrade:"
|
||||
echo "sudo patch -N /usr/lib/prosody/modules/mod_bosh.lua /usr/share/jitsi-meet/prosody-plugins/mod_bosh.lua.patch"
|
||||
else
|
||||
echo "Failed apply auto-config to $PROSODY_HOST_CONFIG which most likely comes from not supported version of jitsi-meet"
|
||||
fi
|
||||
else
|
||||
echo "Prosody config not found at $PROSODY_HOST_CONFIG - unable to auto-configure token authentication"
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
projects. For example:
|
||||
|
||||
* The instance of lib-jitsi-meet's `JitsiConnection` type should be named
|
||||
`connection` or `jitsiConnection` in jitsi-meet-react, not `client`.
|
||||
`connection` or `jitsiConnection` in jitsi-meet, not `client`.
|
||||
|
||||
* The class `ReducerRegistry` should be defined in ReducerRegistry.js and its
|
||||
imports in other files should use the same name. Don't define the class
|
||||
|
||||
@@ -23,7 +23,10 @@ VirtualHost "jitmeet.example.com"
|
||||
"ping"; -- Enable mod_ping
|
||||
}
|
||||
|
||||
c2s_require_encryption = false
|
||||
|
||||
Component "conference.jitmeet.example.com" "muc"
|
||||
storage = "null"
|
||||
--modules_enabled = { "token_verification" }
|
||||
admins = { "focusUser@auth.jitmeet.example.com" }
|
||||
|
||||
|
||||
@@ -179,6 +179,8 @@ VirtualHost "jitsi.example.com"
|
||||
certificate = "/var/lib/prosody/jitsi.example.com.crt";
|
||||
}
|
||||
|
||||
c2s_require_encryption = false
|
||||
|
||||
------ Components ------
|
||||
-- You can specify components to add hosts that provide special services,
|
||||
-- like multi-user conferences, and transports.
|
||||
|
||||
@@ -55,6 +55,7 @@ VirtualHost "jitsi.example.com"
|
||||
"bosh";
|
||||
"pubsub";
|
||||
}
|
||||
c2s_require_encryption = false
|
||||
```
|
||||
- add domain with authentication for conference focus user:
|
||||
```
|
||||
@@ -237,23 +238,4 @@ You are now all set and ready to have your first meet by going to http://jitsi.e
|
||||
|
||||
|
||||
## Enabling recording
|
||||
Currently recording is only supported for linux-64 and macos. To enable it, add
|
||||
the following properties to sip-communicator.properties:
|
||||
```
|
||||
org.jitsi.videobridge.ENABLE_MEDIA_RECORDING=true
|
||||
org.jitsi.videobridge.MEDIA_RECORDING_PATH=/path/to/recordings/dir
|
||||
org.jitsi.videobridge.MEDIA_RECORDING_TOKEN=secret
|
||||
```
|
||||
|
||||
where /path/to/recordings/dir is the path to a pre-existing directory where recordings
|
||||
will be stored (needs to be writeable by the user running jitsi-videobridge),
|
||||
and "secret" is a string which will be used for authentication.
|
||||
|
||||
Then, edit the Jitsi-Meet config.js file and set:
|
||||
```
|
||||
enableRecording: true
|
||||
```
|
||||
|
||||
Restart jitsi-videobridge and start a new conference (making sure that the page
|
||||
is reloaded with the new config.js) -- the organizer of the conference should
|
||||
now have a "recording" button in the floating menu, near the "mute" button.
|
||||
[Jibri](https://github.com/jitsi/jibri)is a set of tools for recording and/or streaming a Jitsi Meet conference.
|
||||
|
||||
@@ -43,8 +43,8 @@ work properly with the native plugins we require.
|
||||
|
||||
Using Xcode
|
||||
|
||||
- Open **ios/jitsi-meet-react.xcworkspace** in Xcode. Make sure it's the
|
||||
workspace file!
|
||||
- Open **ios/jitsi-meet.xcworkspace** in Xcode. Make sure it's the workspace
|
||||
file!
|
||||
|
||||
- Select your device from the top bar and hit the "play" button.
|
||||
|
||||
|
||||
@@ -30,6 +30,17 @@ During the installation, you will be asked to enter the hostname of the Jitsi Me
|
||||
|
||||
This hostname (or IP address) will be used for virtualhost configuration inside the Jitsi Meet and also, you and your correspondents will be using it to access the web conferences.
|
||||
|
||||
#### Advanced configuration
|
||||
If installation is on a machine behind NAT further configuration of jitsi-videobridge is needed in order for it to be accessible.
|
||||
Provided that all required ports are routed (forwarded) to the machine that it runs on. By default these ports are (TCP/443 or TCP/4443 and UDP 10000).
|
||||
The following extra lines need to be added the file `/etc/jitsi/videobridge/sip-communicator.properties`:
|
||||
```
|
||||
org.ice4j.ice.harvest.NAT_HARVESTER_LOCAL_ADDRESS=<Local.IP.Address>
|
||||
org.ice4j.ice.harvest.NAT_HARVESTER_PUBLIC_ADDRESS=<Public.IP.Address>
|
||||
```
|
||||
See [the documenation of ice4j](https://github.com/jitsi/ice4j/blob/master/doc/configuration.md)
|
||||
for details.
|
||||
|
||||
### Open a conference
|
||||
|
||||
Launch a web browser (Chrome, Chromium or latest Opera) and enter in the URL bar the hostname (or IP address) you used in the previous step.
|
||||
@@ -63,7 +74,7 @@ Enjoy!
|
||||
## Uninstall
|
||||
|
||||
```sh
|
||||
apt-get purge jigasi jitsi-meet jicofo jitsi-videobridge
|
||||
apt-get purge jigasi jitsi-meet jitsi-meet-web-config jitsi-meet-web jicofo jitsi-videobridge
|
||||
```
|
||||
|
||||
Sometimes the following packages will fail to uninstall properly:
|
||||
|
||||
36
flow-typed/npm/react-i18next_v2.x.x.js
vendored
@@ -1,36 +0,0 @@
|
||||
// flow-typed signature: 57cf34196930be78935a42e5c8ac3cb6
|
||||
// flow-typed version: ae6284e7b7/react-i18next_v2.x.x/flow_>=v0.36.x_<=v0.39.x
|
||||
|
||||
declare module 'react-i18next' {
|
||||
declare type TFunction = (key?: ?string, data?: ?Object) => string;
|
||||
declare type Locales = string | Array<string>;
|
||||
|
||||
declare type StatelessComponent<P> = (props: P) => ?React$Element<any>;
|
||||
|
||||
declare type Comp<P> = StatelessComponent<P> | Class<React$Component<*, P, *>>;
|
||||
|
||||
declare type Translator<OP, P> = {
|
||||
(component: StatelessComponent<P>): Class<React$Component<void, OP, void>>;
|
||||
<Def, St>(component: Class<React$Component<Def, P, St>>): Class<React$Component<Def, OP, St>>;
|
||||
}
|
||||
|
||||
declare function translate<OP, P>(locales: Locales): Translator<OP, P>;
|
||||
|
||||
declare type NamespacesProps = {
|
||||
components: Array<Comp<*>>,
|
||||
i18n: { loadNamespaces: Function },
|
||||
};
|
||||
|
||||
declare function loadNamespaces(props: NamespacesProps): Promise<void>;
|
||||
|
||||
declare type ProviderProps = { i18n: Object, children: React$Element<any> };
|
||||
|
||||
declare var I18nextProvider: Class<React$Component<void, ProviderProps, void>>;
|
||||
|
||||
declare type InterpolateProps = {
|
||||
children?: React$Element<any>,
|
||||
className?: string,
|
||||
};
|
||||
|
||||
declare var Interpolate: Class<React$Component<void, InterpolateProps, void>>;
|
||||
}
|
||||
8
flow-typed/npm/redux_v3.x.x.js
vendored
@@ -1,5 +1,5 @@
|
||||
// flow-typed signature: ba132c96664f1a05288f3eb2272a3c35
|
||||
// flow-typed version: c4bbd91cfc/redux_v3.x.x/flow_>=v0.33.x
|
||||
// flow-typed signature: 7f1a115f75043c44385071ea3f33c586
|
||||
// flow-typed version: 358375125e/redux_v3.x.x/flow_>=v0.33.x
|
||||
|
||||
declare module 'redux' {
|
||||
|
||||
@@ -27,6 +27,8 @@ declare module 'redux' {
|
||||
|
||||
declare type Reducer<S, A> = (state: S, action: A) => S;
|
||||
|
||||
declare type CombinedReducer<S, A> = (state: $Shape<S> & {} | void, action: A) => S;
|
||||
|
||||
declare type Middleware<S, A> =
|
||||
(api: MiddlewareAPI<S, A>) =>
|
||||
(next: Dispatch<A>) => Dispatch<A>;
|
||||
@@ -49,7 +51,7 @@ declare module 'redux' {
|
||||
declare function bindActionCreators<A, C: ActionCreator<A, any>>(actionCreator: C, dispatch: Dispatch<A>): C;
|
||||
declare function bindActionCreators<A, K, C: ActionCreators<K, A>>(actionCreators: C, dispatch: Dispatch<A>): C;
|
||||
|
||||
declare function combineReducers<O: Object, A>(reducers: O): Reducer<$ObjMap<O, <S>(r: Reducer<S, any>) => S>, A>;
|
||||
declare function combineReducers<O: Object, A>(reducers: O): CombinedReducer<$ObjMap<O, <S>(r: Reducer<S, any>) => S>, A>;
|
||||
|
||||
declare function compose<S, A>(...fns: Array<StoreEnhancer<S, A>>): Function;
|
||||
|
||||
|
||||
BIN
fonts/jitsi.eot
Normal file → Executable file
5
fonts/jitsi.svg
Normal file → Executable file
@@ -7,6 +7,7 @@
|
||||
<font-face units-per-em="1024" ascent="1024" descent="0" />
|
||||
<missing-glyph horiz-adv-x="1024" />
|
||||
<glyph unicode=" " d="" />
|
||||
<glyph unicode="" glyph-name="phone" d="M282 564c62-120 162-220 282-282l94 94c12 12 30 16 44 10 48-16 100-24 152-24 24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44z" />
|
||||
<glyph unicode="" glyph-name="presentation" horiz-adv-x="1088" d="M952.495 1019.065h-818.689c-72.81 0-132.183-60.63-132.183-135.162v-750.719c0-74.473 59.372-135.101 132.183-135.101h818.686c72.936 0 132.314 60.625 132.314 135.101v750.722c0.003 74.532-59.378 135.159-132.311 135.159zM946.346 139.651h-806.14v737.822h806.015l0.126-737.822zM685.753 738.544h216.911v-566.758h-216.911v566.758zM428.672 610.002h216.911v-438.216h-216.911v438.216zM172.339 481.46h216.161v-309.677h-216.161v309.677z" />
|
||||
<glyph unicode="" glyph-name="recDisable" horiz-adv-x="1140" d="M1123.444 1003.015c-23.593 26.481-64.131 28.989-90.74 5.395l-1008.269-893.436c-26.609-23.468-28.991-64.131-5.46-90.676 12.674-14.306 30.308-21.649 48.126-21.649 15.123 0 30.372 5.401 42.544 16.195l130.045 115.22c90.743-81.844 210.569-132.165 342.473-132.101 282.816 0.061 510.913 227.969 511.287 510.972 0.126 109.934-34.682 211.367-93.499 294.72l118.088 104.625c26.483 23.526 28.997 64.129 5.404 90.735zM944.422 513.818c0.128-200.922-161.896-363.201-362.509-362.952-87.56 0.123-167.573 31.151-230.061 82.569l331.277 293.509v-73.176c1.071-60.993 32.696-92.18 94.944-93.692 61.997 1.512 93.686 32.763 95.131 93.756v41.096h-72.227v-47.499c0.251-4.642-0.564-10.607-2.511-17.949-1.25-3.261-3.448-6.020-6.525-8.093-3.197-2.572-7.845-3.828-13.868-3.828-10.543 0.31-17.132 4.268-19.827 11.921-1.068 3.512-1.947 6.905-2.508 10.163-0.254 2.887-0.377 5.532-0.377 7.786v143.511l42.477 37.634c0.215-0.432 0.452-0.851 0.63-1.303 1.947-6.467 2.762-12.799 2.511-19.076v-36.772h72.227v30.121c-0.246 31.245-9.086 54.699-26.363 70.447l40.711 36.069c35.787-56.055 56.803-122.585 56.867-194.244zM239.795 395.47c-12.613 37.023-19.827 76.557-19.827 117.913-0.19 200.236 161.584 362.009 361.945 362.135 56.853 0 110.313-13.302 158.133-36.398l117.846 104.421c-79.444 50.952-173.758 80.817-275.292 80.948-283.377 0.181-511.354-227.729-511.789-511.675-0.126-79.567 18.636-154.679 51.137-221.882l117.848 104.538zM388.576 690.020h-97.514v-249.057l72.23 64.070v0.689h0.815l117.72 104.418c0 0.564 0.123 0.94 0.123 1.509 0.753 53.898-30.369 80.069-93.374 78.37zM405.959 625.517c1.942-2.767 3.074-6.469 3.323-11.112 0.312-4.452 0.438-9.6 0.438-15.246 0.251-10.916-0.689-19.83-2.949-26.985-2.952-7.594-10.983-11.357-24.159-11.357h-19.325v74.043h15.31c7.842 0 13.865-0.683 18.072-2.19 4.397-1.573 7.468-3.953 9.29-7.153z" />
|
||||
<glyph unicode="" glyph-name="recEnable" horiz-adv-x="1142" d="M581.278 1025.708c284.857-0.19 514.807-230.517 514.427-514.997-0.378-285.047-230.073-514.553-514.869-514.615-284.541-0.062-515.311 230.517-514.933 514.422 0.439 285.936 230.009 515.439 515.375 515.19zM580.579 875.756c-201.764-0.123-364.666-163.032-364.478-364.663 0-202.018 162.524-364.735 364.478-364.984 202.018-0.316 365.174 163.030 365.048 365.423-0.252 201.767-163.156 364.35-365.048 364.224zM287.698 688.907h98.196c63.442 1.767 94.785-24.518 94.027-78.863 0.254-19.081-2.211-34.882-7.456-47.521-6.005-12.508-18.706-21.988-38.167-28.181v-0.819c28.373-6.259 43.031-23.573 43.981-51.946v-57.689c0-11.247 0.254-22.813 0.758-34.756 0.819-12.005 3.033-20.979 6.696-27.043h-71.846c-3.727 6.064-6.128 15.038-7.14 27.043-1.012 11.943-1.454 23.509-1.138 34.756v52.321c0 9.603-2.214 16.553-6.573 20.979-4.675 4.107-12.701 6.19-24.012 6.19h-14.599v-141.291h-72.73v326.82zM360.428 558.861h19.463c13.271 0 21.359 3.794 24.331 11.375 2.276 7.204 3.221 16.304 2.969 27.171 0 5.815-0.126 10.867-0.442 15.418-0.252 4.675-1.392 8.404-3.352 11.247-1.831 3.157-4.926 5.561-9.352 7.14-4.233 1.454-10.299 2.211-18.2 2.211h-15.418v-74.564zM498.372 688.907h162.082v-62.687h-89.35v-65.587h78.103v-62.685h-78.103v-73.11h92.822v-62.749h-165.557v326.818zM682.507 599.999c0.316 31.782 9.416 55.542 27.425 71.407 17.44 15.29 40.185 22.936 68.181 22.936 28.247 0 51.119-7.646 68.623-23 17.82-15.798 26.92-39.623 27.171-71.407v-30.333h-72.73v37.031c0.254 6.192-0.57 12.639-2.527 19.209-1.264 3.157-3.475 5.938-6.573 8.214-3.221 1.515-7.898 2.404-13.964 2.404-10.615-0.316-17.249-3.855-19.967-10.618-2.211-6.573-3.223-13.017-2.907-19.209v-161.956c0-2.273 0.126-4.865 0.38-7.772 0.568-3.411 1.454-6.824 2.527-10.233 2.717-7.775 9.352-11.756 19.967-12.007 6.067 0 10.744 1.261 13.964 3.791 3.098 2.15 5.309 4.867 6.573 8.216 1.96 7.33 2.782 13.33 2.527 18.007v47.837h72.73v-41.328c-1.451-61.547-33.364-93.015-95.794-94.469-62.685 1.454-94.53 32.922-95.607 94.343v148.937z" />
|
||||
@@ -20,7 +21,6 @@
|
||||
<glyph unicode="" glyph-name="chat" d="M854 342v512h-684v-598l86 86h598zM854 938c46 0 84-38 84-84v-512c0-46-38-86-84-86h-598l-170-170v768c0 46 38 84 84 84h684z" />
|
||||
<glyph unicode="" glyph-name="edit" d="M884 724l-78-78-160 160 78 78c16 16 44 16 60 0l100-100c16-16 16-44 0-60zM128 288l472 472 160-160-472-472h-160v160z" />
|
||||
<glyph unicode="" glyph-name="share-doc" d="M554 640h236l-236 234v-234zM682 426v86h-340v-86h340zM682 256v86h-340v-86h340zM598 938l256-256v-512c0-46-40-84-86-84h-512c-46 0-86 38-86 84l2 684c0 46 38 84 84 84h342z" />
|
||||
<glyph unicode="" glyph-name="telephone" d="M854 362c24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44l-94-94c62-122 162-220 282-282l94 94c12 12 30 14 44 10 48-16 98-24 152-24zM854 810v44h-44v-44h44zM768 896h128v-128h-86v-86h-42v214zM640 810v-128h-128v44h86v42h-86v128h128v-42h-86v-44h86zM726 896v-214h-44v214h44z" />
|
||||
<glyph unicode="" glyph-name="star-full" d="M512 288l-264-160 70 300-232 202 306 26 120 282 120-282 306-26-232-202 70-300z" />
|
||||
<glyph unicode="" glyph-name="full-screen" d="M598 810h212v-212h-84v128h-128v84zM726 298v128h84v-212h-212v84h128zM214 598v212h212v-84h-128v-128h-84zM298 426v-128h128v-84h-212v212h84z" />
|
||||
<glyph unicode="" glyph-name="exit-full-screen" d="M682 682h128v-84h-212v212h84v-128zM598 214v212h212v-84h-128v-128h-84zM342 682v128h84v-212h-212v84h128zM214 342v84h212v-212h-84v128h-128z" />
|
||||
@@ -45,4 +45,7 @@
|
||||
<glyph unicode="" glyph-name="menu-up" d="M512 682l256-256-60-60-196 196-196-196-60 60z" />
|
||||
<glyph unicode="" glyph-name="menu-down" d="M708 658l60-60-256-256-256 256 60 60 196-196z" />
|
||||
<glyph unicode="" glyph-name="switch-camera" d="M640 362l150 150-150 150v-108h-256v108l-150-150 150-150v108h256v-108zM854 854c46 0 84-40 84-86v-512c0-46-38-86-84-86h-684c-46 0-84 40-84 86v512c0 46 38 86 84 86h136l78 84h256l78-84h136z" />
|
||||
<glyph unicode="" glyph-name="visibility" d="M512 640c70 0 128-58 128-128s-58-128-128-128-128 58-128 128 58 128 128 128zM512 298c118 0 214 96 214 214s-96 214-214 214-214-96-214-214 96-214 214-214zM512 832c214 0 396-132 470-320-74-188-256-320-470-320s-396 132-470 320c74 188 256 320 470 320z" />
|
||||
<glyph unicode="" glyph-name="visibility-off" d="M506 640h6c70 0 128-58 128-128v-8zM322 606c-14-28-24-60-24-94 0-118 96-214 214-214 34 0 66 10 94 24l-66 66c-8-2-18-4-28-4-70 0-128 58-128 128 0 10 2 20 4 28zM86 842l54 54 756-756-54-54c-47.968 47.365-96.266 94.401-144 142-58-24-120-36-186-36-214 0-396 132-470 320 34 84 90 156 160 212-39.017 38.983-77.307 78.693-116 118zM512 726c-28 0-54-6-78-16l-92 92c52 20 110 30 170 30 214 0 394-132 468-320-32-80-82-148-146-202l-124 124c10 24 16 50 16 78 0 118-96 214-214 214z" />
|
||||
<glyph unicode="" glyph-name="dialpad" d="M512 982c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 810c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86zM256 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM256 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM256 982c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 214c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86z" />
|
||||
</font></defs></svg>
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 17 KiB |
BIN
fonts/jitsi.ttf
Normal file → Executable file
BIN
fonts/jitsi.woff
Normal file → Executable file
794
fonts/selection.json
Normal file → Executable file
9
images/countries/au.svg
Executable file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
|
||||
<g stroke-width="1pt">
|
||||
<path fill="#006" d="M0 0h640v480H0z"/>
|
||||
<path d="M0 0v27.95L307.037 250h38.647v-27.95L38.647 0H0zm345.684 0v27.95L38.647 250H0v-27.95L307.037 0h38.647z" fill="#fff"/>
|
||||
<path d="M144.035 0v250h57.614V0h-57.615zM0 83.333v83.333h345.684V83.333H0z" fill="#fff"/>
|
||||
<path d="M0 100v50h345.684v-50H0zM155.558 0v250h34.568V0h-34.568zM0 250l115.228-83.334h25.765L25.765 250H0zM0 0l115.228 83.333H89.463L0 18.633V0zm204.69 83.333L319.92 0h25.764L230.456 83.333H204.69zM345.685 250l-115.228-83.334h25.765l89.464 64.7V250z" fill="#c00"/>
|
||||
<path d="M299.762 392.523l-43.653 3.795 6.013 43.406-30.187-31.764-30.186 31.764 6.014-43.406-43.653-3.795 37.68-22.364-24.244-36.495 40.97 15.514 13.42-41.713 13.42 41.712 40.97-15.515-24.242 36.494m224.444 62.372l-10.537-15.854 17.81 6.742 5.824-18.125 5.825 18.126 17.807-6.742-10.537 15.854 16.37 9.718-18.965 1.65 2.616 18.85-13.116-13.793-13.117 13.794 2.616-18.85-18.964-1.65m16.368-291.815l-10.537-15.856 17.81 6.742 5.824-18.122 5.825 18.12 17.807-6.74-10.537 15.855 16.37 9.717-18.965 1.65 2.616 18.85-13.116-13.793-13.117 13.794 2.616-18.85-18.964-1.65m-89.418 104.883l-10.537-15.853 17.808 6.742 5.825-18.125 5.825 18.125 17.808-6.742-10.536 15.853 16.37 9.72-18.965 1.65 2.615 18.85-13.117-13.795-13.117 13.795 2.617-18.85-18.964-1.65m216.212-37.929l-10.558-15.854 17.822 6.742 5.782-18.125 5.854 18.125 17.772-6.742-10.508 15.854 16.362 9.718-18.97 1.65 2.608 18.85-13.118-13.793-13.117 13.793 2.61-18.85-18.936-1.65m-22.251 73.394l-10.367 6.425 2.914-11.84-9.316-7.863 12.165-.896 4.605-11.29 4.606 11.29 12.165.897-9.317 7.863 2.912 11.84" fill-rule="evenodd" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
6
images/countries/ca.svg
Executable file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
|
||||
<g transform="translate(74.118) scale(.9375)">
|
||||
<path fill="#fff" d="M81.137 0h362.276v512H81.137z"/>
|
||||
<path fill="#bf0a30" d="M-100 0H81.138v512H-100zm543.413 0H624.55v512H443.414zM135.31 247.41l-14.067 4.808 65.456 57.446c4.95 14.764-1.72 19.116-5.97 26.86l71.06-9.02-1.85 71.512 14.718-.423-3.21-70.918 71.13 8.432c-4.402-9.297-8.32-14.233-4.247-29.098l65.414-54.426-11.447-4.144c-9.36-7.222 4.044-34.784 6.066-52.178 0 0-38.195 13.135-40.698 6.262l-9.727-18.685-34.747 38.17c-3.796.91-5.413-.6-6.304-3.808l16.053-79.766-25.42 14.297c-2.128.91-4.256.125-5.658-2.355l-24.45-49.06-25.21 50.95c-1.9 1.826-3.803 2.037-5.382.796l-24.204-13.578 14.53 79.143c-1.156 3.14-3.924 4.025-7.18 2.324l-33.216-37.737c-4.345 6.962-7.29 18.336-13.033 20.885-5.744 2.387-24.98-4.823-37.873-7.637 4.404 15.895 18.176 42.302 9.46 50.957z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 934 B |
5
images/countries/de.svg
Executable file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
|
||||
<path fill="#ffce00" d="M0 320h640v160.002H0z"/>
|
||||
<path d="M0 0h640v160H0z"/>
|
||||
<path fill="#d00" d="M0 160h640v160H0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 220 B |
7
images/countries/fr.svg
Executable file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
|
||||
<g fill-rule="evenodd" stroke-width="1pt">
|
||||
<path fill="#fff" d="M0 0h640v480H0z"/>
|
||||
<path fill="#00267f" d="M0 0h213.337v480H0z"/>
|
||||
<path fill="#f31830" d="M426.662 0H640v480H426.662z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 301 B |
15
images/countries/gb.svg
Executable file
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill-opacity=".67" d="M-85.333 0h682.67v512h-682.67z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#a)" transform="translate(80) scale(.94)">
|
||||
<g stroke-width="1pt">
|
||||
<path fill="#006" d="M-256 0H768.02v512.01H-256z"/>
|
||||
<path d="M-256 0v57.244l909.535 454.768H768.02V454.77L-141.515 0H-256zM768.02 0v57.243L-141.515 512.01H-256v-57.243L653.535 0H768.02z" fill="#fff"/>
|
||||
<path d="M170.675 0v512.01h170.67V0h-170.67zM-256 170.67v170.67H768.02V170.67H-256z" fill="#fff"/>
|
||||
<path d="M-256 204.804v102.402H768.02V204.804H-256zM204.81 0v512.01h102.4V0h-102.4zM-256 512.01L85.34 341.34h76.324l-341.34 170.67H-256zM-256 0L85.34 170.67H9.016L-256 38.164V0zm606.356 170.67L691.696 0h76.324L426.68 170.67h-76.324zM768.02 512.01L426.68 341.34h76.324L768.02 473.848v38.162z" fill="#c00"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 956 B |
18
images/countries/us.svg
Executable file
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="480" width="640" viewBox="0 0 640 480">
|
||||
<g fill-rule="evenodd" transform="scale(.9375)">
|
||||
<g stroke-width="1pt">
|
||||
<path d="M0 0h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0z" fill="#bd3d44"/>
|
||||
<path d="M0 39.385h972.81V78.77H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0zm0 78.77h972.81v39.385H0z" fill="#fff"/>
|
||||
</g>
|
||||
<path fill="#192f5d" d="M0 0h389.12v275.69H0z"/>
|
||||
<g fill="#fff">
|
||||
<path d="M32.427 11.8l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458zM64.855 39.37l3.54 10.896h11.458L70.583 57l3.542 10.897-9.27-6.734-9.269 6.734L59.126 57l-9.269-6.734h11.458zm64.852 0l3.54 10.896h11.457L135.435 57l3.54 10.897-9.268-6.734-9.27 6.734L123.978 57l-9.27-6.734h11.458zm64.855 0l3.54 10.896h11.458L200.29 57l3.541 10.897-9.27-6.734-9.268 6.734L188.833 57l-9.269-6.734h11.457zm64.855 0l3.54 10.896h11.458L265.145 57l3.541 10.897-9.269-6.734-9.27 6.734L253.69 57l-9.27-6.734h11.458zm64.852 0l3.54 10.896h11.457L329.997 57l3.54 10.897-9.268-6.734-9.27 6.734L318.54 57l-9.27-6.734h11.458zM32.427 66.939l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458zM64.855 94.508l3.54 10.897h11.458l-9.27 6.734 3.542 10.897-9.27-6.734-9.269 6.734 3.54-10.897-9.269-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.27-6.734-9.268 6.734 3.54-10.897-9.269-6.734h11.457zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.269-6.734-9.27 6.734 3.542-10.897-9.27-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458zM32.427 122.078l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458zM64.855 149.647l3.54 10.897h11.458l-9.27 6.734 3.542 10.897-9.27-6.734-9.269 6.734 3.54-10.897-9.269-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.27-6.734-9.268 6.734 3.54-10.897-9.269-6.734h11.457zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.269-6.734-9.27 6.734 3.542-10.897-9.27-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458z"/>
|
||||
<g>
|
||||
<path d="M32.427 177.217l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458zM64.855 204.786l3.54 10.897h11.458l-9.27 6.734 3.542 10.897-9.27-6.734-9.269 6.734 3.54-10.897-9.269-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.27-6.734-9.268 6.734 3.54-10.897-9.269-6.734h11.457zm64.855 0l3.54 10.897h11.458l-9.27 6.734 3.541 10.897-9.269-6.734-9.27 6.734 3.542-10.897-9.27-6.734h11.458zm64.852 0l3.54 10.897h11.457l-9.269 6.734 3.54 10.897-9.268-6.734-9.27 6.734 3.541-10.897-9.27-6.734h11.458z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M32.427 232.356l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.853 0l3.541 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735H93.74zm64.856 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.269 6.734 3.54-10.896-9.269-6.735h11.458zm64.852 0l3.54 10.896h11.457l-9.269 6.735 3.54 10.896-9.268-6.734-9.27 6.734 3.541-10.896-9.27-6.735h11.458zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.27-6.734-9.268 6.734 3.54-10.896-9.269-6.735h11.457zm64.855 0l3.54 10.896h11.458l-9.27 6.735 3.541 10.896-9.269-6.734-9.27 6.734 3.542-10.896-9.27-6.735h11.458z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.1 KiB |
@@ -4,6 +4,7 @@
|
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!--#include virtual="base.html" -->
|
||||
<!--#include virtual="local.html" -->
|
||||
<script>
|
||||
window.indexLoadedTime = window.performance.now();
|
||||
console.log("(TIME) index.html loaded:\t", indexLoadedTime);
|
||||
@@ -127,9 +128,8 @@
|
||||
'error', loadErrHandler, true /* capture phase type of listener */);
|
||||
</script>
|
||||
<script><!--#include virtual="/config.js" --></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
|
||||
<script src="static/utils.js?v=1"></script>
|
||||
<!--#include virtual="connection_optimization/connection_optimization.html" -->
|
||||
<script src="connection_optimization/do_external_connect.js?v=1"></script>
|
||||
<script src="libs/do_external_connect.min.js?v=1"></script>
|
||||
<script><!--#include virtual="/interface_config.js" --></script>
|
||||
<script><!--#include virtual="/logging_config.js" --></script>
|
||||
<script src="libs/lib-jitsi-meet.min.js?v=139"></script>
|
||||
|
||||
@@ -38,7 +38,7 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
|
||||
//main toolbar
|
||||
'microphone', 'camera', 'desktop', 'invite', 'fullscreen', 'hangup',
|
||||
//extended toolbar
|
||||
'profile', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'sip', 'dialpad', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line
|
||||
'profile', 'contacts', 'chat', 'recording', 'etherpad', 'sharedvideo', 'dialout', 'settings', 'raisehand', 'filmstrip'], // jshint ignore:line
|
||||
/**
|
||||
* Main Toolbar Buttons
|
||||
* All of them should be in TOOLBAR_BUTTONS
|
||||
|
||||
@@ -78,6 +78,8 @@
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
|
||||
@@ -81,6 +81,13 @@
|
||||
remoteGlobalIDString = 832C81801AAF6DEF007FA2F7;
|
||||
remoteInfo = RCTVibration;
|
||||
};
|
||||
0B50D60B1EA7891100B34818 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 5B09C20C78C74A548AAAC1FA /* KCKeepAwake.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 325F5AB71E6763EC00E6CDE4;
|
||||
remoteInfo = "KCKeepAwake-tvOS";
|
||||
};
|
||||
0B8752851E26E54A004C5CAB /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 5B09C20C78C74A548AAAC1FA /* KCKeepAwake.xcodeproj */;
|
||||
@@ -278,7 +285,7 @@
|
||||
0EA8C046B2BF46279796F07D /* libKCKeepAwake.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libKCKeepAwake.a; sourceTree = "<group>"; };
|
||||
139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = "../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj"; sourceTree = "<group>"; };
|
||||
139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = "../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj"; sourceTree = "<group>"; };
|
||||
13B07F961A680F5B00A75B9A /* jitsi-meet-react.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "jitsi-meet-react.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07F961A680F5B00A75B9A /* jitsi-meet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "jitsi-meet.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = app/AppDelegate.h; sourceTree = "<group>"; };
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = app/AppDelegate.m; sourceTree = "<group>"; };
|
||||
13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
|
||||
@@ -296,7 +303,7 @@
|
||||
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; };
|
||||
B30EF2301DC0ED7C00690F45 /* WebRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebRTC.framework; path = "../node_modules/react-native-webrtc/ios/WebRTC.framework"; sourceTree = "<group>"; };
|
||||
B3A9D0241E0481E10009343D /* POSIX.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = POSIX.m; path = app/POSIX.m; sourceTree = "<group>"; };
|
||||
B3B083EB1D4955FF0069CEE7 /* jitsi-meet-react.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "jitsi-meet-react.entitlements"; sourceTree = "<group>"; };
|
||||
B3B083EB1D4955FF0069CEE7 /* jitsi-meet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "jitsi-meet.entitlements"; sourceTree = "<group>"; };
|
||||
B96AF9B6FBC0453798399985 /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = "<group>"; };
|
||||
BF9643811C34FBB300B0BBDF /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
|
||||
BF9643831C34FBBB00B0BBDF /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
|
||||
@@ -394,6 +401,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0B8752861E26E54A004C5CAB /* libKCKeepAwake.a */,
|
||||
0B50D60C1EA7891100B34818 /* libKCKeepAwake-tvOS.a */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -522,7 +530,7 @@
|
||||
children = (
|
||||
13B07FAE1A68108700A75B9A /* app */,
|
||||
B3BA19B71DC6B02F00BCD481 /* Frameworks */,
|
||||
B3B083EB1D4955FF0069CEE7 /* jitsi-meet-react.entitlements */,
|
||||
B3B083EB1D4955FF0069CEE7 /* jitsi-meet.entitlements */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
6956B374CC3C453DB7B8E82D /* Resources */,
|
||||
@@ -534,7 +542,7 @@
|
||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07F961A680F5B00A75B9A /* jitsi-meet-react.app */,
|
||||
13B07F961A680F5B00A75B9A /* jitsi-meet.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -573,9 +581,9 @@
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
13B07F861A680F5B00A75B9A /* jitsi-meet-react */ = {
|
||||
13B07F861A680F5B00A75B9A /* jitsi-meet */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "jitsi-meet-react" */;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "jitsi-meet" */;
|
||||
buildPhases = (
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
@@ -588,9 +596,9 @@
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "jitsi-meet-react";
|
||||
name = "jitsi-meet";
|
||||
productName = "Hello World";
|
||||
productReference = 13B07F961A680F5B00A75B9A /* jitsi-meet-react.app */;
|
||||
productReference = 13B07F961A680F5B00A75B9A /* jitsi-meet.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
@@ -612,7 +620,7 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "jitsi-meet-react" */;
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "jitsi-meet" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = English;
|
||||
hasScannedForEncodings = 0;
|
||||
@@ -687,7 +695,7 @@
|
||||
);
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
13B07F861A680F5B00A75B9A /* jitsi-meet-react */,
|
||||
13B07F861A680F5B00A75B9A /* jitsi-meet */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -728,6 +736,13 @@
|
||||
remoteRef = 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
0B50D60C1EA7891100B34818 /* libKCKeepAwake-tvOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libKCKeepAwake-tvOS.a";
|
||||
remoteRef = 0B50D60B1EA7891100B34818 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
0B8752861E26E54A004C5CAB /* libKCKeepAwake.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
@@ -974,7 +989,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "jitsi-meet-react.entitlements";
|
||||
CODE_SIGN_ENTITLEMENTS = "jitsi-meet.entitlements";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1004,7 +1019,7 @@
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeet.ios;
|
||||
PRODUCT_NAME = "jitsi-meet-react";
|
||||
PRODUCT_NAME = "jitsi-meet";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -1013,7 +1028,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "jitsi-meet-react.entitlements";
|
||||
CODE_SIGN_ENTITLEMENTS = "jitsi-meet.entitlements";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
@@ -1042,7 +1057,7 @@
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = org.jitsi.JitsiMeet.ios;
|
||||
PRODUCT_NAME = "jitsi-meet-react";
|
||||
PRODUCT_NAME = "jitsi-meet";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
@@ -1149,7 +1164,7 @@
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "jitsi-meet-react" */ = {
|
||||
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "jitsi-meet" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
13B07F941A680F5B00A75B9A /* Debug */,
|
||||
@@ -1158,7 +1173,7 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "jitsi-meet-react" */ = {
|
||||
83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "jitsi-meet" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
83CBBA201A601CBA00E9B192 /* Debug */,
|
||||
@@ -29,9 +29,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "jitsi-meet-react.app"
|
||||
BlueprintName = "jitsi-meet-react"
|
||||
ReferencedContainer = "container:jitsi-meet-react.xcodeproj">
|
||||
BuildableName = "jitsi-meet.app"
|
||||
BlueprintName = "jitsi-meet"
|
||||
ReferencedContainer = "container:jitsi-meet.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
@@ -47,9 +47,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "jitsi-meet-react.app"
|
||||
BlueprintName = "jitsi-meet-react"
|
||||
ReferencedContainer = "container:jitsi-meet-react.xcodeproj">
|
||||
BuildableName = "jitsi-meet.app"
|
||||
BlueprintName = "jitsi-meet"
|
||||
ReferencedContainer = "container:jitsi-meet.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
@@ -70,9 +70,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "jitsi-meet-react.app"
|
||||
BlueprintName = "jitsi-meet-react"
|
||||
ReferencedContainer = "container:jitsi-meet-react.xcodeproj">
|
||||
BuildableName = "jitsi-meet.app"
|
||||
BlueprintName = "jitsi-meet"
|
||||
ReferencedContainer = "container:jitsi-meet.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
@@ -89,9 +89,9 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "13B07F861A680F5B00A75B9A"
|
||||
BuildableName = "jitsi-meet-react.app"
|
||||
BlueprintName = "jitsi-meet-react"
|
||||
ReferencedContainer = "container:jitsi-meet-react.xcodeproj">
|
||||
BuildableName = "jitsi-meet.app"
|
||||
BlueprintName = "jitsi-meet"
|
||||
ReferencedContainer = "container:jitsi-meet.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
@@ -2,6 +2,6 @@
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:jitsi-meet-react.xcodeproj">
|
||||
location = "group:jitsi-meet.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
20
lang/languages-vi.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"en": "",
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"es": "",
|
||||
"fr": "",
|
||||
"hy": "",
|
||||
"it": "",
|
||||
"oc": "",
|
||||
"pl": "",
|
||||
"ptBR": "",
|
||||
"ru": "",
|
||||
"sk": "",
|
||||
"sl": "",
|
||||
"sv": "",
|
||||
"tr": "",
|
||||
"zhCN": "",
|
||||
"nb": "",
|
||||
"eo": ""
|
||||
}
|
||||
415
lang/main-vi.json
Normal file
@@ -0,0 +1,415 @@
|
||||
{
|
||||
"contactlist": "",
|
||||
"addParticipants": "",
|
||||
"roomLocked": "",
|
||||
"roomUnlocked": "",
|
||||
"passwordSetRemotely": "",
|
||||
"connectionsettings": "",
|
||||
"poweredby": "",
|
||||
"feedback": "",
|
||||
"inviteUrlDefaultMsg": "",
|
||||
"me": "",
|
||||
"speaker": "",
|
||||
"raisedHand": "",
|
||||
"defaultNickname": "",
|
||||
"defaultLink": "",
|
||||
"callingName": "",
|
||||
"audioOnly": {
|
||||
"audioOnly": "",
|
||||
"featureToggleDisabled": "",
|
||||
"howToDisable": ""
|
||||
},
|
||||
"userMedia": {
|
||||
"react-nativeGrantPermissions": "",
|
||||
"chromeGrantPermissions": "",
|
||||
"androidGrantPermissions": "",
|
||||
"firefoxGrantPermissions": "",
|
||||
"operaGrantPermissions": "",
|
||||
"iexplorerGrantPermissions": "",
|
||||
"safariGrantPermissions": "",
|
||||
"nwjsGrantPermissions": "",
|
||||
"edgeGrantPermissions": ""
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"keyboardShortcuts": "",
|
||||
"raiseHand": "",
|
||||
"pushToTalk": "",
|
||||
"toggleScreensharing": "",
|
||||
"toggleFilmstrip": "",
|
||||
"toggleShortcuts": "",
|
||||
"focusLocal": "",
|
||||
"focusRemote": "",
|
||||
"toggleChat": "",
|
||||
"mute": "",
|
||||
"fullScreen": "",
|
||||
"videoMute": "",
|
||||
"showSpeakerStats": ""
|
||||
},
|
||||
"welcomepage": {
|
||||
"disable": "",
|
||||
"feature1": {
|
||||
"content": "",
|
||||
"title": ""
|
||||
},
|
||||
"feature2": {
|
||||
"content": "",
|
||||
"title": ""
|
||||
},
|
||||
"feature3": {
|
||||
"content": "",
|
||||
"title": ""
|
||||
},
|
||||
"feature4": {
|
||||
"content": "",
|
||||
"title": ""
|
||||
},
|
||||
"feature5": {
|
||||
"content": "",
|
||||
"title": ""
|
||||
},
|
||||
"feature6": {
|
||||
"content": "",
|
||||
"title": ""
|
||||
},
|
||||
"feature7": {
|
||||
"content": "",
|
||||
"title": ""
|
||||
},
|
||||
"feature8": {
|
||||
"content": "",
|
||||
"title": ""
|
||||
},
|
||||
"go": "",
|
||||
"join": "",
|
||||
"privacy": "",
|
||||
"roomname": "",
|
||||
"roomnamePlaceHolder": "",
|
||||
"sendFeedback": "",
|
||||
"terms": ""
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": "",
|
||||
"title": ""
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
"title": "",
|
||||
"text": "",
|
||||
"rejoinKeyTitle": ""
|
||||
},
|
||||
"toolbar": {
|
||||
"audioonly": "",
|
||||
"mute": "",
|
||||
"videomute": "",
|
||||
"authenticate": "",
|
||||
"lock": "",
|
||||
"invite": "",
|
||||
"chat": "",
|
||||
"etherpad": "",
|
||||
"sharedvideo": "",
|
||||
"sharescreen": "",
|
||||
"fullscreen": "",
|
||||
"sip": "",
|
||||
"Settings": "",
|
||||
"hangup": "",
|
||||
"login": "",
|
||||
"logout": "",
|
||||
"dialpad": "",
|
||||
"sharedVideoMutedPopup": "",
|
||||
"micMutedPopup": "",
|
||||
"talkWhileMutedPopup": "",
|
||||
"unableToUnmutePopup": "",
|
||||
"cameraDisabled": "",
|
||||
"micDisabled": "",
|
||||
"filmstrip": "",
|
||||
"profile": "",
|
||||
"raiseHand": ""
|
||||
},
|
||||
"unsupportedBrowser": {
|
||||
"appInstalled": "",
|
||||
"appNotInstalled": "",
|
||||
"downloadApp": "",
|
||||
"joinConversation": "",
|
||||
"startConference": ""
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "",
|
||||
"filmstrip": "",
|
||||
"contactlist": ""
|
||||
},
|
||||
"chat": {
|
||||
"nickname": {
|
||||
"title": "",
|
||||
"popover": ""
|
||||
},
|
||||
"messagebox": ""
|
||||
},
|
||||
"settings": {
|
||||
"title": "",
|
||||
"update": "",
|
||||
"name": "",
|
||||
"startAudioMuted": "",
|
||||
"startVideoMuted": "",
|
||||
"selectCamera": "",
|
||||
"selectMic": "",
|
||||
"selectAudioOutput": "",
|
||||
"followMe": "",
|
||||
"noDevice": "",
|
||||
"cameraAndMic": "",
|
||||
"moderator": "",
|
||||
"password": "",
|
||||
"audioVideo": ""
|
||||
},
|
||||
"profile": {
|
||||
"title": "",
|
||||
"setDisplayNameLabel": "",
|
||||
"setEmailLabel": "",
|
||||
"setEmailInput": ""
|
||||
},
|
||||
"videothumbnail": {
|
||||
"editnickname": "",
|
||||
"moderator": "",
|
||||
"videomute": "",
|
||||
"mute": "",
|
||||
"kick": "",
|
||||
"muted": "",
|
||||
"domute": "",
|
||||
"flip": "",
|
||||
"remoteControl": ""
|
||||
},
|
||||
"connectionindicator": {
|
||||
"header": "",
|
||||
"bitrate": "",
|
||||
"packetloss": "",
|
||||
"resolution": "",
|
||||
"framerate": "",
|
||||
"less": "",
|
||||
"more": "",
|
||||
"address": "",
|
||||
"remoteport": "",
|
||||
"remoteport_plural_undefined": "",
|
||||
"localport": "",
|
||||
"localport_plural_undefined": "",
|
||||
"localaddress": "",
|
||||
"localaddress_plural_undefined": "",
|
||||
"remoteaddress": "",
|
||||
"remoteaddress_plural_undefined": "",
|
||||
"transport": "",
|
||||
"transport_plural_undefined": "",
|
||||
"bandwidth": "",
|
||||
"na": ""
|
||||
},
|
||||
"notify": {
|
||||
"disconnected": "",
|
||||
"moderator": "",
|
||||
"connected": "",
|
||||
"somebody": "",
|
||||
"me": "",
|
||||
"focus": "",
|
||||
"focusFail": "",
|
||||
"grantedTo": "",
|
||||
"grantedToUnknown": "",
|
||||
"muted": "",
|
||||
"mutedTitle": "",
|
||||
"raisedHand": ""
|
||||
},
|
||||
"dialog": {
|
||||
"add": "",
|
||||
"allow": "",
|
||||
"kickMessage": "",
|
||||
"popupError": "",
|
||||
"passwordErrorTitle": "",
|
||||
"passwordError": "",
|
||||
"passwordError2": "",
|
||||
"connectError": "",
|
||||
"connectErrorWithMsg": "",
|
||||
"incorrectPassword": "",
|
||||
"connecting": "",
|
||||
"copy": "",
|
||||
"error": "",
|
||||
"createPassword": "",
|
||||
"detectext": "",
|
||||
"failtoinstall": "",
|
||||
"failedpermissions": "",
|
||||
"conferenceReloadTitle": "",
|
||||
"conferenceReloadMsg": "",
|
||||
"conferenceDisconnectTitle": "",
|
||||
"conferenceDisconnectMsg": "",
|
||||
"rejoinNow": "",
|
||||
"maxUsersLimitReached": "",
|
||||
"lockTitle": "",
|
||||
"lockMessage": "",
|
||||
"warning": "",
|
||||
"passwordNotSupported": "",
|
||||
"internalErrorTitle": "",
|
||||
"internalError": "",
|
||||
"unableToSwitch": "",
|
||||
"SLDFailure": "",
|
||||
"SRDFailure": "",
|
||||
"oops": "",
|
||||
"currentPassword": "",
|
||||
"passwordLabel": "",
|
||||
"defaultError": "",
|
||||
"passwordRequired": "",
|
||||
"Ok": "",
|
||||
"done": "",
|
||||
"Remove": "",
|
||||
"removePassword": "",
|
||||
"shareVideoTitle": "",
|
||||
"shareVideoLinkError": "",
|
||||
"removeSharedVideoTitle": "",
|
||||
"removeSharedVideoMsg": "",
|
||||
"alreadySharedVideoMsg": "",
|
||||
"WaitingForHost": "",
|
||||
"WaitForHostMsg": "",
|
||||
"IamHost": "",
|
||||
"Cancel": "",
|
||||
"Submit": "",
|
||||
"retry": "",
|
||||
"logoutTitle": "",
|
||||
"logoutQuestion": "",
|
||||
"sessTerminated": "",
|
||||
"hungUp": "",
|
||||
"joinAgain": "",
|
||||
"Share": "",
|
||||
"Save": "",
|
||||
"recording": "",
|
||||
"recordingToken": "",
|
||||
"Dial": "",
|
||||
"sipMsg": "",
|
||||
"passwordCheck": "",
|
||||
"passwordMsg": "",
|
||||
"shareLink": "",
|
||||
"settings1": "",
|
||||
"settings2": "",
|
||||
"settings3": "",
|
||||
"yourPassword": "",
|
||||
"Back": "",
|
||||
"serviceUnavailable": "",
|
||||
"gracefulShutdown": "",
|
||||
"Yes": "",
|
||||
"reservationError": "",
|
||||
"reservationErrorMsg": "",
|
||||
"password": "",
|
||||
"userPassword": "",
|
||||
"token": "",
|
||||
"tokenAuthFailedTitle": "",
|
||||
"tokenAuthFailed": "",
|
||||
"displayNameRequired": "",
|
||||
"enterDisplayName": "",
|
||||
"extensionRequired": "",
|
||||
"firefoxExtensionPrompt": "",
|
||||
"rateExperience": "",
|
||||
"feedbackHelp": "",
|
||||
"feedbackQuestion": "",
|
||||
"thankYou": "",
|
||||
"sorryFeedback": "",
|
||||
"liveStreaming": "",
|
||||
"streamKey": "",
|
||||
"startLiveStreaming": "",
|
||||
"stopStreamingWarning": "",
|
||||
"stopRecordingWarning": "",
|
||||
"stopLiveStreaming": "",
|
||||
"stopRecording": "",
|
||||
"doNotShowWarningAgain": "",
|
||||
"doNotShowMessageAgain": "",
|
||||
"permissionDenied": "",
|
||||
"screenSharingPermissionDeniedError": "",
|
||||
"micErrorPresent": "",
|
||||
"cameraErrorPresent": "",
|
||||
"cameraUnsupportedResolutionError": "",
|
||||
"cameraUnknownError": "",
|
||||
"cameraPermissionDeniedError": "",
|
||||
"cameraNotFoundError": "",
|
||||
"cameraConstraintFailedError": "",
|
||||
"micUnknownError": "",
|
||||
"micPermissionDeniedError": "",
|
||||
"micNotFoundError": "",
|
||||
"micConstraintFailedError": "",
|
||||
"micNotSendingData": "",
|
||||
"cameraNotSendingData": "",
|
||||
"goToStore": "",
|
||||
"externalInstallationTitle": "",
|
||||
"externalInstallationMsg": "",
|
||||
"muteParticipantTitle": "",
|
||||
"muteParticipantBody": "",
|
||||
"muteParticipantButton": "",
|
||||
"remoteControlTitle": "",
|
||||
"remoteControlRequestMessage": "",
|
||||
"remoteControlShareScreenWarning": "",
|
||||
"remoteControlDeniedMessage": "",
|
||||
"remoteControlAllowedMessage": "",
|
||||
"remoteControlErrorMessage": "",
|
||||
"remoteControlStopMessage": "",
|
||||
"close": "",
|
||||
"shareYourScreen": "",
|
||||
"yourEntireScreen": "",
|
||||
"applicationWindow": ""
|
||||
},
|
||||
"email": {
|
||||
"sharedKey": "",
|
||||
"subject": "",
|
||||
"body": "",
|
||||
"and": ""
|
||||
},
|
||||
"connection": {
|
||||
"ERROR": "",
|
||||
"CONNECTING": "",
|
||||
"RECONNECTING": "",
|
||||
"CONNFAIL": "",
|
||||
"AUTHENTICATING": "",
|
||||
"AUTHFAIL": "",
|
||||
"CONNECTED": "",
|
||||
"DISCONNECTED": "",
|
||||
"DISCONNECTING": "",
|
||||
"ATTACHED": ""
|
||||
},
|
||||
"recording": {
|
||||
"pending": "",
|
||||
"on": "",
|
||||
"off": "",
|
||||
"failedToStart": "",
|
||||
"buttonTooltip": "",
|
||||
"error": "",
|
||||
"unavailable": ""
|
||||
},
|
||||
"liveStreaming": {
|
||||
"pending": "",
|
||||
"on": "",
|
||||
"off": "",
|
||||
"unavailable": "",
|
||||
"failedToStart": "",
|
||||
"buttonTooltip": "",
|
||||
"streamIdRequired": "",
|
||||
"streamIdHelp": "",
|
||||
"error": "",
|
||||
"busy": ""
|
||||
},
|
||||
"speakerStats": {
|
||||
"hours": "",
|
||||
"minutes": "",
|
||||
"name": "",
|
||||
"seconds": "",
|
||||
"speakerStats": "",
|
||||
"speakerTime": ""
|
||||
},
|
||||
"deviceSelection": {
|
||||
"deviceSettings": "",
|
||||
"noPermission": "",
|
||||
"previewUnavailable": "",
|
||||
"selectADevice": "",
|
||||
"testAudio": ""
|
||||
},
|
||||
"invite": {
|
||||
"addPassword": "",
|
||||
"dialInNumbers": "",
|
||||
"errorFetchingNumbers": "",
|
||||
"hidePassword": "",
|
||||
"inviteTo": "",
|
||||
"loadingNumbers": "",
|
||||
"locked": "",
|
||||
"noNumbers": "",
|
||||
"numbersDisabled": "",
|
||||
"showPassword": "",
|
||||
"unlocked": ""
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,10 @@
|
||||
"defaultNickname": "ex. Jane Pink",
|
||||
"defaultLink": "e.g. __url__",
|
||||
"callingName": "__name__",
|
||||
"audioOnly": {
|
||||
"audioOnly": "Audio only",
|
||||
"featureToggleDisabled": "Toggling of __feature__ is disabled while in audio only mode"
|
||||
},
|
||||
"userMedia": {
|
||||
"react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
@@ -92,6 +96,7 @@
|
||||
"rejoinKeyTitle": "Rejoin"
|
||||
},
|
||||
"toolbar": {
|
||||
"audioonly": "Enable / Disable audio only mode (saves bandwidth)",
|
||||
"mute": "Mute / Unmute",
|
||||
"videomute": "Start / Stop camera",
|
||||
"authenticate": "Authenticate",
|
||||
@@ -149,12 +154,10 @@
|
||||
"selectAudioOutput": "Audio output",
|
||||
"followMe": "Everyone follows me",
|
||||
"noDevice": "None",
|
||||
"noPermission": "Permission to use device is not granted",
|
||||
"cameraAndMic": "Camera and microphone",
|
||||
"moderator": "MODERATOR",
|
||||
"password": "SET PASSWORD",
|
||||
"audioVideo": "AUDIO AND VIDEO",
|
||||
"setPasswordLabel": "Lock your room with a password."
|
||||
"audioVideo": "AUDIO AND VIDEO"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profile",
|
||||
@@ -214,6 +217,7 @@
|
||||
},
|
||||
"dialog": {
|
||||
"add": "Add",
|
||||
"allow": "Allow",
|
||||
"kickMessage": "Ouch! You have been kicked out of the meet!",
|
||||
"popupError": "Your browser is blocking popup windows from this site. Please enable popups in your browser's security settings and try again.",
|
||||
"passwordErrorTitle": "Password Error",
|
||||
@@ -225,8 +229,6 @@
|
||||
"connecting": "Connecting",
|
||||
"copy": "Copy",
|
||||
"error": "Error",
|
||||
"roomLocked": "This call is locked. New callers must have the link and enter the password to join",
|
||||
"addPassword": "Add a password",
|
||||
"createPassword": "Create password",
|
||||
"detectext": "Error when trying to detect desktopsharing extension.",
|
||||
"failtoinstall": "Failed to install desktop sharing extension",
|
||||
@@ -275,8 +277,6 @@
|
||||
"Save": "Save",
|
||||
"recording": "Recording",
|
||||
"recordingToken": "Enter recording token",
|
||||
"Dial": "Dial",
|
||||
"sipMsg": "Enter SIP number",
|
||||
"passwordCheck": "Are you sure you would like to remove your password?",
|
||||
"passwordMsg": "Set a password to lock your room",
|
||||
"shareLink": "Share the link to the call",
|
||||
@@ -321,11 +321,11 @@
|
||||
"cameraUnknownError": "Cannot use camera for a unknown reason.",
|
||||
"cameraPermissionDeniedError": "You have not granted permission to use your camera. You can still join the conference but others won't see you. Use the camera button in the address bar to fix this.",
|
||||
"cameraNotFoundError": "Camera was not found.",
|
||||
"cameraConstraintFailedError": "Yor camera does not satisfy some of required constraints.",
|
||||
"cameraConstraintFailedError": "Your camera does not satisfy some of the required constraints.",
|
||||
"micUnknownError": "Cannot use microphone for a unknown reason.",
|
||||
"micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
|
||||
"micNotFoundError": "Microphone was not found.",
|
||||
"micConstraintFailedError": "Yor microphone does not satisfy some of required constraints.",
|
||||
"micConstraintFailedError": "Your microphone does not satisfy some of the required constraints.",
|
||||
"micNotSendingData": "We are unable to access your microphone. Please select another device from the settings menu or try to restart the application.",
|
||||
"cameraNotSendingData": "We are unable to access your camera. Please check if another application is using this device, select another device from the settings menu or try to restart the application.",
|
||||
"goToStore": "Go to the webstore",
|
||||
@@ -334,7 +334,9 @@
|
||||
"muteParticipantTitle": "Mute this participant?",
|
||||
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
|
||||
"muteParticipantButton": "Mute",
|
||||
"remoteControlTitle": "Remote Control",
|
||||
"remoteControlTitle": "Remote desktop control",
|
||||
"remoteControlRequestMessage": "Will you allow __user__ to remotely control your desktop?",
|
||||
"remoteControlShareScreenWarning": "Note that if you press \"Allow\" you will share your screen!",
|
||||
"remoteControlDeniedMessage": "__user__ rejected your remote control request!",
|
||||
"remoteControlAllowedMessage": "__user__ accepted your remote control request!",
|
||||
"remoteControlErrorMessage": "An error occurred while trying to request remote control permissions from __user__!",
|
||||
@@ -425,8 +427,34 @@
|
||||
},
|
||||
"deviceSelection": {
|
||||
"deviceSettings": "Device settings",
|
||||
"noOtherDevices": "No other devices available",
|
||||
"noPermission": "Permission not granted",
|
||||
"previewUnavailable": "Preview unavailable",
|
||||
"selectADevice": "Select a device",
|
||||
"testAudio": "Test sound"
|
||||
},
|
||||
"invite": {
|
||||
"addPassword": "Add password",
|
||||
"callNumber": "Call __number__",
|
||||
"enterId": "Enter Meeting ID: __meetingId__ following by # to dial in from a phone",
|
||||
"howToDialIn": "To dial in, use one of the following numbers and meeting ID",
|
||||
"hidePassword": "Hide password",
|
||||
"inviteTo": "Invite people to __conferenceName__",
|
||||
"invitedYouTo": "__userName__ has invited you to the __meetingUrl__ conference",
|
||||
"locked": "This call is locked. New callers must have the link and enter the password to join.",
|
||||
"showPassword": "Show password",
|
||||
"unlocked": "This call is unlocked. Any new caller with the link may join the call."
|
||||
},
|
||||
"videoStatus": {
|
||||
"hd": "HD",
|
||||
"hdVideo": "HD video",
|
||||
"sd": "SD",
|
||||
"sdVideo": "SD video"
|
||||
},
|
||||
"dialOut": {
|
||||
"dial": "Dial",
|
||||
"dialOut": "Call a phone number",
|
||||
"statusMessage": "is now __status__",
|
||||
"enterPhone": "Enter phone number",
|
||||
"phoneNotAllowed": "Oh, we don't support that destination yet! Sorry!"
|
||||
}
|
||||
}
|
||||
|
||||
0
local.html
Normal file
@@ -5,5 +5,6 @@ var loggingConfig = { // eslint-disable-line no-unused-vars
|
||||
// Option to disable LogCollector (which stores the logs on CallStats)
|
||||
//disableLogCollector: true,
|
||||
// Logging level adjustments for verbose modules:
|
||||
'modules/xmpp/strophe.util.js': 'log'
|
||||
'modules/xmpp/strophe.util.js': 'log',
|
||||
'modules/statistics/CallStats.js': 'info'
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
/* global APP, getConfigParamsFromUrl */
|
||||
|
||||
import postisInit from 'postis';
|
||||
|
||||
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
|
||||
import { getJitsiMeetTransport } from '../transport';
|
||||
|
||||
import { API_ID } from './constants';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* List of the available commands.
|
||||
@@ -18,21 +19,11 @@ let commands = {};
|
||||
let initialScreenSharingState = false;
|
||||
|
||||
/**
|
||||
* JitsiMeetExternalAPI id - unique for a webpage.
|
||||
* The transport instance used for communication with external apps.
|
||||
*
|
||||
* @type {Transport}
|
||||
*/
|
||||
const jitsiMeetExternalApiId
|
||||
= getConfigParamsFromUrl().jitsi_meet_external_api_id;
|
||||
|
||||
/**
|
||||
* Postis instance. Used to communicate with the external application. If
|
||||
* undefined, then API is disabled.
|
||||
*/
|
||||
let postis;
|
||||
|
||||
/**
|
||||
* Object that will execute sendMessage.
|
||||
*/
|
||||
const target = window.opener || window.parent;
|
||||
const transport = getJitsiMeetTransport();
|
||||
|
||||
/**
|
||||
* Initializes supported commands.
|
||||
@@ -51,12 +42,17 @@ function initCommands() {
|
||||
'toggle-share-screen': toggleScreenSharing,
|
||||
'video-hangup': () => APP.conference.hangup(),
|
||||
'email': APP.conference.changeLocalEmail,
|
||||
'avatar-url': APP.conference.changeLocalAvatarUrl,
|
||||
'remote-control-event':
|
||||
event => APP.remoteControl.onRemoteControlAPIEvent(event)
|
||||
'avatar-url': APP.conference.changeLocalAvatarUrl
|
||||
};
|
||||
Object.keys(commands).forEach(
|
||||
key => postis.listen(key, args => commands[key](...args)));
|
||||
transport.on('event', ({ data, name }) => {
|
||||
if (name && commands[name]) {
|
||||
commands[name](...data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,25 +68,13 @@ function onDesktopSharingEnabledChanged(enabled = false) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends message to the external application.
|
||||
*
|
||||
* @param {Object} message - The message to be sent.
|
||||
* @returns {void}
|
||||
*/
|
||||
function sendMessage(message) {
|
||||
if (postis) {
|
||||
postis.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the API should be enabled or not.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldBeEnabled() {
|
||||
return typeof jitsiMeetExternalApiId === 'number';
|
||||
return typeof API_ID === 'number';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,21 +90,6 @@ function toggleScreenSharing() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends event object to the external application that has been subscribed for
|
||||
* that event.
|
||||
*
|
||||
* @param {string} name - The name event.
|
||||
* @param {Object} object - Data associated with the event.
|
||||
* @returns {void}
|
||||
*/
|
||||
function triggerEvent(name, object) {
|
||||
sendMessage({
|
||||
method: name,
|
||||
params: object
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements API class that communicates with external API class and provides
|
||||
* interface to access Jitsi Meet features by external applications that embed
|
||||
@@ -137,47 +106,49 @@ class API {
|
||||
* module.
|
||||
* @returns {void}
|
||||
*/
|
||||
init(options = {}) {
|
||||
if (!shouldBeEnabled() && !options.forceEnable) {
|
||||
init({ forceEnable } = {}) {
|
||||
if (!shouldBeEnabled() && !forceEnable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!postis) {
|
||||
APP.conference.addListener(
|
||||
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
|
||||
onDesktopSharingEnabledChanged);
|
||||
this._initPostis();
|
||||
}
|
||||
/**
|
||||
* Current status (enabled/disabled) of API.
|
||||
*
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._enabled = true;
|
||||
|
||||
APP.conference.addListener(
|
||||
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
|
||||
onDesktopSharingEnabledChanged);
|
||||
|
||||
initCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes postis library.
|
||||
* Sends event to the external application.
|
||||
*
|
||||
* @param {Object} event - The event to be sent.
|
||||
* @returns {void}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_initPostis() {
|
||||
const postisOptions = {
|
||||
window: target
|
||||
};
|
||||
|
||||
if (typeof jitsiMeetExternalApiId === 'number') {
|
||||
postisOptions.scope
|
||||
= `jitsi_meet_external_api_${jitsiMeetExternalApiId}`;
|
||||
_sendEvent(event = {}) {
|
||||
if (this._enabled) {
|
||||
transport.sendEvent(event);
|
||||
}
|
||||
postis = postisInit(postisOptions);
|
||||
initCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that message was sent.
|
||||
*
|
||||
* @param {string} body - Message body.
|
||||
* @param {string} message - Message body.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifySendingChatMessage(body) {
|
||||
triggerEvent('outgoing-message', { 'message': body });
|
||||
notifySendingChatMessage(message) {
|
||||
this._sendEvent({
|
||||
name: 'outgoing-message',
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,21 +158,18 @@ class API {
|
||||
* @param {Object} options - Object with the message properties.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyReceivedChatMessage(options = {}) {
|
||||
const { id, nick, body, ts } = options;
|
||||
|
||||
notifyReceivedChatMessage({ body, id, nick, ts } = {}) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerEvent(
|
||||
'incoming-message',
|
||||
{
|
||||
'from': id,
|
||||
'message': body,
|
||||
'nick': nick,
|
||||
'stamp': ts
|
||||
});
|
||||
this._sendEvent({
|
||||
name: 'incoming-message',
|
||||
from: id,
|
||||
message: body,
|
||||
nick,
|
||||
stamp: ts
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,7 +180,10 @@ class API {
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyUserJoined(id) {
|
||||
triggerEvent('participant-joined', { id });
|
||||
this._sendEvent({
|
||||
name: 'participant-joined',
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,7 +194,10 @@ class API {
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyUserLeft(id) {
|
||||
triggerEvent('participant-left', { id });
|
||||
this._sendEvent({
|
||||
name: 'participant-left',
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,39 +205,43 @@ class API {
|
||||
* nickname.
|
||||
*
|
||||
* @param {string} id - User id.
|
||||
* @param {string} displayName - User nickname.
|
||||
* @param {string} displayname - User nickname.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyDisplayNameChanged(id, displayName) {
|
||||
triggerEvent(
|
||||
'display-name-change',
|
||||
{
|
||||
displayname: displayName,
|
||||
id
|
||||
});
|
||||
notifyDisplayNameChanged(id, displayname) {
|
||||
this._sendEvent({
|
||||
name: 'display-name-change',
|
||||
displayname,
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that the conference has
|
||||
* been joined.
|
||||
*
|
||||
* @param {string} room - The room name.
|
||||
* @param {string} roomName - The room name.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyConferenceJoined(room) {
|
||||
triggerEvent('video-conference-joined', { roomName: room });
|
||||
notifyConferenceJoined(roomName) {
|
||||
this._sendEvent({
|
||||
name: 'video-conference-joined',
|
||||
roomName
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that user changed their
|
||||
* nickname.
|
||||
*
|
||||
* @param {string} room - User id.
|
||||
* @param {string} displayName - User nickname.
|
||||
* @param {string} roomName - User id.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyConferenceLeft(room) {
|
||||
triggerEvent('video-conference-left', { roomName: room });
|
||||
notifyConferenceLeft(roomName) {
|
||||
this._sendEvent({
|
||||
name: 'video-conference-left',
|
||||
roomName
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,32 +251,17 @@ class API {
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyReadyToClose() {
|
||||
triggerEvent('video-ready-to-close', {});
|
||||
this._sendEvent({ name: 'video-ready-to-close' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends remote control event.
|
||||
*
|
||||
* @param {RemoteControlEvent} event - The remote control event.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendRemoteControlEvent(event) {
|
||||
sendMessage({
|
||||
method: 'remote-control-event',
|
||||
params: event
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listeners.
|
||||
* Disposes the allocated resources.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
dispose() {
|
||||
if (postis) {
|
||||
postis.destroy();
|
||||
postis = undefined;
|
||||
|
||||
if (this._enabled) {
|
||||
this._enabled = false;
|
||||
APP.conference.removeListener(
|
||||
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
|
||||
onDesktopSharingEnabledChanged);
|
||||
|
||||
13
modules/API/constants.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// XXX The function parseURLParams is exported by the feature base/config (as
|
||||
// defined in the terminology of react/). However, this file is (very likely)
|
||||
// bundled in external_api in addition to app.bundle and, consequently, it is
|
||||
// best to import as little as possible here (rather than the whole feature
|
||||
// base/config) in order to minimize the amount of source code bundled into
|
||||
// multiple bundles.
|
||||
import parseURLParams from '../../react/features/base/config/parseURLParams';
|
||||
|
||||
/**
|
||||
* JitsiMeetExternalAPI id - unique for a webpage.
|
||||
*/
|
||||
export const API_ID
|
||||
= parseURLParams(window.location).jitsi_meet_external_api_id;
|
||||
82
modules/API/external/external_api.js
vendored
@@ -1,5 +1,9 @@
|
||||
import EventEmitter from 'events';
|
||||
import postisInit from 'postis';
|
||||
|
||||
import {
|
||||
PostMessageTransportBackend,
|
||||
Transport
|
||||
} from '../../transport';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
@@ -25,14 +29,14 @@ const commands = {
|
||||
* events expected by jitsi-meet
|
||||
*/
|
||||
const events = {
|
||||
displayNameChange: 'display-name-change',
|
||||
incomingMessage: 'incoming-message',
|
||||
outgoingMessage: 'outgoing-message',
|
||||
participantJoined: 'participant-joined',
|
||||
participantLeft: 'participant-left',
|
||||
readyToClose: 'video-ready-to-close',
|
||||
videoConferenceJoined: 'video-conference-joined',
|
||||
videoConferenceLeft: 'video-conference-left'
|
||||
'display-name-change': 'displayNameChange',
|
||||
'incoming-message': 'incomingMessage',
|
||||
'outgoing-message': 'outgoingMessage',
|
||||
'participant-joined': 'participantJoined',
|
||||
'participant-left': 'participantLeft',
|
||||
'video-ready-to-close': 'readyToClose',
|
||||
'video-conference-joined': 'videoConferenceJoined',
|
||||
'video-conference-left': 'videoConferenceLeft'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -78,8 +82,8 @@ function configToURLParamsArray(config = {}) {
|
||||
|
||||
for (const key in config) { // eslint-disable-line guard-for-in
|
||||
try {
|
||||
params.push(`${key}=${
|
||||
encodeURIComponent(JSON.stringify(config[key]))}`);
|
||||
params.push(
|
||||
`${key}=${encodeURIComponent(JSON.stringify(config[key]))}`);
|
||||
} catch (e) {
|
||||
console.warn(`Error encoding ${key}: ${e}`);
|
||||
}
|
||||
@@ -180,9 +184,13 @@ class JitsiMeetExternalAPI extends EventEmitter {
|
||||
});
|
||||
this._createIFrame(Math.max(height, MIN_HEIGHT),
|
||||
Math.max(width, MIN_WIDTH));
|
||||
this.postis = postisInit({
|
||||
scope: `jitsi_meet_external_api_${id}`,
|
||||
window: this.frame.contentWindow
|
||||
this._transport = new Transport({
|
||||
backend: new PostMessageTransportBackend({
|
||||
postisOptions: {
|
||||
scope: `jitsi_meet_external_api_${id}`,
|
||||
window: this.frame.contentWindow
|
||||
}
|
||||
})
|
||||
});
|
||||
this.numberOfParticipants = 1;
|
||||
this._setupListeners();
|
||||
@@ -225,17 +233,24 @@ class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
_setupListeners() {
|
||||
this.postis.listen('participant-joined',
|
||||
changeParticipantNumber.bind(null, this, 1));
|
||||
this.postis.listen('participant-left',
|
||||
changeParticipantNumber.bind(null, this, -1));
|
||||
|
||||
for (const eventName in events) { // eslint-disable-line guard-for-in
|
||||
const postisMethod = events[eventName];
|
||||
this._transport.on('event', ({ name, ...data }) => {
|
||||
if (name === 'participant-joined') {
|
||||
changeParticipantNumber(this, 1);
|
||||
} else if (name === 'participant-left') {
|
||||
changeParticipantNumber(this, -1);
|
||||
}
|
||||
|
||||
this.postis.listen(postisMethod,
|
||||
(...args) => this.emit(eventName, ...args));
|
||||
}
|
||||
const eventName = events[name];
|
||||
|
||||
if (eventName) {
|
||||
this.emit(eventName, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,16 +334,11 @@ class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* @returns {void}
|
||||
*/
|
||||
dispose() {
|
||||
const frame = document.getElementById(this.frameName);
|
||||
|
||||
this.postis.destroy();
|
||||
if (frame) {
|
||||
frame.src = 'about:blank';
|
||||
}
|
||||
window.setTimeout(() => {
|
||||
this.iframeHolder.removeChild(this.frame);
|
||||
this._transport.dispose();
|
||||
this.removeAllListeners();
|
||||
if (this.iframeHolder) {
|
||||
this.iframeHolder.parentNode.removeChild(this.iframeHolder);
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,9 +360,9 @@ class JitsiMeetExternalAPI extends EventEmitter {
|
||||
|
||||
return;
|
||||
}
|
||||
this.postis.send({
|
||||
method: commands[name],
|
||||
params: args
|
||||
this._transport.sendEvent({
|
||||
data: args,
|
||||
name: commands[name]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -398,7 +408,7 @@ class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* NOTE: This method is not removed for backward comatability purposes.
|
||||
*/
|
||||
removeEventListener(event) {
|
||||
this.removeListeners(event);
|
||||
this.removeAllListeners(event);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
2
modules/API/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export default from './API';
|
||||
export * from './constants';
|
||||
@@ -31,6 +31,9 @@ import {
|
||||
setAudioMuted,
|
||||
setVideoMuted
|
||||
} from '../../react/features/base/media';
|
||||
import {
|
||||
openDeviceSelectionDialog
|
||||
} from '../../react/features/device-selection';
|
||||
import {
|
||||
checkAutoEnableDesktopSharing,
|
||||
dockToolbox,
|
||||
@@ -40,7 +43,7 @@ import {
|
||||
showDialPadButton,
|
||||
showEtherpadButton,
|
||||
showSharedVideoButton,
|
||||
showSIPCallButton,
|
||||
showDialOutButton,
|
||||
showToolbox
|
||||
} from '../../react/features/toolbox';
|
||||
|
||||
@@ -322,6 +325,10 @@ UI.start = function () {
|
||||
|
||||
$("#videoconference_page").mousemove(debouncedShowToolbar);
|
||||
|
||||
// Initialise the recording module.
|
||||
if (config.enableRecording) {
|
||||
Recording.init(eventEmitter, config.recordingType);
|
||||
}
|
||||
// Initialize side panels
|
||||
SidePanels.init(eventEmitter);
|
||||
} else {
|
||||
@@ -355,9 +362,18 @@ UI.start = function () {
|
||||
|
||||
}
|
||||
|
||||
if(APP.tokenData.callee) {
|
||||
UI.showRingOverlay();
|
||||
}
|
||||
const { callee } = APP.store.getState()['features/jwt'];
|
||||
|
||||
callee && UI.showRingOverlay();
|
||||
};
|
||||
|
||||
/**
|
||||
* Invokes cleanup of any deferred execution within relevant UI modules.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.stopDaemons = () => {
|
||||
VideoLayout.resetLargeVideo();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -528,7 +544,7 @@ UI.onPeerVideoTypeChanged
|
||||
UI.updateLocalRole = isModerator => {
|
||||
VideoLayout.showModeratorIndicator();
|
||||
|
||||
APP.store.dispatch(showSIPCallButton(isModerator));
|
||||
APP.store.dispatch(showDialOutButton(isModerator));
|
||||
APP.store.dispatch(showSharedVideoButton());
|
||||
|
||||
Recording.showRecordingButton(isModerator);
|
||||
@@ -573,6 +589,21 @@ UI.updateUserRole = user => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the user status.
|
||||
*
|
||||
* @param {JitsiParticipant} user - The user which status we need to update.
|
||||
* @param {string} status - The new status.
|
||||
*/
|
||||
UI.updateUserStatus = (user, status) => {
|
||||
let displayName = user.getDisplayName();
|
||||
messageHandler.notify(
|
||||
displayName, '', 'connected', "dialOut.statusMessage",
|
||||
{
|
||||
status: UIUtil.escapeHtml(status)
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles smileys in the chat.
|
||||
*/
|
||||
@@ -704,6 +735,14 @@ UI.setVideoMuted = function (id, muted) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Triggers an update of remote video and large video displays so they may pick
|
||||
* up any state changes that have occurred elsewhere.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.updateAllVideos = () => VideoLayout.updateAllVideos();
|
||||
|
||||
/**
|
||||
* Adds a listener that would be notified on the given type of event.
|
||||
*
|
||||
@@ -1317,7 +1356,10 @@ UI.setMicrophoneButtonEnabled
|
||||
= enabled => APP.store.dispatch(setAudioIconEnabled(enabled));
|
||||
|
||||
UI.showRingOverlay = function () {
|
||||
RingOverlay.show(APP.tokenData.callee, interfaceConfig.DISABLE_RINGING);
|
||||
const { callee } = APP.store.getState()['features/jwt'];
|
||||
|
||||
callee && RingOverlay.show(callee, interfaceConfig.DISABLE_RINGING);
|
||||
|
||||
Filmstrip.toggleFilmstrip(false, false);
|
||||
};
|
||||
|
||||
@@ -1365,13 +1407,30 @@ const UIListeners = new Map([
|
||||
UI.toggleChat
|
||||
], [
|
||||
UIEvents.TOGGLE_SETTINGS,
|
||||
() => UI.toggleSidePanel("settings_container")
|
||||
() => {
|
||||
// Opening of device selection is special-cased as it is a dialog
|
||||
// opened through a button in settings and not directly displayed in
|
||||
// settings itself. As it is not useful to only have a settings menu
|
||||
// with a button to open a dialog, open the dialog directly instead.
|
||||
if (interfaceConfig.SETTINGS_SECTIONS.length === 1
|
||||
&& UIUtil.isSettingEnabled('devices')) {
|
||||
APP.store.dispatch(openDeviceSelectionDialog());
|
||||
} else {
|
||||
UI.toggleSidePanel("settings_container");
|
||||
}
|
||||
}
|
||||
], [
|
||||
UIEvents.TOGGLE_CONTACT_LIST,
|
||||
UI.toggleContactList
|
||||
], [
|
||||
UIEvents.TOGGLE_PROFILE,
|
||||
() => APP.tokenData.isGuest && UI.toggleSidePanel("profile_container")
|
||||
() => {
|
||||
const {
|
||||
isGuest
|
||||
} = APP.store.getState()['features/jwt'];
|
||||
|
||||
isGuest && UI.toggleSidePanel('profile_container');
|
||||
}
|
||||
], [
|
||||
UIEvents.TOGGLE_FILMSTRIP,
|
||||
UI.handleToggleFilmstrip
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/* global APP, config, JitsiMeetJS, Promise */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import { openConnection } from '../../../connection';
|
||||
import { setJWT } from '../../../react/features/jwt';
|
||||
import UIUtil from '../util/UIUtil';
|
||||
|
||||
import LoginDialog from './LoginDialog';
|
||||
import UIUtil from '../util/UIUtil';
|
||||
import {openConnection} from '../../../connection';
|
||||
|
||||
const ConnectionErrors = JitsiMeetJS.errors.connection;
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
let externalAuthWindow;
|
||||
let authRequiredDialog;
|
||||
@@ -73,15 +75,20 @@ function redirectToTokenAuthService(roomName) {
|
||||
* @param room the name fo the conference room.
|
||||
*/
|
||||
function initJWTTokenListener(room) {
|
||||
var listener = function (event) {
|
||||
if (externalAuthWindow !== event.source) {
|
||||
var listener = function ({ data, source }) {
|
||||
if (externalAuthWindow !== source) {
|
||||
logger.warn("Ignored message not coming " +
|
||||
"from external authnetication window");
|
||||
return;
|
||||
}
|
||||
if (event.data && event.data.jwtToken) {
|
||||
config.token = event.data.jwtToken;
|
||||
logger.info("Received JWT token:", config.token);
|
||||
|
||||
let jwt;
|
||||
|
||||
if (data && (jwt = data.jwtToken)) {
|
||||
logger.info("Received JSON Web Token (JWT):", jwt);
|
||||
|
||||
APP.store.dispatch(setJWT(jwt));
|
||||
|
||||
var roomName = room.getName();
|
||||
openConnection({retry: false, roomName: roomName })
|
||||
.then(function (connection) {
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
/* global JitsiMeetJS, APP */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import InviteDialogView from './InviteDialogView';
|
||||
import createRoomLocker from './RoomLocker';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
|
||||
const ConferenceEvents = JitsiMeetJS.events.conference;
|
||||
|
||||
/**
|
||||
* Invite module
|
||||
* Constructor takes conference object giving
|
||||
* ability to subscribe on its events
|
||||
*/
|
||||
class Invite {
|
||||
constructor(conference) {
|
||||
this.conference = conference;
|
||||
this.inviteUrl = APP.ConferenceUrl.getInviteUrl();
|
||||
this.createRoomLocker(conference);
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registering listeners.
|
||||
* Primarily listeners for conference events.
|
||||
*/
|
||||
registerListeners() {
|
||||
|
||||
this.conference.on(ConferenceEvents.LOCK_STATE_CHANGED,
|
||||
(locked, error) => {
|
||||
|
||||
logger.log("Received channel password lock change: ", locked,
|
||||
error);
|
||||
|
||||
if (!locked) {
|
||||
this.getRoomLocker().resetPassword();
|
||||
}
|
||||
|
||||
this.setLockedFromElsewhere(locked);
|
||||
});
|
||||
|
||||
this.conference.on(ConferenceEvents.USER_ROLE_CHANGED, (id) => {
|
||||
if (APP.conference.isLocalId(id)
|
||||
&& this.isModerator !== this.conference.isModerator()) {
|
||||
|
||||
this.setModerator(this.conference.isModerator());
|
||||
}
|
||||
});
|
||||
|
||||
APP.UI.addListener( UIEvents.INVITE_CLICKED,
|
||||
() => { this.openLinkDialog(); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the view.
|
||||
* If dialog hasn't been defined -
|
||||
* creates it and updates
|
||||
*/
|
||||
updateView() {
|
||||
if (!this.view) {
|
||||
this.initDialog();
|
||||
}
|
||||
|
||||
this.view.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Room locker factory
|
||||
* @param room
|
||||
* @returns {Object} RoomLocker
|
||||
* @factory
|
||||
*/
|
||||
createRoomLocker(room = this.conference) {
|
||||
let roomLocker = createRoomLocker(room);
|
||||
this.roomLocker = roomLocker;
|
||||
return this.getRoomLocker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Room locker getter
|
||||
* @returns {Object} RoomLocker
|
||||
*/
|
||||
getRoomLocker() {
|
||||
return this.roomLocker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the invite link dialog.
|
||||
*/
|
||||
openLinkDialog () {
|
||||
if (!this.view) {
|
||||
this.initDialog();
|
||||
}
|
||||
|
||||
this.view.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog initialization.
|
||||
* creating view object using as a model this module
|
||||
*/
|
||||
initDialog() {
|
||||
this.view = new InviteDialogView(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Password getter
|
||||
* @returns {String} password
|
||||
*/
|
||||
getPassword() {
|
||||
return this.getRoomLocker().password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches between the moderator view and normal view.
|
||||
*
|
||||
* @param isModerator indicates if the participant is moderator
|
||||
*/
|
||||
setModerator(isModerator) {
|
||||
this.isModerator = isModerator;
|
||||
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to unlock the room.
|
||||
* If the current user is moderator.
|
||||
*/
|
||||
setRoomUnlocked() {
|
||||
if (this.isModerator) {
|
||||
this.getRoomLocker().lock().then(() => {
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK);
|
||||
this.updateView();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to lock the room if
|
||||
* the current user is moderator.
|
||||
* Takes the password.
|
||||
* @param {String} newPass
|
||||
*/
|
||||
setRoomLocked(newPass) {
|
||||
let isModerator = this.isModerator;
|
||||
if (isModerator && (newPass || !this.getRoomLocker().isLocked)) {
|
||||
this.getRoomLocker().lock(newPass).then(() => {
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK);
|
||||
this.updateView();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for encoding
|
||||
* Invite URL
|
||||
* @returns {string}
|
||||
*/
|
||||
getEncodedInviteUrl() {
|
||||
return encodeURI(this.inviteUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is locked flag.
|
||||
* Delegates to room locker
|
||||
* @returns {Boolean} isLocked
|
||||
*/
|
||||
isLocked() {
|
||||
return this.getRoomLocker().isLocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set flag locked from elsewhere to room locker.
|
||||
* @param isLocked
|
||||
*/
|
||||
setLockedFromElsewhere(isLocked) {
|
||||
let roomLocker = this.getRoomLocker();
|
||||
let oldLockState = roomLocker.isLocked;
|
||||
if (oldLockState !== isLocked) {
|
||||
roomLocker.lockedElsewhere = isLocked;
|
||||
APP.UI.emitEvent(UIEvents.TOGGLE_ROOM_LOCK);
|
||||
this.updateView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Invite;
|
||||
@@ -1,378 +0,0 @@
|
||||
/* global $, APP, JitsiMeetJS */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Substate for password
|
||||
* @type {{LOCKED: string, UNLOCKED: string}}
|
||||
*/
|
||||
const States = {
|
||||
LOCKED: 'locked',
|
||||
UNLOCKED: 'unlocked'
|
||||
};
|
||||
|
||||
/**
|
||||
* Class representing view for Invite dialog
|
||||
* @class InviteDialogView
|
||||
*/
|
||||
export default class InviteDialogView {
|
||||
constructor(model) {
|
||||
this.unlockHint = "unlockHint";
|
||||
this.lockHint = "lockHint";
|
||||
this.model = model;
|
||||
|
||||
if (this.model.inviteUrl === null) {
|
||||
this.inviteAttributes = `data-i18n="[value]inviteUrlDefaultMsg"`;
|
||||
} else {
|
||||
this.inviteAttributes
|
||||
= `value="${this.model.getEncodedInviteUrl()}"`;
|
||||
}
|
||||
|
||||
this.initDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization of dialog property
|
||||
*/
|
||||
initDialog() {
|
||||
let dialog = {};
|
||||
dialog.closeFunction = this.closeFunction.bind(this);
|
||||
dialog.submitFunction = this.submitFunction.bind(this);
|
||||
dialog.loadedFunction = this.loadedFunction.bind(this);
|
||||
|
||||
dialog.titleKey = "dialog.shareLink";
|
||||
this.dialog = dialog;
|
||||
|
||||
this.dialog.states = this.getStates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for submitting dialog
|
||||
* @param e
|
||||
* @param v
|
||||
*/
|
||||
submitFunction(e, v) {
|
||||
if (v && this.model.inviteUrl) {
|
||||
JitsiMeetJS.analytics.sendEvent('toolbar.invite.button');
|
||||
} else {
|
||||
JitsiMeetJS.analytics.sendEvent('toolbar.invite.cancel');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for load dialog
|
||||
* @param event
|
||||
*/
|
||||
loadedFunction(event) {
|
||||
if (this.model.inviteUrl) {
|
||||
document.getElementById('inviteLinkRef').select();
|
||||
} else {
|
||||
if (event && event.target) {
|
||||
$(event.target).find('button[value=true]')
|
||||
.prop('disabled', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for closing dialog
|
||||
* @param e
|
||||
* @param v
|
||||
* @param m
|
||||
* @param f
|
||||
*/
|
||||
closeFunction(e, v, m, f) {
|
||||
$(document).off('click', '.copyInviteLink', this.copyToClipboard);
|
||||
|
||||
if(!v && !m && !f)
|
||||
JitsiMeetJS.analytics.sendEvent('toolbar.invite.close');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all states of the dialog
|
||||
* @returns {{}}
|
||||
*/
|
||||
getStates() {
|
||||
let {
|
||||
titleKey
|
||||
} = this.dialog;
|
||||
let doneMsg = APP.translation.generateTranslationHTML('dialog.done');
|
||||
let states = {};
|
||||
let buttons = {};
|
||||
buttons[`${doneMsg}`] = true;
|
||||
|
||||
states[States.UNLOCKED] = {
|
||||
titleKey,
|
||||
html: this.getShareLinkBlock() + this.getAddPasswordBlock(),
|
||||
buttons
|
||||
};
|
||||
states[States.LOCKED] = {
|
||||
titleKey,
|
||||
html: this.getShareLinkBlock() + this.getPasswordBlock(),
|
||||
buttons
|
||||
};
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout for invite link input
|
||||
* @returns {string}
|
||||
*/
|
||||
getShareLinkBlock() {
|
||||
let classes = 'button-control button-control_light copyInviteLink';
|
||||
return (
|
||||
`<div class="form-control">
|
||||
<label class="form-control__label" for="inviteLinkRef"
|
||||
data-i18n="${this.dialog.titleKey}"></label>
|
||||
<div class="form-control__container">
|
||||
<input class="input-control inviteLink"
|
||||
id="inviteLinkRef" type="text"
|
||||
${this.inviteAttributes} readonly>
|
||||
<button data-i18n="dialog.copy" class="${classes}"></button>
|
||||
</div>
|
||||
<p class="form-control__hint ${this.lockHint}">
|
||||
<span class="icon-security-locked"></span>
|
||||
<span data-i18n="dialog.roomLocked"></span>
|
||||
</p>
|
||||
<p class="form-control__hint ${this.unlockHint}">
|
||||
<span class="icon-security"></span>
|
||||
<span data-i18n="roomUnlocked"></span>
|
||||
</p>
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout for adding password input
|
||||
* @returns {string}
|
||||
*/
|
||||
getAddPasswordBlock() {
|
||||
let html;
|
||||
|
||||
if (this.model.isModerator) {
|
||||
html = (`
|
||||
<div class="form-control">
|
||||
<label class="form-control__label"
|
||||
for="newPasswordInput" data-i18n="dialog.addPassword">
|
||||
</label>
|
||||
<div class="form-control__container">
|
||||
<input class="input-control"
|
||||
id="newPasswordInput"
|
||||
type="text"
|
||||
data-i18n="[placeholder]dialog.createPassword">
|
||||
<button id="addPasswordBtn" id="inviteDialogAddPassword"
|
||||
disabled data-i18n="dialog.add"
|
||||
class="button-control button-control_light">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
html = '';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout for password (when room is locked)
|
||||
* @returns {string}
|
||||
*/
|
||||
getPasswordBlock() {
|
||||
let password = this.model.getPassword();
|
||||
let { isModerator } = this.model;
|
||||
|
||||
if (isModerator) {
|
||||
return (`
|
||||
<div class="form-control">
|
||||
<label class="form-control__label"
|
||||
data-i18n="dialog.passwordLabel"></label>
|
||||
<div class="form-control__container">
|
||||
<p>
|
||||
<span class="form-control__text"
|
||||
data-i18n="dialog.currentPassword"></span>
|
||||
<span id="inviteDialogPassword"
|
||||
class="form-control__em">
|
||||
${password}
|
||||
</span>
|
||||
</p>
|
||||
<a class="link form-control__right"
|
||||
id="inviteDialogRemovePassword"
|
||||
data-i18n="dialog.removePassword"></a>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
} else {
|
||||
return (`
|
||||
<div class="form-control">
|
||||
<p>A participant protected this call with a password.</p>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Opening the dialog
|
||||
*/
|
||||
open() {
|
||||
let {
|
||||
submitFunction,
|
||||
loadedFunction,
|
||||
closeFunction
|
||||
} = this.dialog;
|
||||
|
||||
let states = this.getStates();
|
||||
let initial = this.model.roomLocked ? States.LOCKED : States.UNLOCKED;
|
||||
|
||||
APP.UI.messageHandler.openDialogWithStates(states, {
|
||||
submit: submitFunction,
|
||||
loaded: loadedFunction,
|
||||
close: closeFunction,
|
||||
size: 'medium'
|
||||
});
|
||||
$.prompt.goToState(initial);
|
||||
|
||||
this.registerListeners();
|
||||
this.updateView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setting event handlers
|
||||
* used in dialog
|
||||
*/
|
||||
registerListeners() {
|
||||
const ENTER_KEY = 13;
|
||||
let addPasswordBtn = '#addPasswordBtn';
|
||||
let copyInviteLink = '.copyInviteLink';
|
||||
let newPasswordInput = '#newPasswordInput';
|
||||
let removePassword = '#inviteDialogRemovePassword';
|
||||
|
||||
$(document).on('click', copyInviteLink, this.copyToClipboard);
|
||||
$(removePassword).on('click', () => {
|
||||
this.model.setRoomUnlocked();
|
||||
});
|
||||
let boundSetPassword = this._setPassword.bind(this);
|
||||
$(document).on('click', addPasswordBtn, boundSetPassword);
|
||||
let boundDisablePass = this.disableAddPassIfInputEmpty.bind(this);
|
||||
$(document).on('keypress', newPasswordInput, boundDisablePass);
|
||||
|
||||
// We need to handle keydown event because impromptu
|
||||
// is listening to it too for closing the dialog
|
||||
$(newPasswordInput).on('keydown', (e) => {
|
||||
if (e.keyCode === ENTER_KEY) {
|
||||
e.stopPropagation();
|
||||
this._setPassword();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Marking room as locked
|
||||
* @private
|
||||
*/
|
||||
_setPassword() {
|
||||
let $passInput = $('#newPasswordInput');
|
||||
let newPass = $passInput.val();
|
||||
|
||||
if(newPass) {
|
||||
this.model.setRoomLocked(newPass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checking input and if it's empty then
|
||||
* disable add pass button
|
||||
*/
|
||||
disableAddPassIfInputEmpty() {
|
||||
let $passInput = $('#newPasswordInput');
|
||||
let $addPassBtn = $('#addPasswordBtn');
|
||||
|
||||
if(!$passInput.val()) {
|
||||
$addPassBtn.prop('disabled', true);
|
||||
} else {
|
||||
$addPassBtn.prop('disabled', false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copying text to clipboard
|
||||
*/
|
||||
copyToClipboard() {
|
||||
$('.inviteLink').each(function () {
|
||||
let $el = $(this).closest('.jqistate');
|
||||
|
||||
// TOFIX: We can select only visible elements
|
||||
if($el.css('display') === 'block') {
|
||||
this.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
this.blur();
|
||||
}
|
||||
catch (err) {
|
||||
logger.error('error when copy the text');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method syncing the view and the model
|
||||
*/
|
||||
updateView() {
|
||||
let pass = this.model.getPassword();
|
||||
let { isModerator } = this.model;
|
||||
if (this.model.getRoomLocker().lockedElsewhere || !pass) {
|
||||
$('#inviteDialogPassword').attr("data-i18n", "passwordSetRemotely");
|
||||
APP.translation.translateElement($('#inviteDialogPassword'));
|
||||
} else {
|
||||
$('#inviteDialogPassword').removeAttr("data-i18n");
|
||||
$('#inviteDialogPassword').text(pass);
|
||||
}
|
||||
|
||||
// if we are not moderator we cannot remove password
|
||||
if (isModerator)
|
||||
$('#inviteDialogRemovePassword').show();
|
||||
else
|
||||
$('#inviteDialogRemovePassword').hide();
|
||||
|
||||
$('#newPasswordInput').val('');
|
||||
this.disableAddPassIfInputEmpty();
|
||||
|
||||
this.updateInviteLink();
|
||||
|
||||
$.prompt.goToState(
|
||||
(this.model.isLocked())
|
||||
? States.LOCKED
|
||||
: States.UNLOCKED);
|
||||
|
||||
let roomLocked = `.${this.lockHint}`;
|
||||
let roomUnlocked = `.${this.unlockHint}`;
|
||||
|
||||
let showDesc = this.model.isLocked() ? roomLocked : roomUnlocked;
|
||||
let hideDesc = !this.model.isLocked() ? roomLocked : roomUnlocked;
|
||||
|
||||
$(showDesc).show();
|
||||
$(hideDesc).hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates invite link
|
||||
*/
|
||||
updateInviteLink() {
|
||||
// If the invite dialog has been already opened we update the
|
||||
// information.
|
||||
let inviteLink = document.querySelectorAll('.inviteLink');
|
||||
let list = Array.from(inviteLink);
|
||||
list.forEach((inviteLink) => {
|
||||
inviteLink.value = this.model.inviteUrl;
|
||||
inviteLink.select();
|
||||
});
|
||||
|
||||
$('#inviteLinkRef').parent()
|
||||
.find('button[value=true]').prop('disabled', false);
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/* global APP, JitsiMeetJS */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Show notification that user cannot set password for the conference
|
||||
* because server doesn't support that.
|
||||
*/
|
||||
function notifyPasswordNotSupported () {
|
||||
logger.warn('room passwords not supported');
|
||||
APP.UI.messageHandler.showError(
|
||||
"dialog.warning", "dialog.passwordNotSupported");
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notification that setting password for the conference failed.
|
||||
* @param {Error} err error
|
||||
*/
|
||||
function notifyPasswordFailed(err) {
|
||||
logger.warn('setting password failed', err);
|
||||
APP.UI.messageHandler.showError(
|
||||
"dialog.lockTitle", "dialog.lockMessage");
|
||||
}
|
||||
|
||||
const ConferenceErrors = JitsiMeetJS.errors.conference;
|
||||
|
||||
/**
|
||||
* Create new RoomLocker for the conference.
|
||||
* It allows to set or remove password for the conference,
|
||||
* or ask for required password.
|
||||
* @returns {RoomLocker}
|
||||
*/
|
||||
export default function createRoomLocker (room) {
|
||||
let password;
|
||||
/**
|
||||
* If the room was locked from someone other than us, we indicate it with
|
||||
* this property in order to have correct roomLocker state of isLocked.
|
||||
* @type {boolean} whether room is locked, but not from us.
|
||||
*/
|
||||
let lockedElsewhere = false;
|
||||
|
||||
/**
|
||||
* @class RoomLocker
|
||||
*/
|
||||
return {
|
||||
get isLocked () {
|
||||
return !!password || lockedElsewhere;
|
||||
},
|
||||
|
||||
get password () {
|
||||
return password;
|
||||
},
|
||||
|
||||
/**
|
||||
* Allows to set new password
|
||||
* @param newPass
|
||||
* @returns {Promise.<TResult>}
|
||||
*/
|
||||
lock (newPass) {
|
||||
return room.lock(newPass).then(() => {
|
||||
password = newPass;
|
||||
// If the password is undefined this means that we're removing
|
||||
// it for everyone.
|
||||
if (!password)
|
||||
lockedElsewhere = false;
|
||||
}).catch(function (err) {
|
||||
logger.error(err);
|
||||
if (err === ConferenceErrors.PASSWORD_NOT_SUPPORTED) {
|
||||
notifyPasswordNotSupported();
|
||||
} else {
|
||||
notifyPasswordFailed(err);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets that the room is locked from another user, not us.
|
||||
* @param {boolean} value locked/unlocked state
|
||||
*/
|
||||
set lockedElsewhere (value) {
|
||||
lockedElsewhere = value;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether room is locked from someone else.
|
||||
* @returns {boolean} whether room is not locked locally,
|
||||
* but it is still locked.
|
||||
*/
|
||||
get lockedElsewhere () {
|
||||
return lockedElsewhere;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset the password. Can be useful when room
|
||||
* has been unlocked from elsewhere and we can use
|
||||
* this method for sync the pass
|
||||
*/
|
||||
resetPassword() {
|
||||
password = null;
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import UIUtil from '../util/UIUtil';
|
||||
import VideoLayout from '../videolayout/VideoLayout';
|
||||
import Feedback from '../feedback/Feedback.js';
|
||||
|
||||
import { hideToolbox } from '../../../react/features/toolbox';
|
||||
import { setToolboxEnabled } from '../../../react/features/toolbox';
|
||||
|
||||
/**
|
||||
* The dialog for user input.
|
||||
@@ -35,8 +35,10 @@ let dialog = null;
|
||||
* @private
|
||||
*/
|
||||
function _isRecordingButtonEnabled() {
|
||||
return interfaceConfig.TOOLBAR_BUTTONS.indexOf("recording") !== -1
|
||||
&& config.enableRecording && APP.conference.isRecordingSupported();
|
||||
return (
|
||||
interfaceConfig.TOOLBAR_BUTTONS.indexOf("recording") !== -1
|
||||
&& config.enableRecording
|
||||
&& APP.conference.isRecordingSupported());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +131,7 @@ function _requestLiveStreamId() {
|
||||
* Request recording token from the user.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function _requestRecordingToken () {
|
||||
function _requestRecordingToken() {
|
||||
let titleKey = "dialog.recordingToken";
|
||||
let messageString = (
|
||||
`<input name="recordingToken" type="text"
|
||||
@@ -164,7 +166,7 @@ function _requestRecordingToken () {
|
||||
* @returns {Promise}
|
||||
* @private
|
||||
*/
|
||||
function _showStopRecordingPrompt (recordingType) {
|
||||
function _showStopRecordingPrompt(recordingType) {
|
||||
var title;
|
||||
var message;
|
||||
var buttonKey;
|
||||
@@ -179,19 +181,13 @@ function _showStopRecordingPrompt (recordingType) {
|
||||
buttonKey = "dialog.stopRecording";
|
||||
}
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dialog = APP.UI.messageHandler.openTwoButtonDialog({
|
||||
titleKey: title,
|
||||
msgKey: message,
|
||||
leftButtonKey: buttonKey,
|
||||
submitFunction: function(e,v) {
|
||||
if (v) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
},
|
||||
closeFunction: function () {
|
||||
submitFunction: (e, v) => (v ? resolve : reject)(),
|
||||
closeFunction: () => {
|
||||
dialog = null;
|
||||
}
|
||||
});
|
||||
@@ -250,35 +246,12 @@ var Recording = {
|
||||
/**
|
||||
* Initializes the recording UI.
|
||||
*/
|
||||
init (emitter, recordingType) {
|
||||
this.eventEmitter = emitter;
|
||||
init(eventEmitter, recordingType) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
this.recordingType = recordingType;
|
||||
|
||||
this.updateRecordingState(APP.conference.getRecordingState());
|
||||
|
||||
this.initRecordingButton(recordingType);
|
||||
|
||||
// If I am a recorder then I publish my recorder custom role to notify
|
||||
// everyone.
|
||||
if (config.iAmRecorder) {
|
||||
VideoLayout.enableDeviceAvailabilityIcons(
|
||||
APP.conference.getMyUserId(), false);
|
||||
VideoLayout.setLocalVideoVisible(false);
|
||||
Feedback.enableFeedback(false);
|
||||
APP.store.dispatch(hideToolbox());
|
||||
APP.UI.messageHandler.enableNotifications(false);
|
||||
APP.UI.messageHandler.enablePopups(false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialise the recording button.
|
||||
*/
|
||||
initRecordingButton(recordingType) {
|
||||
let selector = $('#toolbar_button_record');
|
||||
|
||||
let button = selector.get(0);
|
||||
UIUtil.setTooltip(button, 'liveStreaming.buttonTooltip', 'right');
|
||||
|
||||
if (recordingType === 'jibri') {
|
||||
this.baseClass = "fa fa-play-circle";
|
||||
this.recordingTitle = "dialog.liveStreaming";
|
||||
@@ -304,101 +277,44 @@ var Recording = {
|
||||
this.recordingBusy = "liveStreaming.busy";
|
||||
}
|
||||
|
||||
// XXX Due to the React-ification of Toolbox, the HTMLElement with id
|
||||
// toolbar_button_record may not exist yet.
|
||||
$(document).on(
|
||||
'click',
|
||||
'#toolbar_button_record',
|
||||
ev => this._onToolbarButtonClick(ev));
|
||||
|
||||
// If I am a recorder then I publish my recorder custom role to notify
|
||||
// everyone.
|
||||
if (config.iAmRecorder) {
|
||||
VideoLayout.enableDeviceAvailabilityIcons(
|
||||
APP.conference.getMyUserId(), false);
|
||||
VideoLayout.setLocalVideoVisible(false);
|
||||
Feedback.enableFeedback(false);
|
||||
APP.store.dispatch(setToolboxEnabled(false));
|
||||
APP.UI.messageHandler.enableNotifications(false);
|
||||
APP.UI.messageHandler.enablePopups(false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialise the recording button.
|
||||
*/
|
||||
initRecordingButton() {
|
||||
const selector = $('#toolbar_button_record');
|
||||
|
||||
UIUtil.setTooltip(selector, 'liveStreaming.buttonTooltip', 'right');
|
||||
|
||||
selector.addClass(this.baseClass);
|
||||
selector.attr("data-i18n", "[content]" + this.recordingButtonTooltip);
|
||||
APP.translation.translateElement(selector);
|
||||
|
||||
var self = this;
|
||||
selector.click(function () {
|
||||
if (dialog)
|
||||
return;
|
||||
JitsiMeetJS.analytics.sendEvent('recording.clicked');
|
||||
switch (self.currentState) {
|
||||
case Status.ON:
|
||||
case Status.RETRYING:
|
||||
case Status.PENDING: {
|
||||
_showStopRecordingPrompt(recordingType).then(
|
||||
() => {
|
||||
self.eventEmitter.emit(UIEvents.RECORDING_TOGGLED);
|
||||
JitsiMeetJS.analytics.sendEvent(
|
||||
'recording.stopped');
|
||||
},
|
||||
() => {});
|
||||
break;
|
||||
}
|
||||
case Status.AVAILABLE:
|
||||
case Status.OFF: {
|
||||
if (recordingType === 'jibri')
|
||||
_requestLiveStreamId().then((streamId) => {
|
||||
self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED,
|
||||
{streamId: streamId});
|
||||
JitsiMeetJS.analytics.sendEvent(
|
||||
'recording.started');
|
||||
}).catch(
|
||||
reason => {
|
||||
if (reason !== APP.UI.messageHandler.CANCEL)
|
||||
logger.error(reason);
|
||||
else
|
||||
JitsiMeetJS.analytics.sendEvent(
|
||||
'recording.canceled');
|
||||
}
|
||||
);
|
||||
else {
|
||||
if (self.predefinedToken) {
|
||||
self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED,
|
||||
{token: self.predefinedToken});
|
||||
JitsiMeetJS.analytics.sendEvent(
|
||||
'recording.started');
|
||||
return;
|
||||
}
|
||||
|
||||
_requestRecordingToken().then((token) => {
|
||||
self.eventEmitter.emit( UIEvents.RECORDING_TOGGLED,
|
||||
{token: token});
|
||||
JitsiMeetJS.analytics.sendEvent(
|
||||
'recording.started');
|
||||
}).catch(
|
||||
reason => {
|
||||
if (reason !== APP.UI.messageHandler.CANCEL)
|
||||
logger.error(reason);
|
||||
else
|
||||
JitsiMeetJS.analytics.sendEvent(
|
||||
'recording.canceled');
|
||||
}
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Status.BUSY: {
|
||||
dialog = APP.UI.messageHandler.openMessageDialog(
|
||||
self.recordingTitle,
|
||||
self.recordingBusy,
|
||||
null,
|
||||
function () {
|
||||
dialog = null;
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
dialog = APP.UI.messageHandler.openMessageDialog(
|
||||
self.recordingTitle,
|
||||
self.recordingUnavailable,
|
||||
null,
|
||||
function () {
|
||||
dialog = null;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows or hides the 'recording' button.
|
||||
* @param show {true} to show the recording button, {false} to hide it
|
||||
*/
|
||||
showRecordingButton (show) {
|
||||
showRecordingButton(show) {
|
||||
let shouldShow = show && _isRecordingButtonEnabled();
|
||||
let id = 'toolbar_button_record';
|
||||
|
||||
@@ -425,7 +341,7 @@ var Recording = {
|
||||
* Sets the state of the recording button.
|
||||
* @param recordingState gives us the current recording state
|
||||
*/
|
||||
updateRecordingUI (recordingState) {
|
||||
updateRecordingUI(recordingState) {
|
||||
|
||||
let oldState = this.currentState;
|
||||
this.currentState = recordingState;
|
||||
@@ -491,7 +407,7 @@ var Recording = {
|
||||
},
|
||||
// checks whether recording is enabled and whether we have params
|
||||
// to start automatically recording
|
||||
checkAutoRecord () {
|
||||
checkAutoRecord() {
|
||||
if (_isRecordingButtonEnabled && config.autoRecord) {
|
||||
this.predefinedToken = UIUtil.escapeHtml(config.autoRecordToken);
|
||||
this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED,
|
||||
@@ -514,6 +430,90 @@ var Recording = {
|
||||
APP.translation.translateElement(labelSelector);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles {@code click} on {@code toolbar_button_record}.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onToolbarButtonClick() {
|
||||
if (dialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
JitsiMeetJS.analytics.sendEvent('recording.clicked');
|
||||
switch (this.currentState) {
|
||||
case Status.ON:
|
||||
case Status.RETRYING:
|
||||
case Status.PENDING: {
|
||||
_showStopRecordingPrompt(this.recordingType).then(
|
||||
() => {
|
||||
this.eventEmitter.emit(UIEvents.RECORDING_TOGGLED);
|
||||
JitsiMeetJS.analytics.sendEvent('recording.stopped');
|
||||
},
|
||||
() => {});
|
||||
break;
|
||||
}
|
||||
case Status.AVAILABLE:
|
||||
case Status.OFF: {
|
||||
if (this.recordingType === 'jibri')
|
||||
_requestLiveStreamId().then(streamId => {
|
||||
this.eventEmitter.emit(
|
||||
UIEvents.RECORDING_TOGGLED,
|
||||
{ streamId });
|
||||
JitsiMeetJS.analytics.sendEvent('recording.started');
|
||||
}).catch(reason => {
|
||||
if (reason !== APP.UI.messageHandler.CANCEL)
|
||||
logger.error(reason);
|
||||
else
|
||||
JitsiMeetJS.analytics.sendEvent('recording.canceled');
|
||||
});
|
||||
else {
|
||||
if (this.predefinedToken) {
|
||||
this.eventEmitter.emit(
|
||||
UIEvents.RECORDING_TOGGLED,
|
||||
{ token: this.predefinedToken });
|
||||
JitsiMeetJS.analytics.sendEvent('recording.started');
|
||||
return;
|
||||
}
|
||||
|
||||
_requestRecordingToken().then((token) => {
|
||||
this.eventEmitter.emit(
|
||||
UIEvents.RECORDING_TOGGLED,
|
||||
{ token });
|
||||
JitsiMeetJS.analytics.sendEvent('recording.started');
|
||||
}).catch(reason => {
|
||||
if (reason !== APP.UI.messageHandler.CANCEL)
|
||||
logger.error(reason);
|
||||
else
|
||||
JitsiMeetJS.analytics.sendEvent('recording.canceled');
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Status.BUSY: {
|
||||
dialog = APP.UI.messageHandler.openMessageDialog(
|
||||
this.recordingTitle,
|
||||
this.recordingBusy,
|
||||
null,
|
||||
() => {
|
||||
dialog = null;
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
dialog = APP.UI.messageHandler.openMessageDialog(
|
||||
this.recordingTitle,
|
||||
this.recordingUnavailable,
|
||||
null,
|
||||
() => {
|
||||
dialog = null;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the toggled state of the recording toolbar button.
|
||||
*
|
||||
|
||||
@@ -22,7 +22,8 @@ function onAvatarVisible(shown) {
|
||||
*/
|
||||
class RingOverlay {
|
||||
/**
|
||||
* @param callee instance of User class from TokenData.js
|
||||
*
|
||||
* @param callee The callee (Object) as defined by the JWT support.
|
||||
* @param {boolean} disableRingingSound if true the ringing sound wont be played.
|
||||
*/
|
||||
constructor(callee, disableRingingSound) {
|
||||
@@ -77,9 +78,9 @@ class RingOverlay {
|
||||
<div id="${this._containerId}" class='ringing' >
|
||||
<div class='ringing__content'>
|
||||
${callingLabel}
|
||||
<img class='ringing__avatar' src="${callee.getAvatarUrl()}" />
|
||||
<img class='ringing__avatar' src="${callee.avatarUrl}" />
|
||||
<div class="ringing__caller-info">
|
||||
<p>${callee.getName()}${callerStateLabel}</p>
|
||||
<p>${callee.name}${callerStateLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
${audioHTML}
|
||||
@@ -137,9 +138,12 @@ class RingOverlay {
|
||||
export default {
|
||||
/**
|
||||
* Shows the ring overlay for the passed callee.
|
||||
* @param callee {class User} the callee. Instance of User class from
|
||||
* TokenData.js
|
||||
* @param {boolean} disableRingingSound if true the ringing sound wont be played.
|
||||
*
|
||||
* @param {Object} callee - The callee. Object containing data about
|
||||
* callee.
|
||||
* @param {boolean} disableRingingSound - If true the ringing sound won't be
|
||||
* played.
|
||||
* @returns {void}
|
||||
*/
|
||||
show(callee, disableRingingSound = false) {
|
||||
if(overlay) {
|
||||
|
||||
@@ -30,7 +30,7 @@ const htmlStr = `
|
||||
<textarea id="usermsg" autofocus
|
||||
data-i18n="[placeholder]chat.messagebox"></textarea>
|
||||
<div id="smileysarea">
|
||||
<div id="smileys" id="toggle_smileys">
|
||||
<div id="smileys">
|
||||
<img src="images/smile.svg"/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -191,7 +191,7 @@ var Chat = {
|
||||
Chat.setChatConversationMode(true);
|
||||
}
|
||||
|
||||
$("#toggle_smileys").click(function() {
|
||||
$("#smileys").click(function() {
|
||||
Chat.toggleSmileys();
|
||||
});
|
||||
|
||||
|
||||
@@ -19,14 +19,12 @@ class ContactList {
|
||||
}
|
||||
|
||||
/**
|
||||
* Is locked flag.
|
||||
* Delegates to Invite module
|
||||
* TO FIX: find a better way to access the IS LOCKED state of the invite.
|
||||
* Returns true if the current conference is locked.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isLocked() {
|
||||
return APP.conference.invite.isLocked();
|
||||
return APP.store.getState()['features/base/conference'].locked;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,9 +34,9 @@ class ContactList {
|
||||
* @param isLocal
|
||||
*/
|
||||
addContact(id, isLocal) {
|
||||
let isExist = this.contacts.some((el) => el.id === id);
|
||||
const exists = this.contacts.some(el => el.id === id);
|
||||
|
||||
if (!isExist) {
|
||||
if (!exists) {
|
||||
let newContact = new Contact({ id, isLocal });
|
||||
this.contacts.push(newContact);
|
||||
APP.UI.emitEvent(UIEvents.CONTACT_ADDED, { id, isLocal });
|
||||
@@ -92,4 +90,4 @@ class ContactList {
|
||||
}
|
||||
}
|
||||
|
||||
export default ContactList;
|
||||
export default ContactList;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/* global $, APP, interfaceConfig */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import { openInviteDialog } from '../../../../react/features/invite';
|
||||
|
||||
import Avatar from '../../avatar/Avatar';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
import UIUtil from '../../util/UIUtil';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
let numberOfContacts = 0;
|
||||
const sidePanelsContainerId = 'sideToolbarContainer';
|
||||
const htmlStr = `
|
||||
@@ -96,7 +99,7 @@ var ContactListView = {
|
||||
this.model = model;
|
||||
this.addInviteButton();
|
||||
this.registerListeners();
|
||||
this.toggleLock();
|
||||
this.setLockDisplay(false);
|
||||
},
|
||||
/**
|
||||
* Adds layout for invite button
|
||||
@@ -108,8 +111,9 @@ var ContactListView = {
|
||||
.insertAdjacentHTML('afterend', this.getInviteButtonLayout());
|
||||
|
||||
APP.translation.translateElement($(container));
|
||||
|
||||
$(document).on('click', '#addParticipantsBtn', () => {
|
||||
APP.UI.emitEvent(UIEvents.INVITE_CLICKED);
|
||||
APP.store.dispatch(openInviteDialog());
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -125,8 +129,8 @@ var ContactListView = {
|
||||
|
||||
return (
|
||||
`<div class="sideToolbarBlock first">
|
||||
<button id="addParticipantsBtn"
|
||||
data-i18n="${key}"
|
||||
<button id="addParticipantsBtn"
|
||||
data-i18n="${key}"
|
||||
class="${classes}"></button>
|
||||
<div>
|
||||
${lockedHtml}
|
||||
@@ -158,7 +162,7 @@ var ContactListView = {
|
||||
let displayNameChange = this.onDisplayNameChange.bind(this);
|
||||
|
||||
APP.UI.addListener( UIEvents.TOGGLE_ROOM_LOCK,
|
||||
this.toggleLock.bind(this));
|
||||
this.setLockDisplay.bind(this));
|
||||
APP.UI.addListener( UIEvents.CONTACT_ADDED,
|
||||
this.onAddContact.bind(this));
|
||||
|
||||
@@ -166,21 +170,28 @@ var ContactListView = {
|
||||
APP.UI.addListener(UIEvents.USER_AVATAR_CHANGED, changeAvatar);
|
||||
APP.UI.addListener(UIEvents.DISPLAY_NAME_CHANGED, displayNameChange);
|
||||
},
|
||||
/**
|
||||
* Updating the view according the model
|
||||
* @param type {String} type of change
|
||||
* @returns {Promise}
|
||||
*/
|
||||
toggleLock() {
|
||||
let isLocked = this.model.isLocked();
|
||||
let showKey = isLocked ? this.lockKey : this.unlockKey;
|
||||
let hideKey = !isLocked ? this.lockKey : this.unlockKey;
|
||||
let showId = `contactList${showKey}`;
|
||||
let hideId = `contactList${hideKey}`;
|
||||
|
||||
$(`#${showId}`).show();
|
||||
$(`#${hideId}`).hide();
|
||||
/**
|
||||
* Updates the view according to the passed in lock state.
|
||||
*
|
||||
* @param {boolean} locked - True to display the locked UI state or false to
|
||||
* display the unlocked UI state.
|
||||
*/
|
||||
setLockDisplay(locked) {
|
||||
let hideKey, showKey;
|
||||
|
||||
if (locked) {
|
||||
hideKey = this.unlockKey;
|
||||
showKey = this.lockKey;
|
||||
} else {
|
||||
hideKey = this.lockKey;
|
||||
showKey = this.unlockKey;
|
||||
}
|
||||
|
||||
$(`#contactList${hideKey}`).hide();
|
||||
$(`#contactList${showKey}`).show();
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates if the chat is currently visible.
|
||||
*
|
||||
@@ -275,4 +286,4 @@ var ContactListView = {
|
||||
}
|
||||
};
|
||||
|
||||
export default ContactListView;
|
||||
export default ContactListView;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* global $, APP, AJS, interfaceConfig, JitsiMeetJS */
|
||||
import { openDialog } from '../../../../react/features/base/dialog';
|
||||
/* global $, APP, AJS, interfaceConfig */
|
||||
import { LANGUAGES } from "../../../../react/features/base/i18n";
|
||||
import { DeviceSelectionDialog }
|
||||
import { openDeviceSelectionDialog }
|
||||
from '../../../../react/features/device-selection';
|
||||
|
||||
import UIUtil from "../../util/UIUtil";
|
||||
@@ -101,34 +100,6 @@ function initSelect2($el, onSelectedCb) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open DeviceSelectionDialog with a configuration based on the environment's
|
||||
* supported abilities.
|
||||
*
|
||||
* @param {boolean} isDeviceListAvailable - Whether or not device enumeration
|
||||
* is possible. This is a value obtained through an async operation whereas all
|
||||
* other configurations for the modal are obtained synchronously.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _openDeviceSelectionModal(isDeviceListAvailable) {
|
||||
APP.store.dispatch(openDialog(DeviceSelectionDialog, {
|
||||
currentAudioOutputId: APP.settings.getAudioOutputDeviceId(),
|
||||
currentAudioTrack: APP.conference.getLocalAudioTrack(),
|
||||
currentVideoTrack: APP.conference.getLocalVideoTrack(),
|
||||
disableAudioInputChange: !JitsiMeetJS.isMultipleAudioInputSupported(),
|
||||
disableDeviceChange: !isDeviceListAvailable
|
||||
|| !JitsiMeetJS.mediaDevices.isDeviceChangeAvailable(),
|
||||
hasAudioPermission: JitsiMeetJS.mediaDevices
|
||||
.isDevicePermissionGranted('audio'),
|
||||
hasVideoPermission: JitsiMeetJS.mediaDevices
|
||||
.isDevicePermissionGranted('video'),
|
||||
hideAudioInputPreview: !JitsiMeetJS.isCollectingLocalStats(),
|
||||
hideAudioOutputSelect: !JitsiMeetJS.mediaDevices
|
||||
.isDeviceChangeAvailable('output')
|
||||
}));
|
||||
}
|
||||
|
||||
export default {
|
||||
init (emitter) {
|
||||
initHTML();
|
||||
@@ -170,12 +141,8 @@ export default {
|
||||
if (UIUtil.isSettingEnabled('devices')) {
|
||||
const wrapperId = 'deviceOptionsWrapper';
|
||||
|
||||
JitsiMeetJS.mediaDevices.isDeviceListAvailable()
|
||||
.then((isDeviceListAvailable) => {
|
||||
$('#deviceSelection').on('click', () => {
|
||||
_openDeviceSelectionModal(isDeviceListAvailable);
|
||||
});
|
||||
});
|
||||
$('#deviceSelection').on('click', () =>
|
||||
APP.store.dispatch(openDeviceSelectionDialog()));
|
||||
|
||||
// Only show the subtitle if this isn't the only setting section.
|
||||
if (interfaceConfig.SETTINGS_SECTIONS.length > 1)
|
||||
|
||||
@@ -157,11 +157,13 @@ const IndicatorFontSizes = {
|
||||
* @param position the position of the tooltip in relation to the element
|
||||
*/
|
||||
setTooltip(element, key, position) {
|
||||
if (element !== null) {
|
||||
element.setAttribute('data-tooltip', TOOLTIP_POSITIONS[position]);
|
||||
element.setAttribute('data-i18n', '[content]' + key);
|
||||
if (element) {
|
||||
const selector = element.jquery ? element : $(element);
|
||||
|
||||
APP.translation.translateElement($(element));
|
||||
selector.attr('data-tooltip', TOOLTIP_POSITIONS[position]);
|
||||
selector.attr('data-i18n', `[content]${key}`);
|
||||
|
||||
APP.translation.translateElement(selector);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* global $, APP, config */
|
||||
/* global $, APP */
|
||||
/* jshint -W101 */
|
||||
|
||||
import JitsiPopover from "../util/JitsiPopover";
|
||||
import VideoLayout from "./VideoLayout";
|
||||
import UIUtil from "../util/UIUtil";
|
||||
|
||||
/**
|
||||
@@ -34,7 +34,6 @@ function ConnectionIndicator(videoContainer, videoId) {
|
||||
this.bitrate = null;
|
||||
this.showMoreValue = false;
|
||||
this.resolution = null;
|
||||
this.isResolutionHD = null;
|
||||
this.transport = [];
|
||||
this.framerate = null;
|
||||
this.popover = null;
|
||||
@@ -401,10 +400,6 @@ ConnectionIndicator.prototype.updateConnectionQuality =
|
||||
let width = qualityToWidth.find(x => percent >= x.percent);
|
||||
this.fullIcon.style.width = width.width;
|
||||
|
||||
if (object && typeof object.isResolutionHD === 'boolean') {
|
||||
this.isResolutionHD = object.isResolutionHD;
|
||||
}
|
||||
this.updateResolutionIndicator();
|
||||
this.updatePopoverData();
|
||||
};
|
||||
|
||||
@@ -414,7 +409,6 @@ ConnectionIndicator.prototype.updateConnectionQuality =
|
||||
*/
|
||||
ConnectionIndicator.prototype.updateResolution = function (resolution) {
|
||||
this.resolution = resolution;
|
||||
this.updateResolutionIndicator();
|
||||
this.updatePopoverData();
|
||||
};
|
||||
|
||||
@@ -457,31 +451,6 @@ ConnectionIndicator.prototype.hideIndicator = function () {
|
||||
this.popover.forceHide();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the resolution indicator.
|
||||
*/
|
||||
ConnectionIndicator.prototype.updateResolutionIndicator = function () {
|
||||
|
||||
if (this.id !== null
|
||||
&& VideoLayout.isCurrentlyOnLarge(this.id)) {
|
||||
|
||||
let showResolutionLabel = false;
|
||||
|
||||
if (this.isResolutionHD !== null)
|
||||
showResolutionLabel = this.isResolutionHD;
|
||||
else if (this.resolution !== null) {
|
||||
let resolutions = this.resolution || {};
|
||||
Object.keys(resolutions).map(function (ssrc) {
|
||||
const { height } = resolutions[ssrc];
|
||||
if (height >= config.minHDHeight)
|
||||
showResolutionLabel = true;
|
||||
});
|
||||
}
|
||||
|
||||
VideoLayout.updateResolutionLabel(showResolutionLabel);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a hover listener to the popover.
|
||||
*/
|
||||
|
||||
@@ -15,18 +15,19 @@ const Filmstrip = {
|
||||
this.filmstripContainerClassName = 'filmstrip';
|
||||
this.filmstrip = $('#remoteVideos');
|
||||
this.eventEmitter = eventEmitter;
|
||||
this._initFilmstripToolbar();
|
||||
this.registerListeners();
|
||||
|
||||
// Show the toggle button and add event listeners only when out of
|
||||
// filmstrip only mode.
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
this._initFilmstripToolbar();
|
||||
this.registerListeners();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes the filmstrip toolbar.
|
||||
*/
|
||||
_initFilmstripToolbar() {
|
||||
// Do not show the toggle button in filmstrip only mode.
|
||||
if (interfaceConfig.filmStripOnly)
|
||||
return;
|
||||
|
||||
let toolbarContainerHTML = this._generateToolbarHTML();
|
||||
let className = this.filmstripContainerClassName;
|
||||
let container = document.querySelector(`.${className}`);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* global $, APP, JitsiMeetJS */
|
||||
/* global $, APP, config, JitsiMeetJS */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import { setLargeVideoHDStatus } from '../../../react/features/base/conference';
|
||||
|
||||
import Avatar from "../avatar/Avatar";
|
||||
import {createDeferred} from '../../util/helpers';
|
||||
import UIEvents from "../../../service/UI/UIEvents";
|
||||
@@ -11,6 +13,14 @@ import AudioLevels from "../audio_levels/AudioLevels";
|
||||
|
||||
const ParticipantConnectionStatus
|
||||
= JitsiMeetJS.constants.participantConnectionStatus;
|
||||
const DESKTOP_CONTAINER_TYPE = 'desktop';
|
||||
/**
|
||||
* The time interval in milliseconds to check the video resolution of the video
|
||||
* being displayed.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
const VIDEO_RESOLUTION_POLL_INTERVAL = 2000;
|
||||
|
||||
/**
|
||||
* Manager for all Large containers.
|
||||
@@ -33,7 +43,7 @@ export default class LargeVideoManager {
|
||||
this.addContainer(VIDEO_CONTAINER_TYPE, this.videoContainer);
|
||||
|
||||
// use the same video container to handle desktop tracks
|
||||
this.addContainer("desktop", this.videoContainer);
|
||||
this.addContainer(DESKTOP_CONTAINER_TYPE, this.videoContainer);
|
||||
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
@@ -48,6 +58,39 @@ export default class LargeVideoManager {
|
||||
e => this.onHoverIn(e),
|
||||
e => this.onHoverOut(e)
|
||||
);
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._updateVideoResolutionStatus
|
||||
= this._updateVideoResolutionStatus.bind(this);
|
||||
|
||||
this.videoContainer.addResizeListener(
|
||||
this._updateVideoResolutionStatus);
|
||||
|
||||
if (!JitsiMeetJS.util.RTCUIHelper.isResizeEventSupported()) {
|
||||
/**
|
||||
* An interval for polling if the displayed video resolution is or
|
||||
* is not high-definition. For browsers that do not support video
|
||||
* resize events, polling is the fallback.
|
||||
*
|
||||
* @private
|
||||
* @type {timeoutId}
|
||||
*/
|
||||
this._updateVideoResolutionInterval = window.setInterval(
|
||||
this._updateVideoResolutionStatus,
|
||||
VIDEO_RESOLUTION_POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops any polling intervals on the instance and and removes any
|
||||
* listeners registered on child components.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
destroy() {
|
||||
window.clearInterval(this._updateVideoResolutionInterval);
|
||||
this.videoContainer.removeResizeListener(
|
||||
this._updateVideoResolutionStatus);
|
||||
}
|
||||
|
||||
onHoverIn (e) {
|
||||
@@ -103,6 +146,8 @@ export default class LargeVideoManager {
|
||||
|
||||
preUpdate.then(() => {
|
||||
const { id, stream, videoType, resolve } = this.newStreamData;
|
||||
const isVideoFromCamera = videoType === VIDEO_CONTAINER_TYPE;
|
||||
|
||||
this.newStreamData = null;
|
||||
|
||||
logger.info("hover in %s", id);
|
||||
@@ -120,9 +165,7 @@ export default class LargeVideoManager {
|
||||
// If the container is VIDEO_CONTAINER_TYPE, we need to check
|
||||
// its stream whether exist and is muted to set isVideoMuted
|
||||
// in rest of the cases it is false
|
||||
let showAvatar
|
||||
= (videoType === VIDEO_CONTAINER_TYPE)
|
||||
&& (!stream || stream.isMuted());
|
||||
let showAvatar = isVideoFromCamera && (!stream || stream.isMuted());
|
||||
|
||||
// If the user's connection is disrupted then the avatar will be
|
||||
// displayed in case we have no video image cached. That is if
|
||||
@@ -130,12 +173,20 @@ export default class LargeVideoManager {
|
||||
// the video was not rendered, before the connection has failed.
|
||||
const isConnectionActive = this._isConnectionActive(id);
|
||||
|
||||
if (videoType === VIDEO_CONTAINER_TYPE
|
||||
if (isVideoFromCamera
|
||||
&& !isConnectionActive
|
||||
&& (isUserSwitch || !container.wasVideoRendered)) {
|
||||
showAvatar = true;
|
||||
}
|
||||
|
||||
// If audio only mode is enabled, always show the avatar for
|
||||
// videos from another participant.
|
||||
if (APP.conference.isAudioOnly()
|
||||
&& (isVideoFromCamera
|
||||
|| videoType === DESKTOP_CONTAINER_TYPE)) {
|
||||
showAvatar = true;
|
||||
}
|
||||
|
||||
let promise;
|
||||
|
||||
// do not show stream if video is muted
|
||||
@@ -159,8 +210,12 @@ export default class LargeVideoManager {
|
||||
|
||||
// Make sure no notification about remote failure is shown as
|
||||
// its UI conflicts with the one for local connection interrupted.
|
||||
const isConnected = APP.conference.isConnectionInterrupted()
|
||||
|| isConnectionActive;
|
||||
// For the purposes of UI indicators, audio only is considered as
|
||||
// an "active" connection.
|
||||
const isConnected
|
||||
= APP.conference.isAudioOnly()
|
||||
|| APP.conference.isConnectionInterrupted()
|
||||
|| isConnectionActive;
|
||||
|
||||
// when isHavingConnectivityIssues, state can be inactive,
|
||||
// interrupted or restoring. We show different message for
|
||||
@@ -504,4 +559,18 @@ export default class LargeVideoManager {
|
||||
onLocalFlipXChange(val) {
|
||||
this.videoContainer.setLocalFlipX(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to update the known resolution state of the
|
||||
* large video.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateVideoResolutionStatus() {
|
||||
const { height, width } = this.videoContainer.getStreamSize();
|
||||
const isCurrentlyHD = Math.min(height, width) >= config.minHDHeight;
|
||||
|
||||
APP.store.dispatch(setLargeVideoHDStatus(isCurrentlyHD));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,12 +375,14 @@ RemoteVideo.prototype._generatePopupMenuSliderItem = function (options) {
|
||||
<span class='popupmenu__icon'>
|
||||
<i class=${options.icon}></i>
|
||||
</span>
|
||||
<input class='popupmenu__slider'
|
||||
type='range'
|
||||
min='0'
|
||||
max=${options.maxValue || 100}
|
||||
value=${options.initialValue || 0}>
|
||||
</input>
|
||||
<div class='popupmenu__slider_container'>
|
||||
<input class='popupmenu__slider'
|
||||
type='range'
|
||||
min='0'
|
||||
max=${options.maxValue || 100}
|
||||
value=${options.initialValue || 0}>
|
||||
</input>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const menuItem = document.createElement('li');
|
||||
@@ -556,6 +558,7 @@ RemoteVideo.prototype.isVideoPlayable = function () {
|
||||
* @inheritDoc
|
||||
*/
|
||||
RemoteVideo.prototype.updateView = function () {
|
||||
$(this.container).toggleClass('audio-only', APP.conference.isAudioOnly());
|
||||
|
||||
this.updateConnectionStatusIndicator();
|
||||
|
||||
|
||||
@@ -459,7 +459,9 @@ SmallVideo.prototype.selectDisplayMode = function() {
|
||||
// Display name is always and only displayed when user is on the stage
|
||||
if (this.isCurrentlyOnLargeVideo()) {
|
||||
return DISPLAY_BLACKNESS_WITH_NAME;
|
||||
} else if (this.isVideoPlayable() && this.selectVideoElement().length) {
|
||||
} else if (this.isVideoPlayable()
|
||||
&& this.selectVideoElement().length
|
||||
&& !APP.conference.isAudioOnly()) {
|
||||
// check hovering and change state to video with name
|
||||
return this._isHovered() ?
|
||||
DISPLAY_VIDEO_WITH_NAME : DISPLAY_VIDEO;
|
||||
|
||||
@@ -216,6 +216,28 @@ export class VideoContainer extends LargeContainer {
|
||||
// copied between new <object> elements
|
||||
//this.$video.on('play', onPlay);
|
||||
this.$video[0].onplay = onPlayCallback;
|
||||
|
||||
/**
|
||||
* A Set of functions to invoke when the video element resizes.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
this._resizeListeners = new Set();
|
||||
|
||||
// As of May 16, 2017, temasys does not support resize events.
|
||||
this.$video[0].onresize = this._onResize.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a function to the known subscribers of video element resize
|
||||
* events.
|
||||
*
|
||||
* @param {Function} callback - The subscriber to notify when the video
|
||||
* element resizes.
|
||||
* @returns {void}
|
||||
*/
|
||||
addResizeListener(callback) {
|
||||
this._resizeListeners.add(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,6 +366,18 @@ export class VideoContainer extends LargeContainer {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a function from the known subscribers of video element resize
|
||||
* events.
|
||||
*
|
||||
* @param {Function} callback - The callback to remove from known
|
||||
* subscribers of video resize events.
|
||||
* @returns {void}
|
||||
*/
|
||||
removeResizeListener(callback) {
|
||||
this._resizeListeners.delete(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update video stream.
|
||||
* @param {JitsiTrack?} stream new stream
|
||||
@@ -502,4 +536,14 @@ export class VideoContainer extends LargeContainer {
|
||||
(this.videoType === VIDEO_CONTAINER_TYPE && !isAvatar)
|
||||
? "#000" : interfaceConfig.DEFAULT_BACKGROUND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback invoked when the video element changes dimensions.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onResize() {
|
||||
this._resizeListeners.forEach(callback => callback());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,18 @@ var VideoLayout = {
|
||||
this.registerListeners();
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleans up any existing largeVideo instance.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
resetLargeVideo() {
|
||||
if (largeVideo) {
|
||||
largeVideo.destroy();
|
||||
}
|
||||
largeVideo = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Registering listeners for UI events in Video layout component.
|
||||
*
|
||||
@@ -132,6 +144,8 @@ var VideoLayout = {
|
||||
},
|
||||
|
||||
initLargeVideo () {
|
||||
this.resetLargeVideo();
|
||||
|
||||
largeVideo = new LargeVideoManager(eventEmitter);
|
||||
if(localFlipX) {
|
||||
largeVideo.onLocalFlipXChange(localFlipX);
|
||||
@@ -176,9 +190,7 @@ var VideoLayout = {
|
||||
let localId = APP.conference.getMyUserId();
|
||||
this.onVideoTypeChanged(localId, stream.videoType);
|
||||
|
||||
if (!stream.isMuted()) {
|
||||
localVideoThumbnail.changeVideo(stream);
|
||||
}
|
||||
localVideoThumbnail.changeVideo(stream);
|
||||
|
||||
/* force update if we're currently being displayed */
|
||||
if (this.isCurrentlyOnLarge(localId)) {
|
||||
@@ -956,6 +968,24 @@ var VideoLayout = {
|
||||
return largeVideo && largeVideo.id === id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers an update of remote video and large video displays so they may
|
||||
* pick up any state changes that have occurred elsewhere.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
updateAllVideos() {
|
||||
const displayedUserId = this.getLargeVideoID();
|
||||
|
||||
if (displayedUserId) {
|
||||
this.updateLargeVideo(displayedUserId, true);
|
||||
}
|
||||
|
||||
Object.keys(remoteVideos).forEach(video => {
|
||||
remoteVideos[video].updateView();
|
||||
});
|
||||
},
|
||||
|
||||
updateLargeVideo (id, forceUpdate) {
|
||||
if (!largeVideo) {
|
||||
return;
|
||||
@@ -1062,16 +1092,6 @@ var VideoLayout = {
|
||||
return largeVideo;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the resolution label, indicating to the user that the large
|
||||
* video stream is currently HD.
|
||||
*/
|
||||
updateResolutionLabel(isResolutionHD) {
|
||||
let id = 'videoResolutionLabel';
|
||||
|
||||
UIUtil.setVisible(id, isResolutionHD);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the flipX state of the local video.
|
||||
* @param {boolean} true for flipped otherwise false;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* global JitsiMeetJS, config, APP */
|
||||
|
||||
/**
|
||||
* Load the integration of a third-party analytics API such as Google
|
||||
* Analytics. Since we cannot guarantee the quality of the third-party service
|
||||
@@ -101,26 +102,37 @@ class Analytics {
|
||||
* null.
|
||||
*/
|
||||
init() {
|
||||
let analytics = JitsiMeetJS.analytics;
|
||||
if(!this.isEnabled() || !analytics)
|
||||
const { analytics } = JitsiMeetJS;
|
||||
|
||||
if (!this.isEnabled() || !analytics)
|
||||
return;
|
||||
|
||||
this._loadHandlers()
|
||||
.then(handlers => {
|
||||
let permanentProperties = {
|
||||
userAgent: navigator.userAgent,
|
||||
roomName: APP.conference.roomName
|
||||
this._loadHandlers().then(
|
||||
handlers => {
|
||||
const permanentProperties = {
|
||||
roomName: APP.conference.roomName,
|
||||
userAgent: navigator.userAgent
|
||||
};
|
||||
let {server, group} = APP.tokenData;
|
||||
if(server) {
|
||||
|
||||
const { group, server } = APP.store.getState()['features/jwt'];
|
||||
|
||||
if (server) {
|
||||
permanentProperties.server = server;
|
||||
}
|
||||
if(group) {
|
||||
if (group) {
|
||||
permanentProperties.group = group;
|
||||
}
|
||||
if (window.jitsiAnalyticsPermanentProperties) {
|
||||
for (var key in window.jitsiAnalyticsPermanentProperties) {
|
||||
permanentProperties[key]
|
||||
= window.jitsiAnalyticsPermanentProperties[key];
|
||||
}
|
||||
}
|
||||
|
||||
analytics.addPermanentProperties(permanentProperties);
|
||||
analytics.setAnalyticsHandlers(handlers);
|
||||
}, error => analytics.dispose() && console.error(error));
|
||||
},
|
||||
error => analytics.dispose() && console.error(error));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
var JSSHA = require('jssha');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Looks for a list of possible BOSH addresses in 'config.boshList' and
|
||||
* sets the value of 'config.bosh' based on that list and 'roomName'.
|
||||
* @param config the configuration object.
|
||||
* @param roomName the name of the room/conference.
|
||||
*/
|
||||
chooseAddress: function(config, roomName) {
|
||||
if (!roomName || !config.boshList || !Array.isArray(config.boshList) ||
|
||||
!config.boshList.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This implements the actual choice of an entry in the list based on
|
||||
// roomName. Please consider the implications for existing deployments
|
||||
// before introducing changes.
|
||||
var hash = (new JSSHA(roomName, 'TEXT')).getHash('SHA-1', 'HEX');
|
||||
var n = parseInt("0x"+hash.substr(-6));
|
||||
var idx = n % config.boshList.length;
|
||||
var attemptFirstAddress;
|
||||
|
||||
config.bosh = config.boshList[idx];
|
||||
logger.log('Setting config.bosh to ' + config.bosh +
|
||||
' (idx=' + idx + ')');
|
||||
|
||||
if (config.boshAttemptFirstList &&
|
||||
Array.isArray(config.boshAttemptFirstList) &&
|
||||
config.boshAttemptFirstList.length > 0) {
|
||||
|
||||
idx = n % config.boshAttemptFirstList.length;
|
||||
attemptFirstAddress = config.boshAttemptFirstList[idx];
|
||||
|
||||
if (attemptFirstAddress != config.bosh) {
|
||||
config.boshAttemptFirst = attemptFirstAddress;
|
||||
logger.log('Setting config.boshAttemptFirst=' +
|
||||
attemptFirstAddress + ' (idx=' + idx + ')');
|
||||
} else {
|
||||
logger.log('Not setting boshAttemptFirst, address matches.');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,52 +0,0 @@
|
||||
/* global $, config, interfaceConfig */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
var configUtil = require('./Util');
|
||||
|
||||
var HttpConfig = {
|
||||
/**
|
||||
* Sends HTTP POST request to specified <tt>endpoint</tt>. In request
|
||||
* the name of the room is included in JSON format:
|
||||
* {
|
||||
* "rooomName": "someroom12345"
|
||||
* }
|
||||
* @param endpoint the name of HTTP endpoint to which HTTP POST request will
|
||||
* be sent.
|
||||
* @param roomName the name of the conference room for which config will be
|
||||
* requested.
|
||||
* @param complete
|
||||
*/
|
||||
obtainConfig: function (endpoint, roomName, complete) {
|
||||
logger.info(
|
||||
"Send config request to " + endpoint + " for room: " + roomName);
|
||||
|
||||
|
||||
$.ajax(
|
||||
endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({"roomName": roomName}),
|
||||
dataType: 'json',
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
logger.error("Get config error: ", jqXHR, errorThrown);
|
||||
var error = "Get config response status: " + textStatus;
|
||||
complete(false, error);
|
||||
},
|
||||
success: function(data) {
|
||||
try {
|
||||
configUtil.overrideConfigJSON(
|
||||
config, interfaceConfig, data);
|
||||
complete(true);
|
||||
return;
|
||||
} catch (exception) {
|
||||
logger.error("Parse config error: ", exception);
|
||||
complete(false, exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = HttpConfig;
|
||||
@@ -1,66 +0,0 @@
|
||||
/* global config, interfaceConfig, loggingConfig, getConfigParamsFromUrl */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
var configUtils = require('./Util');
|
||||
var params = {};
|
||||
|
||||
params = getConfigParamsFromUrl();
|
||||
|
||||
var URLProcessor = {
|
||||
setConfigParametersFromUrl: function () {
|
||||
// Convert 'params' to JSON object
|
||||
// We have:
|
||||
// {
|
||||
// "config.disableAudioLevels": false,
|
||||
// "config.channelLastN": -1,
|
||||
// "interfaceConfig.APP_NAME": "Jitsi Meet"
|
||||
// }
|
||||
// We want to have:
|
||||
// {
|
||||
// "config": {
|
||||
// "disableAudioLevels": false,
|
||||
// "channelLastN": -1
|
||||
// },
|
||||
// interfaceConfig: {
|
||||
// APP_NAME: "Jitsi Meet"
|
||||
// }
|
||||
// }
|
||||
var configJSON = {
|
||||
config: {},
|
||||
interfaceConfig: {},
|
||||
loggingConfig: {}
|
||||
};
|
||||
for (var key in params) {
|
||||
if (typeof key !== "string") {
|
||||
logger.warn("Invalid config key: ", key);
|
||||
continue;
|
||||
}
|
||||
var confObj = null, confKey;
|
||||
if (key.indexOf("config.") === 0) {
|
||||
confObj = configJSON.config;
|
||||
confKey = key.substr("config.".length);
|
||||
|
||||
// prevent passing some parameters which can inject scripts
|
||||
if (confKey === 'analyticsScriptUrls'
|
||||
|| confKey === 'callStatsCustomScriptUrl')
|
||||
continue;
|
||||
|
||||
} else if (key.indexOf("interfaceConfig.") === 0) {
|
||||
confObj = configJSON.interfaceConfig;
|
||||
confKey = key.substr("interfaceConfig.".length);
|
||||
} else if (key.indexOf("loggingConfig.") === 0) {
|
||||
confObj = configJSON.loggingConfig;
|
||||
confKey = key.substr("loggingConfig.".length);
|
||||
}
|
||||
|
||||
if (!confObj)
|
||||
continue;
|
||||
|
||||
confObj[confKey] = params[key];
|
||||
}
|
||||
configUtils.overrideConfigJSON(
|
||||
config, interfaceConfig, loggingConfig, configJSON);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = URLProcessor;
|
||||
@@ -1,54 +0,0 @@
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
var ConfigUtil = {
|
||||
/**
|
||||
* Method overrides JSON properties in <tt>config</tt> and
|
||||
* <tt>interfaceConfig</tt> Objects with the values from <tt>newConfig</tt>
|
||||
* @param config the config object for which we'll be overriding properties
|
||||
* @param interfaceConfig the interfaceConfig object for which we'll be
|
||||
* overriding properties.
|
||||
* @param loggingConfig the logging config object for which we'll be
|
||||
* overriding properties.
|
||||
* @param newConfig object containing configuration properties. Destination
|
||||
* object is selected based on root property name:
|
||||
* {
|
||||
* config: {
|
||||
* // config.js properties to be
|
||||
* },
|
||||
* interfaceConfig: {
|
||||
* // interface_config.js properties here
|
||||
* },
|
||||
* loggingConfig: {
|
||||
* // logging_config.js properties here
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
overrideConfigJSON: function (config,
|
||||
interfaceConfig, loggingConfig, newConfig) {
|
||||
var configRoot, key, value, confObj;
|
||||
for (configRoot in newConfig) {
|
||||
confObj = null;
|
||||
if (configRoot == "config") {
|
||||
confObj = config;
|
||||
} else if (configRoot == "interfaceConfig") {
|
||||
confObj = interfaceConfig;
|
||||
} else if (configRoot == "loggingConfig") {
|
||||
confObj = loggingConfig;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (key in newConfig[configRoot]) {
|
||||
value = newConfig[configRoot][key];
|
||||
if (confObj[key] && typeof confObj[key] !== typeof value) {
|
||||
logger.log("Overriding a " + configRoot +
|
||||
" property with a property of different type.");
|
||||
}
|
||||
logger.info("Overriding " + key + " with: " + value);
|
||||
confObj[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = ConfigUtil;
|
||||
3
modules/remotecontrol/.eslintrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
'extends': '../../react/.eslintrc.js'
|
||||
};
|
||||
@@ -1,17 +1,29 @@
|
||||
/* global $, JitsiMeetJS, APP */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
import * as KeyCodes from "../keycode/keycode";
|
||||
import {EVENT_TYPES, REMOTE_CONTROL_EVENT_TYPE, PERMISSIONS_ACTIONS}
|
||||
from "../../service/remotecontrol/Constants";
|
||||
import RemoteControlParticipant from "./RemoteControlParticipant";
|
||||
import UIEvents from "../../service/UI/UIEvents";
|
||||
/* @flow */
|
||||
|
||||
import { getLogger } from 'jitsi-meet-logger';
|
||||
|
||||
import * as KeyCodes from '../keycode/keycode';
|
||||
import {
|
||||
EVENT_TYPES,
|
||||
PERMISSIONS_ACTIONS,
|
||||
REMOTE_CONTROL_EVENT_NAME
|
||||
} from '../../service/remotecontrol/Constants';
|
||||
import UIEvents from '../../service/UI/UIEvents';
|
||||
|
||||
import RemoteControlParticipant from './RemoteControlParticipant';
|
||||
|
||||
declare var $: Function;
|
||||
declare var APP: Object;
|
||||
declare var JitsiMeetJS: Object;
|
||||
|
||||
const ConferenceEvents = JitsiMeetJS.events.conference;
|
||||
const logger = getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Extract the keyboard key from the keyboard event.
|
||||
* @param event {KeyboardEvent} the event.
|
||||
* @returns {KEYS} the key that is pressed or undefined.
|
||||
*
|
||||
* @param {KeyboardEvent} event - The event.
|
||||
* @returns {KEYS} The key that is pressed or undefined.
|
||||
*/
|
||||
function getKey(event) {
|
||||
return KeyCodes.keyboardEventToKey(event);
|
||||
@@ -19,26 +31,28 @@ function getKey(event) {
|
||||
|
||||
/**
|
||||
* Extract the modifiers from the keyboard event.
|
||||
* @param event {KeyboardEvent} the event.
|
||||
* @returns {Array} with possible values: "shift", "control", "alt", "command".
|
||||
*
|
||||
* @param {KeyboardEvent} event - The event.
|
||||
* @returns {Array} With possible values: "shift", "control", "alt", "command".
|
||||
*/
|
||||
function getModifiers(event) {
|
||||
let modifiers = [];
|
||||
if(event.shiftKey) {
|
||||
modifiers.push("shift");
|
||||
const modifiers = [];
|
||||
|
||||
if (event.shiftKey) {
|
||||
modifiers.push('shift');
|
||||
}
|
||||
|
||||
if(event.ctrlKey) {
|
||||
modifiers.push("control");
|
||||
if (event.ctrlKey) {
|
||||
modifiers.push('control');
|
||||
}
|
||||
|
||||
|
||||
if(event.altKey) {
|
||||
modifiers.push("alt");
|
||||
if (event.altKey) {
|
||||
modifiers.push('alt');
|
||||
}
|
||||
|
||||
if(event.metaKey) {
|
||||
modifiers.push("command");
|
||||
if (event.metaKey) {
|
||||
modifiers.push('command');
|
||||
}
|
||||
|
||||
return modifiers;
|
||||
@@ -50,14 +64,22 @@ function getModifiers(event) {
|
||||
* party of the remote control session.
|
||||
*/
|
||||
export default class Controller extends RemoteControlParticipant {
|
||||
_area: ?Object;
|
||||
_controlledParticipant: string | null;
|
||||
_isCollectingEvents: boolean;
|
||||
_largeVideoChangedListener: Function;
|
||||
_requestedParticipant: string | null;
|
||||
_stopListener: Function;
|
||||
_userLeftListener: Function;
|
||||
|
||||
/**
|
||||
* Creates new instance.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.isCollectingEvents = false;
|
||||
this.controlledParticipant = null;
|
||||
this.requestedParticipant = null;
|
||||
this._isCollectingEvents = false;
|
||||
this._controlledParticipant = null;
|
||||
this._requestedParticipant = null;
|
||||
this._stopListener = this._handleRemoteControlStoppedEvent.bind(this);
|
||||
this._userLeftListener = this._onUserLeft.bind(this);
|
||||
this._largeVideoChangedListener
|
||||
@@ -66,24 +88,28 @@ export default class Controller extends RemoteControlParticipant {
|
||||
|
||||
/**
|
||||
* Requests permissions from the remote control receiver side.
|
||||
* @param {string} userId the user id of the participant that will be
|
||||
*
|
||||
* @param {string} userId - The user id of the participant that will be
|
||||
* requested.
|
||||
* @param {JQuerySelector} eventCaptureArea the area that is going to be
|
||||
* @param {JQuerySelector} eventCaptureArea - The area that is going to be
|
||||
* used mouse and keyboard event capture.
|
||||
* @returns {Promise<boolean>} - resolve values:
|
||||
* true - accept
|
||||
* false - deny
|
||||
* null - the participant has left.
|
||||
* @returns {Promise<boolean>} Resolve values - true(accept), false(deny),
|
||||
* null(the participant has left).
|
||||
*/
|
||||
requestPermissions(userId, eventCaptureArea) {
|
||||
if(!this.enabled) {
|
||||
return Promise.reject(new Error("Remote control is disabled!"));
|
||||
requestPermissions(userId: string, eventCaptureArea: Object) {
|
||||
if (!this._enabled) {
|
||||
return Promise.reject(new Error('Remote control is disabled!'));
|
||||
}
|
||||
this.area = eventCaptureArea;// $("#largeVideoWrapper")
|
||||
logger.log("Requsting remote control permissions from: " + userId);
|
||||
|
||||
this._area = eventCaptureArea;// $("#largeVideoWrapper")
|
||||
logger.log(`Requsting remote control permissions from: ${userId}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let onUserLeft, permissionsReplyListener;
|
||||
|
||||
const clearRequest = () => {
|
||||
this.requestedParticipant = null;
|
||||
this._requestedParticipant = null;
|
||||
APP.conference.removeConferenceListener(
|
||||
ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
permissionsReplyListener);
|
||||
@@ -91,31 +117,35 @@ export default class Controller extends RemoteControlParticipant {
|
||||
ConferenceEvents.USER_LEFT,
|
||||
onUserLeft);
|
||||
};
|
||||
const permissionsReplyListener = (participant, event) => {
|
||||
|
||||
permissionsReplyListener = (participant, event) => {
|
||||
let result = null;
|
||||
|
||||
try {
|
||||
result = this._handleReply(participant, event);
|
||||
} catch (e) {
|
||||
clearRequest();
|
||||
reject(e);
|
||||
}
|
||||
if(result !== null) {
|
||||
if (result !== null) {
|
||||
clearRequest();
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
const onUserLeft = (id) => {
|
||||
if(id === this.requestedParticipant) {
|
||||
onUserLeft = id => {
|
||||
if (id === this._requestedParticipant) {
|
||||
clearRequest();
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
APP.conference.addConferenceListener(
|
||||
ConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
permissionsReplyListener);
|
||||
APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT,
|
||||
onUserLeft);
|
||||
this.requestedParticipant = userId;
|
||||
this._sendRemoteControlEvent(userId, {
|
||||
this._requestedParticipant = userId;
|
||||
this.sendRemoteControlEvent(userId, {
|
||||
type: EVENT_TYPES.permissions,
|
||||
action: PERMISSIONS_ACTIONS.request
|
||||
}, e => {
|
||||
@@ -127,53 +157,58 @@ export default class Controller extends RemoteControlParticipant {
|
||||
|
||||
/**
|
||||
* Handles the reply of the permissions request.
|
||||
* @param {JitsiParticipant} participant the participant that has sent the
|
||||
* reply
|
||||
* @param {RemoteControlEvent} event the remote control event.
|
||||
*
|
||||
* @param {JitsiParticipant} participant - The participant that has sent the
|
||||
* reply.
|
||||
* @param {RemoteControlEvent} event - The remote control event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleReply(participant, event) {
|
||||
const remoteControlEvent = event.event;
|
||||
_handleReply(participant: Object, event: Object) {
|
||||
const userId = participant.getId();
|
||||
if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE
|
||||
&& remoteControlEvent.type === EVENT_TYPES.permissions
|
||||
&& userId === this.requestedParticipant) {
|
||||
if(remoteControlEvent.action !== PERMISSIONS_ACTIONS.grant) {
|
||||
this.area = null;
|
||||
|
||||
if (this._enabled
|
||||
&& event.name === REMOTE_CONTROL_EVENT_NAME
|
||||
&& event.type === EVENT_TYPES.permissions
|
||||
&& userId === this._requestedParticipant) {
|
||||
if (event.action !== PERMISSIONS_ACTIONS.grant) {
|
||||
this._area = undefined;
|
||||
}
|
||||
switch(remoteControlEvent.action) {
|
||||
case PERMISSIONS_ACTIONS.grant: {
|
||||
this.controlledParticipant = userId;
|
||||
logger.log("Remote control permissions granted to: "
|
||||
+ userId);
|
||||
this._start();
|
||||
return true;
|
||||
}
|
||||
case PERMISSIONS_ACTIONS.deny:
|
||||
return false;
|
||||
case PERMISSIONS_ACTIONS.error:
|
||||
throw new Error("Error occurred on receiver side");
|
||||
default:
|
||||
throw new Error("Unknown reply received!");
|
||||
switch (event.action) {
|
||||
case PERMISSIONS_ACTIONS.grant: {
|
||||
this._controlledParticipant = userId;
|
||||
logger.log('Remote control permissions granted to:', userId);
|
||||
this._start();
|
||||
|
||||
return true;
|
||||
}
|
||||
case PERMISSIONS_ACTIONS.deny:
|
||||
return false;
|
||||
case PERMISSIONS_ACTIONS.error:
|
||||
throw new Error('Error occurred on receiver side');
|
||||
default:
|
||||
throw new Error('Unknown reply received!');
|
||||
}
|
||||
} else {
|
||||
//different message type or another user -> ignoring the message
|
||||
// different message type or another user -> ignoring the message
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles remote control stopped.
|
||||
* @param {JitsiParticipant} participant the participant that has sent the
|
||||
* event
|
||||
* @param {Object} event EndpointMessage event from the data channels.
|
||||
* @property {string} type property. The function process only events of
|
||||
* type REMOTE_CONTROL_EVENT_TYPE
|
||||
* @property {RemoteControlEvent} event - the remote control event.
|
||||
*
|
||||
* @param {JitsiParticipant} participant - The participant that has sent the
|
||||
* event.
|
||||
* @param {Object} event - EndpointMessage event from the data channels.
|
||||
* @property {string} type - The function process only events with
|
||||
* name REMOTE_CONTROL_EVENT_NAME.
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleRemoteControlStoppedEvent(participant, event) {
|
||||
if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE
|
||||
&& event.event.type === EVENT_TYPES.stop
|
||||
&& participant.getId() === this.controlledParticipant) {
|
||||
_handleRemoteControlStoppedEvent(participant: Object, event: Object) {
|
||||
if (this._enabled
|
||||
&& event.name === REMOTE_CONTROL_EVENT_NAME
|
||||
&& event.type === EVENT_TYPES.stop
|
||||
&& participant.getId() === this._controlledParticipant) {
|
||||
this._stop();
|
||||
}
|
||||
}
|
||||
@@ -181,9 +216,11 @@ export default class Controller extends RemoteControlParticipant {
|
||||
/**
|
||||
* Starts processing the mouse and keyboard events. Sets conference
|
||||
* listeners. Disables keyboard events.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_start() {
|
||||
logger.log("Starting remote control controller.");
|
||||
logger.log('Starting remote control controller.');
|
||||
APP.UI.addListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
|
||||
this._largeVideoChangedListener);
|
||||
APP.conference.addConferenceListener(
|
||||
@@ -196,35 +233,53 @@ export default class Controller extends RemoteControlParticipant {
|
||||
|
||||
/**
|
||||
* Disables the keyboatd shortcuts. Starts collecting remote control
|
||||
* events.
|
||||
* events. It can be used to resume an active remote control session wchich
|
||||
* was paused with this.pause().
|
||||
*
|
||||
* It can be used to resume an active remote control session wchich was
|
||||
* paused with this.pause().
|
||||
* @returns {void}
|
||||
*/
|
||||
resume() {
|
||||
if(!this.enabled || this.isCollectingEvents) {
|
||||
if (!this._enabled || this._isCollectingEvents || !this._area) {
|
||||
return;
|
||||
}
|
||||
logger.log("Resuming remote control controller.");
|
||||
this.isCollectingEvents = true;
|
||||
logger.log('Resuming remote control controller.');
|
||||
this._isCollectingEvents = true;
|
||||
APP.keyboardshortcut.enable(false);
|
||||
this.area.mousemove(event => {
|
||||
const position = this.area.position();
|
||||
this._sendRemoteControlEvent(this.controlledParticipant, {
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
this._area.mousemove(event => {
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
const position = this._area.position();
|
||||
|
||||
this.sendRemoteControlEvent(this._controlledParticipant, {
|
||||
type: EVENT_TYPES.mousemove,
|
||||
x: (event.pageX - position.left)/this.area.width(),
|
||||
y: (event.pageY - position.top)/this.area.height()
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null
|
||||
x: (event.pageX - position.left) / this._area.width(),
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null
|
||||
y: (event.pageY - position.top) / this._area.height()
|
||||
});
|
||||
});
|
||||
this.area.mousedown(this._onMouseClickHandler.bind(this,
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
this._area.mousedown(this._onMouseClickHandler.bind(this,
|
||||
EVENT_TYPES.mousedown));
|
||||
this.area.mouseup(this._onMouseClickHandler.bind(this,
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
this._area.mouseup(this._onMouseClickHandler.bind(this,
|
||||
EVENT_TYPES.mouseup));
|
||||
this.area.dblclick(
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
this._area.dblclick(
|
||||
this._onMouseClickHandler.bind(this, EVENT_TYPES.mousedblclick));
|
||||
this.area.contextmenu(() => false);
|
||||
this.area[0].onmousewheel = event => {
|
||||
this._sendRemoteControlEvent(this.controlledParticipant, {
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
this._area.contextmenu(() => false);
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
this._area[0].onmousewheel = event => {
|
||||
this.sendRemoteControlEvent(this._controlledParticipant, {
|
||||
type: EVENT_TYPES.mousescroll,
|
||||
x: event.deltaX,
|
||||
y: event.deltaY
|
||||
@@ -239,12 +294,14 @@ export default class Controller extends RemoteControlParticipant {
|
||||
* Stops processing the mouse and keyboard events. Removes added listeners.
|
||||
* Enables the keyboard shortcuts. Displays dialog to notify the user that
|
||||
* remote control session has ended.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_stop() {
|
||||
if(!this.controlledParticipant) {
|
||||
if (!this._controlledParticipant) {
|
||||
return;
|
||||
}
|
||||
logger.log("Stopping remote control controller.");
|
||||
logger.log('Stopping remote control controller.');
|
||||
APP.UI.removeListener(UIEvents.LARGE_VIDEO_ID_CHANGED,
|
||||
this._largeVideoChangedListener);
|
||||
APP.conference.removeConferenceListener(
|
||||
@@ -252,29 +309,28 @@ export default class Controller extends RemoteControlParticipant {
|
||||
this._stopListener);
|
||||
APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT,
|
||||
this._userLeftListener);
|
||||
this.controlledParticipant = null;
|
||||
this._controlledParticipant = null;
|
||||
this.pause();
|
||||
this.area = null;
|
||||
this._area = undefined;
|
||||
APP.UI.messageHandler.openMessageDialog(
|
||||
"dialog.remoteControlTitle",
|
||||
"dialog.remoteControlStopMessage"
|
||||
'dialog.remoteControlTitle',
|
||||
'dialog.remoteControlStopMessage'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes this._stop() mehtod:
|
||||
* Stops processing the mouse and keyboard events. Removes added listeners.
|
||||
* Enables the keyboard shortcuts. Displays dialog to notify the user that
|
||||
* remote control session has ended.
|
||||
* Executes this._stop() mehtod which stops processing the mouse and
|
||||
* keyboard events, removes added listeners, enables the keyboard shortcuts,
|
||||
* displays dialog to notify the user that remote control session has ended.
|
||||
* In addition sends stop message to the controlled participant.
|
||||
*
|
||||
* In addition:
|
||||
* Sends stop message to the controlled participant.
|
||||
* @returns {void}
|
||||
*/
|
||||
stop() {
|
||||
if(!this.controlledParticipant) {
|
||||
if (!this._controlledParticipant) {
|
||||
return;
|
||||
}
|
||||
this._sendRemoteControlEvent(this.controlledParticipant, {
|
||||
this.sendRemoteControlEvent(this._controlledParticipant, {
|
||||
type: EVENT_TYPES.stop
|
||||
});
|
||||
this._stop();
|
||||
@@ -284,88 +340,112 @@ export default class Controller extends RemoteControlParticipant {
|
||||
* Pauses the collecting of events and enables the keyboard shortcus. But
|
||||
* it doesn't removes any other listeners. Basically the remote control
|
||||
* session will be still active after this.pause(), but no events from the
|
||||
* controller side will be captured and sent.
|
||||
* controller side will be captured and sent. You can resume the collecting
|
||||
* of the events with this.resume().
|
||||
*
|
||||
* You can resume the collecting of the events with this.resume().
|
||||
* @returns {void}
|
||||
*/
|
||||
pause() {
|
||||
if(!this.controlledParticipant) {
|
||||
if (!this._controlledParticipant) {
|
||||
return;
|
||||
}
|
||||
logger.log("Pausing remote control controller.");
|
||||
this.isCollectingEvents = false;
|
||||
logger.log('Pausing remote control controller.');
|
||||
this._isCollectingEvents = false;
|
||||
APP.keyboardshortcut.enable(true);
|
||||
this.area.off( "mousemove" );
|
||||
this.area.off( "mousedown" );
|
||||
this.area.off( "mouseup" );
|
||||
this.area.off( "contextmenu" );
|
||||
this.area.off( "dblclick" );
|
||||
$(window).off( "keydown");
|
||||
$(window).off( "keyup");
|
||||
this.area[0].onmousewheel = undefined;
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
this._area.off('mousemove');
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
this._area.off('mousedown');
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
this._area.off('mouseup');
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
this._area.off('contextmenu');
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
this._area.off('dblclick');
|
||||
|
||||
$(window).off('keydown');
|
||||
$(window).off('keyup');
|
||||
|
||||
// $FlowDisableNextLine: we are sure that this._area is not null.
|
||||
this._area[0].onmousewheel = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for mouse click events.
|
||||
* @param {String} type the type of event ("mousedown"/"mouseup")
|
||||
* @param {Event} event the mouse event.
|
||||
*
|
||||
* @param {string} type - The type of event ("mousedown"/"mouseup").
|
||||
* @param {Event} event - The mouse event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMouseClickHandler(type, event) {
|
||||
this._sendRemoteControlEvent(this.controlledParticipant, {
|
||||
type: type,
|
||||
_onMouseClickHandler(type: string, event: Object) {
|
||||
this.sendRemoteControlEvent(this._controlledParticipant, {
|
||||
type,
|
||||
button: event.which
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the remote control session is started.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isStarted() {
|
||||
return this.controlledParticipant !== null;
|
||||
return this._controlledParticipant !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the id of the requested participant
|
||||
* @returns {string} this.requestedParticipant.
|
||||
* Returns the id of the requested participant.
|
||||
*
|
||||
* @returns {string} The id of the requested participant.
|
||||
* NOTE: This id should be the result of JitsiParticipant.getId() call.
|
||||
*/
|
||||
getRequestedParticipant() {
|
||||
return this.requestedParticipant;
|
||||
return this._requestedParticipant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for key press events.
|
||||
* @param {String} type the type of event ("keydown"/"keyup")
|
||||
* @param {Event} event the key event.
|
||||
*
|
||||
* @param {string} type - The type of event ("keydown"/"keyup").
|
||||
* @param {Event} event - The key event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyPessHandler(type, event) {
|
||||
this._sendRemoteControlEvent(this.controlledParticipant, {
|
||||
type: type,
|
||||
_onKeyPessHandler(type: string, event: Object) {
|
||||
this.sendRemoteControlEvent(this._controlledParticipant, {
|
||||
type,
|
||||
key: getKey(event),
|
||||
modifiers: getModifiers(event),
|
||||
modifiers: getModifiers(event)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the stop method if the other side have left.
|
||||
* @param {string} id - the user id for the participant that have left
|
||||
*
|
||||
* @param {string} id - The user id for the participant that have left.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onUserLeft(id) {
|
||||
if(this.controlledParticipant === id) {
|
||||
_onUserLeft(id: string) {
|
||||
if (this._controlledParticipant === id) {
|
||||
this._stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles changes of the participant displayed on the large video.
|
||||
* @param {string} id - the user id for the participant that is displayed.
|
||||
*
|
||||
* @param {string} id - The user id for the participant that is displayed.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLargeVideoIdChanged(id) {
|
||||
if (!this.controlledParticipant) {
|
||||
_onLargeVideoIdChanged(id: string) {
|
||||
if (!this._controlledParticipant) {
|
||||
return;
|
||||
}
|
||||
if(this.controlledParticipant == id) {
|
||||
if (this._controlledParticipant === id) {
|
||||
this.resume();
|
||||
} else {
|
||||
this.pause();
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
/* global APP, JitsiMeetJS, interfaceConfig */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
import {DISCO_REMOTE_CONTROL_FEATURE, REMOTE_CONTROL_EVENT_TYPE, EVENT_TYPES,
|
||||
PERMISSIONS_ACTIONS} from "../../service/remotecontrol/Constants";
|
||||
import RemoteControlParticipant from "./RemoteControlParticipant";
|
||||
/* @flow */
|
||||
|
||||
import { getLogger } from 'jitsi-meet-logger';
|
||||
|
||||
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
|
||||
import {
|
||||
openRemoteControlAuthorizationDialog
|
||||
} from '../../react/features/remote-control';
|
||||
import {
|
||||
DISCO_REMOTE_CONTROL_FEATURE,
|
||||
EVENT_TYPES,
|
||||
PERMISSIONS_ACTIONS,
|
||||
REMOTE_CONTROL_EVENT_NAME
|
||||
} from '../../service/remotecontrol/Constants';
|
||||
import { getJitsiMeetTransport } from '../transport';
|
||||
|
||||
import RemoteControlParticipant from './RemoteControlParticipant';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var config: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
declare var JitsiMeetJS: Object;
|
||||
|
||||
const ConferenceEvents = JitsiMeetJS.events.conference;
|
||||
const logger = getLogger(__filename);
|
||||
|
||||
/**
|
||||
* The transport instance used for communication with external apps.
|
||||
*
|
||||
* @type {Transport}
|
||||
*/
|
||||
const transport = getJitsiMeetTransport();
|
||||
|
||||
/**
|
||||
* This class represents the receiver party for a remote controller session.
|
||||
@@ -14,30 +38,50 @@ const ConferenceEvents = JitsiMeetJS.events.conference;
|
||||
* and executed.
|
||||
*/
|
||||
export default class Receiver extends RemoteControlParticipant {
|
||||
_controller: ?string;
|
||||
_enabled: boolean;
|
||||
_hangupListener: Function;
|
||||
_remoteControlEventsListener: Function;
|
||||
_userLeftListener: Function;
|
||||
|
||||
/**
|
||||
* Creates new instance.
|
||||
* @constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.controller = null;
|
||||
this._controller = null;
|
||||
this._remoteControlEventsListener
|
||||
= this._onRemoteControlEvent.bind(this);
|
||||
this._userLeftListener = this._onUserLeft.bind(this);
|
||||
this._hangupListener = this._onHangup.bind(this);
|
||||
|
||||
// We expect here that even if we receive the supported event earlier
|
||||
// it will be cached and we'll receive it.
|
||||
transport.on('event', event => {
|
||||
if (event.name === REMOTE_CONTROL_EVENT_NAME) {
|
||||
this._onRemoteControlAPIEvent(event);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables / Disables the remote control
|
||||
* @param {boolean} enabled the new state.
|
||||
* Enables / Disables the remote control.
|
||||
*
|
||||
* @param {boolean} enabled - The new state.
|
||||
* @returns {void}
|
||||
*/
|
||||
enable(enabled) {
|
||||
if(this.enabled === enabled) {
|
||||
_enable(enabled: boolean) {
|
||||
if (this._enabled === enabled) {
|
||||
return;
|
||||
}
|
||||
this.enabled = enabled;
|
||||
if(enabled === true) {
|
||||
logger.log("Remote control receiver enabled.");
|
||||
this._enabled = enabled;
|
||||
if (enabled === true) {
|
||||
logger.log('Remote control receiver enabled.');
|
||||
|
||||
// Announce remote control support.
|
||||
APP.connection.addFeature(DISCO_REMOTE_CONTROL_FEATURE, true);
|
||||
APP.conference.addConferenceListener(
|
||||
@@ -46,7 +90,7 @@ export default class Receiver extends RemoteControlParticipant {
|
||||
APP.conference.addListener(JitsiMeetConferenceEvents.BEFORE_HANGUP,
|
||||
this._hangupListener);
|
||||
} else {
|
||||
logger.log("Remote control receiver disabled.");
|
||||
logger.log('Remote control receiver disabled.');
|
||||
this._stop(true);
|
||||
APP.connection.removeFeature(DISCO_REMOTE_CONTROL_FEATURE);
|
||||
APP.conference.removeConferenceListener(
|
||||
@@ -63,35 +107,43 @@ export default class Receiver extends RemoteControlParticipant {
|
||||
* events. Sends stop message to the wrapper application. Optionally
|
||||
* displays dialog for informing the user that remote control session
|
||||
* ended.
|
||||
* @param {boolean} dontShowDialog - if true the dialog won't be displayed.
|
||||
*
|
||||
* @param {boolean} [dontShowDialog] - If true the dialog won't be
|
||||
* displayed.
|
||||
* @returns {void}
|
||||
*/
|
||||
_stop(dontShowDialog = false) {
|
||||
if(!this.controller) {
|
||||
_stop(dontShowDialog: boolean = false) {
|
||||
if (!this._controller) {
|
||||
return;
|
||||
}
|
||||
logger.log("Remote control receiver stop.");
|
||||
this.controller = null;
|
||||
logger.log('Remote control receiver stop.');
|
||||
this._controller = null;
|
||||
APP.conference.removeConferenceListener(ConferenceEvents.USER_LEFT,
|
||||
this._userLeftListener);
|
||||
APP.API.sendRemoteControlEvent({
|
||||
type: EVENT_TYPES.stop
|
||||
});
|
||||
if(!dontShowDialog) {
|
||||
if (this.remoteControlExternalAuth) {
|
||||
transport.sendEvent({
|
||||
name: REMOTE_CONTROL_EVENT_NAME,
|
||||
type: EVENT_TYPES.stop
|
||||
});
|
||||
}
|
||||
if (!dontShowDialog) {
|
||||
APP.UI.messageHandler.openMessageDialog(
|
||||
"dialog.remoteControlTitle",
|
||||
"dialog.remoteControlStopMessage"
|
||||
'dialog.remoteControlTitle',
|
||||
'dialog.remoteControlStopMessage'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls this._stop() and sends stop message to the controller participant
|
||||
* Calls this._stop() and sends stop message to the controller participant.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
stop() {
|
||||
if(!this.controller) {
|
||||
if (!this._controller) {
|
||||
return;
|
||||
}
|
||||
this._sendRemoteControlEvent(this.controller, {
|
||||
this.sendRemoteControlEvent(this._controller, {
|
||||
type: EVENT_TYPES.stop
|
||||
});
|
||||
this._stop();
|
||||
@@ -101,92 +153,187 @@ export default class Receiver extends RemoteControlParticipant {
|
||||
* Listens for data channel EndpointMessage events. Handles only events of
|
||||
* type remote control. Sends "remote-control-event" events to the API
|
||||
* module.
|
||||
* @param {JitsiParticipant} participant the controller participant
|
||||
* @param {Object} event EndpointMessage event from the data channels.
|
||||
* @property {string} type property. The function process only events of
|
||||
* type REMOTE_CONTROL_EVENT_TYPE
|
||||
* @property {RemoteControlEvent} event - the remote control event.
|
||||
*
|
||||
* @param {JitsiParticipant} participant - The controller participant.
|
||||
* @param {Object} event - EndpointMessage event from the data channels.
|
||||
* @param {string} event.name - The function process only events with
|
||||
* name REMOTE_CONTROL_EVENT_NAME.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRemoteControlEvent(participant, event) {
|
||||
if(this.enabled && event.type === REMOTE_CONTROL_EVENT_TYPE) {
|
||||
const remoteControlEvent = event.event;
|
||||
if(this.controller === null
|
||||
&& remoteControlEvent.type === EVENT_TYPES.permissions
|
||||
&& remoteControlEvent.action === PERMISSIONS_ACTIONS.request) {
|
||||
remoteControlEvent.userId = participant.getId();
|
||||
_onRemoteControlEvent(participant: Object, event: Object) {
|
||||
if (event.name !== REMOTE_CONTROL_EVENT_NAME) {
|
||||
return;
|
||||
}
|
||||
|
||||
const remoteControlEvent = Object.assign({}, event);
|
||||
|
||||
if (this._enabled) {
|
||||
if (this._controller === null
|
||||
&& event.type === EVENT_TYPES.permissions
|
||||
&& event.action === PERMISSIONS_ACTIONS.request) {
|
||||
const userId = participant.getId();
|
||||
|
||||
if (!config.remoteControlExternalAuth) {
|
||||
APP.store.dispatch(
|
||||
openRemoteControlAuthorizationDialog(userId));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: Maybe use transport.sendRequest in this case???
|
||||
remoteControlEvent.userId = userId;
|
||||
remoteControlEvent.userJID = participant.getJid();
|
||||
remoteControlEvent.displayName = participant.getDisplayName()
|
||||
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME;
|
||||
remoteControlEvent.screenSharing
|
||||
= APP.conference.isSharingScreen;
|
||||
} else if(this.controller !== participant.getId()) {
|
||||
} else if (this._controller !== participant.getId()) {
|
||||
return;
|
||||
} else if(remoteControlEvent.type === EVENT_TYPES.stop) {
|
||||
} else if (event.type === EVENT_TYPES.stop) {
|
||||
this._stop();
|
||||
|
||||
return;
|
||||
}
|
||||
APP.API.sendRemoteControlEvent(remoteControlEvent);
|
||||
} else if(event.type === REMOTE_CONTROL_EVENT_TYPE) {
|
||||
logger.log("Remote control event is ignored because remote "
|
||||
+ "control is disabled", event);
|
||||
transport.sendEvent(remoteControlEvent);
|
||||
} else {
|
||||
logger.log('Remote control event is ignored because remote '
|
||||
+ 'control is disabled', event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles remote control permission events received from the API module.
|
||||
* @param {String} userId the user id of the participant related to the
|
||||
* Handles remote control permission events.
|
||||
*
|
||||
* @param {string} userId - The user id of the participant related to the
|
||||
* event.
|
||||
* @param {PERMISSIONS_ACTIONS} action the action related to the event.
|
||||
* @param {PERMISSIONS_ACTIONS} action - The action related to the event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRemoteControlPermissionsEvent(userId, action) {
|
||||
if(action === PERMISSIONS_ACTIONS.grant) {
|
||||
APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT,
|
||||
this._userLeftListener);
|
||||
this.controller = userId;
|
||||
logger.log("Remote control permissions granted to: " + userId);
|
||||
if(!APP.conference.isSharingScreen) {
|
||||
APP.conference.toggleScreenSharing();
|
||||
APP.conference.screenSharingPromise.then(() => {
|
||||
if(APP.conference.isSharingScreen) {
|
||||
this._sendRemoteControlEvent(userId, {
|
||||
type: EVENT_TYPES.permissions,
|
||||
action: action
|
||||
});
|
||||
} else {
|
||||
this._sendRemoteControlEvent(userId, {
|
||||
type: EVENT_TYPES.permissions,
|
||||
action: PERMISSIONS_ACTIONS.error
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
this._sendRemoteControlEvent(userId, {
|
||||
type: EVENT_TYPES.permissions,
|
||||
action: PERMISSIONS_ACTIONS.error
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
_onRemoteControlPermissionsEvent(userId: string, action: string) {
|
||||
switch (action) {
|
||||
case PERMISSIONS_ACTIONS.grant:
|
||||
this.grant(userId);
|
||||
break;
|
||||
case PERMISSIONS_ACTIONS.deny:
|
||||
this.deny(userId);
|
||||
break;
|
||||
case PERMISSIONS_ACTIONS.error:
|
||||
this.sendRemoteControlEvent(userId, {
|
||||
type: EVENT_TYPES.permissions,
|
||||
action
|
||||
});
|
||||
break;
|
||||
default:
|
||||
|
||||
// Unknown action. Ignore.
|
||||
}
|
||||
this._sendRemoteControlEvent(userId, {
|
||||
}
|
||||
|
||||
/**
|
||||
* Denies remote control access for user associated with the passed user id.
|
||||
*
|
||||
* @param {string} userId - The id associated with the user who sent the
|
||||
* request for remote control authorization.
|
||||
* @returns {void}
|
||||
*/
|
||||
deny(userId: string) {
|
||||
this.sendRemoteControlEvent(userId, {
|
||||
type: EVENT_TYPES.permissions,
|
||||
action: action
|
||||
action: PERMISSIONS_ACTIONS.deny
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the stop method if the other side have left.
|
||||
* @param {string} id - the user id for the participant that have left
|
||||
* Grants remote control access to user associated with the passed user id.
|
||||
*
|
||||
* @param {string} userId - The id associated with the user who sent the
|
||||
* request for remote control authorization.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onUserLeft(id) {
|
||||
if(this.controller === id) {
|
||||
grant(userId: string) {
|
||||
APP.conference.addConferenceListener(ConferenceEvents.USER_LEFT,
|
||||
this._userLeftListener);
|
||||
this._controller = userId;
|
||||
logger.log(`Remote control permissions granted to: ${userId}`);
|
||||
if (APP.conference.isSharingScreen) {
|
||||
this.sendRemoteControlEvent(userId, {
|
||||
type: EVENT_TYPES.permissions,
|
||||
action: PERMISSIONS_ACTIONS.grant
|
||||
});
|
||||
} else {
|
||||
APP.conference.toggleScreenSharing();
|
||||
APP.conference.screenSharingPromise.then(() => {
|
||||
if (APP.conference.isSharingScreen) {
|
||||
this.sendRemoteControlEvent(userId, {
|
||||
type: EVENT_TYPES.permissions,
|
||||
action: PERMISSIONS_ACTIONS.grant
|
||||
});
|
||||
} else {
|
||||
this.sendRemoteControlEvent(userId, {
|
||||
type: EVENT_TYPES.permissions,
|
||||
action: PERMISSIONS_ACTIONS.error
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
this.sendRemoteControlEvent(userId, {
|
||||
type: EVENT_TYPES.permissions,
|
||||
action: PERMISSIONS_ACTIONS.error
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles remote control events from the external app. Currently only
|
||||
* events with type = EVENT_TYPES.supported or EVENT_TYPES.permissions.
|
||||
*
|
||||
* @param {RemoteControlEvent} event - The remote control event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRemoteControlAPIEvent(event: Object) {
|
||||
switch (event.type) {
|
||||
case EVENT_TYPES.permissions:
|
||||
this._onRemoteControlPermissionsEvent(event.userId, event.action);
|
||||
break;
|
||||
case EVENT_TYPES.supported:
|
||||
this._onRemoteControlSupported();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles events for support for executing remote control events into
|
||||
* the wrapper application.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onRemoteControlSupported() {
|
||||
logger.log('Remote Control supported.');
|
||||
if (config.disableRemoteControl) {
|
||||
logger.log('Remote Control disabled.');
|
||||
} else {
|
||||
this._enable(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the stop method if the other side have left.
|
||||
*
|
||||
* @param {string} id - The user id for the participant that have left.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onUserLeft(id: string) {
|
||||
if (this._controller === id) {
|
||||
this._stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles hangup events. Disables the receiver.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onHangup() {
|
||||
this.enable(false);
|
||||
this._enable(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,88 +1,63 @@
|
||||
/* global APP, config */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
import Controller from "./Controller";
|
||||
import Receiver from "./Receiver";
|
||||
import {EVENT_TYPES, DISCO_REMOTE_CONTROL_FEATURE}
|
||||
from "../../service/remotecontrol/Constants";
|
||||
/* @flow */
|
||||
|
||||
import { getLogger } from 'jitsi-meet-logger';
|
||||
|
||||
import { DISCO_REMOTE_CONTROL_FEATURE }
|
||||
from '../../service/remotecontrol/Constants';
|
||||
|
||||
import Controller from './Controller';
|
||||
import Receiver from './Receiver';
|
||||
|
||||
const logger = getLogger(__filename);
|
||||
|
||||
declare var APP: Object;
|
||||
declare var config: Object;
|
||||
|
||||
/**
|
||||
* Implements the remote control functionality.
|
||||
*/
|
||||
class RemoteControl {
|
||||
_initialized: boolean;
|
||||
controller: Controller;
|
||||
receiver: Receiver;
|
||||
|
||||
/**
|
||||
* Constructs new instance. Creates controller and receiver properties.
|
||||
* @constructor
|
||||
*/
|
||||
constructor() {
|
||||
this.controller = new Controller();
|
||||
this.receiver = new Receiver();
|
||||
this.enabled = false;
|
||||
this.initialized = false;
|
||||
this._initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the remote control - checks if the remote control should be
|
||||
* enabled or not, initializes the API module.
|
||||
* enabled or not.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
init() {
|
||||
if(config.disableRemoteControl || this.initialized
|
||||
|| !APP.conference.isDesktopSharingEnabled) {
|
||||
if (config.disableRemoteControl
|
||||
|| this._initialized
|
||||
|| !APP.conference.isDesktopSharingEnabled) {
|
||||
return;
|
||||
}
|
||||
logger.log("Initializing remote control.");
|
||||
this.initialized = true;
|
||||
APP.API.init({
|
||||
forceEnable: true,
|
||||
});
|
||||
logger.log('Initializing remote control.');
|
||||
this._initialized = true;
|
||||
this.controller.enable(true);
|
||||
if(this.enabled) { // supported message came before init.
|
||||
this._onRemoteControlSupported();
|
||||
}
|
||||
this.receiver = new Receiver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles remote control events from the API module. Currently only events
|
||||
* with type = EVENT_TYPES.supported or EVENT_TYPES.permissions
|
||||
* @param {RemoteControlEvent} event the remote control event.
|
||||
*/
|
||||
onRemoteControlAPIEvent(event) {
|
||||
switch(event.type) {
|
||||
case EVENT_TYPES.supported:
|
||||
this._onRemoteControlSupported();
|
||||
break;
|
||||
case EVENT_TYPES.permissions:
|
||||
this.receiver._onRemoteControlPermissionsEvent(
|
||||
event.userId, event.action);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles API event for support for executing remote control events into
|
||||
* the wrapper application.
|
||||
*/
|
||||
_onRemoteControlSupported() {
|
||||
logger.log("Remote Control supported.");
|
||||
if(!config.disableRemoteControl) {
|
||||
this.enabled = true;
|
||||
if(this.initialized) {
|
||||
this.receiver.enable(true);
|
||||
}
|
||||
} else {
|
||||
logger.log("Remote Control disabled.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the passed user supports remote control or not
|
||||
* @param {JitsiParticipant} user the user to be tested
|
||||
* @returns {Promise<boolean>} the promise will be resolved with true if
|
||||
* Checks whether the passed user supports remote control or not.
|
||||
*
|
||||
* @param {JitsiParticipant} user - The user to be tested.
|
||||
* @returns {Promise<boolean>} The promise will be resolved with true if
|
||||
* the user supports remote control and with false if not.
|
||||
*/
|
||||
checkUserRemoteControlSupport(user) {
|
||||
return user.getFeatures().then(features =>
|
||||
features.has(DISCO_REMOTE_CONTROL_FEATURE), () => false
|
||||
);
|
||||
checkUserRemoteControlSupport(user: Object) {
|
||||
return user.getFeatures().then(
|
||||
features => features.has(DISCO_REMOTE_CONTROL_FEATURE),
|
||||
() => false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +1,70 @@
|
||||
/* global APP */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
import {REMOTE_CONTROL_EVENT_TYPE}
|
||||
from "../../service/remotecontrol/Constants";
|
||||
/* @flow */
|
||||
|
||||
import { getLogger } from 'jitsi-meet-logger';
|
||||
|
||||
import {
|
||||
REMOTE_CONTROL_EVENT_NAME
|
||||
} from '../../service/remotecontrol/Constants';
|
||||
|
||||
const logger = getLogger(__filename);
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* Implements common logic for Receiver class and Controller class.
|
||||
*/
|
||||
export default class RemoteControlParticipant {
|
||||
_enabled: boolean;
|
||||
|
||||
/**
|
||||
* Creates new instance.
|
||||
*/
|
||||
constructor() {
|
||||
this.enabled = false;
|
||||
this._enabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables / Disables the remote control
|
||||
* @param {boolean} enabled the new state.
|
||||
* Enables / Disables the remote control.
|
||||
*
|
||||
* @param {boolean} enabled - The new state.
|
||||
* @returns {void}
|
||||
*/
|
||||
enable(enabled) {
|
||||
this.enabled = enabled;
|
||||
enable(enabled: boolean) {
|
||||
this._enabled = enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends remote control event to other participant trough data channel.
|
||||
* @param {RemoteControlEvent} event the remote control event.
|
||||
* @param {Function} onDataChannelFail handler for data channel failure.
|
||||
*
|
||||
* @param {string} to - The participant who will receive the event.
|
||||
* @param {RemoteControlEvent} event - The remote control event.
|
||||
* @param {Function} onDataChannelFail - Handler for data channel failure.
|
||||
* @returns {void}
|
||||
*/
|
||||
_sendRemoteControlEvent(to, event, onDataChannelFail = () => {}) {
|
||||
if(!this.enabled || !to) {
|
||||
logger.warn("Remote control: Skip sending remote control event."
|
||||
+ " Params:", this.enable, to);
|
||||
sendRemoteControlEvent(
|
||||
to: ?string,
|
||||
event: Object,
|
||||
onDataChannelFail: ?Function) {
|
||||
if (!this._enabled || !to) {
|
||||
logger.warn(
|
||||
'Remote control: Skip sending remote control event. Params:',
|
||||
this.enable,
|
||||
to);
|
||||
|
||||
return;
|
||||
}
|
||||
try{
|
||||
APP.conference.sendEndpointMessage(to,
|
||||
{type: REMOTE_CONTROL_EVENT_TYPE, event});
|
||||
try {
|
||||
APP.conference.sendEndpointMessage(to, {
|
||||
name: REMOTE_CONTROL_EVENT_NAME,
|
||||
...event
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Failed to send EndpointMessage via the datachannels",
|
||||
logger.error(
|
||||
'Failed to send EndpointMessage via the datachannels',
|
||||
e);
|
||||
onDataChannelFail(e);
|
||||
if (typeof onDataChannelFail === 'function') {
|
||||
onDataChannelFail(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
/* global getConfigParamsFromUrl, config */
|
||||
|
||||
/**
|
||||
* Parses and handles JWT tokens. Sets config.token.
|
||||
*/
|
||||
|
||||
import * as jws from "jws";
|
||||
|
||||
/**
|
||||
* Get the JWT token from the URL.
|
||||
*/
|
||||
let params = getConfigParamsFromUrl("search", true);
|
||||
let jwt = params.jwt;
|
||||
|
||||
/**
|
||||
* Implements a user of conference.
|
||||
*/
|
||||
class User {
|
||||
/**
|
||||
* @param name {string} the name of the user.
|
||||
* @param email {string} the email of the user.
|
||||
* @param avatarUrl {string} the URL for the avatar of the user.
|
||||
*/
|
||||
constructor(name, email, avatarUrl) {
|
||||
this._name = name;
|
||||
this._email = email;
|
||||
this._avatarUrl = avatarUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* GETERS START.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the name property
|
||||
*/
|
||||
getName() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email property
|
||||
*/
|
||||
getEmail() {
|
||||
return this._email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the avatar
|
||||
*/
|
||||
getAvatarUrl() {
|
||||
return this._avatarUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* GETERS END.
|
||||
*/
|
||||
}
|
||||
|
||||
/**
|
||||
* Represent the data parsed from the JWT token
|
||||
*/
|
||||
class TokenData{
|
||||
/**
|
||||
* @param {string} the JWT token
|
||||
*/
|
||||
constructor(jwt) {
|
||||
this.isGuest = true;
|
||||
if(!jwt)
|
||||
return;
|
||||
|
||||
this.isGuest = config.enableUserRolesBasedOnToken !== true;
|
||||
|
||||
this.jwt = jwt;
|
||||
|
||||
this._decode();
|
||||
// Use JWT param as token if there is not other token set and if the
|
||||
// iss field is not anonymous. If you want to pass data with JWT token
|
||||
// but you don't want to pass the JWT token for verification the iss
|
||||
// field should be set to "anonymous"
|
||||
if(!config.token && this.payload && this.payload.iss !== "anonymous")
|
||||
config.token = jwt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the JWT token and sets the decoded data to properties.
|
||||
*/
|
||||
_decode() {
|
||||
this.decodedJWT = jws.decode(jwt);
|
||||
if(!this.decodedJWT || !this.decodedJWT.payload)
|
||||
return;
|
||||
this.payload = this.decodedJWT.payload;
|
||||
if(!this.payload.context)
|
||||
return;
|
||||
this.server = this.payload.context.server;
|
||||
this.group = this.payload.context.group;
|
||||
let callerData = this.payload.context.user;
|
||||
let calleeData = this.payload.context.callee;
|
||||
if(callerData)
|
||||
this.caller = new User(callerData.name, callerData.email,
|
||||
callerData.avatarUrl);
|
||||
if(calleeData)
|
||||
this.callee = new User(calleeData.name, calleeData.email,
|
||||
calleeData.avatarUrl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the TokenData instance.
|
||||
*/
|
||||
let data = null;
|
||||
|
||||
/**
|
||||
* Returns the data variable. Creates new TokenData instance if <tt>data</tt>
|
||||
* variable is null.
|
||||
*/
|
||||
export default function getTokenData() {
|
||||
if(!data)
|
||||
data = new TokenData(jwt);
|
||||
return data;
|
||||
}
|
||||
3
modules/transport/.eslintrc.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
'extends': '../../react/.eslintrc.js'
|
||||
};
|
||||
174
modules/transport/PostMessageTransportBackend.js
Normal file
@@ -0,0 +1,174 @@
|
||||
import Postis from 'postis';
|
||||
|
||||
/**
|
||||
* The default options for postis.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
const DEFAULT_POSTIS_OPTIONS = {
|
||||
window: window.opener || window.parent
|
||||
};
|
||||
|
||||
/**
|
||||
* The list of methods of incoming postis messages that we have to support for
|
||||
* backward compatibility for the users that are directly sending messages to
|
||||
* Jitsi Meet (without using external_api.js)
|
||||
*
|
||||
* @type {string[]}
|
||||
*/
|
||||
const LEGACY_INCOMING_METHODS = [
|
||||
'avatar-url',
|
||||
'display-name',
|
||||
'email',
|
||||
'toggle-audio',
|
||||
'toggle-chat',
|
||||
'toggle-contact-list',
|
||||
'toggle-film-strip',
|
||||
'toggle-share-screen',
|
||||
'toggle-video',
|
||||
'video-hangup'
|
||||
];
|
||||
|
||||
/**
|
||||
* The list of methods of outgoing postis messages that we have to support for
|
||||
* backward compatibility for the users that are directly listening to the
|
||||
* postis messages send by Jitsi Meet(without using external_api.js).
|
||||
*
|
||||
* @type {string[]}
|
||||
*/
|
||||
const LEGACY_OUTGOING_METHODS = [
|
||||
'display-name-change',
|
||||
'incoming-message',
|
||||
'outgoing-message',
|
||||
'participant-joined',
|
||||
'participant-left',
|
||||
'video-conference-joined',
|
||||
'video-conference-left',
|
||||
'video-ready-to-close'
|
||||
];
|
||||
|
||||
/**
|
||||
* The postis method used for all messages.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
const POSTIS_METHOD_NAME = 'message';
|
||||
|
||||
/**
|
||||
* Implements message transport using the postMessage API.
|
||||
*/
|
||||
export default class PostMessageTransportBackend {
|
||||
/**
|
||||
* Creates new PostMessageTransportBackend instance.
|
||||
*
|
||||
* @param {Object} options - Optional parameters for configuration of the
|
||||
* transport.
|
||||
*/
|
||||
constructor({ enableLegacyFormat, postisOptions } = {}) {
|
||||
this.postis = Postis({
|
||||
...DEFAULT_POSTIS_OPTIONS,
|
||||
...postisOptions
|
||||
});
|
||||
|
||||
/**
|
||||
* If true PostMessageTransportBackend will process and send messages
|
||||
* using the legacy format and in the same time the current format.
|
||||
* Otherwise all messages (outgoing and incoming) that are using the
|
||||
* legacy format will be ignored.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._enableLegacyFormat = enableLegacyFormat;
|
||||
|
||||
if (this._enableLegacyFormat) {
|
||||
// backward compatibility
|
||||
LEGACY_INCOMING_METHODS.forEach(method =>
|
||||
this.postis.listen(
|
||||
method,
|
||||
params =>
|
||||
this._legacyMessageReceivedCallback(method, params)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this._receiveCallback = () => {
|
||||
// Do nothing until a callback is set by the consumer of
|
||||
// PostMessageTransportBackend via setReceiveCallback.
|
||||
};
|
||||
|
||||
this.postis.listen(
|
||||
POSTIS_METHOD_NAME,
|
||||
message => this._receiveCallback(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming legacy postis messages.
|
||||
*
|
||||
* @param {string} method - The method property from the postis message.
|
||||
* @param {Any} params - The params property from the postis message.
|
||||
* @returns {void}
|
||||
*/
|
||||
_legacyMessageReceivedCallback(method, params = {}) {
|
||||
this._receiveCallback({
|
||||
data: {
|
||||
name: method,
|
||||
data: params
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the passed message via postis using the old format.
|
||||
*
|
||||
* @param {Object} legacyMessage - The message to be sent.
|
||||
* @returns {void}
|
||||
*/
|
||||
_sendLegacyMessage({ name, ...data }) {
|
||||
if (name && LEGACY_OUTGOING_METHODS.indexOf(name) !== -1) {
|
||||
this.postis.send({
|
||||
method: name,
|
||||
params: data
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the allocated resources.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
dispose() {
|
||||
this.postis.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the passed message.
|
||||
*
|
||||
* @param {Object} message - The message to be sent.
|
||||
* @returns {void}
|
||||
*/
|
||||
send(message) {
|
||||
this.postis.send({
|
||||
method: POSTIS_METHOD_NAME,
|
||||
params: message
|
||||
});
|
||||
|
||||
if (this._enableLegacyFormat) {
|
||||
// For the legacy use case we don't need any new fields defined in
|
||||
// Transport class. That's why we are passing only the original
|
||||
// object passed by the consumer of the Transport class which is
|
||||
// message.data.
|
||||
this._sendLegacyMessage(message.data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the callback for receiving data.
|
||||
*
|
||||
* @param {Function} callback - The new callback.
|
||||
* @returns {void}
|
||||
*/
|
||||
setReceiveCallback(callback) {
|
||||
this._receiveCallback = callback;
|
||||
}
|
||||
}
|
||||
265
modules/transport/Transport.js
Normal file
@@ -0,0 +1,265 @@
|
||||
import {
|
||||
MESSAGE_TYPE_EVENT,
|
||||
MESSAGE_TYPE_REQUEST,
|
||||
MESSAGE_TYPE_RESPONSE
|
||||
} from './constants';
|
||||
|
||||
/**
|
||||
* Stores the currnet transport backend that have to be used. Also implements
|
||||
* request/response mechanism.
|
||||
*/
|
||||
export default class Transport {
|
||||
/**
|
||||
* Creates new instance.
|
||||
*
|
||||
* @param {Object} options - Optional parameters for configuration of the
|
||||
* transport backend.
|
||||
*/
|
||||
constructor({ backend } = {}) {
|
||||
/**
|
||||
* Maps an event name and listener that have been added to the Transport
|
||||
* instance.
|
||||
*
|
||||
* @type {Map<string, Function>}
|
||||
*/
|
||||
this._listeners = new Map();
|
||||
|
||||
/**
|
||||
* The request ID counter used for the id property of the request. This
|
||||
* property is used to match the responses with the request.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
this._requestID = 0;
|
||||
|
||||
/**
|
||||
* Maps an IDs of the requests and handlers that will process the
|
||||
* responses of those requests.
|
||||
*
|
||||
* @type {Map<number, Function>}
|
||||
*/
|
||||
this._responseHandlers = new Map();
|
||||
|
||||
/**
|
||||
* A set with the events and requests that were received but not
|
||||
* processed by any listener. They are later passed on every new
|
||||
* listener until they are processed.
|
||||
*
|
||||
* @type {Set<Object>}
|
||||
*/
|
||||
this._unprocessedMessages = new Set();
|
||||
|
||||
/**
|
||||
* Alias.
|
||||
*/
|
||||
this.addListener = this.on;
|
||||
|
||||
if (backend) {
|
||||
this.setBackend(backend);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the current transport backend.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_disposeBackend() {
|
||||
if (this._backend) {
|
||||
this._backend.dispose();
|
||||
this._backend = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming messages from the transport backend.
|
||||
*
|
||||
* @param {Object} message - The message.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onMessageReceived(message) {
|
||||
if (message.type === MESSAGE_TYPE_RESPONSE) {
|
||||
const handler = this._responseHandlers.get(message.id);
|
||||
|
||||
if (handler) {
|
||||
handler(message);
|
||||
this._responseHandlers.delete(message.id);
|
||||
}
|
||||
} else if (message.type === MESSAGE_TYPE_REQUEST) {
|
||||
this.emit('request', message.data, (result, error) => {
|
||||
this._backend.send({
|
||||
type: MESSAGE_TYPE_RESPONSE,
|
||||
error,
|
||||
id: message.id,
|
||||
result
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.emit('event', message.data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the allocated resources.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
dispose() {
|
||||
this._responseHandlers.clear();
|
||||
this._unprocessedMessages.clear();
|
||||
this.removeAllListeners();
|
||||
this._disposeBackend();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls each of the listeners registered for the event named eventName, in
|
||||
* the order they were registered, passing the supplied arguments to each.
|
||||
*
|
||||
* @param {string} eventName - The name of the event.
|
||||
* @returns {boolean} True if the event has been processed by any listener,
|
||||
* false otherwise.
|
||||
*/
|
||||
emit(eventName, ...args) {
|
||||
const listenersForEvent = this._listeners.get(eventName);
|
||||
let isProcessed = false;
|
||||
|
||||
if (listenersForEvent && listenersForEvent.size) {
|
||||
listenersForEvent.forEach(listener => {
|
||||
isProcessed = listener(...args) || isProcessed;
|
||||
});
|
||||
}
|
||||
|
||||
if (!isProcessed) {
|
||||
this._unprocessedMessages.add(args);
|
||||
}
|
||||
|
||||
return isProcessed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the listener function to the listeners collection for the event
|
||||
* named eventName.
|
||||
*
|
||||
* @param {string} eventName - The name of the event.
|
||||
* @param {Function} listener - The listener that will be added.
|
||||
* @returns {Transport} References to the instance of Transport class, so
|
||||
* that calls can be chained.
|
||||
*/
|
||||
on(eventName, listener) {
|
||||
let listenersForEvent = this._listeners.get(eventName);
|
||||
|
||||
if (!listenersForEvent) {
|
||||
listenersForEvent = new Set();
|
||||
this._listeners.set(eventName, listenersForEvent);
|
||||
}
|
||||
|
||||
listenersForEvent.add(listener);
|
||||
|
||||
this._unprocessedMessages.forEach(args => {
|
||||
if (listener(...args)) {
|
||||
this._unprocessedMessages.delete(args);
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all listeners, or those of the specified eventName.
|
||||
*
|
||||
* @param {string} [eventName] - The name of the event. If this parameter is
|
||||
* not specified all listeners will be removed.
|
||||
* @returns {Transport} References to the instance of Transport class, so
|
||||
* that calls can be chained.
|
||||
*/
|
||||
removeAllListeners(eventName) {
|
||||
if (eventName) {
|
||||
this._listeners.delete(eventName);
|
||||
} else {
|
||||
this._listeners.clear();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener function from the listeners collection for the event
|
||||
* named eventName.
|
||||
*
|
||||
* @param {string} eventName - The name of the event.
|
||||
* @param {Function} listener - The listener that will be removed.
|
||||
* @returns {Transport} References to the instance of Transport class, so
|
||||
* that calls can be chained.
|
||||
*/
|
||||
removeListener(eventName, listener) {
|
||||
const listenersForEvent = this._listeners.get(eventName);
|
||||
|
||||
if (listenersForEvent) {
|
||||
listenersForEvent.delete(listener);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the passed event.
|
||||
*
|
||||
* @param {Object} event - The event to be sent.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendEvent(event = {}) {
|
||||
if (this._backend) {
|
||||
this._backend.send({
|
||||
type: MESSAGE_TYPE_EVENT,
|
||||
data: event
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sending request.
|
||||
*
|
||||
* @param {Object} request - The request to be sent.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
sendRequest(request) {
|
||||
if (!this._backend) {
|
||||
return Promise.reject(new Error('No transport backend defined!'));
|
||||
}
|
||||
|
||||
this._requestID++;
|
||||
|
||||
const id = this._requestID;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._responseHandlers.set(id, ({ error, result }) => {
|
||||
if (result) {
|
||||
resolve(result);
|
||||
} else if (error) {
|
||||
reject(error);
|
||||
} else { // no response
|
||||
reject(new Error('Unexpected response format!'));
|
||||
}
|
||||
});
|
||||
|
||||
this._backend.send({
|
||||
type: MESSAGE_TYPE_REQUEST,
|
||||
data: request,
|
||||
id
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the current backend transport.
|
||||
*
|
||||
* @param {Object} backend - The new transport backend that will be used.
|
||||
* @returns {void}
|
||||
*/
|
||||
setBackend(backend) {
|
||||
this._disposeBackend();
|
||||
|
||||
this._backend = backend;
|
||||
this._backend.setReceiveCallback(this._onMessageReceived.bind(this));
|
||||
}
|
||||
}
|
||||
20
modules/transport/constants.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* The message type for events.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const MESSAGE_TYPE_EVENT = 'event';
|
||||
|
||||
/**
|
||||
* The message type for requests.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const MESSAGE_TYPE_REQUEST = 'request';
|
||||
|
||||
/**
|
||||
* The message type for responses.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const MESSAGE_TYPE_RESPONSE = 'response';
|
||||
57
modules/transport/index.js
Normal file
@@ -0,0 +1,57 @@
|
||||
// FIXME: change to '../API' when we update to webpack2. If we do this now all
|
||||
// files from API modules will be included in external_api.js.
|
||||
import { API_ID } from '../API/constants';
|
||||
import { getJitsiMeetGlobalNS } from '../util/helpers';
|
||||
|
||||
import PostMessageTransportBackend from './PostMessageTransportBackend';
|
||||
import Transport from './Transport';
|
||||
|
||||
export {
|
||||
PostMessageTransportBackend,
|
||||
Transport
|
||||
};
|
||||
|
||||
/**
|
||||
* Option for the default low level transport.
|
||||
*
|
||||
* @type {Object}
|
||||
*/
|
||||
const postisOptions = {};
|
||||
|
||||
if (typeof API_ID === 'number') {
|
||||
postisOptions.scope = `jitsi_meet_external_api_${API_ID}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The instance of Transport class that will be used by Jitsi Meet.
|
||||
*
|
||||
* @type {Transport}
|
||||
*/
|
||||
let transport;
|
||||
|
||||
/**
|
||||
* Returns the instance of Transport class that will be used by Jitsi Meet.
|
||||
*
|
||||
* @returns {Transport}
|
||||
*/
|
||||
export function getJitsiMeetTransport() {
|
||||
if (!transport) {
|
||||
transport = new Transport({
|
||||
backend: new PostMessageTransportBackend({
|
||||
enableLegacyFormat: true,
|
||||
postisOptions
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return transport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the transport to passed transport.
|
||||
*
|
||||
* @param {Object} externalTransportBackend - The new transport.
|
||||
* @returns {void}
|
||||
*/
|
||||
getJitsiMeetGlobalNS().setExternalTransportBackend = externalTransportBackend =>
|
||||
transport.setBackend(externalTransportBackend);
|
||||