mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-21 08:47:56 +00:00
Compare commits
60 Commits
1760
...
letsencryp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
029ca1753f | ||
|
|
2301732e2d | ||
|
|
24ee8eb16a | ||
|
|
57065bb274 | ||
|
|
08531ee675 | ||
|
|
e7140ffec7 | ||
|
|
c58c4b7938 | ||
|
|
4e276471e5 | ||
|
|
c5eac63da1 | ||
|
|
866c6d0cf9 | ||
|
|
165294bfb1 | ||
|
|
2d5f0479bd | ||
|
|
e8068cf5ac | ||
|
|
d0171cf386 | ||
|
|
3ae99ea0b9 | ||
|
|
4e9450f200 | ||
|
|
dc2c49f4a9 | ||
|
|
c461e8b63c | ||
|
|
f47bc1163b | ||
|
|
851be2d76e | ||
|
|
63034e6cba | ||
|
|
84b9c5f5fd | ||
|
|
43c8fc6847 | ||
|
|
bc60bd23b2 | ||
|
|
e29120a9c1 | ||
|
|
d383230532 | ||
|
|
9a46896600 | ||
|
|
fba086134d | ||
|
|
2973364c02 | ||
|
|
542bb7caed | ||
|
|
fb47b6ae21 | ||
|
|
aeb301c8d5 | ||
|
|
704e14f008 | ||
|
|
d1050d6b02 | ||
|
|
afc96808e8 | ||
|
|
dc2bae4ae1 | ||
|
|
1d7da21e48 | ||
|
|
affd965d5d | ||
|
|
989161159d | ||
|
|
59a74153dc | ||
|
|
6690c269ef | ||
|
|
b7fd10b905 | ||
|
|
08e1cf1b7e | ||
|
|
54d891afa7 | ||
|
|
ae41782cd4 | ||
|
|
8591fe00b6 | ||
|
|
92f58cb3c1 | ||
|
|
4ad98ca505 | ||
|
|
b9374bde6b | ||
|
|
1ff29384b3 | ||
|
|
4fc714ff10 | ||
|
|
51f0c8a388 | ||
|
|
d01a65f73d | ||
|
|
65239f9ffe | ||
|
|
e5cefcce70 | ||
|
|
8002b5ec6a | ||
|
|
a575f5cc77 | ||
|
|
ab3a80e076 | ||
|
|
dda3798ba9 | ||
|
|
5f387737a1 |
33
CONTRIBUTING.md
Normal file
33
CONTRIBUTING.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# How to contribute
|
||||
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.
|
||||
|
||||
# Code contributions
|
||||
Found a bug and know how to fix it? Great! Please read on.
|
||||
|
||||
## Contributor License Agreement
|
||||
While the Jitsi projects are released under the
|
||||
[Apache License 2.0](https://github.com/jitsi/jitsi-meet/blob/master/LICENSE), the copyright
|
||||
holder and principal creator is [Atlassian](https://www.atlassian.com/). To
|
||||
ensure that we can continue making these projects available under an Open Source license,
|
||||
we need you to sign our Apache-based contributor
|
||||
license agreement as either a [corporation](https://jitsi.org/ccla) or an
|
||||
[individual](https://jitsi.org/icla). If you cannot accept the terms laid out
|
||||
in the agreement, unfortunately, we cannot accept your contribution.
|
||||
|
||||
## Creating Pull Requests
|
||||
- Make sure your code passes the linter rules beforehand. The linter is exeuted
|
||||
automatically when committing code.
|
||||
- Perform **one** logical change per pull request.
|
||||
- Maintain a clean list of commits, squash them if necessary.
|
||||
- Rebase your topic branch on top of the master branch before creating the pull
|
||||
request.
|
||||
10
Makefile
10
Makefile
@@ -10,13 +10,7 @@ STYLES_DESTINATION = css/all.css
|
||||
STYLES_MAIN = css/main.scss
|
||||
WEBPACK = ./node_modules/.bin/webpack
|
||||
|
||||
all: update-deps compile deploy clean
|
||||
|
||||
# FIXME: there is a problem with node-sass not correctly installed (compiled)
|
||||
# a quick fix to make sure it is installed on every update
|
||||
# the problem appears on linux and not on macosx
|
||||
update-deps:
|
||||
$(NPM) update && $(NPM) install node-sass
|
||||
all: compile deploy clean
|
||||
|
||||
compile:
|
||||
$(WEBPACK) -p
|
||||
@@ -55,7 +49,7 @@ deploy-local:
|
||||
|
||||
source-package:
|
||||
mkdir -p source_package/jitsi-meet/css && \
|
||||
cp -r *.js *.html connection_optimization favicon.ico fonts images libs sounds LICENSE lang source_package/jitsi-meet && \
|
||||
cp -r *.js *.html connection_optimization favicon.ico fonts images libs static sounds LICENSE lang source_package/jitsi-meet && \
|
||||
cp css/all.css source_package/jitsi-meet/css && \
|
||||
(cd source_package ; tar cjf ../jitsi-meet.tar.bz2 jitsi-meet) && \
|
||||
rm -rf source_package
|
||||
|
||||
@@ -19,7 +19,7 @@ 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 get our mobile versoins from here:
|
||||
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)
|
||||
|
||||
@@ -86,6 +86,11 @@ npm unlink lib-jitsi-meet
|
||||
npm install
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
If you are looking to contribute to Jitsi Meet, first of all, thank you! Please
|
||||
see our [guidelines for contributing](CONTRIBUTING.md).
|
||||
|
||||
## Embedding in external applications
|
||||
|
||||
Jitsi Meet provides a very flexible way of embedding it in external applications by using the [Jitsi Meet API](doc/api.md).
|
||||
|
||||
@@ -39,6 +39,9 @@ import {
|
||||
participantLeft,
|
||||
participantRoleChanged
|
||||
} from './react/features/base/participants';
|
||||
import {
|
||||
showDesktopPicker
|
||||
} from './react/features/desktop-picker';
|
||||
import {
|
||||
mediaPermissionPromptVisibilityChanged,
|
||||
suspendDetected
|
||||
@@ -66,6 +69,16 @@ let DSExternalInstallationInProgress = false;
|
||||
|
||||
import {VIDEO_CONTAINER_TYPE} from "./modules/UI/videolayout/VideoContainer";
|
||||
|
||||
/*
|
||||
* Logic to open a desktop picker put on the window global for
|
||||
* lib-jitsi-meet to detect and invoke
|
||||
*/
|
||||
window.JitsiMeetScreenObtainer = {
|
||||
openDesktopPicker(onSourceChoose) {
|
||||
APP.store.dispatch(showDesktopPicker(onSourceChoose));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Known custom conference commands.
|
||||
*/
|
||||
@@ -252,8 +265,8 @@ function maybeRedirectToWelcomePage(options) {
|
||||
// save whether current user is guest or not, before navigating
|
||||
// to close page
|
||||
window.sessionStorage.setItem('guest', APP.tokenData.isGuest);
|
||||
assignWindowLocationPathname(
|
||||
options.feedbackSubmitted ? "close.html" : "close2.html");
|
||||
assignWindowLocationPathname('static/'
|
||||
+ (options.feedbackSubmitted ? "close.html" : "close2.html"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -402,7 +415,7 @@ class ConferenceConnector {
|
||||
case ConferenceErrors.NOT_ALLOWED_ERROR:
|
||||
{
|
||||
// let's show some auth not allowed page
|
||||
assignWindowLocationPathname('authError.html');
|
||||
assignWindowLocationPathname('static/authError.html');
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -714,6 +727,18 @@ export default {
|
||||
sendFeedback (overallFeedback, detailedFeedback) {
|
||||
return room.sendFeedback (overallFeedback, detailedFeedback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get speaker stats that track total dominant speaker time.
|
||||
*
|
||||
* @returns {object} A hash with keys being user ids and values being the
|
||||
* library's SpeakerStats model used for calculating time as dominant
|
||||
* speaker.
|
||||
*/
|
||||
getSpeakerStats() {
|
||||
return room.getSpeakerStats();
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the connection times stored in the library.
|
||||
*/
|
||||
@@ -729,6 +754,39 @@ export default {
|
||||
return this._room
|
||||
&& this._room.getConnectionState();
|
||||
},
|
||||
/**
|
||||
* Obtains current P2P ICE connection state.
|
||||
* @return {string|null} ICE connection state or <tt>null</tt> if there's no
|
||||
* P2P connection
|
||||
*/
|
||||
getP2PConnectionState () {
|
||||
return this._room
|
||||
&& this._room.getP2PConnectionState();
|
||||
},
|
||||
/**
|
||||
* Starts P2P (for tests only)
|
||||
* @private
|
||||
*/
|
||||
_startP2P () {
|
||||
try {
|
||||
this._room && this._room.startP2PSession();
|
||||
} catch (error) {
|
||||
logger.error("Start P2P failed", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Stops P2P (for tests only)
|
||||
* @private
|
||||
*/
|
||||
_stopP2P () {
|
||||
try {
|
||||
this._room && this._room.stopP2PSession();
|
||||
} catch (error) {
|
||||
logger.error("Stop P2P failed", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Checks whether or not our connection is currently in interrupted and
|
||||
* reconnect attempts are in progress.
|
||||
@@ -1259,20 +1317,12 @@ export default {
|
||||
APP.UI.showCustomToolbarPopup('#talkWhileMutedPopup', true, 5000);
|
||||
});
|
||||
|
||||
/*
|
||||
room.on(ConferenceEvents.IN_LAST_N_CHANGED, (inLastN) => {
|
||||
//FIXME
|
||||
if (config.muteLocalVideoIfNotInLastN) {
|
||||
// TODO mute or unmute if required
|
||||
// mark video on UI
|
||||
// APP.UI.markVideoMuted(true/false);
|
||||
}
|
||||
});
|
||||
*/
|
||||
room.on(
|
||||
ConferenceEvents.LAST_N_ENDPOINTS_CHANGED, (ids, enteringIds) => {
|
||||
APP.UI.handleLastNEndpoints(ids, enteringIds);
|
||||
ConferenceEvents.LAST_N_ENDPOINTS_CHANGED,
|
||||
(leavingIds, enteringIds) => {
|
||||
APP.UI.handleLastNEndpoints(leavingIds, enteringIds);
|
||||
});
|
||||
|
||||
room.on(
|
||||
ConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
|
||||
(id, isActive) => {
|
||||
@@ -1928,5 +1978,18 @@ export default {
|
||||
*/
|
||||
removeListener (eventName, listener) {
|
||||
eventEmitter.removeListener(eventName, listener);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the participant given by participantId is currently in the
|
||||
* last N set if there's one supported.
|
||||
*
|
||||
* @param participantId the identifier of the participant
|
||||
* @returns {boolean} {true} if the participant given by the participantId
|
||||
* is currently in the last N set or if there's no last N set at this point
|
||||
* and {false} otherwise
|
||||
*/
|
||||
isInLastN (participantId) {
|
||||
return room.isInLastN(participantId);
|
||||
}
|
||||
};
|
||||
|
||||
18
config.js
18
config.js
@@ -20,6 +20,13 @@ var config = { // eslint-disable-line no-unused-vars
|
||||
//focusUserJid: 'focus@auth.jitsi-meet.example.com', // The real JID of focus participant - can be overridden here
|
||||
//defaultSipNumber: '', // Default SIP number
|
||||
|
||||
// The STUN servers that will be used in the peer to peer connections
|
||||
p2pStunServers: [
|
||||
{ urls: "stun:stun.l.google.com:19302" },
|
||||
{ urls: "stun:stun1.l.google.com:19302" },
|
||||
{ urls: "stun:stun2.l.google.com:19302" }
|
||||
],
|
||||
|
||||
// The ID of the jidesha extension for Chrome.
|
||||
desktopSharingChromeExtId: null,
|
||||
// Whether desktop sharing should be disabled on Chrome.
|
||||
@@ -80,5 +87,14 @@ var config = { // eslint-disable-line no-unused-vars
|
||||
// disables or enables RTX (RFC 4588) (defaults to false).
|
||||
disableRtx: false,
|
||||
// Sets the preferred resolution (height) for local video. Defaults to 360.
|
||||
resolution: 720
|
||||
resolution: 720,
|
||||
// Enables peer to peer mode. When enabled system will try to establish
|
||||
// direct connection given that there are exactly 2 participants in
|
||||
// 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
|
||||
// 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
|
||||
};
|
||||
|
||||
@@ -84,7 +84,7 @@ form {
|
||||
height: 74px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
z-index: 2;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
.leftwatermark {
|
||||
@@ -106,7 +106,7 @@ form {
|
||||
font-size: 11pt;
|
||||
color: rgba(255,255,255,.50);
|
||||
text-decoration: none;
|
||||
z-index: 100;
|
||||
z-index: $poweredByZ;
|
||||
}
|
||||
|
||||
.connected {
|
||||
|
||||
@@ -212,24 +212,24 @@
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
:not(.default-scrollbar)::-webkit-scrollbar {
|
||||
background: #06a5df;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
:not(.default-scrollbar)::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
:not(.default-scrollbar)::-webkit-scrollbar-track {
|
||||
background: black;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track-piece {
|
||||
:not(.default-scrollbar)::-webkit-scrollbar-track-piece {
|
||||
background: black;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
:not(.default-scrollbar)::-webkit-scrollbar-thumb {
|
||||
background: #06a5df;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
flex-direction: column-reverse;
|
||||
flex-wrap: nowrap;
|
||||
position: relative;
|
||||
z-index: 1; // Set z-index to make element visible
|
||||
z-index: $zindex1; // Set z-index to make element visible
|
||||
width: $hideFilmstripButtonWidth;
|
||||
|
||||
button {
|
||||
@@ -55,7 +55,7 @@
|
||||
bottom: 0;
|
||||
width:auto;
|
||||
border: $thumbnailsBorder solid transparent;
|
||||
z-index: 5;
|
||||
z-index: $filmstripVideosZ;
|
||||
transition: bottom 2s;
|
||||
overflow: visible !important;
|
||||
/*!!!Removes the gap between the local video container and the remote
|
||||
|
||||
@@ -27,7 +27,86 @@
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
float: none !important;
|
||||
&-filmstrip-only {
|
||||
background-color: $inlayFilmstripOnlyBg;
|
||||
color: $inlayFilmstripOnlyColor;
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
margin-top: 20px;
|
||||
bottom: 30px;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
max-height: 120px;
|
||||
height: 80%;
|
||||
right: 0px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
&__content {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
> .button-control {
|
||||
align-self: center;
|
||||
}
|
||||
> #reloadProgressBar {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
margin-bottom: 0px;
|
||||
width: 100%;
|
||||
border-radius: 0px;
|
||||
> .aui-progress-indicator-value {
|
||||
border-radius: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__container {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 50px;
|
||||
align-self: center;
|
||||
color: $inlayIconColor;
|
||||
opacity: 0.6;
|
||||
}
|
||||
&__icon-container {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
&__avatar-container {
|
||||
position: relative;
|
||||
> img {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon-background {
|
||||
background: $inlayIconBg;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1010;
|
||||
z-index: $jitsipopoverZ;
|
||||
display: none;
|
||||
max-width: 300px;
|
||||
min-width: 100px;
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 8px;
|
||||
z-index: 1;
|
||||
z-index: $zindex1;
|
||||
width: 0;
|
||||
height: 0;
|
||||
content: '';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
overflow: hidden;
|
||||
padding: 20px;
|
||||
margin-left: 10px;
|
||||
z-index: 10;
|
||||
z-index: $zindex10;
|
||||
border-radius: $borderRadius;
|
||||
background-attachment: scroll;
|
||||
background-size: auto auto;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#notice {
|
||||
.notice {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
z-index: $zindex3;
|
||||
margin-top: 6px;
|
||||
|
||||
&__message {
|
||||
background-color: #000000;
|
||||
color: white;
|
||||
padding: 3px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
#noticeText {
|
||||
background-color: #000000;
|
||||
color: white;
|
||||
padding: 3px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1015;
|
||||
z-index: $popoverZ;
|
||||
display: none;
|
||||
max-width: 300px;
|
||||
min-width: 100px;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 0;
|
||||
z-index: 800;
|
||||
z-index: $sideToolbarContainerZ;
|
||||
|
||||
/**
|
||||
* Labels inside the side panel.
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
#subject {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
z-index: $zindex3;
|
||||
width: auto;
|
||||
padding: 5px;
|
||||
margin-left: 40%;
|
||||
@@ -106,7 +106,7 @@
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
z-index: $zindex1;
|
||||
font-size: $toolbarFontSize !important;
|
||||
line-height: 50px !important;
|
||||
vertical-align: middle;
|
||||
|
||||
@@ -104,13 +104,26 @@ $happySoftwareBackground: transparent;
|
||||
/**
|
||||
* Z-indexes. TODO: Replace this by a function.
|
||||
*/
|
||||
$tooltipsZ: 901;
|
||||
$toolbarZ: 900;
|
||||
$overlayZ: 902;
|
||||
$notificationZ: 1012;
|
||||
$ringingZ: 800;
|
||||
$dropdownZ: 901;
|
||||
$zindex0: 0;
|
||||
$zindex1: 1;
|
||||
$zindex2: 2;
|
||||
$zindex3: 3;
|
||||
$filmstripVideosZ: 5;
|
||||
$zindex10: 10;
|
||||
$reloadZ: 20;
|
||||
$poweredByZ: 100;
|
||||
$ringingZ: 300;
|
||||
$sideToolbarContainerZ: 300;
|
||||
$toolbarZ: 400;
|
||||
$tooltipsZ: 401;
|
||||
$dropdownMaskZ: 900;
|
||||
$dropdownZ: 901;
|
||||
$overlayZ: 902;
|
||||
$jitsipopoverZ: 1010;
|
||||
$centeredVideoLabelZ: 1011;
|
||||
$notificationZ: 1012;
|
||||
$popoverZ: 1015;
|
||||
|
||||
|
||||
/**
|
||||
* Font Colors
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
&__toptoolbar {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
z-index: $zindex3;
|
||||
width: 100%;
|
||||
box-sizing: border-box; // Includes the padding in the 100% width.
|
||||
}
|
||||
@@ -59,7 +59,7 @@
|
||||
float: left;
|
||||
@include circle($thumbnailIndicatorSize);
|
||||
box-sizing: border-box;
|
||||
z-index: 3;
|
||||
z-index: $zindex3;
|
||||
background: $dominantSpeakerBg;
|
||||
color: $thumbnailPictogramColor;
|
||||
border: $thumbnailIndicatorBorder solid $thumbnailPictogramColor;
|
||||
@@ -113,7 +113,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
visibility: hidden;
|
||||
z-index: 2;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
z-index: $zindex1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -171,7 +171,7 @@
|
||||
}
|
||||
|
||||
#etherpad {
|
||||
z-index: 0;
|
||||
z-index: $zindex0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +193,7 @@
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
line-height: $thumbnailToolbarHeight;
|
||||
z-index: 2;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,7 +233,7 @@
|
||||
padding: 3px 5px;
|
||||
font-size: 9pt;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,7 +283,7 @@
|
||||
top: 0px;
|
||||
right: 0;
|
||||
margin: 7px;
|
||||
z-index: 3;
|
||||
z-index: $zindex3;
|
||||
width: 18px;
|
||||
height: 13px;
|
||||
color: #FFF;
|
||||
@@ -301,7 +301,7 @@
|
||||
margin-top: -17px;
|
||||
width: 6px;
|
||||
height: 35px;
|
||||
z-index: 2;
|
||||
z-index: $zindex2;
|
||||
border: none;
|
||||
|
||||
.audiodot-top,
|
||||
@@ -344,13 +344,13 @@
|
||||
background-clip: padding-box;
|
||||
-webkit-border-radius: 5px;
|
||||
-webkit-background-clip: padding-box;
|
||||
z-index: 20; /*The reload button should appear on top of the header!*/
|
||||
z-index: $reloadZ; /*The reload button should appear on top of the header!*/
|
||||
}
|
||||
|
||||
.audiolevel {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
z-index: $zindex0;
|
||||
border-radius:1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -408,7 +408,7 @@
|
||||
.noMic {
|
||||
position: absolute;
|
||||
border-radius: 8px;
|
||||
z-index: 1;
|
||||
z-index: $zindex1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url("../images/noMic.png");
|
||||
@@ -420,7 +420,7 @@
|
||||
.noVideo {
|
||||
position: absolute;
|
||||
border-radius: 8px;
|
||||
z-index: 1;
|
||||
z-index: $zindex1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url("../images/noVideo.png");
|
||||
@@ -453,7 +453,7 @@
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: auto;
|
||||
z-index: 2;
|
||||
z-index: $zindex2;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
@@ -477,7 +477,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
top:50%;
|
||||
z-index: 2;
|
||||
z-index: $zindex2;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
@@ -506,7 +506,7 @@
|
||||
#videoResolutionLabel,
|
||||
.centeredVideoLabel {
|
||||
display: none;
|
||||
z-index: 1011;
|
||||
z-index: $centeredVideoLabelZ;
|
||||
}
|
||||
|
||||
.centeredVideoLabel {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
color: #acacac;
|
||||
z-index: 2;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
#disable_welcome:checked + label
|
||||
@@ -35,7 +35,7 @@
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
color: #acacac;
|
||||
z-index: 2;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
#enter_room_form {
|
||||
@@ -74,7 +74,7 @@
|
||||
float: left;
|
||||
background-color: #FFFFFF;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
|
||||
&__reload {
|
||||
@@ -83,7 +83,7 @@
|
||||
color: #acacac;
|
||||
font-size: 1.9em;
|
||||
line-height: 55px;
|
||||
z-index: 3;
|
||||
z-index: $zindex3;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
@@ -104,7 +104,7 @@
|
||||
outline: none;
|
||||
float:left;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: $zindex2;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
&_overlay {
|
||||
color: $primaryButtonColor;
|
||||
background-color: $overlayButtonBg;
|
||||
border-radius: 2px;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background-color: $primaryButtonBackground;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
&_primary {
|
||||
background-color: $primaryButtonBackground;
|
||||
border: 1px solid $primaryButtonBackground;
|
||||
@@ -86,4 +98,4 @@
|
||||
&_center {
|
||||
float: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,10 @@
|
||||
@import 'overlay/overlay';
|
||||
@import 'inlay';
|
||||
@import 'reload_overlay/reload_overlay';
|
||||
@import 'modals/desktop-picker/desktop-picker';
|
||||
@import 'modals/dialog';
|
||||
@import 'modals/feedback/feedback';
|
||||
@import 'modals/speaker_stats/speaker_stats';
|
||||
@import 'videolayout_default';
|
||||
@import 'notice';
|
||||
@import 'popup_menu';
|
||||
|
||||
59
css/modals/desktop-picker/_desktop-picker.scss
Normal file
59
css/modals/desktop-picker/_desktop-picker.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
.desktop-picker-pane {
|
||||
height: 320px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
|
||||
&.source-type-screen {
|
||||
.desktop-picker-source {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.desktop-source-preview-thumbnail {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.desktop-source-preview-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.source-type-window {
|
||||
.desktop-picker-source {
|
||||
display: inline-block;
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-picker-source {
|
||||
color: $defaultDarkFontColor;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
|
||||
&.is-selected {
|
||||
.desktop-source-preview-image-container {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: $borderRadius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-source-preview-label {
|
||||
margin-top: 3px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.desktop-source-preview-thumbnail {
|
||||
box-shadow: 5px 5px 5px grey;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.desktop-source-preview-image-container {
|
||||
padding: 10px;
|
||||
}
|
||||
56
css/modals/speaker_stats/_speaker_stats.scss
Normal file
56
css/modals/speaker_stats/_speaker_stats.scss
Normal file
@@ -0,0 +1,56 @@
|
||||
.speaker-stats {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
color: $auiDialogColor;
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
|
||||
.speaker-stats-item__status-dot {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto;
|
||||
|
||||
&.status-active {
|
||||
background: green;
|
||||
}
|
||||
|
||||
&.status-inactive {
|
||||
background: gray;
|
||||
}
|
||||
}
|
||||
|
||||
.status-user-left {
|
||||
color: $placeHolderColor;
|
||||
}
|
||||
|
||||
.speaker-stats-item__status,
|
||||
.speaker-stats-item__name,
|
||||
.speaker-stats-item__time {
|
||||
display: inline-block;
|
||||
margin: 5px 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.speaker-stats-item__status {
|
||||
width: 5%;
|
||||
}
|
||||
.speaker-stats-item__name {
|
||||
width: 40%;
|
||||
}
|
||||
.speaker-stats-item__time {
|
||||
width: 55%;
|
||||
}
|
||||
|
||||
.speaker-stats-item:nth-child(even) {
|
||||
background: whitesmoke;
|
||||
}
|
||||
|
||||
.speaker-stats-item__name,
|
||||
.speaker-stats-item__time {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,16 @@
|
||||
position: fixed;
|
||||
z-index: $overlayZ;
|
||||
background: $defaultBackground;
|
||||
&.filmstrip-only {
|
||||
@include transparentBg($filmStripOnlyOverlayBg, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
&__container-light {
|
||||
@include transparentBg($defaultBackground, 0.7);
|
||||
&.filmstrip-only {
|
||||
@include transparentBg($filmStripOnlyOverlayBg, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
@@ -21,6 +27,11 @@
|
||||
width: 56%;
|
||||
left: 50%;
|
||||
@include transform(translateX(-50%));
|
||||
&.filmstrip-only {
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
@include transform(none);
|
||||
}
|
||||
|
||||
&_bottom {
|
||||
position: absolute;
|
||||
@@ -33,4 +44,4 @@
|
||||
bottom: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,7 @@
|
||||
#reloadProgressBar {
|
||||
width: 180px;
|
||||
margin: 5px auto;
|
||||
> .aui-progress-indicator-value {
|
||||
background: $reloadProgressBarBg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,14 @@ $primaryButtonFontWeight: 400;
|
||||
|
||||
$buttonShadowColor: #192d4f;
|
||||
|
||||
$overlayButtonBg: #0074E0;
|
||||
|
||||
/**
|
||||
* Color variables
|
||||
**/
|
||||
$defaultBackground: #474747;
|
||||
$filmStripOnlyOverlayBg: #000;
|
||||
$reloadProgressBarBg: #0074E0;
|
||||
|
||||
/**
|
||||
* Connection indicator
|
||||
@@ -60,6 +64,10 @@ $dialogTitleFontWeight: 400;
|
||||
**/
|
||||
$inlayColorBg: lighten($defaultBackground, 20%);
|
||||
$inlayBorderColor: lighten($auiDialogContentBg, 10%);
|
||||
$inlayIconBg: #000;
|
||||
$inlayIconColor: #fff;
|
||||
$inlayFilmstripOnlyColor: #474747;
|
||||
$inlayFilmstripOnlyBg: #fff;
|
||||
|
||||
// Main controls
|
||||
$inputBackground: $controlBackground;
|
||||
|
||||
9
debian/jitsi-meet-web-config.postinst
vendored
9
debian/jitsi-meet-web-config.postinst
vendored
@@ -65,7 +65,7 @@ case "$1" in
|
||||
# SSL for nginx
|
||||
db_get jitsi-meet/cert-choice
|
||||
CERT_CHOICE="$RET"
|
||||
UPLOADED_CERT_CHOICE="A certificate is available and the files are uploaded on the server"
|
||||
UPLOADED_CERT_CHOICE="I want to use my own certificate"
|
||||
|
||||
if [ "$CERT_CHOICE" = "$UPLOADED_CERT_CHOICE" ] ; then
|
||||
db_set jitsi-meet/cert-path-key "/etc/ssl/$JVB_HOSTNAME.key"
|
||||
@@ -223,6 +223,13 @@ case "$1" in
|
||||
invoke-rc.d apache2 reload
|
||||
fi
|
||||
|
||||
echo "----------------"
|
||||
echo ""
|
||||
echo "You can now switch to a Let’s Encrypt certificate. To do so, execute:"
|
||||
echo "/usr/share/jitsi-meet/scripts/install-letsencrypt-cert.sh"
|
||||
echo ""
|
||||
echo "----------------"
|
||||
|
||||
# and we're done with debconf
|
||||
db_stop
|
||||
;;
|
||||
|
||||
3
debian/jitsi-meet-web-config.templates
vendored
3
debian/jitsi-meet-web-config.templates
vendored
@@ -1,9 +1,10 @@
|
||||
Template: jitsi-meet/cert-choice
|
||||
Type: select
|
||||
__Choices: Self-signed certificate will be generated, A certificate is available and the files are uploaded on the server
|
||||
__Choices: Generate a new self-signed certificate (You will later get a chance to obtain a Let's encrypt certificate), I want to use my own certificate
|
||||
_Description: SSL certificate for the Jitsi Meet instance
|
||||
Jitsi Meet is best to be set up with an SSL certificate.
|
||||
Having no certificate, a self-signed one will be generated.
|
||||
By choosing self-signed you will later have a chance to install Let’s Encrypt certificates.
|
||||
Having a certificate signed by a recognised CA, it can be uploaded on the server
|
||||
and point its location. The default filenames will be /etc/ssl/--domain.name--.key
|
||||
for the key and /etc/ssl/--domain.name--.crt for the certificate.
|
||||
|
||||
5
debian/jitsi-meet-web.install
vendored
5
debian/jitsi-meet-web.install
vendored
@@ -1,11 +1,14 @@
|
||||
*.js /usr/share/jitsi-meet/
|
||||
interface_config.js /usr/share/jitsi-meet/
|
||||
logging_config.js /usr/share/jitsi-meet/
|
||||
*.json /usr/share/jitsi-meet/
|
||||
*.html /usr/share/jitsi-meet/
|
||||
*.ico /usr/share/jitsi-meet/
|
||||
libs /usr/share/jitsi-meet/
|
||||
static /usr/share/jitsi-meet/
|
||||
css/all.css /usr/share/jitsi-meet/css/
|
||||
sounds /usr/share/jitsi-meet/
|
||||
fonts /usr/share/jitsi-meet/
|
||||
images /usr/share/jitsi-meet/
|
||||
lang /usr/share/jitsi-meet/
|
||||
connection_optimization /usr/share/jitsi-meet/
|
||||
resources/*.sh /usr/share/jitsi-meet/scripts/
|
||||
|
||||
4
debian/po/templates.pot
vendored
4
debian/po/templates.pot
vendored
@@ -20,13 +20,13 @@ msgstr ""
|
||||
#. Type: select
|
||||
#. Choices
|
||||
#: ../jitsi-meet-web-config.templates:1001
|
||||
msgid "Self-signed certificate will be generated"
|
||||
msgid "Generate a new self-signed certificate (You will later get a chance to obtain a Let's encrypt certificate)"
|
||||
msgstr ""
|
||||
|
||||
#. Type: select
|
||||
#. Choices
|
||||
#: ../jitsi-meet-web-config.templates:1001
|
||||
msgid "A certificate is available and the files are uploaded on the server"
|
||||
msgid "I want to use my own certificate"
|
||||
msgstr ""
|
||||
|
||||
#. Type: select
|
||||
|
||||
61
doc/api.md
61
doc/api.md
@@ -10,37 +10,56 @@ To embed Jitsi Meet in your application you need to add the Jitsi Meet API libra
|
||||
<script src="https://meet.jit.si/external_api.js"></script>
|
||||
```
|
||||
|
||||
The next step for embedding Jitsi Meet is to create the Jitsi Meet API object:
|
||||
|
||||
```javascript
|
||||
<script>
|
||||
var domain = "meet.jit.si";
|
||||
var room = "JitsiMeetAPIExample";
|
||||
var width = 700;
|
||||
var height = 700;
|
||||
var api = new JitsiMeetExternalAPI(domain, room, width, height);
|
||||
</script>
|
||||
```
|
||||
|
||||
You can use the above lines to indicate where exactly you want the Jitsi Meet conference to be placed in your HTML code,
|
||||
or you can specify the parent HTML element for the Jitsi Meet conference in the `JitsiMeetExternalAPI`
|
||||
constructor:
|
||||
## API
|
||||
|
||||
### `api = new JitsiMeetExternalAPI(domain, room, [width], [height], [htmlElement], [configOverwite], [interfaceConfigOverwrite], [noSsl], [jwt])`
|
||||
|
||||
The next step for embedding Jitsi Meet is to create the Jitsi Meet API object.
|
||||
Its constructor gets a number of options:
|
||||
|
||||
* **domain**: domain used to build the conference URL, "meet.jit.si" for
|
||||
example.
|
||||
* **room**: name of the room to join.
|
||||
* **width**: (optional) width for the iframe which will be created.
|
||||
* **height**: (optional) height for the iframe which will be created.
|
||||
* **htmlElement**: (optional) HTL DOM Element where the iframe will be added as
|
||||
a child.
|
||||
* **configOverwite**: (optional) JS object with overrides for options defined in
|
||||
[config.js].
|
||||
* **interfaceConfigOverwrite**: (optional) JS object with overrides for options
|
||||
defined in [interface_config.js].
|
||||
* **noSsl**: (optional, defaults to true) Boolean indicating if the server
|
||||
should be contacted using HTTP or HTTPS.
|
||||
* **jwt**: (optional) [JWT](https://jwt.io/) token.
|
||||
|
||||
Example:
|
||||
|
||||
```javascript
|
||||
var domain = "meet.jit.si";
|
||||
var room = "JitsiMeetAPIExample";
|
||||
var width = 700;
|
||||
var height = 700;
|
||||
var htmlElement = document.querySelector('#meet');
|
||||
var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement);
|
||||
```
|
||||
|
||||
If you don't specify the room the user will enter in new conference with a random room name.
|
||||
|
||||
You can overwrite options set in [config.js]() and [interface_config.js](). For example, to enable the film-strip-only interface mode and disable simulcast, you can use:
|
||||
You can overwrite options set in [config.js] and [interface_config.js].
|
||||
For example, to enable the film-strip-only interface mode, you can use:
|
||||
|
||||
```javascript
|
||||
var configOverwrite = {disableSimulcast: true};
|
||||
var interfaceConfigOverwrite = {filmStripOnly: true};
|
||||
var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement, configOverwrite, interfaceConfigOverwrite);
|
||||
var api = new JitsiMeetExternalAPI(domain, room, width, height, undefined, undefined, interfaceConfigOverwrite);
|
||||
```
|
||||
|
||||
## Controlling the embedded Jitsi Meet Conference
|
||||
You can also pass a jwt token to Jitsi Meet:
|
||||
|
||||
```javascript
|
||||
var jwt = "<jwt_token>";
|
||||
var noSsl = false;
|
||||
var api = new JitsiMeetExternalAPI(domain, room, width, height, htmlElement, configOverwrite, interfaceConfigOverwrite, noSsl, jwt);
|
||||
```
|
||||
|
||||
### Controlling the embedded Jitsi Meet Conference
|
||||
|
||||
You can control the embedded Jitsi Meet conference using the `JitsiMeetExternalAPI` object by using `executeCommand`:
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ server {
|
||||
|
||||
root /usr/share/jitsi-meet;
|
||||
index index.html index.htm;
|
||||
error_page 404 /404.html;
|
||||
error_page 404 /static/404.html;
|
||||
|
||||
location /config.js {
|
||||
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
Allow from all
|
||||
</Directory>
|
||||
|
||||
ErrorDocument 404 /static/404.html
|
||||
|
||||
Alias "/config.js" "/etc/jitsi/meet/jitsi-meet.example.com-config.js"
|
||||
<Location /config.js>
|
||||
Require all granted
|
||||
|
||||
@@ -6,6 +6,34 @@ change references to that to match your host, and generate some passwords for
|
||||
|
||||
There are also some complete [example config files](https://github.com/jitsi/jitsi-meet/tree/master/doc/example-config-files/) available, mentioned in each section.
|
||||
|
||||
## Network description
|
||||
|
||||
This how the network look like:
|
||||
```
|
||||
+ +
|
||||
| |
|
||||
| |
|
||||
v |
|
||||
443 |
|
||||
+-------+ |
|
||||
| | |
|
||||
| NginX | |
|
||||
| | |
|
||||
+--+-+--+ |
|
||||
| | |
|
||||
+------------+ | | +--------------+ |
|
||||
| | | | | | |
|
||||
| jitsi-meet +<---+ +--->+ prosody/xmpp | |
|
||||
| |files 5280 | | |
|
||||
+------------+ +--------------+ v
|
||||
5222,5347^ ^5347 4443
|
||||
+--------+ | | +-------------+
|
||||
| | | | | |
|
||||
| jicofo +----^ ^----+ videobridge |
|
||||
| | | |
|
||||
+--------+ +-------------+
|
||||
```
|
||||
|
||||
## Install prosody
|
||||
```sh
|
||||
apt-get install prosody
|
||||
@@ -76,7 +104,9 @@ Add a new file `jitsi.example.com` in `/etc/nginx/sites-available` (see also the
|
||||
server_names_hash_bucket_size 64;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen 443;
|
||||
# tls configuration that is not covered in this guide
|
||||
# we recommend the use of https://certbot.eff.org/
|
||||
server_name jitsi.example.com;
|
||||
# set the root
|
||||
root /srv/jitsi.example.com;
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
'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="utils.js?v=1"></script>
|
||||
<script src="static/utils.js?v=1"></script>
|
||||
<!--#include virtual="connection_optimization/connection_optimization.html" -->
|
||||
<script src="connection_optimization/do_external_connect.js?v=1"></script>
|
||||
<script><!--#include virtual="/interface_config.js" --></script>
|
||||
|
||||
19
lang/languages-eo.json
Normal file
19
lang/languages-eo.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"en": "",
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"es": "",
|
||||
"fr": "",
|
||||
"hy": "",
|
||||
"it": "",
|
||||
"oc": "",
|
||||
"pl": "",
|
||||
"ptBR": "",
|
||||
"ru": "",
|
||||
"sk": "",
|
||||
"sl": "",
|
||||
"sv": "",
|
||||
"tr": "",
|
||||
"zhCN": "",
|
||||
"nb": ""
|
||||
}
|
||||
@@ -13,5 +13,7 @@
|
||||
"sk": "Eslovac",
|
||||
"sl": "Eslovèn",
|
||||
"sv": "Suedés",
|
||||
"tr": "Turc"
|
||||
"tr": "Turc",
|
||||
"zhCN": "Chinés (China)",
|
||||
"nb": "Norvegian Bokmål"
|
||||
}
|
||||
@@ -6,12 +6,14 @@
|
||||
"fr": "Francês",
|
||||
"hy": "Armênio",
|
||||
"it": "Italiano",
|
||||
"oc": "Provençal",
|
||||
"oc": "Occitano",
|
||||
"pl": "Polonês",
|
||||
"ptBR": "Português (Brasil)",
|
||||
"ru": "Russo",
|
||||
"sk": "Eslovaco",
|
||||
"sl": "Esloveno",
|
||||
"sv": "Sueco",
|
||||
"tr": "Turco"
|
||||
"tr": "Turco",
|
||||
"zhCN": "Chinês (China)",
|
||||
"nb": "Bokmal norueguês"
|
||||
}
|
||||
@@ -1,17 +1,20 @@
|
||||
{
|
||||
"en": "",
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"es": "",
|
||||
"fr": "",
|
||||
"hy": "",
|
||||
"it": "",
|
||||
"oc": "",
|
||||
"pl": "",
|
||||
"ptBR": "",
|
||||
"ru": "",
|
||||
"sk": "",
|
||||
"sl": "",
|
||||
"sv": "",
|
||||
"tr": ""
|
||||
"en": "英语",
|
||||
"bg": "保加利亚语",
|
||||
"de": "德语",
|
||||
"es": "西班牙语",
|
||||
"fr": "法语",
|
||||
"hy": "亚美尼亚语",
|
||||
"it": "意大利语",
|
||||
"oc": "欧西坦语",
|
||||
"pl": "波兰语",
|
||||
"ptBR": "葡萄牙语(巴西)",
|
||||
"ru": "俄语",
|
||||
"sk": "斯洛伐克语",
|
||||
"sl": "斯洛文尼亚语",
|
||||
"sv": "瑞典语",
|
||||
"tr": "土耳其语",
|
||||
"zhCN": "中文(中国)",
|
||||
"nb": "",
|
||||
"eo": ""
|
||||
}
|
||||
@@ -15,5 +15,6 @@
|
||||
"sv": "Swedish",
|
||||
"tr": "Turkish",
|
||||
"zhCN": "Chinese (China)",
|
||||
"nb": "Norwegian Bokmal"
|
||||
"nb": "Norwegian Bokmal",
|
||||
"eo": "Esperanto"
|
||||
}
|
||||
|
||||
374
lang/main-eo.json
Normal file
374
lang/main-eo.json
Normal file
@@ -0,0 +1,374 @@
|
||||
{
|
||||
"contactlist": "",
|
||||
"addParticipants": "",
|
||||
"roomLocked": "",
|
||||
"roomUnlocked": "",
|
||||
"passwordSetRemotely": "",
|
||||
"connectionsettings": "",
|
||||
"poweredby": "",
|
||||
"feedback": "",
|
||||
"inviteUrlDefaultMsg": "",
|
||||
"me": "",
|
||||
"speaker": "",
|
||||
"raisedHand": "",
|
||||
"defaultNickname": "",
|
||||
"defaultLink": "",
|
||||
"callingName": "",
|
||||
"userMedia": {
|
||||
"react-nativeGrantPermissions": "",
|
||||
"chromeGrantPermissions": "",
|
||||
"androidGrantPermissions": "",
|
||||
"firefoxGrantPermissions": "",
|
||||
"operaGrantPermissions": "",
|
||||
"iexplorerGrantPermissions": "",
|
||||
"safariGrantPermissions": "",
|
||||
"nwjsGrantPermissions": ""
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"keyboardShortcuts": "",
|
||||
"raiseHand": "",
|
||||
"pushToTalk": "",
|
||||
"toggleScreensharing": "",
|
||||
"toggleFilmstrip": "",
|
||||
"toggleShortcuts": "",
|
||||
"focusLocal": "",
|
||||
"focusRemote": "",
|
||||
"toggleChat": "",
|
||||
"mute": "",
|
||||
"fullScreen": "",
|
||||
"videoMute": ""
|
||||
},
|
||||
"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": "",
|
||||
"rejoinKeyTitle": ""
|
||||
},
|
||||
"toolbar": {
|
||||
"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": "",
|
||||
"noPermission": "",
|
||||
"cameraAndMic": "",
|
||||
"moderator": "",
|
||||
"password": "",
|
||||
"audioVideo": "",
|
||||
"setPasswordLabel": ""
|
||||
},
|
||||
"profile": {
|
||||
"title": "",
|
||||
"setDisplayNameLabel": "",
|
||||
"setEmailLabel": "",
|
||||
"setEmailInput": ""
|
||||
},
|
||||
"videothumbnail": {
|
||||
"editnickname": "",
|
||||
"moderator": "",
|
||||
"videomute": "",
|
||||
"mute": "",
|
||||
"kick": "",
|
||||
"muted": "",
|
||||
"domute": "",
|
||||
"flip": "",
|
||||
"remoteControl": ""
|
||||
},
|
||||
"connectionindicator": {
|
||||
"header": "",
|
||||
"bitrate": "",
|
||||
"packetloss": "",
|
||||
"resolution": "",
|
||||
"less": "",
|
||||
"more": "",
|
||||
"address": "",
|
||||
"remoteport": "",
|
||||
"remoteport_plural": "",
|
||||
"localport": "",
|
||||
"localport_plural": "",
|
||||
"localaddress": "",
|
||||
"localaddress_plural": "",
|
||||
"remoteaddress": "",
|
||||
"remoteaddress_plural": "",
|
||||
"transport": "",
|
||||
"bandwidth": "",
|
||||
"na": ""
|
||||
},
|
||||
"notify": {
|
||||
"disconnected": "",
|
||||
"moderator": "",
|
||||
"connected": "",
|
||||
"somebody": "",
|
||||
"me": "",
|
||||
"focus": "",
|
||||
"focusFail": "",
|
||||
"grantedTo": "",
|
||||
"grantedToUnknown": "",
|
||||
"muted": "",
|
||||
"mutedTitle": "",
|
||||
"raisedHand": ""
|
||||
},
|
||||
"dialog": {
|
||||
"add": "",
|
||||
"kickMessage": "",
|
||||
"popupError": "",
|
||||
"passwordErrorTitle": "",
|
||||
"passwordError": "",
|
||||
"passwordError2": "",
|
||||
"connectError": "",
|
||||
"connectErrorWithMsg": "",
|
||||
"incorrectPassword": "",
|
||||
"connecting": "",
|
||||
"copy": "",
|
||||
"error": "",
|
||||
"roomLocked": "",
|
||||
"addPassword": "",
|
||||
"createPassword": "",
|
||||
"detectext": "",
|
||||
"failtoinstall": "",
|
||||
"failedpermissions": "",
|
||||
"conferenceReloadTitle": "",
|
||||
"conferenceReloadMsg": "",
|
||||
"conferenceDisconnectTitle": "",
|
||||
"conferenceDisconnectMsg": "",
|
||||
"reconnectNow": "",
|
||||
"conferenceReloadTimeLeft": "",
|
||||
"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": "",
|
||||
"remoteControlDeniedMessage": "",
|
||||
"remoteControlAllowedMessage": "",
|
||||
"remoteControlErrorMessage": "",
|
||||
"remoteControlStopMessage": ""
|
||||
},
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@
|
||||
"roomUnlocked": "Qual que siá amb lo ligam pòt participar",
|
||||
"passwordSetRemotely": "causit per qualqu'un mai",
|
||||
"connectionsettings": "Paramètres de connexion",
|
||||
"poweredby": "Produit per",
|
||||
"feedback": "Donatz-nos vòstre vejaire",
|
||||
"poweredby": "produit per",
|
||||
"feedback": "Donatz-nos lo vòstre vejaire",
|
||||
"inviteUrlDefaultMsg": "Vòstra conferéncia es en cors de creacion...",
|
||||
"me": "ieu",
|
||||
"speaker": "Nautparlaire",
|
||||
"raisedHand": "Volriatz charrar",
|
||||
"raisedHand": "Volriá charrar",
|
||||
"defaultNickname": "ex. Joan Delpuèch",
|
||||
"defaultLink": "ex. __url__",
|
||||
"callingName": "__name__",
|
||||
@@ -39,41 +39,46 @@
|
||||
"videoMute": "Aviar o arrestar vòstra camerà"
|
||||
},
|
||||
"welcomepage": {
|
||||
"go": "Crear",
|
||||
"roomname": "Sasissètz un nom de sala",
|
||||
"disable": "Afichar pas mai aquesta pagina",
|
||||
"feature1": {
|
||||
"title": "De bon utilizar",
|
||||
"content": "Cap de telecargament pas requesit. __app__ s'utiliza dirèctament dempuèi vòstre navigador. Partejar simplament l'URL de vòstra conferéncia amb los autres per començar."
|
||||
"content": "Cap de telecargament pas requesit. __app__ s'utiliza dirèctament dempuèi vòstre navigador. Partejar simplament l'URL de vòstra conferéncia amb los autres per començar.",
|
||||
"title": "De bon utilizar"
|
||||
},
|
||||
"feature2": {
|
||||
"title": "Benda passanta febla",
|
||||
"content": "Las videoconferéncias de mantun participant necessitan mens de 128 kbps. Lo partiment d'ecran e las conferéncias amb solament d'àudio son possiblas amb plan mens de debit."
|
||||
"content": "Las videoconferéncias de mantun participant necessitan mens de 128 kbps. Lo partiment d'ecran e las conferéncias amb solament d'àudio son possiblas amb plan mens de debit.",
|
||||
"title": "Benda passanta febla"
|
||||
},
|
||||
"feature3": {
|
||||
"title": "Open source",
|
||||
"content": "__app__ es jos licéncia Apache. Sètz liure de telecargar, d'utilizar, de modificar e de partejar __app__ segon aquesta licéncia liura."
|
||||
"content": "__app__ es jos licéncia Apache. Sètz liure de telecargar, d'utilizar, de modificar e de partejar __app__ segon aquesta licéncia liura.",
|
||||
"title": "Open source"
|
||||
},
|
||||
"feature4": {
|
||||
"title": "Nombre d'utilizaires illimitat",
|
||||
"content": "I a pas de restriccions artificialas a prepaus del nombre d'utilizaires o de participants a una conferéncia. La poténcia del servidor e la benda passanta son los sols factors limitants."
|
||||
"content": "I a pas de restriccions artificialas a prepaus del nombre d'utilizaires o de participants a una conferéncia. La poténcia del servidor e la benda passanta son los sols factors limitants.",
|
||||
"title": "Nombre d'utilizaires illimitat"
|
||||
},
|
||||
"feature5": {
|
||||
"title": "Partiment d'ecran",
|
||||
"content": "Es aisit de partejar vòstre ecran amb d'autras personas. __app__ es ideal per las presentacions en linha, los corses, e las sessions de supòrt tecnic."
|
||||
"content": "Es aisit de partejar vòstre ecran amb d'autras personas. __app__ es ideal per las presentacions en linha, los corses, e las sessions de supòrt tecnic.",
|
||||
"title": "Partiment d'ecran"
|
||||
},
|
||||
"feature6": {
|
||||
"title": "Salas securizadas",
|
||||
"content": "Besonh de confidencialitat ? Las salas de conferéncia __app__ pòdon èsser securizadas per un senhal per exclure los convidats pas desirats, e prevenir de las interrupcions."
|
||||
"content": "Besonh de confidencialitat ? Las salas de conferéncia __app__ pòdon èsser securizadas per un senhal per exclure los convidats pas desirats, e prevenir de las interrupcions.",
|
||||
"title": "Salas securizadas"
|
||||
},
|
||||
"feature7": {
|
||||
"title": "Nòtas partejadas",
|
||||
"content": "__app__ prepausa Etherpad, un editor de tèxte collaboratiu en temps real qu'es parfèit pels procèsses verbals, l'edicion d'articles e plan mai encara."
|
||||
"content": "__app__ prepausa Etherpad, un editor de tèxte collaboratiu en temps real qu'es parfèit pels procèsses verbals, l'edicion d'articles e plan mai encara.",
|
||||
"title": "Nòtas partejadas"
|
||||
},
|
||||
"feature8": {
|
||||
"title": "Estatisticas d'utilizacion",
|
||||
"content": "Aprenètz mai a prepaus de vòstres utilizaires amb una integracion aisida de Piwik, Google Analytics e d'autres sistèmas d'estatisticas e supervision d'utilizacion."
|
||||
}
|
||||
"content": "Aprenètz mai a prepaus de vòstres utilizaires amb una integracion aisida de Piwik, Google Analytics e d'autres sistèmas d'estatisticas e supervision d'utilizacion.",
|
||||
"title": "Estatisticas d'utilizacion"
|
||||
},
|
||||
"go": "Crear",
|
||||
"join": "PARTICIPATZ",
|
||||
"privacy": "Vida privada",
|
||||
"roomname": "Sasissètz un nom de sala",
|
||||
"roomnamePlaceHolder": "nom de la sala",
|
||||
"sendFeedback": "Mandar vòstra opinion",
|
||||
"terms": "Tèrmes"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
@@ -108,7 +113,14 @@
|
||||
"micDisabled": "Lo microfòn es pas disponible",
|
||||
"filmstrip": "Mostrar / escondre vidèos",
|
||||
"profile": "Modificar vòstre perfil",
|
||||
"raiseHand": "Demandar / Demandar pas mai la paraula"
|
||||
"raiseHand": "o se l'avètz ja<br /><strong>alara</strong>"
|
||||
},
|
||||
"unsupportedBrowser": {
|
||||
"appInstalled": "o se ja l'avètz<br /><strong>alara</strong>",
|
||||
"appNotInstalled": "Vos cal <strong>__app__</strong> per participar a la conversacion a partir de vòstre mobil",
|
||||
"downloadApp": "Telecargar l'aplicacion",
|
||||
"joinConversation": "Participar a la conversacion",
|
||||
"startConference": "Començar una conferéncia"
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "Dobrir / tampar lo chat",
|
||||
@@ -126,15 +138,15 @@
|
||||
"title": "Paramètres",
|
||||
"update": "Mesa a jorn",
|
||||
"name": "Nom",
|
||||
"startAudioMuted": "Tot lo mond comença sens son",
|
||||
"startVideoMuted": "Tot lo mond comença escondut",
|
||||
"selectCamera": "Camèra",
|
||||
"startAudioMuted": "Començan totes sens son",
|
||||
"startVideoMuted": "Començan totes sens vidèo",
|
||||
"selectCamera": "Camerà",
|
||||
"selectMic": "Microfòn",
|
||||
"selectAudioOutput": "Sortida àudio",
|
||||
"followMe": "Tot lo mond me sèc",
|
||||
"noDevice": "Pas cap",
|
||||
"noPermission": "La permission d'utilizar l'aparelh es pas estada donada",
|
||||
"cameraAndMic": "Camèra e microfòn",
|
||||
"cameraAndMic": "Camerà e microfòn",
|
||||
"moderator": "MODERATOR",
|
||||
"password": "DEFINIR UN SENHAL",
|
||||
"audioVideo": "ÀUDIO E VIDÈO",
|
||||
@@ -149,12 +161,13 @@
|
||||
"videothumbnail": {
|
||||
"editnickname": "Clicatz per modificar<br/>vòstre nom",
|
||||
"moderator": "Lo proprietari de<br/>aquesta conferéncia",
|
||||
"videomute": "Un participant a<br/>arrestat sa camèra.",
|
||||
"videomute": "Un participant a<br/>arrestat sa camerà.",
|
||||
"mute": "Un participant a copat son micro",
|
||||
"kick": "Exclure",
|
||||
"muted": "Mut",
|
||||
"domute": "Copar lo son",
|
||||
"flip": "Revirar"
|
||||
"flip": "Revirar",
|
||||
"remoteControl": "Contraròtle alonhat"
|
||||
},
|
||||
"connectionindicator": {
|
||||
"header": "Donadas de connexion",
|
||||
@@ -162,7 +175,7 @@
|
||||
"packetloss": "Pèrda de paquets :",
|
||||
"resolution": "Resolucion :",
|
||||
"less": "Amagar lo detalh",
|
||||
"more": "Mostrar lo deta1h",
|
||||
"more": "Ne veire mai",
|
||||
"address": "Adreça :",
|
||||
"remoteport": "Pòrt distant :",
|
||||
"remoteport_plural": "Pòrts distants :",
|
||||
@@ -208,7 +221,7 @@
|
||||
"createPassword": "Crear un senhal",
|
||||
"detectext": "Una error s'es produita pendent la deteccion de l'extension de partiment d'ecran.",
|
||||
"failtoinstall": "Fracàs de l'installacion de l'extension de partiment d'ecran",
|
||||
"failedpermissions": "Fracàs d'obtencion de las permissions per utilizar lo micro e/o la camèra.",
|
||||
"failedpermissions": "Fracàs d'obtencion de las permissions per utilizar lo micro e/o la camerà.",
|
||||
"conferenceReloadTitle": "Malurosament, quicòm truquèt",
|
||||
"conferenceReloadMsg": "Sèm a trobar una solucion per aquò",
|
||||
"conferenceDisconnectTitle": "Sètz estat desconnectat. Vos cal benlèu verificar vòstra connexion.",
|
||||
@@ -224,7 +237,7 @@
|
||||
"internalError": "Ops ! Quicòm a pas fonccionat. L'error seguenta s'es produsida : [setRemoteDescription]",
|
||||
"unableToSwitch": "Impossible de cambiar lo flux vidèo.",
|
||||
"SLDFailure": "Ops! Quicòm a trucat e lo micro es pas estat copat! (Fracàs SLD)",
|
||||
"SRDFailure": "Ops! Quicòm a trucat e la camèra es pas estada copada! (Fracàs SRD)",
|
||||
"SRDFailure": "Ops ! Quicòm a trucat e la camerà es pas estada arrestada ! (Fracàs SRD)",
|
||||
"oops": "Ops !",
|
||||
"currentPassword": "L'actual senhal es",
|
||||
"passwordLabel": "Senhal",
|
||||
@@ -298,21 +311,26 @@
|
||||
"cameraErrorPresent": "I a agut una error pendent la connexion a la camerà.",
|
||||
"cameraUnsupportedResolutionError": "Vòstra camerà pren pas en carga la resolucion vidèo que cal.",
|
||||
"cameraUnknownError": "Impossible d'emplegar la camerà per una rason desconeguda.",
|
||||
"cameraPermissionDeniedError": "La camèra es pas estada trobada.",
|
||||
"cameraNotFoundError": "La camèra es pas estada trobada.",
|
||||
"cameraPermissionDeniedError": "La camerà es pas estada trobada.",
|
||||
"cameraNotFoundError": "La camerà es pas estada trobada.",
|
||||
"cameraConstraintFailedError": "Vòstra camerà satisfà pas totas las constrentas necessàrias.",
|
||||
"micUnknownError": "Impossible d'utilizar lo microfòn per una rason desconeguda.",
|
||||
"micPermissionDeniedError": "Avètz pas donat l'autorizacion d'utilizar vòstre microfòn. Podètz encara participar a la conferéncia mai los demai vos ausiràn pas. Utilizatz lo boton del microfòn dins la barra d'adreça per resòlvre aquò.",
|
||||
"micNotFoundError": "Lo microfòn es pas estat trobat.",
|
||||
"micConstraintFailedError": "Vòstre microfòn satisfà pas totas las constrentas necessàrias.",
|
||||
"micNotSendingData": "Podèm pas aver l'accès a vòstre microfòn. Mercés de ne causir un autre dins lo menú de paramètres o ensajatz de tornar dubrir l'aplicacion.",
|
||||
"cameraNotSendingData": "Podèm pas aver l'accès a vòstra camèra. Mercés de verificar se una autra aplicacion es pas a l'utilizar, causissètz una autra camèra dins lo menú de paramètres o ensajatz de tornar dubrir l'aplicacion.",
|
||||
"cameraNotSendingData": "Podèm pas aver l'accès a vòstra camerà. Mercés de verificar se una autra aplicacion es pas a l'utilizar, causissètz una autra camerà dins lo menú de paramètres o ensajatz de tornar dubrir l'aplicacion.",
|
||||
"goToStore": "Anar sul webstore",
|
||||
"externalInstallationTitle": "Extension requesida :",
|
||||
"externalInstallationMsg": "Avètz d'installar nòstra extension de partiment d'ecran.",
|
||||
"muteParticipantTitle": "Copar lo micro als participants ?",
|
||||
"muteParticipantBody": "Poiretz pas lo tornar activar lo microfòn, mai eles pòdon o far quand vòlon.",
|
||||
"muteParticipantButton": "Copar lo son"
|
||||
"muteParticipantButton": "Copar lo son",
|
||||
"remoteControlTitle": "Contraròtle alonhat",
|
||||
"remoteControlDeniedMessage": "__user__ a refusat vòstra demanda de contraròtle alonhat !",
|
||||
"remoteControlAllowedMessage": "__user__ a acceptat vòstra demanda de contraròtle alonhat !",
|
||||
"remoteControlErrorMessage": "Error al moment de demandar lo contraròtle alonhat a __user__ !",
|
||||
"remoteControlStopMessage": "La session de contraròtle alonhat es acabada !"
|
||||
},
|
||||
"email": {
|
||||
"sharedKey": [
|
||||
@@ -371,6 +389,7 @@
|
||||
"failedToStart": "Lo dirècte a pas capitat de s'aviar",
|
||||
"buttonTooltip": "Aviar / arrestar lo dirècte",
|
||||
"streamIdRequired": "Mercé de completar lo stream id per aviar lo dirècte.",
|
||||
"streamIdHelp": "Ont tròbi aquò ?",
|
||||
"error": "Fracàs de la transmission en dirècte. Mercés de tornar ensajar.",
|
||||
"busy": "Tots los enresgistraires son ocupats. Mercés de tornar ensajar mai tard."
|
||||
}
|
||||
|
||||
@@ -39,41 +39,46 @@
|
||||
"videoMute": "Iniciar ou parar sua câmera"
|
||||
},
|
||||
"welcomepage": {
|
||||
"go": "IR",
|
||||
"roomname": "Digite o nome da sala",
|
||||
"disable": "Não mostre esta página novamente",
|
||||
"feature1": {
|
||||
"title": "Simples para usar",
|
||||
"content": "Não precisa baixar nada. __app__ trabalha diretamente no seu navegador. Simplesmente compartilhe sua URL da conferência com outros para começar."
|
||||
"content": "Não precisa baixar nada. __app__ trabalha diretamente no seu navegador. Simplesmente compartilhe sua URL da conferência com outros para começar.",
|
||||
"title": "Simples para usar"
|
||||
},
|
||||
"feature2": {
|
||||
"title": "Largura de banda baixa",
|
||||
"content": "Conferências de vídeo de multipartes trabalha com tão pouco quanto 128 kbps. Compartilhamento de tela e conferências de áudio somente são possíveis com muito menos."
|
||||
"content": "Conferências de vídeo de multipartes trabalha com tão pouco quanto 128 kbps. Compartilhamento de tela e conferências de áudio somente são possíveis com muito menos.",
|
||||
"title": "Largura de banda baixa"
|
||||
},
|
||||
"feature3": {
|
||||
"title": "Código aberto",
|
||||
"content": "__app__ é licenciado sob a Licença Apache. Você é livre para baixar, usar, modificar e compartilhar ela com a mesma licença."
|
||||
"content": "__app__ é licenciado sob a Licença Apache. Você é livre para baixar, usar, modificar e compartilhar ela com a mesma licença.",
|
||||
"title": "Código aberto"
|
||||
},
|
||||
"feature4": {
|
||||
"title": "Usuários ilimitados",
|
||||
"content": "Aqui não há restrições artificiais no número de usuários ou participantes da conferência. A potência do servidor e largura de banda são os únicos fatores limitantes."
|
||||
"content": "Aqui não há restrições artificiais no número de usuários ou participantes da conferência. A potência do servidor e largura de banda são os únicos fatores limitantes.",
|
||||
"title": "Usuários ilimitados"
|
||||
},
|
||||
"feature5": {
|
||||
"title": "Compartilhamento de tela",
|
||||
"content": "É fácil compartilhar sua tela com outros. __app__ é ideal para apresentações online, leituras, e sessões de suporte técnico."
|
||||
"content": "É fácil compartilhar sua tela com outros. __app__ é ideal para apresentações online, leituras, e sessões de suporte técnico.",
|
||||
"title": "Compartilhamento de tela"
|
||||
},
|
||||
"feature6": {
|
||||
"title": "Salas seguras",
|
||||
"content": "Precisa alguma privacidade? Salas de conferência do __app__ podem ser seguras com uma senha para excluir visitantes indesejados e prevenir interrupções."
|
||||
"content": "Precisa alguma privacidade? Salas de conferência do __app__ podem ser seguras com uma senha para excluir visitantes indesejados e prevenir interrupções.",
|
||||
"title": "Salas seguras"
|
||||
},
|
||||
"feature7": {
|
||||
"title": "Notas compartilhadas",
|
||||
"content": "__app_ disponibiliza o Etherpad, um editor de texto colaborativo em tempo real, que é ótimo para reuniões rápidas, escrevendo artigos, e mais."
|
||||
"content": "__app_ disponibiliza o Etherpad, um editor de texto colaborativo em tempo real, que é ótimo para reuniões rápidas, escrevendo artigos, e mais.",
|
||||
"title": "Notas compartilhadas"
|
||||
},
|
||||
"feature8": {
|
||||
"title": "Estatísticas de uso",
|
||||
"content": "Aprenda sobre seus usuários através de integração fácil com o Piwik, Google Analytics, e outros sistemas de monitoramento e estatísticas."
|
||||
}
|
||||
"content": "Aprenda sobre seus usuários através de integração fácil com o Piwik, Google Analytics, e outros sistemas de monitoramento e estatísticas.",
|
||||
"title": "Estatísticas de uso"
|
||||
},
|
||||
"go": "IR",
|
||||
"join": "Entrar",
|
||||
"privacy": "Política de Privacidade",
|
||||
"roomname": "Digite o nome da sala",
|
||||
"roomnamePlaceHolder": "Nome da sala",
|
||||
"sendFeedback": "Enviar comentários",
|
||||
"terms": "Termos"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
@@ -110,6 +115,13 @@
|
||||
"profile": "Editar seu perfil",
|
||||
"raiseHand": "Erguer o baixar sua mão"
|
||||
},
|
||||
"unsupportedBrowser": {
|
||||
"appInstalled": "ou se você já tenha isso<br /> <strong>então</strong>",
|
||||
"appNotInstalled": "Você precisa do <strong>__app__</strong> para começar um bate-papo no seu celular",
|
||||
"downloadApp": "Baixe o Aplicativo",
|
||||
"joinConversation": "Entre no bate-papo",
|
||||
"startConference": "Comece uma conferência"
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "Abrir / fechar bate-papo",
|
||||
"filmstrip": "Mostrar/ocultar vídeos",
|
||||
@@ -154,7 +166,8 @@
|
||||
"kick": "Chutar fora",
|
||||
"muted": "Mudo",
|
||||
"domute": "Mudo",
|
||||
"flip": "Inverter"
|
||||
"flip": "Inverter",
|
||||
"remoteControl": "Controle remoto"
|
||||
},
|
||||
"connectionindicator": {
|
||||
"header": "Dados da conexão",
|
||||
@@ -312,7 +325,12 @@
|
||||
"externalInstallationMsg": "Você precisa instalar nossa extensão de compartilhamento de tela.",
|
||||
"muteParticipantTitle": "Deixar mudo este participante?",
|
||||
"muteParticipantBody": "Você não está habilitado para tirar o mudo deles, mas eles podem tirar o mudo deles mesmos a qualquer tempo.",
|
||||
"muteParticipantButton": "Mudo"
|
||||
"muteParticipantButton": "Mudo",
|
||||
"remoteControlTitle": "Controle remoto",
|
||||
"remoteControlDeniedMessage": "__user__ rejeitou sua requisição de controle remoto!",
|
||||
"remoteControlAllowedMessage": "__user__ aceitou sua requisição de controle remoto!",
|
||||
"remoteControlErrorMessage": "Um erro ocorreu enquanto tentava requerer a permissão de controle remoto de __user__!",
|
||||
"remoteControlStopMessage": "A sessão de controle remoto terminou!"
|
||||
},
|
||||
"email": {
|
||||
"sharedKey": [
|
||||
@@ -371,6 +389,7 @@
|
||||
"failedToStart": "Falha ao iniciar a transmissão ao vivo",
|
||||
"buttonTooltip": "Iniciar / parar live stream",
|
||||
"streamIdRequired": "Por favor digite o id da transmissão ao vivo para começar a transmitir.",
|
||||
"streamIdHelp": "Aonde eu encontro isto?",
|
||||
"error": "Falha na transmissão ao vivo. Tente novamente",
|
||||
"busy": "Todos os gravadores estão ocupados no momento. Por favor, tente mais tarde."
|
||||
}
|
||||
|
||||
@@ -1,355 +1,396 @@
|
||||
{
|
||||
"contactlist": "",
|
||||
"addParticipants": "",
|
||||
"roomLocked": "",
|
||||
"roomUnlocked": "",
|
||||
"passwordSetRemotely": "",
|
||||
"connectionsettings": "",
|
||||
"poweredby": "",
|
||||
"feedback": "",
|
||||
"inviteUrlDefaultMsg": "",
|
||||
"me": "",
|
||||
"speaker": "",
|
||||
"raisedHand": "",
|
||||
"defaultNickname": "",
|
||||
"defaultLink": "",
|
||||
"callingName": "",
|
||||
"contactlist": "与会者 (__pcount__)",
|
||||
"addParticipants": "分享链接",
|
||||
"roomLocked": "与会者必须输入密码",
|
||||
"roomUnlocked": "任何人都可以通过此链接参加会议",
|
||||
"passwordSetRemotely": "由其他与会者设置",
|
||||
"connectionsettings": "连接设置",
|
||||
"poweredby": "技术支持",
|
||||
"feedback": "请给我们您的反馈",
|
||||
"inviteUrlDefaultMsg": "您的会议正在被创建。。。",
|
||||
"me": "我",
|
||||
"speaker": "发言人",
|
||||
"raisedHand": "请求发言",
|
||||
"defaultNickname": "例如 星视通",
|
||||
"defaultLink": "例如 __url__",
|
||||
"callingName": "__name__",
|
||||
"userMedia": {
|
||||
"react-nativeGrantPermissions": "",
|
||||
"chromeGrantPermissions": "",
|
||||
"androidGrantPermissions": "",
|
||||
"firefoxGrantPermissions": "",
|
||||
"operaGrantPermissions": "",
|
||||
"iexplorerGrantPermissions": "",
|
||||
"safariGrantPermissions": "",
|
||||
"nwjsGrantPermissions": ""
|
||||
"react-nativeGrantPermissions": "请点击<b><i>允许</i></b>按钮授权使用您的摄像头和麦克风",
|
||||
"chromeGrantPermissions": "请点击<b><i>允许</i></b>按钮授权使用您的摄像头和麦克风",
|
||||
"androidGrantPermissions": "请点击<b><i>允许</i></b>按钮授权使用您的摄像头和麦克风",
|
||||
"firefoxGrantPermissions": "请点击<b><i>共享选择的设备</i></b>按钮授权使用您的摄像头和麦克风",
|
||||
"operaGrantPermissions": "请点击<b><i>允许</i></b>按钮授权使用您的摄像头和麦克风",
|
||||
"iexplorerGrantPermissions": "请点击<b><i>确认</i></b>按钮授权使用您的摄像头和麦克风",
|
||||
"safariGrantPermissions": "请点击<b><i>确认</i></b>按钮授权使用您的摄像头和麦克风",
|
||||
"nwjsGrantPermissions": "请授权使用您的摄像头和麦克风"
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"keyboardShortcuts": "",
|
||||
"raiseHand": "",
|
||||
"pushToTalk": "",
|
||||
"toggleScreensharing": "",
|
||||
"toggleFilmstrip": "",
|
||||
"toggleShortcuts": "",
|
||||
"focusLocal": "",
|
||||
"focusRemote": "",
|
||||
"toggleChat": "",
|
||||
"mute": "",
|
||||
"fullScreen": "",
|
||||
"videoMute": ""
|
||||
"keyboardShortcuts": "快捷键",
|
||||
"raiseHand": "申请或取消发言",
|
||||
"pushToTalk": "按住说话",
|
||||
"toggleScreensharing": "在摄像头和屏幕共享之间切换",
|
||||
"toggleFilmstrip": "显示或隐藏视频",
|
||||
"toggleShortcuts": "显示或隐藏帮助菜单",
|
||||
"focusLocal": "切换到本地视频上",
|
||||
"focusRemote": "切换到远端视频上",
|
||||
"toggleChat": "打开或关闭聊天",
|
||||
"mute": "静音或取消静音",
|
||||
"fullScreen": "全屏或退出全屏",
|
||||
"videoMute": "开启或关闭视频"
|
||||
},
|
||||
"welcomepage": {
|
||||
"go": "",
|
||||
"roomname": "",
|
||||
"disable": "",
|
||||
"disable": "不再显示该页",
|
||||
"feature1": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
"content": "无需下载. __app__ 直接通过浏览器使用。 分享您的会议链接给其他人即可参与会议。",
|
||||
"title": "简单易用"
|
||||
},
|
||||
"feature2": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
"content": "多方视频会议所需带宽仅需128Kbps。 屏幕共享和语音会议所需的带宽更少。",
|
||||
"title": "低带宽"
|
||||
},
|
||||
"feature3": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
"content": "__app__ 有Apache许可. 在此许可下,您可以免费下载,使用,修改和分享该代码",
|
||||
"title": "开源"
|
||||
},
|
||||
"feature4": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
"content": "对于使用者和与会者没有人数的限制。 服务器的性能和带宽是唯一的限制因素。",
|
||||
"title": "不限用户数"
|
||||
},
|
||||
"feature5": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
"content": "和他人共享屏幕非常简单。 __app__ 对于在线演示、讲座和技术支持会议再合适不过了。",
|
||||
"title": "屏幕共享"
|
||||
},
|
||||
"feature6": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
"content": "是否担心隐私安全? __app__ 可以设定会议室密码防止他人进入会议。",
|
||||
"title": "安全"
|
||||
},
|
||||
"feature7": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
"content": "__app__ 的一大特色是Etherpad——一个完美适用于会议、写作等场景,可实时协作的文本编辑器。",
|
||||
"title": "共享笔记"
|
||||
},
|
||||
"feature8": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
}
|
||||
"content": "通过简单地整合Piwik, Google Analytics或者其他使用监控和统计系统来了解您的使用者。",
|
||||
"title": "使用统计"
|
||||
},
|
||||
"go": "开始",
|
||||
"join": "",
|
||||
"privacy": "",
|
||||
"roomname": "请输入房间名",
|
||||
"roomnamePlaceHolder": "",
|
||||
"sendFeedback": "",
|
||||
"terms": ""
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": "",
|
||||
"title": ""
|
||||
"policyText": " ",
|
||||
"title": "__app__ 需要使用您的麦克风和摄像头。"
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
"title": "",
|
||||
"rejoinKeyTitle": ""
|
||||
"title": "由于您的电脑休眠,视频通话已经中断。",
|
||||
"rejoinKeyTitle": "重新加入"
|
||||
},
|
||||
"toolbar": {
|
||||
"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": ""
|
||||
"mute": "静音 / 解除静音",
|
||||
"videomute": "开启 / 关闭 摄像头",
|
||||
"authenticate": "认证",
|
||||
"lock": "锁定 / 解锁 房间",
|
||||
"invite": "分享链接",
|
||||
"chat": "开启 / 关闭 聊天",
|
||||
"etherpad": "开启 / 关闭 共享文档",
|
||||
"sharedvideo": "分享YouTube视频",
|
||||
"sharescreen": "开启 / 关闭 屏幕共享",
|
||||
"fullscreen": "开启 / 关闭 全屏",
|
||||
"sip": "呼叫SIP号码",
|
||||
"Settings": "设置",
|
||||
"hangup": "离开",
|
||||
"login": "登录",
|
||||
"logout": "登出",
|
||||
"dialpad": "开启 / 关闭 拨号盘",
|
||||
"sharedVideoMutedPopup": "您共享的视频已被关闭 <br/> 您可以与其他与会者交谈。",
|
||||
"micMutedPopup": "您的麦克风已被静音 您<br/>可以尽情享受您的共享视频。",
|
||||
"talkWhileMutedPopup": "您在尝试发言吗? 当前您已被静音。",
|
||||
"unableToUnmutePopup": "正在共享视频的时候您不能解除静音。",
|
||||
"cameraDisabled": "摄像头不可用",
|
||||
"micDisabled": "麦克风不可用",
|
||||
"filmstrip": "显示 / 隐藏 视频",
|
||||
"profile": "编辑您的简介",
|
||||
"raiseHand": "请求 / 取消 发言"
|
||||
},
|
||||
"unsupportedBrowser": {
|
||||
"appInstalled": "",
|
||||
"appNotInstalled": "",
|
||||
"downloadApp": "",
|
||||
"joinConversation": "",
|
||||
"startConference": ""
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "",
|
||||
"filmstrip": "",
|
||||
"contactlist": ""
|
||||
"chat": "开启 / 关闭 聊天",
|
||||
"filmstrip": "显示 / 隐藏 视频",
|
||||
"contactlist": "查看和邀请与会者"
|
||||
},
|
||||
"chat": {
|
||||
"nickname": {
|
||||
"title": "",
|
||||
"popover": ""
|
||||
"title": "请在下面的方框内输入昵称",
|
||||
"popover": "选择一个昵称"
|
||||
},
|
||||
"messagebox": ""
|
||||
"messagebox": "请输入文本..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "",
|
||||
"update": "",
|
||||
"name": "",
|
||||
"startAudioMuted": "",
|
||||
"startVideoMuted": "",
|
||||
"selectCamera": "",
|
||||
"selectMic": "",
|
||||
"selectAudioOutput": "",
|
||||
"followMe": "",
|
||||
"noDevice": "",
|
||||
"noPermission": "",
|
||||
"cameraAndMic": "",
|
||||
"moderator": "",
|
||||
"password": "",
|
||||
"audioVideo": "",
|
||||
"setPasswordLabel": ""
|
||||
"title": "设置",
|
||||
"update": "更新",
|
||||
"name": "名称",
|
||||
"startAudioMuted": "所有人开始时静音",
|
||||
"startVideoMuted": "所有人开始时隐藏视频画面",
|
||||
"selectCamera": "摄像头",
|
||||
"selectMic": "麦克风",
|
||||
"selectAudioOutput": "音频输出",
|
||||
"followMe": "所有人跟随我",
|
||||
"noDevice": "未发现设备",
|
||||
"noPermission": "未授权使用设备",
|
||||
"cameraAndMic": "摄像头和麦克风",
|
||||
"moderator": "主持人",
|
||||
"password": "设定密码",
|
||||
"audioVideo": "音频和视频",
|
||||
"setPasswordLabel": "用密码锁定房间"
|
||||
},
|
||||
"profile": {
|
||||
"title": "",
|
||||
"setDisplayNameLabel": "",
|
||||
"setEmailLabel": "",
|
||||
"setEmailInput": ""
|
||||
"title": "简介",
|
||||
"setDisplayNameLabel": "设定您的显示名称",
|
||||
"setEmailLabel": "设置您的个人全球统一标识邮箱",
|
||||
"setEmailInput": "输入您的邮箱"
|
||||
},
|
||||
"videothumbnail": {
|
||||
"editnickname": "",
|
||||
"moderator": "",
|
||||
"videomute": "",
|
||||
"mute": "",
|
||||
"kick": "",
|
||||
"muted": "",
|
||||
"domute": "",
|
||||
"flip": ""
|
||||
"editnickname": "点击编辑您的<br/>显示名称",
|
||||
"moderator": "<br/>此会议的拥有者",
|
||||
"videomute": "与会者已经<br/>关闭了摄像头",
|
||||
"mute": "与会者已被静音",
|
||||
"kick": "踢出",
|
||||
"muted": "已静音",
|
||||
"domute": "静音",
|
||||
"flip": "翻转",
|
||||
"remoteControl": "远程控制"
|
||||
},
|
||||
"connectionindicator": {
|
||||
"header": "",
|
||||
"bitrate": "",
|
||||
"packetloss": "",
|
||||
"resolution": "",
|
||||
"less": "",
|
||||
"more": "",
|
||||
"address": "",
|
||||
"remoteport_plural": "",
|
||||
"remoteport": "",
|
||||
"localport_plural": "",
|
||||
"localport": "",
|
||||
"localaddress_plural": "",
|
||||
"localaddress": "",
|
||||
"remoteaddress_plural": "",
|
||||
"remoteaddress": "",
|
||||
"transport": "",
|
||||
"bandwidth": "",
|
||||
"na": ""
|
||||
"header": "连接数据",
|
||||
"bitrate": "比特率",
|
||||
"packetloss": "丢包",
|
||||
"resolution": "分辨率",
|
||||
"less": "显示更少",
|
||||
"more": "显示更多",
|
||||
"address": "地址",
|
||||
"remoteport_plural": "远程端口:",
|
||||
"remoteport": "远程端口:",
|
||||
"localport_plural": "本地端口:",
|
||||
"localport": "本地端口:",
|
||||
"localaddress_plural": "本地地址:",
|
||||
"localaddress": "本地地址:",
|
||||
"remoteaddress_plural": "远程地址:",
|
||||
"remoteaddress": "远程地址:",
|
||||
"transport": "传输:",
|
||||
"bandwidth": "估计带宽:",
|
||||
"na": "会议开始可回到此处查看连接信息"
|
||||
},
|
||||
"notify": {
|
||||
"disconnected": "",
|
||||
"moderator": "",
|
||||
"connected": "",
|
||||
"somebody": "",
|
||||
"me": "",
|
||||
"focus": "",
|
||||
"focusFail": "",
|
||||
"grantedTo": "",
|
||||
"grantedToUnknown": "",
|
||||
"muted": "",
|
||||
"mutedTitle": "",
|
||||
"raisedHand": ""
|
||||
"disconnected": "已断开连接",
|
||||
"moderator": "已授权主持人权限!",
|
||||
"connected": "已连接",
|
||||
"somebody": "某人",
|
||||
"me": "自己",
|
||||
"focus": "会议聚焦",
|
||||
"focusFail": "__component__ 不可用 - 在__ms__秒后重试",
|
||||
"grantedTo": "主持权限已授予__to__!",
|
||||
"grantedToUnknown": "主持权限已授予$t(somebody)!",
|
||||
"muted": "您已经开始了通话,并处于静音状态。",
|
||||
"mutedTitle": "您已被静音!",
|
||||
"raisedHand": "请求发言"
|
||||
},
|
||||
"dialog": {
|
||||
"add": "",
|
||||
"kickMessage": "",
|
||||
"popupError": "",
|
||||
"passwordErrorTitle": "",
|
||||
"passwordError": "",
|
||||
"passwordError2": "",
|
||||
"connectError": "",
|
||||
"connectErrorWithMsg": "",
|
||||
"incorrectPassword": "",
|
||||
"connecting": "",
|
||||
"copy": "",
|
||||
"error": "",
|
||||
"roomLocked": "",
|
||||
"addPassword": "",
|
||||
"createPassword": "",
|
||||
"detectext": "",
|
||||
"failtoinstall": "",
|
||||
"failedpermissions": "",
|
||||
"conferenceReloadTitle": "",
|
||||
"conferenceReloadMsg": "",
|
||||
"conferenceDisconnectTitle": "",
|
||||
"conferenceDisconnectMsg": "",
|
||||
"reconnectNow": "",
|
||||
"conferenceReloadTimeLeft": "",
|
||||
"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": ""
|
||||
"add": "添加",
|
||||
"kickMessage": "您已被踢出会议!",
|
||||
"popupError": "您的浏览器禁止弹窗,请到浏览器安全设置里设定允许弹窗后重试。",
|
||||
"passwordErrorTitle": "密码错误",
|
||||
"passwordError": "此会议现在受密码保护。只有会议的拥有者可以设定密码。",
|
||||
"passwordError2": "此会议现在受密码保护。只有会议的拥有者可以设定密码。",
|
||||
"connectError": "发生错误,无法连接至会议!",
|
||||
"connectErrorWithMsg": "发生错误,无法连接至会议: __msg__",
|
||||
"incorrectPassword": "密码不正确",
|
||||
"connecting": "连接中",
|
||||
"copy": "复制",
|
||||
"error": "错误",
|
||||
"roomLocked": "此会话已被锁定,新与会者必须有链接地址和密码才能加入",
|
||||
"addPassword": "添加密码",
|
||||
"createPassword": "创建密码",
|
||||
"detectext": "尝试检测桌面共享扩展时发生错误",
|
||||
"failtoinstall": "安装桌面共享扩展失败",
|
||||
"failedpermissions": "未能获取使用本地麦克风或摄像头的权限。",
|
||||
"conferenceReloadTitle": "不好意思,出错了",
|
||||
"conferenceReloadMsg": "正在尝试修复",
|
||||
"conferenceDisconnectTitle": "您已断开连接,请检查您的网络连接。",
|
||||
"conferenceDisconnectMsg": "重新连接中。。。",
|
||||
"reconnectNow": "现在开始重新连接",
|
||||
"conferenceReloadTimeLeft": "__seconds__秒。",
|
||||
"maxUsersLimitReached": "已达到会议的最大人数限制,请稍后尝试!",
|
||||
"lockTitle": "锁定失败",
|
||||
"lockMessage": "锁定会议失败。",
|
||||
"warning": "警告",
|
||||
"passwordNotSupported": "当前不支持给房间加密码。",
|
||||
"internalErrorTitle": "内部错误",
|
||||
"internalError": "发生以下错误: [setRemoteDescription]",
|
||||
"unableToSwitch": "无法转换视频流。",
|
||||
"SLDFailure": "发生错误,无法静音! (SLD故障)",
|
||||
"SRDFailure": "发生错误,无法关闭视频! (SRD故障)",
|
||||
"oops": "哎呀!",
|
||||
"currentPassword": "当前的密码是",
|
||||
"passwordLabel": "密码",
|
||||
"defaultError": "有某种错误",
|
||||
"passwordRequired": "需要密码",
|
||||
"Ok": "好的",
|
||||
"done": "完成",
|
||||
"Remove": "移除",
|
||||
"removePassword": "移除密码",
|
||||
"shareVideoTitle": "分享视频",
|
||||
"shareVideoLinkError": "请提供正确的youtube链接。",
|
||||
"removeSharedVideoTitle": "移除共享的视频",
|
||||
"removeSharedVideoMsg": "您确定要移除共享的视频吗?",
|
||||
"alreadySharedVideoMsg": "其他的与会者正在分享视频,同一时间只能有一个人分享视频。",
|
||||
"WaitingForHost": "等待主持人。。。",
|
||||
"WaitForHostMsg": "会议<b>__room__ </b>还没有开始。如果您是主持人请授权开始,否则请等待主持人。",
|
||||
"IamHost": "我是主持人。",
|
||||
"Cancel": "取消",
|
||||
"Submit": "提交",
|
||||
"retry": "重试",
|
||||
"logoutTitle": "登出",
|
||||
"logoutQuestion": "你确定要登出并停止会议吗",
|
||||
"sessTerminated": "会话终止",
|
||||
"hungUp": "挂断",
|
||||
"joinAgain": "重新加入",
|
||||
"Share": "分享",
|
||||
"Save": "保存",
|
||||
"recording": "录制中",
|
||||
"recordingToken": "输入记录标识",
|
||||
"Dial": "拨号",
|
||||
"sipMsg": "输入SIP号码",
|
||||
"passwordCheck": "确定要移除密码吗?",
|
||||
"passwordMsg": "设定密码来锁定房间",
|
||||
"shareLink": "分享此会议的链接",
|
||||
"settings1": "配置您的会议",
|
||||
"settings2": "与会者静音后加入",
|
||||
"settings3": "需要昵称<br/><br/>设定密码来锁定房间:",
|
||||
"yourPassword": "输入新的密码",
|
||||
"Back": "返回",
|
||||
"serviceUnavailable": "服务不可用",
|
||||
"gracefulShutdown": "服务器正在维护,请稍后再试。",
|
||||
"Yes": "是",
|
||||
"reservationError": "预定系统错误",
|
||||
"reservationErrorMsg": "错误代号: __code__, 提示信息: __msg__",
|
||||
"password": "输入密码",
|
||||
"userPassword": "用户密码",
|
||||
"token": "标识",
|
||||
"tokenAuthFailedTitle": "认证问题",
|
||||
"tokenAuthFailed": "对不起,您未被允许参加此会议。",
|
||||
"displayNameRequired": "需要显示名称",
|
||||
"enterDisplayName": "请输入您的显示名称",
|
||||
"extensionRequired": "需要扩展程序",
|
||||
"firefoxExtensionPrompt": "您需要安装Firefox的扩展才能使用屏幕共享功能。请从<a href='__url__'>这里获取后</a>!重试。",
|
||||
"rateExperience": "请评价您的会议体验。",
|
||||
"feedbackHelp": "您的反馈将帮助我们提高我们的视频体验。",
|
||||
"feedbackQuestion": "告诉我们您的联系方式。",
|
||||
"thankYou": "感谢使用__appName__!",
|
||||
"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": "远程控制",
|
||||
"remoteControlDeniedMessage": "__user__ 拒绝了您的远程控制请求",
|
||||
"remoteControlAllowedMessage": "__user__ 接受了您的远程控制请求",
|
||||
"remoteControlErrorMessage": "在尝试向__user__请求远程控制权限时发生了一个错误",
|
||||
"remoteControlStopMessage": "远程控制结束!"
|
||||
},
|
||||
"email": {
|
||||
"sharedKey": "",
|
||||
"subject": "",
|
||||
"body": "",
|
||||
"and": ""
|
||||
"sharedKey": [
|
||||
"该会议受密码保护,请在加入会议时使用下列密码:",
|
||||
"",
|
||||
"",
|
||||
"__sharedKey__",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"subject": "邀请至__appName__ (__conferenceName__)",
|
||||
"body": [
|
||||
"嗨, 我想请你加入刚建立的__appName__这个会议。",
|
||||
"",
|
||||
"",
|
||||
"请点击下面的链接来加入会议。",
|
||||
"",
|
||||
"",
|
||||
"__roomUrl__",
|
||||
"",
|
||||
"",
|
||||
"__sharedKeyText__",
|
||||
" 请注意__appName__现在只支持下列浏览器:__supportedBrowsers__。",
|
||||
"",
|
||||
"",
|
||||
"马上就可以和你交流了!"
|
||||
],
|
||||
"and": "添加"
|
||||
},
|
||||
"connection": {
|
||||
"ERROR": "",
|
||||
"CONNECTING": "",
|
||||
"RECONNECTING": "",
|
||||
"CONNFAIL": "",
|
||||
"AUTHENTICATING": "",
|
||||
"AUTHFAIL": "",
|
||||
"CONNECTED": "",
|
||||
"DISCONNECTED": "",
|
||||
"DISCONNECTING": "",
|
||||
"ATTACHED": ""
|
||||
"ERROR": "错误",
|
||||
"CONNECTING": "连接中",
|
||||
"RECONNECTING": "网络错误,重连中。。。",
|
||||
"CONNFAIL": "连接失败",
|
||||
"AUTHENTICATING": "认证中",
|
||||
"AUTHFAIL": "认证失败",
|
||||
"CONNECTED": "已连接",
|
||||
"DISCONNECTED": "已断开连接",
|
||||
"DISCONNECTING": "断开连接中",
|
||||
"ATTACHED": "已接入"
|
||||
},
|
||||
"recording": {
|
||||
"pending": "",
|
||||
"on": "",
|
||||
"off": "",
|
||||
"failedToStart": "",
|
||||
"buttonTooltip": "",
|
||||
"error": "",
|
||||
"unavailable": ""
|
||||
"pending": "录制中,等待一位与会者加入",
|
||||
"on": "录制中",
|
||||
"off": "录制已停止",
|
||||
"failedToStart": "录制启动失败",
|
||||
"buttonTooltip": "开始 / 结束录制",
|
||||
"error": "录制失败。请重新尝试。",
|
||||
"unavailable": "录制服务目前不可用。请稍后再试。"
|
||||
},
|
||||
"liveStreaming": {
|
||||
"pending": "",
|
||||
"on": "",
|
||||
"off": "",
|
||||
"unavailable": "",
|
||||
"failedToStart": "",
|
||||
"buttonTooltip": "",
|
||||
"streamIdRequired": "",
|
||||
"error": "",
|
||||
"busy": ""
|
||||
"pending": "启动流媒体。。。",
|
||||
"on": "流媒体直播中",
|
||||
"off": "流媒体直播已结束",
|
||||
"unavailable": "流媒体直播服务当前不可用,请稍后再试。",
|
||||
"failedToStart": "流媒体直播启动失败",
|
||||
"buttonTooltip": "启动 / 停止流媒体直播",
|
||||
"streamIdRequired": "请填写媒体流ID来启动流媒体直播。",
|
||||
"streamIdHelp": "在哪里找到这个",
|
||||
"error": "流媒体直播失败。请重试。",
|
||||
"busy": "所有的录制器都在忙。请稍后重试。"
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,15 @@
|
||||
"defaultLink": "e.g. __url__",
|
||||
"callingName": "__name__",
|
||||
"userMedia": {
|
||||
"react-nativeGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>Allow</i></b> button",
|
||||
"chromeGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>Allow</i></b> button",
|
||||
"androidGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>Allow</i></b> button",
|
||||
"firefoxGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>Share Selected Device</i></b> button",
|
||||
"operaGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>Allow</i></b> button",
|
||||
"iexplorerGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>OK</i></b> button",
|
||||
"safariGrantPermissions": "Please grant permissions to use your camera and microphone by pressing <b><i>OK</i></b> button",
|
||||
"nwjsGrantPermissions": "Please grant permissions to use your camera and microphone"
|
||||
"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.",
|
||||
"androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"firefoxGrantPermissions": "Select <b><i>Share Selected Device</i></b> when your browser asks for permissions.",
|
||||
"operaGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"iexplorerGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
|
||||
"safariGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
|
||||
"nwjsGrantPermissions": "Please grant permissions to use your camera and microphone",
|
||||
"edgeGrantPermissions": "Select <b><i>Yes</i></b> when your browser asks for permissions."
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"keyboardShortcuts": "Keyboard shortcuts",
|
||||
@@ -36,7 +37,8 @@
|
||||
"toggleChat": "Open or close the chat",
|
||||
"mute": "Mute or unmute your microphone",
|
||||
"fullScreen": "Enter or exit full screen",
|
||||
"videoMute": "Start or stop your camera"
|
||||
"videoMute": "Start or stop your camera",
|
||||
"showSpeakerStats": "Show speaker stats"
|
||||
},
|
||||
"welcomepage":{
|
||||
"disable": "Don't show this page again",
|
||||
@@ -81,11 +83,12 @@
|
||||
"terms": "Terms"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
"policyText": " ",
|
||||
"title": "__app__ needs to use your microphone and camera."
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
"title": "Your video call was interrupted, because this computer went to sleep.",
|
||||
"text": "Press <i>Rejoin</i> button to connect back to your conversation.",
|
||||
"rejoinKeyTitle": "Rejoin"
|
||||
},
|
||||
"toolbar": {
|
||||
@@ -177,6 +180,7 @@
|
||||
"bitrate": "Bitrate:",
|
||||
"packetloss": "Packet loss:",
|
||||
"resolution": "Resolution:",
|
||||
"framerate": "Frame rate:",
|
||||
"less": "Show less",
|
||||
"more": "Show more",
|
||||
"address": "Address:",
|
||||
@@ -189,8 +193,10 @@
|
||||
"remoteaddress": "Remote address:",
|
||||
"remoteaddress_plural": "Remote addresses:",
|
||||
"transport": "Transport:",
|
||||
"transport_plural": "Transports:",
|
||||
"bandwidth": "Estimated bandwidth:",
|
||||
"na": "Come back here for connection information once the conference starts"
|
||||
"na": "Come back here for connection information once the conference starts",
|
||||
"direct": " (direct)"
|
||||
},
|
||||
"notify": {
|
||||
"disconnected": "disconnected",
|
||||
@@ -201,7 +207,7 @@
|
||||
"focus": "Conference focus",
|
||||
"focusFail": "__component__ not available - retry in __ms__ sec",
|
||||
"grantedTo": "Moderator rights granted to __to__!",
|
||||
"grantedToUnknown": "Moderator rights granted to $t(somebody)!",
|
||||
"grantedToUnknown": "Moderator rights granted to $t(notify.somebody)!",
|
||||
"muted": "You have started the conversation muted.",
|
||||
"mutedTitle": "You're muted!",
|
||||
"raisedHand": "Would like to speak."
|
||||
@@ -225,12 +231,11 @@
|
||||
"detectext": "Error when trying to detect desktopsharing extension.",
|
||||
"failtoinstall": "Failed to install desktop sharing extension",
|
||||
"failedpermissions": "Failed to obtain permissions to use the local microphone and/or camera.",
|
||||
"conferenceReloadTitle": "Unfortunately, something went wrong",
|
||||
"conferenceReloadMsg": "We're trying to fix this",
|
||||
"conferenceDisconnectTitle": "You have been disconnected. You may want to check your network connection.",
|
||||
"conferenceDisconnectMsg": "Reconnecting in...",
|
||||
"reconnectNow": "Reconnect now",
|
||||
"conferenceReloadTimeLeft": "__seconds__ sec.",
|
||||
"conferenceReloadTitle": "Unfortunately, something went wrong.",
|
||||
"conferenceReloadMsg": "We're trying to fix this. Reconnecting in __seconds__ sec...",
|
||||
"conferenceDisconnectTitle": "You have been disconnected.",
|
||||
"conferenceDisconnectMsg": "You may want to check your network connection. Reconnecting in __seconds__ sec...",
|
||||
"rejoinNow": "Rejoin now",
|
||||
"maxUsersLimitReached": "The limit for maximum number of participants in the conference has been reached. The conference is full. Please try again later!",
|
||||
"lockTitle": "Lock failed",
|
||||
"lockMessage": "Failed to lock the conference.",
|
||||
@@ -333,7 +338,11 @@
|
||||
"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__!",
|
||||
"remoteControlStopMessage": "The remote control session ended!"
|
||||
"remoteControlStopMessage": "The remote control session ended!",
|
||||
"close": "Close",
|
||||
"shareYourScreen": "Share your screen",
|
||||
"yourEntireScreen": "Your entire screen",
|
||||
"applicationWindow": "Application window"
|
||||
},
|
||||
"email":
|
||||
{
|
||||
@@ -378,7 +387,9 @@
|
||||
"FETCH_SESSION_ID": "Obtaining session-id...",
|
||||
"GOT_SESSION_ID": "Obtaining session-id... Done",
|
||||
"GET_SESSION_ID_ERROR": "Get session-id error: __code__",
|
||||
"USER_CONNECTION_INTERRUPTED": "__displayName__ is having connectivity issues..."
|
||||
"USER_CONNECTION_INTERRUPTED": "__displayName__ is having connectivity issues...",
|
||||
"LOW_BANDWIDTH": "Video for __displayName__ has been turned off to save bandwidth"
|
||||
|
||||
},
|
||||
"recording":
|
||||
{
|
||||
@@ -402,5 +413,14 @@
|
||||
"streamIdHelp": "Where do I find this?",
|
||||
"error": "Live streaming failed. Please try again.",
|
||||
"busy": "All recorders are currently busy. Please try again later."
|
||||
},
|
||||
"speakerStats":
|
||||
{
|
||||
"hours": "__count__h",
|
||||
"minutes": "__count__m",
|
||||
"name": "Name",
|
||||
"seconds": "__count__s",
|
||||
"speakerStats": "Speaker Stats",
|
||||
"speakerTime": "Speaker Time"
|
||||
}
|
||||
}
|
||||
|
||||
9
modules/API/external/external_api.js
vendored
9
modules/API/external/external_api.js
vendored
@@ -93,10 +93,12 @@ function changeParticipantNumber(APIInstance, number) {
|
||||
* @param interfaceConfigOverwrite object containing configuration options
|
||||
* defined in interface_config.js to be overridden.
|
||||
* @param noSsl if the value is true https won't be used
|
||||
* @param {string} [jwt] the JWT token if needed by jitsi-meet for
|
||||
* authentication.
|
||||
* @constructor
|
||||
*/
|
||||
function JitsiMeetExternalAPI(domain, room_name, width, height, parentNode,
|
||||
configOverwrite, interfaceConfigOverwrite, noSsl) {
|
||||
configOverwrite, interfaceConfigOverwrite, noSsl, jwt) {
|
||||
if (!width || width < MIN_WIDTH)
|
||||
width = MIN_WIDTH;
|
||||
if (!height || height < MIN_HEIGHT)
|
||||
@@ -121,6 +123,11 @@ function JitsiMeetExternalAPI(domain, room_name, width, height, parentNode,
|
||||
this.url = (noSsl) ? "http" : "https" +"://" + domain + "/";
|
||||
if(room_name)
|
||||
this.url += room_name;
|
||||
|
||||
if (jwt) {
|
||||
this.url += '?jwt=' + jwt;
|
||||
}
|
||||
|
||||
this.url += "#jitsi_meet_external_api_id=" + id;
|
||||
|
||||
var key;
|
||||
|
||||
@@ -315,12 +315,6 @@ UI.start = function () {
|
||||
// Initialise the recording module.
|
||||
if (config.enableRecording)
|
||||
Recording.init(eventEmitter, config.recordingType);
|
||||
|
||||
// Display notice message at the top of the toolbar
|
||||
if (config.noticeMessage) {
|
||||
$('#noticeText').text(config.noticeMessage);
|
||||
UIUtil.setVisible('notice', true);
|
||||
}
|
||||
} else {
|
||||
$("body").addClass("filmstrip-only");
|
||||
UIUtil.setVisible('mainToolbarContainer', false);
|
||||
@@ -564,8 +558,7 @@ UI.updateUserRole = user => {
|
||||
} else {
|
||||
messageHandler.notify(
|
||||
'', 'notify.somebody',
|
||||
'connected', 'notify.grantedToUnknown', {}
|
||||
);
|
||||
'connected', 'notify.grantedToUnknown');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -844,8 +837,8 @@ UI.markDominantSpeaker = function (id) {
|
||||
VideoLayout.onDominantSpeakerChanged(id);
|
||||
};
|
||||
|
||||
UI.handleLastNEndpoints = function (ids, enteringIds) {
|
||||
VideoLayout.onLastNEndpointsChanged(ids, enteringIds);
|
||||
UI.handleLastNEndpoints = function (leavingIds, enteringIds) {
|
||||
VideoLayout.onLastNEndpointsChanged(leavingIds, enteringIds);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* global $, APP, config */
|
||||
/* global $, APP, config, JitsiMeetJS */
|
||||
const ConnectionErrors = JitsiMeetJS.errors.connection;
|
||||
|
||||
/**
|
||||
* Build html for "password required" dialog.
|
||||
@@ -127,14 +128,30 @@ function LoginDialog(successCallback, cancelCallback) {
|
||||
/**
|
||||
* Displays error message in 'finished' state which allows either to cancel
|
||||
* or retry.
|
||||
* @param messageKey the key to the message to be displayed.
|
||||
* @param error the key to the error to be displayed.
|
||||
* @param options the options to the error message (optional)
|
||||
*/
|
||||
this.displayError = function (messageKey, options) {
|
||||
this.displayError = function (error, options) {
|
||||
|
||||
let finishedState = connDialog.getState('finished');
|
||||
|
||||
let errorMessageElem = finishedState.find('#errorMessage');
|
||||
|
||||
let messageKey;
|
||||
if (error === ConnectionErrors.PASSWORD_REQUIRED) {
|
||||
// this is a password required error, as login window was already
|
||||
// open once, this means username or password is wrong
|
||||
messageKey = 'dialog.incorrectPassword';
|
||||
}
|
||||
else {
|
||||
messageKey = 'dialog.connectErrorWithMsg';
|
||||
|
||||
if (!options)
|
||||
options = {};
|
||||
|
||||
options.msg = error;
|
||||
}
|
||||
|
||||
errorMessageElem.attr("data-i18n", messageKey);
|
||||
|
||||
APP.translation.translateElement($(errorMessageElem), options);
|
||||
|
||||
@@ -61,6 +61,9 @@ const htmlStr = `
|
||||
function initHTML() {
|
||||
$(`#${sidePanelsContainerId}`)
|
||||
.append(htmlStr);
|
||||
// make sure we translate the panel, as adding it can be after i18n
|
||||
// library had initialized and translated already present html
|
||||
APP.translation.translateElement($(`#${sidePanelsContainerId}`));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,6 +165,9 @@ export default {
|
||||
selectInput[0].dataset.i18n =
|
||||
`languages:${APP.translation.getCurrentLanguage()}`;
|
||||
|
||||
// translate selectInput, which is the currently selected language
|
||||
// otherwise there will be no selected option
|
||||
APP.translation.translateElement(selectInput);
|
||||
APP.translation.translateElement(selectEl);
|
||||
|
||||
APP.translation.addLanguageChangedListener(
|
||||
|
||||
@@ -36,6 +36,7 @@ function ConnectionIndicator(videoContainer, videoId) {
|
||||
this.resolution = null;
|
||||
this.isResolutionHD = null;
|
||||
this.transport = [];
|
||||
this.framerate = null;
|
||||
this.popover = null;
|
||||
this.id = videoId;
|
||||
this.create();
|
||||
@@ -88,12 +89,17 @@ ConnectionIndicator.prototype.generateText = function () {
|
||||
}
|
||||
|
||||
// GENERATE RESOLUTIONS STRING
|
||||
let resolutions = this.resolution || {};
|
||||
let resolutionStr = Object.keys(resolutions).map(function (ssrc) {
|
||||
const resolutions = this.resolution || {};
|
||||
const resolutionStr = Object.keys(resolutions).map(ssrc => {
|
||||
let {width, height} = resolutions[ssrc];
|
||||
return `${width}x${height}`;
|
||||
}).join(', ') || 'N/A';
|
||||
|
||||
const framerates = this.framerate || {};
|
||||
const frameRateStr = Object.keys(framerates).map(ssrc =>
|
||||
framerates[ssrc]
|
||||
).join(', ') || 'N/A';
|
||||
|
||||
let result = (
|
||||
`<table class="connection-info__container" style='width:100%'>
|
||||
<tr>
|
||||
@@ -119,6 +125,14 @@ ConnectionIndicator.prototype.generateText = function () {
|
||||
${resolutionStr}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span data-i18n='connectionindicator.framerate'></span>
|
||||
</td>
|
||||
<td>
|
||||
${frameRateStr}
|
||||
</td>
|
||||
</tr>
|
||||
</table>`);
|
||||
|
||||
if(this.videoContainer.videoSpanId == "localVideoContainer") {
|
||||
@@ -150,7 +164,12 @@ ConnectionIndicator.prototype.generateText = function () {
|
||||
"data-i18n='connectionindicator.address'></span></td>" +
|
||||
"<td> N/A</td></tr>";
|
||||
} else {
|
||||
var data = {remoteIP: [], localIP:[], remotePort:[], localPort:[]};
|
||||
var data = {
|
||||
remoteIP: [],
|
||||
localIP:[],
|
||||
remotePort:[],
|
||||
localPort:[],
|
||||
transportType:[]};
|
||||
for(i = 0; i < this.transport.length; i++) {
|
||||
var ip = ConnectionIndicator.getIP(this.transport[i].ip);
|
||||
var port = ConnectionIndicator.getPort(this.transport[i].ip);
|
||||
@@ -173,8 +192,15 @@ ConnectionIndicator.prototype.generateText = function () {
|
||||
if(data.localPort.indexOf(localPort) == -1) {
|
||||
data.localPort.push(localPort);
|
||||
}
|
||||
|
||||
if(data.transportType.indexOf(this.transport[i].type) == -1) {
|
||||
data.transportType.push(this.transport[i].type);
|
||||
}
|
||||
}
|
||||
|
||||
// All of the transports should be either P2P or JVB
|
||||
const isP2P = this.transport.length ? this.transport[0].p2p : false;
|
||||
|
||||
var local_address_key = "connectionindicator.localaddress";
|
||||
var remote_address_key = "connectionindicator.remoteaddress";
|
||||
var localTransport =
|
||||
@@ -215,9 +241,18 @@ ConnectionIndicator.prototype.generateText = function () {
|
||||
transport += "</td></tr>";
|
||||
transport += localTransport + "</td></tr>";
|
||||
transport +="<tr>" +
|
||||
"<td><span data-i18n='connectionindicator.transport'>" +
|
||||
"</span></td>" +
|
||||
"<td>" + this.transport[0].type + "</td></tr>";
|
||||
"<td><span data-i18n='connectionindicator.transport' "
|
||||
+ " data-i18n-options='" +
|
||||
JSON.stringify({count: data.transportType.length})
|
||||
+ "'></span></td>" +
|
||||
"<td>"
|
||||
+ ConnectionIndicator.getStringFromArray(data.transportType);
|
||||
// Append (direct) to indicate the P2P type of transport
|
||||
if (isP2P) {
|
||||
transport += "<span data-i18n='connectionindicator.direct'>";
|
||||
}
|
||||
// Close "type" column and end table row
|
||||
transport += "</td></tr>";
|
||||
|
||||
}
|
||||
|
||||
@@ -321,17 +356,17 @@ ConnectionIndicator.prototype.remove = function() {
|
||||
* the user is having connectivity issues.
|
||||
*/
|
||||
ConnectionIndicator.prototype.updateConnectionStatusIndicator
|
||||
= function (isActive) {
|
||||
this.isConnectionActive = isActive;
|
||||
if (this.isConnectionActive) {
|
||||
$(this.interruptedIndicator).hide();
|
||||
$(this.emptyIcon).show();
|
||||
$(this.fullIcon).show();
|
||||
} else {
|
||||
$(this.interruptedIndicator).show();
|
||||
$(this.emptyIcon).hide();
|
||||
$(this.fullIcon).hide();
|
||||
}
|
||||
= function (isActive) {
|
||||
this.isConnectionActive = isActive;
|
||||
if (this.isConnectionActive) {
|
||||
$(this.interruptedIndicator).hide();
|
||||
$(this.emptyIcon).show();
|
||||
$(this.fullIcon).show();
|
||||
} else {
|
||||
$(this.interruptedIndicator).show();
|
||||
$(this.emptyIcon).hide();
|
||||
$(this.fullIcon).hide();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -358,6 +393,8 @@ ConnectionIndicator.prototype.updateConnectionQuality =
|
||||
if (object.resolution) {
|
||||
this.resolution = object.resolution;
|
||||
}
|
||||
if (object.framerate)
|
||||
this.framerate = object.framerate;
|
||||
}
|
||||
|
||||
let width = qualityToWidth.find(x => percent >= x.percent);
|
||||
@@ -380,6 +417,15 @@ ConnectionIndicator.prototype.updateResolution = function (resolution) {
|
||||
this.updatePopoverData();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the framerate
|
||||
* @param framerate the new resolution
|
||||
*/
|
||||
ConnectionIndicator.prototype.updateFramerate = function (framerate) {
|
||||
this.framerate = framerate;
|
||||
this.updatePopoverData();
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the content of the popover if its visible
|
||||
* @param force to work even if popover is not visible
|
||||
|
||||
@@ -127,7 +127,9 @@ export default class LargeVideoManager {
|
||||
// the video was not rendered, before the connection has failed.
|
||||
const isHavingConnectivityIssues
|
||||
= APP.conference.isParticipantConnectionActive(id) === false;
|
||||
if (isHavingConnectivityIssues
|
||||
|
||||
if (videoType === VIDEO_CONTAINER_TYPE
|
||||
&& isHavingConnectivityIssues
|
||||
&& (isUserSwitch || !container.wasVideoRendered)) {
|
||||
showAvatar = true;
|
||||
}
|
||||
@@ -155,10 +157,15 @@ 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()
|
||||
|| !isHavingConnectivityIssues;
|
||||
|
||||
this.updateParticipantConnStatusIndication(
|
||||
id,
|
||||
APP.conference.isConnectionInterrupted()
|
||||
|| !isHavingConnectivityIssues);
|
||||
isConnected,
|
||||
(isHavingConnectivityIssues)
|
||||
? "connection.USER_CONNECTION_INTERRUPTED"
|
||||
: "connection.LOW_BANDWIDTH");
|
||||
|
||||
// resolve updateLargeVideo promise after everything is done
|
||||
promise.then(resolve);
|
||||
@@ -180,10 +187,11 @@ export default class LargeVideoManager {
|
||||
* @param {string} id the id of remote participant(MUC nickname)
|
||||
* @param {boolean} isConnected true if the connection is active or false
|
||||
* when the user is having connectivity issues.
|
||||
* @param {string} messageKey the i18n key of the message
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
updateParticipantConnStatusIndication (id, isConnected) {
|
||||
updateParticipantConnStatusIndication (id, isConnected, messageKey) {
|
||||
|
||||
// Apply grey filter on the large video
|
||||
this.videoContainer.showRemoteConnectionProblemIndicator(!isConnected);
|
||||
@@ -196,7 +204,7 @@ export default class LargeVideoManager {
|
||||
let displayName
|
||||
= APP.conference.getParticipantDisplayName(id);
|
||||
this._setRemoteConnectionMessage(
|
||||
"connection.USER_CONNECTION_INTERRUPTED",
|
||||
messageKey,
|
||||
{ displayName: displayName });
|
||||
|
||||
// Show it now only if the VideoContainer is on top
|
||||
@@ -332,7 +340,7 @@ export default class LargeVideoManager {
|
||||
*/
|
||||
showRemoteConnectionMessage (show) {
|
||||
if (typeof show !== 'boolean') {
|
||||
show = APP.conference.isParticipantConnectionActive(this.id);
|
||||
show = !APP.conference.isParticipantConnectionActive(this.id);
|
||||
}
|
||||
|
||||
if (show) {
|
||||
@@ -458,7 +466,7 @@ export default class LargeVideoManager {
|
||||
// "avatar" and "video connection" can not be displayed both
|
||||
// at the same time, but the latter is of higher priority and it
|
||||
// will hide the avatar one if will be displayed.
|
||||
this.showRemoteConnectionMessage(/* fet the current state */);
|
||||
this.showRemoteConnectionMessage(/* fetch the current state */);
|
||||
this.showLocalConnectionMessage(/* fetch the current state */);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -460,12 +460,12 @@ RemoteVideo.prototype.setMutedView = function(isMuted) {
|
||||
* @private
|
||||
*/
|
||||
RemoteVideo.prototype._figureOutMutedWhileDisconnected
|
||||
= function(isDisconnected) {
|
||||
if (isDisconnected && this.isVideoMuted) {
|
||||
this.mutedWhileDisconnected = true;
|
||||
} else if (!isDisconnected && !this.isVideoMuted) {
|
||||
this.mutedWhileDisconnected = false;
|
||||
}
|
||||
= function(isDisconnected) {
|
||||
if (isDisconnected && this.isVideoMuted) {
|
||||
this.mutedWhileDisconnected = true;
|
||||
} else if (!isDisconnected && !this.isVideoMuted) {
|
||||
this.mutedWhileDisconnected = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -554,8 +554,7 @@ RemoteVideo.prototype.isVideoPlayable = function () {
|
||||
*/
|
||||
RemoteVideo.prototype.updateView = function () {
|
||||
|
||||
this.updateConnectionStatusIndicator(
|
||||
null /* will obtain the status from 'conference' */);
|
||||
this.updateConnectionStatusIndicator();
|
||||
|
||||
// This must be called after 'updateConnectionStatusIndicator' because it
|
||||
// affects the display mode by modifying 'mutedWhileDisconnected' flag
|
||||
@@ -564,19 +563,13 @@ RemoteVideo.prototype.updateView = function () {
|
||||
|
||||
/**
|
||||
* Updates the UI to reflect user's connectivity status.
|
||||
* @param isActive {boolean|null} 'true' if user's connection is active or
|
||||
* 'false' when the use is having some connectivity issues and a warning
|
||||
* should be displayed. When 'null' is passed then the current value will be
|
||||
* obtained from the conference instance.
|
||||
*/
|
||||
RemoteVideo.prototype.updateConnectionStatusIndicator = function (isActive) {
|
||||
// Check for initial value if 'isActive' is not defined
|
||||
if (typeof isActive !== "boolean") {
|
||||
isActive = this.isConnectionActive();
|
||||
if (isActive === null) {
|
||||
// Cancel processing at this point - no update
|
||||
return;
|
||||
}
|
||||
RemoteVideo.prototype.updateConnectionStatusIndicator = function () {
|
||||
const isActive = this.isConnectionActive();
|
||||
|
||||
if (isActive === null) {
|
||||
// Cancel processing at this point - no update
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(this.id + " thumbnail is connection active ? " + isActive);
|
||||
@@ -700,50 +693,22 @@ RemoteVideo.prototype.addRemoteStreamElement = function (stream) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/hide peer container for the given id.
|
||||
*/
|
||||
RemoteVideo.prototype.showPeerContainer = function (state) {
|
||||
if (!this.container)
|
||||
return;
|
||||
|
||||
var isHide = state === 'hide';
|
||||
var resizeThumbnails = false;
|
||||
|
||||
if (!isHide) {
|
||||
if (!$(this.container).is(':visible')) {
|
||||
resizeThumbnails = true;
|
||||
$(this.container).show();
|
||||
}
|
||||
// Call updateView, so that we'll figure out if avatar
|
||||
// should be displayed based on video muted status and whether or not
|
||||
// it's in the lastN set
|
||||
this.updateView();
|
||||
}
|
||||
else if ($(this.container).is(':visible') && isHide)
|
||||
{
|
||||
resizeThumbnails = true;
|
||||
$(this.container).hide();
|
||||
if(this.connectionIndicator)
|
||||
this.connectionIndicator.hide();
|
||||
}
|
||||
|
||||
if (resizeThumbnails) {
|
||||
this.VideoLayout.resizeThumbnails();
|
||||
}
|
||||
|
||||
// We want to be able to pin a participant from the contact list, even
|
||||
// if he's not in the lastN set!
|
||||
// ContactList.setClickable(id, !isHide);
|
||||
|
||||
};
|
||||
|
||||
RemoteVideo.prototype.updateResolution = function (resolution) {
|
||||
if (this.connectionIndicator) {
|
||||
this.connectionIndicator.updateResolution(resolution);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates this video framerate indication.
|
||||
* @param framerate the value to update
|
||||
*/
|
||||
RemoteVideo.prototype.updateFramerate = function (framerate) {
|
||||
if (this.connectionIndicator) {
|
||||
this.connectionIndicator.updateFramerate(framerate);
|
||||
}
|
||||
};
|
||||
|
||||
RemoteVideo.prototype.removeConnectionIndicator = function () {
|
||||
if (this.connectionIndicator)
|
||||
this.connectionIndicator.remove();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global $, JitsiMeetJS, interfaceConfig */
|
||||
/* global $, APP, JitsiMeetJS, interfaceConfig */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import Avatar from "../avatar/Avatar";
|
||||
@@ -446,7 +446,7 @@ SmallVideo.prototype.isCurrentlyOnLargeVideo = function () {
|
||||
SmallVideo.prototype.isVideoPlayable = function() {
|
||||
return this.videoStream // Is there anything to display ?
|
||||
&& !this.isVideoMuted && !this.videoStream.isMuted() // Muted ?
|
||||
&& (this.isLocal || this.VideoLayout.isInLastN(this.id));
|
||||
&& (this.isLocal || APP.conference.isInLastN(this.id));
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global config, APP, $, interfaceConfig */
|
||||
/* global APP, $, interfaceConfig */
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import FilmStrip from "./FilmStrip";
|
||||
@@ -14,10 +14,6 @@ var remoteVideos = {};
|
||||
var localVideoThumbnail = null;
|
||||
|
||||
var currentDominantSpeaker = null;
|
||||
var localLastNCount = config.channelLastN;
|
||||
var localLastNSet = [];
|
||||
var lastNEndpointsCache = [];
|
||||
var lastNPickupId = null;
|
||||
|
||||
var eventEmitter = null;
|
||||
|
||||
@@ -60,8 +56,6 @@ function onContactClicked (id) {
|
||||
// let the bridge adjust its lastN set for myjid and store
|
||||
// the pinned user in the lastNPickupId variable to be
|
||||
// picked up later by the lastN changed event handler.
|
||||
|
||||
lastNPickupId = id;
|
||||
eventEmitter.emit(UIEvents.PINNED_ENDPOINT, remoteVideo, true);
|
||||
}
|
||||
}
|
||||
@@ -114,8 +108,6 @@ var VideoLayout = {
|
||||
// the local video thumb maybe one pixel
|
||||
this.resizeThumbnails(false, true);
|
||||
|
||||
this.lastNCount = config.channelLastN;
|
||||
|
||||
this.registerListeners();
|
||||
},
|
||||
|
||||
@@ -162,14 +154,6 @@ var VideoLayout = {
|
||||
largeVideo.updateLargeVideoAudioLevel(lvl);
|
||||
},
|
||||
|
||||
isInLastN (resource) {
|
||||
return this.lastNCount < 0 || // lastN is disabled
|
||||
// lastNEndpoints cache not built yet
|
||||
(this.lastNCount > 0 && !lastNEndpointsCache.length) ||
|
||||
(lastNEndpointsCache &&
|
||||
lastNEndpointsCache.indexOf(resource) !== -1);
|
||||
},
|
||||
|
||||
changeLocalAudio (stream) {
|
||||
let localAudio = document.getElementById('localAudio');
|
||||
localAudio = stream.attach(localAudio);
|
||||
@@ -457,13 +441,8 @@ var VideoLayout = {
|
||||
remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
|
||||
}
|
||||
|
||||
// In case this is not currently in the last n we don't show it.
|
||||
if (localLastNCount && localLastNCount > 0 &&
|
||||
FilmStrip.getThumbs().remoteThumbs.length >= localLastNCount + 2) {
|
||||
remoteVideo.showPeerContainer('hide');
|
||||
} else {
|
||||
VideoLayout.resizeThumbnails(false, true);
|
||||
}
|
||||
VideoLayout.resizeThumbnails(false, true);
|
||||
|
||||
// Initialize the view
|
||||
remoteVideo.updateView();
|
||||
},
|
||||
@@ -708,145 +687,32 @@ var VideoLayout = {
|
||||
/**
|
||||
* On last N change event.
|
||||
*
|
||||
* @param lastNEndpoints the list of last N endpoints
|
||||
* @param endpointsLeavingLastN the list currently leaving last N
|
||||
* endpoints
|
||||
* @param endpointsEnteringLastN the list currently entering last N
|
||||
* endpoints
|
||||
*/
|
||||
onLastNEndpointsChanged (lastNEndpoints, endpointsEnteringLastN) {
|
||||
if (this.lastNCount !== lastNEndpoints.length)
|
||||
this.lastNCount = lastNEndpoints.length;
|
||||
|
||||
lastNEndpointsCache = lastNEndpoints;
|
||||
|
||||
// Say A, B, C, D, E, and F are in a conference and LastN = 3.
|
||||
//
|
||||
// If LastN drops to, say, 2, because of adaptivity, then E should see
|
||||
// thumbnails for A, B and C. A and B are in E's server side LastN set,
|
||||
// so E sees them. C is only in E's local LastN set.
|
||||
//
|
||||
// If F starts talking and LastN = 3, then E should see thumbnails for
|
||||
// F, A, B. B gets "ejected" from E's server side LastN set, but it
|
||||
// enters E's local LastN ejecting C.
|
||||
|
||||
// Increase the local LastN set size, if necessary.
|
||||
if (this.lastNCount > localLastNCount) {
|
||||
localLastNCount = this.lastNCount;
|
||||
onLastNEndpointsChanged (endpointsLeavingLastN, endpointsEnteringLastN) {
|
||||
if (endpointsLeavingLastN) {
|
||||
endpointsLeavingLastN.forEach(this._updateRemoteVideo, this);
|
||||
}
|
||||
|
||||
// Update the local LastN set preserving the order in which the
|
||||
// endpoints appeared in the LastN/local LastN set.
|
||||
var nextLocalLastNSet = lastNEndpoints.slice(0);
|
||||
for (var i = 0; i < localLastNSet.length; i++) {
|
||||
if (nextLocalLastNSet.length >= localLastNCount) {
|
||||
break;
|
||||
}
|
||||
|
||||
var resourceJid = localLastNSet[i];
|
||||
if (nextLocalLastNSet.indexOf(resourceJid) === -1) {
|
||||
nextLocalLastNSet.push(resourceJid);
|
||||
}
|
||||
if (endpointsEnteringLastN) {
|
||||
endpointsEnteringLastN.forEach(this._updateRemoteVideo, this);
|
||||
}
|
||||
},
|
||||
|
||||
localLastNSet = nextLocalLastNSet;
|
||||
var updateLargeVideo = false;
|
||||
|
||||
// Handle LastN/local LastN changes.
|
||||
FilmStrip.getThumbs().remoteThumbs.each(( index, element ) => {
|
||||
var resourceJid = getPeerContainerResourceId(element);
|
||||
var smallVideo = remoteVideos[resourceJid];
|
||||
|
||||
// We do not want to process any logic for our own(local) video
|
||||
// because the local participant is never in the lastN set.
|
||||
// The code of this function might detect that the local participant
|
||||
// has been dropped out of the lastN set and will update the large
|
||||
// video
|
||||
// Detected from avatar tests, where lastN event override
|
||||
// local video pinning
|
||||
if(APP.conference.isLocalId(resourceJid))
|
||||
return;
|
||||
|
||||
var isReceived = true;
|
||||
if (resourceJid &&
|
||||
lastNEndpoints.indexOf(resourceJid) < 0 &&
|
||||
localLastNSet.indexOf(resourceJid) < 0) {
|
||||
logger.log("Remove from last N", resourceJid);
|
||||
if (smallVideo)
|
||||
smallVideo.showPeerContainer('hide');
|
||||
else if (!APP.conference.isLocalId(resourceJid))
|
||||
logger.error("No remote video for: " + resourceJid);
|
||||
isReceived = false;
|
||||
} else if (resourceJid &&
|
||||
//TOFIX: smallVideo may be undefined
|
||||
smallVideo.isVisible() &&
|
||||
lastNEndpoints.indexOf(resourceJid) < 0 &&
|
||||
localLastNSet.indexOf(resourceJid) >= 0) {
|
||||
|
||||
// TOFIX: if we're here we already know that the smallVideo
|
||||
// exists. Look at the previous FIX above.
|
||||
if (smallVideo)
|
||||
smallVideo.showPeerContainer('avatar');
|
||||
else if (!APP.conference.isLocalId(resourceJid))
|
||||
logger.error("No remote video for: " + resourceJid);
|
||||
isReceived = false;
|
||||
}
|
||||
|
||||
if (!isReceived) {
|
||||
// resourceJid has dropped out of the server side lastN set, so
|
||||
// it is no longer being received. If resourceJid was being
|
||||
// displayed in the large video we have to switch to another
|
||||
// user.
|
||||
if (!updateLargeVideo &&
|
||||
this.isCurrentlyOnLarge(resourceJid)) {
|
||||
updateLargeVideo = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0)
|
||||
endpointsEnteringLastN = lastNEndpoints;
|
||||
|
||||
if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) {
|
||||
endpointsEnteringLastN.forEach(function (resourceJid) {
|
||||
|
||||
var remoteVideo = remoteVideos[resourceJid];
|
||||
if (remoteVideo)
|
||||
remoteVideo.showPeerContainer('show');
|
||||
|
||||
if (!remoteVideo.isVisible()) {
|
||||
logger.log("Add to last N", resourceJid);
|
||||
|
||||
remoteVideo.addRemoteStreamElement(remoteVideo.videoStream);
|
||||
|
||||
if (lastNPickupId == resourceJid) {
|
||||
// Clean up the lastN pickup id.
|
||||
lastNPickupId = null;
|
||||
|
||||
VideoLayout.handleVideoThumbClicked(resourceJid);
|
||||
|
||||
updateLargeVideo = false;
|
||||
}
|
||||
remoteVideo.waitForPlayback(
|
||||
remoteVideo.selectVideoElement()[0],
|
||||
remoteVideo.videoStream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// The endpoint that was being shown in the large video has dropped out
|
||||
// of the lastN set and there was no lastN pickup jid. We need to update
|
||||
// the large video now.
|
||||
|
||||
if (updateLargeVideo) {
|
||||
var resource;
|
||||
// Find out which endpoint to show in the large video.
|
||||
for (i = 0; i < lastNEndpoints.length; i++) {
|
||||
resource = lastNEndpoints[i];
|
||||
if (!resource || APP.conference.isLocalId(resource))
|
||||
continue;
|
||||
|
||||
// videoSrcToSsrc needs to be update for this call to succeed.
|
||||
this.updateLargeVideo(resource);
|
||||
break;
|
||||
/**
|
||||
* Updates remote video by id if it exists.
|
||||
* @param {string} id of the remote video
|
||||
* @private
|
||||
*/
|
||||
_updateRemoteVideo(id) {
|
||||
const remoteVideo = remoteVideos[id];
|
||||
if (remoteVideo) {
|
||||
remoteVideo.updateView();
|
||||
if (remoteVideo.isCurrentlyOnLargeVideo()) {
|
||||
this.updateLargeVideo(id);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -857,21 +723,37 @@ var VideoLayout = {
|
||||
* @param object
|
||||
*/
|
||||
updateLocalConnectionStats (percent, object) {
|
||||
let resolutions = object.resolution;
|
||||
const { framerate, resolution } = object;
|
||||
|
||||
object.resolution = resolutions[APP.conference.getMyUserId()];
|
||||
// FIXME overwrites 'lib-jitsi-meet' internal object
|
||||
// Why library internal objects are passed as event's args ?
|
||||
object.resolution = resolution[APP.conference.getMyUserId()];
|
||||
object.framerate = framerate[APP.conference.getMyUserId()];
|
||||
localVideoThumbnail.updateStatsIndicator(percent, object);
|
||||
|
||||
Object.keys(resolutions).forEach(function (id) {
|
||||
Object.keys(resolution).forEach(function (id) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let resolution = resolutions[id];
|
||||
let resolutionValue = resolution[id];
|
||||
let remoteVideo = remoteVideos[id];
|
||||
|
||||
if (resolution && remoteVideo) {
|
||||
remoteVideo.updateResolution(resolution);
|
||||
if (resolutionValue && remoteVideo) {
|
||||
remoteVideo.updateResolution(resolutionValue);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(framerate).forEach(function (id) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const framerateValue = framerate[id];
|
||||
const remoteVideo = remoteVideos[id];
|
||||
|
||||
if (framerateValue && remoteVideo) {
|
||||
remoteVideo.updateFramerate(framerateValue);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const logger = require("jitsi-meet-logger").getLogger(__filename);
|
||||
|
||||
import { replace } from '../util/helpers';
|
||||
import { reload, replace } from '../util/helpers';
|
||||
|
||||
/**
|
||||
* The modules stores information about the URL used to start the conference and
|
||||
@@ -67,7 +67,14 @@ export default class ConferenceUrl {
|
||||
* Reloads the conference using original URL with all of the parameters.
|
||||
*/
|
||||
reload() {
|
||||
logger.info("Reloading the conference using URL: " + this.originalURL);
|
||||
replace(this.originalURL);
|
||||
logger.info(`Reloading the conference using URL: ${this.originalURL}`);
|
||||
|
||||
// Check if we are in an iframe and reload with the reload() utility
|
||||
// because replace() is not working on an iframe.
|
||||
if(window.self !== window.top) {
|
||||
reload();
|
||||
} else {
|
||||
replace(this.originalURL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/* global APP, $, JitsiMeetJS */
|
||||
|
||||
import {
|
||||
toggleDialog
|
||||
} from '../../react/features/base/dialog';
|
||||
import { SpeakerStats } from '../../react/features/speaker-stats';
|
||||
|
||||
/**
|
||||
* The reference to the shortcut dialogs when opened.
|
||||
*/
|
||||
@@ -29,6 +34,13 @@ 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");
|
||||
|
||||
/**
|
||||
* FIXME: Currently focus keys are directly implemented below in onkeyup.
|
||||
* They should be moved to the SmallVideo instead.
|
||||
|
||||
46
package.json
46
package.json
@@ -17,18 +17,22 @@
|
||||
"//": "Callstats.io does not work with recent versions of jsSHA (2.0.1 in particular)",
|
||||
"dependencies": {
|
||||
"@atlassian/aui": "6.0.6",
|
||||
"@atlaskit/button": "1.0.3",
|
||||
"@atlaskit/button-group": "1.0.0",
|
||||
"@atlaskit/modal-dialog": "1.2.4",
|
||||
"@atlaskit/tabs": "1.2.5",
|
||||
"async": "0.9.0",
|
||||
"autosize": "1.18.13",
|
||||
"bootstrap": "3.1.1",
|
||||
"es6-iterator": "2.0.0",
|
||||
"es6-symbol": "3.1.0",
|
||||
"i18next": "7.0.0",
|
||||
"es6-iterator": "2.0.1",
|
||||
"es6-symbol": "3.1.1",
|
||||
"i18next": "7.1.3",
|
||||
"i18next-browser-languagedetector": "1.0.1",
|
||||
"i18next-xhr-backend": "1.3.0",
|
||||
"i18next-xhr-backend": "1.4.1",
|
||||
"jitsi-meet-logger": "jitsi/jitsi-meet-logger",
|
||||
"jquery": "2.1.4",
|
||||
"jquery-contextmenu": "2.4.3",
|
||||
"jquery-i18next": "1.1.0",
|
||||
"jquery-i18next": "1.2.0",
|
||||
"jQuery-Impromptu": "trentrichardson/jQuery-Impromptu#v6.0.0",
|
||||
"jquery-ui": "1.10.5",
|
||||
"jssha": "1.5.0",
|
||||
@@ -37,11 +41,11 @@
|
||||
"postis": "2.2.0",
|
||||
"react": "15.4.2",
|
||||
"react-dom": "15.4.2",
|
||||
"react-i18next": "2.2.0",
|
||||
"react-native": "0.42.0",
|
||||
"react-i18next": "2.2.3",
|
||||
"react-native": "0.42.3",
|
||||
"react-native-background-timer": "1.0.0",
|
||||
"react-native-immersive": "0.0.4",
|
||||
"react-native-keep-awake": "2.0.2",
|
||||
"react-native-keep-awake": "2.0.3",
|
||||
"react-native-locale-detector": "1.0.1",
|
||||
"react-native-prompt": "1.0.0",
|
||||
"react-native-vector-icons": "4.0.0",
|
||||
@@ -57,22 +61,22 @@
|
||||
"xmldom": "0.1.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "6.23.1",
|
||||
"babel-eslint": "7.1.1",
|
||||
"babel-loader": "6.3.2",
|
||||
"babel-core": "6.24.0",
|
||||
"babel-eslint": "7.2.1",
|
||||
"babel-loader": "6.4.1",
|
||||
"babel-polyfill": "6.23.0",
|
||||
"babel-preset-es2015": "6.22.0",
|
||||
"babel-preset-es2015": "6.24.0",
|
||||
"babel-preset-react": "6.23.0",
|
||||
"babel-preset-stage-1": "6.22.0",
|
||||
"clean-css": "3.4.25",
|
||||
"css-loader": "0.26.2",
|
||||
"eslint": "3.16.1",
|
||||
"eslint-plugin-flowtype": "2.30.0",
|
||||
"css-loader": "0.28.0",
|
||||
"eslint": "3.18.0",
|
||||
"eslint-plugin-flowtype": "2.30.4",
|
||||
"eslint-plugin-import": "2.2.0",
|
||||
"eslint-plugin-jsdoc": "2.4.0",
|
||||
"eslint-plugin-react": "6.10.0",
|
||||
"eslint-plugin-react-native": "2.2.1",
|
||||
"expose-loader": "0.7.1",
|
||||
"eslint-plugin-jsdoc": "3.0.0",
|
||||
"eslint-plugin-react": "6.10.3",
|
||||
"eslint-plugin-react-native": "2.3.1",
|
||||
"expose-loader": "0.7.3",
|
||||
"file-loader": "0.10.1",
|
||||
"flow-bin": "0.38.0",
|
||||
"haste-resolver-webpack-plugin": "0.2.2",
|
||||
@@ -81,8 +85,8 @@
|
||||
"json-loader": "0.5.4",
|
||||
"node-sass": "3.13.1",
|
||||
"precommit-hook": "3.0.0",
|
||||
"string-replace-loader": "1.0.5",
|
||||
"style-loader": "0.13.2",
|
||||
"string-replace-loader": "1.1.0",
|
||||
"style-loader": "0.16.1",
|
||||
"webpack": "1.14.0",
|
||||
"webpack-dev-server": "1.16.3"
|
||||
},
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
import { Linking } from 'react-native';
|
||||
|
||||
import '../../audio-mode';
|
||||
import '../../background';
|
||||
import { Platform } from '../../base/react';
|
||||
import '../../full-screen';
|
||||
import '../../wake-lock';
|
||||
import '../../mobile/audio-mode';
|
||||
import '../../mobile/background';
|
||||
import '../../mobile/full-screen';
|
||||
import '../../mobile/wake-lock';
|
||||
|
||||
import { AbstractApp } from './AbstractApp';
|
||||
|
||||
@@ -47,8 +47,8 @@ export class App extends AbstractApp {
|
||||
* by this app.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @see https://facebook.github.io/react-native/docs/linking.html
|
||||
* @returns {void}
|
||||
* @see https://facebook.github.io/react-native/docs/linking.html
|
||||
*/
|
||||
componentWillMount() {
|
||||
super.componentWillMount();
|
||||
@@ -61,8 +61,8 @@ export class App extends AbstractApp {
|
||||
* handled by this app.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @see https://facebook.github.io/react-native/docs/linking.html
|
||||
* @returns {void}
|
||||
* @see https://facebook.github.io/react-native/docs/linking.html
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
Linking.removeEventListener('url', this._onLinkingURL);
|
||||
|
||||
22
react/features/base/dialog/actionTypes.js
Normal file
22
react/features/base/dialog/actionTypes.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Symbol } from '../react';
|
||||
|
||||
/**
|
||||
* The type of Redux action which closes a dialog
|
||||
*
|
||||
* {
|
||||
* type: HIDE_DIALOG
|
||||
* }
|
||||
*/
|
||||
export const HIDE_DIALOG = Symbol('HIDE_DIALOG');
|
||||
|
||||
/**
|
||||
* The type of Redux action which begins a request to open a dialog.
|
||||
*
|
||||
* {
|
||||
* type: OPEN_DIALOG,
|
||||
* component: React.Component,
|
||||
* props: React.PropTypes.object
|
||||
* }
|
||||
*
|
||||
*/
|
||||
export const OPEN_DIALOG = Symbol('OPEN_DIALOG');
|
||||
54
react/features/base/dialog/actions.js
Normal file
54
react/features/base/dialog/actions.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
HIDE_DIALOG,
|
||||
OPEN_DIALOG
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Signals Dialog to close its dialog.
|
||||
*
|
||||
* @returns {{
|
||||
* type: HIDE_DIALOG
|
||||
* }}
|
||||
*/
|
||||
export function hideDialog() {
|
||||
return {
|
||||
type: HIDE_DIALOG
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals Dialog to open dialog.
|
||||
*
|
||||
* @param {Object} component - The component to display as dialog.
|
||||
* @param {Object} componentProps - The properties needed for that component.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function openDialog(component, componentProps) {
|
||||
return {
|
||||
type: OPEN_DIALOG,
|
||||
component,
|
||||
componentProps
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals Dialog to open a dialog with the specified component if the component
|
||||
* is not already open. If it is open, then Dialog is signaled to close
|
||||
* its dialog.
|
||||
*
|
||||
* @param {Object} component - The component to display as dialog.
|
||||
* @param {Object} componentProps - The properties needed for that component.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function toggleDialog(component, componentProps) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const dialogState = state['features/base/dialog'];
|
||||
|
||||
if (dialogState.component === component) {
|
||||
dispatch(hideDialog());
|
||||
} else {
|
||||
dispatch(openDialog(component, componentProps));
|
||||
}
|
||||
};
|
||||
}
|
||||
110
react/features/base/dialog/components/AbstractDialog.js
Normal file
110
react/features/base/dialog/components/AbstractDialog.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { hideDialog } from '../actions';
|
||||
|
||||
/**
|
||||
* Abstract dialog to display dialogs.
|
||||
*/
|
||||
export default class AbstractDialog extends Component {
|
||||
|
||||
/**
|
||||
* Abstract Dialog component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* Whether cancel button is disabled. Enabled by default.
|
||||
*/
|
||||
cancelDisabled: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Optional i18n key to change the cancel button title.
|
||||
*/
|
||||
cancelTitleKey: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* Used to show hide the dialog on cancel.
|
||||
*/
|
||||
dispatch: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Is ok button enabled/disabled. Enabled by default.
|
||||
*/
|
||||
okDisabled: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Optional i18n key to change the ok button title.
|
||||
*/
|
||||
okTitleKey: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* The handler for onCancel event.
|
||||
*/
|
||||
onCancel: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* The handler for the event when submitting the dialog.
|
||||
*/
|
||||
onSubmit: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Used to obtain translations in children classes.
|
||||
*/
|
||||
t: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Key to use for showing a title.
|
||||
*/
|
||||
titleKey: React.PropTypes.string
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new Dialog instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches action to hide the dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
let hide = true;
|
||||
|
||||
if (this.props.onCancel) {
|
||||
hide = this.props.onCancel();
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
this.props.dispatch(hideDialog());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches the action when submitting the dialog.
|
||||
*
|
||||
* @private
|
||||
* @param {string} value - The submitted value if any.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmit(value) {
|
||||
let hide = true;
|
||||
|
||||
if (this.props.onSubmit) {
|
||||
hide = this.props.onSubmit(value);
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
this.props.dispatch(hideDialog());
|
||||
}
|
||||
}
|
||||
}
|
||||
58
react/features/base/dialog/components/Dialog.native.js
Normal file
58
react/features/base/dialog/components/Dialog.native.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Prompt from 'react-native-prompt';
|
||||
|
||||
import { translate } from '../../i18n';
|
||||
|
||||
import AbstractDialog from './AbstractDialog';
|
||||
|
||||
/**
|
||||
* Native dialog using Prompt.
|
||||
*/
|
||||
class Dialog extends AbstractDialog {
|
||||
|
||||
/**
|
||||
* Native sialog component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* I18n key to put as body title.
|
||||
*/
|
||||
bodyKey: React.PropTypes.string
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
cancelDisabled,
|
||||
cancelTitleKey,
|
||||
bodyKey,
|
||||
okDisabled,
|
||||
okTitleKey,
|
||||
t,
|
||||
titleKey
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Prompt
|
||||
cancelText = { cancelDisabled
|
||||
? undefined : t(cancelTitleKey || 'dialog.Cancel') }
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
placeholder = { t(bodyKey) }
|
||||
submitText = { okDisabled
|
||||
? undefined : t(okTitleKey || 'dialog.Ok') }
|
||||
title = { t(titleKey) }
|
||||
visible = { true } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(Dialog));
|
||||
159
react/features/base/dialog/components/Dialog.web.js
Normal file
159
react/features/base/dialog/components/Dialog.web.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import AKButton from '@atlaskit/button';
|
||||
import AKButtonGroup from '@atlaskit/button-group';
|
||||
import ModalDialog from '@atlaskit/modal-dialog';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../i18n';
|
||||
|
||||
import AbstractDialog from './AbstractDialog';
|
||||
|
||||
/**
|
||||
* Web dialog that uses atlaskit modal-dialog to display dialogs.
|
||||
*/
|
||||
class Dialog extends AbstractDialog {
|
||||
|
||||
/**
|
||||
* Web dialog component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* This is the body of the dialog, the component children.
|
||||
*/
|
||||
children: React.PropTypes.node,
|
||||
|
||||
/**
|
||||
* Whether the dialog is modal. This means clicking on the blanket will
|
||||
* leave the dialog open. No cancel button.
|
||||
*/
|
||||
isModal: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Width of the dialog, can be:
|
||||
* - 'small' (400px), 'medium' (600px), 'large' (800px),
|
||||
* 'x-large' (968px)
|
||||
* - integer value for pixel width
|
||||
* - string value for percentage
|
||||
*/
|
||||
width: React.PropTypes.string
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<ModalDialog
|
||||
footer = { this._renderFooter() }
|
||||
header = { this._renderHeader() }
|
||||
isOpen = { true }
|
||||
onDialogDismissed = { this._onCancel }
|
||||
width = { this.props.width || 'medium' }>
|
||||
<div>
|
||||
<form
|
||||
id = 'modal-dialog-form'
|
||||
onSubmit = { this._onSubmit }>
|
||||
{ this.props.children }
|
||||
</form>
|
||||
</div>
|
||||
</ModalDialog>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render cancel button.
|
||||
*
|
||||
* @returns {*} The cancel button if enabled and dialog is not modal.
|
||||
* @private
|
||||
*/
|
||||
_renderCancelButton() {
|
||||
if (this.props.cancelDisabled || this.props.isModal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AKButton
|
||||
appearance = 'subtle'
|
||||
id = 'modal-dialog-cancel-button'
|
||||
onClick = { this._onCancel }>
|
||||
{ this.props.t(this.props.cancelTitleKey || 'dialog.Cancel') }
|
||||
</AKButton>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render component in dialog footer.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderFooter() {
|
||||
return (
|
||||
<footer>
|
||||
<AKButtonGroup>
|
||||
{ this._renderCancelButton() }
|
||||
{ this._renderOKButton() }
|
||||
</AKButtonGroup>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render component in dialog header.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderHeader() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<header>
|
||||
<h2>
|
||||
{ t(this.props.titleKey) }
|
||||
</h2>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render ok button.
|
||||
*
|
||||
* @returns {*} The ok button if enabled.
|
||||
* @private
|
||||
*/
|
||||
_renderOKButton() {
|
||||
if (this.props.submitDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AKButton
|
||||
appearance = 'primary'
|
||||
form = 'modal-dialog-form'
|
||||
id = 'modal-dialog-ok-button'
|
||||
onClick = { this._onSubmit }>
|
||||
{ this.props.t(this.props.okTitleKey || 'dialog.Ok') }
|
||||
</AKButton>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches action to hide the dialog.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
if (this.props.isModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
super._onCancel();
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect()(Dialog));
|
||||
61
react/features/base/dialog/components/DialogContainer.js
Normal file
61
react/features/base/dialog/components/DialogContainer.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
/**
|
||||
* Implements a DialogContainer that will be responsible for
|
||||
* showing all dialogs. We will need a separate container so we can handle
|
||||
* multiple dialogs, showing them simultaneously or queueing them.
|
||||
*/
|
||||
export class DialogContainer extends Component {
|
||||
|
||||
/**
|
||||
* DialogContainer component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The component to render.
|
||||
*/
|
||||
_component: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* The props to pass to the component that will be rendered.
|
||||
*/
|
||||
_componentProps: React.PropTypes.object
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
if (!this.props._component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
this.props._component, this.props._componentProps);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated Dialog's props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _component: React.Component,
|
||||
* _props: React.PropTypes.object
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_component: state['features/base/dialog'].component,
|
||||
_componentProps: state['features/base/dialog'].componentProps
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(DialogContainer);
|
||||
2
react/features/base/dialog/components/index.js
Normal file
2
react/features/base/dialog/components/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as DialogContainer } from './DialogContainer';
|
||||
export { default as Dialog } from './Dialog';
|
||||
4
react/features/base/dialog/index.js
Normal file
4
react/features/base/dialog/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './actions';
|
||||
export * from './components';
|
||||
|
||||
import './reducer';
|
||||
31
react/features/base/dialog/reducer.js
Normal file
31
react/features/base/dialog/reducer.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ReducerRegistry, setStateProperties } from '../redux';
|
||||
|
||||
import {
|
||||
HIDE_DIALOG,
|
||||
OPEN_DIALOG
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Listen for actions which show or hide dialogs.
|
||||
*
|
||||
* @param {Object[]} state - Current state.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @returns {{}}
|
||||
*/
|
||||
ReducerRegistry.register('features/base/dialog', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case HIDE_DIALOG:
|
||||
return setStateProperties(state, {
|
||||
component: undefined,
|
||||
componentProps: undefined
|
||||
});
|
||||
case OPEN_DIALOG:
|
||||
return setStateProperties(state, {
|
||||
component: action.component,
|
||||
componentProps: action.componentProps
|
||||
});
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
@@ -65,9 +65,9 @@ export class Video extends Component {
|
||||
componentDidMount() {
|
||||
// RTCView currently does not support media events, so just fire
|
||||
// onPlaying callback when <RTCView> is rendered.
|
||||
if (this.props.onPlaying) {
|
||||
this.props.onPlaying();
|
||||
}
|
||||
const { onPlaying } = this.props;
|
||||
|
||||
onPlaying && onPlaying();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +77,7 @@ export class Video extends Component {
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
render() {
|
||||
const stream = this.props.stream;
|
||||
const { stream } = this.props;
|
||||
|
||||
if (stream) {
|
||||
const streamURL = stream.toURL();
|
||||
@@ -91,18 +91,18 @@ export class Video extends Component {
|
||||
const style = styles.video;
|
||||
const objectFit = (style && style.objectFit) || 'cover';
|
||||
|
||||
const mirror = this.props.mirror;
|
||||
const { mirror } = this.props;
|
||||
|
||||
// XXX RTCView doesn't currently support mirroring, even when
|
||||
// providing a transform style property. As a workaround, wrap the
|
||||
// RTCView inside another View and apply the transform style
|
||||
// property to that View instead.
|
||||
// XXX RTCView may not support support mirroring, even when
|
||||
// providing a transform style property (e.g. iOS) . As a
|
||||
// workaround, wrap the RTCView inside another View and apply the
|
||||
// transform style property to that View instead.
|
||||
const mirrorWorkaround = mirror && !RTCVIEW_SUPPORTS_MIRROR;
|
||||
|
||||
// eslint-disable-next-line no-extra-parens
|
||||
const video = (
|
||||
<RTCView
|
||||
mirror = { !mirrorWorkaround }
|
||||
mirror = { mirrorWorkaround ? false : mirror }
|
||||
objectFit = { objectFit }
|
||||
streamURL = { streamURL }
|
||||
style = { style }
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './_';
|
||||
export * from './styles';
|
||||
|
||||
29
react/features/base/util/timeUtils.js
Normal file
29
react/features/base/util/timeUtils.js
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Counts how many whole hours are included in the given time total.
|
||||
*
|
||||
* @param {number} milliseconds - The millisecond total to get hours from.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getHoursCount(milliseconds) {
|
||||
return Math.floor(milliseconds / (60 * 60 * 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts how many whole minutes are included in the given time total.
|
||||
*
|
||||
* @param {number} milliseconds - The millisecond total to get minutes from.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getMinutesCount(milliseconds) {
|
||||
return Math.floor(milliseconds / (60 * 1000) % 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts how many whole seconds are included in the given time total.
|
||||
*
|
||||
* @param {number} milliseconds - The millisecond total to get seconds from.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getSecondsCount(milliseconds) {
|
||||
return Math.floor(milliseconds / 1000 % 60);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
import { connect as reactReduxConnect } from 'react-redux';
|
||||
|
||||
import { connect, disconnect } from '../../base/connection';
|
||||
import { DialogContainer } from '../../base/dialog';
|
||||
import { Container } from '../../base/react';
|
||||
import { FilmStrip } from '../../film-strip';
|
||||
import { LargeVideo } from '../../large-video';
|
||||
@@ -125,6 +126,8 @@ class Conference extends Component {
|
||||
<Toolbar visible = { toolbarVisible } />
|
||||
<FilmStrip visible = { !toolbarVisible } />
|
||||
|
||||
<DialogContainer />
|
||||
|
||||
{
|
||||
this._renderPrompt()
|
||||
}
|
||||
|
||||
@@ -4,26 +4,16 @@ import React, { Component } from 'react';
|
||||
import { connect as reactReduxConnect } from 'react-redux';
|
||||
|
||||
import { connect, disconnect } from '../../base/connection';
|
||||
import { DialogContainer } from '../../base/dialog';
|
||||
import { Watermarks } from '../../base/react';
|
||||
import { FeedbackButton } from '../../feedback';
|
||||
import { OverlayContainer } from '../../overlay';
|
||||
import { Notice } from '../../toolbar';
|
||||
import { HideNotificationBarStyle } from '../../unsupported-browser';
|
||||
|
||||
declare var $: Function;
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* For legacy reasons, inline style for display none.
|
||||
*
|
||||
* @private
|
||||
* @type {{
|
||||
* display: string
|
||||
* }}
|
||||
*/
|
||||
const _DISPLAY_NONE_STYLE = {
|
||||
display: 'none'
|
||||
};
|
||||
|
||||
/**
|
||||
* The conference page of the Web application.
|
||||
*/
|
||||
@@ -77,14 +67,8 @@ class Conference extends Component {
|
||||
return (
|
||||
<div id = 'videoconference_page'>
|
||||
<div id = 'mainToolbarContainer'>
|
||||
<div
|
||||
className = 'notice'
|
||||
id = 'notice'
|
||||
style = { _DISPLAY_NONE_STYLE }>
|
||||
<span
|
||||
className = 'noticeText'
|
||||
id = 'noticeText' />
|
||||
</div>
|
||||
<Notice />
|
||||
|
||||
<div
|
||||
className = 'toolbar'
|
||||
id = 'mainToolbar' />
|
||||
@@ -170,7 +154,7 @@ class Conference extends Component {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogContainer />
|
||||
<OverlayContainer />
|
||||
<HideNotificationBarStyle />
|
||||
</div>
|
||||
|
||||
20
react/features/desktop-picker/actionTypes.js
Normal file
20
react/features/desktop-picker/actionTypes.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Symbol } from '../base/react';
|
||||
|
||||
/**
|
||||
* Action to remove known DesktopCapturerSources.
|
||||
*
|
||||
* {
|
||||
* type: RESET_DESKTOP_SOURCES,
|
||||
* }
|
||||
*/
|
||||
export const RESET_DESKTOP_SOURCES = Symbol('RESET_DESKTOP_SOURCES');
|
||||
|
||||
/**
|
||||
* Action to replace stored DesktopCapturerSources with new sources.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_DESKTOP_SOURCES,
|
||||
* sources: {Array}
|
||||
* }
|
||||
*/
|
||||
export const UPDATE_DESKTOP_SOURCES = Symbol('UPDATE_DESKTOP_SOURCES');
|
||||
87
react/features/desktop-picker/actions.js
Normal file
87
react/features/desktop-picker/actions.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { getLogger } from 'jitsi-meet-logger';
|
||||
|
||||
import { openDialog } from '../base/dialog';
|
||||
import {
|
||||
RESET_DESKTOP_SOURCES,
|
||||
UPDATE_DESKTOP_SOURCES
|
||||
} from './actionTypes';
|
||||
import { DesktopPicker } from './components';
|
||||
|
||||
const logger = getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Signals to remove all stored DesktopCapturerSources.
|
||||
*
|
||||
* @returns {{
|
||||
* type: RESET_DESKTOP_SOURCES
|
||||
* }}
|
||||
*/
|
||||
export function resetDesktopSources() {
|
||||
return {
|
||||
type: RESET_DESKTOP_SOURCES
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins a request to get available DesktopCapturerSources.
|
||||
*
|
||||
* @param {Array} types - An array with DesktopCapturerSource type strings.
|
||||
* @param {Object} options - Additional configuration for getting a list
|
||||
* of sources.
|
||||
* @param {Object} options.thumbnailSize - The desired height and width
|
||||
* of the return native image object used for the preview image of the source.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function obtainDesktopSources(types, options = {}) {
|
||||
const capturerOptions = {
|
||||
types
|
||||
};
|
||||
|
||||
if (options.thumbnailSize) {
|
||||
capturerOptions.thumbnailSize = options.thumbnailSize;
|
||||
}
|
||||
|
||||
return dispatch => {
|
||||
if (window.JitsiMeetElectron
|
||||
&& window.JitsiMeetElectron.obtainDesktopStreams) {
|
||||
window.JitsiMeetElectron.obtainDesktopStreams(
|
||||
sources => dispatch(updateDesktopSources(sources)),
|
||||
error => logger.error(
|
||||
`Error while obtaining desktop sources: ${error}`),
|
||||
capturerOptions
|
||||
);
|
||||
} else {
|
||||
logger.error('Called JitsiMeetElectron.obtainDesktopStreams '
|
||||
+ 'but it is not defined');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to open a dialog with the DesktopPicker component.
|
||||
*
|
||||
* @param {Function} onSourceChoose - The callback to invoke when
|
||||
* a DesktopCapturerSource has been chosen.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function showDesktopPicker(onSourceChoose) {
|
||||
return openDialog(DesktopPicker, {
|
||||
onSourceChoose
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals new DesktopCapturerSources have been received.
|
||||
*
|
||||
* @param {Object} sources - Arrays with DesktopCapturerSources.
|
||||
* @returns {{
|
||||
* type: UPDATE_DESKTOP_SOURCES,
|
||||
* sources: Array
|
||||
* }}
|
||||
*/
|
||||
export function updateDesktopSources(sources) {
|
||||
return {
|
||||
type: UPDATE_DESKTOP_SOURCES,
|
||||
sources
|
||||
};
|
||||
}
|
||||
264
react/features/desktop-picker/components/DesktopPicker.js
Normal file
264
react/features/desktop-picker/components/DesktopPicker.js
Normal file
@@ -0,0 +1,264 @@
|
||||
/* global config */
|
||||
|
||||
import Tabs from '@atlaskit/tabs';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Dialog, hideDialog } from '../../base/dialog';
|
||||
import { translate } from '../../base/i18n';
|
||||
import {
|
||||
resetDesktopSources,
|
||||
obtainDesktopSources
|
||||
} from '../actions';
|
||||
import DesktopPickerPane from './DesktopPickerPane';
|
||||
|
||||
const updateInterval = 1000;
|
||||
const thumbnailSize = {
|
||||
height: 300,
|
||||
width: 300
|
||||
};
|
||||
const tabConfigurations = [
|
||||
{
|
||||
label: 'dialog.yourEntireScreen',
|
||||
type: 'screen',
|
||||
isDefault: true
|
||||
},
|
||||
{
|
||||
label: 'dialog.applicationWindow',
|
||||
type: 'window'
|
||||
}
|
||||
];
|
||||
|
||||
const validTypes = tabConfigurations.map(configuration => configuration.type);
|
||||
const configuredTypes = config.desktopSharingChromeSources || [];
|
||||
|
||||
const tabsToPopulate = tabConfigurations.filter(configuration =>
|
||||
configuredTypes.includes(configuration.type)
|
||||
&& validTypes.includes(configuration.type)
|
||||
);
|
||||
const typesToFetch = tabsToPopulate.map(configuration => configuration.type);
|
||||
|
||||
/**
|
||||
* React component for DesktopPicker.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class DesktopPicker extends Component {
|
||||
/**
|
||||
* DesktopPicker component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* Used to request DesktopCapturerSources.
|
||||
*/
|
||||
dispatch: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* The callback to be invoked when the component is closed or
|
||||
* when a DesktopCapturerSource has been chosen.
|
||||
*/
|
||||
onSourceChoose: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* An object with arrays of DesktopCapturerSources. The key
|
||||
* should be the source type.
|
||||
*/
|
||||
sources: React.PropTypes.object,
|
||||
|
||||
/**
|
||||
* Used to obtain translations.
|
||||
*/
|
||||
t: React.PropTypes.func
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new DesktopPicker instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
selectedSourceId: ''
|
||||
};
|
||||
|
||||
this._poller = null;
|
||||
this._onCloseModal = this._onCloseModal.bind(this);
|
||||
this._onPreviewClick = this._onPreviewClick.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._updateSources = this._updateSources.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an immediate update request for DesktopCapturerSources and
|
||||
* begin requesting updates at an interval.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillMount() {
|
||||
this._updateSources();
|
||||
this._startPolling();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up component and DesktopCapturerSource store state.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
this._stopPolling();
|
||||
this.props.dispatch(resetDesktopSources());
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this mounted React Component that it will receive new props.
|
||||
* Sets a default selected source if one is not already set.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @param {Object} nextProps - The read-only React Component props that this
|
||||
* instance will receive.
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.state.selectedSourceId
|
||||
&& nextProps.sources.screen.length) {
|
||||
this.setState({ selectedSourceId: nextProps.sources.screen[0].id });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
isModal = { false }
|
||||
okTitleKey = 'dialog.Share'
|
||||
onCancel = { this._onCloseModal }
|
||||
onSubmit = { this._onSubmit }
|
||||
titleKey = 'dialog.shareYourScreen'
|
||||
width = 'medium' >
|
||||
{ this._renderTabs() }
|
||||
</Dialog>);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to get currently available DesktopCapturerSources.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_updateSources() {
|
||||
this.props.dispatch(obtainDesktopSources(
|
||||
typesToFetch,
|
||||
{
|
||||
thumbnailSize
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an interval to update knwon available DesktopCapturerSources.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_startPolling() {
|
||||
this._stopPolling();
|
||||
this._poller = window.setInterval(this._updateSources,
|
||||
updateInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the interval to update DesktopCapturerSources.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_stopPolling() {
|
||||
window.clearInterval(this._poller);
|
||||
this._poller = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the currently selected DesktopCapturerSource.
|
||||
*
|
||||
* @param {string} id - The id of DesktopCapturerSource.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPreviewClick(id) {
|
||||
this.setState({ selectedSourceId: id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to close the modal and execute callbacks
|
||||
* with the selected source id.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmit() {
|
||||
this._onCloseModal(this.state.selectedSourceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an action to hide the DesktopPicker and invokes
|
||||
* the passed in callback with a selectedSourceId, if any.
|
||||
*
|
||||
* @param {string} id - The id of the DesktopCapturerSource to pass into
|
||||
* the onSourceChoose callback.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCloseModal(id = '') {
|
||||
this.props.onSourceChoose(id);
|
||||
this.props.dispatch(hideDialog());
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures and renders the tabs for display.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderTabs() {
|
||||
const tabs = tabsToPopulate.map(tabConfig => {
|
||||
const type = tabConfig.type;
|
||||
|
||||
return {
|
||||
label: this.props.t(tabConfig.label),
|
||||
defaultSelected: tabConfig.isDefault,
|
||||
content: <DesktopPickerPane
|
||||
key = { type }
|
||||
onClick = { this._onPreviewClick }
|
||||
onDoubleClick = { this._onCloseModal }
|
||||
selectedSourceId = { this.state.selectedSourceId }
|
||||
sources = { this.props.sources[type] || [] }
|
||||
type = { type } />
|
||||
};
|
||||
});
|
||||
|
||||
return <Tabs tabs = { tabs } />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated DesktopPicker's props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @protected
|
||||
* @returns {{
|
||||
* sources: Object
|
||||
* }}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
sources: state['features/desktop-picker/sources']
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(DesktopPicker));
|
||||
@@ -0,0 +1,69 @@
|
||||
import React, { Component } from 'react';
|
||||
import DesktopSourcePreview from './DesktopSourcePreview';
|
||||
|
||||
/**
|
||||
* React component for showing a grid of DesktopSourcePreviews.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class DesktopPickerPane extends Component {
|
||||
/**
|
||||
* DesktopPickerPane component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The handler to be invoked when a DesktopSourcePreview is clicked.
|
||||
*/
|
||||
onClick: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* The handler to be invoked when a DesktopSourcePreview is
|
||||
* double clicked.
|
||||
*/
|
||||
onDoubleClick: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* The id of the DesktopCapturerSource that is currently selected.
|
||||
*/
|
||||
selectedSourceId: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* An array of DesktopCapturerSources.
|
||||
*/
|
||||
sources: React.PropTypes.array,
|
||||
|
||||
/**
|
||||
* The source type of the DesktopCapturerSources to display.
|
||||
*/
|
||||
type: React.PropTypes.string
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const previews = this.props.sources.map(source =>
|
||||
<DesktopSourcePreview
|
||||
isSelected = { source.id === this.props.selectedSourceId }
|
||||
key = { source.id }
|
||||
onClick = { this.props.onClick }
|
||||
onDoubleClick = { this.props.onDoubleClick }
|
||||
source = { source } />
|
||||
);
|
||||
const classnames = 'desktop-picker-pane default-scrollbar '
|
||||
+ `source-type-${this.props.type}`;
|
||||
|
||||
return (
|
||||
<div className = { classnames }>
|
||||
{ previews }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DesktopPickerPane;
|
||||
@@ -0,0 +1,97 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
/**
|
||||
* React component for displaying a preview of a DesktopCapturerSource.
|
||||
*
|
||||
* @extends Component
|
||||
*/
|
||||
class DesktopSourcePreview extends Component {
|
||||
/**
|
||||
* DesktopSourcePreview component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* If true the 'is-selected' class will be added to the component.
|
||||
*/
|
||||
isSelected: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* The callback to invoke when the component is clicked.
|
||||
* The id of the DesktopCapturerSource will be passed in.
|
||||
*/
|
||||
onClick: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* The callback to invoke when the component is double clicked.
|
||||
* The id of the DesktopCapturerSource will be passed in.
|
||||
*/
|
||||
onDoubleClick: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* The DesktopCapturerSource to display.
|
||||
*/
|
||||
source: React.PropTypes.object
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new DesktopSourcePreview instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onDoubleClick = this._onDoubleClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const isSelectedClass = this.props.isSelected ? 'is-selected' : '';
|
||||
const displayClasses = `desktop-picker-source ${isSelectedClass}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { displayClasses }
|
||||
onClick = { this._onClick }
|
||||
onDoubleClick = { this._onDoubleClick }>
|
||||
<div className = 'desktop-source-preview-image-container'>
|
||||
<img
|
||||
className = 'desktop-source-preview-thumbnail'
|
||||
src = { this.props.source.thumbnail.toDataURL() } />
|
||||
</div>
|
||||
<div className = 'desktop-source-preview-label'>
|
||||
{ this.props.source.name }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the passed in onClick callback.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
this.props.onClick(this.props.source.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the passed in onDoubleClick callback.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDoubleClick() {
|
||||
this.props.onDoubleClick(this.props.source.id);
|
||||
}
|
||||
}
|
||||
|
||||
export default DesktopSourcePreview;
|
||||
1
react/features/desktop-picker/components/index.js
Normal file
1
react/features/desktop-picker/components/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default as DesktopPicker } from './DesktopPicker';
|
||||
5
react/features/desktop-picker/index.js
Normal file
5
react/features/desktop-picker/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './actionTypes';
|
||||
export * from './actions';
|
||||
export * from './components';
|
||||
|
||||
import './reducer';
|
||||
59
react/features/desktop-picker/reducer.js
Normal file
59
react/features/desktop-picker/reducer.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
import {
|
||||
RESET_DESKTOP_SOURCES,
|
||||
UPDATE_DESKTOP_SOURCES
|
||||
} from './actionTypes';
|
||||
|
||||
const defaultState = {
|
||||
screen: [],
|
||||
window: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for actions that mutate the known available DesktopCapturerSources.
|
||||
*
|
||||
* @param {Object[]} state - Current state.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @param {Array} action.sources - DesktopCapturerSources.
|
||||
* @returns {Object}
|
||||
*/
|
||||
ReducerRegistry.register(
|
||||
'features/desktop-picker/sources',
|
||||
(state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case RESET_DESKTOP_SOURCES:
|
||||
return { ...defaultState };
|
||||
case UPDATE_DESKTOP_SOURCES:
|
||||
return seperateSourcesByType(action.sources);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Converts an array of DesktopCapturerSources to an object with types
|
||||
* for keys and values being an array with sources of the key's type.
|
||||
*
|
||||
* @param {Array} sources - DesktopCapturerSources.
|
||||
* @returns {Object} An object with the sources split into seperate arrays
|
||||
* based on source type.
|
||||
* @private
|
||||
*/
|
||||
function seperateSourcesByType(sources = []) {
|
||||
const sourcesByType = {
|
||||
screen: [],
|
||||
window: []
|
||||
};
|
||||
|
||||
sources.forEach(source => {
|
||||
const sourceIdParts = source.id.split(':');
|
||||
const sourceType = sourceIdParts[0];
|
||||
|
||||
if (sourcesByType[sourceType]) {
|
||||
sourcesByType[sourceType].push(source);
|
||||
}
|
||||
});
|
||||
|
||||
return sourcesByType;
|
||||
}
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
import { NativeModules } from 'react-native';
|
||||
|
||||
import { APP_WILL_MOUNT } from '../app';
|
||||
import { APP_WILL_MOUNT } from '../../app';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_JOIN
|
||||
} from '../base/conference';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
} from '../../base/conference';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
|
||||
/**
|
||||
* Middleware that captures conference actions and sets the correct audio mode
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Symbol } from '../base/react';
|
||||
import { Symbol } from '../../base/react';
|
||||
|
||||
/**
|
||||
* The type of redux action to set the AppState API change event listener.
|
||||
@@ -10,8 +10,7 @@ import { Symbol } from '../base/react';
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export const _SET_APP_STATE_LISTENER
|
||||
= Symbol('_SET_APP_STATE_LISTENER');
|
||||
export const _SET_APP_STATE_LISTENER = Symbol('_SET_APP_STATE_LISTENER');
|
||||
|
||||
/**
|
||||
* The type of redux action which signals that video will be muted because the
|
||||
@@ -27,6 +26,18 @@ export const _SET_APP_STATE_LISTENER
|
||||
export const _SET_BACKGROUND_VIDEO_MUTED
|
||||
= Symbol('_SET_BACKGROUND_VIDEO_MUTED');
|
||||
|
||||
/**
|
||||
* The type of redux action which sets the video channel's lastN (value).
|
||||
*
|
||||
* {
|
||||
* type: _SET_LASTN,
|
||||
* lastN: boolean
|
||||
* }
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
export const _SET_LASTN = Symbol('_SET_LASTN');
|
||||
|
||||
/**
|
||||
* The type of redux action which signals that the app state has changed (in
|
||||
* terms of execution mode). The app state can be one of 'active', 'inactive',
|
||||
@@ -1,30 +1,12 @@
|
||||
import { setVideoMuted } from '../base/media';
|
||||
import { setVideoMuted } from '../../base/media';
|
||||
|
||||
import {
|
||||
_SET_APP_STATE_LISTENER,
|
||||
_SET_BACKGROUND_VIDEO_MUTED,
|
||||
_SET_LASTN,
|
||||
APP_STATE_CHANGED
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Signals that the App state has changed (in terms of execution state). The
|
||||
* application can be in 3 states: 'active', 'inactive' and 'background'.
|
||||
*
|
||||
* @param {string} appState - The new App state.
|
||||
* @public
|
||||
* @returns {{
|
||||
* type: APP_STATE_CHANGED,
|
||||
* appState: string
|
||||
* }}
|
||||
* @see {@link https://facebook.github.io/react-native/docs/appstate.html}
|
||||
*/
|
||||
export function appStateChanged(appState: string) {
|
||||
return {
|
||||
type: APP_STATE_CHANGED,
|
||||
appState
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener to be used with React Native's AppState API.
|
||||
*
|
||||
@@ -54,24 +36,49 @@ export function _setAppStateListener(listener: ?Function) {
|
||||
*/
|
||||
export function _setBackgroundVideoMuted(muted: boolean) {
|
||||
return (dispatch, getState) => {
|
||||
if (muted) {
|
||||
const mediaState = getState()['features/base/media'];
|
||||
// Disable remote video when we mute by setting lastN to 0.
|
||||
// Skip it if the conference is in audio only mode, as it's
|
||||
// already configured to have no video.
|
||||
const { audioOnly } = getState()['features/base/conference'];
|
||||
|
||||
if (mediaState.video.muted) {
|
||||
if (!audioOnly) {
|
||||
let lastN;
|
||||
|
||||
if (muted) {
|
||||
lastN = 0;
|
||||
} else {
|
||||
const { config } = getState()['features/base/lib-jitsi-meet'];
|
||||
|
||||
lastN = config.channelLastN;
|
||||
if (typeof lastN === 'undefined') {
|
||||
lastN = -1;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: _SET_LASTN,
|
||||
lastN
|
||||
});
|
||||
}
|
||||
|
||||
if (muted) {
|
||||
const { video } = getState()['features/base/media'];
|
||||
|
||||
if (video.muted) {
|
||||
// Video is already muted, do nothing.
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const bgState = getState()['features/background'];
|
||||
const { videoMuted } = getState()['features/background'];
|
||||
|
||||
if (!bgState.videoMuted) {
|
||||
if (!videoMuted) {
|
||||
// We didn't mute video, do nothing.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Remember that video was muted due to the app going to the background
|
||||
// vs user's choice.
|
||||
// Remember that local video was muted due to the app going to the
|
||||
// background vs user's choice.
|
||||
dispatch({
|
||||
type: _SET_BACKGROUND_VIDEO_MUTED,
|
||||
muted
|
||||
@@ -79,3 +86,22 @@ export function _setBackgroundVideoMuted(muted: boolean) {
|
||||
dispatch(setVideoMuted(muted));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the App state has changed (in terms of execution state). The
|
||||
* application can be in 3 states: 'active', 'inactive' and 'background'.
|
||||
*
|
||||
* @param {string} appState - The new App state.
|
||||
* @public
|
||||
* @returns {{
|
||||
* type: APP_STATE_CHANGED,
|
||||
* appState: string
|
||||
* }}
|
||||
* @see {@link https://facebook.github.io/react-native/docs/appstate.html}
|
||||
*/
|
||||
export function appStateChanged(appState: string) {
|
||||
return {
|
||||
type: APP_STATE_CHANGED,
|
||||
appState
|
||||
};
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import type { Dispatch } from 'redux';
|
||||
import {
|
||||
APP_WILL_MOUNT,
|
||||
APP_WILL_UNMOUNT
|
||||
} from '../app';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
} from '../../app';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
|
||||
import {
|
||||
_setAppStateListener,
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from './actions';
|
||||
import {
|
||||
_SET_APP_STATE_LISTENER,
|
||||
_SET_LASTN,
|
||||
APP_STATE_CHANGED
|
||||
} from './actionTypes';
|
||||
|
||||
@@ -46,6 +47,20 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case _SET_LASTN: {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
|
||||
if (conference) {
|
||||
try {
|
||||
conference.setLastN(action.lastN);
|
||||
} catch (err) {
|
||||
console.warn(`Failed to set lastN: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case APP_STATE_CHANGED:
|
||||
_appStateChanged(store.dispatch, action.appState);
|
||||
break;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
import { ReducerRegistry } from '../../base/redux';
|
||||
|
||||
import {
|
||||
_SET_APP_STATE_LISTENER,
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_JOIN
|
||||
} from '../base/conference';
|
||||
import { Platform } from '../base/react';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
} from '../../base/conference';
|
||||
import { Platform } from '../../base/react';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
|
||||
/**
|
||||
* Middleware that captures conference actions and activates or deactivates the
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT
|
||||
} from '../base/conference';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
} from '../../base/conference';
|
||||
import { MiddlewareRegistry } from '../../base/redux';
|
||||
|
||||
/**
|
||||
* Middleware that captures conference actions and activates or deactivates the
|
||||
@@ -1,80 +0,0 @@
|
||||
/* global APP */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
/**
|
||||
* Implements an abstract React Component for overlay - the components which are
|
||||
* displayed on top of the application covering the whole screen.
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class AbstractOverlay extends Component {
|
||||
/**
|
||||
* Initializes a new AbstractOverlay instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
* @public
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
/**
|
||||
* Indicates the CSS style of the overlay. If true, then ighter;
|
||||
* darker, otherwise.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
isLightOverlay: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
render() {
|
||||
const containerClass
|
||||
= this.state.isLightOverlay
|
||||
? 'overlay__container-light'
|
||||
: 'overlay__container';
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { containerClass }
|
||||
id = 'overlay'>
|
||||
<div className = 'overlay__content'>
|
||||
{
|
||||
this._renderOverlayContent()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the page.
|
||||
*
|
||||
* @returns {void}
|
||||
* @protected
|
||||
*/
|
||||
_reconnectNow() {
|
||||
// FIXME: In future we should dispatch an action here that will result
|
||||
// in reload.
|
||||
APP.ConferenceUrl.reload();
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method which should be used by subclasses to provide the overlay
|
||||
* content.
|
||||
*
|
||||
* @returns {ReactElement|null}
|
||||
* @protected
|
||||
*/
|
||||
_renderOverlayContent() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
192
react/features/overlay/components/AbstractPageReloadOverlay.js
Normal file
192
react/features/overlay/components/AbstractPageReloadOverlay.js
Normal file
@@ -0,0 +1,192 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { randomInt } from '../../base/util';
|
||||
|
||||
import { reconnectNow } from '../functions';
|
||||
import ReloadButton from './ReloadButton';
|
||||
|
||||
declare var AJS: Object;
|
||||
declare var APP: Object;
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Implements abstract React Component for the page reload overlays.
|
||||
*/
|
||||
export default class AbstractPageReloadOverlay extends Component {
|
||||
/**
|
||||
* AbstractPageReloadOverlay component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The indicator which determines whether the reload was caused by
|
||||
* network failure.
|
||||
*
|
||||
* @public
|
||||
* @type {boolean}
|
||||
*/
|
||||
isNetworkFailure: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* The reason for the error that will cause the reload.
|
||||
* NOTE: Used by PageReloadOverlay only.
|
||||
*
|
||||
* @public
|
||||
* @type {string}
|
||||
*/
|
||||
reason: React.PropTypes.string
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new AbstractPageReloadOverlay instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
* @public
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
/**
|
||||
* How long the overlay dialog will be displayed, before the conference
|
||||
* will be reloaded.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
const timeoutSeconds = 10 + randomInt(0, 20);
|
||||
|
||||
let message, title;
|
||||
|
||||
if (this.props.isNetworkFailure) {
|
||||
title = 'dialog.conferenceDisconnectTitle';
|
||||
message = 'dialog.conferenceDisconnectMsg';
|
||||
} else {
|
||||
title = 'dialog.conferenceReloadTitle';
|
||||
message = 'dialog.conferenceReloadMsg';
|
||||
}
|
||||
|
||||
this.state = {
|
||||
/**
|
||||
* The translation key for the title of the overlay.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
message,
|
||||
|
||||
/**
|
||||
* Current value(time) of the timer.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
timeLeft: timeoutSeconds,
|
||||
|
||||
/**
|
||||
* How long the overlay dialog will be displayed before the
|
||||
* conference will be reloaded.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
timeoutSeconds,
|
||||
|
||||
/**
|
||||
* The translation key for the title of the overlay.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
title
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the button for relaod the page if necessary.
|
||||
*
|
||||
* @returns {ReactElement|null}
|
||||
* @private
|
||||
*/
|
||||
_renderButton() {
|
||||
if (this.props.isNetworkFailure) {
|
||||
return (
|
||||
<ReloadButton textKey = 'dialog.rejoinNow' />
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the progress bar.
|
||||
*
|
||||
* @returns {ReactElement|null}
|
||||
* @protected
|
||||
*/
|
||||
_renderProgressBar() {
|
||||
return (
|
||||
<div
|
||||
className = 'aui-progress-indicator'
|
||||
id = 'reloadProgressBar'>
|
||||
<span className = 'aui-progress-indicator-value' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* React Component method that executes once component is mounted.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
* @protected
|
||||
*/
|
||||
componentDidMount() {
|
||||
// FIXME (CallStats - issue) This event will not make it to CallStats
|
||||
// because the log queue is not flushed before "fabric terminated" is
|
||||
// sent to the backed.
|
||||
// FIXME: We should dispatch action for this.
|
||||
APP.conference.logEvent(
|
||||
'page.reload',
|
||||
/* value */ undefined,
|
||||
/* label */ this.props.reason);
|
||||
logger.info(
|
||||
'The conference will be reloaded after '
|
||||
+ `${this.state.timeoutSeconds} seconds.`);
|
||||
|
||||
AJS.progressBars.update('#reloadProgressBar', 0);
|
||||
|
||||
this.intervalId = setInterval(() => {
|
||||
if (this.state.timeLeft === 0) {
|
||||
clearInterval(this.intervalId);
|
||||
reconnectNow();
|
||||
} else {
|
||||
this.setState(prevState => {
|
||||
return {
|
||||
timeLeft: prevState.timeLeft - 1
|
||||
};
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* React Component method that executes once component is updated.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
* @protected
|
||||
*/
|
||||
componentDidUpdate() {
|
||||
AJS.progressBars.update('#reloadProgressBar',
|
||||
(this.state.timeoutSeconds - this.state.timeLeft)
|
||||
/ this.state.timeoutSeconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the timer interval.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
}
|
||||
133
react/features/overlay/components/FilmStripOnlyOverlayFrame.js
Normal file
133
react/features/overlay/components/FilmStripOnlyOverlayFrame.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
getAvatarURL,
|
||||
getLocalParticipant
|
||||
} from '../../base/participants';
|
||||
|
||||
import OverlayFrame from './OverlayFrame';
|
||||
|
||||
/**
|
||||
* Implements a React Component for the frame of the overlays in filmstrip only
|
||||
* mode.
|
||||
*/
|
||||
class FilmStripOnlyOverlayFrame extends Component {
|
||||
/**
|
||||
* FilmStripOnlyOverlayFrame component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The source (e.g. URI, URL) of the avatar image of the local
|
||||
* participant.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_avatar: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* The children components to be displayed into the overlay frame for
|
||||
* filmstrip only mode.
|
||||
*
|
||||
* @type {ReactElement}
|
||||
*/
|
||||
children: React.PropTypes.node.isRequired,
|
||||
|
||||
/**
|
||||
* The css class name for the icon that will be displayed over the
|
||||
* avatar.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
icon: React.PropTypes.string,
|
||||
|
||||
/**
|
||||
* Indicates the css style of the overlay. If true, then lighter;
|
||||
* darker, otherwise.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
isLightOverlay: React.PropTypes.bool
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders content related to the icon.
|
||||
*
|
||||
* @returns {ReactElement|null}
|
||||
* @private
|
||||
*/
|
||||
_renderIcon() {
|
||||
if (!this.props.icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const iconClass = `inlay-filmstrip-only__icon ${this.props.icon}`;
|
||||
const iconBGClass = 'inlay-filmstrip-only__icon-background';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className = { iconBGClass } />
|
||||
<div className = 'inlay-filmstrip-only__icon-container'>
|
||||
<span className = { iconClass } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<OverlayFrame isLightOverlay = { this.props.isLightOverlay }>
|
||||
<div className = 'inlay-filmstrip-only'>
|
||||
<div className = 'inlay-filmstrip-only__content'>
|
||||
{
|
||||
this.props.children
|
||||
}
|
||||
</div>
|
||||
<div className = 'inlay-filmstrip-only__avatar-container'>
|
||||
<Avatar uri = { this.props._avatar } />
|
||||
{
|
||||
this._renderIcon()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</OverlayFrame>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated FilmStripOnlyOverlayFrame
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _avatar: string
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const participant
|
||||
= getLocalParticipant(
|
||||
state['features/base/participants']);
|
||||
const { avatarId, avatarUrl, email } = participant || {};
|
||||
|
||||
return {
|
||||
_avatar: getAvatarURL({
|
||||
avatarId,
|
||||
avatarUrl,
|
||||
email,
|
||||
participantId: participant.id
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(FilmStripOnlyOverlayFrame);
|
||||
@@ -1,12 +1,17 @@
|
||||
/* global APP */
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import PageReloadFilmStripOnlyOverlay from './PageReloadFilmStripOnlyOverlay';
|
||||
import PageReloadOverlay from './PageReloadOverlay';
|
||||
import SuspendedFilmStripOnlyOverlay from './SuspendedFilmStripOnlyOverlay';
|
||||
import SuspendedOverlay from './SuspendedOverlay';
|
||||
import UserMediaPermissionsFilmStripOnlyOverlay
|
||||
from './UserMediaPermissionsFilmStripOnlyOverlay';
|
||||
import UserMediaPermissionsOverlay from './UserMediaPermissionsOverlay';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Implements a React Component that will display the correct overlay when
|
||||
* needed.
|
||||
@@ -94,6 +99,25 @@ class OverlayContainer extends Component {
|
||||
_suspendDetected: React.PropTypes.bool
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new ReloadTimer instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
* @public
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
/**
|
||||
* Indicates whether the film strip only mode is enabled or not.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
filmStripOnly: interfaceConfig.filmStripOnly
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* React Component method that executes once component is updated.
|
||||
*
|
||||
@@ -117,25 +141,28 @@ class OverlayContainer extends Component {
|
||||
* @public
|
||||
*/
|
||||
render() {
|
||||
const filmStripOnlyMode = this.state.filmStripOnly;
|
||||
let overlayComponent, props;
|
||||
|
||||
if (this.props._connectionEstablished && this.props._haveToReload) {
|
||||
return (
|
||||
<PageReloadOverlay
|
||||
isNetworkFailure = { this.props._isNetworkFailure }
|
||||
reason = { this.props._reason } />
|
||||
);
|
||||
overlayComponent = filmStripOnlyMode
|
||||
? PageReloadFilmStripOnlyOverlay : PageReloadOverlay;
|
||||
props = {
|
||||
isNetworkFailure: this.props._isNetworkFailure,
|
||||
reason: this.props._reason
|
||||
};
|
||||
} else if (this.props._suspendDetected) {
|
||||
overlayComponent = filmStripOnlyMode
|
||||
? SuspendedFilmStripOnlyOverlay : SuspendedOverlay;
|
||||
} else if (this.props._isMediaPermissionPromptVisible) {
|
||||
overlayComponent = filmStripOnlyMode
|
||||
? UserMediaPermissionsFilmStripOnlyOverlay
|
||||
: UserMediaPermissionsOverlay;
|
||||
props = { browser: this.props._browser };
|
||||
}
|
||||
|
||||
if (this.props._suspendDetected) {
|
||||
return (
|
||||
<SuspendedOverlay />
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props._isMediaPermissionPromptVisible) {
|
||||
return (
|
||||
<UserMediaPermissionsOverlay
|
||||
browser = { this.props._browser } />
|
||||
);
|
||||
if (overlayComponent) {
|
||||
return React.createElement(overlayComponent, props);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
77
react/features/overlay/components/OverlayFrame.js
Normal file
77
react/features/overlay/components/OverlayFrame.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Implements a React Component for the frame of the overlays.
|
||||
*/
|
||||
export default class OverlayFrame extends Component {
|
||||
/**
|
||||
* OverlayFrame component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The children components to be displayed into the overlay frame.
|
||||
*/
|
||||
children: React.PropTypes.node.isRequired,
|
||||
|
||||
/**
|
||||
* Indicates the css style of the overlay. If true, then lighter;
|
||||
* darker, otherwise.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
isLightOverlay: React.PropTypes.bool
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new AbstractOverlay instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
* @public
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
/**
|
||||
* Indicates whether the film strip only mode is enabled or not.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
filmStripOnly: interfaceConfig.filmStripOnly
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
render() {
|
||||
let containerClass = this.props.isLightOverlay
|
||||
? 'overlay__container-light' : 'overlay__container';
|
||||
let contentClass = 'overlay__content';
|
||||
|
||||
if (this.state.filmStripOnly) {
|
||||
containerClass += ' filmstrip-only';
|
||||
contentClass += ' filmstrip-only';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { containerClass }
|
||||
id = 'overlay'>
|
||||
<div className = { contentClass }>
|
||||
{
|
||||
this.props.children
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
|
||||
import AbstractPageReloadOverlay from './AbstractPageReloadOverlay';
|
||||
import FilmStripOnlyOverlayFrame from './FilmStripOnlyOverlayFrame';
|
||||
|
||||
/**
|
||||
* Implements a React Component for page reload overlay for filmstrip only
|
||||
* mode. Shown before the conference is reloaded. Shows a warning message and
|
||||
* counts down towards the reload.
|
||||
*/
|
||||
class PageReloadFilmStripOnlyOverlay extends AbstractPageReloadOverlay {
|
||||
/**
|
||||
* PageReloadFilmStripOnlyOverlay component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
...AbstractPageReloadOverlay.propTypes,
|
||||
|
||||
/**
|
||||
* The function to translate human-readable text.
|
||||
*
|
||||
* @public
|
||||
* @type {Function}
|
||||
*/
|
||||
t: React.PropTypes.func
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { message, timeLeft, title } = this.state;
|
||||
|
||||
return (
|
||||
<FilmStripOnlyOverlayFrame>
|
||||
<div className = 'inlay-filmstrip-only__container'>
|
||||
<div className = 'inlay-filmstrip-only__title'>
|
||||
{ t(title) }
|
||||
</div>
|
||||
<div className = 'inlay-filmstrip-only__text'>
|
||||
{ t(message, { seconds: timeLeft }) }
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
this._renderButton()
|
||||
}
|
||||
{
|
||||
this._renderProgressBar()
|
||||
}
|
||||
</FilmStripOnlyOverlayFrame>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(PageReloadFilmStripOnlyOverlay);
|
||||
@@ -1,194 +1,58 @@
|
||||
import React from 'react';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
import { randomInt } from '../../base/util';
|
||||
|
||||
import AbstractOverlay from './AbstractOverlay';
|
||||
import ReloadTimer from './ReloadTimer';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
import AbstractPageReloadOverlay from './AbstractPageReloadOverlay';
|
||||
import OverlayFrame from './OverlayFrame';
|
||||
|
||||
/**
|
||||
* Implements a React Component for page reload overlay. Shown before the
|
||||
* conference is reloaded. Shows a warning message and counts down towards the
|
||||
* reload.
|
||||
*/
|
||||
class PageReloadOverlay extends AbstractOverlay {
|
||||
class PageReloadOverlay extends AbstractPageReloadOverlay {
|
||||
/**
|
||||
* PageReloadOverlay component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The indicator which determines whether the reload was caused by
|
||||
* network failure.
|
||||
* @public
|
||||
* @type {boolean}
|
||||
*/
|
||||
isNetworkFailure: React.PropTypes.bool,
|
||||
...AbstractPageReloadOverlay.propTypes,
|
||||
|
||||
/**
|
||||
* The reason for the error that will cause the reload.
|
||||
* NOTE: Used by PageReloadOverlay only.
|
||||
* The function to translate human-readable text.
|
||||
*
|
||||
* @public
|
||||
* @type {string}
|
||||
* @type {Function}
|
||||
*/
|
||||
reason: React.PropTypes.string
|
||||
t: React.PropTypes.func
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a new PageReloadOverlay instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
* @public
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
/**
|
||||
* How long the overlay dialog will be displayed, before the conference
|
||||
* will be reloaded.
|
||||
* @type {number}
|
||||
*/
|
||||
const timeoutSeconds = 10 + randomInt(0, 20);
|
||||
|
||||
let isLightOverlay, message, title;
|
||||
|
||||
if (this.props.isNetworkFailure) {
|
||||
title = 'dialog.conferenceDisconnectTitle';
|
||||
message = 'dialog.conferenceDisconnectMsg';
|
||||
isLightOverlay = true;
|
||||
} else {
|
||||
title = 'dialog.conferenceReloadTitle';
|
||||
message = 'dialog.conferenceReloadMsg';
|
||||
isLightOverlay = false;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
...this.state,
|
||||
|
||||
/**
|
||||
* Indicates the css style of the overlay. If true, then lighter;
|
||||
* darker, otherwise.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
isLightOverlay,
|
||||
|
||||
/**
|
||||
* The translation key for the title of the overlay.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
message,
|
||||
|
||||
/**
|
||||
* How long the overlay dialog will be displayed before the
|
||||
* conference will be reloaded.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
timeoutSeconds,
|
||||
|
||||
/**
|
||||
* The translation key for the title of the overlay.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
title
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is executed when comonent is mounted.
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
|
||||
// FIXME (CallStats - issue) This event will not make it to CallStats
|
||||
// because the log queue is not flushed before "fabric terminated" is
|
||||
// sent to the backed.
|
||||
// FIXME: We should dispatch action for this.
|
||||
APP.conference.logEvent(
|
||||
'page.reload',
|
||||
/* value */ undefined,
|
||||
/* label */ this.props.reason);
|
||||
logger.info(
|
||||
'The conference will be reloaded after '
|
||||
+ `${this.state.timeoutSeconds} seconds.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the button for relaod the page if necessary.
|
||||
*
|
||||
* @returns {ReactElement|null}
|
||||
* @private
|
||||
*/
|
||||
_renderButton() {
|
||||
if (this.props.isNetworkFailure) {
|
||||
const className
|
||||
= 'button-control button-control_primary button-control_center';
|
||||
const { t } = this.props;
|
||||
|
||||
/* eslint-disable react/jsx-handler-names */
|
||||
|
||||
return (
|
||||
<button
|
||||
className = { className }
|
||||
id = 'reconnectNow'
|
||||
onClick = { this._reconnectNow }>
|
||||
{ t('dialog.reconnectNow') }
|
||||
</button>
|
||||
);
|
||||
|
||||
|
||||
/* eslint-enable react/jsx-handler-names */
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs overlay body with the warning message and count down towards
|
||||
* the conference reload.
|
||||
*
|
||||
* @returns {ReactElement|null}
|
||||
* @override
|
||||
* @protected
|
||||
*/
|
||||
_renderOverlayContent() {
|
||||
const { t } = this.props;
|
||||
|
||||
/* eslint-disable react/jsx-handler-names */
|
||||
render() {
|
||||
const { isNetworkFailure, t } = this.props;
|
||||
const { message, timeLeft, title } = this.state;
|
||||
|
||||
return (
|
||||
<div className = 'inlay'>
|
||||
<span
|
||||
className = 'reload_overlay_title'>
|
||||
{ t(this.state.title) }
|
||||
</span>
|
||||
<span
|
||||
className = 'reload_overlay_text'>
|
||||
{ t(this.state.message) }
|
||||
</span>
|
||||
<ReloadTimer
|
||||
end = { 0 }
|
||||
interval = { 1 }
|
||||
onFinish = { this._reconnectNow }
|
||||
start = { this.state.timeoutSeconds }
|
||||
step = { -1 } />
|
||||
{ this._renderButton() }
|
||||
</div>
|
||||
<OverlayFrame isLightOverlay = { isNetworkFailure }>
|
||||
<div className = 'inlay'>
|
||||
<span
|
||||
className = 'reload_overlay_title'>
|
||||
{ t(title) }
|
||||
</span>
|
||||
<span className = 'reload_overlay_text'>
|
||||
{ t(message, { seconds: timeLeft }) }
|
||||
</span>
|
||||
{ this._renderProgressBar() }
|
||||
{ this._renderButton() }
|
||||
</div>
|
||||
</OverlayFrame>
|
||||
);
|
||||
|
||||
/* eslint-enable react/jsx-handler-names */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
60
react/features/overlay/components/ReloadButton.js
Normal file
60
react/features/overlay/components/ReloadButton.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
|
||||
import { reconnectNow } from '../functions';
|
||||
|
||||
/**
|
||||
* Implements a React Component for button for the overlays that will reload
|
||||
* the page.
|
||||
*/
|
||||
class ReloadButton extends Component {
|
||||
/**
|
||||
* PageReloadOverlay component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The function to translate human-readable text.
|
||||
*
|
||||
* @public
|
||||
* @type {Function}
|
||||
*/
|
||||
t: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* The translation key for the text in the button.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
textKey: React.PropTypes.string.isRequired
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the button for relaod the page if necessary.
|
||||
*
|
||||
* @returns {ReactElement|null}
|
||||
* @private
|
||||
*/
|
||||
render() {
|
||||
const className
|
||||
= 'button-control button-control_overlay button-control_center';
|
||||
const { t } = this.props;
|
||||
|
||||
/* eslint-disable react/jsx-handler-names */
|
||||
|
||||
return (
|
||||
<button
|
||||
className = { className }
|
||||
onClick = { reconnectNow }>
|
||||
{ t(this.props.textKey) }
|
||||
</button>
|
||||
);
|
||||
|
||||
/* eslint-enable react/jsx-handler-names */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate(ReloadButton);
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { translate, translateToHTML } from '../../base/i18n';
|
||||
|
||||
import FilmStripOnlyOverlayFrame from './FilmStripOnlyOverlayFrame';
|
||||
import ReloadButton from './ReloadButton';
|
||||
|
||||
/**
|
||||
* Implements a React Component for suspended overlay for filmstrip only mode.
|
||||
* Shown when suspended is detected.
|
||||
*/
|
||||
class SuspendedFilmStripOnlyOverlay extends Component {
|
||||
/**
|
||||
* SuspendedFilmStripOnlyOverlay component's property types.
|
||||
*
|
||||
* @static
|
||||
*/
|
||||
static propTypes = {
|
||||
/**
|
||||
* The function to translate human-readable text.
|
||||
*
|
||||
* @public
|
||||
* @type {Function}
|
||||
*/
|
||||
t: React.PropTypes.func
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<FilmStripOnlyOverlayFrame isLightOverlay = { true }>
|
||||
<div className = 'inlay-filmstrip-only__container'>
|
||||
<div className = 'inlay-filmstrip-only__title'>
|
||||
{ t('suspendedoverlay.title') }
|
||||
</div>
|
||||
<div className = 'inlay-filmstrip-only__text'>
|
||||
{ translateToHTML(t, 'suspendedoverlay.text') }
|
||||
</div>
|
||||
</div>
|
||||
<ReloadButton textKey = 'suspendedoverlay.rejoinKeyTitle' />
|
||||
</FilmStripOnlyOverlayFrame>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(SuspendedFilmStripOnlyOverlay);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user