Compare commits

..

1 Commits

Author SHA1 Message Date
Hristo Terezov
4d1ed86e49 ref(Thumbnail): Create React component. 2020-11-12 11:10:04 -06:00
119 changed files with 3191 additions and 3337 deletions

View File

@@ -53,7 +53,6 @@ import {
updateDeviceList
} from './react/features/base/devices';
import {
browser,
isFatalJitsiConnectionError,
JitsiConferenceErrors,
JitsiConferenceEvents,
@@ -494,9 +493,9 @@ export default {
JitsiMeetJS.mediaDevices.addEventListener(
JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN,
browserName =>
browser =>
APP.store.dispatch(
mediaPermissionPromptVisibilityChanged(true, browserName))
mediaPermissionPromptVisibilityChanged(true, browser))
);
let tryCreateLocalTracks;
@@ -1606,10 +1605,8 @@ export default {
*/
async _createPresenterStreamEffect(height = null, cameraDeviceId = null) {
if (!this.localPresenterVideo) {
const camera = cameraDeviceId ?? getUserSelectedCameraDeviceId(APP.store.getState());
try {
this.localPresenterVideo = await createLocalPresenterTrack({ cameraDeviceId: camera }, height);
this.localPresenterVideo = await createLocalPresenterTrack({ cameraDeviceId }, height);
} catch (err) {
logger.error('Failed to create a camera track for presenter', err);
@@ -1650,38 +1647,38 @@ export default {
// Create a new presenter track and apply the presenter effect.
if (!this.localPresenterVideo && !mute) {
const { height, width } = this.localVideo.track.getSettings() ?? this.localVideo.track.getConstraints();
const isPortrait = height >= width;
let { aspectRatio, height } = this.localVideo.track.getSettings();
const { width } = this.localVideo.track.getSettings();
let desktopResizeConstraints = {};
let resizeDesktopStream = false;
const DESKTOP_STREAM_CAP = 720;
// Config.js setting for resizing high resolution desktop tracks to 720p when presenter is turned on.
const resizeEnabled = config.videoQuality && config.videoQuality.resizeDesktopForPresenter;
const highResolutionTrack
= (isPortrait && width > DESKTOP_STREAM_CAP) || (!isPortrait && height > DESKTOP_STREAM_CAP);
// Determine the constraints if the desktop track needs to be resized.
// Resizing is needed when the resolution cannot be determined or when
// the window is bigger than 720p.
if (height && width) {
aspectRatio = aspectRatio ?? (width / height).toPrecision(4);
const advancedConstraints = [ { aspectRatio } ];
const isPortrait = height >= width;
// Resizing the desktop track for presenter is causing blurriness of the desktop share on chrome.
// Disable resizing by default, enable it only when config.js setting is enabled.
// Firefox doesn't return width and height for desktop tracks. Therefore, track needs to be resized
// for creating the canvas for presenter.
const resizeDesktopStream = browser.isFirefox() || (highResolutionTrack && resizeEnabled);
if (resizeDesktopStream) {
let desktopResizeConstraints = {};
if (height && width) {
const advancedConstraints = [ { aspectRatio: (width / height).toPrecision(4) } ];
const constraint = isPortrait ? { width: DESKTOP_STREAM_CAP } : { height: DESKTOP_STREAM_CAP };
advancedConstraints.push(constraint);
desktopResizeConstraints.advanced = advancedConstraints;
} else {
desktopResizeConstraints = {
width: 1280,
height: 720
};
// Determine which dimension needs resizing and resize only that side
// keeping the aspect ratio same as before.
if (isPortrait && width > DESKTOP_STREAM_CAP) {
resizeDesktopStream = true;
advancedConstraints.push({ width: DESKTOP_STREAM_CAP });
} else if (!isPortrait && height > DESKTOP_STREAM_CAP) {
resizeDesktopStream = true;
advancedConstraints.push({ height: DESKTOP_STREAM_CAP });
}
// Apply the contraints on the desktop track.
desktopResizeConstraints.advanced = advancedConstraints;
} else {
resizeDesktopStream = true;
desktopResizeConstraints = {
width: 1280,
height: 720
};
}
if (resizeDesktopStream) {
try {
await this.localVideo.track.applyConstraints(desktopResizeConstraints);
} catch (err) {
@@ -1689,22 +1686,20 @@ export default {
return;
}
height = this.localVideo.track.getSettings().height ?? DESKTOP_STREAM_CAP;
}
const trackHeight = resizeDesktopStream
? this.localVideo.track.getSettings().height ?? DESKTOP_STREAM_CAP
: height;
const defaultCamera = getUserSelectedCameraDeviceId(APP.store.getState());
let effect;
try {
effect = await this._createPresenterStreamEffect(trackHeight);
effect = await this._createPresenterStreamEffect(height,
defaultCamera);
} catch (err) {
logger.error('Failed to unmute Presenter Video', err);
logger.error('Failed to unmute Presenter Video');
maybeShowErrorDialog(err);
return;
}
// Replace the desktop track on the peerconnection.
try {
await this.localVideo.setEffect(effect);
APP.store.dispatch(setVideoMuted(mute, MEDIA_TYPE.PRESENTER));
@@ -1997,7 +1992,6 @@ export default {
formattedDisplayName
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
});
APP.UI.changeDisplayName(id, formattedDisplayName);
}
);
room.on(
@@ -2060,10 +2054,7 @@ export default {
});
room.on(JitsiConferenceEvents.KICKED, participant => {
APP.UI.hideStats();
APP.store.dispatch(kickedOut(room, participant));
// FIXME close
});
room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
@@ -2396,11 +2387,6 @@ export default {
APP.keyboardshortcut.init();
APP.store.dispatch(conferenceJoined(room));
const displayName
= APP.store.getState()['features/base/settings'].displayName;
APP.UI.changeDisplayName('localVideoContainer', displayName);
},
/**
@@ -2898,10 +2884,6 @@ export default {
APP.store.dispatch(updateSettings({
displayName: formattedNickname
}));
if (room) {
APP.UI.changeDisplayName(id, formattedNickname);
}
},
/**

View File

@@ -275,13 +275,9 @@ var config = {
// // at least 360 pixels tall. If the thumbnail height reaches 720 pixels then the application will switch to
// // the high quality.
// minHeightForQualityLvl: {
// 360: 'standard',
// 360: 'standard,
// 720: 'high'
// },
//
// // Provides a way to resize the desktop track to 720p (if it is greater than 720p) before creating a canvas
// // for the presenter mode (camera picture-in-picture mode with screenshare).
// resizeDesktopForPresenter: false
// }
// },
// // Options for the recording limit notification.
@@ -363,12 +359,17 @@ var config = {
// Default language for the user interface.
// defaultLanguage: 'en',
// Disables profile and the edit of all fields from the profile settings (display name and email)
// disableProfile: false,
// If true all users without a token will be considered guests and all users
// with token will be considered non-guests. Only guests will be allowed to
// edit their profile.
enableUserRolesBasedOnToken: false,
// Whether or not some features are checked based on token.
// enableFeaturesBasedOnToken: false,
// Enable lock room for all moderators, even when userRolesBasedOnToken is enabled and participants are guests.
// lockRoomGuestEnabled: false,
// When enabled the password used for locking a room is restricted to up to the number of digits specified
// roomPasswordNumberOfDigits: 10,
// default: roomPasswordNumberOfDigits: false,

View File

@@ -28,6 +28,9 @@ body {
overflow: hidden;
color: $defaultColor;
background: $defaultBackground;
&.filmstrip-only {
background: transparent;
}
}
/**
@@ -67,6 +70,16 @@ body {
cursor: pointer;
}
/**
* AtlasKitThemeProvider sets a background color on an app-wrapping div, thereby
* preventing transparency in filmstrip-only mode. The selector chosen to
* override this behavior is specific to where the AtlasKitThemeProvider might
* be placed within the app hierarchy.
*/
.filmstrip-only #react > .ckAJgx {
background: transparent;
}
p {
margin: 0;
}
@@ -201,74 +214,3 @@ form {
background: rgba(0, 0, 0, .5);
border-radius: 4px;
}
.desktop-browser {
@media only screen and (max-width: $smallScreen) {
.watermark {
width: 20%;
height: 20%;
}
.new-toolbox {
.toolbox-content {
.button-group-center, .button-group-left, .button-group-right {
.toolbox-button {
.toolbox-icon {
width: 28px;
height: 28px;
svg {
width: 18px;
height: 18px;
}
}
&:nth-child(2) {
.toolbox-icon {
width: 30px;
height: 30px;
}
}
}
}
}
}
}
@media only screen and (max-width: $verySmallScreen) {
#videoResolutionLabel {
display: none;
}
.vertical-filmstrip .filmstrip {
display: none;
}
.new-toolbox {
.toolbox-content {
.button-group-center, .button-group-left, .button-group-right {
.settings-button-small-icon {
display: none;
}
.toolbox-button {
.toolbox-icon {
width: 18px;
height: 18px;
svg {
width: 12px;
height: 12px;
}
}
&:nth-child(2) {
.toolbox-icon {
width: 20px;
height: 20px;
}
}
}
}
}
}
.chrome-extension-banner {
display: none;
}
}
}

View File

@@ -27,4 +27,84 @@
font-size: 50px;
}
&-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;
}
}
&__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 {
height: 100%;
position: relative;
> img {
height: 100%;
}
}
&__icon-background {
background: $inlayIconBg;
opacity: 0.6;
position: absolute;
width: 100%;
height: 100%;
top: 0px;
}
}
}

View File

@@ -7,8 +7,9 @@
display: flex;
flex-direction: column;
position: relative;
overflow: auto;
width: 100%;
height: 100%;
overflow: auto;
.meetings-list-empty {
text-align: center;
@@ -19,34 +20,11 @@
flex-direction: column;
.description {
color: #2f3237;
font-size: 14px;
line-height: 18px;
margin-bottom: 16px;
max-width: 436px;
font-size: 16px;
padding: 20px;
}
}
.meetings-list-empty-image {
text-align: center;
margin: 24px 0 20px 0;
}
.meetings-list-empty-button {
align-items: center;
color: #0163FF;
cursor: pointer;
display: flex;
font-size: 14px;
line-height: 18px;
margin: 24px 0 32px 0;
}
.meetings-list-empty-icon {
display: inline-block;
margin-right: 8px;
}
.button {
background: #0074E0;
border-radius: 4px;
@@ -54,7 +32,7 @@
display: flex;
justify-content: center;
align-items: center;
padding: 8px;
padding: 5px 10px;
cursor: pointer;
}
@@ -65,13 +43,12 @@
}
.item {
background: #fff;
background: rgba(255,255,255,0.50);
box-sizing: border-box;
border-radius: 4px;
display: inline-flex;
margin: 4px 4px 0 4px;
min-height: 60px;
width: calc(100% - 8px);
margin-top: 5px;
min-height: 92px;
width: 100%;
word-break: break-word;
display: flex;
flex-direction: row;
@@ -84,41 +61,37 @@
.left-column {
display: flex;
flex-direction: column;
width: 140px;
flex-grow: 0;
padding-left: 16px;
padding-top: 13px;
padding-left: 30px;
padding-top: 25px;
.date {
font-weight: bold;
padding-bottom: 5px;
}
}
.right-column {
display: flex;
flex-direction: column;
flex-grow: 1;
padding-left: 16px;
padding-top: 13px;
position: relative;
}
padding-left: 30px;
padding-top: 25px;
.title {
font-size: 12px;
font-weight: 600;
line-height: 16px;
padding-bottom: 4px;
.title {
font-size: 16px;
font-weight: bold;
padding-bottom: 5px;
}
.subtitle {
color: #5E6D7A;
font-weight: normal;
font-size: 12px;
line-height: 16px;
}
.actions {
display: flex;
align-items: center;
justify-content: center;
flex-grow: 0;
margin-right: 16px;
padding-right: 30px;
}
&.with-click-handler {
@@ -126,7 +99,7 @@
}
&.with-click-handler:hover {
background-color: #c7ddff;
background-color: #75A7E7;
}
.add-button {
@@ -147,20 +120,4 @@
display: block
}
}
.delete-meeting {
display: none;
margin-right: 16px;
position: absolute;
&> svg {
fill: #0074e0;
}
}
.item:hover {
.delete-meeting {
display: block;
}
}
}

View File

