Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b487e3c89 | ||
|
|
70e1bfc6b8 | ||
|
|
d65295db8b | ||
|
|
34be638fca | ||
|
|
2a0973a897 | ||
|
|
42b51e3c5c | ||
|
|
ff0e392ca8 | ||
|
|
1da95d2e37 | ||
|
|
b7c4ebba84 | ||
|
|
66ababc6c8 | ||
|
|
e128c03f56 | ||
|
|
d5b40280ab | ||
|
|
346980308b | ||
|
|
abd30e0269 | ||
|
|
cbe395f463 | ||
|
|
59d4523d72 | ||
|
|
27f968e753 | ||
|
|
a2ebc169e4 | ||
|
|
d10cc66036 | ||
|
|
3d0226ccd0 | ||
|
|
acb4d12928 | ||
|
|
600f7bcf7b | ||
|
|
1ff89c5a1c | ||
|
|
56b12bd969 | ||
|
|
2333249b05 | ||
|
|
aabe641047 | ||
|
|
dc5a29a976 | ||
|
|
82ecfac4ee | ||
|
|
23fea490aa | ||
|
|
1d60300016 | ||
|
|
6536f82559 | ||
|
|
4464a11314 | ||
|
|
2855ea1500 | ||
|
|
258dc594dd | ||
|
|
a1476c68f1 | ||
|
|
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 |
@@ -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.
|
||||
|
||||
@@ -19,6 +19,9 @@ 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)
|
||||
@@ -97,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";
|
||||
|
||||
174
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
|
||||
@@ -50,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;
|
||||
@@ -273,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;
|
||||
@@ -309,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;
|
||||
@@ -373,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));
|
||||
@@ -392,7 +386,7 @@ class ConferenceConnector {
|
||||
_onConferenceFailed(err, ...params) {
|
||||
APP.store.dispatch(conferenceFailed(room, err, ...params));
|
||||
logger.error('CONFERENCE FAILED:', err, ...params);
|
||||
APP.UI.hideRingOverLay();
|
||||
APP.UI.hideRingOverlay();
|
||||
switch (err) {
|
||||
|
||||
case ConferenceErrors.CONNECTION_ERROR:
|
||||
@@ -410,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:
|
||||
@@ -604,8 +598,8 @@ export default {
|
||||
|
||||
APP.store.dispatch(showDesktopSharingButton());
|
||||
|
||||
APP.remoteControl.init();
|
||||
this._createRoom(tracks);
|
||||
APP.remoteControl.init();
|
||||
|
||||
if (UIUtil.isButtonEnabled('contacts')
|
||||
&& !interfaceConfig.filmStripOnly) {
|
||||
@@ -630,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();
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -985,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();
|
||||
@@ -1088,6 +1080,43 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers a tooltip to display when a feature was attempted to be used
|
||||
* while in audio only mode.
|
||||
*
|
||||
* @param {string} featureName - The name of the feature that attempted to
|
||||
* toggle.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_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,
|
||||
toggleScreenSharing(shareScreen = !this.isSharingScreen) {
|
||||
@@ -1100,6 +1129,11 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isAudioOnly()) {
|
||||
this._displayAudioOnlyTooltip('screenShare');
|
||||
return;
|
||||
}
|
||||
|
||||
this.videoSwitchInProgress = true;
|
||||
let externalInstallation = false;
|
||||
|
||||
@@ -1252,6 +1286,8 @@ export default {
|
||||
|
||||
// check the roles for the new user and reflect them
|
||||
APP.UI.updateUserRole(user);
|
||||
|
||||
updateRemoteThumbnailsVisibility();
|
||||
});
|
||||
room.on(ConferenceEvents.USER_LEFT, (id, user) => {
|
||||
APP.store.dispatch(participantLeft(id, user));
|
||||
@@ -1259,8 +1295,16 @@ export default {
|
||||
APP.API.notifyUserLeft(id);
|
||||
APP.UI.removeUser(id, user.getDisplayName());
|
||||
APP.UI.onSharedVideoStop(id);
|
||||
|
||||
updateRemoteThumbnailsVisibility();
|
||||
});
|
||||
|
||||
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) => {
|
||||
if (this.isLocalId(id)) {
|
||||
@@ -1403,6 +1447,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();
|
||||
@@ -1431,6 +1479,8 @@ export default {
|
||||
reportError(e);
|
||||
}
|
||||
}
|
||||
|
||||
updateRemoteThumbnailsVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1445,10 +1495,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") {
|
||||
@@ -1507,7 +1565,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) => {
|
||||
@@ -1598,10 +1663,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 = {
|
||||
@@ -1656,6 +1717,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);
|
||||
@@ -1702,6 +1771,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)
|
||||
);
|
||||
@@ -1736,6 +1817,8 @@ export default {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateRemoteThumbnailsVisibility();
|
||||
});
|
||||
room.addCommandListener(
|
||||
this.commands.defaults.SHARED_VIDEO, ({value, attributes}, id) => {
|
||||
@@ -1751,6 +1834,23 @@ export default {
|
||||
APP.UI.onSharedVideoUpdate(id, value, attributes);
|
||||
}
|
||||
});
|
||||
|
||||
function updateRemoteThumbnailsVisibility() {
|
||||
const localUserId = APP.conference.getMyUserId();
|
||||
const remoteParticipantsCount = room.getParticipantCount() - 1;
|
||||
|
||||
// Get the remote thumbnail count for cases where there are
|
||||
// non-participants displaying video, such as with video sharing.
|
||||
const remoteVideosCount = APP.UI.getRemoteVideosCount();
|
||||
|
||||
const shouldShowRemoteThumbnails = interfaceConfig.filmStripOnly
|
||||
|| (APP.UI.isPinned(localUserId) && remoteVideosCount)
|
||||
|| remoteVideosCount > 1
|
||||
|| remoteParticipantsCount !== remoteVideosCount;
|
||||
|
||||
APP.UI.setRemoteThumbnailsVisibility(
|
||||
Boolean(shouldShowRemoteThumbnails));
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Adds any room listener.
|
||||
@@ -1929,7 +2029,7 @@ export default {
|
||||
*/
|
||||
hangup(requestFeedback = false) {
|
||||
eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
|
||||
APP.UI.hideRingOverLay();
|
||||
APP.UI.hideRingOverlay();
|
||||
let requestFeedbackPromise = requestFeedback
|
||||
? APP.UI.requestFeedbackOnHangup()
|
||||
// false - because the thank you dialog shouldn't be displayed
|
||||
@@ -2052,6 +2152,12 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.store.dispatch(participantUpdated({
|
||||
id: this.getMyUserId(),
|
||||
local: true,
|
||||
name: formattedNickname
|
||||
}));
|
||||
|
||||
APP.settings.setDisplayName(formattedNickname);
|
||||
if (room) {
|
||||
room.setDisplayName(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
|
||||
@@ -57,6 +57,9 @@ var config = { // eslint-disable-line no-unused-vars
|
||||
webrtcIceTcpDisable: false,
|
||||
|
||||
openSctp: true, // Toggle to enable/disable SCTP channels
|
||||
|
||||
// Disable hiding of remote thumbnails when in a 1-on-1 conference call.
|
||||
disable1On1Mode: false,
|
||||
disableStats: false,
|
||||
disableAudioLevels: false,
|
||||
channelLastN: -1, // The default value of the channel attribute last-n.
|
||||
@@ -76,6 +79,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
|
||||
@@ -92,7 +97,7 @@ var config = { // eslint-disable-line no-unused-vars
|
||||
// the room. If that succeeds the conference will stop sending data through
|
||||
// the JVB and use the peer to peer connection instead. When 3rd participant
|
||||
// joins the conference will be moved back to the JVB connection.
|
||||
//enableP2P: true
|
||||
enableP2P: true
|
||||
// How long we're going to wait, before going back to P2P after
|
||||
// the 3rd participant has left the conference (to filter out page reload)
|
||||
//backToP2PDelay: 5
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,37 +1,32 @@
|
||||
/* global config,
|
||||
createConnectionExternally,
|
||||
getConfigParamsFromUrl,
|
||||
getRoomName */
|
||||
/* global config, createConnectionExternally */
|
||||
|
||||
import getRoomName from '../react/features/base/config/getRoomName';
|
||||
import parseURLParams from '../react/features/base/config/parseURLParams';
|
||||
|
||||
/**
|
||||
* Implements external connect using createConnectionExternally function defined
|
||||
* in external_connect.js for Jitsi Meet. Parses the room name and token from
|
||||
* the URL and executes createConnectionExternally.
|
||||
* 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
|
||||
* 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
|
||||
* external_connect.js.
|
||||
*/
|
||||
|
||||
const hashParams = getConfigParamsFromUrl('hash', true);
|
||||
const searchParams = getConfigParamsFromUrl('search', true);
|
||||
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;
|
||||
|
||||
// URL params have higher proirity than config params.
|
||||
let url
|
||||
= hashParams.hasOwnProperty('config.externalConnectUrl')
|
||||
? hashParams['config.externalConnectUrl']
|
||||
: config.externalConnectUrl;
|
||||
|
||||
if (url && window.createConnectionExternally) {
|
||||
const roomName = getRoomName();
|
||||
|
||||
if (roomName) {
|
||||
if (url && (roomName = getRoomName())) {
|
||||
url += `?room=${roomName}`;
|
||||
|
||||
const token
|
||||
= hashParams['config.token'] || config.token || searchParams.jwt;
|
||||
const token = parseURLParams(window.location, true, 'search').jwt;
|
||||
|
||||
if (token) {
|
||||
url += `&token=${token}`;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -145,3 +150,28 @@ form {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-style default OS scrollbar.
|
||||
*/
|
||||
::-webkit-scrollbar {
|
||||
background: transparent;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, .5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,28 @@
|
||||
a:active {
|
||||
color: black;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
background: #06a5df;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: black;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background: black;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #06a5df;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
#chat_container.is-conversation-mode #chatconversation {
|
||||
@@ -212,28 +234,6 @@
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
:not(.default-scrollbar)::-webkit-scrollbar {
|
||||
background: #06a5df;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
:not(.default-scrollbar)::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:not(.default-scrollbar)::-webkit-scrollbar-track {
|
||||
background: black;
|
||||
}
|
||||
|
||||
:not(.default-scrollbar)::-webkit-scrollbar-track-piece {
|
||||
background: black;
|
||||
}
|
||||
|
||||
:not(.default-scrollbar)::-webkit-scrollbar-thumb {
|
||||
background: #06a5df;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#usermsg::-webkit-scrollbar-track-piece {
|
||||
background: #3a3a3a;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -62,10 +62,18 @@
|
||||
videos. */
|
||||
font-size: 0pt;
|
||||
|
||||
#filmstripLocalVideo {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
bottom: -196px;
|
||||
}
|
||||
|
||||
.remote-videos-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.videocontainer {
|
||||
display: none;
|
||||
position: relative;
|
||||
@@ -130,4 +138,13 @@
|
||||
margin-bottom: auto;
|
||||
padding-right: $defaultToolbarSize;
|
||||
}
|
||||
|
||||
.remote-videos-container {
|
||||
transition: opacity 1s;
|
||||
|
||||
&.hide-videos {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,119 +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";
|
||||
}
|
||||
.icon-dialpad:before {
|
||||
content: "\e61c";
|
||||
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%;
|
||||
|
||||
@@ -44,4 +44,28 @@
|
||||
border-width: 5px;
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override default "top" styles to support popovers appearing from the
|
||||
* left of the popover trigger element.
|
||||
*/
|
||||
&.left {
|
||||
margin-left: -$popoverMenuPadding;
|
||||
margin-top: 0;
|
||||
|
||||
.arrow {
|
||||
border-color: transparent transparent transparent $popoverBg;
|
||||
border-width: 5px 0px 5px 5px;
|
||||
margin-left: 0;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
.jitsipopover {
|
||||
&__menu-padding {
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
width: $popoverMenuPadding;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 @@ $rateStarSize: 34px;
|
||||
* Modals
|
||||
*/
|
||||
$modalButtonFontSize: 14px;
|
||||
$modalMockAKInputBackground: #fafbfc;
|
||||
$modalMockAKInputBorder: 1px solid #f4f5f7;
|
||||
$modalTextColor: #333;
|
||||
|
||||
/**
|
||||
@@ -125,11 +127,11 @@ $toolbarZ: 400;
|
||||
$tooltipsZ: 401;
|
||||
$dropdownMaskZ: 900;
|
||||
$dropdownZ: 901;
|
||||
$overlayZ: 902;
|
||||
$jitsipopoverZ: 1010;
|
||||
$centeredVideoLabelZ: 1011;
|
||||
$notificationZ: 1012;
|
||||
$popoverZ: 1015;
|
||||
$overlayZ: 1016;
|
||||
|
||||
|
||||
/**
|
||||
@@ -147,6 +149,7 @@ $inputControlEmColor: #f29424;
|
||||
//buttons
|
||||
$linkFontColor: #489afe;
|
||||
$linkHoverFontColor: #287ade;
|
||||
$formPadding: 16px;
|
||||
|
||||
/**
|
||||
* Unsupported browser
|
||||
|
||||
143
css/_vertical_filmstrip_overrides.scss
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Override other styles to support vertical filmstrip mode.
|
||||
*/
|
||||
.vertical-filmstrip {
|
||||
.filmstrip {
|
||||
align-items: flex-end;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
height: 100%;
|
||||
|
||||
/**
|
||||
* Hide videos by making them slight to the right.
|
||||
*/
|
||||
.filmstrip__videos {
|
||||
right: 0;
|
||||
transition: right 2s;
|
||||
|
||||
&.hidden {
|
||||
bottom: auto;
|
||||
right: -196px;
|
||||
}
|
||||
}
|
||||
|
||||
#filmstripLocalVideo {
|
||||
height: auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove unnecssary padding that is normally used to prevent horizontal
|
||||
* filmstrip from overlapping the left edge of the screen.
|
||||
*/
|
||||
#filmstripLocalVideo,
|
||||
#filmstripRemoteVideos {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#filmstripRemoteVideos {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
overflow-x: hidden !important;
|
||||
|
||||
.remote-videos-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the hide filmstrip icon so it points towards the right edge
|
||||
* of the screen.
|
||||
*/
|
||||
&__toolbar {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the remote video menu trigger to the bottom left of the
|
||||
* video thumbnail.
|
||||
*/
|
||||
.remotevideomenu {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
top: auto;
|
||||
right: auto;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
#remoteVideos {
|
||||
flex-direction: column-reverse;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.videocontainer {
|
||||
/**
|
||||
* Move status icons to the bottom right of the thumbnail.
|
||||
*/
|
||||
&__toolbar {
|
||||
text-align: right;
|
||||
|
||||
.toolbar-icon {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* These styles are for the video labels that display on the top right. The
|
||||
* styles adjust the labels' positioning as the filmstrip itself or
|
||||
* filmstrip's remote videos appear and disappear.
|
||||
*
|
||||
* The class with-filmstrip is for when the filmstrip is visible.
|
||||
* The class without-filmstrip is for when the filmstrip has been toggled to
|
||||
* be hidden.
|
||||
* The class opening is for when the filmstrip is transitioning from hidden
|
||||
* to visible.
|
||||
* The class with-remote-videos is for when the filmstrip has remote videos
|
||||
* displayed, as opposed to 1-on-1 mode where they might be hidden.
|
||||
* The class without-remote-videos is for when the filmstrip is visible
|
||||
* but it has no videos to display.
|
||||
*/
|
||||
.video-state-indicator.moveToCorner {
|
||||
transition: right 0.5s;
|
||||
|
||||
&.with-filmstrip.with-remote-videos {
|
||||
&#recordingLabel {
|
||||
right: 200px;
|
||||
}
|
||||
|
||||
&#videoResolutionLabel {
|
||||
right: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
&.with-filmstrip.without-remote-videos {
|
||||
transition-delay: 0.5s;
|
||||
}
|
||||
|
||||
&.with-filmstrip.with-remote-videos.opening {
|
||||
transition: 0.9s;
|
||||
transition-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
&.without-filmstrip {
|
||||
transition: 1.2s ease-in-out;
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move toastr closer to the bottom of the screen and move left to avoid
|
||||
* overlapping of videos when they are configured at default height.
|
||||
*/
|
||||
#toast-container {
|
||||
&.notification-bottom-right {
|
||||
bottom: 25px;
|
||||
right: 130 + 2 * ($thumbnailVideoMargin + $thumbnailsBorder) + $thumbnailVideoBorder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,12 @@
|
||||
visibility: hidden;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
&.audio-only {
|
||||
.videoThumbnailProblemFilter {
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#localVideoWrapper {
|
||||
@@ -489,31 +495,42 @@
|
||||
0px 0px 1px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.filmstrip-only {
|
||||
#videoResolutionLabel {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.video-state-indicator {
|
||||
background: $videoStateIndicatorBackground;
|
||||
color: $videoStateIndicatorColor;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
height: 40px;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
padding: 10px 5px;
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
|
||||
i {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
#videoResolutionLabel,
|
||||
.centeredVideoLabel {
|
||||
display: none;
|
||||
z-index: $centeredVideoLabelZ;
|
||||
.centeredVideoLabel.moveToCorner {
|
||||
z-index: $tooltipsZ;
|
||||
}
|
||||
|
||||
.centeredVideoLabel {
|
||||
bottom: 45%;
|
||||
border-radius: 2px;
|
||||
display: none;
|
||||
-webkit-transition: all 2s 2s linear;
|
||||
transition: all 2s 2s linear;
|
||||
z-index: $centeredVideoLabelZ;
|
||||
|
||||
&.moveToCorner {
|
||||
bottom: auto;
|
||||
@@ -528,4 +545,60 @@
|
||||
|
||||
.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 {
|
||||
background: $toolbarSelectBackground;
|
||||
|
||||
.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';
|
||||
@@ -71,5 +73,6 @@
|
||||
@import 'policy';
|
||||
@import 'filmstrip';
|
||||
@import 'unsupported-browser/main';
|
||||
@import 'vertical_filmstrip_overrides';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
@@ -79,6 +79,12 @@
|
||||
|
||||
.modal-dialog-form {
|
||||
color: $modalTextColor;
|
||||
|
||||
.input-control {
|
||||
background: $modalMockAKInputBackground;
|
||||
border: $modalMockAKInputBorder;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
.modal-dialog-footer {
|
||||
font-size: $modalButtonFontSize;
|
||||
|
||||
@@ -4,12 +4,6 @@
|
||||
.device-selectors {
|
||||
font-size: 14px;
|
||||
|
||||
/* ensure all child components do not exceed parent width */
|
||||
button,
|
||||
div {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
@@ -20,8 +14,41 @@
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +68,7 @@
|
||||
}
|
||||
|
||||
.device-selection-video-container {
|
||||
/* TOFIX: to be removed when we move out from muted preview */
|
||||
background: black;
|
||||
border-radius: 3px;
|
||||
/* TOFIX-END */
|
||||
height: 160px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.video-input-preview {
|
||||
@@ -56,7 +79,7 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.video-input-preview-muted {
|
||||
.video-input-preview-error {
|
||||
color: $participantNameColor;
|
||||
display: none;
|
||||
left: 0;
|
||||
@@ -66,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%;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -26,6 +26,7 @@ VirtualHost "jitmeet.example.com"
|
||||
c2s_require_encryption = false
|
||||
|
||||
Component "conference.jitmeet.example.com" "muc"
|
||||
storage = "null"
|
||||
--modules_enabled = { "token_verification" }
|
||||
admins = { "focusUser@auth.jitmeet.example.com" }
|
||||
|
||||
|
||||
@@ -238,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
@@ -7,11 +7,11 @@
|
||||
<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" />
|
||||
<glyph unicode="" glyph-name="connection" horiz-adv-x="1444" d="M3.881 210.835h220.26v-210.835h-220.26v210.835zM308.817 414.143h220.27v-414.143h-220.27v414.143zM613.764 617.412h220.268v-617.412h-220.268v617.412zM918.685 820.715h220.265v-820.715h-220.265v820.715zM1223.629 1024h220.263v-1024h-220.263v1024z" />
|
||||
<glyph unicode="" glyph-name="dialpad" horiz-adv-x="1026" d="M74.418 881.299h239.304v-228.491h-239.304v228.491zM393.455 881.299h239.304v-228.491h-239.304v228.491zM712.494 881.299h239.263v-228.491h-239.263v228.491zM74.418 562.265h239.304v-228.555h-239.304v228.555zM393.455 562.265h239.304v-228.555h-239.304v228.555zM712.494 562.265h239.263v-228.555h-239.263v228.555zM74.418 243.166h239.304v-228.465h-239.304v228.465zM393.455 243.166h239.304v-228.465h-239.304v228.465zM712.494 243.166h239.263v-228.465h-239.263v228.465z" />
|
||||
<glyph unicode="" glyph-name="connection-lost" horiz-adv-x="1414" d="M0 299.153h196.337v-187.951h-196.337v187.951zM271.842 480.372h196.337v-369.169h-196.337v369.169zM543.656 661.562h196.337v-550.36h-196.337v550.36zM815.47 842.766v-731.564h119.56c-14.589 33.025-23.125 71.503-23.232 111.943 0.132 86.42 38.697 163.851 99.656 216.468l0.348 403.153h-196.332zM1087.292 1024v-533.672c28.874 10.572 62.222 16.73 97.009 16.825 35.717-0.129 69.823-6.614 101.322-18.371l-1.999 535.218h-196.332zM1192.868 439.852c-0.009 0-0.020 0-0.031 0-122.247 0-221.351-98.447-221.372-219.896 0-0.007 0-0.014 0-0.021 0-121.467 99.111-219.935 221.372-219.935 0.011 0 0.021 0 0.032 0 122.248 0.014 221.345 98.477 221.345 219.935 0 0.007 0 0.013 0 0.020-0.021 121.441-99.11 219.883-221.345 219.897zM1194.706 372.607c87.601-0.006 158.614-69.787 158.614-155.866 0-0.006 0-0.012 0-0.019-0.022-86.062-71.026-155.822-158.614-155.828-87.588 0.006-158.593 69.766-158.615 155.826 0 0.007 0 0.014 0 0.020 0 86.079 71.013 155.86 158.613 155.866zM1286.795 355.682l48.348-52.528-236.375-217.567-48.348 52.528 236.375 217.567z" />
|
||||
<glyph unicode="" glyph-name="avatar" d="M512 204c106 0 200 56 256 138-2 84-172 132-256 132-86 0-254-48-256-132 56-82 150-138 256-138zM512 810c-70 0-128-58-128-128s58-128 128-128 128 58 128 128-58 128-128 128zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
|
||||
<glyph unicode="" glyph-name="download" d="M726 470h-128v170h-172v-170h-128l214-214zM826 596c110-8 198-100 198-212 0-118-96-214-214-214h-554c-142 0-256 114-256 256 0 132 100 240 228 254 54 102 160 174 284 174 156 0 284-110 314-258z" />
|
||||
@@ -21,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" />
|
||||
@@ -46,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: 16 KiB After Width: | Height: | Size: 17 KiB |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.woff
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 |
@@ -127,7 +127,6 @@
|
||||
'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="libs/do_external_connect.min.js?v=1"></script>
|
||||
<script><!--#include virtual="/interface_config.js" --></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', '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
|
||||
@@ -55,6 +55,12 @@ var interfaceConfig = { // eslint-disable-line no-unused-vars
|
||||
* Whether to only show the filmstrip (and hide the toolbar).
|
||||
*/
|
||||
filmStripOnly: false,
|
||||
|
||||
/**
|
||||
* Whether to show thumbnails in filmstrip as a column instead of as a row.
|
||||
*/
|
||||
VERTICAL_FILMSTRIP: true,
|
||||
|
||||
//A html text to be shown to guests on the close page, false disables it
|
||||
CLOSE_PAGE_GUEST_HINT: false,
|
||||
RANDOM_AVATAR_URL_PREFIX: false,
|
||||
|
||||
@@ -285,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>"; };
|
||||
@@ -303,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; };
|
||||
@@ -530,7 +530,7 @@
|
||||
children = (
|
||||
13B07FAE1A68108700A75B9A /* app */,
|
||||
B3BA19B71DC6B02F00BCD481 /* Frameworks */,
|
||||
B3B083EB1D4955FF0069CEE7 /* jitsi-meet-react.entitlements */,
|
||||
B3B083EB1D4955FF0069CEE7 /* jitsi-meet.entitlements */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
6956B374CC3C453DB7B8E82D /* Resources */,
|
||||
@@ -542,7 +542,7 @@
|
||||
83CBBA001A601CBA00E9B192 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07F961A680F5B00A75B9A /* jitsi-meet-react.app */,
|
||||
13B07F961A680F5B00A75B9A /* jitsi-meet.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -581,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 */,
|
||||
@@ -596,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 */
|
||||
@@ -620,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;
|
||||
@@ -695,7 +695,7 @@
|
||||
);
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
13B07F861A680F5B00A75B9A /* jitsi-meet-react */,
|
||||
13B07F861A680F5B00A75B9A /* jitsi-meet */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -989,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;
|
||||
@@ -1019,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;
|
||||
@@ -1028,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 = (
|
||||
@@ -1057,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;
|
||||
@@ -1164,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 */,
|
||||
@@ -1173,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",
|
||||
@@ -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__!",
|
||||
@@ -424,10 +426,35 @@
|
||||
"speakerTime": "Speaker Time"
|
||||
},
|
||||
"deviceSelection": {
|
||||
"currentlyVideoMuted": "Video is currently muted",
|
||||
"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!"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
114
modules/UI/UI.js
@@ -4,10 +4,6 @@ const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
var UI = {};
|
||||
|
||||
import {
|
||||
updateDeviceList
|
||||
} from '../../react/features/base/devices';
|
||||
|
||||
import Chat from "./side_pannels/chat/Chat";
|
||||
import SidePanels from "./side_pannels/SidePanels";
|
||||
import Avatar from "./avatar/Avatar";
|
||||
@@ -27,6 +23,10 @@ import RingOverlay from "./ring_overlay/RingOverlay";
|
||||
import UIErrors from './UIErrors';
|
||||
import { debounce } from "../util/helpers";
|
||||
|
||||
|
||||
import {
|
||||
updateDeviceList
|
||||
} from '../../react/features/base/devices';
|
||||
import {
|
||||
setAudioMuted,
|
||||
setVideoMuted
|
||||
@@ -43,7 +43,7 @@ import {
|
||||
showDialPadButton,
|
||||
showEtherpadButton,
|
||||
showSharedVideoButton,
|
||||
showSIPCallButton,
|
||||
showDialOutButton,
|
||||
showToolbox
|
||||
} from '../../react/features/toolbox';
|
||||
|
||||
@@ -309,6 +309,10 @@ UI.start = function () {
|
||||
SideContainerToggler.init(eventEmitter);
|
||||
Filmstrip.init(eventEmitter);
|
||||
|
||||
// By default start with remote videos hidden and rely on other logic to
|
||||
// make them visible.
|
||||
UI.setRemoteThumbnailsVisibility(false);
|
||||
|
||||
VideoLayout.init(eventEmitter);
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
VideoLayout.initLargeVideo();
|
||||
@@ -339,6 +343,10 @@ UI.start = function () {
|
||||
JitsiPopover.enabled = false;
|
||||
}
|
||||
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
$("body").addClass("vertical-filmstrip");
|
||||
}
|
||||
|
||||
document.title = interfaceConfig.APP_NAME;
|
||||
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
@@ -362,9 +370,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();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -476,7 +493,7 @@ UI.getSharedDocumentManager = () => etherpadManager;
|
||||
UI.addUser = function (user) {
|
||||
var id = user.getId();
|
||||
var displayName = user.getDisplayName();
|
||||
UI.hideRingOverLay();
|
||||
UI.hideRingOverlay();
|
||||
if (UI.ContactList)
|
||||
UI.ContactList.addContact(id);
|
||||
|
||||
@@ -535,7 +552,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);
|
||||
@@ -580,6 +597,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.
|
||||
*/
|
||||
@@ -711,6 +743,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.
|
||||
*
|
||||
@@ -1110,6 +1150,15 @@ UI.getLargeVideo = function () {
|
||||
return VideoLayout.getLargeVideo();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether or not the passed in user id is currently pinned to the large
|
||||
* video.
|
||||
*
|
||||
* @param {string} userId - The id of the user to check is pinned or not.
|
||||
* @returns {boolean} True if the user is currently pinned to the large video.
|
||||
*/
|
||||
UI.isPinned = userId => VideoLayout.getPinnedId() === userId;
|
||||
|
||||
/**
|
||||
* Shows dialog with a link to FF extension.
|
||||
*/
|
||||
@@ -1324,25 +1373,25 @@ 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);
|
||||
};
|
||||
|
||||
UI.hideRingOverLay = function () {
|
||||
if (!RingOverlay.hide())
|
||||
return;
|
||||
Filmstrip.toggleFilmstrip(true, false);
|
||||
};
|
||||
UI.hideRingOverlay
|
||||
= () => RingOverlay.hide() && Filmstrip.toggleFilmstrip(true, false);
|
||||
|
||||
/**
|
||||
* Indicates if any the "top" overlays are currently visible. The check includes
|
||||
* the call overlay, suspended overlay, GUM permissions overlay
|
||||
* and a page reload overlay.
|
||||
* the call overlay, the suspended overlay, the GUM permissions overlay, and the
|
||||
* page-reload overlay.
|
||||
*
|
||||
* @returns {*|boolean} {true} if the overlay is visible, {false} otherwise
|
||||
* @returns {*|boolean} {true} if an overlay is visible; {false}, otherwise
|
||||
*/
|
||||
UI.isOverlayVisible = function () {
|
||||
return RingOverlay.isVisible() || this.overlayVisible;
|
||||
return this.isRingOverlayVisible() || this.overlayVisible;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1357,6 +1406,23 @@ UI.isRingOverlayVisible = () => RingOverlay.isVisible();
|
||||
*/
|
||||
UI.onUserFeaturesChanged = user => VideoLayout.onUserFeaturesChanged(user);
|
||||
|
||||
/**
|
||||
* Returns the number of known remote videos.
|
||||
*
|
||||
* @returns {number} The number of remote videos.
|
||||
*/
|
||||
UI.getRemoteVideosCount = () => VideoLayout.getRemoteVideosCount();
|
||||
|
||||
/**
|
||||
* Makes remote thumbnail videos visible or not visible.
|
||||
*
|
||||
* @param {boolean} shouldHide - True if remote thumbnails should be hidden,
|
||||
* false f they should be visible.
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.setRemoteThumbnailsVisibility
|
||||
= shouldHide => Filmstrip.setRemoteVideoVisibility(shouldHide);
|
||||
|
||||
const UIListeners = new Map([
|
||||
[
|
||||
UIEvents.ETHERPAD_CLICKED,
|
||||
@@ -1389,7 +1455,13 @@ const UIListeners = new Map([
|
||||
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;
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
@@ -196,6 +196,9 @@ function _showStopRecordingPrompt(recordingType) {
|
||||
|
||||
/**
|
||||
* Moves the element given by {selector} to the top right corner of the screen.
|
||||
* Set additional classes that can be used to style the selector relative to the
|
||||
* state of the filmstrip.
|
||||
*
|
||||
* @param selector the selector for the element to move
|
||||
* @param move {true} to move the element, {false} to move it back to its intial
|
||||
* position
|
||||
@@ -208,6 +211,20 @@ function moveToCorner(selector, move) {
|
||||
selector.addClass(moveToCornerClass);
|
||||
else if (!move && containsClass)
|
||||
selector.removeClass(moveToCornerClass);
|
||||
|
||||
const {
|
||||
remoteVideosVisible,
|
||||
visible
|
||||
} = APP.store.getState()['features/filmstrip'];
|
||||
const filmstripWasHidden = selector.hasClass('without-filmstrip');
|
||||
const filmstipIsOpening = filmstripWasHidden && visible;
|
||||
selector.toggleClass('opening', filmstipIsOpening);
|
||||
|
||||
selector.toggleClass('with-filmstrip', visible);
|
||||
selector.toggleClass('without-filmstrip', !visible);
|
||||
|
||||
selector.toggleClass('with-remote-videos', remoteVideosVisible);
|
||||
selector.toggleClass('without-remote-videos', !remoteVideosVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,6 +312,10 @@ var Recording = {
|
||||
APP.UI.messageHandler.enableNotifications(false);
|
||||
APP.UI.messageHandler.enablePopups(false);
|
||||
}
|
||||
|
||||
this.eventEmitter.addListener(UIEvents.UPDATED_FILMSTRIP_DISPLAY, () =>{
|
||||
this._updateStatusLabel();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* global $, APP */
|
||||
/* jshint -W101 */
|
||||
|
||||
import UIEvents from "../../../service/UI/UIEvents";
|
||||
|
||||
/**
|
||||
@@ -22,22 +22,26 @@ function onAvatarVisible(shown) {
|
||||
*/
|
||||
class RingOverlay {
|
||||
/**
|
||||
* @param callee instance of User class from TokenData.js
|
||||
* @param {boolean} disableRingingSound if true the ringing sound wont be played.
|
||||
*
|
||||
* @param callee The callee (Object) as defined by the JWT support.
|
||||
* @param {boolean} disableRinging if true the ringing sound wont be played.
|
||||
*/
|
||||
constructor(callee, disableRingingSound) {
|
||||
constructor(callee, disableRinging) {
|
||||
this._containerId = 'ringOverlay';
|
||||
this._audioContainerId = 'ringOverlayRinging';
|
||||
this.isRinging = true;
|
||||
this.callee = callee;
|
||||
this.disableRingingSound = disableRingingSound;
|
||||
this.disableRinging = disableRinging;
|
||||
this.render();
|
||||
if(!disableRingingSound)
|
||||
if (!disableRinging)
|
||||
this._initAudio();
|
||||
this._timeout = setTimeout(() => {
|
||||
this.destroy();
|
||||
this.render();
|
||||
}, 30000);
|
||||
this._timeout
|
||||
= setTimeout(
|
||||
() => {
|
||||
this.destroy();
|
||||
this.render();
|
||||
},
|
||||
30000);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,7 +50,7 @@ class RingOverlay {
|
||||
_initAudio() {
|
||||
this.audio = document.getElementById(this._audioContainerId);
|
||||
this.audio.play();
|
||||
this._setAudioTimeout();
|
||||
this.interval = setInterval(() => this.audio.play(), 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +61,8 @@ class RingOverlay {
|
||||
*/
|
||||
_changeBackground(solid) {
|
||||
const container = $("#" + this._containerId);
|
||||
if(solid) {
|
||||
|
||||
if (solid) {
|
||||
container.addClass("solidBG");
|
||||
} else {
|
||||
container.removeClass("solidBG");
|
||||
@@ -70,16 +75,17 @@ class RingOverlay {
|
||||
_getHtmlStr(callee) {
|
||||
let callingLabel = this.isRinging ? "<p>Calling...</p>" : "";
|
||||
let callerStateLabel = this.isRinging ? "" : " isn't available";
|
||||
let audioHTML = this.disableRingingSound ? ""
|
||||
let audioHTML = this.disableRinging ? ""
|
||||
: "<audio id=\"" + this._audioContainerId
|
||||
+ "\" src=\"./sounds/ring.ogg\" />";
|
||||
|
||||
return `
|
||||
<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}
|
||||
@@ -119,34 +125,28 @@ class RingOverlay {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
if(this._timeout) {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the interval that is going to play the ringing sound.
|
||||
*/
|
||||
_setAudioTimeout() {
|
||||
this.interval = setInterval( () => {
|
||||
this.audio.play();
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
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} disableRinging - If true the ringing sound won't be
|
||||
* played.
|
||||
* @returns {void}
|
||||
*/
|
||||
show(callee, disableRingingSound = false) {
|
||||
if(overlay) {
|
||||
show(callee, disableRinging = false) {
|
||||
if (overlay) {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
overlay = new RingOverlay(callee, disableRingingSound);
|
||||
overlay = new RingOverlay(callee, disableRinging);
|
||||
APP.UI.addListener(UIEvents.LARGE_VIDEO_AVATAR_VISIBLE,
|
||||
onAvatarVisible);
|
||||
},
|
||||
@@ -156,7 +156,7 @@ export default {
|
||||
* overlay.
|
||||
*/
|
||||
hide() {
|
||||
if(!overlay) {
|
||||
if (!overlay) {
|
||||
return false;
|
||||
}
|
||||
overlay.destroy();
|
||||
@@ -172,7 +172,7 @@ export default {
|
||||
* @returns {boolean} true if the ring overlay is currently displayed or
|
||||
* false otherwise.
|
||||
*/
|
||||
isVisible () {
|
||||
isVisible() {
|
||||
return overlay !== null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -456,6 +456,9 @@ export default class SharedVideoManager {
|
||||
// revert to original behavior (prevents pausing
|
||||
// for participants not sharing the video to pause it)
|
||||
$("#sharedVideo").css("pointer-events","auto");
|
||||
|
||||
this.emitter.emit(
|
||||
UIEvents.UPDATE_SHARED_VIDEO, null, 'removed');
|
||||
});
|
||||
|
||||
this.url = null;
|
||||
@@ -656,7 +659,7 @@ SharedVideoThumb.prototype.createContainer = function (spanId) {
|
||||
avatar.src = "https://img.youtube.com/vi/" + this.url + "/0.jpg";
|
||||
container.appendChild(avatar);
|
||||
|
||||
var remotes = document.getElementById('remoteVideos');
|
||||
var remotes = document.getElementById('filmstripRemoteVideosContainer');
|
||||
return remotes.appendChild(container);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -57,7 +57,7 @@ function updateVisualNotification() {
|
||||
const unreadMsgElement
|
||||
= unreadMsgSelector.length > 0 ? unreadMsgSelector[0] : undefined;
|
||||
|
||||
if (unreadMessages) {
|
||||
if (unreadMessages && unreadMsgElement) {
|
||||
unreadMsgElement.innerHTML = unreadMessages.toString();
|
||||
|
||||
APP.store.dispatch(dockToolbox(true));
|
||||
@@ -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,4 +1,74 @@
|
||||
/* global $ */
|
||||
|
||||
const positionConfigurations = {
|
||||
left: {
|
||||
|
||||
// Align the popover's right side to the target element.
|
||||
my: 'right',
|
||||
|
||||
// Align the popover to the left side of the target element.
|
||||
at: 'left',
|
||||
|
||||
// Force the popover to fit within the viewport.
|
||||
collision: 'fit',
|
||||
|
||||
/**
|
||||
* Callback invoked by jQuery UI tooltip.
|
||||
*
|
||||
* @param {Object} position - The top and bottom position the popover
|
||||
* element should be set at.
|
||||
* @param {Object} element. - Additional size and position information
|
||||
* about the popover element and target.
|
||||
* @param {Object} elements.element - Has position and size related data
|
||||
* for the popover element itself.
|
||||
* @param {Object} elements.target - Has position and size related data
|
||||
* for the target element the popover displays from.
|
||||
*/
|
||||
using: function setPositionLeft(position, elements) {
|
||||
const { element, target } = elements;
|
||||
|
||||
$('.jitsipopover').css({
|
||||
display: 'table',
|
||||
left: position.left,
|
||||
top: position.top
|
||||
});
|
||||
|
||||
// Move additional padding to the right edge of the popover and
|
||||
// allow css to take care of width. The padding is used to maintain
|
||||
// a hover state between the target and the popover.
|
||||
$('.jitsipopover > .jitsipopover__menu-padding').css({
|
||||
left: element.width
|
||||
});
|
||||
|
||||
// Find the distance from the top of the popover to the center of
|
||||
// the target and use that value to position the arrow to point to
|
||||
// it.
|
||||
const verticalCenterOfTarget = target.height / 2;
|
||||
const verticalDistanceFromTops = target.top - element.top;
|
||||
const verticalPositionOfTargetCenter
|
||||
= verticalDistanceFromTops + verticalCenterOfTarget;
|
||||
|
||||
$('.jitsipopover > .arrow').css({
|
||||
left: element.width,
|
||||
top: verticalPositionOfTargetCenter
|
||||
});
|
||||
}
|
||||
},
|
||||
top: {
|
||||
my: "bottom",
|
||||
at: "top",
|
||||
collision: "fit",
|
||||
using: function setPositionTop(position, elements) {
|
||||
var calcLeft = elements.target.left - elements.element.left +
|
||||
elements.target.width/2;
|
||||
$(".jitsipopover").css(
|
||||
{top: position.top, left: position.left, display: "table"});
|
||||
$(".jitsipopover > .arrow").css({left: calcLeft});
|
||||
$(".jitsipopover > .jitsipopover__menu-padding").css(
|
||||
{left: calcLeft - 50});
|
||||
}
|
||||
}
|
||||
};
|
||||
var JitsiPopover = (function () {
|
||||
/**
|
||||
* The default options
|
||||
@@ -7,7 +77,8 @@ var JitsiPopover = (function () {
|
||||
skin: 'white',
|
||||
content: '',
|
||||
hasArrow: true,
|
||||
onBeforePosition: undefined
|
||||
onBeforePosition: undefined,
|
||||
position: 'top'
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -21,7 +92,6 @@ var JitsiPopover = (function () {
|
||||
function JitsiPopover(element, options)
|
||||
{
|
||||
this.options = Object.assign({}, defaultOptions, options);
|
||||
|
||||
this.elementIsHovered = false;
|
||||
this.popoverIsHovered = false;
|
||||
this.popoverShown = false;
|
||||
@@ -45,12 +115,15 @@ var JitsiPopover = (function () {
|
||||
* Returns template for popover
|
||||
*/
|
||||
JitsiPopover.prototype.getTemplate = function () {
|
||||
const { hasArrow, position, skin } = this.options;
|
||||
|
||||
let arrow = '';
|
||||
if (this.options.hasArrow) {
|
||||
if (hasArrow) {
|
||||
arrow = '<div class="arrow"></div>';
|
||||
}
|
||||
|
||||
return (
|
||||
`<div class="jitsipopover ${this.options.skin}">
|
||||
`<div class="jitsipopover ${skin} ${position}">
|
||||
${arrow}
|
||||
<div class="jitsipopover__content"></div>
|
||||
<div class="jitsipopover__menu-padding"></div>
|
||||
@@ -129,21 +202,14 @@ var JitsiPopover = (function () {
|
||||
* Refreshes the position of the popover.
|
||||
*/
|
||||
JitsiPopover.prototype.refreshPosition = function () {
|
||||
$(".jitsipopover").position({
|
||||
my: "bottom",
|
||||
at: "top",
|
||||
collision: "fit",
|
||||
of: this.element,
|
||||
using: function (position, elements) {
|
||||
var calcLeft = elements.target.left - elements.element.left +
|
||||
elements.target.width/2;
|
||||
$(".jitsipopover").css(
|
||||
{top: position.top, left: position.left, display: "table"});
|
||||
$(".jitsipopover > .arrow").css({left: calcLeft});
|
||||
$(".jitsipopover > .jitsipopover__menu-padding").css(
|
||||
{left: calcLeft - 50});
|
||||
const positionOptions = Object.assign(
|
||||
{},
|
||||
positionConfigurations[this.options.position],
|
||||
{
|
||||
of: this.element
|
||||
}
|
||||
});
|
||||
);
|
||||
$(".jitsipopover").position(positionOptions);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* global $, APP, config */
|
||||
/* global $, APP, interfaceConfig */
|
||||
/* 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;
|
||||
@@ -310,7 +309,8 @@ ConnectionIndicator.prototype.create = function () {
|
||||
this.popover = new JitsiPopover($(element), {
|
||||
content: popoverContent,
|
||||
skin: "black",
|
||||
onBeforePosition: el => APP.translation.translateElement(el)
|
||||
onBeforePosition: el => APP.translation.translateElement(el),
|
||||
position: interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top'
|
||||
});
|
||||
|
||||
// override popover show method to make sure we will update the content
|
||||
@@ -401,10 +401,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 +410,6 @@ ConnectionIndicator.prototype.updateConnectionQuality =
|
||||
*/
|
||||
ConnectionIndicator.prototype.updateResolution = function (resolution) {
|
||||
this.resolution = resolution;
|
||||
this.updateResolutionIndicator();
|
||||
this.updatePopoverData();
|
||||
};
|
||||
|
||||
@@ -457,31 +452,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.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/* global $, APP, JitsiMeetJS, interfaceConfig */
|
||||
/* global $, APP, config, JitsiMeetJS, interfaceConfig */
|
||||
|
||||
import {
|
||||
setFilmstripRemoteVideosVisibility,
|
||||
setFilmstripVisibility
|
||||
} from '../../../react/features/filmstrip';
|
||||
|
||||
import UIEvents from "../../../service/UI/UIEvents";
|
||||
import UIUtil from "../util/UIUtil";
|
||||
@@ -14,19 +19,40 @@ const Filmstrip = {
|
||||
this.iconMenuUpClassName = 'icon-menu-up';
|
||||
this.filmstripContainerClassName = 'filmstrip';
|
||||
this.filmstrip = $('#remoteVideos');
|
||||
this.filmstripRemoteVideos = $('#filmstripRemoteVideosContainer');
|
||||
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();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets a class on the remote videos container for CSS to adjust visibility
|
||||
* of the remote videos. Will no-op if config.debug is truthy, as should be
|
||||
* the case with torture tests.
|
||||
*
|
||||
* @param {boolean} shouldHide - True if remote videos should be hidden,
|
||||
* false if they should be visible.
|
||||
* @returns {void}
|
||||
*/
|
||||
setRemoteVideoVisibility(shouldShow) {
|
||||
// Allow disabling on 1-on-1 UI mode. Used by torture tests.
|
||||
if (config.disable1On1Mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
APP.store.dispatch(setFilmstripRemoteVideosVisibility(shouldShow));
|
||||
this.filmstripRemoteVideos.toggleClass('hide-videos', !shouldShow);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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}`);
|
||||
@@ -149,11 +175,14 @@ const Filmstrip = {
|
||||
|
||||
// Emit/fire UIEvents.TOGGLED_FILMSTRIP.
|
||||
const eventEmitter = this.eventEmitter;
|
||||
const isFilmstripVisible = this.isFilmstripVisible();
|
||||
|
||||
if (eventEmitter) {
|
||||
eventEmitter.emit(
|
||||
UIEvents.TOGGLED_FILMSTRIP,
|
||||
this.isFilmstripVisible());
|
||||
}
|
||||
APP.store.dispatch(setFilmstripVisibility(isFilmstripVisible));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -176,7 +205,10 @@ const Filmstrip = {
|
||||
* @returns {number} height
|
||||
*/
|
||||
getFilmstripHeight() {
|
||||
if (this.isFilmstripVisible()) {
|
||||
// FIXME Make it more clear the getFilmstripHeight check is used in
|
||||
// horizontal film strip mode for calculating how tall large video
|
||||
// display should be.
|
||||
if (this.isFilmstripVisible() && !interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
return $(`.${this.filmstripContainerClassName}`).outerHeight();
|
||||
} else {
|
||||
return 0;
|
||||
@@ -363,13 +395,27 @@ const Filmstrip = {
|
||||
(remoteLocalWidthRatio * numberRemoteThumbs + 1), availableHeight *
|
||||
interfaceConfig.LOCAL_THUMBNAIL_RATIO);
|
||||
const h = lW / interfaceConfig.LOCAL_THUMBNAIL_RATIO;
|
||||
|
||||
const removeVideoWidth = lW * remoteLocalWidthRatio;
|
||||
|
||||
let localVideo;
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
// scale both width and height
|
||||
localVideo = {
|
||||
thumbWidth: removeVideoWidth,
|
||||
thumbHeight: h * remoteLocalWidthRatio
|
||||
};
|
||||
} else {
|
||||
localVideo = {
|
||||
thumbWidth: lW,
|
||||
thumbHeight: h
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
localVideo:{
|
||||
thumbWidth: lW,
|
||||
thumbHeight: h
|
||||
},
|
||||
localVideo,
|
||||
remoteVideo: {
|
||||
thumbWidth: lW * remoteLocalWidthRatio,
|
||||
thumbWidth: removeVideoWidth,
|
||||
thumbHeight: h
|
||||
}
|
||||
};
|
||||
@@ -405,10 +451,15 @@ const Filmstrip = {
|
||||
}));
|
||||
}
|
||||
promises.push(new Promise((resolve) => {
|
||||
this.filmstrip.animate({
|
||||
// adds 2 px because of small video 1px border
|
||||
height: remote.thumbHeight + 2
|
||||
}, this._getAnimateOptions(animate, resolve));
|
||||
// Let CSS take care of height in vertical filmstrip mode.
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
resolve();
|
||||
} else {
|
||||
this.filmstrip.animate({
|
||||
// adds 2 px because of small video 1px border
|
||||
height: remote.thumbHeight + 2
|
||||
}, this._getAnimateOptions(animate, resolve));
|
||||
}
|
||||
}));
|
||||
|
||||
promises.push(new Promise(() => {
|
||||
@@ -455,8 +506,7 @@ const Filmstrip = {
|
||||
}
|
||||
|
||||
let localThumb = $("#localVideoContainer");
|
||||
let remoteThumbs = this.filmstrip.children(selector)
|
||||
.not("#localVideoContainer");
|
||||
let remoteThumbs = this.filmstripRemoteVideos.children(selector);
|
||||
|
||||
// Exclude the local video container if it has been hidden.
|
||||
if (localThumb.hasClass("hidden")) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,8 @@ RemoteVideo.prototype._initPopupMenu = function (popupMenuElement) {
|
||||
content: popupMenuElement.outerHTML,
|
||||
skin: "black",
|
||||
hasArrow: false,
|
||||
onBeforePosition: el => APP.translation.translateElement(el)
|
||||
onBeforePosition: el => APP.translation.translateElement(el),
|
||||
position: interfaceConfig.VERTICAL_FILMSTRIP ? 'left' : 'top'
|
||||
};
|
||||
let element = $("#" + this.videoSpanId + " .remotevideomenu");
|
||||
this.popover = new JitsiPopover(element, options);
|
||||
@@ -375,12 +376,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 +559,7 @@ RemoteVideo.prototype.isVideoPlayable = function () {
|
||||
* @inheritDoc
|
||||
*/
|
||||
RemoteVideo.prototype.updateView = function () {
|
||||
$(this.container).toggleClass('audio-only', APP.conference.isAudioOnly());
|
||||
|
||||
this.updateConnectionStatusIndicator();
|
||||
|
||||
@@ -797,7 +801,7 @@ RemoteVideo.createContainer = function (spanId) {
|
||||
overlay.className = "videocontainer__hoverOverlay";
|
||||
container.appendChild(overlay);
|
||||
|
||||
var remotes = document.getElementById('remoteVideos');
|
||||
var remotes = document.getElementById('filmstripRemoteVideosContainer');
|
||||
return remotes.appendChild(container);
|
||||
};
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
@@ -423,6 +435,19 @@ var VideoLayout = {
|
||||
remoteVideo = new RemoteVideo(user, VideoLayout, eventEmitter);
|
||||
this._setRemoteControlProperties(user, remoteVideo);
|
||||
this.addRemoteVideoContainer(id, remoteVideo);
|
||||
|
||||
const remoteVideosCount = Object.keys(remoteVideos).length;
|
||||
|
||||
if (remoteVideosCount === 1) {
|
||||
window.setTimeout(() => {
|
||||
const updatedRemoteVideosCount
|
||||
= Object.keys(remoteVideos).length;
|
||||
|
||||
if (updatedRemoteVideosCount === 1 && remoteVideos[id]) {
|
||||
this._maybePlaceParticipantOnLargeVideo(id);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -453,11 +478,24 @@ var VideoLayout = {
|
||||
logger.info(resourceJid + " video is now active", videoElement);
|
||||
|
||||
VideoLayout.resizeThumbnails(
|
||||
false, false, function() {$(videoElement).show();});
|
||||
false, false, () => {
|
||||
if (videoElement) {
|
||||
$(videoElement).show();
|
||||
}
|
||||
});
|
||||
|
||||
// Update the large video to the last added video only if there's no
|
||||
// current dominant, focused speaker or update it to
|
||||
// the current dominant speaker.
|
||||
this._maybePlaceParticipantOnLargeVideo(resourceJid);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the large video to the last added video only if there's no current
|
||||
* dominant, focused speaker or update it to the current dominant speaker.
|
||||
*
|
||||
* @params {string} resourceJid - The id of the user to maybe display on
|
||||
* large video.
|
||||
* @returns {void}
|
||||
*/
|
||||
_maybePlaceParticipantOnLargeVideo(resourceJid) {
|
||||
if ((!pinnedId &&
|
||||
!currentDominantSpeaker &&
|
||||
this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE)) ||
|
||||
@@ -538,6 +576,7 @@ var VideoLayout = {
|
||||
if (onComplete && typeof onComplete === "function")
|
||||
onComplete();
|
||||
});
|
||||
|
||||
return { localVideo, remoteVideo };
|
||||
},
|
||||
|
||||
@@ -956,6 +995,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 +1119,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;
|
||||
@@ -1113,6 +1160,15 @@ var VideoLayout = {
|
||||
*/
|
||||
getLargeVideoWrapper() {
|
||||
return this.getCurrentlyOnLargeContainer().$wrapper;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the number of remove video ids.
|
||||
*
|
||||
* @returns {number} The number of remote videos.
|
||||
*/
|
||||
getRemoteVideosCount() {
|
||||
return Object.keys(remoteVideos).length;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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,31 @@ 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;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global APP, $, JitsiMeetJS */
|
||||
/* global APP, $, JitsiMeetJS, interfaceConfig */
|
||||
|
||||
import {
|
||||
toggleDialog
|
||||
@@ -34,12 +34,14 @@ function initGlobalShortcuts() {
|
||||
});
|
||||
KeyboardShortcut._addShortcutToHelp("SPACE","keyboardShortcuts.pushToTalk");
|
||||
|
||||
KeyboardShortcut.registerShortcut("T", null, () => {
|
||||
JitsiMeetJS.analytics.sendEvent("shortcut.speakerStats.clicked");
|
||||
APP.store.dispatch(toggleDialog(SpeakerStats, {
|
||||
conference: APP.conference
|
||||
}));
|
||||
}, "keyboardShortcuts.showSpeakerStats");
|
||||
if(!interfaceConfig.filmStripOnly) {
|
||||
KeyboardShortcut.registerShortcut("T", null, () => {
|
||||
JitsiMeetJS.analytics.sendEvent("shortcut.speakerStats.clicked");
|
||||
APP.store.dispatch(toggleDialog(SpeakerStats, {
|
||||
conference: APP.conference
|
||||
}));
|
||||
}, "keyboardShortcuts.showSpeakerStats");
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: Currently focus keys are directly implemented below in onkeyup.
|
||||
|
||||
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);
|
||||
@@ -16,35 +16,6 @@ export function createDeferred() {
|
||||
return deferred;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload page.
|
||||
*/
|
||||
export function reload() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to a specific new URL by replacing the current location (in the
|
||||
* history).
|
||||
*
|
||||
* @param {string} url the URL pointing to the location where the user should
|
||||
* be redirected to.
|
||||
*/
|
||||
export function replace(url) {
|
||||
window.location.replace(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the error and reports it to the global error handler.
|
||||
*
|
||||
* @param e {Error} the error
|
||||
* @param msg {string} [optional] the message printed in addition to the error
|
||||
*/
|
||||
export function reportError(e, msg = "") {
|
||||
logger.error(msg, e);
|
||||
window.onerror && window.onerror(msg, null, null, null, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a debounced function that delays invoking func until after wait
|
||||
* milliseconds have elapsed since the last time the debounced function was
|
||||
@@ -74,3 +45,49 @@ export function debounce(fn, wait = 0, options = {}) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace for all global variables, functions, etc that we need.
|
||||
*
|
||||
* @returns {Object} the namespace.
|
||||
*
|
||||
* NOTE: After React-ifying everything this should be the only global.
|
||||
*/
|
||||
export function getJitsiMeetGlobalNS() {
|
||||
if (!window.JitsiMeetJS) {
|
||||
window.JitsiMeetJS = {};
|
||||
}
|
||||
if (!window.JitsiMeetJS.app) {
|
||||
window.JitsiMeetJS.app = {};
|
||||
}
|
||||
return window.JitsiMeetJS.app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload page.
|
||||
*/
|
||||
export function reload() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to a specific new URL by replacing the current location (in the
|
||||
* history).
|
||||
*
|
||||
* @param {string} url the URL pointing to the location where the user should
|
||||
* be redirected to.
|
||||
*/
|
||||
export function replace(url) {
|
||||
window.location.replace(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the error and reports it to the global error handler.
|
||||
*
|
||||
* @param e {Error} the error
|
||||
* @param msg {string} [optional] the message printed in addition to the error
|
||||
*/
|
||||
export function reportError(e, msg = "") {
|
||||
logger.error(msg, e);
|
||||
window.onerror && window.onerror(msg, null, null, null, e);
|
||||
}
|
||||
|
||||
31
package.json
@@ -18,7 +18,7 @@
|
||||
"dependencies": {
|
||||
"@atlaskit/button": "1.0.3",
|
||||
"@atlaskit/button-group": "1.0.0",
|
||||
"@atlaskit/dropdown-menu": "1.1.12",
|
||||
"@atlaskit/dropdown-menu": "1.4.0",
|
||||
"@atlaskit/field-text": "2.0.3",
|
||||
"@atlaskit/icon": "6.0.0",
|
||||
"@atlaskit/modal-dialog": "1.2.4",
|
||||
@@ -29,29 +29,30 @@
|
||||
"bootstrap": "3.1.1",
|
||||
"es6-iterator": "2.0.1",
|
||||
"es6-symbol": "3.1.1",
|
||||
"i18next": "7.1.3",
|
||||
"i18next": "8.2.1",
|
||||
"i18next-browser-languagedetector": "1.0.1",
|
||||
"i18next-xhr-backend": "1.4.1",
|
||||
"jitsi-meet-logger": "jitsi/jitsi-meet-logger",
|
||||
"jquery": "2.1.4",
|
||||
"jquery-contextmenu": "2.4.3",
|
||||
"jquery-contextmenu": "2.4.5",
|
||||
"jquery-i18next": "1.2.0",
|
||||
"jQuery-Impromptu": "trentrichardson/jQuery-Impromptu#v6.0.0",
|
||||
"jquery-ui": "1.10.5",
|
||||
"jssha": "1.5.0",
|
||||
"jws": "3.1.4",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "jitsi/lib-jitsi-meet",
|
||||
"lodash": "4.17.4",
|
||||
"postis": "2.2.0",
|
||||
"react": "15.4.2",
|
||||
"react-dom": "15.4.2",
|
||||
"react-i18next": "2.2.3",
|
||||
"react-i18next": "4.1.0",
|
||||
"react-native": "0.42.3",
|
||||
"react-native-background-timer": "1.0.0",
|
||||
"react-native-immersive": "0.0.4",
|
||||
"react-native-keep-awake": "2.0.3",
|
||||
"react-native-background-timer": "1.0.1",
|
||||
"react-native-immersive": "0.0.5",
|
||||
"react-native-keep-awake": "2.0.4",
|
||||
"react-native-locale-detector": "1.0.1",
|
||||
"react-native-prompt": "1.0.0",
|
||||
"react-native-vector-icons": "4.0.1",
|
||||
"react-native-vector-icons": "4.1.1",
|
||||
"react-native-webrtc": "jitsi/react-native-webrtc",
|
||||
"react-redux": "5.0.4",
|
||||
"redux": "3.6.0",
|
||||
@@ -66,20 +67,20 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "6.24.1",
|
||||
"babel-eslint": "7.2.1",
|
||||
"babel-loader": "6.4.1",
|
||||
"babel-eslint": "7.2.3",
|
||||
"babel-loader": "7.0.0",
|
||||
"babel-polyfill": "6.23.0",
|
||||
"babel-preset-es2015": "6.24.1",
|
||||
"babel-preset-react": "6.24.1",
|
||||
"babel-preset-stage-1": "6.24.1",
|
||||
"clean-css": "3.4.25",
|
||||
"css-loader": "0.28.0",
|
||||
"css-loader": "0.28.1",
|
||||
"eslint": "3.19.0",
|
||||
"eslint-plugin-flowtype": "2.30.4",
|
||||
"eslint-plugin-import": "2.2.0",
|
||||
"eslint-plugin-jsdoc": "3.0.0",
|
||||
"eslint-plugin-jsdoc": "3.1.0",
|
||||
"eslint-plugin-react": "6.10.3",
|
||||
"eslint-plugin-react-native": "2.3.1",
|
||||
"eslint-plugin-react-native": "2.3.2",
|
||||
"expose-loader": "0.7.3",
|
||||
"file-loader": "0.11.1",
|
||||
"flow-bin": "0.38.0",
|
||||
@@ -90,7 +91,7 @@
|
||||
"node-sass": "3.13.1",
|
||||
"precommit-hook": "3.0.0",
|
||||
"string-replace-loader": "1.2.0",
|
||||
"style-loader": "0.16.1",
|
||||
"style-loader": "0.17.0",
|
||||
"webpack": "1.14.0",
|
||||
"webpack-dev-server": "1.16.3"
|
||||
},
|
||||
|
||||