Compare commits

...

11 Commits

Author SHA1 Message Date
damencho
b022ce60ac fix: Optimizes hot paths in prosody modules, string comparisons. 2020-11-06 12:55:02 -06:00
damencho
20ce38bd4c feat: Show cc button for ongoing transcribed meetings for guests 2020-11-06 08:12:59 -06:00
damencho
c4ba97e87c feat: Drop lockRoomGuestEnabled. 2020-11-06 08:12:59 -06:00
damencho
4b8aae90e0 feat: Drop HIDE_KICK_BUTTON_FOR_GUESTS setting.
The main config contains disableRemoteMute and remoteVideoMenu: { disableKick: true} options, which can be used.
2020-11-06 08:12:59 -06:00
damencho
c2539bf615 feat: Drop buttons tooltips specific to guests. 2020-11-06 08:12:59 -06:00
damencho
4fdd4b66f7 fix: Hide copy password if it is not available. Fixes #7783 2020-11-06 08:12:59 -06:00
damencho
9fa29d7353 feat: Profile tab does not depend on isGuest.
Introduced a config property to disable profile.
2020-11-06 08:12:59 -06:00
damencho
c14f639639 feat: Drops SHOW_JITSI_WATERMARK_FOR_GUESTS and SHOW_WATERMARK_FOR_GUESTS. 2020-11-06 08:12:59 -06:00
damencho
c007477ee9 fix: Show livestream button only for moderators. 2020-11-06 08:12:59 -06:00
Andrei Bora
50997ae6ac Stringify boolean values from jwt user context 2020-11-06 06:15:45 -06:00
Vlad Piersec
f8a41aea9c feat(welcome_page): Redesign welcome page 2020-11-06 13:50:30 +02:00
48 changed files with 687 additions and 413 deletions

View File

@@ -359,17 +359,12 @@ var config = {
// Default language for the user interface.
// defaultLanguage: 'en',
// 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,
// Disables profile and the edit of all fields from the profile settings (display name and email)
// disableProfile: 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

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

View File

@@ -30,6 +30,67 @@
}
@media only screen and (max-width: $verySmallScreen) {
.welcome {
#enter_room {
position: relative;
height: 42px;
.welcome-page-button {
font-size: 16px;
left: 0;
position: absolute;
top: 68px;
text-align: center;
width: 100%;
}
}
.header {
background: #06345E;
background-image: linear-gradient(180deg, rgba(8, 110, 202, 0.8) 0%, rgba(8, 110, 202, 0) 100%);
#enter_room {
.enter-room-input-container {
padding-right: 0;
}
.warning-without-link,
.warning-with-link {
top: 120px;
}
}
}
.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;
}
}
#videoResolutionLabel {
display: none;
}

View File