@@ -1,67 +1,70 @@
@media only screen and (max-width: $verySmallScreen) {
.welcome {
display: block;
@media only screen and (max-width: $smallScreen) {
.watermark {
width: 20%;
height: 20%;
}
#enter_room {
position: relative;
height: 42px;
.new-toolbox {
.toolbox-content {
.button-group-center, .button-group-left, .button-group-right {
.toolbox-button {
.toolbox-icon {
width: 28px;
height: 28px;
svg {
width: 18px;
height: 18px;
}
}
.welcome-page-button {
font-size: 16px;
left: 0;
position: absolute;
top: 68px;
text-align: center;
width: 100%;
}
}
.header {
background-color: #002637;
#enter_room {
.enter-room-input-container {
padding-right: 0;
}
.warning-without-link,
.warning-with-link {
top: 120px;
&:nth-child(2) {
.toolbox-icon {
width: 30px;
height: 30px;
}
}
}
}
}
.welcome-tabs {
display: none;
}
.header-text-title {
text-align: center;
}
.welcome-cards-container {
padding: 0;
}
&.without-content {
.header {
height: 100%;
}
}
#moderated-meetings {
display: none;
}
.welcome-footer-row-block {
display: block;
}
.welcome-badge {
margin-right: 16px;
}
.welcome-footer {
display: none;
}
}
}
@media only screen and (max-width: $verySmallScreen) {
#videoResolutionLabel {
display: none;
}
.desktop-browser {
.vertical-filmstrip .filmstrip {
display: none;
}
}
.new-toolbox {
.toolbox-content {
.button-group-center, .button-group-left, .button-group-right {
.settings-button-small-icon {
display: none;
}
.toolbox-button {
.toolbox-icon {
width: 18px;
height: 18px;
svg {
width: 12px;
height: 12px;
}
}
&:nth-child(2) {
.toolbox-icon {
width: 20px;
height: 20px;
}
}
}
}
}
}
.chrome-extension-banner {
display: none;
}
}

View File

@@ -161,47 +161,71 @@ $unsupportedDesktopBrowserTextFontSize: 21px;
/**
* The size of the default watermark.
*/
$watermarkWidth: 71px;
$watermarkHeight: 32px;
$watermarkWidth: 186px;
$watermarkHeight: 74px;
$welcomePageWatermarkWidth: 71px;
$welcomePageWatermarkHeight: 32px;
$welcomePageWatermarkWidth: 186px;
$welcomePageWatermarkHeight: 74px;
/**
* Welcome page variables.
*/
$welcomePageDescriptionColor: #fff;
$welcomePageFontFamily: inherit;
$welcomePageBackground: none;
$welcomePageBackground: linear-gradient(-90deg, #1251AE 0%, #0074FF 50%, #1251AE 100%);
$welcomePageTitleColor: #fff;
$welcomePageHeaderBackground: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), url('../images/welcome-background.png');
$welcomePageHeaderBackgroundPosition: center;
$welcomePageHeaderBackground: none;
$welcomePageHeaderBackgroundSmall: none;
$welcomePageHeaderBackgroundPosition: none;
$welcomePageHeaderBackgroundRepeat: none;
$welcomePageHeaderBackgroundSize: cover;
$welcomePageHeaderBackgroundSize: none;
$welcomePageHeaderPaddingBottom: 0px;
$welcomePageHeaderTitleMaxWidth: initial;
$welcomePageHeaderTextAlign: center;
$welcomePageHeaderMinHeight: fit-content;
$welcomePageHeaderContainerDisplay: flex;
$welcomePageHeaderContainerMargin: 104px 32px 0 32px;
$welcomePageHeaderTextMarginTop: 35px;
$welcomePageHeaderTextMarginBottom: 35px;
$welcomePageHeaderTextDisplay: flex;
$welcomePageHeaderTextWidth: 650px;
$welcomePageHeaderTextTitleMarginBottom: 0;
$welcomePageHeaderTextTitleFontSize: 42px;
$welcomePageHeaderTextTitleFontWeight: normal;
$welcomePageHeaderTextTitleLineHeight: 50px;
$welcomePageHeaderTextTitleMarginBottom: 16px;
$welcomePageHeaderTextTitleFontSize: 2.5rem;
$welcomePageHeaderTextTitleFontWeight: 500;
$welcomePageHeaderTextTitleLineHeight: 1.18;
$welcomePageHeaderTextTitleOpacity: 1;
$welcomePageHeaderTextDescriptionDisplay: inherit;
$welcomePageHeaderTextDescriptionFontSize: 1rem;
$welcomePageHeaderTextDescriptionFontWeight: 400;
$welcomePageHeaderTextDescriptionLineHeight: 24px;
$welcomePageHeaderTextDescriptionMarginBottom: 20px;
$welcomePageHeaderTextDescriptionAlignSelf: inherit;
$welcomePageEnterRoomDisplay: flex;
$welcomePageEnterRoomWidth: calc(100% - 32px);
$welcomePageEnterRoomPadding: 4px;
$welcomePageEnterRoomMargin: 0 auto;
$welcomePageEnterRoomWidth: 680px;
$welcomePageEnterRoomPadding: 25px 30px;
$welcomePageEnterRoomBorderRadius: 0px;
$welcomePageEnterRoomInputContainerPadding: 0 8px 5px 0px;
$welcomePageEnterRoomInputContainerBorderWidth: 0px 0px 2px 0px;
$welcomePageEnterRoomInputContainerBorderStyle: solid;
$welcomePageEnterRoomInputContainerBorderImage: linear-gradient(to right, #dee1e6, #fff) 1;
$welcomePageEnterRoomTitleDisplay: inherit;
$welcomePageTabContainerDisplay: flex;
$welcomePageTabContentDisplay: inherit;
$welcomePageTabButtonsDisplay: flex;
$welcomePageTabDisplay: block;
$welcomePageButtonWidth: 51px;
$welcomePageButtonMinWidth: inherit;
$welcomePageButtonFontSize: 14px;
$welcomePageButtonHeight: 35px;
$welcomePageButtonFontWeight: inherit;
$welcomePageButtonBorderRadius: 4px;
$welcomePageButtonLineHeight: 35px;
/**
* Deep-linking page variables.
*/

View File

@@ -5,7 +5,6 @@ body.welcome-page {
.welcome {
background-image: $welcomePageBackground;
background-color: #fff;
display: flex;
flex-direction: column;
font-family: $welcomePageFontFamily;
@@ -19,15 +18,21 @@ body.welcome-page {
background-repeat: $welcomePageHeaderBackgroundRepeat;
background-size: $welcomePageHeaderBackgroundSize;
padding-bottom: $welcomePageHeaderPaddingBottom;
background-color: #131519;
height: 400px;
align-items: center;
display: flex;
flex-direction: column;
min-height: $welcomePageHeaderMinHeight;
overflow: hidden;
position: relative;
text-align: center;
.header-container {
display: $welcomePageHeaderContainerDisplay;
.header-text {
display: $welcomePageHeaderTextDisplay;
flex-direction: column;
margin: $welcomePageHeaderContainerMargin;
margin-top: $watermarkHeight + $welcomePageHeaderTextMarginTop;
margin-bottom: $welcomePageHeaderTextMarginBottom;
max-width: calc(100% - 40px);
width: $welcomePageHeaderTextWidth;
z-index: $zindex2;
}
@@ -37,52 +42,50 @@ body.welcome-page {
font-weight: $welcomePageHeaderTextTitleFontWeight;
line-height: $welcomePageHeaderTextTitleLineHeight;
margin-bottom: $welcomePageHeaderTextTitleMarginBottom;
max-width: $welcomePageHeaderTitleMaxWidth;
opacity: $welcomePageHeaderTextTitleOpacity;
text-align: $welcomePageHeaderTextAlign;
}
.header-text-subtitle {
color: #fff;
font-size: 20px;
font-weight: 600;
line-height: 26px;
margin: 16px 0 32px 0;
text-align: $welcomePageHeaderTextAlign;
.header-text-description {
display: $welcomePageHeaderTextDescriptionDisplay;
color: $welcomePageDescriptionColor;
font-size: $welcomePageHeaderTextDescriptionFontSize;
font-weight: $welcomePageHeaderTextDescriptionFontWeight;
line-height: $welcomePageHeaderTextDescriptionLineHeight;
margin-bottom: $welcomePageHeaderTextDescriptionMarginBottom;
align-self: $welcomePageHeaderTextDescriptionAlignSelf;
}
#enter_room {
display: $welcomePageEnterRoomDisplay;
align-items: center;
max-width: 480px;
max-width: calc(100% - 40px);
width: $welcomePageEnterRoomWidth;
z-index: $zindex2;
background-color: #fff;
padding: $welcomePageEnterRoomPadding;
border-radius: 4px;
margin: $welcomePageEnterRoomMargin;
border-radius: $welcomePageEnterRoomBorderRadius;
.enter-room-input-container {
width: 100%;
padding: $welcomePageEnterRoomInputContainerPadding;
text-align: left;
color: #253858;
flex-grow: 1;
height: fit-content;
padding-right: 4px;
position: relative;
.enter-room-title {
display: $welcomePageEnterRoomTitleDisplay;
font-size: 18px;
font-weight: bold;
padding-bottom: 5px;
}
.enter-room-input {
border: 0;
background: #fff;
border-width: $welcomePageEnterRoomInputContainerBorderWidth;
border-style: $welcomePageEnterRoomInputContainerBorderStyle;
border-image: $welcomePageEnterRoomInputContainerBorderImage;
display: inline-block;
height: 50px;
width: 100%;
font-size: 14px;
padding-left: 10px;
&:focus {
outline: auto 2px #005fcc;
}
}
.insecure-room-name-warning {
@@ -106,28 +109,16 @@ body.welcome-page {
}
}
.warning-without-link {
position: absolute;
top: 44px;
left: -10px;
}
.warning-with-link {
position: absolute;
top: 84px;
}
}
#moderated-meetings {
max-width: calc(100% - 40px);
padding: 16px 0 39px 0;
margin: $welcomePageEnterRoomMargin;
width: $welcomePageEnterRoomWidth;
p {
color: $welcomePageDescriptionColor;
text-align: $welcomePageHeaderTextAlign;
text-align: left;
a {
color: inherit;
@@ -135,70 +126,76 @@ body.welcome-page {
}
}
}
}
.tab-container {
font-size: 16px;
position: relative;
text-align: left;
display: $welcomePageTabContainerDisplay;
flex-direction: column;
.tab-content{
display: $welcomePageTabContentDisplay;
height: 250px;
margin: 5px 0px;
overflow: hidden;
flex-grow: 1;
.tab-container {
font-size: 16px;
position: relative;
}
text-align: left;
min-height: 354px;
width: 710px;
background: #75A7E7;
display: $welcomePageTabContainerDisplay;
flex-direction: column;
.tab-buttons {
background-color: #c7ddff;
border-radius: 6px;
color: #0163FF;
font-size: 14px;
line-height: 18px;
margin: 4px;
display: $welcomePageTabButtonsDisplay;
.tab {
background-color: #c7ddff;
border-radius: 7px;
cursor: pointer;
display: $welcomePageTabDisplay;
.tab-content{
display: $welcomePageTabContentDisplay;
margin: 5px 0px;
overflow: hidden;
flex-grow: 1;
margin: 2px;
padding: 7px 0;
text-align: center;
position: relative;
&.selected {
background-color: #FFF;
> * {
position: absolute;
}
}
.tab-buttons {
font-size: 18px;
color: #FFFFFF;
display: $welcomePageTabButtonsDisplay;
flex-grow: 0;
flex-direction: row;
min-height: 54px;
width: 100%;
.tab {
display: $welcomePageTabDisplay;
text-align: center;
background: rgba(9,30,66,0.37);
height: 55px;
line-height: 54px;
flex-grow: 1;
cursor: pointer;
&.selected, &:hover {
background: rgba(9,30,66,0.71);
}
&:last-child {
margin-left: 1px;
}
}
}
}
}
.welcome-page-button {
border: 0;
font-size: 14px;
width: $welcomePageButtonWidth;
min-width: $welcomePageButtonMinWidth;
height: $welcomePageButtonHeight;
font-size: $welcomePageButtonFontSize;
font-weight: $welcomePageButtonFontWeight;
background: #0074E0;
border-radius: 3px;
border-radius: $welcomePageButtonBorderRadius;
color: #FFFFFF;
text-align: center;
vertical-align: middle;
line-height: $welcomePageButtonLineHeight;
cursor: pointer;
padding: 16px 20px;
&:focus-within {
outline: auto 2px #022e61;
}
}
.welcome-page-settings {
background: rgba(255, 255, 255, 0.38);
border-radius: 3px;
color: $welcomePageDescriptionColor;
padding: 4px;
position: absolute;
top: 32px;
right: 32px;
@@ -220,84 +217,4 @@ body.welcome-page {
height: $welcomePageWatermarkHeight;
}
}
&.without-content {
.welcome-card {
min-width: 500px;
max-width: 580px;
}
}
.welcome-cards-container {
color:#131519;
padding-top: 40px;
}
.welcome-card-row {
display: flex;
justify-content: center;
padding: 0 32px;
}
.welcome-card-text {
padding: 32px;
}
.welcome-card {
width: 49%;
border-radius: 8px;
&--dark {
background: #444447;
color: #fff;
}
&--blue {
background: #D5E5FF;
}
&--grey {
background: #F2F3F4;
}
&--shadow {
box-shadow: 0px 4px 30px rgba(0, 0, 0, 0.15);
}
}
.welcome-footer {
background: #131519;
color: #fff;
margin-top: 40px;
position: relative;
}
.welcome-footer-centered {
max-width: 688px;
margin: 0 auto;
}
.welcome-footer-padded {
padding: 0px 16px;
}
.welcome-footer-row-block {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #424447;
&:last-child {
border-bottom: none;
}
}
.welcome-footer--row-1 {
padding: 40px 0 24px 0;
}
.welcome-footer-row-1-text {
max-width: 200px;
margin-right: 16px;
}
}

View File

@@ -67,6 +67,20 @@
}
}
/**
* Style the filmstrip videos in filmstrip-only mode.
*/
&__videos-filmstripOnly {
margin-top: auto;
margin-bottom: auto;
.filmstrip__videos {
&#filmstripLocalVideo {
bottom: 0px;
}
}
}
.remote-videos-container {
transition: opacity 1s;
}

View File

@@ -145,6 +145,26 @@
}
}
/**
* Override other styles to support vertical filmstrip mode.
*/
.filmstrip-only .vertical-filmstrip {
.filmstrip {
flex-direction: row-reverse;
}
.filmstrip__videos-filmstripOnly {
margin-top: auto;
margin-bottom: auto;
height: 100%;
}
.filmstrip__videos {
&#filmstripLocalVideo {
bottom: 0px;
}
}
}
/**
* Workarounds for Edge and Firefox not handling scrolling properly with
* flex-direction: column-reverse. The remove videos in filmstrip should

View File

@@ -1,4 +1,9 @@
.video-quality-dialog {
.hide-warning {
height: 0;
visibility: hidden;
}
.video-quality-dialog-title {
margin-bottom: 10px;
}
@@ -104,6 +109,30 @@
word-spacing: unset;
}
}
&.video-not-supported {
.video-quality-dialog-labels {
color: gray;
}
.video-quality-dialog-slider {
@mixin sliderTrackDisabledStyles() {
background: rgba(14, 22, 36, 0.1);
}
&::-ms-track {
@include sliderTrackDisabledStyles();
}
&::-moz-range-track {
@include sliderTrackDisabledStyles();
}
&::-webkit-slider-runnable-track {
@include sliderTrackDisabledStyles();
}
}
}
}
.modal-dialog-form {

View File

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

View File

@@ -41,6 +41,7 @@ $overlayButtonBg: #0074E0;
* Color variables
**/
$defaultBackground: #474747;
$filmstripOnlyOverlayBg: #000;
$reloadProgressBarBg: #0074E0;
/**
@@ -58,6 +59,10 @@ $dialogErrorText: #344563;
**/
$inlayColorBg: lighten($defaultBackground, 20%);
$inlayBorderColor: lighten($baseLight, 10%);
$inlayIconBg: #000;
$inlayIconColor: #fff;
$inlayFilmstripOnlyColor: #474747;
$inlayFilmstripOnlyBg: #fff;
// Main controls
$placeHolderColor: #a7a7a7;

View File

@@ -13,7 +13,9 @@
height: 180,
parentNode: undefined,
configOverwrite: {},
interfaceConfigOverwrite: {}
interfaceConfigOverwrite: {
filmStripOnly: true
}
}
var api = new JitsiMeetExternalAPI(domain, options);
</script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,21 +0,0 @@
<svg width="68" height="72" viewBox="0 0 68 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="5.64514" width="65.3548" height="65.3548" rx="7" stroke="#A4B8D1" stroke-width="2"/>
<rect y="23.2258" width="67.3548" height="2.0213" fill="#A4B8D1"/>
<rect x="14.5161" width="2.32258" height="14.5161" fill="#A4B8D1"/>
<rect x="11.6129" y="12.1935" width="8.12903" height="2.32258" fill="#A4B8D1"/>
<rect x="50.5161" width="2.32258" height="14.5161" fill="#A4B8D1"/>
<rect x="47.6129" y="12.1935" width="8.12903" height="2.32258" fill="#A4B8D1"/>
<circle cx="24.387" cy="37.7419" r="2.32258" fill="#A4B8D1"/>
<circle cx="33.6774" cy="37.7419" r="2.32258" fill="#A4B8D1"/>
<circle cx="42.9677" cy="37.7419" r="2.32258" fill="#A4B8D1"/>
<circle cx="52.258" cy="37.7419" r="2.32258" fill="#A4B8D1"/>
<circle cx="24.387" cy="47.0322" r="2.32258" fill="#A4B8D1"/>
<circle cx="15.0968" cy="47.0322" r="2.32258" fill="#A4B8D1"/>
<circle cx="33.6774" cy="47.0322" r="2.32258" fill="#A4B8D1"/>
<circle cx="42.9677" cy="47.0322" r="2.32258" fill="#A4B8D1"/>
<circle cx="24.387" cy="56.3226" r="2.32258" fill="#A4B8D1"/>
<circle cx="15.0968" cy="56.3226" r="2.32258" fill="#A4B8D1"/>
<circle cx="33.6774" cy="56.3226" r="2.32258" fill="#A4B8D1"/>
<circle cx="42.9677" cy="56.3226" r="2.32258" fill="#A4B8D1"/>
<circle cx="52.258" cy="47.0322" r="2.32258" fill="#A4B8D1"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

BIN
images/watermark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

View File

@@ -182,7 +182,6 @@
<!--#include virtual="title.html" -->
<!--#include virtual="plugin.head.html" -->
<!--#include virtual="static/welcomePageAdditionalContent.html" -->
<!--#include virtual="static/welcomePageAdditionalCard.html" -->
<!--#include virtual="static/settingsToolbarAdditionalContent.html" -->
</head>
<body>

View File

@@ -46,9 +46,9 @@ var interfaceConfig = {
DEFAULT_BACKGROUND: '#474747',
DEFAULT_LOCAL_DISPLAY_NAME: 'me',
DEFAULT_LOGO_URL: 'images/watermark.svg',
DEFAULT_LOGO_URL: 'images/watermark.png',
DEFAULT_REMOTE_DISPLAY_NAME: 'Fellow Jitster',
DEFAULT_WELCOME_PAGE_LOGO_URL: 'images/watermark.svg',
DEFAULT_WELCOME_PAGE_LOGO_URL: 'images/watermark.png',
DISABLE_DOMINANT_SPEAKER_INDICATOR: false,
@@ -86,9 +86,7 @@ var interfaceConfig = {
*/
DISABLE_VIDEO_BACKGROUND: false,
DISPLAY_WELCOME_FOOTER: true,
DISPLAY_WELCOME_PAGE_ADDITIONAL_CARD: false,
DISPLAY_WELCOME_PAGE_CONTENT: false,
DISPLAY_WELCOME_PAGE_CONTENT: true,
DISPLAY_WELCOME_PAGE_TOOLBAR_ADDITIONAL_CONTENT: false,
ENABLE_DIAL_OUT: true,
@@ -97,6 +95,11 @@ var interfaceConfig = {
FILM_STRIP_MAX_HEIGHT: 120,
/**
* Whether to only show the filmstrip (and hide the toolbar).
*/
filmStripOnly: false,
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
/**
@@ -133,21 +136,6 @@ var interfaceConfig = {
*/
MOBILE_APP_PROMO: true,
/**
* Specify custom URL for downloading android mobile app.
*/
MOBILE_DOWNLOAD_LINK_ANDROID: 'https://play.google.com/store/apps/details?id=org.jitsi.meet',
/**
* Specify custom URL for downloading f droid app.
*/
MOBILE_DOWNLOAD_LINK_F_DROID: 'https://f-droid.org/en/packages/org.jitsi.meet/',
/**
* Specify URL for downloading ios mobile app.
*/
MOBILE_DOWNLOAD_LINK_IOS: 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905',
NATIVE_APP_NAME: 'Jitsi Meet',
// Names of browsers which should show a warning stating the current browser
@@ -181,6 +169,7 @@ var interfaceConfig = {
SHOW_JITSI_WATERMARK: true,
SHOW_POWERED_BY: false,
SHOW_PROMOTIONAL_CLOSE_PAGE: false,
SHOW_WATERMARK_FOR_GUESTS: true, // if watermark is disabled by default, it can be shown only for guests
/*
* If indicated some of the error dialogs may point to the support URL for
@@ -234,12 +223,27 @@ var interfaceConfig = {
*/
VIDEO_QUALITY_LABEL_DISABLED: false,
/**
* When enabled, the kick participant button will not be presented for users without a JWT
*/
// HIDE_KICK_BUTTON_FOR_GUESTS: false,
/**
* How many columns the tile view can expand to. The respected range is
* between 1 and 5.
*/
// TILE_VIEW_MAX_COLUMNS: 5,
/**
* Specify custom URL for downloading android mobile app.
*/
// MOBILE_DOWNLOAD_LINK_ANDROID: 'https://play.google.com/store/apps/details?id=org.jitsi.meet',
/**
* Specify URL for downloading ios mobile app.
*/
// MOBILE_DOWNLOAD_LINK_IOS: 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905',
/**
* Specify Firebase dynamic link properties for the mobile apps.
*/

View File

@@ -27,12 +27,12 @@
"enGB": "Inglese (Regno Unito)",
"da": "Danese",
"ca": "Catalano",
"zhTW": "Cinese (Taiwan)",
"nl": "Olandese",
"hu": "Ungaro",
"hr": "Croato",
"frCA": "Francese (Canada)",
"fi": "Finlandese",
"et": "Etiope",
"esUS": "Spagnolo (USA)"
"zhTW": "",
"nl": "",
"hu": "",
"hr": "",
"frCA": "",
"fi": "",
"et": "",
"esUS": ""
}

View File

@@ -1,48 +1,27 @@
{
"en": "英語",
"af": "アフリカーンス語",
"ar": "アラビア語",
"az": "アゼルバイジャン語",
"bg": "ブルガリア語",
"ca": "カタルーニャ語",
"cs": "チェコ語",
"da": "デンマーク語",
"de": "ドイツ語",
"el": "ギリシア語",
"enGB": "英語 (英国)",
"eo": "エスペラント語",
"es": "スペイン語",
"esUS": "スペイン語 (ラテンアメリカ)",
"et": "エストニア語",
"eu": "バスク語",
"fi": "フィンランド語",
"fr": "フランス語",
"frCA": "フランス語 (カナダ)",
"he": "ヘブライ語",
"mr": "マラーティー語",
"hr": "クロアチア語",
"hu": "ハンガリー語",
"hy": "アルメニア語",
"id": "インドネシア語",
"it": "イタリア語",
"ja": "日本語",
"kab": "カビル語",
"ko": "韓国語",
"lt": "リトアニア語",
"nl": "オランダ語",
"nb": "ノルウェー語 (ブークモール)",
"oc": "オック語",
"pl": "ポーランド語",
"ptBR": "ポルトガル語 (ブラジル)",
"ru": "ロシア語",
"ro": "ルーマニア語",
"sc": "サルデーニャ語",
"sk": "スロバキア語",
"sl": "スロベニア語",
"sr": "セルビア語",
"sv": "スウェーデン語",
"th": "タイ語",
"tr": "トルコ語",
"uk": "ウクライナ語",
"vi": "ベトナム語",
"zhCN": "中国語 (中国)",
"zhTW": "中国語 (台湾)"
}
"zhCN": "中国語 (中国)"
}

View File

@@ -1,50 +1,48 @@
{
"en": "Anglés",
"af": "Afrikaans",
"ar": "Arabi",
"bg": "Bulgar",
"ca": "Catalan",
"cs": "Chèc",
"da": "Danés",
"de": "Aleman",
"el": "Grèc",
"enGB": "Anglés (Reialme Unit)",
"eo": "Esperanto",
"es": "Castelhan",
"esUS": "Espanhòl (America latina)",
"et": "Estonian",
"eu": "Basc",
"fi": "Finés",
"fr": "Francés",
"frCA": "Francés (Canadian)",
"he": "Ebrèu",
"mr":"Marathi",
"hr": "Croat",
"hu": "Ongrés",
"hy": "Armenian",
"id": "Indonesian",
"it": "Italian",
"ja": "Japonés",
"kab": "Cabil",
"ko": "Corean",
"lt": "Lituanian",
"nl": "Neerlandés",
"oc": "Occitan",
"pl": "Polonés",
"ptBR": "Portugués (Brasil)",
"ru": "Rus",
"ro": "Romanian",
"sc": "Sarde",
"sk": "Eslovac",
"sl": "Eslovèn",
"sr": "Sèrbe",
"sv": "Suedés",
"th": "Tai",
"tr": "Turc",
"uk": "Ucraïnian",
"vi": "Vietnamian",
"zhCN": "Chinés (China)",
"zhTW": "Chinés (Taiwan)"
"zhTW": "Chinés (Taiwan)",
"et": "Estonian",
"da": "Danés",
"uk": "Ucraïnian",
"th": "Tai",
"sk": "Eslovac",
"sc": "Sarde",
"lt": "Lituanian",
"id": "Indonesian",
"he": "Ebrèu",
"eu": "Basc",
"mr": "Marathi",
"sl": "Eslovèn",
"ro": "Romanian",
"ar": "Arabi"
}

View File

@@ -99,11 +99,9 @@
},
"connectionindicator": {
"address": "Adresse:",
"audio_ssrc": "Audio-SSRC:",
"bandwidth": "Geschätzte Bandbreite:",
"bitrate": "Bitrate:",
"bridgeCount": "Serverzahl: ",
"codecs": "Codecs (A/V): ",
"connectedTo": "Verbunden mit:",
"e2e_rtt": "E2E RTT:",
"framerate": "Bildwiederholrate:",
@@ -127,12 +125,9 @@
"remoteport": "Entfernter Port:",
"remoteport_plural": "Entfernte Ports:",
"resolution": "Auflösung:",
"savelogs": "Logs speichern",
"participant_id": "Teilnehmer-ID:",
"status": "Verbindung:",
"transport": "Protokoll:",
"transport_plural": "Protokolle:",
"video_ssrc": "Video-SSRC:"
"transport_plural": "Protokolle:"
},
"dateUtils": {
"earlier": "Früher",
@@ -201,7 +196,10 @@
"displayNameRequired": "Hallo! Wie ist Ihr Name?",
"done": "Fertig",
"e2eeDescription": "Ende-zu-Ende-Verschlüsselung ist derzeit noch EXPERIMENTELL. Bitte beachten Sie, dass das Aktivieren der Ende-zu-Ende-Verschlüsselung diverse serverseitige Funktionen deaktiviert: Aufnahmen, Livestreaming und Telefoneinwahl. Bitte beachten Sie außerdem, dass der Konferenz dann nur noch mit Browsern beigetreten werden kann, die Insertable Streams unterstützen.",
"e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
"e2eeLabel": "E2EE-Schlüssel",
"e2eeNoKey": "Keiner",
"e2eeToggleSet": "Schlüssel festlegen",
"e2eeSet": "Setzen",
"e2eeWarning": "WARNUNG: Nicht alle Teilnehmer dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Teilnehmer nichts mehr sehen oder hören.",
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
"error": "Fehler",
@@ -366,7 +364,7 @@
"password": "$t(lockRoomPasswordUppercase):",
"title": "Teilen",
"tooltip": "Freigabe-Link und Einwahlinformationen für dieses Meeting",
"label": "Einwahlinformationen"
"label": "Konferenzinformationen"
},
"inviteDialog": {
"alertText": "Die Einladung einiger Teilnehmer ist fehlgeschlagen.",
@@ -505,7 +503,6 @@
"poweredby": "Betrieben von",
"prejoin": {
"audioAndVideoError": "Audio- und Videofehler:",
"audioDeviceProblem": "Es gibt ein Problem mit Ihrem Audiogerät.",
"audioOnlyError": "Audiofehler:",
"audioTrackError": "Audiotrack konnte nicht erstellt werden.",
"calling": "Rufaufbau",
@@ -513,35 +510,15 @@
"callMeAtNumber": "Mich unter dieser Nummer anrufen:",
"configuringDevices": "Geräte werden eingerichtet …",
"connectedWithAudioQ": "Sie sind mit Audio verbunden?",
"connection": {
"good": "Ihre Internetverbindung sieht gut aus!",
"nonOptimal": "Ihre Internetverbindung ist nicht optimal.",
"poor": "Sie haben eine schlechte Internetverbindung."
},
"connectionDetails": {
"audioClipping": "Ihr Ton wird wahrscheinlich abgehackt sein.",
"audioHighQuality": "Ihr Ton sollte exzellent klingen.",
"audioLowNoVideo": "Ihr Ton wird wahrscheinlich schlecht klingen und es wird kein Video geben.",
"goodQuality": "Großartig! Ihre Bild- und Tonqualität sollte super sein.",
"noMediaConnectivity": "Es konnte für diesen Test keine Medienverbindung hergestellt werden. Das wird gewöhnlich durch eine Firewall oder ein NAT ausgelöst.",
"noVideo": "Ihr Bild wird wahrscheinlich eine schlechte Qualität haben.",
"undetectable": "Wenn Sie mit Ihrem Browser weiterhin Probleme in Konferenzen haben, sollten Sie die Verbindung und Funktion Ihrer Lautsprecher, Ihres Mikrofons und Ihrer Kamera überprüfen. Stellen Sie außerdem sicher, dass Ihr Browser die erforderlichen Rechte hat, auf das Mikrofon und die Kamera zuzugreifen, und dass Sie die neuste Browserversion installiert haben. Sollten Sie immer noch Probleme haben, kontaktieren Sie bitte den Entwickler der Webanwendung.",
"veryPoorConnection": "Ihre Konferenzqualität wird wahrscheinlich sehr schlecht sein.",
"videoFreezing": "Ihr Bild wird wahrscheinlich einfrieren, schwarz werden und eine geringe Auflösung haben.",
"videoHighQuality": "Ihr Bild sollte sehr gut aussehen.",
"videoLowQuality": "Ihr Bild wird wahrscheinlich eine geringe Auflösung und Bildrate haben.",
"videoTearing": "Ihr Bild wird wahrscheinlich eine geringe Auflösung haben oder Artefakte aufweisen."
},
"copyAndShare": "Konferenzlink kopieren & teilen",
"dialInMeeting": "Telefoneinwahl",
"dialInPin": "In die Konferenz einwählen und PIN eingeben:",
"dialing": "Wählen",
"doNotShow": "Diesen Bildschirm nicht mehr anzeigen",
"doNotShow": "Nicht mehr anzeigen",
"errorDialOut": "Anruf fehlgeschlagen",
"errorDialOutDisconnected": "Anruf fehlgeschlagen. Verbindungsabbruch",
"errorDialOutFailed": "Anruf fehlgeschlagen. Anruf fehlgeschlagen",
"errorDialOutStatus": "Fehler beim Abrufen des Anrufstatus",
"errorMissingName": "Bitte geben Sie Ihren Namen ein, um der Konferenz beizutreten.",
"errorStatusCode": "Anruf fehlgeschlagen. Statuscode: {{status}}",
"errorValidation": "Nummerverifikation fehlgeschlagen",
"iWantToDialIn": "Ich möchte mich einwählen",
@@ -698,6 +675,7 @@
"document": "Geteiltes Dokument schließen",
"download": "Unsere Apps herunterladen",
"embedMeeting": "Konferenz einbetten",
"e2ee": "Ende-zu-Ende-Verschlüsselung",
"feedback": "Feedback hinterlassen",
"fullScreen": "Vollbildmodus ein-/ausschalten",
"grantModerator": "Zum Moderator machen",
@@ -876,12 +854,12 @@
"getHelp": "Hilfe",
"go": "Los",
"goSmall": "Los",
"info": "Einwahlinformationen",
"info": "Informationen",
"join": "ERSTELLEN / BEITRETEN",
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, unter der Sie der einzige Moderator sind.",
"privacy": "Datenschutz",
"recentList": "Verlauf",
"recentListDelete": "Eintrag löschen",
"recentListDelete": "Löschen",
"recentListEmpty": "Ihr Konferenzverlauf ist derzeit leer. Reden Sie mit Ihrem Team und Ihre vergangenen Konferenzen landen hier.",
"reducedUIText": "Willkommen bei {{app}}!",
"roomNameAllowedChars": "Der Konferenzname sollte keines der folgenden Zeichen enthalten: ?, &, :, ', \", %, #.",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -21,8 +21,7 @@
"bluetooth": "Bluetooth",
"headphones": "Cuffie",
"phone": "Telefono",
"speaker": "Vivavoce",
"none": ""
"speaker": "Vivavoce"
},
"audioOnly": {
"audioOnly": "Solo audio"
@@ -52,12 +51,7 @@
"popover": "Scegli un nickname",
"title": "Inserire un nickname per utilizzare la chat"
},
"title": "Chat",
"you": "",
"privateNotice": "",
"noMessagesMessage": "",
"messageTo": "",
"fieldPlaceHolder": ""
"title": "Chat"
},
"connectingOverlay": {
"joiningRoom": "Collegamento al tuo meeting in corso…"
@@ -72,11 +66,7 @@
"DISCONNECTED": "Disconnesso",
"DISCONNECTING": "Disconnessione in corso",
"ERROR": "Errore",
"RECONNECTING": "Si è verificato un problema di rete. Riconnessione...",
"LOW_BANDWIDTH": "",
"GOT_SESSION_ID": "",
"GET_SESSION_ID_ERROR": "",
"FETCH_SESSION_ID": ""
"RECONNECTING": "Si è verificato un problema di rete. Riconnessione..."
},
"connectionindicator": {
"address": "Indirizzo:",
@@ -106,9 +96,7 @@
"resolution": "Risoluzione:",
"status": "Connessione:",
"transport": "Trasporto:",
"transport_plural": "Trasporti:",
"turn": "(ruota)",
"e2e_rtt": ""
"turn": "(ruota)"
},
"dateUtils": {
"earlier": "Prima",
@@ -120,10 +108,8 @@
"description": "Non è successo nulla? Abbiamo provato ad avviare la tua videoconferenza sull'app desktop di {{app}}. Prova di nuovo o avviala nell'app web di {{app}}.",
"descriptionWithoutWeb": "",
"downloadApp": "Scarica l'app",
"ifDoNotHaveApp": "Se non hai ancora l'app:",
"ifHaveApp": "Se hai già l'app:",
"joinInApp": "Entra in riunione usando l'app",
"launchWebButton": "Avvia sul web",
"openApp": "Prosegui verso l'app",
"title": "Sto avviando la tua videoconferenza su {{app}}…",
"tryAgainButton": "Prova di nuovo sul desktop"
},
@@ -191,7 +177,7 @@
"kickParticipantDialog": "Sei sicuro di voler espellere questo partecipante?",
"kickParticipantTitle": "Espellere questi partecipante?",
"kickTitle": "Espulso dal meeting",
"liveStreaming": "Diretta",
"liveStreaming": "Live Streaming",
"liveStreamingDisabledForGuestTooltip": "Gli ospiti non possono avviare una diretta.",
"liveStreamingDisabledTooltip": "Trasmissioni in diretta disabilitate.",
"lockMessage": "Impossibile bloccare la conferenza.",
@@ -255,7 +241,7 @@
"stopLiveStreaming": "Ferma la diretta",
"stopRecording": "Ferma registrazione",
"stopRecordingWarning": "Sei sicuro di voler interrompere la registrazione?",
"stopStreamingWarning": "Sei sicuro di voler interrompere la diretta?",
"stopStreamingWarning": "Sei sicuro di voler interrompere il live streaming?",
"streamKey": "Chiave per trasmissione in diretta",
"Submit": "Invia",
"thankYou": "Grazie per aver usato {{appName}}!",
@@ -269,18 +255,7 @@
"WaitForHostMsgWOk": "La conferenza <b>{{room}}</b> non è ancora cominciata. Se sei l'organizzatore, allora premi OK per autenticarti. Altrimenti, aspetta l'arrivo dell'organizzatore.",
"WaitingForHost": "In attesa dell'organizzatore ...",
"Yes": "Sì",
"yourEntireScreen": "Schermo intero",
"sendPrivateMessageTitle": "",
"sendPrivateMessageOk": "",
"sendPrivateMessageCancel": "",
"sendPrivateMessage": "",
"screenSharingAudio": "",
"muteEveryoneStartMuted": "",
"muteEveryoneSelf": "",
"muteEveryoneTitle": "",
"muteEveryoneDialog": "",
"muteEveryoneElseTitle": "",
"muteEveryoneElseDialog": ""
"yourEntireScreen": "Schermo intero"
},
"dialOut": {
"statusMessage": "è ora {{status}}"
@@ -359,28 +334,27 @@
"toggleFilmstrip": "Mostra o nascondi anteprime video",
"toggleScreensharing": "Cambia modalità tra videocamera e condivisione schermo",
"toggleShortcuts": "Mostra o nascondi le scorciatoie",
"videoMute": "Accendo o spegni la videocamera",
"videoQuality": ""
"videoMute": "Accendo o spegni la videocamera"
},
"liveStreaming": {
"busy": "Stiamo cercando di liberare risorse per la diretta. Riprova tra qualche minuto.",
"busy": "Stiamo cercando di liberare risorse per lo streaming. Riprova tra qualche minuto.",
"busyTitle": "Tutti gli streamer sono impegnati al momento",
"changeSignIn": "Cambia account",
"choose": "Scegli una trasmissione in diretta",
"chooseCTA": "Scegli un'opzione di trasmissione. Attualmente sei loggato come {{email}}.",
"enterStreamKey": "Inserisci qui la tua chiave YouTube per le trasmissioni in diretta.",
"error": "Diretta fallita. Prova di nuovo.",
"error": "Live streaming fallito. Prova di nuovo.",
"errorAPI": "Si è verificato un errore durante l'accesso ai tuoi broadcast YouTube. Prova a effettuare nuovamente il login.",
"errorLiveStreamNotEnabled": "La diretta non è attivata su {{email}}. Per favore abilita la diretta o effettua l'accesso con un account abilitato alle dirette.",
"expandedOff": "La diretta è stata interrotta",
"expandedOn": "La conferenza è attualmente in diretta su YouTube.",
"expandedPending": "La diretta è in fase di avvio...",
"expandedOff": "La diretta &egrave; stata interrotta",
"expandedOn": "La conferenza &egrave; attualmente in diretta su YouTube.",
"expandedPending": "La diretta &egrave; in fase di avvio...",
"failedToStart": "Avvio trasmissione in diretta fallito",
"getStreamKeyManually": "Non siamo stati in grado di trovare nessuna trasmissione dal vivo. Prova ad ottenere una chiave stream da Youtube",
"invalidStreamKey": "La chiave stream potrebbe non essere corretta.",
"off": "La diretta si è interrotta",
"on": "Trasmissione in diretta",
"pending": "Avvio diretta...",
"pending": "Avvio live stream...",
"serviceName": "Servizio live streaming",
"signedInAs": "Sei attualmente collegato come:",
"signIn": "Registrati con Google",
@@ -388,11 +362,7 @@
"signOut": "Esci",
"start": "Inizia una diretta",
"streamIdHelp": "Cos'è questo?",
"unavailableTitle": "La diretta non è disponibile",
"onBy": "",
"offBy": "",
"googlePrivacyPolicy": "Google Privacy Policy",
"youtubeTerms": "YouTube terms of services"
"unavailableTitle": "Live streaming non disponibile"
},
"localRecording": {
"clientState": {
@@ -410,8 +380,8 @@
"me": "io",
"messages": {
"engaged": "Registrazione locale avviata.",
"finished": "La registrazione della sessione {{token}} è terminata. Invia il file della registrazione al moderatore.",
"finishedModerator": "La registrazione della sessione {{token}} è terminata. Il file della traccia local è stato salvato. Richiedere ai partecipanti di inviare le loro registrazioni.",
"finished": "La registrazione della sessione {{token}} &egrave; terminata. Invia il file della registrazione al moderatore.",
"finishedModerator": "La registrazione della sessione {{token}} &egrave; terminata. Il file della traccia local &egrave; stato salvato. Richiedere ai partecipanti di inviare le loro registrazioni.",
"notModerator": "Non sei un moderatore. Non puoi avviare o interrompere la registrazione"
},
"moderator": "Moderatore",
@@ -455,8 +425,7 @@
"unmute": "",
"newDeviceCameraTitle": "Trovata nuova videocamera",
"newDeviceAudioTitle": "Trovata nuova origine audio",
"newDeviceAction": "Usala",
"suboptimalBrowserWarning": "Ci spiace che la tua videoconferenza non sarà ottimale, qui. Stiamo cercando modi per migliorare, ma fino ad allora per favore prova ad usare <a href='{{recommendedBrowserPageLink}}' target='_blank'>browser supportati completamente/a>."
"newDeviceAction": "Usala"
},
"passwordSetRemotely": "definita da altro utente",
"passwordDigitsOnly": "Fino a {{number}} cifre",
@@ -504,9 +473,7 @@
"signIn": "Entra",
"signOut": "Esci",
"unavailable": "Ops! Il {{serviceName}} non è al momento disponibile. Stiamo lavorando per risolvere il problema. Riprova più tardi.",
"unavailableTitle": "Registrazione non disponibile",
"onBy": "{{name}} registrazione iniziata",
"offBy": "{{name}} registrazione fermata"
"unavailableTitle": "Registrazione non disponibile"
},
"sectionList": {
"pullToRefresh": "Trascina per aggiornare"
@@ -532,9 +499,7 @@
"selectMic": "Microfono",
"startAudioMuted": "Tutti cominciano con il microfono disattivato",
"startVideoMuted": "Tutti cominciano con il video disattivato",
"title": "Impostazioni",
"speakers": "Altoparlanti",
"microphones": "Microfoni"
"title": "Impostazioni"
},
"settingsView": {
"alertOk": "OK",
@@ -549,11 +514,7 @@
"serverURL": "URL del server",
"startWithAudioMuted": "Inizia con l'audio disattivato",
"startWithVideoMuted": "Avvia con il video disattivato",
"version": "Versione",
"showAdvanced": "Mostra impostazioni avanzate",
"disableP2P": "Disabilita modalità uno-a-uno",
"disableCallIntegration": "Disabilita integrazione nativa delle chiamate",
"advanced": "Avanzate"
"version": "Versione"
},
"share": {
"dialInfoText": "\n\n=====\n\nVuoi solo ascoltare la conferenza da un telefono?\n\n{{defaultDialInNumber}}Clicca questo link per vedere i numeri telefonici di questo meeting\n{{dialInfoPageUrl}}",
@@ -610,12 +571,7 @@
"tileView": "Vedi tutti i partecipanti insieme, o uno solo",
"toggleCamera": "Cambia videocamera",
"videomute": "Attiva/disattiva videocamera",
"videoblur": "Attiva/disattiva offuscamento video",
"privateMessage": "Invia messaggio privato",
"muteEveryone": "Zittisci tutti",
"moreOptions": "Mostra più opzioni",
"help": "Aiuto",
"download": "Scarica altre app"
"videoblur": "Attiva/disattiva offuscamento video"
},
"addPeople": "Aggiungi persone alla chiamata",
"audioOnlyOff": "Anche video",
@@ -638,6 +594,7 @@
"logout": "Logout",
"lowerYourHand": "Abbassa la mano",
"moreActions": "Più azioni",
"moreOptions": "Più opzioni",
"mute": "Microfono Attiva / Disattiva",
"openChat": "Apri una chat",
"pip": "Abilita visualizzazione immagine nellimmagine",
@@ -741,7 +698,7 @@
"privacy": "Privacy",
"recentList": "Recente",
"recentListDelete": "Cancella",
"recentListEmpty": "La tua lista è vuota. Chatta con qualcuno del tuo team e lo vedrai apparire nella lista di meeting recenti.",
"recentListEmpty": "La tua lista &egrave; vuota. Chatta con qualcuno del tuo team e lo vedrai apparire nella lista di meeting recenti.",
"reducedUIText": "",
"roomname": "Inserisci Nome Stanza",
"roomnameHint": "Inserisci il nome o l'URL della stanza alla quale vuoi accedere. Puoi anche inventarti un nome, assicurati solo che le persone che vuoi contattare lo sappiano, così che possano inserire lo stesso nome.",
@@ -749,18 +706,8 @@
"terms": "Termini di utilizzo",
"title": "Il sistema di conferenza sicuro, funzionale e completamente gratuito."
},
"documentSharing": {
"title": ""
},
"defaultNickname": "",
"chromeExtensionBanner": {
"dontShowAgain": "",
"buttonText": "",
"installExtensionText": ""
},
"raisedHand": "Vorrebbe parlare",
"lonelyMeetingExperience": {
"button": "Invita altri",
"button": "Invita gli altri",
"youAreAlone": "Sei l'unico in riunione"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -122,13 +122,7 @@
"status": "Connexion :",
"transport": "Transpòrt :",
"transport_plural": "Transpòrts :",
"e2e_rtt": "E2E RTT:",
"codecs": "Codecs (A/V): ",
"video_ssrc": "Vidèo SSRC:",
"maxEnabledResolution": "enviar max",
"savelogs": "Enregistrar jornals",
"participant_id": "Id participant:",
"audio_ssrc": "Àudio SSRC:"
"e2e_rtt": "E2E RTT:"
},
"dateUtils": {
"earlier": "Mai dora",
@@ -301,20 +295,7 @@
"muteEveryoneSelf": "vos",
"muteEveryoneTitle": "Rendre mut tot lo monde?",
"muteEveryoneDialog": "Volètz vertadièrament copar lo son a tot lo monde? Poiretz pas lo restablir, mas eles poiràn o far quora que vòlgan.",
"muteEveryoneElseTitle": "Copar lo son a totes levat {{whom}}?",
"add": "Ajustar",
"copied": "Copiat",
"grantModeratorDialog": "Volètz vertadièrament far venir aqueste participant moderator?",
"readMore": "mai",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Impossible pendent una difusion activa",
"screenSharingFailedTitle": "Fracàs del partiment d'ecran!",
"e2eeLabel": "Activar lo chiframant del cap a la fin",
"grantModeratorTitle": "Passar moderator",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Impossible pendent un enregistrament actiu",
"e2eeDescription": "Lo chiframent del cap a la fin es actualament EXPERIMENTALA. Mercés de gardar a l'esperit qu'activar lo chiframent del cap a la fin desactivarà en efièch los servicis costat servidor coma: l'enregistrament, la difusion en dirècte e las participacions telefonicas. Remembratz tanben que la conferéncia foncionarà pas que per lo monde que participan amb un navigador compatible amb los fluxes inseribles.",
"screenSharingFailed": "Ops! Quicòm a trucat, avèm pas pogut començar lo partiment d'ecran!",
"e2eeWarning": "AVERTIMENT: pas totes los participants d'aquesta conferéncia semblan poder suportar lo chiframent del cap a la fin. Se l'activatz poiràn pas vos veire nimai vos entendre.",
"muteEveryoneElseDialog": "Un còp mut, poiretz pas mai lo tornar la paraula, mas la se pòdon tornar quora vòlgan."
"muteEveryoneElseTitle": "Copar lo son a totes levat {{whom}}?"
},
"dialOut": {
"statusMessage": "ara es {{status}}"
@@ -429,9 +410,7 @@
"streamIdHelp": "Ques aquò ?",
"unavailableTitle": "Difusion en dirècte indisponibla",
"googlePrivacyPolicy": "Politica de confidencialitat de Google",
"youtubeTerms": "Condicions dutilizacion de YouTube",
"limitNotificationDescriptionWeb": "A causa d'una brava demanda vòstra difusion serà limitada a {{limit}} min. Per de difusion illimitada ensajatz <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Vòstra difusion serà limitada a {{limit}} min. Per de difusions illimitada ensajatz {{app}}."
"youtubeTerms": "Condicions dutilizacion de YouTube"
},
"localRecording": {
"clientState": {
@@ -663,10 +642,7 @@
"muteEveryone": "Rendre mut tot lo monde",
"moreOptions": "Mostrar mai dopcions",
"e2ee": "Chiframent del cap a la fin",
"security": "Opcions de seguretat",
"embedMeeting": "Conferéncia integrada",
"grantModerator": "Passar moderator",
"lobbyButton": "Activar/Desactivar mòde sala d'espèra"
"security": "Opcions de seguretat"
},
"addPeople": "Ajustar de monde a vòstra sonada",
"audioOnlyOff": "Desactivar lo mòde connexion febla",
@@ -717,17 +693,14 @@
"videomute": "Aviar / Arrestar la camèra",
"startvideoblur": "Trebolar mon rèire-plan",
"stopvideoblur": "Desactivar lo borrolatge del rèire-plan",
"noisyAudioInputDesc": "Sembla que vòstre microfòn mene bruch, pensatz de lo copar o de lo cambiar.",
"noisyAudioInputDesc": "",
"noisyAudioInputTitle": "Vòstre microfòn sembla brusent !",
"noAudioSignalDialInLinkDesc": "",
"noAudioSignalDialInDesc": "",
"muteEveryone": "Rendre mut tot lo monde",
"moreOptions": "Autras opcions",
"e2ee": "Chiframent del cap a la fin",
"security": "Opcions de seguretat",
"embedMeeting": "Integrar conferéncia",
"lobbyButtonDisable": "Desactivar lo mòde sala d'espèra",
"lobbyButtonEnable": "Activar mòde sala d'espèra"
"security": "Opcions de seguretat"
},
"transcribing": {
"ccButtonTooltip": "Aviar / Arrestat los sostítols",
@@ -822,12 +795,7 @@
"sendFeedback": "Mandar vòstra opinion",
"terms": "Tèrmes",
"title": "Conferéncias vidèo securizadas amb plen de foncionalitats e complètament gratuitas",
"getHelp": "Obténer dajuda",
"startMeeting": "Començar la reünion",
"jitsiOnMobile": "Jitsi sus mobil telecargatz nòstra aplicacion e començatz de conferéncias de pertot",
"moderatedMessage": "O <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservatz una URL de conferéncia</a> a l'avança ont sètz l'unic moderator.",
"jitsiMeet": "Jitsi Meet",
"secureMeetings": "Conferéncias seguras e de nauta qualitat"
"getHelp": "Obténer dajuda"
},
"helpView": {
"header": "Centre dajuda"
@@ -864,27 +832,7 @@
"initiated": "Sonada aviada",
"joinWithoutAudio": "Rejónher sens àudio",
"joinMeeting": "Rejónher la conferéncia",
"joinAudioByPhone": "Rejónher amb làudio del telefòn",
"audioDeviceProblem": "I a un problèma amb vòstre periferic àudio",
"showScreen": "Activar l'ecran de preconferéncia",
"connection": {
"good": "Vòstra connexion Internet sembla bona!",
"nonOptimal": "Vòstra connexion Internet es pas optimala",
"poor": "Vòstra connexion Internet es febla"
},
"connectionDetails": {
"videoHighQuality": "Nos esperam a trobar vòstra qualitat vidèo de bona qualitat.",
"audioClipping": "Nos esperam a trobar vòstre àudio troncat.",
"audioHighQuality": "Nos esperam a trobar vòstra qualitat àudio excellenta.",
"audioLowNoVideo": "Nos esperam a trobar vòstra qualitat àudio febla e cap de vidèo.",
"goodQuality": "Crane! Vòstra qualitat serà geniala.",
"noMediaConnectivity": "Avèm pas trobat cap de biais d'establir una connectivitat mèdia per aquesta pròva. Sovent es a causat d'un parafòc o un NAT.",
"noVideo": "Nos esperam a trobat vòstra qualitat vidèo òrra.",
"veryPoorConnection": "Nos esperam a trobar vòstra qualitat vidèo plan òrra.",
"videoFreezing": "Nos esperam a veire vòstra vidèo se gelar, venir negra e se pixelizar."
},
"premeeting": "Preconferéncia",
"errorMissingName": "Mercés de picar vòstre nom per rejónher la conferéncia"
"joinAudioByPhone": "Rejónher amb làudio del telefòn"
},
"lobby": {
"reject": "Regetar",
@@ -907,15 +855,7 @@
"emailField": "Picata vòstra adreça electronica",
"disableDialogSubmit": "Desactivar",
"backToKnockModeButton": "Cap de senhal, demandar a participar a la plaça",
"allow": "Autorizar",
"knockingParticipantList": "Lista de participants en espèra",
"dialogTitle": "Mòde sala d'espèra",
"notificationLobbyDisabled": "Lo mòde sala d'espèra es estat desactivat per {{originParticipantName}}",
"notificationLobbyEnabled": "Lo mòde sala d'espèra activat per {{originParticipantName}}",
"notificationTitle": "Sala d'espèra",
"toggleLabel": "Activar la sala d'espèra",
"notificationLobbyAccessDenied": "{{originParticipantName}} a decidit de regetar la demanda de {{targetParticipantName}}",
"notificationLobbyAccessGranted": "{{originParticipantName}} a autorizat {{targetParticipantName}} a dintrar"
"allow": "Autorizar"
},
"security": {
"securityOptions": "Opcions de seguretat",
@@ -925,8 +865,7 @@
},
"e2ee": {
"labelToolTip": "La comunicacion àudio e vidèo daquesta sonada es chifrada del cap a la fin"
},
"embedMeeting": {
"title": "Integrar aquesta conferéncia"
}
}

