Compare commits

..

28 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
Avram Tudor
88c02fb658 Merge pull request #8039 from jitsi/tavram/fix-double-slash
fix(jaas) fix double slash for branded invite urls
2020-11-05 16:04:59 +02:00
Tudor-Ovidiu Avram
0f64c66f91 fix(jaas) fix double slash for branded invite urls 2020-11-05 15:43:53 +02:00
Дамян Минков
9f65ae52f1 fix: Prosody modules - drop unused and duplicate code and drop chatty debug statements (#8027)
* chore: Updates mod_smacks.lua version to latest.

https://hg.prosody.im/prosody-modules/file/db75772afb28/mod_smacks/mod_smacks.lua

* Drop unused modules.

* Update docs.

* Move utility functions away from domain mapper.

* Remove some chatty debug log messages.

* Drops not needed patch for mod_websocket.
2020-11-04 08:25:03 -06:00
Vlad Piersec
a242e86b23 chore(deps) amplitude 7.3.1 2020-11-04 16:18:06 +02:00
Saúl Ibarra Corretgé
4211db0893 fix(rn, screen-sharing) don't render own screen-share in large view 2020-11-04 14:45:16 +01:00
Titus-Andrei Moldovan
9a35026d6a feat(android) add screen-sharing support
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
Co-authored-by: zycwind <391321232@qq.com>
2020-11-04 14:45:16 +01:00
slauth
9742e90bb5 allow wildcard in token issuer verification 2020-11-03 10:45:47 -06:00
Saúl Ibarra Corretgé
2a01d3550c fix(conference) remove no longer needed code
There is no need for setting the availability of desktop sharing anymore. It can
now be detected on the spot.

The reson for the previous code was that way back when browser extensions were
needed, it was possible to start a conference without desktopo sharing support
and get it afterwards. This is no longer the case.
2020-11-03 16:15:01 +01:00
rugk
efce5a831b feat(misc) automatically assign feature-request tag to issues
…if they are reported as a feature request, of course.
2020-11-03 09:41:20 +01:00
damencho
e0117e03e8 feat: Reduce pings and adds xmpp ping config. 2020-11-02 16:57:45 -06:00
Hristo Terezov
1648e4b407 ref(thumbnail): mutedWhileDisconnected -> redux 2020-11-02 13:49:36 -06:00
Vlad Piersec
b02136d013 feat(prejoin): Add name from jwt to prejoin screen 2020-10-30 13:09:18 -05:00
damencho
734631a7a4 fix: Avoids storing lobby room instance in the main room object.
We sometimes see "error   Top-level error, please report:
                  /usr/lib/prosody/util/serialization.lua:38: Can't serialize table: table has multiple references".
This also slows down restarting prosody.
2020-10-30 12:52:08 -05:00
Vlad Piersec
9fbb35b6e1 feat(vpaas): Track vpaas conference join 2020-10-30 09:26:21 +02:00
Hristo Terezov
f45af351d8 ref(thumbnail): use connectionStatus from redux. 2020-10-29 12:05:40 -05:00
JB Joatton
1f4cd22875 fix(lang): update fr translation 2020-10-29 16:27:47 +01:00
Adão Santos
53cc724b3b fix(lang) update ptBR translation 2020-10-29 13:56:06 +01:00
102 changed files with 1402 additions and 1523 deletions

View File

@@ -1,7 +1,9 @@
---
name: "Feature request"
about: Suggest an idea for this project
title: ''
labels: 'feature-request'
assignees: ''
---
<!--

View File

@@ -2,10 +2,3 @@
* Notifies interested parties that hangup procedure will start.
*/
export const BEFORE_HANGUP = 'conference.before_hangup';
/**
* Notifies interested parties that desktop sharing enable/disable state is
* changed.
*/
export const DESKTOP_SHARING_ENABLED_CHANGED
= 'conference.desktop_sharing_enabled_changed';

View File

@@ -12,7 +12,7 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-feature
android:glEsVersion="0x00020000"
@@ -34,8 +34,7 @@
android:launchMode="singleTask"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="adjustResize">
</activity>
android:windowSoftInputMode="adjustResize"></activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
<service
@@ -46,7 +45,9 @@
</intent-filter>
</service>
<service android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService" />
<service
android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService"
android:foregroundServiceType="mediaProjection" />
</application>
</manifest>

View File

@@ -43,6 +43,7 @@ class PictureInPictureModule extends ReactContextBaseJavaModule {
private static final String TAG = NAME;
private static boolean isSupported;
private boolean isDisabled;
public PictureInPictureModule(ReactApplicationContext reactContext) {
super(reactContext);
@@ -83,6 +84,10 @@ class PictureInPictureModule extends ReactContextBaseJavaModule {
*/
@TargetApi(Build.VERSION_CODES.O)
public void enterPictureInPicture() {
if (isDisabled) {
return;
}
if (!isSupported) {
throw new IllegalStateException("Picture-in-Picture not supported");
}
@@ -126,6 +131,11 @@ class PictureInPictureModule extends ReactContextBaseJavaModule {
}
}
@ReactMethod
public void setPictureInPictureDisabled(Boolean disabled) {
this.isDisabled = disabled;
}
public boolean isPictureInPictureSupported() {
return isSupported;
}

View File

@@ -41,8 +41,7 @@ import {
lockStateChanged,
onStartMutedPolicyChanged,
p2pStatusChanged,
sendLocalParticipant,
setDesktopSharingEnabled
sendLocalParticipant
} from './react/features/base/conference';
import {
checkAndNotifyForNewDevice,
@@ -441,17 +440,8 @@ export default {
* the tracks won't exist).
*/
_localTracksInitialized: false,
isSharingScreen: false,
/**
* Indicates if the desktop sharing functionality has been enabled.
* It takes into consideration the status returned by
* {@link JitsiMeetJS.isDesktopSharingEnabled()}. The latter can be false
* either if the desktop sharing is not supported by the current browser
* or if it was disabled through lib-jitsi-meet specific options (check
* config.js for listed options).
*/
isDesktopSharingEnabled: false,
isSharingScreen: false,
/**
* The local audio track (if any).
@@ -679,14 +669,6 @@ export default {
con.addEventListener(JitsiConnectionEvents.CONNECTION_FAILED, _connectionFailedHandler);
APP.connection = connection = con;
// Desktop sharing related stuff:
this.isDesktopSharingEnabled
= JitsiMeetJS.isDesktopSharingEnabled();
eventEmitter.emit(JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED, this.isDesktopSharingEnabled);
APP.store.dispatch(
setDesktopSharingEnabled(this.isDesktopSharingEnabled));
this._createRoom(tracks);
APP.remoteControl.init();
@@ -1134,20 +1116,6 @@ export default {
return room ? room.getParticipantById(id) : null;
},
/**
* Get participant connection status for the participant.
*
* @param {string} id participant's identifier(MUC nickname)
*
* @returns {ParticipantConnectionStatus|null} the status of the participant
* or null if no such participant is found or participant is the local user.
*/
getParticipantConnectionStatus(id) {
const participant = this.getParticipantById(id);
return participant ? participant.getConnectionStatus() : null;
},
/**
* Gets the display name foe the <tt>JitsiParticipant</tt> identified by
* the given <tt>id</tt>.
@@ -1546,9 +1514,8 @@ export default {
if (this.videoSwitchInProgress) {
return Promise.reject('Switch in progress.');
}
if (!this.isDesktopSharingEnabled) {
return Promise.reject(
'Cannot toggle screen sharing: not supported.');
if (!JitsiMeetJS.isDesktopSharingEnabled()) {
return Promise.reject('Cannot toggle screen sharing: not supported.');
}
if (this.isAudioOnly()) {

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

@@ -293,8 +293,8 @@ PODS:
- React
- react-native-splash-screen (3.2.0):
- React
- react-native-webrtc (1.84.0):
- React
- react-native-webrtc (1.84.1):
- React-Core
- react-native-webview (10.9.0):
- React
- React-RCTActionSheet (0.61.5-jitsi.2):
@@ -562,7 +562,7 @@ SPEC CHECKSUMS:
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
react-native-webrtc: 9268ae9a2bc9730796b0968d012327e92c392adf
react-native-webrtc: edd689b0d5a462d7a6f6f52bca3f9414fc0ee11c
react-native-webview: 6ee7868ca8eba635dbf7963986d1ab7959da0391
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6

View File

@@ -730,7 +730,7 @@
"stopScreenSharing": "Arrêter le partage d'écran",
"stopSubtitles": "Désactiver les sous-titres",
"stopSharedVideo": "Arrêter la vidéo YouTube",
"talkWhileMutedPopup": "Vous voulez parler ? Vôtre micro est coupé.",
"talkWhileMutedPopup": "Vous voulez parler ? Votre micro est coupé.",
"tileViewToggle": "Activer/désactiver la vue mosaïque",
"toggleCamera": "Changer de caméra",
"videomute": "Démarrer / Arrêter la caméra",
@@ -769,7 +769,7 @@
"errorAlreadyInvited": "{{displayName}} est déjà invité(e)",
"errorInvite": "La conférence n'est pas encore établie. Veuillez réessayer plus tard.",
"errorInviteFailed": "Nous tentons de résoudre le problème. Veuillez réessayer plus tard.",
"errorInviteFailedTitle": "l'invitation de {{displayName}} a échoué",
"errorInviteFailedTitle": "L'invitation de {{displayName}} a échoué",
"errorInviteTitle": "Erreur lors de l'invitation",
"pending": "{{displayName}} a été invité(e)"
},
@@ -809,7 +809,7 @@
"join": "Touchez pour rejoindre",
"roomname": "Saisissez un nom de salle"
},
"appDescription": "Foncez tchater en vidéo avec toute le monde. En fait, vous pouvez inviter tout ceux que vous connaissez. {{app}} est une solution de visioconférence entièrement chiffrée et 100% libre que vous pouvez utiliser en permanence, chaque jours, et sans aucun compte requis.",
"appDescription": "Foncez tchater en vidéo avec toute le monde. En fait, vous pouvez inviter tout ceux que vous connaissez. {{app}} est une solution de visioconférence entièrement chiffrée et 100% libre que vous pouvez utiliser en permanence, chaque jour, et sans aucun compte requis.",
"audioVideoSwitch": {
"audio": "Voix",
"video": "Vidéo"
@@ -847,10 +847,10 @@
"allow": "Autoriser",
"backToKnockModeButton": "Aucun mot de passe, demander à rejoindre plutôt",
"dialogTitle": "Mode lobby",
"disableDialogContent": "Le mode lobby est actuellement activé. Cette fonctionnalité garantit que les participants indésirables ne peuvent pas rejoindre votre réunion. Souhaitez-vous la désactiver?",
"disableDialogContent": "Le mode lobby est actuellement activé. Cette fonctionnalité garantit que les participants indésirables ne peuvent pas rejoindre votre réunion. Souhaitez-vous la désactiver ?",
"disableDialogSubmit": "Désactiver",
"emailField": "Saisissez votre adresse email",
"enableDialogPasswordField": "Définir le mot de passe (optionel)",
"enableDialogPasswordField": "Définir le mot de passe (optionnel)",
"enableDialogSubmit": "Activer",
"enableDialogText": "Le mode lobby vous permet de protéger votre réunion en autorisant les personnes à entrer qu'après l'approbation formelle d'un modérateur.",
"enterPasswordButton": "Saisissez un mot de passe de réunion",

View File

@@ -512,6 +512,12 @@
"sectionList": {
"pullToRefresh": "Puxe para atualizar"
},
"security": {
"about": "Voce pode adicionar uma $t(lockRoomPassword) em sua reunião. Participantes irão precisar informar a $t(lockRoomPassword) antes de se juntarem na reunião.",
"aboutReadOnly": "Moderadores podem adicionar uma $t(lockRoomPassword) na reunião. Participantes irão precisar informar a $t(lockRoomPassword) antes de se juntarem na reunião",
"insecureRoomNameWarning": "Essa sala não está protegida. Participantes indesejados poderão entrar na sua reunião. Considere configurar a segurança da sua reunião utilizando o botão de segurança.",
"securityOptions": "Opções de segurança"
},
"settings": {
"calendar": {
"about": "A integração do calendário {{appName}} é usada para acessar com segurança o seu calendário para que ele possa ler os próximos eventos.",

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

@@ -2,7 +2,6 @@
import Logger from 'jitsi-meet-logger';
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
import {
createApiEvent,
sendAnalytics
@@ -14,7 +13,7 @@ import {
setSubject
} from '../../react/features/base/conference';
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import { pinParticipant } from '../../react/features/base/participants';
import {
processExternalDeviceRequest
@@ -46,14 +45,6 @@ declare var APP: Object;
*/
let commands = {};
/**
* The state of screen sharing(started/stopped) before the screen sharing is
* enabled and initialized.
* NOTE: This flag help us to cache the state and use it if toggle-share-screen
* was received before the initialization.
*/
let initialScreenSharingState = false;
/**
* The transport instance used for communication with external apps.
*
@@ -430,19 +421,6 @@ function initCommands() {
});
}
/**
* Listens for desktop/screen sharing enabled events and toggles the screen
* sharing if needed.
*
* @param {boolean} enabled - Current screen sharing enabled status.
* @returns {void}
*/
function onDesktopSharingEnabledChanged(enabled = false) {
if (enabled && initialScreenSharingState) {
toggleScreenSharing();
}
}
/**
* Check whether the API should be enabled or not.
*
@@ -470,12 +448,10 @@ function shouldBeEnabled() {
* @returns {void}
*/
function toggleScreenSharing(enable) {
if (APP.conference.isDesktopSharingEnabled) {
// eslint-disable-next-line no-empty-function
APP.conference.toggleScreenSharing(enable).catch(() => {});
} else {
initialScreenSharingState = !initialScreenSharingState;
if (JitsiMeetJS.isDesktopSharingEnabled()) {
APP.conference.toggleScreenSharing(enable).catch(() => {
logger.warn('Failed to toggle screen-sharing');
});
}
}
@@ -508,10 +484,6 @@ class API {
*/
this._enabled = true;
APP.conference.addListener(
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
onDesktopSharingEnabledChanged);
initCommands();
}
@@ -1058,9 +1030,6 @@ class API {
dispose() {
if (this._enabled) {
this._enabled = false;
APP.conference.removeListener(
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
onDesktopSharingEnabledChanged);
}
}
}

View File

@@ -12,6 +12,7 @@ import {
JitsiParticipantConnectionStatus
} from '../../../react/features/base/lib-jitsi-meet';
import { VIDEO_TYPE } from '../../../react/features/base/media';
import { getParticipantById } from '../../../react/features/base/participants';
import { CHAT_SIZE } from '../../../react/features/chat';
import {
updateKnownLargeVideoResolution
@@ -224,9 +225,8 @@ export default class LargeVideoManager {
const wasUsersImageCached
= !isUserSwitch && container.wasVideoRendered;
const isVideoMuted = !stream || stream.isMuted();
const connectionStatus
= APP.conference.getParticipantConnectionStatus(id);
const participant = getParticipantById(APP.store.getState(), id);
const connectionStatus = participant?.connectionStatus;
const isVideoRenderable
= !isVideoMuted
&& (APP.conference.isLocalId(id)
@@ -479,8 +479,8 @@ export default class LargeVideoManager {
*/
showRemoteConnectionMessage(show) {
if (typeof show !== 'boolean') {
const connStatus
= APP.conference.getParticipantConnectionStatus(this.id);
const participant = getParticipantById(APP.store.getState(), this.id);
const connStatus = participant?.connectionStatus;
// eslint-disable-next-line no-param-reassign
show = !APP.conference.isLocalId(this.id)

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

@@ -100,17 +100,6 @@ export default class RemoteVideo extends SmallVideo {
*/
this._canPlayEventReceived = false;
/**
* The flag is set to <tt>true</tt> if remote participant's video gets muted
* during his media connection disruption. This is to prevent black video
* being render on the thumbnail, because even though once the video has
* been played the image usually remains on the video element it seems that
* after longer period of the video element being hidden this image can be
* lost.
* @type {boolean}
*/
this.mutedWhileDisconnected = false;
// Bind event handlers so they are only bound once for every instance.
// TODO The event handlers should be turned into actions so changes can be
// handled through reducers and middleware.
@@ -306,34 +295,6 @@ export default class RemoteVideo extends SmallVideo {
this._generatePopupContent();
}
/**
* Video muted status changed handler.
*/
onVideoMute() {
super.updateView();
// Update 'mutedWhileDisconnected' flag
this._figureOutMutedWhileDisconnected();
}
/**
* Figures out the value of {@link #mutedWhileDisconnected} flag by taking into
* account remote participant's network connectivity and video muted status.
*
* @private
*/
_figureOutMutedWhileDisconnected() {
const isActive = this.isConnectionActive();
const isVideoMuted
= isRemoteTrackMuted(APP.store.getState()['features/base/tracks'], MEDIA_TYPE.VIDEO, this.id);
if (!isActive && isVideoMuted) {
this.mutedWhileDisconnected = true;
} else if (isActive && !isVideoMuted) {
this.mutedWhileDisconnected = false;
}
}
/**
* Removes the remote stream element corresponding to the given stream and
* parent container.
@@ -364,17 +325,6 @@ export default class RemoteVideo extends SmallVideo {
this.updateView();
}
/**
* Checks whether the remote user associated with this <tt>RemoteVideo</tt>
* has connectivity issues.
*
* @return {boolean} <tt>true</tt> if the user's connection is fine or
* <tt>false</tt> otherwise.
*/
isConnectionActive() {
return this.user.getConnectionStatus() === JitsiParticipantConnectionStatus.ACTIVE;
}
/**
* The remote video is considered "playable" once the can play event has been received. It will be allowed to
* display video also in {@link JitsiParticipantConnectionStatus.INTERRUPTED} if the video has received the canplay
@@ -386,12 +336,13 @@ export default class RemoteVideo extends SmallVideo {
* @override
*/
isVideoPlayable() {
const connectionState = APP.conference.getParticipantConnectionStatus(this.id);
const participant = getParticipantById(APP.store.getState(), this.id);
const { connectionStatus, mutedWhileDisconnected } = participant || {};
return super.isVideoPlayable()
&& this._canPlayEventReceived
&& (connectionState === JitsiParticipantConnectionStatus.ACTIVE
|| (connectionState === JitsiParticipantConnectionStatus.INTERRUPTED && !this.mutedWhileDisconnected));
&& (connectionStatus === JitsiParticipantConnectionStatus.ACTIVE
|| (connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED && !mutedWhileDisconnected));
}
/**
@@ -399,27 +350,9 @@ export default class RemoteVideo extends SmallVideo {
*/
updateView() {
this.$container.toggleClass('audio-only', APP.conference.isAudioOnly());
this.updateConnectionStatusIndicator();
// This must be called after 'updateConnectionStatusIndicator' because it
// affects the display mode by modifying 'mutedWhileDisconnected' flag
super.updateView();
}
/**
* Updates the UI to reflect user's connectivity status.
*/
updateConnectionStatusIndicator() {
const connectionStatus = this.user.getConnectionStatus();
logger.debug(`${this.id} thumbnail connection status: ${connectionStatus}`);
// FIXME rename 'mutedWhileDisconnected' to 'mutedWhileNotRendering'
// Update 'mutedWhileDisconnected' flag
this._figureOutMutedWhileDisconnected();
this.updateConnectionStatus(connectionStatus);
}
/**
* Removes RemoteVideo from the page.
*/

View File

@@ -95,16 +95,6 @@ export default class SmallVideo {
this.videoIsHovered = false;
this.videoType = undefined;
/**
* The current state of the user's bridge connection. The value should be
* a string as enumerated in the library's participantConnectionStatus
* constants.
*
* @private
* @type {string|null}
*/
this._connectionStatus = null;
/**
* Whether or not the connection indicator should be displayed.
*
@@ -210,16 +200,6 @@ export default class SmallVideo {
this.updateIndicators();
}
/**
* Updates the connectionStatus stat which displays in the ConnectionIndicator.
* @returns {void}
*/
updateConnectionStatus(connectionStatus) {
this._connectionStatus = connectionStatus;
this.updateIndicators();
}
/**
* Create or updates the ReactElement for displaying status indicators about
* audio mute, video mute, and moderator status.
@@ -453,6 +433,7 @@ export default class SmallVideo {
*/
computeDisplayModeInput() {
let isScreenSharing = false;
let connectionStatus, mutedWhileDisconnected;
const state = APP.store.getState();
const participant = getParticipantById(state, this.id);
@@ -461,6 +442,8 @@ export default class SmallVideo {
const track = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, this.id);
isScreenSharing = typeof track !== 'undefined' && track.videoType === 'desktop';
connectionStatus = participant.connectionStatus;
mutedWhileDisconnected = participant.mutedWhileDisconnected;
}
return {
@@ -470,8 +453,8 @@ export default class SmallVideo {
tileViewActive: shouldDisplayTileView(state),
isVideoPlayable: this.isVideoPlayable(),
hasVideo: Boolean(this.selectVideoElement().length),
connectionStatus: APP.conference.getParticipantConnectionStatus(this.id),
mutedWhileDisconnected: this.mutedWhileDisconnected,
connectionStatus,
mutedWhileDisconnected,
canPlayEventReceived: this._canPlayEventReceived,
videoStream: Boolean(this.videoStream),
isScreenSharing,
@@ -714,7 +697,6 @@ export default class SmallVideo {
{ this._showConnectionIndicator
? <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
connectionStatus = { this._connectionStatus }
iconSize = { iconSize }
isLocalVideo = { this.isLocal }
enableStatsDisplay = { !interfaceConfig.filmStripOnly }

View File

@@ -337,7 +337,7 @@ const VideoLayout = {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
remoteVideo.onVideoMute();
remoteVideo.updateView();
}
}
@@ -391,12 +391,6 @@ const VideoLayout = {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
// Updating only connection status indicator is not enough, because
// when we the connection is restored while the avatar was displayed
// (due to 'muted while disconnected' condition) we may want to show
// the video stream again and in order to do that the display mode
// must be updated.
// remoteVideo.updateConnectionStatusIndicator(isActive);
remoteVideo.updateView();
}
},

View File

@@ -3,6 +3,7 @@
import EventEmitter from 'events';
import { getLogger } from 'jitsi-meet-logger';
import JitsiMeetJS from '../../react/features/base/lib-jitsi-meet';
import { DISCO_REMOTE_CONTROL_FEATURE }
from '../../service/remotecontrol/Constants';
import * as RemoteControlEvents
@@ -68,9 +69,7 @@ class RemoteControl extends EventEmitter {
* @returns {void}
*/
init() {
if (config.disableRemoteControl
|| this._initialized
|| !APP.conference.isDesktopSharingEnabled) {
if (config.disableRemoteControl || this._initialized || !JitsiMeetJS.isDesktopSharingEnabled()) {
return;
}
logger.log('Initializing remote control.');

16
package-lock.json generated
View File

@@ -4922,9 +4922,9 @@
"dev": true
},
"amplitude-js": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-7.1.1.tgz",
"integrity": "sha512-grEQf0p4V/q4aIcGYdGEJ6EquBXu91R/RorsYTQvh9O6sxjpwHf5vSDICQJq7twEElBrSHoSF77GUvC9ZTBj4A==",
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-7.3.1.tgz",
"integrity": "sha512-dsJU9MdtDDAOtKnbHrJuVBgsL5UGxD1P2B7doGdAQ1hxxT/5mFrmJTFzi1tKe+2ir3QtcRa9B0qvH8TMsGw22A==",
"requires": {
"@amplitude/ua-parser-js": "0.7.24",
"blueimp-md5": "^2.10.0",
@@ -10771,8 +10771,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#3bfdb98e3b4db12ba28d88aa8710afc4d1b458b2",
"from": "github:jitsi/lib-jitsi-meet#3bfdb98e3b4db12ba28d88aa8710afc4d1b458b2",
"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",
@@ -14265,9 +14265,9 @@
"integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
},
"react-native-webrtc": {
"version": "1.84.0",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.84.0.tgz",
"integrity": "sha512-xPOFbrcehuBzLnFy3keCM2HyMsyCVDQjQNAn8SIHKH/PA8Q7kZ4spuytc2E1hBTr7zH/vQ2Px+DWqu7on12jag==",
"version": "1.84.1",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.84.1.tgz",
"integrity": "sha512-ewZBgKE+YhLaivo9Wh6aiaEp8ZRvFMqblrkDl1nptQiNNH6CungoAzSOxGDnHWAxepRfiUrW5qnADrsYKmaNeQ==",
"requires": {
"base64-js": "^1.1.2",
"event-target-shim": "^1.0.5",

View File

@@ -40,7 +40,7 @@
"@svgr/webpack": "4.3.2",
"@tensorflow-models/body-pix": "2.0.4",
"@tensorflow/tfjs": "1.5.1",
"amplitude-js": "7.1.1",
"amplitude-js": "7.3.1",
"base64-js": "1.3.1",
"bc-css-flags": "3.0.0",
"dropbox": "4.0.9",
@@ -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#3bfdb98e3b4db12ba28d88aa8710afc4d1b458b2",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#6bb0b86c0a7dd22bb5798236d9b80ca578b28d21",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",
@@ -84,7 +84,7 @@
"react-native-svg-transformer": "0.14.3",
"react-native-url-polyfill": "1.2.0",
"react-native-watch-connectivity": "0.4.3",
"react-native-webrtc": "1.84.0",
"react-native-webrtc": "1.84.1",
"react-native-webview": "10.9.0",
"react-native-youtube-iframe": "1.2.3",
"react-redux": "7.1.0",
@@ -92,7 +92,7 @@
"react-transition-group": "2.4.0",
"redux": "4.0.4",
"redux-thunk": "2.2.0",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm.git#566a16885897704d6e6d67a1d5ac5d39781db2af",
"rnnoise-wasm": "github:jitsi/rnnoise-wasm#566a16885897704d6e6d67a1d5ac5d39781db2af",
"rtcstats": "github:jitsi/rtcstats#v6.2.0",
"stackblur-canvas": "2.3.0",
"styled-components": "3.4.9",

View File

@@ -772,6 +772,22 @@ export function createTrackMutedEvent(mediaType, reason, muted = true) {
};
}
/**
* Creates an event for joining a vpaas conference.
*
* @param {string} tenant - The conference tenant.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createVpaasConferenceJoinedEvent(tenant) {
return {
action: 'vpaas.conference.joined',
attributes: {
tenant
}
};
}
/**
* Creates an event for an action on the welcome page.
*

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

@@ -140,18 +140,6 @@ export const P2P_STATUS_CHANGED = 'P2P_STATUS_CHANGED';
*/
export const SEND_TONES = 'SEND_TONES';
/**
* The type of (redux) action which sets the desktop sharing enabled flag for
* the current conference.
*
* {
* type: SET_DESKTOP_SHARING_ENABLED,
* desktopSharingEnabled: boolean
* }
*/
export const SET_DESKTOP_SHARING_ENABLED
= 'SET_DESKTOP_SHARING_ENABLED';
/**
* The type of (redux) action which updates the current known status of the
* Follow Me feature.

View File

@@ -43,7 +43,6 @@ import {
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SEND_TONES,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_PASSWORD,
SET_PASSWORD_FAILED,
@@ -573,22 +572,6 @@ export function sendTones(tones: string, duration: number, pause: number) {
};
}
/**
* Sets the flag for indicating if desktop sharing is enabled.
*
* @param {boolean} desktopSharingEnabled - True if desktop sharing is enabled.
* @returns {{
* type: SET_DESKTOP_SHARING_ENABLED,
* desktopSharingEnabled: boolean
* }}
*/
export function setDesktopSharingEnabled(desktopSharingEnabled: boolean) {
return {
type: SET_DESKTOP_SHARING_ENABLED,
desktopSharingEnabled
};
}
/**
* Enables or disables the Follow Me feature.
*

View File

@@ -73,6 +73,7 @@ export function commonUserJoinedHandling(
} else {
dispatch(participantJoined({
botType: user.getBotType(),
connectionStatus: user.getConnectionStatus(),
conference,
id,
name: displayName,

View File

@@ -0,0 +1,74 @@
// @flow
import { setPictureInPictureDisabled } from '../../mobile/picture-in-picture/functions';
import { setAudioOnly } from '../audio-only';
import JitsiMeetJS from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux';
import { TOGGLE_SCREENSHARING } from '../tracks/actionTypes';
import { destroyLocalDesktopTrackIfExists, replaceLocalTrack } from '../tracks/actions';
import { getLocalVideoTrack, isLocalVideoTrackDesktop } from '../tracks/functions';
import './middleware.any';
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case TOGGLE_SCREENSHARING: {
_toggleScreenSharing(store);
break;
}
}
return next(action);
});
/**
* Toggles screen sharing.
*
* @private
* @param {Store} store - The redux.
* @returns {void}
*/
function _toggleScreenSharing(store) {
const { dispatch, getState } = store;
const state = getState();
const isSharing = isLocalVideoTrackDesktop(state);
if (isSharing) {
dispatch(destroyLocalDesktopTrackIfExists());
} else {
_startScreenSharing(dispatch, state);
}
}
/**
* Creates desktop track and replaces the local one.
*
* @private
* @param {Dispatch} dispatch - The redux {@code dispatch} function.
* @param {Object} state - The redux state.
* @returns {void}
*/
function _startScreenSharing(dispatch, state) {
setPictureInPictureDisabled(true);
JitsiMeetJS.createLocalTracks({ devices: [ 'desktop' ] })
.then(tracks => {
const track = tracks[0];
const currentLocalTrack = getLocalVideoTrack(state['features/base/tracks']);
const currentJitsiTrack = currentLocalTrack && currentLocalTrack.jitsiTrack;
dispatch(replaceLocalTrack(currentJitsiTrack, track));
const { enabled: audioOnly } = state['features/base/audio-only'];
if (audioOnly) {
dispatch(setAudioOnly(false));
}
})
.catch(error => {
console.log('ERROR creating ScreeSharing stream ', error);
setPictureInPictureDisabled(false);
});
}

View File

@@ -0,0 +1,23 @@
// @flow
import UIEvents from '../../../../service/UI/UIEvents';
import { MiddlewareRegistry } from '../redux';
import { TOGGLE_SCREENSHARING } from '../tracks/actionTypes';
import './middleware.any';
declare var APP: Object;
MiddlewareRegistry.register((/* store */) => next => action => {
switch (action.type) {
case TOGGLE_SCREENSHARING: {
if (typeof APP === 'object') {
APP.UI.emitEvent(UIEvents.TOGGLE_SCREENSHARING);
}
break;
}
}
return next(action);
});

View File

@@ -16,7 +16,6 @@ import {
CONFERENCE_WILL_LEAVE,
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_PASSWORD,
SET_PENDING_SUBJECT_CHANGE,
@@ -76,9 +75,6 @@ ReducerRegistry.register(
case P2P_STATUS_CHANGED:
return _p2pStatusChanged(state, action);
case SET_DESKTOP_SHARING_ENABLED:
return _setDesktopSharingEnabled(state, action);
case SET_FOLLOW_ME:
return set(state, 'followMeEnabled', action.enabled);
@@ -343,21 +339,6 @@ function _p2pStatusChanged(state, action) {
return set(state, 'p2p', action.p2p);
}
/**
* Reduces a specific Redux action SET_DESKTOP_SHARING_ENABLED of the feature
* base/conference.
*
* @param {Object} state - The Redux state of the feature base/conference.
* @param {Action} action - The Redux action SET_DESKTOP_SHARING_ENABLED to
* reduce.
* @private
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _setDesktopSharingEnabled(state, action) {
return set(state, 'desktopSharingEnabled', action.desktopSharingEnabled);
}
/**
* Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
*

View File

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

View File

@@ -59,7 +59,7 @@ export function getInviteURL(stateOrGetState: Function | Object): string {
if (inviteDomain) {
const meetingId
= state['features/base/config'].brandingRoomAlias || urlWithoutParams.pathname;
= state['features/base/config'].brandingRoomAlias || urlWithoutParams.pathname.replace('/', '');
return `${inviteDomain}/${meetingId}`;
}

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

@@ -14,3 +14,15 @@ import { parseURLParams } from '../util';
export function parseJWTFromURLParams(url: URL = window.location) {
return parseURLParams(url, true, 'search').jwt;
}
/**
* Returns the user name after decoding the jwt.
*
* @param {Object} state - The app state.
* @returns {string}
*/
export function getJwtName(state: Object) {
const { user } = state['features/base/jwt'];
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

@@ -16,6 +16,7 @@ import {
getParticipantCount
} from '../participants/functions';
import { MiddlewareRegistry } from '../redux';
import { isLocalVideoTrackDesktop } from '../tracks/functions';
import { limitLastN } from './functions';
import logger from './logger';
@@ -78,7 +79,7 @@ function _updateLastN({ getState }) {
}
if (typeof appState !== 'undefined' && appState !== 'active') {
lastN = 0;
lastN = isLocalVideoTrackDesktop(state) ? 1 : 0;
} else if (audioOnly) {
const { screenShares, tileViewEnabled } = state['features/video-layout'];
const largeVideoParticipantId = state['features/large-video'].participantId;

View File

@@ -13,7 +13,7 @@ import { isRoomValid, SET_ROOM } from '../conference';
import JitsiMeetJS from '../lib-jitsi-meet';
import { MiddlewareRegistry } from '../redux';
import { getPropertyValue } from '../settings';
import { setTrackMuted, TRACK_ADDED } from '../tracks';
import { isLocalVideoTrackDesktop, setTrackMuted, TRACK_ADDED } from '../tracks';
import { setAudioMuted, setCameraFacingMode, setVideoMuted } from './actions';
import {
@@ -73,13 +73,15 @@ MiddlewareRegistry.register(store => next => action => {
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _appStateChanged({ dispatch }, next, action) {
const { appState } = action;
const mute = appState !== 'active'; // Note that 'background' and 'inactive' are treated equal.
function _appStateChanged({ dispatch, getState }, next, action) {
if (navigator.product === 'ReactNative') {
const { appState } = action;
const mute = appState !== 'active' && !isLocalVideoTrackDesktop(getState());
sendAnalytics(createTrackMutedEvent('video', 'background mode', mute));
sendAnalytics(createTrackMutedEvent('video', 'background mode', mute));
dispatch(setVideoMuted(mute, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
dispatch(setVideoMuted(mute, MEDIA_TYPE.VIDEO, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
}
return next(action);
}

View File

@@ -19,7 +19,8 @@ import {
import {
getLocalParticipant,
getNormalizedDisplayName,
getParticipantDisplayName
getParticipantDisplayName,
figureOutMutedWhileDisconnectedStatus
} from './functions';
/**
@@ -216,12 +217,15 @@ export function muteRemoteParticipant(id) {
* }}
*/
export function participantConnectionStatusChanged(id, connectionStatus) {
return {
type: PARTICIPANT_UPDATED,
participant: {
connectionStatus,
id
}
return (dispatch, getState) => {
return {
type: PARTICIPANT_UPDATED,
participant: {
connectionStatus,
id,
mutedWhileDisconnected: figureOutMutedWhileDisconnectedStatus(getState(), id, connectionStatus)
}
};
};
}

View File

@@ -5,7 +5,7 @@ import { getGravatarURL } from '@jitsi/js-utils/avatar';
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
import { toState } from '../redux';
import { getTrackByMediaTypeAndParticipant } from '../tracks';
import { getTrackByMediaTypeAndParticipant, isRemoteTrackMuted } from '../tracks';
import { createDeferred } from '../util';
import {
@@ -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;
}
/**
@@ -366,6 +359,45 @@ export function shouldRenderParticipantVideo(stateful: Object | Function, id: st
return participantIsInLargeVideoWithScreen;
}
/**
* Figures out the value of mutedWhileDisconnected status by taking into
* account remote participant's network connectivity and video muted status.
* The flag is set to <tt>true</tt> if remote participant's video gets muted
* during his media connection disruption. This is to prevent black video
* being render on the thumbnail, because even though once the video has
* been played the image usually remains on the video element it seems that
* after longer period of the video element being hidden this image can be
* lost.
*
* @param {Object|Function} stateful - Object or function that can be resolved
* to the Redux state.
* @param {string} participantID - The ID of the participant.
* @param {string} [connectionStatus] - A connection status to be used.
* @returns {boolean} - The mutedWhileDisconnected value.
*/
export function figureOutMutedWhileDisconnectedStatus(
stateful: Function | Object, participantID: string, connectionStatus: ?string) {
const state = toState(stateful);
const participant = getParticipantById(state, participantID);
if (!participant || participant.local) {
return undefined;
}
const isActive = (connectionStatus || participant.connectionStatus) === JitsiParticipantConnectionStatus.ACTIVE;
const isVideoMuted = isRemoteTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID);
let mutedWhileDisconnected = participant.mutedWhileDisconnected || false;
if (!isActive && isVideoMuted) {
mutedWhileDisconnected = true;
} else if (isActive && !isVideoMuted) {
mutedWhileDisconnected = false;
}
return mutedWhileDisconnected;
}
/**
* Resolves the first loadable avatar URL for a participant.
*

View File

@@ -12,6 +12,7 @@ import {
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
import { playSound, registerSound, unregisterSound } from '../sounds';
import { getTrackByJitsiTrack, TRACK_ADDED, TRACK_REMOVED, TRACK_UPDATED } from '../tracks';
import {
DOMINANT_SPEAKER_CHANGED,
@@ -41,7 +42,8 @@ import {
getLocalParticipant,
getParticipantById,
getParticipantCount,
getParticipantDisplayName
getParticipantDisplayName,
figureOutMutedWhileDisconnectedStatus
} from './functions';
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
@@ -134,6 +136,11 @@ MiddlewareRegistry.register(store => next => action => {
case PARTICIPANT_UPDATED:
return _participantJoinedOrUpdated(store, next, action);
case TRACK_ADDED:
case TRACK_REMOVED:
case TRACK_UPDATED:
return _trackChanged(store, next, action);
}
return next(action);
@@ -452,6 +459,55 @@ function _registerSounds({ dispatch }) {
dispatch(registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_FILE));
}
/**
* Notifies the feature base/participants that the action there has been a change in the tracks of the participants.
*
* @param {Store} store - The redux store in which the specified {@code action} is being dispatched.
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the specified {@code action} in the
* specified {@code store}.
* @param {Action} action - The redux action {@code PARTICIPANT_JOINED} or {@code PARTICIPANT_UPDATED} which is being
* dispatched in the specified {@code store}.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _trackChanged({ dispatch, getState }, next, action) {
const { jitsiTrack } = action.track;
let track;
if (action.type === TRACK_REMOVED) {
track = getTrackByJitsiTrack(getState()['features/base/tracks'], jitsiTrack);
}
const result = next(action);
if (action.type !== TRACK_REMOVED) {
track = getTrackByJitsiTrack(getState()['features/base/tracks'], jitsiTrack);
}
if (typeof track === 'undefined' || track.local) {
return result;
}
const { participantId } = track;
const state = getState();
const participant = getParticipantById(state, participantId);
if (!participant) {
return result;
}
const mutedWhileDisconnected = figureOutMutedWhileDisconnectedStatus(state, participantId);
if (participant.mutedWhileDisconnected !== mutedWhileDisconnected) {
dispatch(participantUpdated({
id: participantId,
mutedWhileDisconnected
}));
}
return result;
}
/**
* Unregisters sounds related with the participants feature.
*

View File

@@ -221,6 +221,7 @@ function _participantJoined({ participant }) {
isJigasi,
loadableAvatarUrl,
local: local || false,
mutedWhileDisconnected: local ? undefined : false,
name,
pinned: pinned || false,
presence,

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

@@ -1,9 +1,11 @@
// @flow
import _ from 'lodash';
import { PREJOIN_INITIALIZED } from '../../prejoin/actionTypes';
import { APP_WILL_MOUNT } from '../app';
import { setAudioOnly } from '../audio-only';
import { SET_LOCATION_URL } from '../connection/actionTypes'; // minimize imports to avoid circular imports
import { getJwtName } from '../jwt/functions';
import { getLocalParticipant, participantUpdated } from '../participants';
import { MiddlewareRegistry } from '../redux';
import { parseURLParams } from '../util';
@@ -27,6 +29,10 @@ MiddlewareRegistry.register(store => next => action => {
case APP_WILL_MOUNT:
_initializeCallIntegration(store);
break;
case PREJOIN_INITIALIZED: {
_maybeUpdateDisplayName(store);
break;
}
case SETTINGS_UPDATED:
_maybeHandleCallIntegrationChange(action);
_maybeSetAudioOnly(store, action);
@@ -115,6 +121,26 @@ function _maybeSetAudioOnly(
}
}
/**
* Updates the display name to the one in JWT if there is one.
*
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _maybeUpdateDisplayName({ dispatch, getState }) {
const state = getState();
const hasJwt = Boolean(state['features/base/jwt'].jwt);
if (hasJwt) {
const displayName = getJwtName(state);
dispatch(updateSettings({
displayName
}));
}
}
/**
* Updates the local participant according to settings changes.
*

View File

@@ -9,7 +9,8 @@ import {
MEDIA_TYPE,
setAudioMuted,
setVideoMuted,
VIDEO_MUTISM_AUTHORITY
VIDEO_MUTISM_AUTHORITY,
VIDEO_TYPE
} from '../media';
import { getLocalParticipant } from '../participants';
@@ -24,7 +25,13 @@ import {
TRACK_UPDATED,
TRACK_WILL_CREATE
} from './actionTypes';
import { createLocalTracksF, getLocalTrack, getLocalTracks, getTrackByJitsiTrack } from './functions';
import {
createLocalTracksF,
getLocalTrack,
getLocalTracks,
getLocalVideoTrack,
getTrackByJitsiTrack
} from './functions';
import logger from './logger';
/**
@@ -40,6 +47,8 @@ export function createDesiredLocalTracks(...desiredTypes) {
return (dispatch, getState) => {
const state = getState();
dispatch(destroyLocalDesktopTrackIfExists());
if (desiredTypes.length === 0) {
const { audio, video } = state['features/base/media'];
@@ -663,6 +672,22 @@ function _trackCreateCanceled(mediaType) {
};
}
/**
* If thee local track if of type Desktop, it calls _disposeAndRemoveTracks) on it.
*
* @returns {Function}
*/
export function destroyLocalDesktopTrackIfExists() {
return (dispatch, getState) => {
const videoTrack = getLocalVideoTrack(getState()['features/base/tracks']);
const isDesktopTrack = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
if (isDesktopTrack) {
dispatch(_disposeAndRemoveTracks([ videoTrack.jitsiTrack ]));
}
};
}
/**
* Sets UID of the displayed no data from source notification. Used to track
* if the notification was previously displayed in this context.

View File

@@ -1,7 +1,7 @@
/* global APP */
import JitsiMeetJS, { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { MEDIA_TYPE, setAudioMuted } from '../media';
import { MEDIA_TYPE, VIDEO_TYPE, setAudioMuted } from '../media';
import {
getUserSelectedCameraDeviceId,
getUserSelectedMicDeviceId
@@ -383,6 +383,19 @@ export function isLocalTrackMuted(tracks, mediaType) {
return !track || track.muted;
}
/**
* Checks if the local video track is of type DESKtOP.
*
* @param {Object} state - The redux state.
* @returns {boolean}
*/
export function isLocalVideoTrackDesktop(state) {
const videoTrack = getLocalVideoTrack(state['features/base/tracks']);
return videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
}
/**
* Returns true if the remote track of the given media type and the given
* participant is muted, false otherwise.

View File

@@ -1,9 +1,11 @@
import { sendAnalytics, createVpaasConferenceJoinedEvent } from '../analytics';
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { PARTICIPANT_JOINED } from '../base/participants/actionTypes';
import { MiddlewareRegistry } from '../base/redux';
import { SET_BILLING_ID } from './actionTypes';
import { countEndpoint } from './actions';
import { setBillingId } from './functions';
import { isVpaasMeeting, extractVpaasTenantFromPath, setBillingId } from './functions';
/**
* The redux middleware for billing counter.
@@ -14,6 +16,11 @@ import { setBillingId } from './functions';
MiddlewareRegistry.register(store => next => async action => {
switch (action.type) {
case CONFERENCE_JOINED: {
_maybeTrackVpaasConferenceJoin(store.getState());
break;
}
case SET_BILLING_ID: {
setBillingId(action.value);
@@ -34,3 +41,17 @@ MiddlewareRegistry.register(store => next => async action => {
return next(action);
});
/**
* Tracks the conference join event if the meeting is a vpaas one.
*
* @param {Store} state - The app state.
* @returns {Function}
*/
function _maybeTrackVpaasConferenceJoin(state) {
if (isVpaasMeeting(state)) {
sendAnalytics(createVpaasConferenceJoinedEvent(
extractVpaasTenantFromPath(
state['features/base/connection'].locationURL.pathname)));
}
}

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

@@ -7,6 +7,7 @@ import { translate } from '../../../base/i18n';
import { Icon, IconConnectionActive, IconConnectionInactive } from '../../../base/icons';
import { JitsiParticipantConnectionStatus } from '../../../base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../../../base/media';
import { getLocalParticipant, getParticipantById } from '../../../base/participants';
import { Popover } from '../../../base/popover';
import { connect } from '../../../base/redux';
import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
@@ -61,6 +62,12 @@ const QUALITY_TO_WIDTH: Array<Object> = [
*/
type Props = AbstractProps & {
/**
* The current condition of the user's connection, matching one of the
* enumerated values in the library.
*/
_connectionStatus: string,
/**
* Whether or not the component should ignore setting a visibility class for
* hiding the component when the connection quality is not strong.
@@ -72,12 +79,6 @@ type Props = AbstractProps & {
*/
audioSsrc: number,
/**
* The current condition of the user's connection, matching one of the
* enumerated values in the library.
*/
connectionStatus: string,
/**
* The Redux dispatch function.
*/
@@ -200,13 +201,13 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
* @returns {string}
*/
_getConnectionColorClass() {
const { connectionStatus } = this.props;
const { _connectionStatus } = this.props;
const { percent } = this.state.stats;
const { INACTIVE, INTERRUPTED } = JitsiParticipantConnectionStatus;
if (connectionStatus === INACTIVE) {
if (_connectionStatus === INACTIVE) {
return 'status-other';
} else if (connectionStatus === INTERRUPTED) {
} else if (_connectionStatus === INTERRUPTED) {
return 'status-lost';
} else if (typeof percent === 'undefined') {
return 'status-high';
@@ -224,7 +225,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
_getConnectionStatusTip() {
let tipKey;
switch (this.props.connectionStatus) {
switch (this.props._connectionStatus) {
case JitsiParticipantConnectionStatus.INTERRUPTED:
tipKey = 'connectionindicator.quality.lost';
break;
@@ -275,12 +276,12 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
* @returns {string}
*/
_getVisibilityClass() {
const { connectionStatus } = this.props;
const { _connectionStatus } = this.props;
return this.state.showIndicator
|| this.props.alwaysVisible
|| connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED
|| connectionStatus === JitsiParticipantConnectionStatus.INACTIVE
|| _connectionStatus === JitsiParticipantConnectionStatus.INTERRUPTED
|| _connectionStatus === JitsiParticipantConnectionStatus.INACTIVE
? 'show-connection-indicator' : 'hide-connection-indicator';
}
@@ -304,7 +305,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
* @returns {ReactElement}
*/
_renderIcon() {
if (this.props.connectionStatus
if (this.props._connectionStatus
=== JitsiParticipantConnectionStatus.INACTIVE) {
return (
<span className = 'connection_ninja'>
@@ -319,7 +320,7 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, State> {
let iconWidth;
let emptyIconWrapperClassName = 'connection_empty';
if (this.props.connectionStatus
if (this.props._connectionStatus
=== JitsiParticipantConnectionStatus.INTERRUPTED) {
// emptyIconWrapperClassName is used by the torture tests to
@@ -434,21 +435,29 @@ export function _mapDispatchToProps(dispatch: Dispatch<any>) {
* @returns {Props}
*/
export function _mapStateToProps(state: Object, ownProps: Props) {
const { participantId } = ownProps;
const conference = state['features/base/conference'].conference;
const participant
= typeof participantId === 'undefined' ? getLocalParticipant(state) : getParticipantById(state, participantId);
const props = {
_connectionStatus: participant?.connectionStatus
};
if (conference) {
const firstVideoTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.VIDEO, ownProps.participantId);
state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
const firstAudioTrack = getTrackByMediaTypeAndParticipant(
state['features/base/tracks'], MEDIA_TYPE.AUDIO, ownProps.participantId);
state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantId);
return {
...props,
audioSsrc: firstAudioTrack ? conference.getSsrcByTrack(firstAudioTrack.jitsiTrack) : undefined,
videoSsrc: firstVideoTrack ? conference.getSsrcByTrack(firstVideoTrack.jitsiTrack) : undefined
};
}
return {};
return {
...props
};
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ConnectionIndicator));

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

@@ -3,9 +3,10 @@
import React, { PureComponent } from 'react';
import { ColorSchemeRegistry } from '../../base/color-scheme';
import { ParticipantView } from '../../base/participants';
import { ParticipantView, getParticipantById } from '../../base/participants';
import { connect } from '../../base/redux';
import { StyleType } from '../../base/styles';
import { isLocalVideoTrackDesktop } from '../../base/tracks/functions';
import { AVATAR_SIZE } from './styles';
@@ -14,6 +15,11 @@ import { AVATAR_SIZE } from './styles';
*/
type Props = {
/**
* Whether video should be disabled.
*/
_disableVideo: boolean,
/**
* Application's viewport height.
*/
@@ -112,6 +118,7 @@ class LargeVideo extends PureComponent<Props, State> {
useConnectivityInfoLabel
} = this.state;
const {
_disableVideo,
_participantId,
_styles,
onClick
@@ -120,6 +127,7 @@ class LargeVideo extends PureComponent<Props, State> {
return (
<ParticipantView
avatarSize = { avatarSize }
disableVideo = { _disableVideo }
onPress = { onClick }
participantId = { _participantId }
style = { _styles.largeVideo }
@@ -139,11 +147,19 @@ class LargeVideo extends PureComponent<Props, State> {
* @returns {Props}
*/
function _mapStateToProps(state) {
const { participantId } = state['features/large-video'];
const participant = getParticipantById(state, participantId);
const { clientHeight: height, clientWidth: width } = state['features/base/responsive-ui'];
let disableVideo = false;
if (participant?.local) {
disableVideo = isLocalVideoTrackDesktop(state);
}
return {
_disableVideo: disableVideo,
_height: height,
_participantId: state['features/large-video'].participantId,
_participantId: participantId,
_styles: ColorSchemeRegistry.get(state, 'LargeVideo'),
_width: width
};

View File

@@ -7,6 +7,7 @@ import { translate } from '../../../base/i18n';
import { IconMenuDown } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { isLocalVideoTrackDesktop } from '../../../base/tracks/functions';
import { enterPictureInPicture } from '../actions';
type Props = AbstractButtonProps & {
@@ -63,7 +64,7 @@ class PictureInPictureButton extends AbstractButton<Props, *> {
*/
function _mapStateToProps(state): Object {
const flag = Boolean(getFeatureFlag(state, PIP_ENABLED));
let enabled = flag;
let enabled = flag && !isLocalVideoTrackDesktop(state);
// Override flag for Android, since it might be unsupported.
if (Platform.OS === 'android' && !NativeModules.PictureInPicture.SUPPORTED) {

View File

@@ -0,0 +1,15 @@
// @flow
import { NativeModules } from 'react-native';
/**
* Enabled/Disables the PictureInPicture mode in PiP native module.
*
* @param {boolean} disabled - Whether the PiP mode should be disabled.
* @returns {void}
*/
export function setPictureInPictureDisabled(disabled: boolean) {
const { PictureInPicture } = NativeModules;
PictureInPicture.setPictureInPictureDisabled(disabled);
}

View File

@@ -1,3 +1,4 @@
export * from './actions';
export * from './actionTypes';
export * from './components';
export * from './functions';

View File

@@ -4,6 +4,11 @@
*/
export const PREJOIN_START_CONFERENCE = 'PREJOIN_START_CONFERENCE';
/**
* Action type to signal that prejoin page was initialized.
*/
export const PREJOIN_INITIALIZED = 'PREJOIN_INITIALIZED';
/**
* Action type to set the status of the device.
*/

View File

@@ -18,6 +18,7 @@ import { executeDialOutRequest, executeDialOutStatusRequest, getDialInfoPageURL
import { showErrorNotification } from '../notifications';
import {
PREJOIN_INITIALIZED,
PREJOIN_START_CONFERENCE,
SET_DEVICE_STATUS,
SET_DIALOUT_COUNTRY,
@@ -195,7 +196,7 @@ export function dialOut(onSuccess: Function, onFail: Function) {
export function initPrejoin(tracks: Object[], errors: Object) {
return async function(dispatch: Function) {
dispatch(setPrejoinDeviceErrors(errors));
dispatch(prejoinInitialized());
tracks.forEach(track => dispatch(trackAdded(track)));
};
@@ -269,6 +270,17 @@ export function openDialInPage() {
};
}
/**
* Action used to signal that the prejoin page has been initialized.
*
* @returns {Object}
*/
function prejoinInitialized() {
return {
type: PREJOIN_INITIALIZED
};
}
/**
* Creates a new audio track based on a device id and replaces the current one.
*

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

@@ -9,7 +9,6 @@ import {
sendAnalytics
} from '../../analytics';
import { setAudioOnly } from '../../base/audio-only';
import { hasAvailableDevices } from '../../base/devices';
import { translate } from '../../base/i18n';
import {
VIDEO_MUTISM_AUTHORITY,
@@ -19,6 +18,7 @@ import { connect } from '../../base/redux';
import { AbstractVideoMuteButton } from '../../base/toolbox/components';
import type { AbstractButtonProps } from '../../base/toolbox/components';
import { getLocalVideoType, isLocalCameraTrackMuted } from '../../base/tracks';
import { isVideoMuteButtonDisabled } from '../functions';
declare var APP: Object;
@@ -190,7 +190,7 @@ function _mapStateToProps(state): Object {
return {
_audioOnly: Boolean(audioOnly),
_videoDisabled: !hasAvailableDevices(state, 'videoInput'),
_videoDisabled: isVideoMuteButtonDisabled(state),
_videoMediaType: getLocalVideoType(tracks),
_videoMuted: isLocalCameraTrackMuted(tracks)
};

View File

@@ -23,6 +23,7 @@ import HelpButton from '../HelpButton';
import AudioOnlyButton from './AudioOnlyButton';
import MoreOptionsButton from './MoreOptionsButton';
import RaiseHandButton from './RaiseHandButton';
import ScreenSharingButton from './ScreenSharingButton.js';
import ToggleCameraButton from './ToggleCameraButton';
import styles from './styles';
@@ -131,6 +132,7 @@ class OverflowMenu extends PureComponent<Props, State> {
<AudioOnlyButton { ...buttonProps } />
<RaiseHandButton { ...buttonProps } />
<LobbyModeButton { ...buttonProps } />
<ScreenSharingButton { ...buttonProps } />
<MoreOptionsButton { ...moreOptionsButtonProps } />
<Collapsible collapsed = { !showMore }>
<ToggleCameraButton { ...buttonProps } />

View File

@@ -0,0 +1,77 @@
// @flow
import { Platform } from 'react-native';
import { translate } from '../../../base/i18n';
import { IconShareDesktop } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { toggleScreensharing, isLocalVideoTrackDesktop } from '../../../base/tracks';
/**
* The type of the React {@code Component} props of {@link ScreenSharingButton}.
*/
type Props = AbstractButtonProps & {
/**
* Whether video is currently muted or not.
*/
_screensharing: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
};
/**
* An implementation of a button for toggling screen sharing.
*/
class ScreenSharingButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.shareYourScreen';
icon = IconShareDesktop;
label = 'toolbar.startScreenSharing';
toggledLabel = 'toolbar.stopScreenSharing';
/**
* Handles clicking / pressing the button.
*
* @override
* @protected
* @returns {void}
*/
_handleClick() {
this.props.dispatch(toggleScreensharing());
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props._screensharing;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code ToggleCameraButton} component.
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _disabled: boolean,
* _screensharing: boolean
* }}
*/
function _mapStateToProps(state): Object {
return {
_screensharing: isLocalVideoTrackDesktop(state),
visible: Platform.OS === 'android'
};
}
export default translate(connect(_mapStateToProps)(ScreenSharingButton));

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

@@ -25,6 +25,7 @@ import {
IconShareDesktop,
IconShareVideo
} from '../../../base/icons';
import JitsiMeetJS from '../../../base/lib-jitsi-meet';
import {
getLocalParticipant,
getParticipants,
@@ -127,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.
*/
@@ -992,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');
}
/**
@@ -1403,7 +1404,7 @@ class Toolbox extends Component<Props, State> {
*/
function _mapStateToProps(state) {
const { conference, locked } = state['features/base/conference'];
let { desktopSharingEnabled } = state['features/base/conference'];
let desktopSharingEnabled = JitsiMeetJS.isDesktopSharingEnabled();
const {
callStatsID,
enableFeaturesBasedOnToken
@@ -1425,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
@@ -1447,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

@@ -1,7 +1,9 @@
// @flow
import { hasAvailableDevices } from '../base/devices';
import { TOOLBOX_ALWAYS_VISIBLE, getFeatureFlag } from '../base/flags';
import { toState } from '../base/redux';
import { isLocalVideoTrackDesktop } from '../base/tracks';
/**
* Returns true if the toolbox is visible.
@@ -18,3 +20,13 @@ export function isToolboxVisible(stateful: Object | Function) {
return enabled && (alwaysVisible || visible || participantCount === 1 || flag);
}
/**
* Indicates if the video mute button is disabled or not.
*
* @param {string} state - The state from the Redux store.
* @returns {boolean}
*/
export function isVideoMuteButtonDisabled(state: Object) {
return !hasAvailableDevices(state, 'videoInput') || isLocalVideoTrackDesktop(state);
}

View File

@@ -77,3 +77,13 @@ export function isAudioSettingsButtonDisabled(state: Object) {
export function isVideoSettingsButtonDisabled(state: Object) {
return !hasAvailableDevices(state, 'videoInput');
}
/**
* Indicates if the video mute button is disabled or not.
*
* @param {string} state - The state from the Redux store.
* @returns {boolean}
*/
export function isVideoMuteButtonDisabled(state: Object) {
return !hasAvailableDevices(state, 'videoInput');
}

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

@@ -19,6 +19,7 @@ import { connect } from '../../base/redux';
import { ColorPalette } from '../../base/styles';
import {
createDesiredLocalTracks,
destroyLocalDesktopTrackIfExists,
destroyLocalTracks
} from '../../base/tracks';
import { HelpView } from '../../help';
@@ -81,6 +82,8 @@ class WelcomePage extends AbstractWelcomePage {
if (this.props._settings.startAudioOnly) {
dispatch(destroyLocalTracks());
} else {
dispatch(destroyLocalDesktopTrackIfExists());
// Make sure we don't request the permission for the camera from
// the start. We will, however, create a video track iff the user
// already granted the permission.

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,5 +0,0 @@
local jibri_queue_component
= module:get_option_string(
"jibri_queue_component", "jibriqueue"..module.host);
module:add_identity("component", "jibri-queue", jibri_queue_component);

View File

@@ -1,559 +0,0 @@
local st = require "util.stanza";
local jid = require "util.jid";
local http = require "net.http";
local json = require "cjson";
local inspect = require('inspect');
local socket = require "socket";
local uuid_gen = require "util.uuid".generate;
local jwt = require "luajwtjitsi";
local it = require "util.iterators";
local neturl = require "net.url";
local parse = neturl.parseQuery;
local get_room_from_jid = module:require "util".get_room_from_jid;
local room_jid_match_rewrite = module:require "util".room_jid_match_rewrite;
local is_healthcheck_room = module:require "util".is_healthcheck_room;
local room_jid_split_subdomain = module:require "util".room_jid_split_subdomain;
local internal_room_jid_match_rewrite = module:require "util".internal_room_jid_match_rewrite;
local async_handler_wrapper = module:require "util".async_handler_wrapper;
-- this basically strips the domain from the conference.domain address
local parentHostName = string.gmatch(tostring(module.host), "%w+.(%w.+)")();
if parentHostName == nil then
log("error", "Failed to start - unable to get parent hostname");
return;
end
local parentCtx = module:context(parentHostName);
if parentCtx == nil then
log("error",
"Failed to start - unable to get parent context for host: %s",
tostring(parentHostName));
return;
end
local token_util = module:require "token/util".new(parentCtx);
local ASAPKeyServer;
local ASAPKeyPath;
local ASAPKeyId;
local ASAPIssuer;
local ASAPAudience;
local ASAPAcceptedIssuers;
local ASAPAcceptedAudiences;
local ASAPTTL;
local ASAPTTL_THRESHOLD;
local ASAPKey;
local JibriRegion;
local disableTokenVerification;
local muc_component_host;
local external_api_url;
local jwtKeyCacheSize;
local jwtKeyCache;
local function load_config()
ASAPKeyServer = module:get_option_string("asap_key_server");
if ASAPKeyServer then
module:log("debug", "ASAP Public Key URL %s", ASAPKeyServer);
token_util:set_asap_key_server(ASAPKeyServer);
end
ASAPKeyPath
= module:get_option_string("asap_key_path", '/etc/prosody/certs/asap.key');
ASAPKeyId
= module:get_option_string("asap_key_id", 'jitsi');
ASAPIssuer
= module:get_option_string("asap_issuer", 'jitsi');
ASAPAudience
= module:get_option_string("asap_audience", 'jibri-queue');
ASAPAcceptedIssuers
= module:get_option_array('asap_accepted_issuers',{'jibri-queue'});
module:log("debug", "ASAP Accepted Issuers %s", ASAPAcceptedIssuers);
token_util:set_asap_accepted_issuers(ASAPAcceptedIssuers);
ASAPAcceptedAudiences
= module:get_option_array('asap_accepted_audiences',{'*'});
module:log("debug", "ASAP Accepted Audiences %s", ASAPAcceptedAudiences);
token_util:set_asap_accepted_audiences(ASAPAcceptedAudiences);
-- do not require room to be set on tokens for jibri queue
token_util:set_asap_require_room_claim(false);
ASAPTTL
= module:get_option_number("asap_ttl", 3600);
ASAPTTL_THRESHOLD
= module:get_option_number("asap_ttl_threshold", 600);
queueServiceURL
= module:get_option_string("jibri_queue_url");
JibriRegion
= module:get_option_string("jibri_region", 'default');
-- option to enable/disable token verifications
disableTokenVerification
= module:get_option_boolean("disable_jibri_queue_token_verification", false);
muc_component_host
= module:get_option_string("muc_component");
external_api_url = module:get_option_string("external_api_url",tostring(parentHostName));
module:log("debug", "External advertised API URL", external_api_url);
-- TODO: Figure out a less arbitrary default cache size.
jwtKeyCacheSize
= module:get_option_number("jwt_pubkey_cache_size", 128);
jwtKeyCache = require"util.cache".new(jwtKeyCacheSize);
if queueServiceURL == nil then
log("error", "No jibri_queue_url specified. No service to contact!");
return;
end
if muc_component_host == nil then
log("error", "No muc_component specified. No muc to operate on for jibri queue!");
return;
end
-- Read ASAP key once on module startup
local f = io.open(ASAPKeyPath, "r");
if f then
ASAPKey = f:read("*all");
f:close();
if not ASAPKey then
module:log("warn", "No ASAP Key read from %s, disabling jibri queue component plugin", ASAPKeyPath);
return
end
else
module:log("warn", "Error reading ASAP Key %s, disabling jibri queue component plugin", ASAPKeyPath);
return
end
return true;
end
local function reload_config()
module:log("info", "Reloading configuration for jibri queue component");
local config_success = load_config();
-- clear ASAP public key cache on config reload
token_util:clear_asap_cache();
if not config_success then
log("error", "Unsuccessful reconfiguration, jibri queue component may misbehave");
end
end
local config_success = load_config();
if not config_success then
log("error", "Unsuccessful configuration step, jibri queue component disabled")
return;
end
local http_headers = {
["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")",
["Content-Type"] = "application/json"
};
-- we use async to detect Prosody 0.10 and earlier
local have_async = pcall(require, "util.async");
if not have_async then
module:log("warn", "conference duration will not work with Prosody version 0.10 or less.");
return;
end
log("info", "Starting jibri queue handling for %s", muc_component_host);
local function round(num, numDecimalPlaces)
local mult = 10^(numDecimalPlaces or 0)
return math.floor(num * mult + 0.5) / mult
end
local function generateToken(audience)
audience = audience or ASAPAudience
local t = os.time()
local err
local exp_key = 'asap_exp.'..audience
local token_key = 'asap_token.'..audience
local exp = jwtKeyCache:get(exp_key)
local token = jwtKeyCache:get(token_key)
--if we find a token and it isn't too far from expiry, then use it
if token ~= nil and exp ~= nil then
exp = tonumber(exp)
if (exp - t) > ASAPTTL_THRESHOLD then
return token
end
end
--expiry is the current time plus TTL
exp = t + ASAPTTL
local payload = {
iss = ASAPIssuer,
aud = audience,
nbf = t,
exp = exp,
}
-- encode
local alg = "RS256"
token, err = jwt.encode(payload, ASAPKey, alg, {kid = ASAPKeyId})
if not err then
token = 'Bearer '..token
jwtKeyCache:set(exp_key,exp)
jwtKeyCache:set(token_key,token)
return token
else
return ''
end
end
local function sendIq(participant,action,requestId,time,position,token)
local iqId = uuid_gen();
local from = module:get_host();
local outStanza = st.iq({type = 'set', from = from, to = participant, id = iqId}):tag("jibri-queue",
{ xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId, action = action });
if token then
outStanza:tag("token"):text(token):up()
end
if time then
outStanza:tag("time"):text(tostring(time)):up()
end
if position then
outStanza:tag("position"):text(tostring(position)):up()
end
module:send(outStanza);
end
local function cb(content_, code_, response_, request_)
if code_ == 200 or code_ == 204 then
module:log("debug", "URL Callback: Code %s, Content %s, Request (host %s, path %s, body %s), Response: %s",
code_, content_, request_.host, request_.path, inspect(request_.body), inspect(response_));
else
module:log("warn", "URL Callback non successful: Code %s, Content %s, Request (%s), Response: %s",
code_, content_, inspect(request_), inspect(response_));
end
end
local function sendEvent(type,room_address,participant,requestId,replyIq,replyError)
local event_ts = round(socket.gettime()*1000);
local node, host, resource, target_subdomain = room_jid_split_subdomain(room_address);
local room_param = '';
if target_subdomain then
room_param = target_subdomain..'/'..node;
else
room_param = node;
end
local out_event = {
["conference"] = room_address,
["roomParam"] = room_param,
["eventType"] = type,
["participant"] = participant,
["externalApiUrl"] = external_api_url.."/jibriqueue/update",
["requestId"] = requestId,
["region"] = JibriRegion,
}
module:log("debug","Sending event %s",inspect(out_event));
local headers = http_headers or {}
headers['Authorization'] = generateToken()
module:log("debug","Sending headers %s",inspect(headers));
local requestURL = queueServiceURL.."/job/recording"
if type=="LeaveQueue" then
requestURL = requestURL .."/cancel"
end
local request = http.request(requestURL, {
headers = headers,
method = "POST",
body = json.encode(out_event)
}, function (content_, code_, response_, request_)
if code_ == 200 or code_ == 204 then
module:log("debug", "URL Callback: Code %s, Content %s, Request (host %s, path %s, body %s), Response: %s",
code_, content_, request_.host, request_.path, inspect(request_.body), inspect(response_));
if (replyIq) then
module:log("debug", "sending reply IQ %s",inspect(replyIq));
module:send(replyIq);
end
else
module:log("warn", "URL Callback non successful: Code %s, Content %s, Request (%s), Response: %s",
code_, content_, inspect(request_), inspect(response_));
if (replyError) then
module:log("warn", "sending reply error IQ %s",inspect(replyError));
module:send(replyError);
end
end
end);
end
function clearRoomQueueByOccupant(room, occupant)
room.jibriQueue[occupant.jid] = nil;
end
function addRoomQueueByOccupant(room, occupant, requestId)
room.jibriQueue[occupant.jid] = requestId;
end
-- receives iq from client currently connected to the room
function on_iq(event)
local requestId;
-- Check the type of the incoming stanza to avoid loops:
if event.stanza.attr.type == "error" then
return; -- We do not want to reply to these, so leave.
end
if event.stanza.attr.to == module:get_host() then
if event.stanza.attr.type == "set" then
local reply = st.reply(event.stanza);
local replyError = st.error_reply(event.stanza,'cancel','internal-server-error',"Queue Server Error");
local jibriQueue
= event.stanza:get_child('jibri-queue', 'http://jitsi.org/protocol/jibri-queue');
if jibriQueue then
module:log("debug", "Received Jibri Queue Request: %s ",inspect(jibriQueue));
local roomAddress = jibriQueue.attr.room;
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
if not room then
module:log("warn", "No room found %s", roomAddress);
return false;
end
local from = event.stanza.attr.from;
local occupant = room:get_occupant_by_real_jid(from);
if not occupant then
module:log("warn", "No occupant %s found for %s", from, roomAddress);
return false;
end
local action = jibriQueue.attr.action;
if action == 'join' then
-- join action, so send event out
requestId = uuid_gen();
module:log("debug","Received join queue request for jid %s occupant %s requestId %s",roomAddress,occupant.jid,requestId);
-- now handle new jibri queue message
addRoomQueueByOccupant(room, occupant, requestId);
reply:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
replyError:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
module:log("debug","Sending JoinQueue event for jid %s occupant %s reply %s",roomAddress,occupant.jid,inspect(reply));
sendEvent('JoinQueue',roomAddress,occupant.jid,requestId,reply,replyError);
end
if action == 'leave' then
requestId = jibriQueue.attr.requestId;
module:log("debug","Received leave queue request for jid %s occupant %s requestId %s",roomAddress,occupant.jid,requestId);
-- TODO: check that requestId is the same as cached value
clearRoomQueueByOccupant(room, occupant);
reply:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
replyError:add_child(st.stanza("jibri-queue", { xmlns = 'http://jitsi.org/protocol/jibri-queue', requestId = requestId})):up()
module:log("debug","Sending LeaveQueue event for jid %s occupant %s reply %s",roomAddress,occupant.jid,inspect(reply));
sendEvent('LeaveQueue',roomAddress,occupant.jid,requestId,reply,replyError);
end
else
module:log("warn","Jibri Queue Stanza missing child %s",inspect(event.stanza))
end
end
end
return true
end
-- create recorder queue cache for the room
function room_created(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
room.jibriQueue = {};
end
-- Conference ended, clear all queue cache jids
function room_destroyed(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
for jid, x in pairs(room.jibriQueue) do
if x then
sendEvent('LeaveQueue',internal_room_jid_match_rewrite(room.jid),jid,x);
end
end
end
-- Occupant left remove it from the queue if it joined the queue
function occupant_leaving(event)
local room = event.room;
if is_healthcheck_room(room.jid) then
return;
end
local occupant = event.occupant;
local requestId = room.jibriQueue[occupant.jid];
-- check if user has cached queue request
if requestId then
-- remove occupant from queue cache, signal backend
room.jibriQueue[occupant.jid] = nil;
sendEvent('LeaveQueue',internal_room_jid_match_rewrite(room.jid),occupant.jid,requestId);
end
end
module:hook("iq/host", on_iq);
-- executed on every host added internally in prosody, including components
function process_host(host)
if host == muc_component_host then -- the conference muc component
module:log("debug","Hook to muc events on %s", host);
local muc_module = module:context(host);
muc_module:hook("muc-room-created", room_created, -1);
-- muc_module:hook("muc-occupant-joined", occupant_joined, -1);
muc_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
muc_module:hook("muc-room-destroyed", room_destroyed, -1);
end
end
if prosody.hosts[muc_component_host] == nil then
module:log("debug","No muc component found, will listen for it: %s", muc_component_host)
-- when a host or component is added
prosody.events.add_handler("host-activated", process_host);
else
process_host(muc_component_host);
end
module:log("info", "Loading jibri_queue_component");
--- Verifies room name, domain name with the values in the token
-- @param token the token we received
-- @param room_name the room name
-- @param group name of the group (optional)
-- @param session the session to use for storing token specific fields
-- @return true if values are ok or false otherwise
function verify_token(token, room_jid, session)
if disableTokenVerification then
return true;
end
-- if not disableTokenVerification and we do not have token
-- stop here, cause the main virtual host can have guest access enabled
-- (allowEmptyToken = true) and we will allow access to rooms info without
-- a token
if token == nil then
log("warn", "no token provided");
return false;
end
session.auth_token = token;
local verified, reason, message = token_util:process_and_verify_token(session);
if not verified then
log("warn", "not a valid token %s: %s", tostring(reason), tostring(message));
log("debug", "invalid token %s", token);
return false;
end
return true;
end
--- Handles request for updating jibri queue status
-- @param event the http event, holds the request query
-- @return GET response, containing a json with response details
function handle_update_jibri_queue(event)
local body = json.decode(event.request.body);
module:log("debug","Update Jibri Queue Event Received: %s",inspect(body));
local token = event.request.headers["authorization"];
if not token then
token = ''
else
local prefixStart, prefixEnd = token:find("Bearer ");
if prefixStart ~= 1 then
module:log("error", "REST event: Invalid authorization header format. The header must start with the string 'Bearer '");
return { status_code = 403; };
end
token = token:sub(prefixEnd + 1);
end
local user_jid = body["participant"];
local roomAddress = body["conference"];
local userJWT = body["token"];
local action = body["action"];
local time = body["time"];
local position = body["position"];
local requestId = body["requestId"];
if not action then
if userJWT then
action = 'token';
else
action = 'info';
end
end
local room_jid = room_jid_match_rewrite(roomAddress);
if not verify_token(token, room_jid, {}) then
log("error", "REST event: Invalid token for room %s to route action %s for requestId %s", roomAddress, action, requestId);
return { status_code = 403; };
end
local room = get_room_from_jid(room_jid);
if (not room) then
log("error", "REST event: no room found %s to route action %s for requestId %s", roomAddress, action, requestId);
return { status_code = 404; };
end
local occupant = room:get_occupant_by_real_jid(user_jid);
if not occupant then
log("warn", "REST event: No occupant %s found for %s to route action %s for requestId %s", user_jid, roomAddress, action, requestId);
return { status_code = 404; };
end
if not room.jibriQueue[occupant.jid] then
log("warn", "REST event: No queue request found for occupant %s in conference %s to route action %s for requestId %s",occupant.jid,room.jid, action, requestId)
return { status_code = 404; };
end
if not requestId then
requestId = room.jibriQueue[occupant.jid];
end
if action == 'token' and userJWT then
log("debug", "REST event: Token received for occupant %s in conference %s requestId %s, clearing room queue");
clearRoomQueueByOccupant(room, occupant);
end
log("debug", "REST event: Sending update for occupant %s in conference %s to route action %s for requestId %s",occupant.jid,room.jid, action, requestId);
sendIq(occupant.jid,action,requestId,time,position,userJWT);
return { status_code = 200; };
end
module:depends("http");
module:provides("http", {
default_path = "/";
name = "jibriqueue";
route = {
["POST /jibriqueue/update"] = function (event) return async_handler_wrapper(event,handle_update_jibri_queue) end;
};
});
module:hook_global('config-reloaded', reload_config);

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

@@ -7,99 +7,32 @@ local jid = require "util.jid";
local filters = require "util.filters";
local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
if not muc_domain_base then
module:log("warn", "No 'muc_mapper_domain_base' option set, disabling muc_mapper plugin inactive");
return
end
-- The "real" MUC domain that we are proxying to
local muc_domain = module:get_option_string("muc_mapper_domain", muc_domain_prefix.."."..muc_domain_base);
local escaped_muc_domain_base = muc_domain_base:gsub("%p", "%%%1");
local escaped_muc_domain_prefix = muc_domain_prefix:gsub("%p", "%%%1");
-- The pattern used to extract the target subdomain (e.g. extract 'foo' from 'foo.muc.example.com')
local target_subdomain_pattern = "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
-- table to store all incoming iqs without roomname in it, like discoinfo to the muc compoent
local roomless_iqs = {};
if not muc_domain then
module:log("warn", "No 'muc_mapper_domain' option set, disabling muc_mapper plugin inactive");
return
end
-- Utility function to check and convert a room JID from virtual room1@muc.foo.example.com to real [foo]room1@muc.example.com
local function match_rewrite_to_jid(room_jid, stanza)
local node, host, resource = jid.split(room_jid);
local target_subdomain = host and host:match(target_subdomain_pattern);
if not target_subdomain then
module:log("debug", "No need to rewrite out 'to' %s", room_jid);
return room_jid;
end
-- Ok, rewrite room_jid address to new format
local new_node, new_host, new_resource;
if node then
new_node, new_host, new_resource = "["..target_subdomain.."]"..node, muc_domain, resource;
else
module:log("debug", "No room name provided so rewriting only host 'to' %s", room_jid);
new_host, new_resource = muc_domain, resource;
if (stanza.attr and stanza.attr.id) then
roomless_iqs[stanza.attr.id] = stanza.attr.to;
end
end
room_jid = jid.join(new_node, new_host, new_resource);
module:log("debug", "Rewrote to %s", room_jid);
return room_jid
end
-- Utility function to check and convert a room JID from real [foo]room1@muc.example.com to virtual room1@muc.foo.example.com
local function match_rewrite_from_jid(room_jid, stanza)
local node, host, resource = jid.split(room_jid);
if host ~= muc_domain or not node then
module:log("debug", "No need to rewrite %s (not from the MUC host) %s, %s", room_jid, stanza.attr.id, roomless_iqs[stanza.attr.id]);
if (stanza.attr and stanza.attr.id and roomless_iqs[stanza.attr.id]) then
local result = roomless_iqs[stanza.attr.id];
roomless_iqs[stanza.attr.id] = nil;
return result;
end
return room_jid;
end
local target_subdomain, target_node = node:match("^%[([^%]]+)%](.+)$");
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);
module:log("debug", "Rewrote to %s", room_jid);
return room_jid
end
local util = module:require "util";
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
-- We must filter stanzas in order to hook in to all incoming and outgoing messaging which skips the stanza routers
function filter_stanza(stanza)
if stanza.name == "message" or stanza.name == "iq" or stanza.name == "presence" then
module:log("debug", "Filtering stanza type %s to %s from %s",stanza.name,stanza.attr.to,stanza.attr.from);
-- module:log("debug", "Filtering stanza type %s to %s from %s",stanza.name,stanza.attr.to,stanza.attr.from);
if stanza.name == "iq" then
local conf = stanza:get_child('conference')
if conf then
module:log("debug", "Filtering stanza conference %s to %s from %s",conf.attr.room,stanza.attr.to,stanza.attr.from);
conf.attr.room = match_rewrite_to_jid(conf.attr.room, stanza)
-- module:log("debug", "Filtering stanza conference %s to %s from %s",conf.attr.room,stanza.attr.to,stanza.attr.from);
conf.attr.room = room_jid_match_rewrite(conf.attr.room, stanza)
end
end
if stanza.attr.to then
stanza.attr.to = match_rewrite_to_jid(stanza.attr.to, stanza)
stanza.attr.to = room_jid_match_rewrite(stanza.attr.to, stanza)
end
if stanza.attr.from then
stanza.attr.from = match_rewrite_from_jid(stanza.attr.from, stanza)
stanza.attr.from = internal_room_jid_match_rewrite(stanza.attr.from, stanza)
end
end
return stanza;
@@ -107,7 +40,6 @@ end
function filter_session(session)
module:log("warn", "Session filters applied");
-- filters.add_filter(session, "stanzas/in", filter_stanza_in);
filters.add_filter(session, "stanzas/out", filter_stanza);
end
@@ -128,14 +60,14 @@ end
local function outgoing_stanza_rewriter(event)
local stanza = event.stanza;
if stanza.attr.to then
stanza.attr.to = match_rewrite_to_jid(stanza.attr.to, stanza)
stanza.attr.to = room_jid_match_rewrite(stanza.attr.to, stanza)
end
end
local function incoming_stanza_rewriter(event)
local stanza = event.stanza;
if stanza.attr.from then
stanza.attr.from = match_rewrite_from_jid(stanza.attr.from, stanza)
stanza.attr.from = internal_room_jid_match_rewrite(stanza.attr.from, stanza)
end
end

View File

@@ -166,7 +166,7 @@ function attach_lobby_room(room)
new_room:set_persistent(true);
module:log("debug","Lobby room jid = %s created",lobby_room_jid);
new_room.main_room = room;
room._data.lobbyroom = new_room;
room._data.lobbyroom = new_room.jid;
room:save(true);
return true
end
@@ -178,9 +178,12 @@ function destroy_lobby_room(room, newjid, message)
if not message then
message = 'Lobby room closed.';
end
if room and room._data.lobbyroom then
room._data.lobbyroom:set_persistent(false);
room._data.lobbyroom:destroy(newjid, message);
if lobby_muc_service and room and room._data.lobbyroom then
local lobby_room_obj = lobby_muc_service.get_room_from_jid(room._data.lobbyroom);
if lobby_room_obj then
lobby_room_obj:set_persistent(false);
lobby_room_obj:destroy(newjid, message);
end
room._data.lobbyroom = nil;
end
end
@@ -315,7 +318,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
label = 'Lobby room jid';
value = '';
});
event.formdata['muc#roominfo_lobbyroom'] = room._data.lobbyroom.jid;
event.formdata['muc#roominfo_lobbyroom'] = room._data.lobbyroom;
end
end);
@@ -364,7 +367,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
local reply = st.error_reply(stanza, 'auth', 'registration-required'):up();
reply.tags[1].attr.code = '407';
reply:tag('x', {xmlns = MUC_NS}):up();
reply:tag('lobbyroom'):text(room._data.lobbyroom.jid);
reply:tag('lobbyroom'):text(room._data.lobbyroom);
event.origin.send(reply:tag('x', {xmlns = MUC_NS}));
return true;
end
@@ -377,13 +380,16 @@ process_host_module(main_muc_component_config, function(host_module, host)
local from = stanza:get_child('x', 'http://jabber.org/protocol/muc#user')
:get_child('invite').attr.from;
if room._data.lobbyroom then
local occupant = room._data.lobbyroom:get_occupant_by_real_jid(invitee);
if occupant then
local display_name = occupant:get_presence():get_child_text(
'nick', 'http://jabber.org/protocol/nick');
if lobby_muc_service and room._data.lobbyroom then
local lobby_room_obj = lobby_muc_service.get_room_from_jid(room._data.lobbyroom);
if lobby_room_obj then
local occupant = lobby_room_obj:get_occupant_by_real_jid(invitee);
if occupant then
local display_name = occupant:get_presence():get_child_text(
'nick', 'http://jabber.org/protocol/nick');
notify_lobby_access(room, from, occupant.nick, display_name, true);
notify_lobby_access(room, from, occupant.nick, display_name, true);
end
end
end
end);

View File

@@ -17,7 +17,7 @@ local cache = dep.softreq("util.cache"); -- only available in prosody 0.10+
local uuid_generate = require "util.uuid".generate;
local jid = require "util.jid";
local t_insert, t_remove = table.insert, table.remove;
local t_remove = table.remove;
local math_min = math.min;
local math_max = math.max;
local os_time = os.time;
@@ -26,6 +26,7 @@ local add_filter = require "util.filters".add_filter;
local timer = require "util.timer";
local datetime = require "util.datetime";
local xmlns_mam2 = "urn:xmpp:mam:2";
local xmlns_sm2 = "urn:xmpp:sm:2";
local xmlns_sm3 = "urn:xmpp:sm:3";
local xmlns_errors = "urn:ietf:params:xml:ns:xmpp-stanzas";
@@ -34,11 +35,11 @@ local xmlns_delay = "urn:xmpp:delay";
local sm2_attr = { xmlns = xmlns_sm2 };
local sm3_attr = { xmlns = xmlns_sm3 };
local resume_timeout = module:get_option_number("smacks_hibernation_time", 300);
local resume_timeout = module:get_option_number("smacks_hibernation_time", 600);
local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", false);
local s2s_resend = module:get_option_boolean("smacks_s2s_resend", false);
local max_unacked_stanzas = module:get_option_number("smacks_max_unacked_stanzas", 0);
local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 60);
local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 30);
local max_hibernated_sessions = module:get_option_number("smacks_max_hibernated_sessions", 10);
local max_old_sessions = module:get_option_number("smacks_max_old_sessions", 10);
local core_process_stanza = prosody.core_process_stanza;
@@ -200,8 +201,15 @@ local function request_ack_if_needed(session, force, reason)
end
local function outgoing_stanza_filter(stanza, session)
local is_stanza = stanza.attr and not stanza.attr.xmlns and not stanza.name:find":";
if is_stanza and not stanza._cached then -- Stanza in default stream namespace
-- XXX: Normally you wouldn't have to check the xmlns for a stanza as it's
-- supposed to be nil.
-- However, when using mod_smacks with mod_websocket, then mod_websocket's
-- stanzas/out filter can get called before this one and adds the xmlns.
local is_stanza = stanza.attr and
(not stanza.attr.xmlns or stanza.attr.xmlns == 'jabber:client')
and not stanza.name:find":";
if is_stanza and not stanza._cached then
local queue = session.outgoing_stanza_queue;
local cached_stanza = st.clone(stanza);
cached_stanza._cached = true;
@@ -400,12 +408,14 @@ local function handle_unacked_stanzas(session)
session.outgoing_stanza_queue = {};
for i=1,#queue do
if not module:fire_event("delivery/failure", { session = session, stanza = queue[i] }) then
local reply = st.reply(queue[i]);
if reply.attr.to ~= session.full_jid then
reply.attr.type = "error";
reply:tag("error", error_attr)
:tag("recipient-unavailable", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"});
core_process_stanza(session, reply);
if queue[i].attr.type ~= "error" then
local reply = st.reply(queue[i]);
if reply.attr.to ~= session.full_jid then
reply.attr.type = "error";
reply:tag("error", error_attr)
:tag("recipient-unavailable", {xmlns = "urn:ietf:params:xml:ns:xmpp-stanzas"});
core_process_stanza(session, reply);
end
end
end
end
@@ -413,37 +423,46 @@ local function handle_unacked_stanzas(session)
end
-- don't send delivery errors for messages which will be delivered by mam later on
-- check if stanza was archived --> this will allow us to send back errors for stanzas not archived
-- because the user configured the server to do so ("no-archive"-setting for one special contact for example)
local function get_stanza_id(stanza, by_jid)
for tag in stanza:childtags("stanza-id", "urn:xmpp:sid:0") do
if tag.attr.by == by_jid then
return tag.attr.id;
end
end
return nil;
end
module:hook("delivery/failure", function(event)
local session, stanza = event.session, event.stanza;
-- Only deal with authenticated (c2s) sessions
if session.username then
if stanza.name == "message" and stanza.attr.xmlns == nil and
( stanza.attr.type == "chat" or ( stanza.attr.type or "normal" ) == "normal" ) then
-- don't store messages in offline store if they are mam results
local mam_result = stanza:get_child("result", xmlns_mam2);
if mam_result ~= nil then
return true; -- stanza already "handled", don't send an error and don't add it to offline storage
end
-- do nothing here for normal messages and don't send out "message delivery errors",
-- because messages are already in MAM at this point (no need to frighten users)
if session.mam_requested and stanza._was_archived then
local stanza_id = get_stanza_id(stanza, jid.bare(session.full_jid));
if session.mam_requested and stanza_id ~= nil then
session.log("debug", "mod_smacks delivery/failure returning true for mam-handled stanza: mam-archive-id=%s", tostring(stanza_id));
return true; -- stanza handled, don't send an error
end
-- store message in offline store, if this client does not use mam *and* was the last client online
local sessions = prosody.hosts[module.host].sessions[session.username] and
prosody.hosts[module.host].sessions[session.username].sessions or nil;
if sessions and next(sessions) == session.resource and next(sessions, session.resource) == nil then
module:fire_event("message/offline/handle", { origin = session, stanza = stanza } );
return true; -- stanza handled, don't send an error
local ok = module:fire_event("message/offline/handle", { origin = session, stanza = stanza } );
session.log("debug", "mod_smacks delivery/failuere returning %s for offline-handled stanza", tostring(ok));
return ok; -- if stanza was handled, don't send an error
end
end
end
end);
-- mark stanzas as archived --> this will allow us to send back errors for stanzas not archived
-- because the user configured the server to do so ("no-archive"-setting for one special contact for example)
module:hook("archive-message-added", function(event)
local session, stanza, for_user, stanza_id = event.origin, event.stanza, event.for_user, event.id;
if session then session.log("debug", "Marking stanza as archived, archive_id: %s, stanza: %s", tostring(stanza_id), tostring(stanza:top_tag())); end
if not session then module:log("debug", "Marking stanza as archived in unknown session, archive_id: %s, stanza: %s", tostring(stanza_id), tostring(stanza:top_tag())); end
stanza._was_archived = true;
end);
module:hook("pre-resource-unbind", function (event)
local session, err = event.session, event.error;
if session.smacks then

View File

@@ -93,8 +93,7 @@ end
-- saves start time if it is new dominat speaker
-- or calculates and accumulates time of speaking
function SpeakerStats:setDominantSpeaker(isNowDominantSpeaker)
log("debug",
"set isDominant %s for %s", tostring(isNowDominantSpeaker), self.nick);
-- log("debug", "set isDominant %s for %s", tostring(isNowDominantSpeaker), self.nick);
if not self:isDominantSpeaker() and isNowDominantSpeaker then
self._dominantSpeakerStart = socket.gettime()*1000;

View File

@@ -1,101 +0,0 @@
--- mod_websocket.lua
+++ mod_websocket.lua
@@ -163,34 +163,34 @@ function handle_request(event)
return 403;
end
- local function websocket_close(code, message)
+ local function websocket_close(conn, code, message)
conn:write(build_close(code, message));
conn:close();
end
local dataBuffer;
- local function handle_frame(frame)
+ local function handle_frame(conn, frame)
local opcode = frame.opcode;
local length = frame.length;
module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
-- Error cases
if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
- websocket_close(1002, "Reserved bits not zero");
+ websocket_close(conn, 1002, "Reserved bits not zero");
return false;
end
if opcode == 0x8 then -- close frame
if length == 1 then
- websocket_close(1002, "Close frame with payload, but too short for status code");
+ websocket_close(conn, 1002, "Close frame with payload, but too short for status code");
return false;
elseif length >= 2 then
local status_code = parse_close(frame.data)
if status_code < 1000 then
- websocket_close(1002, "Closed with invalid status code");
+ websocket_close(conn, 1002, "Closed with invalid status code");
return false;
elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
- websocket_close(1002, "Closed with reserved status code");
+ websocket_close(conn, 1002, "Closed with reserved status code");
return false;
end
end
@@ -198,28 +198,28 @@ function handle_request(event)
if opcode >= 0x8 then
if length > 125 then -- Control frame with too much payload
- websocket_close(1002, "Payload too large");
+ websocket_close(conn, 1002, "Payload too large");
return false;
end
if not frame.FIN then -- Fragmented control frame
- websocket_close(1002, "Fragmented control frame");
+ websocket_close(conn, 1002, "Fragmented control frame");
return false;
end
end
if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then
- websocket_close(1002, "Reserved opcode");
+ websocket_close(conn, 1002, "Reserved opcode");
return false;
end
if opcode == 0x0 and not dataBuffer then
- websocket_close(1002, "Unexpected continuation frame");
+ websocket_close(conn, 1002, "Unexpected continuation frame");
return false;
end
if (opcode == 0x1 or opcode == 0x2) and dataBuffer then
- websocket_close(1002, "Continuation frame expected");
+ websocket_close(conn, 1002, "Continuation frame expected");
return false;
end
@@ -229,11 +229,11 @@ function handle_request(event)
elseif opcode == 0x1 then -- Text frame
dataBuffer = {frame.data};
elseif opcode == 0x2 then -- Binary frame
- websocket_close(1003, "Only text frames are supported");
+ websocket_close(conn, 1003, "Only text frames are supported");
return;
elseif opcode == 0x8 then -- Close request
- websocket_close(1000, "Goodbye");
- return;
+ websocket_close(conn, 1000, "Goodbye");
+ return "";
elseif opcode == 0x9 then -- Ping frame
frame.opcode = 0xA;
conn:write(build_frame(frame));
@@ -276,7 +276,7 @@ function handle_request(event)
while frame do
frameBuffer = frameBuffer:sub(length + 1);
- local result = handle_frame(frame);
+ local result = handle_frame(session.conn, frame);
if not result then return; end
cache[#cache+1] = filter_open_close(result);
frame, length = parse_frame(frameBuffer);

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;
@@ -142,40 +144,44 @@ function Util:get_public_key(keyId)
end
--- Verifies issuer part of token
-- @param 'iss' claim from the token to verify
-- @param 'issClaim' claim from the token to verify
-- @param 'acceptedIssuers' list of issuers to check
-- @return nil and error string or true for accepted claim
function Util:verify_issuer(issClaim, acceptedIssuers)
if not acceptedIssuers then
acceptedIssuers = self.acceptedIssuers
end
module:log("debug","verify_issuer claim: %s against accepted: %s",issClaim, acceptedIssuers);
module:log("debug", "verify_issuer claim: %s against accepted: %s", issClaim, acceptedIssuers);
for i, iss in ipairs(acceptedIssuers) do
if iss == '*' then
-- "*" indicates to accept any issuer in the claims so return success
return true;
end
if issClaim == iss then
--claim matches an accepted issuer so return success
-- claim matches an accepted issuer so return success
return true;
end
end
--if issClaim not found in acceptedIssuers, fail claim
-- if issClaim not found in acceptedIssuers, fail claim
return nil, "Invalid issuer ('iss' claim)";
end
--- Verifies audience part of token
-- @param 'aud' claim from the token to verify
-- @param 'audClaim' claim from the token to verify
-- @return nil and error string or true for accepted claim
function Util:verify_audience(audClaim)
module:log("debug","verify_audience claim: %s against accepted: %s",audClaim, self.acceptedAudiences);
module:log("debug", "verify_audience claim: %s against accepted: %s", audClaim, self.acceptedAudiences);
for i, aud in ipairs(self.acceptedAudiences) do
if aud == '*' then
--* indicates to accept any audience in the claims so return success
-- "*" indicates to accept any audience in the claims so return success
return true;
end
if audClaim == aud then
--claim matches an accepted audience so return success
-- claim matches an accepted audience so return success
return true;
end
end
--if issClaim not found in acceptedIssuers, fail claim
-- if audClaim not found in acceptedAudiences, fail claim
return nil, "Invalid audience ('aud' claim)";
end
@@ -346,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

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