@@ -161,71 +161,47 @@ $unsupportedDesktopBrowserTextFontSize: 21px;
/**
* The size of the default watermark.
*/
$watermarkWidth: 186px;
$watermarkHeight: 74px;
$watermarkWidth: 71px;
$watermarkHeight: 32px;
$welcomePageWatermarkWidth: 186px;
$welcomePageWatermarkHeight: 74px;
$welcomePageWatermarkWidth: 71px;
$welcomePageWatermarkHeight: 32px;
/**
* Welcome page variables.
*/
$welcomePageDescriptionColor: #fff;
$welcomePageFontFamily: inherit;
$welcomePageBackground: linear-gradient(-90deg, #1251AE 0%, #0074FF 50%, #1251AE 100%);
$welcomePageBackground: none;
$welcomePageTitleColor: #fff;
$welcomePageHeaderBackground: none;
$welcomePageHeaderBackgroundSmall: none;
$welcomePageHeaderBackground: linear-gradient(0deg, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), url('/images/welcome-background.png');
$welcomePageHeaderBackgroundPosition: none;
$welcomePageHeaderBackgroundRepeat: none;
$welcomePageHeaderBackgroundSize: none;
$welcomePageHeaderBackgroundSize: cover;
$welcomePageHeaderPaddingBottom: 0px;
$welcomePageHeaderMinHeight: fit-content;
$welcomePageHeaderTitleMaxWidth: initial;
$welcomePageHeaderTextAlign: center;
$welcomePageHeaderTextMarginTop: 35px;
$welcomePageHeaderTextMarginBottom: 35px;
$welcomePageHeaderTextDisplay: flex;
$welcomePageHeaderTextWidth: 650px;
$welcomePageHeaderContainerDisplay: flex;
$welcomePageHeaderContainerMargin: 146px 32px 0 32px;
$welcomePageHeaderTextTitleMarginBottom: 16px;
$welcomePageHeaderTextTitleFontSize: 2.5rem;
$welcomePageHeaderTextTitleFontWeight: 500;
$welcomePageHeaderTextTitleLineHeight: 1.18;
$welcomePageHeaderTextTitleMarginBottom: 0;
$welcomePageHeaderTextTitleFontSize: 42px;
$welcomePageHeaderTextTitleFontWeight: normal;
$welcomePageHeaderTextTitleLineHeight: 50px;
$welcomePageHeaderTextTitleOpacity: 1;
$welcomePageHeaderTextDescriptionDisplay: inherit;
$welcomePageHeaderTextDescriptionFontSize: 1rem;
$welcomePageHeaderTextDescriptionFontWeight: 400;
$welcomePageHeaderTextDescriptionLineHeight: 24px;
$welcomePageHeaderTextDescriptionMarginBottom: 20px;
$welcomePageHeaderTextDescriptionAlignSelf: inherit;
$welcomePageEnterRoomDisplay: flex;
$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;
$welcomePageEnterRoomWidth: calc(100% - 32px);
$welcomePageEnterRoomPadding: 4px;
$welcomePageEnterRoomMargin: 0 auto;
$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,6 +5,7 @@ body.welcome-page {
.welcome {
background-image: $welcomePageBackground;
background-color: #fff;
display: flex;
flex-direction: column;
font-family: $welcomePageFontFamily;
@@ -18,21 +19,15 @@ body.welcome-page {
background-repeat: $welcomePageHeaderBackgroundRepeat;
background-size: $welcomePageHeaderBackgroundSize;
padding-bottom: $welcomePageHeaderPaddingBottom;
align-items: center;
display: flex;
flex-direction: column;
min-height: $welcomePageHeaderMinHeight;
background-color: #002637;
height: 480px;
overflow: hidden;
position: relative;
text-align: center;
.header-text {
display: $welcomePageHeaderTextDisplay;
.header-container {
display: $welcomePageHeaderContainerDisplay;
flex-direction: column;
margin-top: $watermarkHeight + $welcomePageHeaderTextMarginTop;
margin-bottom: $welcomePageHeaderTextMarginBottom;
max-width: calc(100% - 40px);
width: $welcomePageHeaderTextWidth;
margin: $welcomePageHeaderContainerMargin;
z-index: $zindex2;
}
@@ -42,50 +37,52 @@ body.welcome-page {
font-weight: $welcomePageHeaderTextTitleFontWeight;
line-height: $welcomePageHeaderTextTitleLineHeight;
margin-bottom: $welcomePageHeaderTextTitleMarginBottom;
max-width: $welcomePageHeaderTitleMaxWidth;
opacity: $welcomePageHeaderTextTitleOpacity;
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;
.header-text-subtitle {
color: #fff;
font-size: 20px;
font-weight: 600;
line-height: 26px;
margin: 16px 0 32px 0;
text-align: $welcomePageHeaderTextAlign;
}
#enter_room {
display: $welcomePageEnterRoomDisplay;
align-items: center;
max-width: calc(100% - 40px);
max-width: 480px;
width: $welcomePageEnterRoomWidth;
z-index: $zindex2;
background-color: #fff;
padding: $welcomePageEnterRoomPadding;
border-radius: $welcomePageEnterRoomBorderRadius;
border-radius: 4px;
margin: $welcomePageEnterRoomMargin;
.enter-room-input-container {
width: 100%;
padding: $welcomePageEnterRoomInputContainerPadding;
text-align: left;
color: #253858;
flex-grow: 1;
height: fit-content;
.enter-room-title {
display: $welcomePageEnterRoomTitleDisplay;
font-size: 18px;
font-weight: bold;
padding-bottom: 5px;
}
padding-right: 4px;
position: relative;
.enter-room-input {
border-width: $welcomePageEnterRoomInputContainerBorderWidth;
border-style: $welcomePageEnterRoomInputContainerBorderStyle;
border-image: $welcomePageEnterRoomInputContainerBorderImage;
border: 0;
background: #fff;
display: inline-block;
height: 50px;
width: 100%;
font-size: 14px;
padding-left: 10px;
&:focus {
outline: auto 2px #005fcc;
}
}
.insecure-room-name-warning {
@@ -109,16 +106,28 @@ 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: left;
text-align: $welcomePageHeaderTextAlign;
a {
color: inherit;
@@ -126,76 +135,70 @@ body.welcome-page {
}
}
}
}
.tab-container {
font-size: 16px;
.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;
position: relative;
text-align: left;
min-height: 354px;
width: 710px;
background: #75A7E7;
display: $welcomePageTabContainerDisplay;
flex-direction: column;
}
.tab-content{
display: $welcomePageTabContentDisplay;
margin: 5px 0px;
overflow: hidden;
.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;
flex-grow: 1;
position: relative;
margin: 2px;
padding: 7px 0;
text-align: center;
> * {
position: absolute;
&.selected {
background-color: #FFF;
}
}
.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 {
width: $welcomePageButtonWidth;
min-width: $welcomePageButtonMinWidth;
height: $welcomePageButtonHeight;
font-size: $welcomePageButtonFontSize;
font-weight: $welcomePageButtonFontWeight;
border: 0;
font-size: 14px;
background: #0074E0;
border-radius: $welcomePageButtonBorderRadius;
border-radius: 3px;
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;
@@ -217,4 +220,83 @@ body.welcome-page {
height: $welcomePageWatermarkHeight;
}
}
&.without-content {
.welcome-card {
min-width: 500px;
}
}
.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;
}
}

BIN
images/app-store-badge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

21
images/calendar.svg Normal file
View File

@@ -0,0 +1,21 @@
<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>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
images/f-droid-badge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

8
images/watermark.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

View File

@@ -182,6 +182,7 @@
<!--#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.png',
DEFAULT_LOGO_URL: 'images/watermark.svg',
DEFAULT_REMOTE_DISPLAY_NAME: 'Fellow Jitster',
DEFAULT_WELCOME_PAGE_LOGO_URL: 'images/watermark.png',
DEFAULT_WELCOME_PAGE_LOGO_URL: 'images/watermark.svg',
DISABLE_DOMINANT_SPEAKER_INDICATOR: false,
@@ -86,7 +86,9 @@ var interfaceConfig = {
*/
DISABLE_VIDEO_BACKGROUND: false,
DISPLAY_WELCOME_PAGE_CONTENT: true,
DISPLAY_WELCOME_FOOTER: true,
DISPLAY_WELCOME_PAGE_ADDITIONAL_CARD: false,
DISPLAY_WELCOME_PAGE_CONTENT: false,
DISPLAY_WELCOME_PAGE_TOOLBAR_ADDITIONAL_CONTENT: false,
ENABLE_DIAL_OUT: true,
@@ -136,6 +138,21 @@ 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
@@ -169,7 +186,6 @@ 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
@@ -223,27 +239,12 @@ 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

@@ -220,7 +220,6 @@
"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)",
@@ -255,7 +254,6 @@
"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!",
@@ -287,7 +285,6 @@
"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!",
@@ -878,6 +875,8 @@
"goSmall": "GO",
"info": "Dial-in info",
"join": "CREATE / JOIN",
"jitsiMeet": "Jitsi Meet",
"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",
@@ -888,6 +887,8 @@
"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",
"secureMeetings": "Secure and high quality meetings",
"startMeeting": "Start meeting",
"terms": "Terms",
"title": "Secure, fully featured, and completely free video conferencing"
},

View File

@@ -104,7 +104,7 @@ export default class LocalVideo extends SmallVideo {
}
this._renderDisplayName({
allowEditing: APP.store.getState()['features/base/jwt'].isGuest,
allowEditing: !config.disableProfile,
displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
elementID: 'localDisplayName',
participantID: this.id

View File

@@ -298,13 +298,13 @@ export function maybeRedirectToWelcomePage(options: Object = {}) {
return;
}
const { isGuest, jwt } = getState()['features/base/jwt'];
const { 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', isGuest);
window.sessionStorage.setItem('guest', !jwt);
window.sessionStorage.setItem('jwt', jwt);
let path = 'close.html';

View File

@@ -85,6 +85,7 @@ export default [
'disableInviteFunctions',
'disableLocalVideoFlip',
'disableNS',
'disableProfile',
'disableRemoteControl',
'disableRemoteMute',
'disableRtx',

View File

@@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

@@ -29,10 +29,7 @@ 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, 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.
// have decided to store in the feature jwt
return _setConfigOrLocationURL(store, next, action);
case SET_JWT:
@@ -128,12 +125,6 @@ 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,24 +4,6 @@ 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.
@@ -33,13 +15,12 @@ const DEFAULT_STATE = {
*/
ReducerRegistry.register(
'features/base/jwt',
(state = DEFAULT_STATE, action) => {
(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

@@ -298,12 +298,9 @@ 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,
ignoreToken: ?boolean = false) {
export function isLocalParticipantModerator(stateful: Object | Function) {
const state = toState(stateful);
const localParticipant = getLocalParticipant(state);
@@ -311,11 +308,7 @@ export function isLocalParticipantModerator(
return false;
}
return (
localParticipant.role === PARTICIPANT_ROLE.MODERATOR
&& (ignoreToken
|| !state['features/base/config'].enableUserRolesBasedOnToken
|| !state['features/base/jwt'].isGuest));
return localParticipant.role === PARTICIPANT_ROLE.MODERATOR;
}
/**

View File

@@ -6,6 +6,7 @@ import {
getLocalizedDateFormatter,
getLocalizedDurationFormatter
} from '../../../i18n';
import { Icon, IconTrash } from '../../../icons';
import Container from './Container';
import Text from './Text';
@@ -38,9 +39,9 @@ type Props = {
meetings: Array<Object>,
/**
* Defines what happens when an item in the section list is clicked
* Handler for deleting an item.
*/
onItemClick: Function
onItemDelete?: Function
};
/**
@@ -138,6 +139,25 @@ 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;
/**
@@ -156,7 +176,7 @@ export default class MeetingsList extends Component<Props> {
title,
url
} = meeting;
const { hideURL = false } = this.props;
const { hideURL = false, onItemDelete } = this.props;
const onPress = this._onPress(url);
const rootClassName
= `item ${
@@ -168,10 +188,10 @@ export default class MeetingsList extends Component<Props> {
key = { index }
onClick = { onPress }>
<Container className = 'left-column'>
<Text className = 'date'>
<Text className = 'title'>
{ _toDateString(date) }
</Text>
<Text>
<Text className = 'subtitle'>
{ _toTimeString(time) }
</Text>
</Container>
@@ -187,13 +207,18 @@ export default class MeetingsList extends Component<Props> {
}
{
typeof duration === 'number' ? (
<Text>
<Text className = 'subtitle'>
{ 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

@@ -225,7 +225,6 @@ class Watermarks extends Component<Props, State> {
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { isGuest } = state['features/base/jwt'];
const {
customizationReady,
customizationFailed,
@@ -239,12 +238,11 @@ function _mapStateToProps(state, ownProps) {
DEFAULT_LOGO_URL,
JITSI_WATERMARK_LINK,
SHOW_JITSI_WATERMARK,
SHOW_JITSI_WATERMARK_FOR_GUESTS,
filmStripOnly
} = interfaceConfig;
let _showJitsiWatermark = (!filmStripOnly
&& (customizationReady && !customizationFailed)
&& (SHOW_JITSI_WATERMARK || (isGuest && SHOW_JITSI_WATERMARK_FOR_GUESTS)))
&& SHOW_JITSI_WATERMARK)
|| !isValidRoom;
let _logoUrl = logoImageUrl;
let _logoLink = logoClickUrl;

View File

@@ -8,6 +8,7 @@ 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';
@@ -185,16 +186,22 @@ class CalendarList extends AbstractPage<Props> {
return (
<div className = 'meetings-list-empty'>
<p className = 'description'>
<div className = 'meetings-list-empty-image'>
<img src = '/images/calendar.svg' />
</div>
<div className = 'description'>
{ t('welcomepage.connectCalendarText', {
app: interfaceConfig.APP_NAME,
provider: interfaceConfig.PROVIDER_NAME
}) }
</p>
</div>
<div
className = 'button'
className = 'meetings-list-empty-button'
onClick = { this._onOpenSettings }>
{ t('welcomepage.connectCalendarButton') }
<Icon
className = 'meetings-list-empty-icon'
src = { IconPlusCalendar } />
<span>{ t('welcomepage.connectCalendarButton') }</span>
</div>
</div>
);

View File

@@ -4,6 +4,7 @@ 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}.
@@ -60,7 +61,9 @@ class JoinButton extends Component<Props> {
<div
className = 'button join-button'
onClick = { this._onClick }>
{ t('calendarSync.join') }
<Icon
size = '14'
src = { IconAdd } />
</div>
</Tooltip>
);

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 !isGuest(state) && Boolean(peopleSearchUrl);
return state['features/base/jwt'].jwt && Boolean(peopleSearchUrl);
}
/**
@@ -368,16 +368,6 @@ 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

@@ -6,6 +6,7 @@ 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';
@@ -55,6 +56,19 @@ 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));
}
/**
@@ -78,6 +92,7 @@ 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.slice(-3).reverse()
recentList.reverse()
.map(item => {
return {
date: item.date,

View File

@@ -3,7 +3,10 @@
import { openDialog } from '../../../base/dialog';
import { IconLiveStreaming } from '../../../base/icons';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { getLocalParticipant } from '../../../base/participants';
import {
getLocalParticipant,
isLocalParticipantModerator
} from '../../../base/participants';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { getActiveSession } from '../../functions';
@@ -125,13 +128,14 @@ 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 = liveStreamingEnabled;
visible = isModerator && liveStreamingEnabled;
if (enableFeaturesBasedOnToken) {
visible = visible && String(features.livestreaming) === 'true';
@@ -140,13 +144,7 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
if (!visible && !_disabled) {
_disabled = true;
visible = true;
// button and tooltip
if (state['features/base/jwt'].isGuest) {
_tooltip = 'dialog.liveStreamingDisabledForGuestTooltip';
} else {
_tooltip = 'dialog.liveStreamingDisabledTooltip';
}
_tooltip = 'dialog.liveStreamingDisabledTooltip';
}
}
}

View File

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

View File

@@ -4,7 +4,6 @@ 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';
@@ -42,11 +41,7 @@ class KickButton extends AbstractKickButton {
* @returns {ReactElement}
*/
render() {
const { participantID, t, visible } = this.props;
if (!visible) {
return null;
}
const { participantID, t } = this.props;
return (
<RemoteVideoMenuButton
@@ -61,22 +56,4 @@ class KickButton extends AbstractKickButton {
_handleClick: () => void
}
/**
* 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));
export default translate(KickButton);

View File

@@ -154,9 +154,15 @@ function PasswordSection({
<a
className = 'remove-password'
onClick = { onPasswordRemove }>{ t('dialog.Remove') }</a>
<a
className = 'copy-password'
onClick = { onPasswordCopy }>{ t('dialog.copy') }</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
}
</>
);
}

View File

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

View File

@@ -126,14 +126,13 @@ 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') && jwt.isGuest;
= configuredTabs.includes('profile') && !state['features/base/config'].disableProfile;
const showCalendarSettings
= configuredTabs.includes('calendar') && isCalendarEnabled(state);
const tabs = [];

View File

@@ -85,7 +85,12 @@ export class AbstractClosedCaptionButton
export function _abstractMapStateToProps(state: Object, ownProps: Object) {
const { _requestingSubtitles } = state['features/subtitles'];
const { transcribingEnabled } = state['features/base/config'];
const { visible = Boolean(transcribingEnabled && isLocalParticipantModerator(state)) } = ownProps;
const { isTranscribing } = state['features/transcribing'];
// if the participant is moderator, it can enable transcriptions and if
// transcriptions are already started for the meeting, guests can just show them
const { visible = Boolean(transcribingEnabled
&& (isLocalParticipantModerator(state) || isTranscribing)) } = ownProps;
return {
_requestingSubtitles,

View File

@@ -31,14 +31,12 @@ class ClosedCaptionButton
* @returns {Props}
*/
export function mapStateToProps(state: Object, ownProps: Object) {
const { transcribingEnabled } = state['features/base/config'];
const { isGuest = true } = state['features/base/jwt'];
const enabled = getFeatureFlag(state, CLOSE_CAPTIONS_ENABLED, true) && transcribingEnabled && !isGuest;
const { visible = enabled } = ownProps;
const enabled = getFeatureFlag(state, CLOSE_CAPTIONS_ENABLED, true);
const abstractProps = _abstractMapStateToProps(state, ownProps);
return {
..._abstractMapStateToProps(state, ownProps),
visible
...abstractProps,
visible: abstractProps.visible && enabled
};
}

View File

@@ -120,7 +120,7 @@ class OverflowMenuProfileItem extends Component<Props> {
function _mapStateToProps(state) {
return {
_localParticipant: getLocalParticipant(state),
_unclickable: !state['features/base/jwt'].isGuest
_unclickable: state['features/base/config'].disableProfile
|| !interfaceConfig.SETTINGS_SECTIONS.includes('profile')
};
}

View File

@@ -128,16 +128,16 @@ type Props = {
*/
_fullScreen: boolean,
/**
* Whether or not the profile is disabled.
*/
_isProfileDisabled: boolean,
/**
* Whether or not the tile view is enabled.
*/
_tileViewEnabled: boolean,
/**
* Whether or not the current user is logged in through a JWT.
*/
_isGuest: boolean,
/**
* Whether or not the current meeting belongs to a JaaS user.
*/
@@ -993,7 +993,7 @@ class Toolbox extends Component<Props, State> {
* @returns {boolean}
*/
_isProfileVisible() {
return this.props._isGuest && this._shouldShowButton('profile');
return !this.props._isProfileDisabled && this._shouldShowButton('profile');
}
/**
@@ -1426,15 +1426,7 @@ function _mapStateToProps(state) {
desktopSharingEnabled = getParticipants(state)
.find(({ features = {} }) =>
String(features['screen-sharing']) === 'true') !== undefined;
// we want to show button and tooltip
if (state['features/base/jwt'].isGuest) {
desktopSharingDisabledTooltipKey
= 'dialog.shareYourScreenDisabledForGuest';
} else {
desktopSharingDisabledTooltipKey
= 'dialog.shareYourScreenDisabled';
}
desktopSharingDisabledTooltipKey = 'dialog.shareYourScreenDisabled';
}
// NB: We compute the buttons again here because if URL parameters were used to
@@ -1448,7 +1440,7 @@ function _mapStateToProps(state) {
_desktopSharingDisabledTooltipKey: desktopSharingDisabledTooltipKey,
_dialog: Boolean(state['features/base/dialog'].component),
_feedbackConfigured: Boolean(callStatsID),
_isGuest: state['features/base/jwt'].isGuest,
_isProfileDisabled: Boolean(state['features/base/config'].disableProfile),
_isVpaasMeeting: isVpaasMeeting(state),
_fullScreen: fullScreen,
_tileViewEnabled: shouldDisplayTileView(state),

View File

@@ -47,9 +47,6 @@ export default class Tabs extends Component<Props> {
return (
<div className = 'tab-container'>
<div className = 'tab-content'>
{ content }
</div>
{ tabs.length > 1 ? (
<div className = 'tab-buttons'>
{
@@ -64,6 +61,9 @@ export default class Tabs extends Component<Props> {
}
</div>) : null
}
<div className = 'tab-content'>
{ content }
</div>
</div>
);
}

View File

@@ -20,12 +20,6 @@ import Tabs from './Tabs';
*/
export const ROOM_NAME_VALIDATE_PATTERN_STR = '^[^?&:\u0022\u0027%#]+$';
/**
* Maximum number of pixels corresponding to a mobile layout.
* @type {number}
*/
const WINDOW_WIDTH_THRESHOLD = 425;
/**
* The Web container rendering the welcome page.
*
@@ -78,6 +72,17 @@ class WelcomePage extends AbstractWelcomePage {
*/
this._additionalToolbarContentRef = null;
this._additionalCardRef = null;
/**
* The template to use as the additional card displayed near the main one.
*
* @private
* @type {HTMLTemplateElement|null}
*/
this._additionalCardTemplate = document.getElementById(
'welcome-page-additional-card-template');
/**
* The template to use as the main content for the welcome page. If
* not found then only the welcome page head will display.
@@ -102,12 +107,14 @@ class WelcomePage extends AbstractWelcomePage {
// Bind event handlers so they are only bound once per instance.
this._onFormSubmit = this._onFormSubmit.bind(this);
this._onRoomChange = this._onRoomChange.bind(this);
this._setAdditionalCardRef = this._setAdditionalCardRef.bind(this);
this._setAdditionalContentRef
= this._setAdditionalContentRef.bind(this);
this._setRoomInputRef = this._setRoomInputRef.bind(this);
this._setAdditionalToolbarContentRef
= this._setAdditionalToolbarContentRef.bind(this);
this._onTabSelected = this._onTabSelected.bind(this);
this._renderFooter = this._renderFooter.bind(this);
}
/**
@@ -137,6 +144,12 @@ class WelcomePage extends AbstractWelcomePage {
this._additionalToolbarContentTemplate.content.cloneNode(true)
);
}
if (this._shouldShowAdditionalCard()) {
this._additionalCardRef.appendChild(
this._additionalCardTemplate.content.cloneNode(true)
);
}
}
/**
@@ -159,10 +172,10 @@ class WelcomePage extends AbstractWelcomePage {
*/
render() {
const { _moderatedRoomServiceUrl, t } = this.props;
const { APP_NAME, DEFAULT_WELCOME_PAGE_LOGO_URL } = interfaceConfig;
const { DEFAULT_WELCOME_PAGE_LOGO_URL, DISPLAY_WELCOME_FOOTER } = interfaceConfig;
const showAdditionalCard = this._shouldShowAdditionalCard();
const showAdditionalContent = this._shouldShowAdditionalContent();
const showAdditionalToolbarContent = this._shouldShowAdditionalToolbarContent();
const showResponsiveText = this._shouldShowResponsiveText();
return (
<div
@@ -172,6 +185,7 @@ class WelcomePage extends AbstractWelcomePage {
<div className = 'welcome-watermark'>
<Watermarks defaultJitsiLogoURL = { DEFAULT_WELCOME_PAGE_LOGO_URL } />
</div>
<div className = 'header'>
<div className = 'welcome-page-settings'>
<SettingsButton
@@ -184,64 +198,82 @@ class WelcomePage extends AbstractWelcomePage {
}
</div>
<div className = 'header-image' />
<div className = 'header-text'>
<div className = 'header-container'>
<h1 className = 'header-text-title'>
{ t('welcomepage.title') }
{ t('welcomepage.jitsiMeet') }
</h1>
<p className = 'header-text-description'>
{ t('welcomepage.appDescription',
{ app: APP_NAME }) }
</p>
</div>
<div id = 'enter_room'>
<div className = 'enter-room-input-container'>
<div className = 'enter-room-title'>
{ t('welcomepage.enterRoomTitle') }
<span className = 'header-text-subtitle'>
{ t('welcomepage.secureMeetings')}
</span>
<div id = 'enter_room'>
<div className = 'enter-room-input-container'>
<form onSubmit = { this._onFormSubmit }>
<input
aria-disabled = 'false'
aria-label = 'Meeting name input'
autoFocus = { true }
className = 'enter-room-input'
id = 'enter_room_field'
onChange = { this._onRoomChange }
pattern = { ROOM_NAME_VALIDATE_PATTERN_STR }
placeholder = { this.state.roomPlaceholder }
ref = { this._setRoomInputRef }
title = { t('welcomepage.roomNameAllowedChars') }
type = 'text'
value = { this.state.room } />
<div
className = { _moderatedRoomServiceUrl
? 'warning-with-link'
: 'warning-without-link' }>
{ this._renderInsecureRoomNameWarning() }
</div>
</form>
</div>
<form onSubmit = { this._onFormSubmit }>
<input
autoFocus = { true }
className = 'enter-room-input'
id = 'enter_room_field'
onChange = { this._onRoomChange }
pattern = { ROOM_NAME_VALIDATE_PATTERN_STR }
placeholder = { this.state.roomPlaceholder }
ref = { this._setRoomInputRef }
title = { t('welcomepage.roomNameAllowedChars') }
type = 'text'
value = { this.state.room } />
{ this._renderInsecureRoomNameWarning() }
</form>
<button
aria-disabled = 'false'
aria-label = 'Start meeting'
className = 'welcome-page-button'
id = 'enter_room_button'
onClick = { this._onFormSubmit }
tabIndex = '0'
type = 'button'>
{ t('welcomepage.startMeeting') }
</button>
</div>
<div
className = 'welcome-page-button'
id = 'enter_room_button'
onClick = { this._onFormSubmit }>
{
showResponsiveText
? t('welcomepage.goSmall')
: t('welcomepage.go')
}
</div>
</div>
{ _moderatedRoomServiceUrl && (
<div id = 'moderated-meetings'>
<p>
{
translateToHTML(
{ _moderatedRoomServiceUrl && (
<div id = 'moderated-meetings'>
<p>
{
translateToHTML(
t, 'welcomepage.moderatedMessage', { url: _moderatedRoomServiceUrl })
}
</p>
</div>
) }
{ this._renderTabs() }
}
</p>
</div>)}
</div>
</div>
{ showAdditionalContent
? <div
className = 'welcome-page-content'
ref = { this._setAdditionalContentRef } />
: null }
<div className = 'welcome-cards-container'>
<div className = 'welcome-card-row'>
<div className = 'welcome-tabs welcome-card welcome-card--blue'>
{ this._renderTabs() }
</div>
{ showAdditionalCard
? <div
className = 'welcome-card welcome-card--dark'
ref = { this._setAdditionalCardRef } />
: null }
</div>
{ showAdditionalContent
? <div
className = 'welcome-page-content'
ref = { this._setAdditionalContentRef } />
: null }
</div>
{ DISPLAY_WELCOME_FOOTER && this._renderFooter()}
</div>
);
}
@@ -302,6 +334,45 @@ class WelcomePage extends AbstractWelcomePage {
this.setState({ selectedTab: tabIndex });
}
/**
* Renders the footer.
*
* @returns {ReactElement}
*/
_renderFooter() {
const { t } = this.props;
const {
MOBILE_DOWNLOAD_LINK_ANDROID,
MOBILE_DOWNLOAD_LINK_F_DROID,
MOBILE_DOWNLOAD_LINK_IOS
} = interfaceConfig;
return (<footer className = 'welcome-footer'>
<div className = 'welcome-footer-centered'>
<div className = 'welcome-footer-padded'>
<div className = 'welcome-footer-row-block welcome-footer--row-1'>
<div className = 'welcome-footer-row-1-text'>{t('welcomepage.jitsiOnMobile')}</div>
<a
className = 'welcome-badge'
href = { MOBILE_DOWNLOAD_LINK_IOS }>
<img src = './images/app-store-badge.png' />
</a>
<a
className = 'welcome-badge'
href = { MOBILE_DOWNLOAD_LINK_ANDROID }>
<img src = './images/google-play-badge.png' />
</a>
<a
className = 'welcome-badge'
href = { MOBILE_DOWNLOAD_LINK_F_DROID }>
<img src = './images/f-droid-badge.png' />
</a>
</div>
</div>
</div>
</footer>);
}
/**
* Renders tabs to show previous meetings and upcoming calendar events. The
* tabs are purposefully hidden on mobile browsers.
@@ -342,6 +413,19 @@ class WelcomePage extends AbstractWelcomePage {
tabs = { tabs } />);
}
/**
* Sets the internal reference to the HTMLDivElement used to hold the
* additional card shown near the tabs card.
*
* @param {HTMLDivElement} el - The HTMLElement for the div that is the root
* of the welcome page content.
* @private
* @returns {void}
*/
_setAdditionalCardRef(el) {
this._additionalCardRef = el;
}
/**
* Sets the internal reference to the HTMLDivElement used to hold the
* welcome page content.
@@ -380,6 +464,19 @@ class WelcomePage extends AbstractWelcomePage {
this._roomInputRef = el;
}
/**
* Returns whether or not an additional card should be displayed near the tabs.
*
* @private
* @returns {boolean}
*/
_shouldShowAdditionalCard() {
return interfaceConfig.DISPLAY_WELCOME_PAGE_ADDITIONAL_CARD
&& this._additionalCardTemplate
&& this._additionalCardTemplate.content
&& this._additionalCardTemplate.innerHTML.trim();
}
/**
* Returns whether or not additional content should be displayed below
* the welcome page's header for entering a room name.
@@ -407,20 +504,6 @@ class WelcomePage extends AbstractWelcomePage {
&& this._additionalToolbarContentTemplate.content
&& this._additionalToolbarContentTemplate.innerHTML.trim();
}
/**
* Returns whether or not the screen has a size smaller than a custom margin
* and therefore display different text in the go button.
*
* @private
* @returns {boolean}
*/
_shouldShowResponsiveText() {
const { innerWidth } = window;
return innerWidth <= WINDOW_WIDTH_THRESHOLD;
}
}
export default translate(connect(_mapStateToProps)(WelcomePage));

View File

@@ -1,6 +1,8 @@
local jid = require "util.jid";
local um_is_admin = require "core.usermanager".is_admin;
local is_healthcheck_room = module:require "util".is_healthcheck_room;
local util = module:require "util";
local is_healthcheck_room = util.is_healthcheck_room;
local extract_subdomain = util.extract_subdomain;
local moderated_subdomains;
local moderated_rooms;
@@ -22,11 +24,14 @@ end
-- -> true, room_name, subdomain
-- -> true, room_name, nil (if no subdomain is used for the room)
local function is_moderated(room_jid)
if #moderated_subdomains == 0 and #moderated_rooms == 0 then
return false;
end
local room_node = jid.node(room_jid);
-- parses bare room address, for multidomain expected format is:
-- [subdomain]roomName@conference.domain
local target_subdomain, target_room_name = room_node:match("^%[([^%]]+)%](.+)$");
local target_subdomain, target_room_name = extract_subdomain(room_node);
if target_subdomain then
if moderated_subdomains:contains(target_subdomain) then
return true, target_room_name, target_subdomain;

View File

@@ -1,5 +1,6 @@
local ext_events = module:require "ext_events"
local jid = require "util.jid"
local extract_subdomain = module:require "util".extract_subdomain;
-- Options and configuration
local poltergeist_component = module:get_option_string(
@@ -33,7 +34,7 @@ local function url_from_room_jid(room_jid)
local node, _, _ = jid.split(room_jid)
if not node then return nil end
local target_subdomain, target_node = node:match("^%[([^%]]+)%](.+)$")
local target_subdomain, target_node = extract_subdomain(node);
if not(target_node or target_subdomain) then
return "https://"..muc_domain_base.."/"..node

View File

@@ -9,7 +9,9 @@ local jid = require "util.jid";
local json_safe = require "cjson.safe";
local path = require "util.paths";
local sha256 = require "util.hashes".sha256;
local http_get_with_retry = module:require "util".http_get_with_retry;
local main_util = module:require "util";
local http_get_with_retry = main_util.http_get_with_retry;
local extract_subdomain = main_util.extract_subdomain;
local nr_retries = 3;
@@ -350,7 +352,7 @@ function Util:verify_room(session, room_address)
local room_node = jid.node(room_address);
-- parses bare room address, for multidomain expected format is:
-- [subdomain]roomName@conference.domain
local target_subdomain, target_room = room_node:match("^%[([^%]]+)%](.+)$");
local target_subdomain, target_room = extract_subdomain(room_node);
-- if we have '*' as room name in token, this means all rooms are allowed
-- so we will use the actual name of the room when constructing strings

View File

@@ -33,6 +33,12 @@ local roomless_iqs = {};
-- (e.g. from room1@conference.foo.example.com/res returns (room1, example.com, res, foo))
local function room_jid_split_subdomain(room_jid)
local node, host, resource = jid.split(room_jid);
-- optimization, skip matching if there is no subdomain or it is not the muc component address at all
if host == muc_domain or not starts_with(host, muc_domain_prefix) then
return node, host, resource;
end
local target_subdomain = host and host:match(target_subdomain_pattern);
return node, host, resource, target_subdomain
end
@@ -80,11 +86,13 @@ local function internal_room_jid_match_rewrite(room_jid, stanza)
return room_jid;
end
local target_subdomain, target_node = node:match("^%[([^%]]+)%](.+)$");
local target_subdomain, target_node = extract_subdomain(node);
if not (target_node and target_subdomain) then
-- module:log("debug", "Not rewriting... unexpected node format: %s", node);
return room_jid;
end
-- Ok, rewrite room_jid address to pretty format
local new_node, new_host, new_resource = target_node, muc_domain_prefix..".".. target_subdomain.."."..muc_domain_base, resource;
room_jid = jid.join(new_node, new_host, new_resource);
@@ -183,6 +191,7 @@ function update_presence_identity(
stanza:tag("identity"):tag("user");
for k, v in pairs(user) do
v = tostring(v)
stanza:tag(k):text(v):up();
end
stanza:up();
@@ -225,6 +234,17 @@ function is_feature_allowed(session, feature)
end
end
--- Extracts the subdomain and room name from internal jid node [foo]room1
-- @return subdomain(optional, if extracted or nil), the room name
function extract_subdomain(room_node)
-- optimization, skip matching if there is no subdomain, no [subdomain] part in the beginning of the node
if not starts_with(room_node, '[') then
return room_node;
end
return room_node:match("^%[([^%]]+)%](.+)$");
end
function starts_with(str, start)
return str:sub(1, #start) == start
end
@@ -305,6 +325,7 @@ function http_get_with_retry(url, retry)
end
return {
extract_subdomain = extract_subdomain;
is_feature_allowed = is_feature_allowed;
is_healthcheck_room = is_healthcheck_room;
get_room_from_jid = get_room_from_jid;

View File

@@ -0,0 +1 @@
<template id = "welcome-page-additional-card-template"></template>