View File

@@ -149,10 +149,7 @@
"launchWebButton": "Запустить в браузере",
"openApp": "Перейти к приложению",
"title": "Запуск вашей встречи в {{app}}...",
"tryAgainButton": "Повторите в настольном приложении",
"joinInApp": "Присоединиться к этой встрече с помощью приложения",
"ifHaveApp": "Если у Вас уже есть приложение:",
"ifDoNotHaveApp": "Если у Вас ещё нет приложения:"
"tryAgainButton": "Повторите в настольном приложении"
},
"defaultLink": "напр. {{url}}",
"defaultNickname": "напр. Яна Цветкова",

View File

@@ -220,6 +220,7 @@
"kickTitle": "Ouch! {{participantDisplayName}} kicked you out of the meeting",
"liveStreaming": "Live Streaming",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Not possible while recording is active",
"liveStreamingDisabledForGuestTooltip": "Guests can't start live streaming.",
"liveStreamingDisabledTooltip": "Start live stream disabled.",
"lockMessage": "Failed to lock the conference.",
"lockRoom": "Add meeting $t(lockRoomPasswordUppercase)",
@@ -254,6 +255,7 @@
"readMore": "more",
"recording": "Recording",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Not possible while a live stream is active",
"recordingDisabledForGuestTooltip": "Guests can't start recordings.",
"recordingDisabledTooltip": "Start recording disabled.",
"rejoinNow": "Rejoin now",
"remoteControlAllowedMessage": "{{user}} accepted your remote control request!",
@@ -285,6 +287,7 @@
"shareVideoTitle": "Share a video",
"shareYourScreen": "Share your screen",
"shareYourScreenDisabled": "Screen sharing disabled.",
"shareYourScreenDisabledForGuest": "Guests can't screen share.",
"startLiveStreaming": "Start live stream",
"startRecording": "Start recording",
"startRemoteControlErrorMessage": "An error occurred while trying to start the remote control session!",
@@ -678,7 +681,6 @@
},
"startupoverlay": {
"policyText": " ",
"genericTitle": "The meeting needs to use your microphone and camera.",
"title": "{{app}} needs to use your microphone and camera."
},
"suspendedoverlay": {
@@ -838,6 +840,8 @@
"ld": "LD",
"ldTooltip": "Viewing low definition video",
"lowDefinition": "Low definition",
"onlyAudioAvailable": "Only audio is available",
"onlyAudioSupported": "We only support audio in this browser.",
"sd": "SD",
"sdTooltip": "Viewing standard definition video",
"standardDefinition": "Standard definition"
@@ -872,11 +876,8 @@
"getHelp": "Get help",
"go": "GO",
"goSmall": "GO",
"headerTitle": "Jitsi Meet",
"headerSubtitle": "Secure and high quality meetings",
"info": "Dial-in info",
"join": "CREATE / JOIN",
"jitsiOnMobile": "Jitsi on mobile download our apps and start a meeting from anywhere",
"moderatedMessage": "Or <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">book a meeting URL</a> in advance where you are the only moderator.",
"privacy": "Privacy",
"recentList": "Recent",
@@ -887,7 +888,6 @@
"roomname": "Enter room name",
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
"sendFeedback": "Send feedback",
"startMeeting": "Start meeting",
"terms": "Terms",
"title": "Secure, fully featured, and completely free video conferencing"
},

View File

@@ -124,13 +124,16 @@ function changeParticipantNumber(APIInstance, number) {
* configuration options defined in interface_config.js to be overridden.
* @param {string} [options.jwt] - The JWT token if needed by jitsi-meet for
* authentication.
* @param {boolean} [options.noSSL] - If the value is true https won't be used.
* @param {string} [options.roomName] - The name of the room to join.
* @returns {string} The URL.
*/
function generateURL(domain, options = {}) {
return urlObjectToString({
...options,
url: `https://${domain}/#jitsi_meet_external_api_id=${id}`
url:
`${options.noSSL ? 'http' : 'https'}://${
domain}/#jitsi_meet_external_api_id=${id}`
});
}
@@ -161,6 +164,7 @@ function parseArguments(args) {
parentNode,
configOverwrite,
interfaceConfigOverwrite,
noSSL,
jwt,
onload
] = args;
@@ -172,6 +176,7 @@ function parseArguments(args) {
parentNode,
configOverwrite,
interfaceConfigOverwrite,
noSSL,
jwt,
onload
};
@@ -232,6 +237,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* configuration options defined in config.js to be overridden.
* @param {Object} [options.interfaceConfigOverwrite] - Object containing
* configuration options defined in interface_config.js to be overridden.
* @param {boolean} [options.noSSL] - If the value is true https won't be
* used.
* @param {string} [options.jwt] - The JWT token if needed by jitsi-meet for
* authentication.
* @param {string} [options.onload] - The onload function that will listen
@@ -254,6 +261,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
parentNode = document.body,
configOverwrite = {},
interfaceConfigOverwrite = {},
noSSL = false,
jwt = undefined,
onload = undefined,
invitees,
@@ -268,6 +276,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
configOverwrite,
interfaceConfigOverwrite,
jwt,
noSSL,
roomName,
devices,
userInfo,

View File

@@ -1,4 +1,4 @@
/* global APP, $, config */
/* global APP, $, config, interfaceConfig */
const UI = {};
@@ -7,7 +7,6 @@ import EventEmitter from 'events';
import Logger from 'jitsi-meet-logger';
import { isMobileBrowser } from '../../react/features/base/environment/utils';
import { getLocalParticipant } from '../../react/features/base/participants';
import { toggleChat } from '../../react/features/chat';
import { setDocumentUrl } from '../../react/features/etherpad';
import { setFilmstripVisible } from '../../react/features/filmstrip';
@@ -99,29 +98,11 @@ UI.notifyReservationError = function(code, msg) {
});
};
/**
* Change nickname for the user.
* @param {string} id user id
* @param {string} displayName new nickname
*/
UI.changeDisplayName = function(id, displayName) {
VideoLayout.onDisplayNameChanged(id, displayName);
};
/**
* Initialize conference UI.
*/
UI.initConference = function() {
const { getState } = APP.store;
const { id, name } = getLocalParticipant(getState);
UI.showToolbar();
const displayName = config.displayJids ? id : name;
if (displayName) {
UI.changeDisplayName('localVideoContainer', displayName);
}
};
/**
@@ -143,7 +124,9 @@ UI.start = function() {
$.prompt.setDefaults({ persistent: false });
VideoLayout.init(eventEmitter);
VideoLayout.initLargeVideo();
if (!interfaceConfig.filmStripOnly) {
VideoLayout.initLargeVideo();
}
// Do not animate the video area on UI start (second argument passed into
// resizeVideoArea) because the animation is not visible anyway. Plus with
@@ -159,7 +142,10 @@ UI.start = function() {
$('body').addClass('desktop-browser');
}
if (config.iAmRecorder) {
if (interfaceConfig.filmStripOnly) {
$('body').addClass('filmstrip-only');
APP.store.dispatch(setNotificationsEnabled(false));
} else if (config.iAmRecorder) {
// in case of iAmSipGateway keep local video visible
if (!config.iAmSipGateway) {
VideoLayout.setLocalVideoVisible(false);
@@ -242,19 +228,12 @@ UI.getSharedDocumentManager = () => etherpadManager;
* @param {JitsiParticipant} user
*/
UI.addUser = function(user) {
const id = user.getId();
const displayName = user.getDisplayName();
const status = user.getStatus();
if (status) {
// FIXME: move updateUserStatus in participantPresenceChanged action
UI.updateUserStatus(user, status);
}
// set initial display name
if (displayName) {
UI.changeDisplayName(id, displayName);
}
};
/**
@@ -489,14 +468,6 @@ UI.handleLastNEndpoints = function(leavingIds, enteringIds) {
*/
UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
/**
* Hide connection quality statistics from UI.
*/
UI.hideStats = function() {
VideoLayout.hideStats();
};
UI.notifyTokenAuthFailed = function() {
messageHandler.showError({
descriptionKey: 'dialog.tokenAuthFailed',

View File

@@ -1,10 +1,15 @@
/* global $ */
/* global $, APP */
import Logger from 'jitsi-meet-logger';
/* eslint-disable no-unused-vars */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
import SmallVideo from '../videolayout/SmallVideo';
const logger = Logger.getLogger(__filename);
/* eslint-enable no-unused-vars */
/**
*
@@ -24,17 +29,12 @@ export default class SharedVideoThumb extends SmallVideo {
this.videoSpanId = 'sharedVideoContainer';
this.container = this.createContainer(this.videoSpanId);
this.$container = $(this.container);
this.renderThumbnail();
this._setThumbnailSize();
this.bindHoverHandler();
this.updateDisplayName();
this.container.onclick = this._onContainerClick;
}
/**
*
*/
initializeAvatar() {} // eslint-disable-line no-empty-function
/**
*
* @param {*} spanId
@@ -45,18 +45,6 @@ export default class SharedVideoThumb extends SmallVideo {
container.id = spanId;
container.className = 'videocontainer';
// add the avatar
const avatar = document.createElement('img');
avatar.className = 'sharedVideoAvatar';
avatar.src = `https://img.youtube.com/vi/${this.url}/0.jpg`;
container.appendChild(avatar);
const displayNameContainer = document.createElement('div');
displayNameContainer.className = 'displayNameContainer';
container.appendChild(displayNameContainer);
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
@@ -68,21 +56,14 @@ export default class SharedVideoThumb extends SmallVideo {
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
* Renders the thumbnail.
*/
updateDisplayName() {
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId
} does not exist`);
return;
}
this._renderDisplayName({
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
}

View File

@@ -37,7 +37,6 @@ const Filmstrip = {
*/
resizeThumbnailsForTileView(width, height, forceUpdate = false) {
const thumbs = this._getThumbs(!forceUpdate);
const avatarSize = height / 2;
if (thumbs.localThumb) {
thumbs.localThumb.css({
@@ -58,11 +57,6 @@ const Filmstrip = {
width: `${width}px`
});
}
$('.avatar-container').css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
},
/**
@@ -77,7 +71,6 @@ const Filmstrip = {
if (thumbs.localThumb) {
const { height, width } = local;
const avatarSize = height / 2;
thumbs.localThumb.css({
height: `${height}px`,
@@ -85,15 +78,10 @@ const Filmstrip = {
'min-width': `${width}px`,
width: `${width}px`
});
$('#localVideoContainer > .avatar-container').css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
if (thumbs.remoteThumbs) {
const { height, width } = remote;
const avatarSize = height / 2;
thumbs.remoteThumbs.css({
height: `${height}px`,
@@ -101,10 +89,6 @@ const Filmstrip = {
'min-width': `${width}px`,
width: `${width}px`
});
$('#filmstripRemoteVideosContainer > span > .avatar-container').css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
},
@@ -126,10 +110,6 @@ const Filmstrip = {
'min-width': '',
'min-height': ''
});
$('#localVideoContainer > .avatar-container').css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
}
if (thumbs.remoteThumbs) {
@@ -142,10 +122,6 @@ const Filmstrip = {
'min-width': '',
'min-height': ''
});
$('#filmstripRemoteVideosContainer > span > .avatar-container').css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
}
},

View File

@@ -1,23 +1,23 @@
/* global $, config, interfaceConfig, APP */
/* global $, config, APP */
import Logger from 'jitsi-meet-logger';
/* eslint-disable no-unused-vars */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
import { VideoTrack } from '../../../react/features/base/media';
import { updateSettings } from '../../../react/features/base/settings';
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
import UIEvents from '../../../service/UI/UIEvents';
import SmallVideo from './SmallVideo';
const logger = Logger.getLogger(__filename);
/**
*
*/
@@ -37,6 +37,7 @@ export default class LocalVideo extends SmallVideo {
this.isLocal = true;
this._setThumbnailSize();
this.updateDOMLocation();
this.renderThumbnail();
this.localVideoId = null;
this.bindHoverHandler();
@@ -44,7 +45,6 @@ export default class LocalVideo extends SmallVideo {
this._buildContextMenu();
}
this.emitter = emitter;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left top' : 'top center';
Object.defineProperty(this, 'id', {
get() {
@@ -53,18 +53,6 @@ export default class LocalVideo extends SmallVideo {
});
this.initBrowserSpecificProperties();
// Set default display name.
this.updateDisplayName();
// Initialize the avatar display with an avatar url selected from the redux
// state. Redux stores the local user with a hardcoded participant id of
// 'local' if no id has been assigned yet.
this.initializeAvatar();
this.addAudioLevelIndicator();
this.updateIndicators();
this.updateStatusBar();
this.container.onclick = this._onContainerClick;
}
@@ -77,38 +65,19 @@ export default class LocalVideo extends SmallVideo {
containerSpan.classList.add('videocontainer');
containerSpan.id = this.videoSpanId;
containerSpan.innerHTML = `
<div class = 'videocontainer__background'></div>
<span id = 'localVideoWrapper'></span>
<div class = 'videocontainer__toolbar'></div>
<div class = 'videocontainer__toptoolbar'></div>
<div class = 'videocontainer__hoverOverlay'></div>
<div class = 'displayNameContainer'></div>
<div class = 'avatar-container'></div>`;
return containerSpan;
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
* Renders the thumbnail.
*/
updateDisplayName() {
if (!this.container) {
logger.warn(
`Unable to set displayName - ${this.videoSpanId
} does not exist`);
return;
}
this._renderDisplayName({
allowEditing: !config.disableProfile,
displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
elementID: 'localDisplayName',
participantID: this.id
});
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
/**
@@ -118,7 +87,6 @@ export default class LocalVideo extends SmallVideo {
changeVideo(stream) {
this.videoStream = stream;
this.localVideoId = `localVideo_${stream.getId()}`;
this._updateVideoElement();
// eslint-disable-next-line eqeqeq
const isVideo = stream.videoType != 'desktop';
@@ -128,17 +96,6 @@ export default class LocalVideo extends SmallVideo {
this.setFlipX(isVideo ? settings.localFlipX : false);
const endedHandler = () => {
const localVideoContainer
= document.getElementById('localVideoWrapper');
// Only remove if there is no video and not a transition state.
// Previous non-react logic created a new video element with each track
// removal whereas react reuses the video component so it could be the
// stream ended but a new one is being used.
if (localVideoContainer && this.videoStream.isEnded()) {
ReactDOM.unmountComponentAtNode(localVideoContainer);
}
this._notifyOfStreamEnded();
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
};
@@ -254,35 +211,5 @@ export default class LocalVideo extends SmallVideo {
: document.getElementById('filmstripLocalVideoThumbnail');
appendTarget && appendTarget.appendChild(this.container);
this._updateVideoElement();
}
/**
* Renders the React Element for displaying video in {@code LocalVideo}.
*
*/
_updateVideoElement() {
const localVideoContainer = document.getElementById('localVideoWrapper');
const videoTrack
= getLocalVideoTrack(APP.store.getState()['features/base/tracks']);
ReactDOM.render(
<Provider store = { APP.store }>
<VideoTrack
id = 'localVideo_container'
videoTrack = { videoTrack } />
</Provider>,
localVideoContainer
);
// Ensure the video gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case video does not autoplay. Also, set the playsinline attribute on the
// video element so that local video doesn't open in full screen by default
// in Safari browser on iOS.
const video = this.container.querySelector('video');
video && video.setAttribute('playsinline', 'true');
video && !config.testing?.noAutoPlayVideo && video.play();
}
}

View File

@@ -19,6 +19,7 @@ import {
pinParticipant
} from '../../../react/features/base/participants';
import { isRemoteTrackMuted } from '../../../react/features/base/tracks';
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
import { PresenceLabel } from '../../../react/features/presence-status';
import {
REMOTE_CONTROL_MENU_STATES,
@@ -42,16 +43,6 @@ function createContainer(spanId) {
container.id = spanId;
container.className = 'videocontainer';
container.innerHTML = `
<div class = 'videocontainer__background'></div>
<div class = 'videocontainer__toptoolbar'></div>
<div class = 'videocontainer__toolbar'></div>
<div class = 'videocontainer__hoverOverlay'></div>
<div class = 'displayNameContainer'></div>
<div class = 'avatar-container'></div>
<div class ='presence-label-container'></div>
<span class = 'remotevideomenu'></span>`;
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
@@ -82,10 +73,7 @@ export default class RemoteVideo extends SmallVideo {
this._audioStreamElement = null;
this._supportsRemoteControl = false;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
this.addRemoteVideoContainer();
this.updateIndicators();
this.updateDisplayName();
this.bindHoverHandler();
this.flipX = false;
this.isLocal = false;
@@ -117,17 +105,26 @@ export default class RemoteVideo extends SmallVideo {
addRemoteVideoContainer() {
this.container = createContainer(this.videoSpanId);
this.$container = $(this.container);
this.initializeAvatar();
this.renderThumbnail();
this._setThumbnailSize();
this.initBrowserSpecificProperties();
this.updateRemoteVideoMenu();
this.updateStatusBar();
this.addAudioLevelIndicator();
this.addPresenceLabel();
return this.container;
}
/**
* Renders the thumbnail.
*/
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
/**
* Generates the popup menu content.
*
@@ -135,6 +132,10 @@ export default class RemoteVideo extends SmallVideo {
* @private
*/
_generatePopupContent() {
if (interfaceConfig.filmStripOnly) {
return;
}
const remoteVideoMenuContainer
= this.container.querySelector('.remotevideomenu');
@@ -353,9 +354,9 @@ export default class RemoteVideo extends SmallVideo {
* Removes RemoteVideo from the page.
*/
remove() {
super.remove();
this.removePresenceLabel();
this.removeRemoteVideoMenu();
ReactDOM.unmountComponentAtNode(this.container);
super.remove();
}
/**
@@ -428,24 +429,6 @@ export default class RemoteVideo extends SmallVideo {
}
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
*/
updateDisplayName() {
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId} does not exist`);
return;
}
this._renderDisplayName({
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
}
/**
* Removes remote video menu element from video element identified by
* given <tt>videoElementId</tt>.
@@ -460,39 +443,4 @@ export default class RemoteVideo extends SmallVideo {
menuSpan.remove();
}
}
/**
* Mounts the {@code PresenceLabel} for displaying the participant's current
* presence status.
*
* @return {void}
*/
addPresenceLabel() {
const presenceLabelContainer = this.container.querySelector('.presence-label-container');
if (presenceLabelContainer) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<PresenceLabel
participantID = { this.id }
className = 'presence-label' />
</I18nextProvider>
</Provider>,
presenceLabelContainer);
}
}
/**
* Unmounts the {@code PresenceLabel} component.
*
* @return {void}
*/
removePresenceLabel() {
const presenceLabelContainer = this.container.querySelector('.presence-label-container');
if (presenceLabelContainer) {
ReactDOM.unmountComponentAtNode(presenceLabelContainer);
}
}
}

View File

@@ -95,30 +95,6 @@ export default class SmallVideo {
this.videoIsHovered = false;
this.videoType = undefined;
/**
* Whether or not the connection indicator should be displayed.
*
* @private
* @type {boolean}
*/
this._showConnectionIndicator = !interfaceConfig.CONNECTION_INDICATOR_DISABLED;
/**
* Whether or not the dominant speaker indicator should be displayed.
*
* @private
* @type {boolean}
*/
this._showDominantSpeaker = false;
/**
* Whether or not the raised hand indicator should be displayed.
*
* @private
* @type {boolean}
*/
this._showRaisedHand = false;
// Bind event handlers so they are only bound once for every instance.
this.updateView = this.updateView.bind(this);
@@ -179,103 +155,22 @@ export default class SmallVideo {
this.$container.hover(
() => {
this.videoIsHovered = true;
this.renderThumbnail(true);
this.updateView();
this.updateIndicators();
},
() => {
this.videoIsHovered = false;
this.renderThumbnail(false);
this.updateView();
this.updateIndicators();
}
);
}
/**
* Unmounts the ConnectionIndicator component.
* @returns {void}
*/
removeConnectionIndicator() {
this._showConnectionIndicator = false;
this.updateIndicators();
}
/**
* Create or updates the ReactElement for displaying status indicators about
* audio mute, video mute, and moderator status.
*
* @returns {void}
* Renders the thumbnail.
*/
updateStatusBar() {
const statusBarContainer = this.container.querySelector('.videocontainer__toolbar');
if (!statusBarContainer) {
return;
}
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<StatusIndicators
participantID = { this.id } />
</I18nextProvider>
</Provider>,
statusBarContainer);
}
/**
* Adds the element indicating the audio level of the participant.
*
* @returns {void}
*/
addAudioLevelIndicator() {
let audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
return;
}
audioLevelContainer = document.createElement('span');
audioLevelContainer.className = 'audioindicator-container';
this.container.appendChild(audioLevelContainer);
this.updateAudioLevelIndicator();
}
/**
* Removes the element indicating the audio level of the participant.
*
* @returns {void}
*/
removeAudioLevelIndicator() {
const audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
ReactDOM.unmountComponentAtNode(audioLevelContainer);
}
}
/**
* Updates the audio level for this small video.
*
* @param lvl the new audio level to set
* @returns {void}
*/
updateAudioLevelIndicator(lvl = 0) {
const audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
ReactDOM.render(<AudioLevelIndicator audioLevel = { lvl }/>, audioLevelContainer);
}
}
/**
* Queries the component's DOM for the element that should be the parent to the
* AudioLevelIndicator.
*
* @returns {HTMLElement} The DOM element that holds the AudioLevelIndicator.
*/
_getAudioLevelContainer() {
return this.container.querySelector('.audioindicator-container');
renderThumbnail() {
// Should be implemented by in subclasses.
}
/**
@@ -292,62 +187,6 @@ export default class SmallVideo {
return $($(this.container).find('video')[0]);
}
/**
* Selects the HTML image element which displays user's avatar.
*
* @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
* element which displays the user's avatar.
*/
$avatar() {
return this.$container.find('.avatar-container');
}
/**
* Returns the display name element, which appears on the video thumbnail.
*
* @return {jQuery} a jQuery selector pointing to the display name element of
* the video thumbnail
*/
$displayName() {
return this.$container.find('.displayNameContainer');
}
/**
* Creates or updates the participant's display name that is shown over the
* video preview.
*
* @param {Object} props - The React {@code Component} props to pass into the
* {@code DisplayName} component.
* @returns {void}
*/
_renderDisplayName(props) {
const displayNameContainer = this.container.querySelector('.displayNameContainer');
if (displayNameContainer) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<DisplayName { ...props } />
</I18nextProvider>
</Provider>,
displayNameContainer);
}
}
/**
* Removes the component responsible for showing the participant's display name,
* if its container is present.
*
* @returns {void}
*/
removeDisplayName() {
const displayNameContainer = this.container.querySelector('.displayNameContainer');
if (displayNameContainer) {
ReactDOM.unmountComponentAtNode(displayNameContainer);
}
}
/**
* Enables / disables the css responsible for focusing/pinning a video
* thumbnail.
@@ -516,43 +355,6 @@ export default class SmallVideo {
}
}
/**
* Updates the react component displaying the avatar with the passed in avatar
* url.
*
* @returns {void}
*/
initializeAvatar() {
const thumbnail = this.$avatar().get(0);
if (thumbnail) {
// Maybe add a special case for local participant, as on init of
// LocalVideo.js the id is set to "local" but will get updated later.
ReactDOM.render(
<Provider store = { APP.store }>
<AvatarDisplay
className = 'userAvatar'
participantId = { this.id } />
</Provider>,
thumbnail
);
}
}
/**
* Unmounts any attached react components (particular the avatar image) from
* the avatar container.
*
* @returns {void}
*/
removeAvatar() {
const thumbnail = this.$avatar().get(0);
if (thumbnail) {
ReactDOM.unmountComponentAtNode(thumbnail);
}
}
/**
* Shows or hides the dominant speaker indicator.
* @param show whether to show or hide.
@@ -569,30 +371,8 @@ export default class SmallVideo {
return;
}
if (this._showDominantSpeaker === show) {
return;
}
this._showDominantSpeaker = show;
this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
this.updateIndicators();
this.updateView();
}
/**
* Shows or hides the raised hand indicator.
* @param show whether to show or hide.
*/
showRaisedHandIndicator(show) {
if (!this.container) {
logger.warn(`Unable to raised hand indication - ${
this.videoSpanId} does not exist`);
return;
}
this._showRaisedHand = show;
this.updateIndicators();
this.$container.toggleClass('active-speaker', show);
}
/**
@@ -623,19 +403,7 @@ export default class SmallVideo {
*/
remove() {
logger.log('Remove thumbnail', this.id);
this.removeAudioLevelIndicator();
const toolbarContainer
= this.container.querySelector('.videocontainer__toolbar');
if (toolbarContainer) {
ReactDOM.unmountComponentAtNode(toolbarContainer);
}
this.removeConnectionIndicator();
this.removeDisplayName();
this.removeAvatar();
this._unmountIndicators();
this._unmountThumbnail();
// Remove whole container
if (this.container.parentNode) {
@@ -650,76 +418,9 @@ export default class SmallVideo {
* @returns {void}
*/
rerender() {
this.updateIndicators();
this.updateStatusBar();
this.updateView();
}
/**
* Updates the React element responsible for showing connection status, dominant
* speaker, and raised hand icons. Uses instance variables to get the necessary
* state to display. Will create the React element if not already created.
*
* @private
* @returns {void}
*/
updateIndicators() {
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
if (!indicatorToolbar) {
return;
}
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
const iconSize = NORMAL;
const showConnectionIndicator = this.videoIsHovered || !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
const state = APP.store.getState();
const currentLayout = getCurrentLayout(state);
const participantCount = getParticipantCount(state);
let statsPopoverPosition, tooltipPosition;
if (currentLayout === LAYOUTS.TILE_VIEW) {
statsPopoverPosition = 'right top';
tooltipPosition = 'right';
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
statsPopoverPosition = this.statsPopoverLocation;
tooltipPosition = 'left';
} else {
statsPopoverPosition = this.statsPopoverLocation;
tooltipPosition = 'top';
}
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<div>
<AtlasKitThemeProvider mode = 'dark'>
{ this._showConnectionIndicator
? <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
iconSize = { iconSize }
isLocalVideo = { this.isLocal }
enableStatsDisplay = { true }
participantId = { this.id }
statsPopoverPosition = { statsPopoverPosition } />
: null }
<RaisedHandIndicator
iconSize = { iconSize }
participantId = { this.id }
tooltipPosition = { tooltipPosition } />
{ this._showDominantSpeaker && participantCount > 2
? <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
: null }
</AtlasKitThemeProvider>
</div>
</I18nextProvider>
</Provider>,
indicatorToolbar
);
}
/**
* Callback invoked when the thumbnail is clicked and potentially trigger
* pinning of the participant.
@@ -777,18 +478,10 @@ export default class SmallVideo {
}
/**
* Removes the React element responsible for showing connection status, dominant
* speaker, and raised hand icons.
*
* @private
* @returns {void}
* Unmounts the thumbnail.
*/
_unmountIndicators() {
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
if (indicatorToolbar) {
ReactDOM.unmountComponentAtNode(indicatorToolbar);
}
_unmountThumbnail() {
ReactDOM.unmountComponentAtNode(this.container);
}
/**
@@ -802,10 +495,6 @@ export default class SmallVideo {
switch (layout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
this.$container.css('padding-top', `${heightToWidthPercent}%`);
this.$avatar().css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
@@ -815,7 +504,6 @@ export default class SmallVideo {
if (typeof size !== 'undefined') {
const { height, width } = size;
const avatarSize = height / 2;
this.$container.css({
height: `${height}px`,
@@ -823,10 +511,6 @@ export default class SmallVideo {
'min-width': `${width}px`,
width: `${width}px`
});
this.$avatar().css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
break;
}
@@ -836,7 +520,6 @@ export default class SmallVideo {
if (typeof thumbnailSize !== 'undefined') {
const { height, width } = thumbnailSize;
const avatarSize = height / 2;
this.$container.css({
height: `${height}px`,
@@ -844,10 +527,6 @@ export default class SmallVideo {
'min-width': `${width}px`,
width: `${width}px`
});
this.$avatar().css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
break;
}

View File

@@ -1,4 +1,4 @@
/* global APP, $ */
/* global APP, $, interfaceConfig */
import Logger from 'jitsi-meet-logger';
@@ -116,12 +116,6 @@ const VideoLayout = {
* @param lvl the new audio level to update to
*/
setAudioLevel(id, lvl) {
const smallVideo = this.getSmallVideo(id);
if (smallVideo) {
smallVideo.updateAudioLevelIndicator(lvl);
}
if (largeVideo && id === largeVideo.id) {
largeVideo.updateLargeVideoAudioLevel(lvl);
}
@@ -137,19 +131,6 @@ const VideoLayout = {
this._updateLargeVideoIfDisplayed(localId);
},
/**
* Get's the localID of the conference and set it to the local video
* (small one). This needs to be called as early as possible, when muc is
* actually joined. Otherwise events can come with information like email
* and setting them assume the id is already set.
*/
mucJoined() {
// FIXME: replace this call with a generic update call once SmallVideo
// only contains a ReactElement. Then remove this call once the
// Filmstrip is fully in React.
localVideoThumbnail.updateIndicators();
},
/**
* Shows/hides local video.
* @param {boolean} true to make the local video visible, false - otherwise
@@ -264,6 +245,10 @@ const VideoLayout = {
* @returns {void}
*/
onPinChange(pinnedParticipantID) {
if (interfaceConfig.filmStripOnly) {
return;
}
getAllThumbnails().forEach(thumbnail =>
thumbnail.focus(pinnedParticipantID === thumbnail.getId()));
},
@@ -341,22 +326,6 @@ const VideoLayout = {
this._updateLargeVideoIfDisplayed(id, true);
},
/**
* Display name changed.
*/
onDisplayNameChanged(id) {
if (id === 'localVideoContainer'
|| APP.conference.isLocalId(id)) {
localVideoThumbnail.updateDisplayName();
} else {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
remoteVideo.updateDisplayName();
}
}
},
/**
* On dominant speaker changed event.
*
@@ -423,20 +392,6 @@ const VideoLayout = {
}
},
/**
* Hides all the indicators
*/
hideStats() {
for (const video in remoteVideos) { // eslint-disable-line guard-for-in
const remoteVideo = remoteVideos[video];
if (remoteVideo) {
remoteVideo.removeConnectionIndicator();
}
}
localVideoThumbnail.removeConnectionIndicator();
},
removeParticipantContainer(id) {
// Unlock large video
if (this.getPinnedId() === id) {
@@ -487,15 +442,6 @@ const VideoLayout = {
},
changeUserAvatar(id, avatarUrl) {
const smallVideo = VideoLayout.getSmallVideo(id);
if (smallVideo) {
smallVideo.initializeAvatar();
} else {
logger.warn(
`Missed avatar update - no small video yet for ${id}`
);
}
if (this.isCurrentlyOnLarge(id)) {
largeVideo.updateAvatar(avatarUrl);
}

View File

@@ -1,4 +1,4 @@
/* global APP, $ */
/* global APP, $, interfaceConfig */
import Logger from 'jitsi-meet-logger';
@@ -203,12 +203,14 @@ const KeyboardShortcut = {
});
this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
this.registerShortcut('T', null, () => {
sendAnalytics(createShortcutEvent('speaker.stats'));
APP.store.dispatch(toggleDialog(SpeakerStats, {
conference: APP.conference
}));
}, 'keyboardShortcuts.showSpeakerStats');
if (!interfaceConfig.filmStripOnly) {
this.registerShortcut('T', null, () => {
sendAnalytics(createShortcutEvent('speaker.stats'));
APP.store.dispatch(toggleDialog(SpeakerStats, {
conference: APP.conference
}));
}, 'keyboardShortcuts.showSpeakerStats');
}
/**
* FIXME: Currently focus keys are directly implemented below in

4
package-lock.json generated
View File

@@ -10771,8 +10771,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#7fcdcc26ab7b9f05fb460a8485a361fb46943b61",
"from": "github:jitsi/lib-jitsi-meet#7fcdcc26ab7b9f05fb460a8485a361fb46943b61",
"version": "github:jitsi/lib-jitsi-meet#6bb0b86c0a7dd22bb5798236d9b80ca578b28d21",
"from": "github:jitsi/lib-jitsi-meet#6bb0b86c0a7dd22bb5798236d9b80ca578b28d21",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "1.0.3",

View File

@@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#7fcdcc26ab7b9f05fb460a8485a361fb46943b61",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#6bb0b86c0a7dd22bb5798236d9b80ca578b28d21",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",

View File

@@ -298,13 +298,13 @@ export function maybeRedirectToWelcomePage(options: Object = {}) {
return;
}
const { jwt } = getState()['features/base/jwt'];
const { isGuest, jwt } = getState()['features/base/jwt'];
let hashParam;
// save whether current user is guest or not, and pass auth token,
// before navigating to close page
window.sessionStorage.setItem('guest', !jwt);
window.sessionStorage.setItem('guest', isGuest);
window.sessionStorage.setItem('jwt', jwt);
let path = 'close.html';

View File

@@ -128,9 +128,6 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
titleKey: 'dialog.sessTerminated'
}));
if (typeof APP !== 'undefined') {
APP.UI.hideStats();
}
break;
}
case JitsiConferenceErrors.CONNECTION_ERROR: {

View File

@@ -85,14 +85,12 @@ export default [
'disableInviteFunctions',
'disableLocalVideoFlip',
'disableNS',
'disableProfile',
'disableRemoteControl',
'disableRemoteMute',
'disableRtx',
'disableSimulcast',
'disableSuspendVideo',
'disableThirdPartyRequests',
'displayJids',
'doNotStoreRoom',
'e2eping',
'enableDisplayNameInStats',

View File

@@ -54,5 +54,6 @@ export default [
'UNSUPPORTED_BROWSERS',
'VERTICAL_FILMSTRIP',
'VIDEO_LAYOUT_FIT',
'VIDEO_QUALITY_LABEL_DISABLED'
'VIDEO_QUALITY_LABEL_DISABLED',
'filmStripOnly'
];

View File

@@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.99996 2.50002C4.99996 2.03978 5.37306 1.66669 5.83329 1.66669C6.29353 1.66669 6.66663 2.03978 6.66663 2.50002V3.33335H13.3333V2.50002C13.3333 2.03978 13.7064 1.66669 14.1666 1.66669C14.6269 1.66669 15 2.03978 15 2.50002V3.33335H16.6666C17.5871 3.33335 18.3333 4.07955 18.3333 5.00002V16.6667C18.3333 17.5872 17.5871 18.3334 16.6666 18.3334H3.33329C2.41282 18.3334 1.66663 17.5872 1.66663 16.6667V5.00002C1.66663 4.07955 2.41282 3.33335 3.33329 3.33335H4.99996V2.50002ZM3.33329 16.6667V5.00002H16.6666V16.6667H3.33329ZM9.99996 6.66669C9.53972 6.66669 9.16663 7.03978 9.16663 7.50002V10H6.66662C6.20639 10 5.83329 10.3731 5.83329 10.8334C5.83329 11.2936 6.20639 11.6667 6.66662 11.6667H9.16663V14.1667C9.16663 14.6269 9.53972 15 9.99996 15C10.4602 15 10.8333 14.6269 10.8333 14.1667V11.6667H13.3333C13.7935 11.6667 14.1666 11.2936 14.1666 10.8334C14.1666 10.3731 13.7935 10 13.3333 10H10.8333V7.50002C10.8333 7.03978 10.4602 6.66669 9.99996 6.66669Z" fill="#0163FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -11,7 +11,6 @@ export { default as IconAudioOnly } from './visibility.svg';
export { default as IconAudioOnlyOff } from './visibility-off.svg';
export { default as IconAudioRoute } from './volume.svg';
export { default as IconBlurBackground } from './blur-background.svg';
export { default as IconPlusCalendar } from './calendar-plus.svg';
export { default as IconCamera } from './camera.svg';
export { default as IconCameraDisabled } from './camera-disabled.svg';
export { default as IconCancelSelection } from './cancel.svg';

View File

@@ -1,5 +1,7 @@
/* @flow */
import jwtDecode from 'jwt-decode';
import { parseURLParams } from '../util';
/**
@@ -22,7 +24,7 @@ export function parseJWTFromURLParams(url: URL = window.location) {
* @returns {string}
*/
export function getJwtName(state: Object) {
const { user } = state['features/base/jwt'];
const jwtData = jwtDecode(state['features/base/jwt'].jwt);
return user?.name || '';
return jwtData?.context?.user?.name || '';
}

View File

@@ -29,7 +29,10 @@ MiddlewareRegistry.register(store => next => action => {
case SET_CONFIG:
case SET_LOCATION_URL:
// XXX The JSON Web Token (JWT) is not the only piece of state that we
// have decided to store in the feature jwt
// have decided to store in the feature jwt, there is isGuest as well
// which depends on the states of the features base/config and jwt. So
// the JSON Web Token comes from the conference/room's URL and isGuest
// needs a recalculation upon SET_CONFIG as well.
return _setConfigOrLocationURL(store, next, action);
case SET_JWT:
@@ -125,6 +128,12 @@ function _setJWT(store, next, action) {
if (!Object.keys(actionPayload).length) {
if (jwt) {
const {
enableUserRolesBasedOnToken
} = store.getState()['features/base/config'];
action.isGuest = !enableUserRolesBasedOnToken;
let jwtPayload;
try {

View File

@@ -4,6 +4,24 @@ import { equals, ReducerRegistry } from '../redux';
import { SET_JWT } from './actionTypes';
/**
* The default/initial redux state of the feature jwt.
*
* @private
* @type {{
* isGuest: boolean
* }}
*/
const DEFAULT_STATE = {
/**
* The indicator which determines whether the local participant is a guest
* in the conference.
*
* @type {boolean}
*/
isGuest: true
};
/**
* Reduces redux actions which affect the JSON Web Token (JWT) stored in the
* redux store.
@@ -15,12 +33,13 @@ import { SET_JWT } from './actionTypes';
*/
ReducerRegistry.register(
'features/base/jwt',
(state = {}, action) => {
(state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_JWT: {
// eslint-disable-next-line no-unused-vars
const { type, ...payload } = action;
const nextState = {
...DEFAULT_STATE,
...payload
};

View File

@@ -99,6 +99,13 @@ class Video extends Component<Props> {
}
this._attachTrack(this.props.videoTrack);
if (this._videoElement && this.props.autoPlay) {
// Ensure the video gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case video does not autoplay.
this._videoElement.play();
}
}
/**
@@ -142,6 +149,8 @@ class Video extends Component<Props> {
* @returns {ReactElement}
*/
render() {
// NOTE: Maybe we should render null if we don't have video track or if the video track has ended.
return (
<video
autoPlay = { this.props.autoPlay }

View File

@@ -10,6 +10,7 @@ import {
import { APP_STATE_CHANGED } from '../../mobile/background';
import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
import { isRoomValid, SET_ROOM } from '../conference';
import JitsiMeetJS from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux';
import { getPropertyValue } from '../settings';
import { isLocalVideoTrackDesktop, setTrackMuted, TRACK_ADDED } from '../tracks';
@@ -161,33 +162,41 @@ function _setRoom({ dispatch, getState }, next, action) {
// XXX After the introduction of the "Video <-> Voice" toggle on the
// WelcomePage, startAudioOnly is utilized even outside of
// conferences/meetings.
const audioOnly
= Boolean(
getPropertyValue(
state,
'startAudioOnly',
/* sources */ {
// FIXME Practically, base/config is (really) correct
// only if roomIsValid. At the time of this writing,
// base/config is overwritten by URL params which leaves
// base/config incorrect on the WelcomePage after
// leaving a conference which explicitly overwrites
// base/config with URL params.
config: roomIsValid,
let audioOnly;
// XXX We've already overwritten base/config with
// urlParams if roomIsValid. However, settings are more
// important than the server-side config. Consequently,
// we need to read from urlParams anyway. We also
// probably want to read from urlParams when
// !roomIsValid.
urlParams: true,
if (JitsiMeetJS.mediaDevices.supportsVideo()) {
audioOnly
= Boolean(
getPropertyValue(
state,
'startAudioOnly',
/* sources */ {
// FIXME Practically, base/config is (really) correct
// only if roomIsValid. At the time of this writing,
// base/config is overwritten by URL params which leaves
// base/config incorrect on the WelcomePage after
// leaving a conference which explicitly overwrites
// base/config with URL params.
config: roomIsValid,
// The following don't have complications around whether
// they are defined or not:
jwt: false,
settings: true
}));
// XXX We've already overwritten base/config with
// urlParams if roomIsValid. However, settings are more
// important than the server-side config. Consequently,
// we need to read from urlParams anyway. We also
// probably want to read from urlParams when
// !roomIsValid.
urlParams: true,
// The following don't have complications around whether
// they are defined or not:
jwt: false,
settings: true
}));
} else {
// Default to audio-only if the (execution) environment does not
// support (sending and/or receiving) video.
audioOnly = true;
}
sendAnalytics(createStartAudioOnlyEvent(audioOnly));
logger.log(`Start audio only set to ${audioOnly.toString()}`);

View File

@@ -298,9 +298,12 @@ export function isIconUrl(icon: ?string | ?Object) {
*
* @param {Object|Function} stateful - Object or function that can be resolved
* to the Redux state.
* @param {?boolean} ignoreToken - When true we ignore the token check.
* @returns {boolean}
*/
export function isLocalParticipantModerator(stateful: Object | Function) {
export function isLocalParticipantModerator(
stateful: Object | Function,
ignoreToken: ?boolean = false) {
const state = toState(stateful);
const localParticipant = getLocalParticipant(state);
@@ -308,7 +311,11 @@ export function isLocalParticipantModerator(stateful: Object | Function) {
return false;
}
return localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
return (
localParticipant.role === PARTICIPANT_ROLE.MODERATOR
&& (ignoreToken
|| !state['features/base/config'].enableUserRolesBasedOnToken
|| !state['features/base/jwt'].isGuest));
}
/**

View File

@@ -216,10 +216,9 @@ class CopyMeetingUrl extends Component<Props, State> {
*/
function mapStateToProps(state) {
const { enableAutomaticUrlCopy } = state['features/base/config'];
const { customizationReady } = state['features/dynamic-branding'];
return {
url: customizationReady ? getCurrentConferenceUrl(state) : '',
url: getCurrentConferenceUrl(state),
_enableAutomaticUrlCopy: enableAutomaticUrlCopy || false
};
}

View File

@@ -6,7 +6,6 @@ import {
getLocalizedDateFormatter,
getLocalizedDurationFormatter
} from '../../../i18n';
import { Icon, IconTrash } from '../../../icons';
import Container from './Container';
import Text from './Text';
@@ -39,9 +38,9 @@ type Props = {
meetings: Array<Object>,
/**
* Handler for deleting an item.
* Defines what happens when an item in the section list is clicked
*/
onItemDelete?: Function
onItemClick: Function
};
/**
@@ -139,25 +138,6 @@ export default class MeetingsList extends Component<Props> {
return null;
}
_onDelete: Object => Function;
/**
* Returns a function that is used on the onDelete callback.
*
* @param {Object} item - The item to be deleted.
* @private
* @returns {Function}
*/
_onDelete(item) {
const { onItemDelete } = this.props;
return evt => {
evt.stopPropagation();
onItemDelete && onItemDelete(item);
};
}
_renderItem: (Object, number) => React$Node;
/**
@@ -176,7 +156,7 @@ export default class MeetingsList extends Component<Props> {
title,
url
} = meeting;
const { hideURL = false, onItemDelete } = this.props;
const { hideURL = false } = this.props;
const onPress = this._onPress(url);
const rootClassName
= `item ${
@@ -188,10 +168,10 @@ export default class MeetingsList extends Component<Props> {
key = { index }
onClick = { onPress }>
<Container className = 'left-column'>
<Text className = 'title'>
<Text className = 'date'>
{ _toDateString(date) }
</Text>
<Text className = 'subtitle'>
<Text>
{ _toTimeString(time) }
</Text>
</Container>
@@ -207,18 +187,13 @@ export default class MeetingsList extends Component<Props> {
}
{
typeof duration === 'number' ? (
<Text className = 'subtitle'>
<Text>
{ getLocalizedDurationFormatter(duration) }
</Text>) : null
}
</Container>
<Container className = 'actions'>
{ elementAfter || null }
{ onItemDelete && <Icon
className = 'delete-meeting'
onClick = { this._onDelete(meeting) }
src = { IconTrash } />}
</Container>
</Container>
);

View File

@@ -84,7 +84,13 @@ class Watermarks extends Component<Props, State> {
constructor(props: Props) {
super(props);
const showBrandWatermark = interfaceConfig.SHOW_BRAND_WATERMARK;
let showBrandWatermark;
if (interfaceConfig.filmStripOnly) {
showBrandWatermark = false;
} else {
showBrandWatermark = interfaceConfig.SHOW_BRAND_WATERMARK;
}
this.state = {
brandWatermarkLink:
@@ -219,6 +225,7 @@ class Watermarks extends Component<Props, State> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { isGuest } = state['features/base/jwt'];
const {
customizationReady,
customizationFailed,
@@ -231,11 +238,13 @@ function _mapStateToProps(state, ownProps) {
const {
DEFAULT_LOGO_URL,
JITSI_WATERMARK_LINK,
SHOW_JITSI_WATERMARK
SHOW_JITSI_WATERMARK,
SHOW_JITSI_WATERMARK_FOR_GUESTS,
filmStripOnly
} = interfaceConfig;
let _showJitsiWatermark = (
customizationReady && !customizationFailed
&& SHOW_JITSI_WATERMARK)
let _showJitsiWatermark = (!filmStripOnly
&& (customizationReady && !customizationFailed)
&& (SHOW_JITSI_WATERMARK || (isGuest && SHOW_JITSI_WATERMARK_FOR_GUESTS)))
|| !isValidRoom;
let _logoUrl = logoImageUrl;
let _logoLink = logoClickUrl;

View File

@@ -514,7 +514,7 @@ export function urlObjectToString(o: Object): ?string {
// pathname
// Web's ExternalAPI roomName
const room = _fixRoom(o.roomName || o.room);
const room = o.roomName || o.room;
if (room
&& (url.pathname.endsWith('/')

View File

@@ -8,7 +8,6 @@ import {
sendAnalytics
} from '../../analytics';
import { translate } from '../../base/i18n';
import { Icon, IconPlusCalendar } from '../../base/icons';
import { AbstractPage } from '../../base/react';
import { connect } from '../../base/redux';
import { openSettingsDialog, SETTINGS_TABS } from '../../settings';
@@ -186,22 +185,16 @@ class CalendarList extends AbstractPage<Props> {
return (
<div className = 'meetings-list-empty'>
<div className = 'meetings-list-empty-image'>
<img src = './images/calendar.svg' />
</div>
<div className = 'description'>
<p className = 'description'>
{ t('welcomepage.connectCalendarText', {
app: interfaceConfig.APP_NAME,
provider: interfaceConfig.PROVIDER_NAME
}) }
</div>
</p>
<div
className = 'meetings-list-empty-button'
className = 'button'
onClick = { this._onOpenSettings }>
<Icon
className = 'meetings-list-empty-icon'
src = { IconPlusCalendar } />
<span>{ t('welcomepage.connectCalendarButton') }</span>
{ t('welcomepage.connectCalendarButton') }
</div>
</div>
);

View File

@@ -4,7 +4,6 @@ import Tooltip from '@atlaskit/tooltip';
import React, { Component } from 'react';
import { translate } from '../../base/i18n';
import { Icon, IconAdd } from '../../base/icons';
/**
* The type of the React {@code Component} props of {@link JoinButton}.
@@ -61,9 +60,7 @@ class JoinButton extends Component<Props> {
<div
className = 'button join-button'
onClick = { this._onClick }>
<Icon
size = '14'
src = { IconAdd } />
{ t('calendarSync.join') }
</div>
</Tooltip>
);

View File

@@ -152,9 +152,11 @@ StateListenerRegistry.register(
* @returns {void}
*/
function _addChatMsgListener(conference, store) {
if ((typeof APP !== 'undefined' && !isButtonEnabled('chat'))
if ((typeof interfaceConfig === 'object' && interfaceConfig.filmStripOnly)
|| (typeof APP !== 'undefined' && !isButtonEnabled('chat'))
|| store.getState()['features/base/config'].iAmRecorder) {
// We don't register anything on web if the chat button is not enabled in interfaceConfig
// We don't register anything on web if we're in filmStripOnly mode, or
// the chat button is not enabled in interfaceConfig.
// or we are in iAmRecorder mode
return;
}

View File

@@ -14,7 +14,7 @@ import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList, LobbyScreen } from '../../../lobby';
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
import { fullScreenChanged, setToolboxAlwaysVisible, showToolbox } from '../../../toolbox/actions.web';
import { Toolbox } from '../../../toolbox/components/web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { maybeShowSuboptimalExperienceNotification } from '../../functions';
@@ -28,6 +28,7 @@ import Labels from './Labels';
import { default as Notice } from './Notice';
declare var APP: Object;
declare var config: Object;
declare var interfaceConfig: Object;
/**
@@ -174,13 +175,18 @@ class Conference extends AbstractConference<Props, *> {
* @returns {ReactElement}
*/
render() {
const {
// XXX The character casing of the name filmStripOnly utilized by
// interfaceConfig is obsolete but legacy support is required.
filmStripOnly: filmstripOnly
} = interfaceConfig;
const {
_iAmRecorder,
_isLobbyScreenVisible,
_layoutClassName,
_showPrejoin
} = this.props;
const hideLabels = _iAmRecorder;
const hideLabels = filmstripOnly || _iAmRecorder;
return (
<div
@@ -192,18 +198,18 @@ class Conference extends AbstractConference<Props, *> {
<div id = 'videospace'>
<LargeVideo />
<KnockingParticipantList />
<Filmstrip />
<Filmstrip filmstripOnly = { filmstripOnly } />
{ hideLabels || <Labels /> }
</div>
{ _showPrejoin || _isLobbyScreenVisible || <Toolbox /> }
<Chat />
{ filmstripOnly || _showPrejoin || _isLobbyScreenVisible || <Toolbox /> }
{ filmstripOnly || <Chat /> }
{ this.renderNotificationsContainer() }
<CalleeInfoContainer />
{ _showPrejoin && <Prejoin />}
{ !filmstripOnly && _showPrejoin && <Prejoin />}
</div>
);
}
@@ -250,6 +256,9 @@ class Conference extends AbstractConference<Props, *> {
dispatch(connect());
maybeShowSuboptimalExperienceNotification(dispatch, t);
interfaceConfig.filmStripOnly
&& dispatch(setToolboxAlwaysVisible(true));
}
}

View File

@@ -56,7 +56,7 @@ export function maybeOpenFeedbackDialog(conference: Object) {
const state = getState();
const { feedbackPercentage = 100 } = state['features/base/config'];
if (config.iAmRecorder) {
if (interfaceConfig.filmStripOnly || config.iAmRecorder) {
// Intentionally fall through the if chain to prevent further action
// from being taken with regards to showing feedback.
} else if (state['features/base/dialog'].component === FeedbackDialog) {

View File

@@ -17,6 +17,8 @@ import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { setFilmstripHovered, setFilmstripVisible } from '../../actions';
import { shouldRemoteVideosBeVisible } from '../../functions';
import Toolbar from './Toolbar';
declare var APP: Object;
declare var interfaceConfig: Object;
@@ -40,6 +42,11 @@ type Props = {
*/
_columns: number,
/**
* Whether the UI/UX is filmstrip-only.
*/
_filmstripOnly: boolean,
/**
* The width of the filmstrip.
*/
@@ -135,12 +142,14 @@ class Filmstrip extends Component <Props> {
* @inheritdoc
*/
componentDidMount() {
APP.keyboardshortcut.registerShortcut(
'F',
'filmstripPopover',
this._onShortcutToggleFilmstrip,
'keyboardShortcuts.toggleFilmstrip'
);
if (!this.props._filmstripOnly) {
APP.keyboardshortcut.registerShortcut(
'F',
'filmstripPopover',
this._onShortcutToggleFilmstrip,
'keyboardShortcuts.toggleFilmstrip'
);
}
}
/**
@@ -198,7 +207,7 @@ class Filmstrip extends Component <Props> {
let toolbar = null;
if (!this.props._hideToolbar) {
toolbar = this._renderToggleButton();
toolbar = this.props._filmstripOnly ? <Toolbar /> : this._renderToggleButton();
}
return (
@@ -358,20 +367,24 @@ class Filmstrip extends Component <Props> {
function _mapStateToProps(state) {
const { iAmSipGateway } = state['features/base/config'];
const { hovered, visible } = state['features/filmstrip'];
const isFilmstripOnly = Boolean(interfaceConfig.filmStripOnly);
const reduceHeight
= state['features/toolbox'].visible && interfaceConfig.TOOLBAR_BUTTONS.length;
= !isFilmstripOnly && state['features/toolbox'].visible && interfaceConfig.TOOLBAR_BUTTONS.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
reduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''}`.trim();
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
const videosClassName = `filmstrip__videos${
isFilmstripOnly ? ' filmstrip__videos-filmstripOnly' : ''}${
visible ? '' : ' hidden'}`;
const { gridDimensions = {}, filmstripWidth } = state['features/filmstrip'].tileViewDimensions;
return {
_className: className,
_columns: gridDimensions.columns,
_currentLayout: getCurrentLayout(state),
_filmstripOnly: isFilmstripOnly,
_filmstripWidth: filmstripWidth,
_hideScrollbar: Boolean(iAmSipGateway),
_hideToolbar: Boolean(iAmSipGateway),

View File

@@ -0,0 +1,567 @@
// @flow
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import React, { Component } from 'react';
import { AudioLevelIndicator } from '../../../audio-level-indicator';
import { Avatar } from '../../../base/avatar';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
import {
getLocalParticipant,
getParticipantById,
getParticipantCount
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import { getLocalAudioTrack, getLocalVideoTrack, getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import { ConnectionIndicator } from '../../../connection-indicator';
import { DisplayName } from '../../../display-name';
import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
import { PresenceLabel } from '../../../presence-status';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
const JitsiTrackEvents = JitsiMeetJS.events.track;
declare var interfaceConfig: Object;
type State = {
audioLevel: number
};
/**
* The type of the React {@code Component} props of {@link Thumbnail}.
*/
type Props = {
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
_height: number,
_heightToWidthPercent: number,
/**
* The video track that will be displayed in the thumbnail.
*/
_videoTrack: ?Object,
/**
* The audio track related to the participant.
*/
_audioTrack: ?Object,
_width: number,
/**
* The ID of the participant related to the thumbnaul.
*/
_participant: Object,
_defaultLocalDisplayName: string,
_isGuest: boolean,
_participantCount: number,
_isFilmstripOnly: boolean,
_connectionIndicatorDisabled: boolean,
_connectionIndicatorAutoHideEnabled: boolean,
_isDominantSpeakerDisabled: boolean,
participantID: ?string,
isHovered: ?boolean,
dispatch: Function
};
/**
* TODO.
*
* @returns {number}
*/
function _getIndicatorsIconSize() {
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
return NORMAL;
}
/**
* Implements a thumbnail.
*
* @extends Component
*/
class Thumbnail extends Component<Props, State> {
/**
* Initializes a new Thumbnail instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
audioLevel: 0
};
this._updateAudioLevel = this._updateAudioLevel.bind(this);
}
/**
* Starts listening for audio level updates after the initial render.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
this._listenForAudioUpdates();
}
/**
* Stops listening for audio level updates on the old track and starts
* listening instead on the new track.
*
* @inheritdoc
* @returns {void}
*/
componentDidUpdate(prevProps: Props) {
if (prevProps._audioTrack !== this.props._audioTrack) {
this._stopListeningForAudioUpdates(prevProps._audioTrack);
this._listenForAudioUpdates();
this._updateAudioLevel(0);
}
}
/**
* Unsubscribe from audio level updates.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
this._stopListeningForAudioUpdates(this.props._audioTrack);
}
/**
* Starts listening for audio level updates from the library.
*
* @private
* @returns {void}
*/
_listenForAudioUpdates() {
const { _audioTrack } = this.props;
if (_audioTrack) {
const { jitsiTrack } = _audioTrack;
jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
}
}
/**
* Stops listening to further updates from the passed track.
*
* @param {Object} audioTrack - The track.
* @private
* @returns {void}
*/
_stopListeningForAudioUpdates(audioTrack) {
if (audioTrack) {
const { jitsiTrack } = audioTrack;
jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
}
}
_updateAudioLevel: (number) => void;
/**
* Updates the internal state of the last know audio level. The level should
* be between 0 and 1, as the level will be used as a percentage out of 1.
*
* @param {number} audioLevel - The new audio level for the track.
* @private
* @returns {void}
*/
_updateAudioLevel(audioLevel) {
this.setState({
audioLevel
});
}
/**
* Returns an object with the styles for the video container and the avatar container.
*
* @returns {Object} - The styles for the video container and the avatar container.
*/
_getStyles(): Object {
const { _height, _heightToWidthPercent, _currentLayout } = this.props;
let styles;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const avatarSize = _height / 2;
styles = {
avatarContainer: {
height: `${avatarSize}px`,
width: `${avatarSize}px`
}
};
break;
}
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
styles = {
avatarContainer: {
height: '50%',
width: `${_heightToWidthPercent / 2}%`
}
};
break;
}
}
return styles;
}
/**
* Renders a fake participant (youtube video) thumbnail.
*
* @param {string} id - The id of the participant.
* @returns {ReactElement}
*/
_renderFakeParticipant(id) {
return (
<>
<img
className = 'sharedVideoAvatar'
src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
<div className = 'displayNameContainer'>
<DisplayName
elementID = 'sharedVideoContainer_name'
participantID = { id } />
</div>
</>
);
}
/**
* Renders the local participant's thumbnail.
*
* @param {string} id - The ID of the participant.
* @returns {ReactElement}
*/
_renderLocalParticipant(id) {
const styles = this._getStyles();
const {
_participant,
_participantCount,
_videoTrack,
_defaultLocalDisplayName,
_isGuest,
_isFilmstripOnly,
_isDominantSpeakerDisabled,
_connectionIndicatorDisabled,
_connectionIndicatorAutoHideEnabled,
_currentLayout
} = this.props;
const { audioLevel = 0 } = this.state;
const iconSize = _getIndicatorsIconSize();
const showConnectionIndicator = this.props.isHovered || !_connectionIndicatorAutoHideEnabled;
const { dominantSpeaker = false } = _participant;
const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
let statsPopoverPosition, tooltipPosition;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
statsPopoverPosition = 'right top';
tooltipPosition = 'right';
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
statsPopoverPosition = 'left top';
tooltipPosition = 'left';
break;
default:
statsPopoverPosition = 'top center';
tooltipPosition = 'top';
}
return (
<>
<div className = 'videocontainer__background' />
<span id = 'localVideoWrapper'>
<VideoTrack
id = 'localVideo_container'
videoTrack = { _videoTrack } />
</span>
<div className = 'videocontainer__toolbar'>
<StatusIndicators participantID = { id } />
</div>
<div className = 'videocontainer__toptoolbar'>
<div>
<AtlasKitThemeProvider mode = 'dark'>
{ _connectionIndicatorDisabled
? null
: <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
enableStatsDisplay = { !_isFilmstripOnly }
iconSize = { iconSize }
isLocalVideo = { true }
participantId = { id }
statsPopoverPosition = { statsPopoverPosition } />
}
<RaisedHandIndicator
iconSize = { iconSize }
participantId = { id }
tooltipPosition = { tooltipPosition } />
{ showDominantSpeaker && _participantCount > 2
? <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
: null }
</AtlasKitThemeProvider>
</div>
</div>
<div className = 'videocontainer__hoverOverlay' />
<div className = 'displayNameContainer'>
<DisplayName
allowEditing = { _isGuest }
displayNameSuffix = { _defaultLocalDisplayName }
elementID = 'localDisplayName'
participantID = { _participant?.id } />
</div>
<div
className = 'avatar-container'
style = { styles.avatarContainer }>
<Avatar
className = 'userAvatar'
participantId = { id } />
</div>
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
</>
);
}
/**
* Renders a remote participant's 'thumbnail.
*
* @param {string} id - The id of the participant.
* @returns {ReactElement}
*/
_renderRemoteParticipant(id) {
const styles = this._getStyles();
const {
_participant,
_participantCount,
_isFilmstripOnly,
_currentLayout,
_connectionIndicatorDisabled,
_connectionIndicatorAutoHideEnabled,
_isDominantSpeakerDisabled
} = this.props;
const { audioLevel = 0 } = this.state;
const showConnectionIndicator = this.props.isHovered || !_connectionIndicatorAutoHideEnabled;
const { dominantSpeaker = false } = _participant;
const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
const iconSize = _getIndicatorsIconSize();
let remoteMenuPosition, statsPopoverPosition, tooltipPosition;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
statsPopoverPosition = 'right top';
tooltipPosition = 'right';
remoteMenuPosition = 'left top';
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
statsPopoverPosition = 'left bottom';
tooltipPosition = 'left';
remoteMenuPosition = 'left bottom';
break;
default:
statsPopoverPosition = 'top center';
tooltipPosition = 'top';
remoteMenuPosition = 'top center';
}
return (
<>
<div className = 'videocontainer__background' />
<div className = 'videocontainer__toptoolbar'>
<div>
<AtlasKitThemeProvider mode = 'dark'>
{ _connectionIndicatorDisabled
? null
: <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
enableStatsDisplay = { !_isFilmstripOnly }
iconSize = { iconSize }
isLocalVideo = { false }
participantId = { id }
statsPopoverPosition = { statsPopoverPosition } />
}
<RaisedHandIndicator
iconSize = { iconSize }
participantId = { id }
tooltipPosition = { tooltipPosition } />
{ showDominantSpeaker && _participantCount > 2
? <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
: null }
</AtlasKitThemeProvider>
</div>
</div>
<div className = 'videocontainer__toolbar'>
<StatusIndicators participantID = { id } />
</div>
<div className = 'videocontainer__hoverOverlay' />
<div className = 'displayNameContainer'>
<DisplayName
elementID = { `participant_${id}_name` }
participantID = { id } />
</div>
<div
className = 'avatar-container'
style = { styles.avatarContainer }>
<Avatar
className = 'userAvatar'
participantId = { id } />
</div>
<div className = 'presence-label-container'>
<PresenceLabel
className = 'presence-label'
participantID = { id } />
</div>
<span className = 'remotevideomenu'>
{/* <AtlasKitThemeProvider mode = 'dark'>
<RemoteVideoMenuTriggerButton
initialVolumeValue = { initialVolumeValue }
menuPosition = { remoteMenuPosition }
onMenuDisplay = {this._onRemoteVideoMenuDisplay.bind(this)}
onRemoteControlToggle = { onRemoteControlToggle }
onVolumeChange = { onVolumeChange }
participantID = { participantID }
remoteControlState = { remoteControlState } />
</AtlasKitThemeProvider> */}
</span>
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
</>
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _participant } = this.props;
if (!_participant) {
return null;
}
const { id, isFakeParticipant, local = false } = _participant;
if (local) {
return this._renderLocalParticipant(id);
}
if (isFakeParticipant) {
return this._renderFakeParticipant(id);
}
return this._renderRemoteParticipant(id);
}
}
/**
* Maps (parts of) the redux state to the associated props for this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {{
* _videoTrack: Object
* }}
*/
function _mapStateToProps(state, ownProps): Object {
const { participantID } = ownProps;
// Only the local participant won't have id for the time when the conference is not yet joined.
const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
const isLocal = participant?.local ?? true;
const _videoTrack = isLocal
? getLocalVideoTrack(state['features/base/tracks'])
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID);
const _audioTrack = isLocal
? getLocalAudioTrack(state['features/base/tracks'])
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantID);
const _currentLayout = getCurrentLayout(state);
let size = {};
const _isGuest = state['features/base/jwt'].isGuest;
switch (_currentLayout) {
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const {
horizontalViewDimensions = {
local: {},
remote: {}
}
} = state['features/filmstrip'];
const { local, remote } = horizontalViewDimensions;
const { width, height } = isLocal ? local : remote;
size = {
_width: width,
_height: height
};
break;
}
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
size = {
_heightToWidthPercent: isLocal
? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
: 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
};
break;
case LAYOUTS.TILE_VIEW: {
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
size = {
_width: width,
_height: height
};
break;
}
}
return {
_isGuest,
_videoTrack,
_audioTrack,
_currentLayout,
_participant: participant,
_participantCount: getParticipantCount(state),
_isFilmstripOnly: interfaceConfig.filmStripOnly,
_defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
_connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
_connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
...size
};
}
export default connect(_mapStateToProps)(Thumbnail);

View File

@@ -0,0 +1,101 @@
// @flow
import React, { Component } from 'react';
import { connect, equals } from '../../../base/redux';
import { SettingsButton } from '../../../settings';
import {
AudioMuteButton,
HangupButton,
VideoMuteButton
} from '../../../toolbox/components';
declare var interfaceConfig: Object;
// XXX: We are not currently using state here, but in the future, when
// interfaceConfig is part of redux we will. This has to be retrieved from the store.
const visibleButtons = new Set(interfaceConfig.TOOLBAR_BUTTONS);
/**
* The type of the React {@code Component} props of {@link Toolbar}.
*/
type Props = {
/**
* The set of buttons which should be visible in this {@code Toolbar}.
*/
_visibleButtons: Set<string>
};
/**
* Implements the conference toolbar on React/Web for filmstrip-only mode.
*
* @extends Component
*/
class Toolbar extends Component<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<div
className = 'filmstrip-toolbox'
id = 'new-toolbox'>
<HangupButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('hangup') } />
<AudioMuteButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('microphone') } />
<VideoMuteButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('camera') } />
<SettingsButton
tooltipPosition = 'left'
visible = { this._shouldShowButton('fodeviceselection') } />
</div>
);
}
_shouldShowButton: (string) => boolean;
/**
* Returns if a button name has been explicitly configured to be displayed.
*
* @param {string} buttonName - The name of the button, as expected in
* {@link intefaceConfig}.
* @private
* @returns {boolean} True if the button should be displayed, false
* otherwise.
*/
_shouldShowButton(buttonName) {
return this.props._visibleButtons.has(buttonName);
}
}
/**
* Maps (parts of) the redux state to the associated props for this component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _visibleButtons: Set<string>
* }}
*/
function _mapStateToProps(state): Object { // eslint-disable-line no-unused-vars
// XXX: We are not currently using state here, but in the future, when
// interfaceConfig is part of redux we will.
//
// NB: We compute the buttons again here because if URL parameters were used to
// override them we'd miss it.
const buttons = new Set(interfaceConfig.TOOLBAR_BUTTONS);
return {
_visibleButtons: equals(visibleButtons, buttons) ? visibleButtons : buttons
};
}
export default connect(_mapStateToProps)(Toolbar);

View File

@@ -56,6 +56,9 @@ export function shouldRemoteVideosBeVisible(state: Object) {
|| ((pinnedParticipant = getPinnedParticipant(state))
&& pinnedParticipant.local)))
|| (typeof interfaceConfig === 'object'
&& interfaceConfig.filmStripOnly)
|| state['features/base/config'].disable1On1Mode);
}

View File

@@ -352,7 +352,7 @@ export function invitePeopleAndChatRooms( // eslint-disable-line max-params
export function isAddPeopleEnabled(state: Object): boolean {
const { peopleSearchUrl } = state['features/base/config'];
return state['features/base/jwt'].jwt && Boolean(peopleSearchUrl);
return !isGuest(state) && Boolean(peopleSearchUrl);
}
/**
@@ -368,6 +368,16 @@ export function isDialOutEnabled(state: Object): boolean {
&& conference && conference.isSIPCallingSupported();
}
/**
* Determines if the current user is guest or not.
*
* @param {Object} state - Current state.
* @returns {boolean}
*/
export function isGuest(state: Object): boolean {
return state['features/base/jwt'].isGuest;
}
/**
* Checks whether a string looks like it could be for a phone number.
*

View File

@@ -0,0 +1,112 @@
// @flow
import React, { Component } from 'react';
import { Avatar } from '../../../base/avatar';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import OverlayFrame from './OverlayFrame';
/**
* The type of the React {@code Component} props of
* {@link FilmstripOnlyOverlayFrame}.
*/
type Props = {
/**
* The ID of the local participant.
*/
_localParticipantId: string,
/**
* The children components to be displayed into the overlay frame for
* filmstrip only mode.
*/
children: React$Node,
/**
* The css class name for the icon that will be displayed over the avatar.
*/
icon: string,
/**
* Indicates the css style of the overlay. If true, then lighter; darker,
* otherwise.
*/
isLightOverlay: boolean
};
/**
* Implements a React Component for the frame of the overlays in filmstrip only
* mode.
*/
class FilmstripOnlyOverlayFrame extends Component<Props> {
/**
* 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}
*/
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 participantId = { this.props._localParticipantId } />
{
this._renderIcon()
}
</div>
</div>
</OverlayFrame>
);
}
}
/**
* Maps (parts of) the Redux state to the associated FilmstripOnlyOverlayFrame
* props.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _localParticipantId: string
* }}
*/
function _mapStateToProps(state) {
return {
_localParticipantId: (getLocalParticipant(state) || {}).id
};
}
export default connect(_mapStateToProps)(FilmstripOnlyOverlayFrame);

View File

@@ -21,10 +21,44 @@ type Props = {
isLightOverlay?: boolean
};
/**
* The type of the React {@code Component} state of {@link OverlayFrame}.
*/
type State = {
/**
* Whether or not the application is currently displaying in filmstrip only
* mode.
*/
filmstripOnly: boolean
};
/**
* Implements a React {@link Component} for the frame of the overlays.
*/
export default class OverlayFrame extends Component<Props> {
export default class OverlayFrame extends Component<Props, State> {
/**
* Initializes a new AbstractOverlay instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* @public
*/
constructor(props: Props) {
super(props);
this.state = {
/**
* Indicates whether the filmstrip only mode is enabled or not.
*
* @type {boolean}
*/
filmstripOnly:
typeof interfaceConfig !== 'undefined'
&& interfaceConfig.filmStripOnly
};
}
/**
* Implements React's {@link Component#render()}.
*
@@ -32,11 +66,20 @@ export default class OverlayFrame extends Component<Props> {
* @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 = { this.props.isLightOverlay ? 'overlay__container-light' : 'overlay__container' }
className = { containerClass }
id = 'overlay'>
<div className = { 'overlay__content' }>
<div className = { contentClass }>
{
this.props.children
}

View File

@@ -0,0 +1,50 @@
// @flow
import React from 'react';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractPageReloadOverlay, { type Props, abstractMapStateToProps }
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<Props> {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
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>
);
}
_renderButton: () => React$Element<*> | null
_renderProgressBar: () => React$Element<*> | null
}
export default translate(
connect(abstractMapStateToProps)(PageReloadFilmstripOnlyOverlay));

View File

@@ -0,0 +1,41 @@
// @flow
import React from 'react';
import { translate, translateToHTML } from '../../../base/i18n';
import AbstractSuspendedOverlay from './AbstractSuspendedOverlay';
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 AbstractSuspendedOverlay {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
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);

View File

@@ -0,0 +1,54 @@
// @flow
import React from 'react';
import { translate, translateToHTML } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractUserMediaPermissionsOverlay, { abstractMapStateToProps }
from './AbstractUserMediaPermissionsOverlay';
import FilmstripOnlyOverlayFrame from './FilmstripOnlyOverlayFrame';
declare var interfaceConfig: Object;
/**
* Implements a React Component for overlay with guidance how to proceed with
* gUM prompt. This component will be displayed only for filmstrip only mode.
*/
class UserMediaPermissionsFilmstripOnlyOverlay
extends AbstractUserMediaPermissionsOverlay {
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { t } = this.props;
const textKey = `userMedia.${this.props.browser}GrantPermissions`;
return (
<FilmstripOnlyOverlayFrame
icon = 'icon-mic-camera-combined'
isLightOverlay = { true }>
<div className = 'inlay-filmstrip-only__container'>
<div className = 'inlay-filmstrip-only__title'>
{
t('startupoverlay.title',
{ app: interfaceConfig.APP_NAME })
}
</div>
<div className = 'inlay-filmstrip-only__text'>
{
translateToHTML(t, textKey)
}
</div>
</div>
</FilmstripOnlyOverlayFrame>
);
}
}
export default translate(
connect(abstractMapStateToProps)(UserMediaPermissionsFilmstripOnlyOverlay));

View File

@@ -32,7 +32,8 @@ class UserMediaPermissionsOverlay extends AbstractUserMediaPermissionsOverlay {
<span className = 'inlay__icon icon-camera' />
<h3 className = 'inlay__title'>
{
t('startupoverlay.genericTitle')
t('startupoverlay.title',
{ app: interfaceConfig.APP_NAME })
}
</h3>
<span className = 'inlay__text'>

View File

@@ -1,7 +1,19 @@
// @flow
export { default as FilmstripOnlyOverlayFrame } from './FilmstripOnlyOverlayFrame';
export { default as OverlayFrame } from './OverlayFrame';
export {
default as PageReloadFilmstripOnlyOverlay
} from './PageReloadFilmstripOnlyOverlay';
export { default as PageReloadOverlay } from './PageReloadOverlay';
export {
default as SuspendedFilmstripOnlyOverlay
} from './SuspendedFilmstripOnlyOverlay';
export { default as SuspendedOverlay } from './SuspendedOverlay';
export { default as UserMediaPermissionsOverlay } from './UserMediaPermissionsOverlay';
export {
default as UserMediaPermissionsFilmstripOnlyOverlay
} from './UserMediaPermissionsFilmstripOnlyOverlay';
export {
default as UserMediaPermissionsOverlay
} from './UserMediaPermissionsOverlay';

View File

@@ -1,8 +1,11 @@
// @flow
import {
PageReloadFilmstripOnlyOverlay,
PageReloadOverlay,
SuspendedFilmstripOnlyOverlay,
SuspendedOverlay,
UserMediaPermissionsFilmstripOnlyOverlay,
UserMediaPermissionsOverlay
} from './components/web';
@@ -14,9 +17,22 @@ declare var interfaceConfig: Object;
* @returns {Array<Object>}
*/
export function getOverlays(): Array<Object> {
return [
PageReloadOverlay,
const overlays = [
SuspendedOverlay,
UserMediaPermissionsOverlay
];
const filmstripOnly
= typeof interfaceConfig === 'object' && interfaceConfig.filmStripOnly;
if (filmstripOnly) {
overlays.push(
PageReloadFilmstripOnlyOverlay,
SuspendedFilmstripOnlyOverlay,
UserMediaPermissionsFilmstripOnlyOverlay);
} else {
overlays.push(PageReloadOverlay);
}
return overlays;
}

View File

@@ -6,7 +6,6 @@ import type { Dispatch } from 'redux';
import { translate } from '../../base/i18n';
import { MeetingsList } from '../../base/react';
import { connect } from '../../base/redux';
import { deleteRecentListEntry } from '../actions';
import { isRecentListEnabled, toDisplayableList } from '../functions';
import AbstractRecentList from './AbstractRecentList';
@@ -56,19 +55,6 @@ class RecentList extends AbstractRecentList<Props> {
this._getRenderListEmptyComponent
= this._getRenderListEmptyComponent.bind(this);
this._onPress = this._onPress.bind(this);
this._onItemDelete = this._onItemDelete.bind(this);
}
_onItemDelete: Object => void;
/**
* Deletes a recent entry.
*
* @param {Object} entry - The entry to be deleted.
* @inheritdoc
*/
_onItemDelete(entry) {
this.props.dispatch(deleteRecentListEntry(entry));
}
/**
@@ -92,7 +78,6 @@ class RecentList extends AbstractRecentList<Props> {
hideURL = { true }
listEmptyComponent = { this._getRenderListEmptyComponent() }
meetings = { recentList }
onItemDelete = { this._onItemDelete }
onPress = { this._onPress } />
);
}

View File

@@ -12,7 +12,7 @@ import { parseURIString, safeDecodeURIComponent } from '../base/util';
*/
export function toDisplayableList(recentList) {
return (
recentList.reverse()
recentList.slice(-3).reverse()
.map(item => {
return {
date: item.date,

View File

@@ -3,10 +3,7 @@
import { openDialog } from '../../../base/dialog';
import { IconLiveStreaming } from '../../../base/icons';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import {
getLocalParticipant,
isLocalParticipantModerator
} from '../../../base/participants';
import { getLocalParticipant } from '../../../base/participants';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { getActiveSession } from '../../functions';
@@ -128,14 +125,13 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
// If the containing component provides the visible prop, that is one
// above all, but if not, the button should be autonomus and decide on
// its own to be visible or not.
const isModerator = isLocalParticipantModerator(state);
const {
enableFeaturesBasedOnToken,
liveStreamingEnabled
} = state['features/base/config'];
const { features = {} } = getLocalParticipant(state);
visible = isModerator && liveStreamingEnabled;
visible = liveStreamingEnabled;
if (enableFeaturesBasedOnToken) {
visible = visible && String(features.livestreaming) === 'true';
@@ -144,7 +140,13 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
if (!visible && !_disabled) {
_disabled = true;
visible = true;
_tooltip = 'dialog.liveStreamingDisabledTooltip';
// button and tooltip
if (state['features/base/jwt'].isGuest) {
_tooltip = 'dialog.liveStreamingDisabledForGuestTooltip';
} else {
_tooltip = 'dialog.liveStreamingDisabledTooltip';
}
}
}
}

View File

@@ -153,7 +153,13 @@ export function _mapStateToProps(state: Object, ownProps: Props): Object {
if (!visible && !_disabled) {
_disabled = true;
visible = true;
_tooltip = 'dialog.recordingDisabledTooltip';
// button and tooltip
if (state['features/base/jwt'].isGuest) {
_tooltip = 'dialog.recordingDisabledForGuestTooltip';
} else {
_tooltip = 'dialog.recordingDisabledTooltip';
}
}
}
}

View File

@@ -1,102 +0,0 @@
// @flow
import React from 'react';
import { Dialog } from '../../base/dialog';
import { getLocalParticipant, getParticipantDisplayName } from '../../base/participants';
import { muteAllParticipants } from '../actions';
import AbstractMuteRemoteParticipantDialog, {
type Props as AbstractProps
} from './AbstractMuteRemoteParticipantDialog';
/**
* The type of the React {@code Component} props of
* {@link AbstractMuteEveryoneDialog}.
*/
export type Props = AbstractProps & {
content: string,
exclude: Array<string>,
title: string
};
/**
*
* An abstract Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
*
* @extends AbstractMuteRemoteParticipantDialog
*/
export default class AbstractMuteEveryoneDialog<P: Props> extends AbstractMuteRemoteParticipantDialog<P> {
static defaultProps = {
exclude: [],
muteLocal: false
};
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { content, title } = this.props;
return (
<Dialog
okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit }
titleString = { title }
width = 'small'>
<div>
{ content }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
/**
* Callback to be invoked when the value of this dialog is submitted.
*
* @returns {boolean}
*/
_onSubmit() {
const {
dispatch,
exclude
} = this.props;
dispatch(muteAllParticipants(exclude));
return true;
}
}
/**
* Maps (parts of) the Redux state to the associated {@code AbstractMuteEveryoneDialog}'s props.
*
* @param {Object} state - The redux state.
* @param {Object} ownProps - The properties explicitly passed to the component.
* @returns {Props}
*/
export function abstractMapStateToProps(state: Object, ownProps: Props) {
const { exclude, t } = ownProps;
const whom = exclude
// eslint-disable-next-line no-confusing-arrow
.map(id => id === getLocalParticipant(state).id
? t('dialog.muteEveryoneSelf')
: getParticipantDisplayName(state, id))
.join(', ');
return whom.length ? {
content: t('dialog.muteEveryoneElseDialog'),
title: t('dialog.muteEveryoneElseTitle', { whom })
} : {
content: t('dialog.muteEveryoneDialog'),
title: t('dialog.muteEveryoneTitle')
};
}

View File

@@ -1,48 +0,0 @@
// @flow
import { createToolbarEvent, sendAnalytics } from '../../analytics';
import { openDialog } from '../../base/dialog';
import { IconMuteEveryone } from '../../base/icons';
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox/components';
import { MuteEveryoneDialog } from '.';
export type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The ID of the participant object that this button is supposed to keep unmuted.
*/
participantID: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
/**
* An abstract remote video menu button which mutes all the other participants.
*/
export default class AbstractMuteEveryoneElseButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.muteEveryoneElse';
icon = IconMuteEveryone;
label = 'videothumbnail.domuteOthers';
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
sendAnalytics(createToolbarEvent('mute.everyoneelse.pressed'));
dispatch(openDialog(MuteEveryoneDialog, { exclude: [ participantID ] }));
}
}

View File

@@ -1,67 +0,0 @@
// @flow
import React from 'react';
import { Text } from 'react-native';
import { ColorSchemeRegistry } from '../../../base/color-scheme';
import { ConfirmDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import { StyleType } from '../../../base/styles';
import AbstractMuteEveryoneDialog, {
abstractMapStateToProps,
type Props as AbstractProps } from '../AbstractMuteEveryoneDialog';
type Props = AbstractProps & {
/**
* The color-schemed stylesheet of the base/dialog feature.
*/
_dialogStyles: StyleType
}
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
*
* @extends AbstractMuteEveryoneDialog
*/
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
return (
<ConfirmDialog
okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit } >
<Text style = { this.props._dialogStyles.text }>
{ `${this.props.title} \n\n ${this.props.content}` }
</Text>
</ConfirmDialog>
);
}
_onSubmit: () => boolean;
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {{
* _dialogStyles: StyleType
* }}
*/
function _mapStateToProps(state: Object, ownProps: Props) {
return {
...abstractMapStateToProps(state, ownProps),
_dialogStyles: ColorSchemeRegistry.get(state, 'Dialog')
};
}
export default translate(connect(_mapStateToProps)(MuteEveryoneDialog));

View File

@@ -1,20 +0,0 @@
// @flow
import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import AbstractMuteEveryoneElseButton from '../AbstractMuteEveryoneElseButton';
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
visible: isLocalParticipantModerator(state)
};
}
export default translate(connect(_mapStateToProps)(AbstractMuteEveryoneElseButton));

View File

@@ -16,7 +16,6 @@ import { hideRemoteVideoMenu } from '../../actions';
import GrantModeratorButton from './GrantModeratorButton';
import KickButton from './KickButton';
import MuteButton from './MuteButton';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import PinButton from './PinButton';
import styles from './styles';
@@ -105,7 +104,6 @@ class RemoteVideoMenu extends PureComponent<Props> {
<GrantModeratorButton { ...buttonProps } />
<PinButton { ...buttonProps } />
<PrivateMessageButton { ...buttonProps } />
<MuteEveryoneElseButton { ...buttonProps } />
</BottomSheet>
);
}

View File

@@ -1,7 +1,12 @@
// @flow
export { default as GrantModeratorDialog } from './GrantModeratorDialog';
export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
export {
default as GrantModeratorDialog
} from './GrantModeratorDialog';
export {
default as KickRemoteParticipantDialog
} from './KickRemoteParticipantDialog';
export {
default as MuteRemoteParticipantDialog
} from './MuteRemoteParticipantDialog';
export { default as RemoteVideoMenu } from './RemoteVideoMenu';

View File

@@ -4,6 +4,7 @@ import React from 'react';
import { translate } from '../../../base/i18n';
import { IconKick } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractKickButton, {
type Props
} from '../AbstractKickButton';
@@ -41,7 +42,11 @@ class KickButton extends AbstractKickButton {
* @returns {ReactElement}
*/
render() {
const { participantID, t } = this.props;
const { participantID, t, visible } = this.props;
if (!visible) {
return null;
}
return (
<RemoteVideoMenuButton
@@ -56,4 +61,22 @@ class KickButton extends AbstractKickButton {
_handleClick: () => void
}
export default translate(KickButton);
/**
* Maps (parts of) the redux state to {@link KickButton}'s React {@code Component}
* props.
*
* @param {Object} state - The redux store/state.
* @private
* @returns {Object}
*/
function _mapStateToProps(state: Object) {
const shouldHide = interfaceConfig.HIDE_KICK_BUTTON_FOR_GUESTS && state['features/base/jwt'].isGuest;
return {
visible: !shouldHide
};
}
export default translate(connect(_mapStateToProps)(KickButton));

View File

@@ -5,15 +5,53 @@ import React from 'react';
import { Dialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { connect } from '../../../base/redux';
import AbstractMuteEveryoneDialog, { abstractMapStateToProps, type Props } from '../AbstractMuteEveryoneDialog';
import { muteAllParticipants } from '../../actions';
import AbstractMuteRemoteParticipantDialog, {
type Props as AbstractProps
} from '../AbstractMuteRemoteParticipantDialog';
declare var APP: Object;
/**
* The type of the React {@code Component} props of
* {@link MuteEveryoneDialog}.
*/
type Props = AbstractProps & {
/**
* The IDs of the remote participants to exclude from being muted.
*/
exclude: Array<string>
};
/**
* Translations needed for dialog rendering.
*/
type Translations = {
/**
* Content text.
*/
content: string,
/**
* Title text.
*/
title: string
}
/**
* A React Component with the contents for a dialog that asks for confirmation
* from the user before muting all remote participants.
* from the user before muting a remote participant.
*
* @extends AbstractMuteEveryoneDialog
* @extends Component
*/
class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
class MuteEveryoneDialog extends AbstractMuteRemoteParticipantDialog<Props> {
static defaultProps = {
exclude: [],
muteLocal: false
};
/**
* Implements React's {@link Component#render()}.
*
@@ -21,20 +59,64 @@ class MuteEveryoneDialog extends AbstractMuteEveryoneDialog<Props> {
* @returns {ReactElement}
*/
render() {
const { content, title } = this._getTranslations();
return (
<Dialog
okKey = 'dialog.muteParticipantButton'
onSubmit = { this._onSubmit }
titleString = { this.props.title }
titleString = { title }
width = 'small'>
<div>
{ this.props.content }
{ content }
</div>
</Dialog>
);
}
_onSubmit: () => boolean;
/**
* Callback to be invoked when the value of this dialog is submitted.
*
* @returns {boolean}
*/
_onSubmit() {
const {
dispatch,
exclude
} = this.props;
dispatch(muteAllParticipants(exclude));
return true;
}
/**
* Method to get translations depending on whether we have an exclusive
* mute or not.
*
* @returns {Translations}
* @private
*/
_getTranslations(): Translations {
const { exclude, t } = this.props;
const { conference } = APP;
const whom = exclude
// eslint-disable-next-line no-confusing-arrow
.map(id => conference.isLocalId(id)
? t('dialog.muteEveryoneSelf')
: conference.getParticipantDisplayName(id))
.join(', ');
return whom.length ? {
content: t('dialog.muteEveryoneElseDialog'),
title: t('dialog.muteEveryoneElseTitle', { whom })
} : {
content: t('dialog.muteEveryoneDialog'),
title: t('dialog.muteEveryoneTitle')
};
}
}
export default translate(connect(abstractMapStateToProps)(MuteEveryoneDialog));
export default translate(connect()(MuteEveryoneDialog));

View File

@@ -2,13 +2,17 @@
import React from 'react';
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { IconMuteEveryoneElse } from '../../../base/icons';
import { connect } from '../../../base/redux';
import AbstractMuteEveryoneElseButton, {
import AbstractMuteButton, {
_mapStateToProps,
type Props
} from '../AbstractMuteEveryoneElseButton';
} from '../AbstractMuteButton';
import MuteEveryoneDialog from './MuteEveryoneDialog';
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
/**
@@ -16,9 +20,9 @@ import RemoteVideoMenuButton from './RemoteVideoMenuButton';
* every participant in the conference except the one with the given
* participantID
*/
class MuteEveryoneElseButton extends AbstractMuteEveryoneElseButton {
class MuteEveryoneElseButton extends AbstractMuteButton {
/**
* Instantiates a new {@code Component}.
* Instantiates a new {@code MuteEveryoneElseButton}.
*
* @inheritdoc
*/
@@ -49,6 +53,19 @@ class MuteEveryoneElseButton extends AbstractMuteEveryoneElseButton {
}
_handleClick: () => void;
/**
* Handles clicking / pressing the button, and opens a confirmation dialog.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch, participantID } = this.props;
sendAnalytics(createToolbarEvent('mute.everyoneelse.pressed'));
dispatch(openDialog(MuteEveryoneDialog, { exclude: [ participantID ] }));
}
}
export default translate(connect()(MuteEveryoneElseButton));
export default translate(connect(_mapStateToProps)(MuteEveryoneElseButton));

View File

@@ -9,11 +9,10 @@ import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { isRemoteTrackMuted } from '../../../base/tracks';
import MuteEveryoneElseButton from './MuteEveryoneElseButton';
import {
GrantModeratorButton,
MuteButton,
MuteEveryoneElseButton,
KickButton,
PrivateMessageMenuButton,
RemoteControlButton,

View File

@@ -1,15 +1,26 @@
// @flow
export { default as GrantModeratorButton } from './GrantModeratorButton';
export { default as GrantModeratorDialog } from './GrantModeratorDialog';
export {
default as GrantModeratorDialog
} from './GrantModeratorDialog';
export { default as KickButton } from './KickButton';
export { default as KickRemoteParticipantDialog } from './KickRemoteParticipantDialog';
export {
default as KickRemoteParticipantDialog
} from './KickRemoteParticipantDialog';
export { default as MuteButton } from './MuteButton';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
export { default as MuteEveryoneElseButton } from './MuteEveryoneElseButton';
export { default as MuteRemoteParticipantDialog } from './MuteRemoteParticipantDialog';
export { default as MuteEveryoneDialog } from './MuteEveryoneDialog';
export {
default as MuteRemoteParticipantDialog
} from './MuteRemoteParticipantDialog';
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
export { REMOTE_CONTROL_MENU_STATES, default as RemoteControlButton } from './RemoteControlButton';
export {
REMOTE_CONTROL_MENU_STATES,
default as RemoteControlButton
} from './RemoteControlButton';
export { default as RemoteVideoMenu } from './RemoteVideoMenu';
export { default as RemoteVideoMenuTriggerButton } from './RemoteVideoMenuTriggerButton';
export {
default as RemoteVideoMenuTriggerButton
} from './RemoteVideoMenuTriggerButton';
export { default as VolumeSlider } from './VolumeSlider';

View File

@@ -104,7 +104,7 @@ class PasswordRequiredPrompt extends Component<Props, State> {
name = 'lockKey'
onChange = { this._onPasswordChanged }
shouldFitContainer = { true }
type = 'password'
type = 'text'
value = { this.state.password } />
</div>
);

View File

@@ -154,15 +154,9 @@ function PasswordSection({
<a
className = 'remove-password'
onClick = { onPasswordRemove }>{ t('dialog.Remove') }</a>
{
// There are cases like lobby and grant moderator when password is not available
password ? <>
<a
className = 'copy-password'
onClick = { onPasswordCopy }>{ t('dialog.copy') }</a>
</> : null
}
<a
className = 'copy-password'
onClick = { onPasswordCopy }>{ t('dialog.copy') }</a>
</>
);
}

View File

@@ -125,10 +125,13 @@ function mapStateToProps(state) {
locked,
password
} = state['features/base/conference'];
const { roomPasswordNumberOfDigits } = state['features/base/config'];
const {
lockRoomGuestEnabled,
roomPasswordNumberOfDigits
} = state['features/base/config'];
return {
_canEditPassword: isLocalParticipantModerator(state),
_canEditPassword: isLocalParticipantModerator(state, lockRoomGuestEnabled),
_conference: conference,
_dialIn: state['features/invite'],
_locked: locked,

View File

@@ -5,14 +5,22 @@ import { translate } from '../../../base/i18n';
import { IconSettings } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { openDeviceSelectionPopup } from '../../../device-selection';
import { openSettingsDialog } from '../../actions';
import { SETTINGS_TABS } from '../../constants';
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} props of {@link SettingsButton}.
*/
type Props = AbstractButtonProps & {
/**
* Whether we are in filmstrip only mode or not.
*/
_filmstripOnly: boolean,
/**
* The default tab at which the settings dialog will be opened.
*/
@@ -41,12 +49,36 @@ class SettingsButton extends AbstractButton<Props, *> {
*/
_handleClick() {
const {
_filmstripOnly,
defaultTab = SETTINGS_TABS.DEVICES,
dispatch } = this.props;
sendAnalytics(createToolbarEvent('settings'));
dispatch(openSettingsDialog(defaultTab));
if (_filmstripOnly) {
dispatch(openDeviceSelectionPopup());
} else {
dispatch(openSettingsDialog(defaultTab));
}
}
}
export default translate(connect()(SettingsButton));
/**
* Maps (parts of) the redux state to the associated props for the
* {@code SettingsButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _filmstripOnly: boolean
* }}
*/
function _mapStateToProps(state): Object { // eslint-disable-line no-unused-vars
// XXX: We are not currently using state here, but in the future, when
// interfaceConfig is part of redux we will.
return {
_filmstripOnly: Boolean(interfaceConfig.filmStripOnly)
};
}
export default translate(connect(_mapStateToProps)(SettingsButton));

View File

@@ -126,13 +126,14 @@ class SettingsDialog extends Component<Props> {
*/
function _mapStateToProps(state) {
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
const jwt = state['features/base/jwt'];
// The settings sections to display.
const showDeviceSettings = configuredTabs.includes('devices');
const moreTabProps = getMoreTabProps(state);
const { showModeratorSettings, showLanguageSettings, showPrejoinSettings } = moreTabProps;
const showProfileSettings
= configuredTabs.includes('profile') && !state['features/base/config'].disableProfile;
= configuredTabs.includes('profile') && jwt.isGuest;
const showCalendarSettings
= configuredTabs.includes('calendar') && isCalendarEnabled(state);
const tabs = [];

View File

@@ -141,7 +141,14 @@ export default class JitsiStreamPresenterEffect {
timeMs: 1000 / this._frameRate
});
return this._canvas.captureStream(this._frameRate);
const capturedStream = this._canvas.captureStream(this._frameRate);
// Put emphasis on the text details for the presenter's stream
// See https://www.w3.org/TR/mst-content-hint/
// $FlowExpectedError
capturedStream.getVideoTracks()[0].contentHint = 'text';
return capturedStream;
}
/**

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