Compare commits

...

20 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
3bf82b573c shared-video: use a more recent video by default 2020-06-03 11:27:08 +02:00
hmuresan
b4b4339a1a external_api: add start/stop recording commands 2020-06-03 09:30:19 +02:00
Hristo Terezov
6773aed67f feat(recording): Limit notification 2020-06-02 16:00:54 -05:00
Saúl Ibarra Corretgé
d740752522 rn,responsive-ui: refactor dimensions detection
Use a dimensions detecting root component. The Dimensions module does not
measure the app's view size, but the Window, which may not be the same, for
example on iOS when PiP is used.

Also refactor the aspect ratio wrap component since it can be taken directly
from the store.

Last, remove the use of DimensionsDetector on LargeVideo and TileView since they
occupy the full-screen anyway.

Fixes PiP mode on iOS.
2020-06-02 16:54:28 +02:00
Marius Bardan
d93b219c7f lang: update RO translations 2020-06-02 10:19:06 +02:00
Marius Bardan
10cd150a07 lang: update RO translations 2020-06-02 10:19:06 +02:00
Jaya Allamsetty
a31f3c0c76 fix(config): Add missing capScreenshareBitrate to config.js 2020-05-29 14:04:30 -04:00
Simon Honegger
af39186a5f fix: typeof returns a string, so this condition was always true 2020-05-29 19:53:05 +02:00
Saúl Ibarra Corretgé
d4d1d0aa70 dev: add plugin for circular dependency detection
Example run: https://gist.github.com/saghul/e5e12edc108bdedbcbe65a3d7528235f
2020-05-29 10:37:09 +02:00
Saúl Ibarra Corretgé
3a88f4939c misc: break import cycle 2020-05-29 10:37:09 +02:00
Saúl Ibarra Corretgé
fe221fe4be deps: run npm audit fix 2020-05-29 10:37:09 +02:00
Jaya Allamsetty
1caaa47f5e chore(deps): update lib-jitsi-meet
fix(safari): Disable VAD processing on Safari - cfbb511bce
2020-05-28 17:19:07 -04:00
Hristo Terezov
a2c4d17e4d fix(record):web/mobile match disable functionality 2020-05-28 15:39:49 -05:00
Hristo Terezov
ce1de9e1e7 feat(recording): Disable buttons on active session 2020-05-28 15:39:49 -05:00
Hristo Terezov
3e7abf3da0 feat(subtitles): Disable for guests. 2020-05-28 13:43:18 -05:00
damencho
8b4f1789a6 chore(deps): Update lib-jitsi-meet, callstats using full URL. 2020-05-28 11:15:33 -05:00
Gabriel Imre
444e2b90df callstats: add siteID passing; sanitize confID path 2020-05-28 10:00:45 -05:00
Saúl Ibarra Corretgé
7de88995a5 labels: don't disable all labels when VIDEO_QUALITY_LABEL_DISABLED is set
Fixes: https://github.com/jitsi/jitsi-meet/issues/6880
2020-05-28 10:15:52 +02:00
Hristo Terezov
f0c6e934ce feat(config):InsecureRoomNameWarning config option 2020-05-27 18:03:15 -05:00
Дамян Минков
78b01d2c97 Adding whitelist and move away from using custom field for password. (#6621)
* Adding whitelist and move away from using custom field for password.

We re-use room lock for lobby password.

* Make sure we do not run muc-occupant-pre-join for non members only rooms.

* Destroying lobby room, when main room is destroyed or membersonly is disabled.

* Adds destroy reason.

* Clears lobby room instance on destroy.

Fixes problem with on/off/on of lobby feature.

* Add lobby room jid only when members only is on.

* Sends main room jid on lobby destroy.

We can use that in client loggic to auto-join lobby participants to main room as lobby is disabled while waiting.

* fix: Fixes using is_healthcheck_room.

* squash: Enables lobby rooms feature by default.

* chore(deps): Update lib-jitsi-meet, to enable lobby rooms.
2020-05-27 18:01:41 -05:00
50 changed files with 1232 additions and 1095 deletions

View File

@@ -101,7 +101,10 @@ import {
trackAdded,
trackRemoved
} from './react/features/base/tracks';
import { getJitsiMeetGlobalNS } from './react/features/base/util';
import {
getBackendSafePath,
getJitsiMeetGlobalNS
} from './react/features/base/util';
import { showDesktopPicker } from './react/features/desktop-picker';
import { appendSuffix } from './react/features/display-name';
import { setE2EEKey } from './react/features/e2ee';
@@ -1258,7 +1261,7 @@ export default {
items[key] = param[1];
}
if (typeof items.e2eekey !== undefined) {
if (typeof items.e2eekey !== 'undefined') {
APP.store.dispatch(setE2EEKey(items.e2eekey));
// Clean URL in browser history.
@@ -1364,7 +1367,13 @@ export default {
const options = config;
const { email, name: nick } = getLocalParticipant(APP.store.getState());
const { locationURL } = APP.store.getState()['features/base/connection'];
const state = APP.store.getState();
const { locationURL } = state['features/base/connection'];
const { tenant } = state['features/base/jwt'];
if (tenant) {
options.siteID = tenant;
}
if (options.enableDisplayNameInStats && nick) {
options.statisticsDisplayName = nick;
@@ -1376,7 +1385,7 @@ export default {
options.applicationName = interfaceConfig.APP_NAME;
options.getWiFiStatsMethod = this._getWiFiStatsMethod;
options.confID = `${locationURL.host}${locationURL.pathname}`;
options.confID = `${locationURL.host}${getBackendSafePath(locationURL.pathname)}`;
options.createVADProcessor = createRnnoiseProcessorPromise;
// Disable CallStats, if requessted.

View File

@@ -54,6 +54,13 @@ var config = {
// Disables the auto-play behavior of *all* newly created video element.
// This is useful when the client runs on a host with limited resources.
// noAutoPlayVideo: false
// Enable / disable 500 Kbps bitrate cap on desktop tracks. When enabled,
// simulcast is turned off for the desktop share. If presenter is turned
// on while screensharing is in progress, the max bitrate is automatically
// adjusted to 2.5 Mbps. This takes a value between 0 and 1 which determines
// the probability for this to be enabled.
// capScreenshareBitrate: 1 // 0 to disable
},
// Disables ICE/UDP by filtering out local and remote UDP candidates in
@@ -210,6 +217,21 @@ var config = {
// Default value for the channel "last N" attribute. -1 for unlimited.
channelLastN: -1,
// // Options for the recording limit notification.
// recordingLimit: {
//
// // The recording limit in minutes. Note: This number appears in the notification text
// // but doesn't enforce the actual recording time limit. This should be configured in
// // jibri!
// limit: 60,
//
// // The name of the app with unlimited recordings.
// appName: 'Unlimited recordings APP',
//
// // The URL of the app with unlimited recordings.
// appURL: 'https://unlimited.recordings.app.com/'
// },
// Disables or enables RTX (RFC 4588) (defaults to false).
// disableRtx: false,

View File

@@ -46,8 +46,12 @@ VirtualHost "jitmeet.example.com"
"speakerstats";
"turncredentials";
"conference_duration";
"muc_lobby_rooms";
}
c2s_require_encryption = false
lobby_muc = "lobby.jitmeet.example.com"
main_muc = "conference.jitmeet.example.com"
-- muc_lobby_whitelist = { "recorder.jitmeet.example.com" } -- Here we can whitelist jibri to enter lobby enabled rooms
Component "conference.jitmeet.example.com" "muc"
storage = "memory"
@@ -81,3 +85,9 @@ Component "speakerstats.jitmeet.example.com" "speakerstats_component"
Component "conferenceduration.jitmeet.example.com" "conference_duration_component"
muc_component = "conference.jitmeet.example.com"
Component "lobby.jitmeet.example.com" "muc"
storage = "memory"
restrict_room_creation = true
muc_room_locking = false
muc_room_default_public_jids = true

View File

@@ -1,38 +1,45 @@
{
"en": "Engleză",
"af": "Afrikaans",
"az": "",
"ar": "Arabă",
"bg": "Bulgară",
"ca": "Catalană",
"cs": "Cehă",
"da": "Daneză",
"de": "Germană",
"el": "Greacă",
"enGB": "Engleză (Regatul Unit)",
"eo": "Esperanto",
"es": "Spaniolă",
"esUS": "Spaniolă (America Latină)",
"et": "Estonă",
"eu": "Bască",
"fi": "Finlandeză",
"fr": "Franceză",
"hy": "Armeniana",
"frCA": "Franceză (Canada)",
"he": "Ebraică",
"hr": "Croată",
"hu": "Maghiară",
"hy": "Armeană",
"id": "Indoneziană",
"it": "Italiană",
"ja": "Japoneză",
"ko": "Koreană",
"nb": "",
"oc": "Occitan",
"lt": "Lituaniană",
"nl": "Olandeză",
"oc": "Occitană",
"pl": "Poloneză",
"ptBR": "Portugheză (Brazilia)",
"ru": "Rusă",
"ro": "Română",
"sc": "Sardă",
"sk": "Slovacă",
"sl": "Slovenă",
"sv": "Suedeză",
"th": "Thailandeză",
"tr": "Turcă",
"uk": "Ucraineană",
"vi": "Vietnameză",
"zhCN": "Chineză (China)",
"zhTW": "Chineză (Taiwan)",
"nl": "Olandeză",
"hu": "Maghiară",
"hr": "Croată",
"frCA": "Franceză (Canadia)",
"fi": "Finlandeză",
"et": "Estoniană",
"esUS": "Spaniolă (America Latină)",
"enGB": "Engleză (Regatul Unit)",
"da": "Daneză",
"că": "Catalană"
"zhTW": "Chineză (Taiwan)"
}

View File

@@ -21,7 +21,8 @@
"bluetooth": "Bluetooth",
"headphones": "Căști",
"phone": "Telefon",
"speaker": "Difuzor"
"speaker": "Difuzor",
"none": ""
},
"audioOnly": {
"audioOnly": "Doar audio"
@@ -51,7 +52,12 @@
"popover": "Alegeți un pseudonim",
"title": "Introduceți un pseudonim pentru a conversa"
},
"title": "Apel video"
"title": "Apel video",
"you": "",
"privateNotice": "",
"noMessagesMessage": "",
"messageTo": "",
"fieldPlaceHolder": ""
},
"connectingOverlay": {
"joiningRoom": "Sunteți conectat la conversația dumneavoastră ..."
@@ -66,7 +72,11 @@
"DISCONNECTED": "Deconectat",
"DISCONNECTING": "Se deconectează",
"ERROR": "Eroare",
"RECONNECTING": "A apărut o eroare de rețea. Reconectare..."
"RECONNECTING": "A apărut o eroare de rețea. Reconectare...",
"LOW_BANDWIDTH": "",
"GOT_SESSION_ID": "",
"GET_SESSION_ID_ERROR": "",
"FETCH_SESSION_ID": ""
},
"connectionindicator": {
"address": "Adresă:",
@@ -97,7 +107,8 @@
"status": "Conexiune:",
"transport": "Mod Transport:",
"transport_plural": "Moduri Transport:",
"turn": " (turn)"
"turn": " (turn)",
"e2e_rtt": ""
},
"dateUtils": {
"earlier": "Mai devreme",
@@ -109,6 +120,8 @@
"description": "Nu s-a întâmplat nimic? Am încercat să vă deschidem conversația în {{app}} aplicația pentru desktop. Încercați din nou sau deschideți {{app}} aplicația web.",
"descriptionWithoutWeb": "",
"downloadApp": "Descărcați aplicația",
"ifDoNotHaveApp": "Dacă nu aveti încă aplicația atunci:",
"ifHaveApp": "Dacă aveti deja aplicația:",
"launchWebButton": "Deschideți în browser",
"openApp": "Continuați spre aplicație",
"title": "Deschidere apel video în {{app}}...",
@@ -256,13 +269,24 @@
"WaitForHostMsgWOk": "Conferința {{room}} nu a început. Daca sunteți moderatorul, apăsați butonul OK pentru autentificare. Dacă nu, așteptați ca moderatorul să înceapă conferința.",
"WaitingForHost": "Așteptare moderator conferință ...",
"Yes": "Da",
"yourEntireScreen": "Întregul ecran"
"yourEntireScreen": "Întregul ecran",
"sendPrivateMessageTitle": "",
"sendPrivateMessageOk": "",
"sendPrivateMessageCancel": "",
"sendPrivateMessage": "",
"screenSharingAudio": "",
"muteEveryoneStartMuted": "",
"muteEveryoneSelf": "",
"muteEveryoneTitle": "",
"muteEveryoneDialog": "",
"muteEveryoneElseTitle": "",
"muteEveryoneElseDialog": ""
},
"dialOut": {
"statusMessage": "Este {{status}}"
},
"feedback": {
"average": "Mediu",
"average": "Medie",
"bad": "Rău",
"detailsLabel": "Spuneți-ne mai multe despre experiența dumneavoastră.",
"good": "Bine",
@@ -335,7 +359,8 @@
"toggleFilmstrip": "Afișați sau ascundeți imagini video",
"toggleScreensharing": "Comutați între cameră și partajare ecran",
"toggleShortcuts": "Arătați sau ascundeți comenzi rapide tastatură",
"videoMute": "Porniți sau opriți camera"
"videoMute": "Porniți sau opriți camera",
"videoQuality": ""
},
"liveStreaming": {
"busy": "Lucrăm la eliberarea resurselor de transmitere. Vă rugam să încercați din nou în câteva minute.",
@@ -363,7 +388,11 @@
"signOut": "Deconectare",
"start": "Începeți o transmitere live",
"streamIdHelp": "Ce înseamnă acest lucru?",
"unavailableTitle": "Transmitere live indisponibilă"
"unavailableTitle": "Transmitere live indisponibilă",
"onBy": "",
"offBy": "",
"googlePrivacyPolicy": "Politica de confidențialitate Google",
"youtubeTerms": "Termeni și condiții Youtube"
},
"localRecording": {
"clientState": {
@@ -426,9 +455,10 @@
"unmute": "",
"newDeviceCameraTitle": "A fost detectată o cameră noua",
"newDeviceAudioTitle": "A fost detectat un dispozitiv audio nou",
"newDeviceAction": "Utilizați"
"newDeviceAction": "Utilizați",
"suboptimalBrowserWarning": "Folosind acest browser nu veți beneficia de cea mai bună experiență pentru aceste apeluri video. În timp ce lucrăm la asta, vă recomandăm să folosiți unul din <a href='{{recommendedBrowserPageLink}}' target='_blank'>browser-ele suportate</a>."
},
"passwordSetRemotely": "Setat de un alt membru",
"passwordSetRemotely": "Setată de un alt membru",
"passwordDigitsOnly": "Până la {{number}} cifre",
"poweredby": "cu sprijinul",
"presenceStatus": {
@@ -474,7 +504,9 @@
"signIn": "Conectare",
"signOut": "Deconectare",
"unavailable": "Oops! Serviciul {{serviceName}} este indisponibil momentan. Se lucrează la remedierea acestei probleme. Vă rugam să încercați mai tărziu.",
"unavailableTitle": "Înregistrare indisponibilă"
"unavailableTitle": "Înregistrare indisponibilă",
"onBy": "{{name}} a pornit înregistrarea",
"offBy": "{{name}} a oprit înregistrarea"
},
"sectionList": {
"pullToRefresh": "Trageți pentru a reîmprospătă"
@@ -499,8 +531,10 @@
"selectCamera": "Cameră",
"selectMic": "Microfon",
"startAudioMuted": "Toată lumea începe cu sunetul dezactivat",
"startVideoMuted": "Toată lumea începe cu sunetul dezactivat",
"title": "Setări"
"startVideoMuted": "Toată lumea începe cu fară video",
"title": "Setări",
"speakers": "Difuzoare",
"microphones": "Microfoane"
},
"settingsView": {
"alertOk": "OK",
@@ -515,7 +549,11 @@
"serverURL": "Server URL",
"startWithAudioMuted": "Începeți cu sunetul dezactivat",
"startWithVideoMuted": "Începeți cu video dezactivat",
"version": "Version"
"version": "Version",
"showAdvanced": "Arată setările avansate",
"disableP2P": "Dezactivează modul Peer-To-Peer",
"disableCallIntegration": "Dezactivează integrarea cu apelurile native",
"advanced": "Avansat"
},
"share": {
"dialInfoText": "\n\n=====\n\nVreti doar să apelați telefonic?\n\n{{defaultDialInNumber}}Faceți click pe acest link pentru a vizualiza numerele pentru apelare în acest apel video\n{{dialInfoPageUrl}}",
@@ -525,10 +563,10 @@
"speakerStats": {
"hours": "{{count}}h",
"minutes": "{{count}}m",
"name": "Name",
"name": "Nume",
"seconds": "{{count}}s",
"speakerStats": "Parametrii difuzorului",
"speakerTime": "Durată participant"
"speakerStats": "Statistici participanți",
"speakerTime": "Durată vorbire participant"
},
"startupoverlay": {
"policyText": " ",
@@ -572,9 +610,14 @@
"tileView": "Afișați/ascundeți miniatura video",
"toggleCamera": "Afișați / ascundeți camera",
"videomute": "Activați / dezactivați înregistrarea",
"videoblur": ""
"videoblur": "",
"privateMessage": "Trimite un mesaj privat",
"muteEveryone": "Oprește microfonul tuturor",
"moreOptions": "Arată mai multe opțiuni",
"help": "Ajutor",
"download": "Descarcă aplicațiile noastre"
},
"addPeople": "Adaugați persoane în apel",
"addPeople": "Adăugați persoane în apel",
"audioOnlyOff": "Dezactivați modul 'doar audio'",
"audioOnlyOn": "Activați modul 'doar audio'",
"audioRoute": "Selectați dispozitivul pentru sunet",
@@ -664,9 +707,6 @@
"lowDefinition": "Calitate redusă",
"onlyAudioAvailable": "Doar audio este disponibil",
"onlyAudioSupported": "În acest navigator este suportat doar 'mod audio'.",
"p2pEnabled": "Peer-to-Peer activat",
"p2pVideoQualityDescription": "În modul peer-to-peer pentru calitatea apelurilor primite puteți alege doar între calitate superioară sau 'mod audio'. Alte setări nu pot fi activate până când modul peer-to-peer nu este dezactivat.",
"recHighDefinitionOnly": "Este de preferat o calitate superioară.",
"sd": "SD",
"standardDefinition": "Calitate standard"
},
@@ -698,7 +738,7 @@
"go": "ÎNCEPEȚI",
"join": "ACCESARE",
"info": "Informații",
"privacy": "Securitate",
"privacy": "Confidențialitate",
"recentList": "Recent",
"recentListDelete": "Ștergeți",
"recentListEmpty": "Lista dumneavoastră recentă este momentan goală. Discutați cu echipa dumneavoastră și veți găsi toate conversațiile aici.",
@@ -707,13 +747,16 @@
"roomnameHint": "Introduceți numele sau adresa web a ședinței la care doriți să vă conectați. Puteți asocia un nume, dar transmiteți și celorlalți participanți acest nume.",
"sendFeedback": "Lăsați-ne feedback",
"terms": "Termeni",
"title": "Video-conferință securizata, cu opțiuni multiple și complet gratuită "
"title": "Video-conferință securizată, cu multiple funcționalități și complet gratuită"
},
"lonelyMeetingExperience": {
"button": "Invită alte persoane",
"youAreAlone": "Ești singura persoană din acest apel"
"documentSharing": {
"title": ""
},
"helpView": {
"header": "Centru de ajutor"
}
"defaultNickname": "",
"chromeExtensionBanner": {
"dontShowAgain": "",
"buttonText": "",
"installExtensionText": ""
},
"raisedHand": "Ar dori să vorbească"
}

View File

@@ -216,6 +216,7 @@
"kickParticipantTitle": "Kick this participant?",
"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.",
@@ -249,6 +250,7 @@
"popupError": "Your browser is blocking pop-up windows from this site. Please enable pop-ups in your browser's security settings and try again.",
"popupErrorTitle": "Pop-up blocked",
"recording": "Recording",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Not possible while a live stream is active",
"recordingDisabledForGuestTooltip": "Guests can't start recordings.",
"recordingDisabledTooltip": "Start recording disabled.",
"rejoinNow": "Rejoin now",
@@ -393,6 +395,8 @@
"videoQuality": "Manage call quality"
},
"liveStreaming": {
"limitNotificationDescriptionWeb": "Due to high demand your streaming will be limited to {{limit}} min. For unlimited streaming try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Your streaming will be limited to {{limit}} min. For unlimited streaming try {{app}}.",
"busy": "We're working on freeing streaming resources. Please try again in a few minutes.",
"busyTitle": "All streamers are currently busy",
"changeSignIn": "Switch accounts.",
@@ -550,6 +554,8 @@
},
"raisedHand": "Would like to speak",
"recording": {
"limitNotificationDescriptionWeb": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"limitNotificationDescriptionNative": "Due to high demand your recording will be limited to {{limit}} min. For unlimited recordings try <3>{{app}}</3>.",
"authDropboxText": "Upload to Dropbox",
"availableSpace": "Available space: {{spaceLeft}} MB (approximately {{duration}} minutes of recording)",
"beta": "BETA",

View File

@@ -8,16 +8,21 @@ import {
sendAnalytics
} from '../../react/features/analytics';
import {
getCurrentConference,
sendTones,
setPassword,
setSubject
} from '../../react/features/base/conference';
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import {
processExternalDeviceRequest
} from '../../react/features/device-selection/functions';
import { isEnabled as isDropboxEnabled } from '../../react/features/dropbox';
import { setE2EEKey } from '../../react/features/e2ee';
import { invite } from '../../react/features/invite';
import { RECORDING_TYPES } from '../../react/features/recording/constants';
import { getActiveSession } from '../../react/features/recording/functions';
import { muteAllParticipants } from '../../react/features/remote-video-menu/actions';
import { toggleTileView } from '../../react/features/video-layout';
import { setVideoQuality } from '../../react/features/video-quality';
@@ -190,6 +195,114 @@ function initCommands() {
logger.debug('Set video quality command received');
sendAnalytics(createApiEvent('set.video.quality'));
APP.store.dispatch(setVideoQuality(frameHeight));
},
/**
* Starts a file recording or streaming depending on the passed on params.
* For youtube streams, `youtubeStreamKey` must be passed on. `youtubeBroadcastID` is optional.
* For dropbox recording, recording `mode` should be `file` and a dropbox oauth2 token must be provided.
* For file recording, recording `mode` should be `file` and optionally `shouldShare` could be passed on.
* No other params should be passed.
*
* @param { string } arg.mode - Recording mode, either `file` or `stream`.
* @param { string } arg.dropboxToken - Dropbox oauth2 token.
* @param { boolean } arg.shouldShare - Whether the recording should be shared with the participants or not.
* Only applies to certain jitsi meet deploys.
* @param { string } arg.youtubeStreamKey - The youtube stream key.
* @param { string } arg.youtubeBroadcastID - The youtube broacast ID.
* @returns {void}
*/
'start-recording': ({ mode, dropboxToken, shouldShare, youtubeStreamKey, youtubeBroadcastID }) => {
const state = APP.store.getState();
const conference = getCurrentConference(state);
if (!conference) {
logger.error('Conference is not defined');
return;
}
if (dropboxToken && !isDropboxEnabled(state)) {
logger.error('Failed starting recording: dropbox is not enabled on this deployment');
return;
}
if (mode === JitsiRecordingConstants.mode.STREAM && !youtubeStreamKey) {
logger.error('Failed starting recording: missing youtube stream key');
return;
}
let recordingConfig;
if (mode === JitsiRecordingConstants.mode.FILE) {
if (dropboxToken) {
recordingConfig = {
mode: JitsiRecordingConstants.mode.FILE,
appData: JSON.stringify({
'file_recording_metadata': {
'upload_credentials': {
'service_name': RECORDING_TYPES.DROPBOX,
'token': dropboxToken
}
}
})
};
} else {
recordingConfig = {
mode: JitsiRecordingConstants.mode.FILE,
appData: JSON.stringify({
'file_recording_metadata': {
'share': shouldShare
}
})
};
}
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
recordingConfig = {
broadcastId: youtubeBroadcastID,
mode: JitsiRecordingConstants.mode.STREAM,
streamId: youtubeStreamKey
};
} else {
logger.error('Invalid recording mode provided');
return;
}
conference.startRecording(recordingConfig);
},
/**
* Stops a recording or streaming in progress.
*
* @param {string} mode - `file` or `stream`.
* @returns {void}
*/
'stop-recording': mode => {
const state = APP.store.getState();
const conference = getCurrentConference(state);
if (!conference) {
logger.error('Conference is not defined');
return;
}
if (![ JitsiRecordingConstants.mode.FILE, JitsiRecordingConstants.mode.STREAM ].includes(mode)) {
logger.error('Invalid recording mode provided!');
return;
}
const activeSession = getActiveSession(state, mode);
if (activeSession && activeSession.id) {
conference.stopRecording(activeSession.id);
} else {
logger.error('No recording or streaming session found');
}
}
};
transport.on('event', ({ data, name }) => {
@@ -514,7 +627,8 @@ class API {
notifyDeviceListChanged(devices: Object) {
this._sendEvent({
name: 'device-list-changed',
devices });
devices
});
}
/**

View File

@@ -37,6 +37,8 @@ const commands = {
sendEndpointTextMessage: 'send-endpoint-text-message',
sendTones: 'send-tones',
setVideoQuality: 'set-video-quality',
startRecording: 'start-recording',
stopRecording: 'stop-recording',
subject: 'subject',
submitFeedback: 'submit-feedback',
toggleAudio: 'toggle-audio',

View File

@@ -31,7 +31,7 @@ export const SHARED_VIDEO_CONTAINER_TYPE = 'sharedvideo';
* Example shared video link.
* @type {string}
*/
const defaultSharedVideoLink = 'https://www.youtube.com/watch?v=xNXN7CZk8X0';
const defaultSharedVideoLink = 'https://youtu.be/TB7LlM4erx8';
const updateInterval = 5000; // milliseconds
/**

719
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -56,7 +56,7 @@
"js-md5": "0.6.1",
"js-utils": "github:jitsi/js-utils#cf11996bd866fdb47326c59a5d3bc24be17282d4",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#c94f6a570f69ebfe18de6c1549cc76370c791468",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#cfbb511bcec24df44879a1fc271498e2d80e8477",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.13",
"moment": "2.19.4",
@@ -110,6 +110,7 @@
"@babel/runtime": "7.5.5",
"babel-eslint": "10.0.1",
"babel-loader": "8.0.4",
"circular-dependency-plugin": "5.2.0",
"clean-css": "3.4.25",
"css-loader": "0.28.7",
"eslint": "5.6.1",
@@ -128,7 +129,7 @@
"string-replace-loader": "2.1.1",
"style-loader": "0.19.0",
"unorm": "1.6.0",
"webpack": "4.27.1",
"webpack": "4.43.0",
"webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.3.11",
"webpack-dev-server": "3.11.0"

View File

@@ -9,7 +9,7 @@ import { DialogContainer } from '../../base/dialog';
import { CALL_INTEGRATION_ENABLED, updateFlags } from '../../base/flags';
import '../../base/jwt';
import { Platform } from '../../base/react';
import '../../base/responsive-ui';
import { DimensionsDetector, clientResized } from '../../base/responsive-ui';
import { updateSettings } from '../../base/settings';
import '../../google-api';
import '../../mobile/audio-mode';
@@ -78,6 +78,9 @@ export class App extends AbstractApp {
// This will effectively kill the app. In accord with the Web, do not
// kill the app.
this._maybeDisableExceptionsManager();
// Bind event handler so it is only bound once per instance.
this._onDimensionsChanged = this._onDimensionsChanged.bind(this);
}
/**
@@ -107,6 +110,21 @@ export class App extends AbstractApp {
});
}
/**
* Overrides the parent method to inject {@link DimensionsDetector} as
* the top most component.
*
* @override
*/
_createMainElement(component, props) {
return (
<DimensionsDetector
onDimensionsChanged = { this._onDimensionsChanged }>
{ super._createMainElement(component, props) }
</DimensionsDetector>
);
}
/**
* Attempts to disable the use of React Native
* {@link ExceptionsManager#handleException} on platforms and in
@@ -144,6 +162,22 @@ export class App extends AbstractApp {
}
}
_onDimensionsChanged: (width: number, height: number) => void;
/**
* Updates the known available size for the app to occupy.
*
* @param {number} width - The component's current width.
* @param {number} height - The component's current height.
* @private
* @returns {void}
*/
_onDimensionsChanged(width: number, height: number) {
const { dispatch } = this.state.store;
dispatch(clientResized(width, height));
}
/**
* Renders the platform specific dialog container.
*

View File

@@ -24,6 +24,7 @@ import {
} from '../participants';
import { getLocalTracks, trackAdded, trackRemoved } from '../tracks';
import {
getBackendSafePath,
getBackendSafeRoomName,
getJitsiMeetGlobalNS
} from '../util';
@@ -417,7 +418,9 @@ export function createConference() {
}
const config = state['features/base/config'];
const { tenant } = state['features/base/jwt'];
const { email, name: nick } = getLocalParticipant(state);
const conference
= connection.initJitsiConference(
@@ -425,7 +428,8 @@ export function createConference() {
...config,
applicationName: getName(),
getWiFiStatsMethod: getJitsiMeetGlobalNS().getWiFiStats,
confID: `${locationURL.host}${locationURL.pathname}`,
confID: `${locationURL.host}${getBackendSafePath(locationURL.pathname)}`,
siteID: tenant,
statisticsDisplayName: config.enableDisplayNameInStats ? nick : undefined,
statisticsId: config.enableEmailInStats ? email : undefined
});

View File

@@ -146,6 +146,7 @@ function _setJWT(store, next, action) {
action.callee = context.callee;
action.group = context.group;
action.server = context.server;
action.tenant = context.tenant;
action.user = user;
user && _overwriteLocalParticipant(

View File

@@ -1,8 +1,8 @@
import { combineReducers } from 'redux';
import { CONFERENCE_FAILED, CONFERENCE_LEFT } from '../conference';
import { CONFERENCE_FAILED, CONFERENCE_LEFT } from '../conference/actionTypes';
import { ReducerRegistry } from '../redux';
import { TRACK_REMOVED } from '../tracks';
import { TRACK_REMOVED } from '../tracks/actionTypes';
import {
SET_AUDIO_AVAILABLE,

View File

@@ -1,70 +0,0 @@
// @flow
import React, { Component } from 'react';
import { connect } from '../../redux';
import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from '../constants';
/**
* The type of the React {@code Component} props of {@link AspectRatioAware}.
*/
type Props = {
aspectRatio: ASPECT_RATIO_NARROW | ASPECT_RATIO_WIDE
};
/**
* Determines whether a specific React {@code Component} decorated into an
* {@link AspectRatioAware} has {@link ASPECT_RATIO_NARROW} as the value of its
* {@code aspectRatio} React prop.
*
* @param {AspectRatioAware} component - An {@link AspectRatioAware} which may
* have an {@code aspectRatio} React prop.
* @returns {boolean}
*/
export function isNarrowAspectRatio(component: React$Component<*>) {
return component.props.aspectRatio === ASPECT_RATIO_NARROW;
}
/**
* Decorates a specific React {@code Component} class into an
* {@link AspectRatioAware} which provides the React prop {@code aspectRatio}
* updated on each redux state change.
*
* @param {Class<React$Component>} WrappedComponent - A React {@code Component}
* class to be wrapped.
* @returns {AspectRatioAwareWrapper}
*/
export function makeAspectRatioAware(
WrappedComponent: Class<React$Component<*>>
): Class<React$Component<*>> {
/**
* Renders {@code WrappedComponent} with the React prop {@code aspectRatio}.
*/
class AspectRatioAware extends Component<Props> {
/**
* Implement's React render method to wrap the nested component.
*
* @returns {React$Element}
*/
render(): React$Element<*> {
return <WrappedComponent { ...this.props } />;
}
}
return connect(_mapStateToProps)(AspectRatioAware);
}
/**
* Maps (parts of) the redux state to {@link AspectRatioAware} props.
*
* @param {Object} state - The whole redux state.
* @private
* @returns {{
* aspectRatio: Symbol
* }}
*/
function _mapStateToProps(state) {
return {
aspectRatio: state['features/base/responsive-ui'].aspectRatio
};
}

View File

@@ -1,13 +1,9 @@
// @flow
import React, { PureComponent } from 'react';
import { View } from 'react-native';
import { StyleSheet, View } from 'react-native';
import styles from './styles';
/**
* AspectRatioDetector component's property types.
*/
type Props = {
/**
@@ -64,7 +60,7 @@ export default class DimensionsDetector extends PureComponent<Props> {
return (
<View
onLayout = { this._onLayout }
style = { styles.dimensionsDetector } >
style = { StyleSheet.absoluteFillObject } >
{ this.props.children }
</View>
);

View File

@@ -1,2 +1 @@
export * from './AspectRatioAware';
export { default as DimensionsDetector } from './DimensionsDetector';

View File

@@ -1,14 +0,0 @@
import { createStyleSheet } from '../../styles';
/**
* The styles of the feature base/responsive-ui.
*/
export default createStyleSheet({
/**
* The style of {@link DimensionsDetector} used on react-native.
*/
dimensionsDetector: {
alignSelf: 'stretch',
flex: 1
}
});

View File

@@ -1,16 +1,10 @@
// @flow
import { Dimensions } from 'react-native';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import { MiddlewareRegistry } from '../../base/redux';
import { CLIENT_RESIZED } from './actionTypes';
import { setAspectRatio, setReducedUI } from './actions';
/**
* Dimensions change handler.
*/
let handler;
/**
* Middleware that handles widnow dimension changes and updates the aspect ratio and
@@ -19,65 +13,19 @@ let handler;
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
MiddlewareRegistry.register(({ dispatch }) => next => action => {
const result = next(action);
switch (action.type) {
case APP_WILL_UNMOUNT: {
_appWillUnmount();
case CLIENT_RESIZED: {
const { clientWidth: width, clientHeight: height } = action;
dispatch(setAspectRatio(width, height));
dispatch(setReducedUI(width, height));
break;
}
case APP_WILL_MOUNT:
_appWillMount(store);
break;
}
return result;
});
/**
* Notifies this feature that the action {@link APP_WILL_MOUNT} is being
* dispatched within a specific redux {@code store}.
*
* @param {Store} store - The redux store in which the specified {@code action}
* is being dispatched.
* @private
* @returns {void}
*/
function _appWillMount(store) {
handler = dim => {
_onDimensionsChange(dim, store);
};
Dimensions.addEventListener('change', handler);
}
/**
* Notifies this feature that the action {@link APP_WILL_UNMOUNT} is being
* dispatched within a specific redux {@code store}.
*
* @private
* @returns {void}
*/
function _appWillUnmount() {
Dimensions.removeEventListener('change', handler);
handler = undefined;
}
/**
* Handles window dimension changes.
*
* @param {Object} dimensions - The new dimensions.
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _onDimensionsChange(dimensions, store) {
const { width, height } = dimensions.window;
const { dispatch } = store;
dispatch(setAspectRatio(width, height));
dispatch(setReducedUI(width, height));
}

View File

@@ -23,7 +23,6 @@ const DEFAULT_STATE = {
ReducerRegistry.register('features/base/responsive-ui', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case CLIENT_RESIZED: {
return {
...state,
clientWidth: action.clientWidth,

View File

@@ -99,6 +99,24 @@ function _fixURIStringScheme(uri: string) {
return uri;
}
/**
* Converts a path to a backend-safe format, by splitting the path '/' processing each part.
* Properly lowercased and url encoded.
*
* @param {string?} path - The path to convert.
* @returns {string?}
*/
export function getBackendSafePath(path: ?string): ?string {
if (!path) {
return path;
}
return path
.split('/')
.map(getBackendSafeRoomName)
.join('/');
}
/**
* Converts a room name to a backend-safe format. Properly lowercased and url encoded.
*

View File

@@ -50,8 +50,9 @@ export default class AbstractInsecureRoomNameLabel extends PureComponent<Props>
*/
export function _mapStateToProps(state: Object): $Shape<Props> {
const { room } = state['features/base/conference'];
const { enableInsecureRoomNameWarning = false } = state['features/base/config'];
return {
_visible: room && isInsecureRoomName(room)
_visible: enableInsecureRoomNameWarning && room && isInsecureRoomName(room)
};
}

View File

@@ -8,10 +8,7 @@ import { appNavigate } from '../../../app';
import { PIP_ENABLED, getFeatureFlag } from '../../../base/flags';
import { Container, LoadingIndicator, TintedView } from '../../../base/react';
import { connect } from '../../../base/redux';
import {
isNarrowAspectRatio,
makeAspectRatioAware
} from '../../../base/responsive-ui';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import { TestConnectionInfo } from '../../../base/testing';
import { ConferenceNotification, isCalendarEnabled } from '../../../calendar-sync';
import { Chat } from '../../../chat';
@@ -45,10 +42,13 @@ import styles, { NAVBAR_GRADIENT_COLORS } from './styles';
*/
type Props = AbstractProps & {
/**
* Application's aspect ratio.
*/
_aspectRatio: Symbol,
/**
* Wherther the calendar feature is enabled or not.
*
* @private
*/
_calendarEnabled: boolean,
@@ -57,15 +57,11 @@ type Props = AbstractProps & {
* conference which includes establishing the XMPP connection and then
* joining the room. If truthy, then an activity/loading indicator will be
* rendered.
*
* @private
*/
_connecting: boolean,
/**
* Set to {@code true} when the filmstrip is currently visible.
*
* @private
*/
_filmstripVisible: boolean,
@@ -76,34 +72,17 @@ type Props = AbstractProps & {
/**
* Whether Picture-in-Picture is enabled.
*
* @private
*/
_pictureInPictureEnabled: boolean,
/**
* The indicator which determines whether the UI is reduced (to accommodate
* smaller display areas).
*
* @private
*/
_reducedUI: boolean,
/**
* The handler which dispatches the (redux) action {@link setToolboxVisible}
* to show/hide the {@link Toolbox}.
*
* @param {boolean} visible - {@code true} to show the {@code Toolbox} or
* {@code false} to hide it.
* @private
* @returns {void}
*/
_setToolboxVisible: Function,
/**
* The indicator which determines whether the Toolbox is visible.
*
* @private
*/
_toolboxVisible: boolean,
@@ -249,6 +228,7 @@ class Conference extends AbstractConference<Props, *> {
*/
_renderContent() {
const {
_aspectRatio,
_connecting,
_filmstripVisible,
_largeVideoParticipantId,
@@ -257,7 +237,8 @@ class Conference extends AbstractConference<Props, *> {
_toolboxVisible
} = this.props;
const showGradient = _toolboxVisible;
const applyGradientStretching = _filmstripVisible && isNarrowAspectRatio(this) && !_shouldDisplayTileView;
const applyGradientStretching
= _filmstripVisible && _aspectRatio === ASPECT_RATIO_NARROW && !_shouldDisplayTileView;
if (_reducedUI) {
return this._renderContentForReducedUi();
@@ -393,7 +374,9 @@ class Conference extends AbstractConference<Props, *> {
// flex layout. The only option that seemed to limit the notification's
// size was explicit 'width' value which is not better than the margin
// added here.
if (this.props._filmstripVisible && !isNarrowAspectRatio(this)) {
const { _aspectRatio, _filmstripVisible } = this.props;
if (_filmstripVisible && _aspectRatio !== ASPECT_RATIO_NARROW) {
notificationsStyle.marginRight = FILMSTRIP_SIZE;
}
@@ -433,7 +416,7 @@ function _mapStateToProps(state) {
joining,
leaving
} = state['features/base/conference'];
const { reducedUI } = state['features/base/responsive-ui'];
const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
// XXX There is a window of time between the successful establishment of the
// XMPP connection and the subsequent commencement of joining the MUC during
@@ -449,61 +432,15 @@ function _mapStateToProps(state) {
return {
...abstractMapStateToProps(state),
/**
* Wherther the calendar feature is enabled or not.
*
* @private
* @type {boolean}
*/
_aspectRatio: aspectRatio,
_calendarEnabled: isCalendarEnabled(state),
/**
* The indicator which determines that we are still connecting to the
* conference which includes establishing the XMPP connection and then
* joining the room. If truthy, then an activity/loading indicator will
* be rendered.
*
* @private
* @type {boolean}
*/
_connecting: Boolean(connecting_),
/**
* Is {@code true} when the filmstrip is currently visible.
*/
_filmstripVisible: isFilmstripVisible(state),
/**
* The ID of the participant currently on stage.
*/
_largeVideoParticipantId: state['features/large-video'].participantId,
/**
* Whether Picture-in-Picture is enabled.
*
* @private
* @type {boolean}
*/
_pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),
/**
* The indicator which determines whether the UI is reduced (to
* accommodate smaller display areas).
*
* @private
* @type {boolean}
*/
_reducedUI: reducedUI,
/**
* The indicator which determines whether the Toolbox is visible.
*
* @private
* @type {boolean}
*/
_toolboxVisible: isToolboxVisible(state)
};
}
export default connect(_mapStateToProps)(makeAspectRatioAware(Conference));
export default connect(_mapStateToProps)(Conference);

View File

@@ -5,10 +5,7 @@ import { TouchableOpacity, View } from 'react-native';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { connect } from '../../../base/redux';
import {
isNarrowAspectRatio,
makeAspectRatioAware
} from '../../../base/responsive-ui';
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
import {
RecordingExpandedLabel
} from '../../../recording';
@@ -29,14 +26,19 @@ import styles from './styles';
type Props = AbstractLabelsProps & {
/**
* Function to translate i18n labels.
* Application's aspect ratio.
*/
t: Function,
_aspectRatio: Symbol,
/**
* True if the labels should be visible, false otherwise.
*/
_visible: boolean
_visible: boolean,
/**
* Function to translate i18n labels.
*/
t: Function
};
type State = {
@@ -149,12 +151,13 @@ class Labels extends AbstractLabels<Props, State> {
* @inheritdoc
*/
render() {
if (!this.props._visible) {
const { _aspectRatio, _filmstripVisible, _visible } = this.props;
if (!_visible) {
return null;
}
const wide = !isNarrowAspectRatio(this);
const { _filmstripVisible } = this.props;
const wide = _aspectRatio === ASPECT_RATIO_WIDE;
return (
<View
@@ -354,8 +357,9 @@ class Labels extends AbstractLabels<Props, State> {
function _mapStateToProps(state) {
return {
..._abstractMapStateToProps(state),
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
_visible: !shouldDisplayNotifications(state)
};
}
export default connect(_mapStateToProps)(makeAspectRatioAware(Labels));
export default connect(_mapStateToProps)(Labels);

View File

@@ -176,8 +176,6 @@ class Conference extends AbstractConference<Props, *> {
*/
render() {
const {
VIDEO_QUALITY_LABEL_DISABLED,
// XXX The character casing of the name filmStripOnly utilized by
// interfaceConfig is obsolete but legacy support is required.
filmStripOnly: filmstripOnly
@@ -187,10 +185,7 @@ class Conference extends AbstractConference<Props, *> {
_layoutClassName,
_showPrejoin
} = this.props;
const hideVideoQualityLabel
= filmstripOnly
|| VIDEO_QUALITY_LABEL_DISABLED
|| _iAmRecorder;
const hideLabels = filmstripOnly || _iAmRecorder;
return (
<div
@@ -203,7 +198,7 @@ class Conference extends AbstractConference<Props, *> {
<InviteMore />
<div id = 'videospace'>
<LargeVideo />
{ hideVideoQualityLabel
{ hideLabels
|| <Labels /> }
<Filmstrip filmstripOnly = { filmstripOnly } />
</div>

View File

@@ -9,6 +9,8 @@ import AbstractLabels, {
type Props
} from '../AbstractLabels';
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} state of {@link Labels}.
*/
@@ -39,8 +41,7 @@ class Labels extends AbstractLabels<Props, State> {
*/
static getDerivedStateFromProps(props: Props, prevState: State) {
return {
filmstripBecomingVisible: !prevState.filmstripBecomingVisible
&& props._filmstripVisible
filmstripBecomingVisible: !prevState.filmstripBecomingVisible && props._filmstripVisible
};
}
@@ -67,6 +68,7 @@ class Labels extends AbstractLabels<Props, State> {
render() {
const { _filmstripVisible } = this.props;
const { filmstripBecomingVisible } = this.state;
const { VIDEO_QUALITY_LABEL_DISABLED } = interfaceConfig;
const className = `large-video-labels ${
filmstripBecomingVisible ? 'opening' : ''} ${
_filmstripVisible ? 'with-filmstrip' : 'without-filmstrip'}`;
@@ -91,7 +93,7 @@ class Labels extends AbstractLabels<Props, State> {
this._renderTranscribingLabel()
}
{
this.props._showVideoQualityLabel
this.props._showVideoQualityLabel && !VIDEO_QUALITY_LABEL_DISABLED
&& this._renderVideoQualityLabel()
}
{

View File

@@ -5,10 +5,7 @@ import { ScrollView } from 'react-native';
import { Container, Platform } from '../../../base/react';
import { connect } from '../../../base/redux';
import {
isNarrowAspectRatio,
makeAspectRatioAware
} from '../../../base/responsive-ui';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import { isFilmstripVisible } from '../../functions';
import LocalThumbnail from './LocalThumbnail';
@@ -20,24 +17,23 @@ import styles from './styles';
*/
type Props = {
/**
* Application's aspect ratio.
*/
_aspectRatio: Symbol,
/**
* The indicator which determines whether the filmstrip is enabled.
*
* @private
*/
_enabled: boolean,
/**
* The participants in the conference.
*
* @private
*/
_participants: Array<any>,
/**
* The indicator which determines whether the filmstrip is visible.
*
* @private
*/
_visible: boolean
};
@@ -90,40 +86,36 @@ class Filmstrip extends Component<Props> {
* @returns {ReactElement}
*/
render() {
if (!this.props._enabled) {
const { _aspectRatio, _enabled, _participants, _visible } = this.props;
if (!_enabled) {
return null;
}
const isNarrowAspectRatio_ = isNarrowAspectRatio(this);
const filmstripStyle
= isNarrowAspectRatio_
? styles.filmstripNarrow
: styles.filmstripWide;
const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
const filmstripStyle = isNarrowAspectRatio ? styles.filmstripNarrow : styles.filmstripWide;
return (
<Container
style = { filmstripStyle }
visible = { this.props._visible }>
visible = { _visible }>
{
this._separateLocalThumbnail
&& !isNarrowAspectRatio_
&& !isNarrowAspectRatio
&& <LocalThumbnail />
}
<ScrollView
horizontal = { isNarrowAspectRatio_ }
horizontal = { isNarrowAspectRatio }
showsHorizontalScrollIndicator = { false }
showsVerticalScrollIndicator = { false }
style = { styles.scrollView } >
{
!this._separateLocalThumbnail
&& !isNarrowAspectRatio_
!this._separateLocalThumbnail && !isNarrowAspectRatio
&& <LocalThumbnail />
}
{
this._sort(
this.props._participants,
isNarrowAspectRatio_)
this._sort(_participants, isNarrowAspectRatio)
.map(p => (
<Thumbnail
key = { p.id }
@@ -131,14 +123,12 @@ class Filmstrip extends Component<Props> {
}
{
!this._separateLocalThumbnail
&& isNarrowAspectRatio_
!this._separateLocalThumbnail && isNarrowAspectRatio
&& <LocalThumbnail />
}
</ScrollView>
{
this._separateLocalThumbnail
&& isNarrowAspectRatio_
this._separateLocalThumbnail && isNarrowAspectRatio
&& <LocalThumbnail />
}
</Container>
@@ -150,13 +140,13 @@ class Filmstrip extends Component<Props> {
*
* @param {Participant[]} participants - The array of {@code Participant}s
* to sort in display order.
* @param {boolean} isNarrowAspectRatio_ - Indicates if the aspect ratio is
* @param {boolean} isNarrowAspectRatio - Indicates if the aspect ratio is
* wide or narrow.
* @private
* @returns {Participant[]} A new array containing the elements of the
* specified {@code participants} array sorted in display order.
*/
_sort(participants, isNarrowAspectRatio_) {
_sort(participants, isNarrowAspectRatio) {
// XXX Array.prototype.sort() is not appropriate because (1) it operates
// in place and (2) it is not necessarily stable.
@@ -164,7 +154,7 @@ class Filmstrip extends Component<Props> {
...participants
];
if (isNarrowAspectRatio_) {
if (isNarrowAspectRatio) {
// When the narrow aspect ratio is used, we want to have the remote
// participants from right to left with the newest added/joined to
// the leftmost side. The local participant is the leftmost item.
@@ -180,42 +170,18 @@ class Filmstrip extends Component<Props> {
*
* @param {Object} state - The redux state.
* @private
* @returns {{
* _participants: Participant[],
* _visible: boolean
* }}
* @returns {Props}
*/
function _mapStateToProps(state) {
const participants = state['features/base/participants'];
const { enabled } = state['features/filmstrip'];
return {
/**
* The indicator which determines whether the filmstrip is enabled.
*
* @private
* @type {boolean}
*/
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
_enabled: enabled,
/**
* The remote participants in the conference.
*
* @private
* @type {Participant[]}
*/
_participants: participants.filter(p => !p.local),
/**
* The indicator which determines whether the filmstrip is visible. The
* mobile/react-native Filmstrip is visible when there are at least 2
* participants in the conference (including the local one).
*
* @private
* @type {boolean}
*/
_visible: isFilmstripVisible(state)
};
}
export default connect(_mapStateToProps)(makeAspectRatioAware(Filmstrip));
export default connect(_mapStateToProps)(Filmstrip);

View File

@@ -13,11 +13,7 @@ import {
setMaxReceiverVideoQuality
} from '../../../base/conference';
import { connect } from '../../../base/redux';
import {
DimensionsDetector,
isNarrowAspectRatio,
makeAspectRatioAware
} from '../../../base/responsive-ui';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import Thumbnail from './Thumbnail';
import styles from './styles';
@@ -27,11 +23,26 @@ import styles from './styles';
*/
type Props = {
/**
* Application's aspect ratio.
*/
_aspectRatio: Symbol,
/**
* Application's viewport height.
*/
_height: number,
/**
* The participants in the conference.
*/
_participants: Array<Object>,
/**
* Application's viewport height.
*/
_width: number,
/**
* Invoked to update the receiver video quality.
*/
@@ -43,22 +54,6 @@ type Props = {
onClick: Function
};
/**
* The type of the React {@link Component} state of {@link TileView}.
*/
type State = {
/**
* The available width for {@link TileView} to occupy.
*/
height: number,
/**
* The available height for {@link TileView} to occupy.
*/
width: number
};
/**
* The margin for each side of the tile view. Taken away from the available
* height and width for the tile container to display in.
@@ -82,25 +77,7 @@ const TILE_ASPECT_RATIO = 1;
*
* @extends Component
*/
class TileView extends Component<Props, State> {
state = {
height: 0,
width: 0
};
/**
* Initializes a new {@code TileView} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handler so it is only bound once per instance.
this._onDimensionsChanged = this._onDimensionsChanged.bind(this);
}
class TileView extends Component<Props> {
/**
* Implements React's {@link Component#componentDidMount}.
*
@@ -126,32 +103,27 @@ class TileView extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { onClick } = this.props;
const { height, width } = this.state;
const rowElements = this._groupIntoRows(
this._renderThumbnails(), this._getColumnCount());
const { _height, _width, onClick } = this.props;
const rowElements = this._groupIntoRows(this._renderThumbnails(), this._getColumnCount());
return (
<DimensionsDetector
onDimensionsChanged = { this._onDimensionsChanged }>
<ScrollView
style = {{
...styles.tileView,
height,
width
}}>
<TouchableWithoutFeedback onPress = { onClick }>
<View
style = {{
...styles.tileViewRows,
minHeight: height,
minWidth: width
}}>
{ rowElements }
</View>
</TouchableWithoutFeedback>
</ScrollView>
</DimensionsDetector>
<ScrollView
style = {{
...styles.tileView,
height: _height,
width: _width
}}>
<TouchableWithoutFeedback onPress = { onClick }>
<View
style = {{
...styles.tileViewRows,
minHeight: _height,
minWidth: _width
}}>
{ rowElements }
</View>
</TouchableWithoutFeedback>
</ScrollView>
);
}
@@ -167,7 +139,7 @@ class TileView extends Component<Props, State> {
// For narrow view, tiles should stack on top of each other for a lonely
// call and a 1:1 call. Otherwise tiles should be grouped into rows of
// two.
if (isNarrowAspectRatio(this)) {
if (this.props._aspectRatio === ASPECT_RATIO_NARROW) {
return participantCount >= 3 ? 2 : 1;
}
@@ -209,19 +181,17 @@ class TileView extends Component<Props, State> {
* @returns {Object}
*/
_getTileDimensions() {
const { _participants } = this.props;
const { height, width } = this.state;
const { _height, _participants, _width } = this.props;
const columns = this._getColumnCount();
const participantCount = _participants.length;
const heightToUse = height - (MARGIN * 2);
const widthToUse = width - (MARGIN * 2);
const heightToUse = _height - (MARGIN * 2);
const widthToUse = _width - (MARGIN * 2);
let tileWidth;
// If there is going to be at least two rows, ensure that at least two
// rows display fully on screen.
if (participantCount / columns > 1) {
tileWidth
= Math.min(widthToUse / columns, heightToUse / 2);
tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
} else {
tileWidth = Math.min(widthToUse / columns, heightToUse);
}
@@ -247,8 +217,7 @@ class TileView extends Component<Props, State> {
for (let i = 0; i < thumbnails.length; i++) {
if (i % rowLength === 0) {
const thumbnailsInRow
= thumbnails.slice(i, i + rowLength);
const thumbnailsInRow = thumbnails.slice(i, i + rowLength);
rowElements.push(
<View
@@ -263,23 +232,6 @@ class TileView extends Component<Props, State> {
return rowElements;
}
_onDimensionsChanged: (width: number, height: number) => void;
/**
* Updates the known available state for {@link TileView} to occupy.
*
* @param {number} width - The component's current width.
* @param {number} height - The component's current height.
* @private
* @returns {void}
*/
_onDimensionsChanged(width: number, height: number) {
this.setState({
height,
width
});
}
/**
* Creates React Elements to display each participant in a thumbnail. Each
* tile will be.
@@ -326,14 +278,17 @@ class TileView extends Component<Props, State> {
*
* @param {Object} state - The redux state.
* @private
* @returns {{
* _participants: Participant[]
* }}
* @returns {Props}
*/
function _mapStateToProps(state) {
const responsiveUi = state['features/base/responsive-ui'];
return {
_participants: state['features/base/participants']
_aspectRatio: responsiveUi.aspectRatio,
_height: responsiveUi.clientHeight,
_participants: state['features/base/participants'],
_width: responsiveUi.clientWidth
};
}
export default connect(_mapStateToProps)(makeAspectRatioAware(TileView));
export default connect(_mapStateToProps)(TileView);

View File

@@ -1,11 +1,10 @@
// @flow
import React, { Component } from 'react';
import React, { PureComponent } from 'react';
import { ColorSchemeRegistry } from '../../base/color-scheme';
import { ParticipantView } from '../../base/participants';
import { connect } from '../../base/redux';
import { DimensionsDetector } from '../../base/responsive-ui';
import { StyleType } from '../../base/styles';
import { AVATAR_SIZE } from './styles';
@@ -15,6 +14,11 @@ import { AVATAR_SIZE } from './styles';
*/
type Props = {
/**
* Application's viewport height.
*/
_height: number,
/**
* The ID of the participant (to be) depicted by LargeVideo.
*
@@ -27,6 +31,11 @@ type Props = {
*/
_styles: StyleType,
/**
* Application's viewport height.
*/
_width: number,
/**
* Callback to invoke when the {@code LargeVideo} is clicked/pressed.
*/
@@ -62,50 +71,33 @@ const DEFAULT_STATE = {
*
* @extends Component
*/
class LargeVideo extends Component<Props, State> {
class LargeVideo extends PureComponent<Props, State> {
state = {
...DEFAULT_STATE
};
/** Initializes a new {@code LargeVideo} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onDimensionsChanged = this._onDimensionsChanged.bind(this);
}
_onDimensionsChanged: (width: number, height: number) => void;
/**
* Handle this component's dimension changes. In case we deem it's too
* Handles dimension changes. In case we deem it's too
* small, the connectivity indicator won't be rendered and the avatar
* will occupy the entirety of the available screen state.
*
* @param {number} width - The component's current width.
* @param {number} height - The component's current height.
* @private
* @returns {void}
* @inheritdoc
*/
_onDimensionsChanged(width: number, height: number) {
static getDerivedStateFromProps(props: Props) {
const { _height, _width } = props;
// Get the size, rounded to the nearest even number.
const size = 2 * Math.round(Math.min(height, width) / 2);
let nextState;
const size = 2 * Math.round(Math.min(_height, _width) / 2);
if (size < AVATAR_SIZE * 1.5) {
nextState = {
return {
avatarSize: size - 15, // Leave some margin.
useConnectivityInfoLabel: false
};
} else {
nextState = DEFAULT_STATE;
}
this.setState(nextState);
return DEFAULT_STATE;
}
/**
@@ -126,18 +118,15 @@ class LargeVideo extends Component<Props, State> {
} = this.props;
return (
<DimensionsDetector
onDimensionsChanged = { this._onDimensionsChanged }>
<ParticipantView
avatarSize = { avatarSize }
onPress = { onClick }
participantId = { _participantId }
style = { _styles.largeVideo }
testHintId = 'org.jitsi.meet.LargeVideo'
useConnectivityInfoLabel = { useConnectivityInfoLabel }
zOrder = { 0 }
zoomEnabled = { true } />
</DimensionsDetector>
<ParticipantView
avatarSize = { avatarSize }
onPress = { onClick }
participantId = { _participantId }
style = { _styles.largeVideo }
testHintId = 'org.jitsi.meet.LargeVideo'
useConnectivityInfoLabel = { useConnectivityInfoLabel }
zOrder = { 0 }
zoomEnabled = { true } />
);
}
}
@@ -147,15 +136,16 @@ class LargeVideo extends Component<Props, State> {
*
* @param {Object} state - Redux state.
* @private
* @returns {{
* _participantId: string,
* _styles: StyleType
* }}
* @returns {Props}
*/
function _mapStateToProps(state) {
const { clientHeight: height, clientWidth: width } = state['features/base/responsive-ui'];
return {
_height: height,
_participantId: state['features/large-video'].participantId,
_styles: ColorSchemeRegistry.get(state, 'LargeVideo')
_styles: ColorSchemeRegistry.get(state, 'LargeVideo'),
_width: width
};
}

View File

@@ -55,6 +55,11 @@ export type Props = {
*/
isDismissAllowed: boolean,
/**
* Maximum lines of the description.
*/
maxLines: ?number,
/**
* Callback invoked when the user clicks to dismiss the notification.
*/

View File

@@ -11,6 +11,13 @@ import AbstractNotification, {
import styles from './styles';
/**
* Default value for the maxLines prop.
*
* @type {number}
*/
const DEFAULT_MAX_LINES = 1;
/**
* Implements a React {@link Component} to display a notification.
*
@@ -24,9 +31,7 @@ class Notification extends AbstractNotification<Props> {
* @returns {ReactElement}
*/
render() {
const {
isDismissAllowed
} = this.props;
const { isDismissAllowed } = this.props;
return (
<View
@@ -61,7 +66,7 @@ class Notification extends AbstractNotification<Props> {
* @private
*/
_renderContent() {
const { t, title, titleArguments, titleKey } = this.props;
const { maxLines = DEFAULT_MAX_LINES, t, title, titleArguments, titleKey } = this.props;
const titleText = title || (titleKey && t(titleKey, titleArguments));
const description = this._getDescription();
@@ -69,7 +74,7 @@ class Notification extends AbstractNotification<Props> {
return description.map((line, index) => (
<Text
key = { index }
numberOfLines = { 1 }
numberOfLines = { maxLines }
style = { styles.contentText }>
{ line }
</Text>
@@ -78,7 +83,7 @@ class Notification extends AbstractNotification<Props> {
return (
<Text
numberOfLines = { 1 }
numberOfLines = { maxLines }
style = { styles.contentText } >
{ titleText }
</Text>

View File

@@ -40,7 +40,7 @@ export default {
notification: {
backgroundColor: '#768898',
flexDirection: 'row',
height: 48,
minHeight: 48,
marginTop: 0.5 * BoxModel.margin
},

View File

@@ -0,0 +1,42 @@
// @flow
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { showNotification } from '../notifications';
export * from './actions.any';
/**
* Signals that a started recording notification should be shown on the
* screen for a given period.
*
* @param {string} streamType - The type of the stream ({@code file} or
* {@code stream}).
* @returns {showNotification}
*/
export function showRecordingLimitNotification(streamType: string) {
return (dispatch: Function, getState: Function) => {
const isLiveStreaming = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
let descriptionKey, titleKey;
if (isLiveStreaming) {
descriptionKey = 'liveStreaming.limitNotificationDescriptionNative';
titleKey = 'dialog.liveStreaming';
} else {
descriptionKey = 'recording.limitNotificationDescriptionNative';
titleKey = 'dialog.recording';
}
const { recordingLimit = {} } = getState()['features/base/config'];
const { limit, appName } = recordingLimit;
return dispatch(showNotification({
descriptionArguments: {
limit,
app: appName
},
descriptionKey,
titleKey,
maxLines: 2
}, 10000));
};
}

View File

@@ -0,0 +1,27 @@
// @flow
import React from 'react';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { showNotification } from '../notifications';
import { RecordingLimitNotificationDescription } from './components';
export * from './actions.any';
/**
* Signals that a started recording notification should be shown on the
* screen for a given period.
*
* @param {string} streamType - The type of the stream ({@code file} or
* {@code stream}).
* @returns {showNotification}
*/
export function showRecordingLimitNotification(streamType: string) {
const isLiveStreaming = streamType === JitsiMeetJS.constants.recording.mode.STREAM;
return showNotification({
description: <RecordingLimitNotificationDescription isLiveStreaming = { isLiveStreaming } />,
titleKey: isLiveStreaming ? 'dialog.liveStreaming' : 'dialog.recording'
}, 10000);
}

View File

@@ -1,6 +1,7 @@
// @flow
import { openDialog } from '../../../base/dialog';
import { IconLiveStreaming } from '../../../base/icons';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import { getLocalParticipant } from '../../../base/participants';
import {
@@ -25,6 +26,16 @@ export type Props = AbstractButtonProps & {
*/
_isLiveStreamRunning: boolean,
/**
* True if the button needs to be disabled.
*/
_disabled: Boolean,
/**
* The tooltip to display when hovering over the button.
*/
_tooltip: ?String,
/**
* The redux {@code dispatch} function.
*/
@@ -39,12 +50,22 @@ export type Props = AbstractButtonProps & {
/**
* An abstract class of a button for starting and stopping live streaming.
*/
export default class AbstractLiveStreamButton<P: Props>
extends AbstractButton<P, *> {
export default class AbstractLiveStreamButton<P: Props> extends AbstractButton<P, *> {
accessibilityLabel = 'dialog.accessibilityLabel.liveStreaming';
icon = IconLiveStreaming;
label = 'dialog.startLiveStreaming';
toggledLabel = 'dialog.stopLiveStreaming';
/**
* Returns the tooltip that should be displayed when the button is disabled.
*
* @private
* @returns {string}
*/
_getTooltip() {
return this.props._tooltip || '';
}
/**
* Handles clicking / pressing the button.
*
@@ -60,6 +81,16 @@ export default class AbstractLiveStreamButton<P: Props>
));
}
/**
* Returns a boolean value indicating if this button is disabled or not.
*
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._disabled;
}
/**
* Indicates whether this button is in toggled state or not.
*
@@ -80,6 +111,7 @@ export default class AbstractLiveStreamButton<P: Props>
* @param {Props} ownProps - The own props of the Component.
* @private
* @returns {{
* _disabled: boolean,
* _isLiveStreamRunning: boolean,
* visible: boolean
* }}
@@ -87,9 +119,10 @@ export default class AbstractLiveStreamButton<P: Props>
export function _mapStateToProps(state: Object, ownProps: Props) {
let { visible } = ownProps;
// a button can be disabled/enabled only if enableFeaturesBasedOnToken
// is on
let disabledByFeatures;
// A button can be disabled/enabled only if enableFeaturesBasedOnToken
// is on or if the recording is running.
let _disabled;
let _tooltip = '';
if (typeof visible === 'undefined') {
// If the containing component provides the visible prop, that is one
@@ -105,14 +138,33 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
if (enableFeaturesBasedOnToken) {
visible = visible && String(features.livestreaming) === 'true';
disabledByFeatures = String(features.livestreaming) === 'disabled';
_disabled = String(features.livestreaming) === 'disabled';
if (!visible && !_disabled) {
_disabled = true;
visible = true;
// button and tooltip
if (state['features/base/jwt'].isGuest) {
_tooltip = 'dialog.liveStreamingDisabledForGuestTooltip';
} else {
_tooltip = 'dialog.liveStreamingDisabledTooltip';
}
}
}
}
// disable the button if the recording is running.
if (getActiveSession(state, JitsiRecordingConstants.mode.FILE)) {
_disabled = true;
_tooltip = 'dialog.liveStreamingDisabledBecauseOfActiveRecordingTooltip';
}
return {
_disabled,
_isLiveStreamRunning: Boolean(
getActiveSession(state, JitsiRecordingConstants.mode.STREAM)),
disabledByFeatures,
_tooltip,
visible
};
}

View File

@@ -2,19 +2,8 @@
import { LIVE_STREAMING_ENABLED, getFeatureFlag } from '../../../../base/flags';
import { translate } from '../../../../base/i18n';
import { IconLiveStreaming } from '../../../../base/icons';
import { connect } from '../../../../base/redux';
import AbstractLiveStreamButton, {
_mapStateToProps as _abstractMapStateToProps,
type Props
} from '../AbstractLiveStreamButton';
/**
* An implementation of a button for starting and stopping live streaming.
*/
class LiveStreamButton extends AbstractLiveStreamButton<Props> {
icon = IconLiveStreaming;
}
import AbstractLiveStreamButton, { _mapStateToProps as _abstractMapStateToProps } from '../AbstractLiveStreamButton';
/**
* Maps (parts of) the redux state to the associated props for this component.
@@ -35,4 +24,4 @@ export function mapStateToProps(state: Object, ownProps: Object) {
};
}
export default translate(connect(mapStateToProps)(LiveStreamButton));
export default translate(connect(mapStateToProps)(AbstractLiveStreamButton));

View File

@@ -1,61 +1,14 @@
// @flow
import { translate } from '../../../../base/i18n';
import { IconLiveStreaming } from '../../../../base/icons';
import { connect } from '../../../../base/redux';
import AbstractLiveStreamButton, {
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
type Props
} from '../AbstractLiveStreamButton';
declare var interfaceConfig: Object;
type Props = AbstractProps & {
/**
* True if the button should be disabled, false otherwise.
*
* NOTE: On web, if the feature is not disabled on purpose, then we still
* show the button but disabled and with a tooltip rendered on it,
* explaining why it's not available.
*/
_disabled: boolean,
/**
* Tooltip for the button when it's disabled in a certain way.
*/
_liveStreamDisabledTooltipKey: ?string
}
/**
* An implementation of a button for starting and stopping live streaming.
*/
class LiveStreamButton extends AbstractLiveStreamButton<Props> {
icon = IconLiveStreaming;
/**
* Returns the tooltip that should be displayed when the button is disabled.
*
* @private
* @returns {string}
*/
_getTooltip() {
return this.props._liveStreamDisabledTooltipKey || '';
}
/**
* Helper function to be implemented by subclasses, which must return a
* boolean value indicating if this button is disabled or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._disabled;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code LiveStreamButton} component.
@@ -74,36 +27,14 @@ function _mapStateToProps(state: Object, ownProps: Props) {
const abstractProps = _abstractMapStateToProps(state, ownProps);
let { visible } = ownProps;
const _disabledByFeatures = abstractProps.disabledByFeatures;
let _disabled = false;
let _liveStreamDisabledTooltipKey;
if (!abstractProps.visible
&& _disabledByFeatures !== undefined && !_disabledByFeatures) {
_disabled = true;
// button and tooltip
if (state['features/base/jwt'].isGuest) {
_liveStreamDisabledTooltipKey
= 'dialog.liveStreamingDisabledForGuestTooltip';
} else {
_liveStreamDisabledTooltipKey
= 'dialog.liveStreamingDisabledTooltip';
}
}
if (typeof visible === 'undefined') {
visible = interfaceConfig.TOOLBAR_BUTTONS.includes('livestreaming')
&& (abstractProps.visible
|| Boolean(_liveStreamDisabledTooltipKey));
visible = interfaceConfig.TOOLBAR_BUTTONS.includes('livestreaming') && abstractProps.visible;
}
return {
...abstractProps,
_disabled,
_liveStreamDisabledTooltipKey,
visible
};
}
export default translate(connect(_mapStateToProps)(LiveStreamButton));
export default translate(connect(_mapStateToProps)(AbstractLiveStreamButton));

View File

@@ -5,6 +5,7 @@ import {
sendAnalytics
} from '../../../analytics';
import { openDialog } from '../../../base/dialog';
import { IconToggleRecording } from '../../../base/icons';
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
import {
getLocalParticipant,
@@ -24,11 +25,21 @@ import { StartRecordingDialog, StopRecordingDialog } from './_';
*/
export type Props = AbstractButtonProps & {
/**
* True if the button needs to be disabled.
*/
_disabled: Boolean,
/**
* True if there is a running active recording, false otherwise.
*/
_isRecordingRunning: boolean,
/**
* The tooltip to display when hovering over the button.
*/
_tooltip: ?String,
/**
* The redux {@code dispatch} function.
*/
@@ -43,12 +54,22 @@ export type Props = AbstractButtonProps & {
/**
* An abstract implementation of a button for starting and stopping recording.
*/
export default class AbstractRecordButton<P: Props>
extends AbstractButton<P, *> {
export default class AbstractRecordButton<P: Props> extends AbstractButton<P, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.recording';
icon = IconToggleRecording;
label = 'dialog.startRecording';
toggledLabel = 'dialog.stopRecording';
/**
* Returns the tooltip that should be displayed when the button is disabled.
*
* @private
* @returns {string}
*/
_getTooltip() {
return this.props._tooltip || '';
}
/**
* Handles clicking / pressing the button.
*
@@ -80,7 +101,7 @@ export default class AbstractRecordButton<P: Props>
* @returns {boolean}
*/
_isDisabled() {
return false;
return this.props._disabled;
}
/**
@@ -103,17 +124,18 @@ export default class AbstractRecordButton<P: Props>
* @param {Props} ownProps - The own props of the Component.
* @private
* @returns {{
* _disabled: boolean,
* _isRecordingRunning: boolean,
* disabledByFeatures: boolean,
* visible: boolean
* }}
*/
export function _mapStateToProps(state: Object, ownProps: Props): Object {
let { visible } = ownProps;
// a button can be disabled/enabled only if enableFeaturesBasedOnToken
// is on
let disabledByFeatures;
// a button can be disabled/enabled if enableFeaturesBasedOnToken
// is on or if the livestreaming is running.
let _disabled;
let _tooltip = '';
if (typeof visible === 'undefined') {
// If the containing component provides the visible prop, that is one
@@ -126,19 +148,35 @@ export function _mapStateToProps(state: Object, ownProps: Props): Object {
} = state['features/base/config'];
const { features = {} } = getLocalParticipant(state);
visible = isModerator
&& fileRecordingsEnabled;
visible = isModerator && fileRecordingsEnabled;
if (enableFeaturesBasedOnToken) {
visible = visible && String(features.recording) === 'true';
disabledByFeatures = String(features.recording) === 'disabled';
_disabled = String(features.recording) === 'disabled';
if (!visible && !_disabled) {
_disabled = true;
visible = true;
// button and tooltip
if (state['features/base/jwt'].isGuest) {
_tooltip = 'dialog.recordingDisabledForGuestTooltip';
} else {
_tooltip = 'dialog.recordingDisabledTooltip';
}
}
}
}
// disable the button if the livestreaming is running.
if (getActiveSession(state, JitsiRecordingConstants.mode.STREAM)) {
_disabled = true;
_tooltip = 'dialog.recordingDisabledBecauseOfActiveLiveStreamingTooltip';
}
return {
_isRecordingRunning:
Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE)),
disabledByFeatures,
_disabled,
_isRecordingRunning: Boolean(getActiveSession(state, JitsiRecordingConstants.mode.FILE)),
_tooltip,
visible
};
}

View File

@@ -4,19 +4,8 @@ import { Platform } from 'react-native';
import { IOS_RECORDING_ENABLED, RECORDING_ENABLED, getFeatureFlag } from '../../../../base/flags';
import { translate } from '../../../../base/i18n';
import { IconToggleRecording } from '../../../../base/icons';
import { connect } from '../../../../base/redux';
import AbstractRecordButton, {
_mapStateToProps as _abstractMapStateToProps,
type Props
} from '../AbstractRecordButton';
/**
* An implementation of a button for starting and stopping recording.
*/
class RecordButton extends AbstractRecordButton<Props> {
icon = IconToggleRecording;
}
import AbstractRecordButton, { _mapStateToProps as _abstractMapStateToProps } from '../AbstractRecordButton';
/**
* Maps (parts of) the redux state to the associated props for this component.
@@ -38,4 +27,4 @@ export function mapStateToProps(state: Object, ownProps: Object) {
};
}
export default translate(connect(mapStateToProps)(RecordButton));
export default translate(connect(mapStateToProps)(AbstractRecordButton));

View File

@@ -1,61 +1,14 @@
// @flow
import { translate } from '../../../../base/i18n';
import { IconToggleRecording } from '../../../../base/icons';
import { connect } from '../../../../base/redux';
import AbstractRecordButton, {
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
type Props
} from '../AbstractRecordButton';
declare var interfaceConfig: Object;
type Props = AbstractProps & {
/**
* True if the button should be disabled, false otherwise.
*
* NOTE: On web, if the feature is not disabled on purpose, then we still
* show the button but disabled and with a tooltip rendered on it,
* explaining why it's not available.
*/
_disabled: boolean,
/**
* Tooltip for the button when it's disabled in a certain way.
*/
_fileRecordingsDisabledTooltipKey: ?string
}
/**
* An implementation of a button for starting and stopping recording.
*/
class RecordButton extends AbstractRecordButton<Props> {
icon = IconToggleRecording;
/**
* Returns the tooltip that should be displayed when the button is disabled.
*
* @private
* @returns {string}
*/
_getTooltip() {
return this.tooltip || '';
}
/**
* Helper function to be implemented by subclasses, which must return a
* boolean value indicating if this button is disabled or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isDisabled() {
return this.props._disabled;
}
}
/**
* Maps (parts of) the redux state to the associated props for the
* {@code RecordButton} component.
@@ -74,36 +27,14 @@ export function _mapStateToProps(state: Object, ownProps: Props): Object {
const abstractProps = _abstractMapStateToProps(state, ownProps);
let { visible } = ownProps;
const _disabledByFeatures = abstractProps.disabledByFeatures;
let _disabled = false;
let _fileRecordingsDisabledTooltipKey;
if (!abstractProps.visible
&& _disabledByFeatures !== undefined && !_disabledByFeatures) {
_disabled = true;
// button and tooltip
if (state['features/base/jwt'].isGuest) {
_fileRecordingsDisabledTooltipKey
= 'dialog.recordingDisabledForGuestTooltip';
} else {
_fileRecordingsDisabledTooltipKey
= 'dialog.recordingDisabledTooltip';
}
}
if (typeof visible === 'undefined') {
visible = interfaceConfig.TOOLBAR_BUTTONS.includes('recording')
&& (abstractProps.visible
|| Boolean(_fileRecordingsDisabledTooltipKey));
visible = interfaceConfig.TOOLBAR_BUTTONS.includes('recording') && abstractProps.visible;
}
return {
...abstractProps,
visible,
_disabled,
_fileRecordingsDisabledTooltipKey
visible
};
}
export default translate(connect(_mapStateToProps)(RecordButton));
export default translate(connect(_mapStateToProps)(AbstractRecordButton));

View File

@@ -0,0 +1,81 @@
// @flow
import React from 'react';
import { translate, translateToHTML } from '../../../base/i18n';
import { connect } from '../../../base/redux';
/**
* The type of the React {@code Component} props of {@link RecordingLimitNotificationDescription}.
*/
type Props = {
/**
* The limit of time in minutes for the recording.
*/
_limit: number,
/**
* The name of the app with unlimited recordings.
*/
_appName: string,
/**
* The URL to the app with unlimited recordings.
*/
_appURL: string,
/**
* True if the notification is related to the livestreaming and false if not.
*/
isLiveStreaming: Boolean,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* A component that renders the description of the notification for the recording initiator.
*
* @param {Props} props - The props of the component.
* @returns {Component}
*/
function RecordingLimitNotificationDescription(props: Props) {
const { _limit, _appName, _appURL, isLiveStreaming, t } = props;
return (
<span>
{
translateToHTML(
t,
`${isLiveStreaming ? 'liveStreaming' : 'recording'}.limitNotificationDescriptionWeb`, {
limit: _limit,
app: _appName,
url: _appURL
})
}
</span>
);
}
/**
* Maps part of the Redix state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state): $Shape<Props> {
const { recordingLimit = {} } = state['features/base/config'];
const { limit: _limit, appName: _appName, appURL: _appURL } = recordingLimit;
return {
_limit,
_appName,
_appURL
};
}
export default translate(connect(_mapStateToProps)(RecordingLimitNotificationDescription));

View File

@@ -1,3 +1,4 @@
// @flow
export { default as RecordingLabel } from './RecordingLabel';
export { default as RecordingLimitNotificationDescription } from './RecordingLimitNotificationDescription';

View File

@@ -26,6 +26,7 @@ import {
hidePendingRecordingNotification,
showPendingRecordingNotification,
showRecordingError,
showRecordingLimitNotification,
showStartedRecordingNotification,
showStoppedRecordingNotification,
updateRecordingSessionData
@@ -44,6 +45,8 @@ import {
RECORDING_ON_SOUND_FILE
} from './sounds';
declare var interfaceConfig: Object;
/**
* StateListenerRegistry provides a reliable way to detect the leaving of a
* conference, where we need to clean up the recording sessions.
@@ -131,7 +134,8 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
const {
iAmRecorder,
iAmSipGateway,
disableRecordAudioNotification
disableRecordAudioNotification,
recordingLimit
} = getState()['features/base/config'];
if (iAmRecorder && !iAmSipGateway) {
@@ -151,9 +155,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
if (updatedSessionData.status === ON
&& (!oldSessionData || oldSessionData.status !== ON)) {
const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
if (initiator) {
const initiatorName = initiator && getParticipantDisplayName(getState, initiator.getId());
initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
} else if (typeof recordingLimit === 'object') {
// Show notification with additional information to the initiator.
dispatch(showRecordingLimitNotification(mode));
}
initiatorName && dispatch(showStartedRecordingNotification(mode, initiatorName));
sendAnalytics(createRecordingEvent('start', mode));
if (disableRecordAudioNotification) {

View File

@@ -83,8 +83,9 @@ export class AbstractClosedCaptionButton
*/
export function _abstractMapStateToProps(state: Object, ownProps: Object) {
const { _requestingSubtitles } = state['features/subtitles'];
const { isGuest = true } = state['features/base/jwt'];
const { transcribingEnabled } = state['features/base/config'];
const { visible = Boolean(transcribingEnabled) } = ownProps;
const { visible = Boolean(transcribingEnabled && !isGuest) } = ownProps;
return {
_requestingSubtitles,

View File

@@ -32,7 +32,8 @@ class ClosedCaptionButton
*/
export function mapStateToProps(state: Object, ownProps: Object) {
const { transcribingEnabled } = state['features/base/config'];
const enabled = getFeatureFlag(state, CLOSE_CAPTIONS_ENABLED, true) && transcribingEnabled;
const { isGuest = true } = state['features/base/jwt'];
const enabled = getFeatureFlag(state, CLOSE_CAPTIONS_ENABLED, true) && transcribingEnabled && !isGuest;
const { visible = enabled } = ownProps;
return {

View File

@@ -20,6 +20,11 @@ type Props = {
*/
_calendarEnabled: boolean,
/**
* Whether the insecure room name functionality is enabled or not.
*/
_enableInsecureRoomNameWarning: boolean,
/**
* Whether the recent list is enabled
*/
@@ -214,7 +219,7 @@ export class AbstractWelcomePage extends Component<Props, *> {
_onRoomChange(value: string) {
this.setState({
room: value,
insecureRoomName: value && isInsecureRoomName(value)
insecureRoomName: this.props._enableInsecureRoomNameWarning && value && isInsecureRoomName(value)
});
}
@@ -226,7 +231,7 @@ export class AbstractWelcomePage extends Component<Props, *> {
* @returns {ReactElement}
*/
_renderInsecureRoomNameWarning() {
if (this.state.insecureRoomName) {
if (this.props._enableInsecureRoomNameWarning && this.state.insecureRoomName) {
return this._doRenderInsecureRoomNameWarning();
}
@@ -273,6 +278,7 @@ export class AbstractWelcomePage extends Component<Props, *> {
export function _mapStateToProps(state: Object) {
return {
_calendarEnabled: isCalendarEnabled(state),
_enableInsecureRoomNameWarning: state['features/base/config'].enableInsecureRoomNameWarning || false,
_recentListEnabled: isRecentListEnabled(),
_room: state['features/base/conference'].room,
_settings: state['features/base/settings']

View File

@@ -8,7 +8,7 @@
-- lobby_muc = "lobby.jitmeet.example.com"
-- main_muc = "conference.jitmeet.example.com"
--
-- Component "lobbyrooms.damencho.jitsi.net" "muc"
-- Component "lobby.jitmeet.example.com" "muc"
-- storage = "memory"
-- muc_room_cache_size = 1000
-- restrict_room_creation = true
@@ -42,6 +42,8 @@ if lobby_muc_component_config == nil then
return ;
end
local whitelist = module:get_option_set("muc_lobby_whitelist", {});
local lobby_muc_service;
local main_muc_service;
@@ -165,51 +167,48 @@ end);
process_host_module(main_muc_component_config, function(host_module, host)
main_muc_service = prosody.hosts[host].modules.muc;
-- adds new field to the form so moderators can use it to set shared password
host_module:hook('muc-config-form', function(event)
table.insert(event.form, {
name = 'muc#roomconfig_lobbypassword';
type = 'text-private';
label = 'Shared Password';
value = '';
});
end, 90-4);
-- hooks when lobby is enabled to create its room, only done here or by admin
host_module:hook('muc-config-submitted', function(event)
local room = event.room;
local members_only = event.fields['muc#roomconfig_membersonly'] and true or nil;
if members_only then
local node = jid_split(event.room.jid);
local node = jid_split(room.jid);
local lobby_room_jid = node .. '@' .. lobby_muc_component_config;
if not lobby_muc_service.get_room_from_jid(lobby_room_jid) then
local new_room = lobby_muc_service.create_room(lobby_room_jid);
new_room.main_room = event.room;
event.room._data.lobbyroom = lobby_room_jid;
new_room.main_room = room;
room._data.lobbyroom = new_room;
event.status_codes["104"] = true;
local lobby_password = event.fields['muc#roomconfig_lobbypassword'];
if lobby_password then
new_room.main_room.lobby_password = lobby_password;
end
end
elseif room._data.lobbyroom then
room._data.lobbyroom:destroy(room.jid, 'Lobby room closed.');
room._data.lobbyroom = nil;
end
end);
host_module:hook("muc-room-destroyed",function(event)
local room = event.room;
if room._data.lobbyroom then
room._data.lobbyroom:destroy(nil, 'Lobby room closed.');
room._data.lobbyroom = nil;
end
end);
host_module:hook("muc-disco#info", function (event)
if (event.room._data.lobbyroom) then
local room = event.room;
if (room._data.lobbyroom and room:get_members_only()) then
table.insert(event.form, {
name = "muc#roominfo_lobbyroom";
label = "Lobby room jid";
value = "";
});
event.formdata["muc#roominfo_lobbyroom"] = event.room._data.lobbyroom;
event.formdata["muc#roominfo_lobbyroom"] = room._data.lobbyroom.jid;
end
end);
host_module:hook('muc-occupant-pre-join', function (event)
local room, stanza = event.room, event.stanza;
if is_healthcheck_room(room.jid) then
if is_healthcheck_room(room.jid) or not room:get_members_only() then
return;
end
@@ -218,28 +217,42 @@ process_host_module(main_muc_component_config, function(host_module, host)
return;
end
local password = join:get_child_text("lobbySharedPassword");
if password and event.room.lobby_password and password == room.lobby_password then
local invitee = event.stanza.attr.from;
local invitee = event.stanza.attr.from;
local invitee_bare_jid = jid_bare(invitee);
local _, invitee_domain = jid_split(invitee);
local whitelistJoin = false;
-- whitelist participants
if whitelist:contains(invitee_domain) or whitelist:contains(invitee_bare_jid) then
whitelistJoin = true;
end
local password = join:get_child_text('password', MUC_NS);
if password and room:get_password() and password == room:get_password() then
whitelistJoin = true;
end
if whitelistJoin then
local affiliation = room:get_affiliation(invitee);
if not affiliation or affiliation == 0 then
event.occupant.role = 'participant';
room:set_affiliation(true, jid_bare(invitee), "member");
room:set_affiliation(true, invitee_bare_jid, "member");
room:save();
return;
end
end
-- we want to add the custom lobbyroom field to fill in the lobby room jid
elseif room._data.members_only then
local invitee = event.stanza.attr.from;
local affiliation = room:get_affiliation(invitee);
if not affiliation or affiliation == 'none' then
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);
event.origin.send(reply:tag('x', {xmlns = MUC_NS}));
return true;
end
local invitee = event.stanza.attr.from;
local affiliation = room:get_affiliation(invitee);
if not affiliation or affiliation == 'none' then
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);
event.origin.send(reply:tag('x', {xmlns = MUC_NS}));
return true;
end
end, -4); -- the default hook on members_only module is on -5
end);

View File

@@ -1,5 +1,6 @@
/* global __dirname */
const CircularDependencyPlugin = require('circular-dependency-plugin');
const process = require('process');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
@@ -11,6 +12,7 @@ const devServerProxyTarget
= process.env.WEBPACK_DEV_SERVER_PROXY_TARGET || 'https://alpha.jitsi.net';
const analyzeBundle = process.argv.indexOf('--analyze-bundle') !== -1;
const detectCircularDeps = process.argv.indexOf('--detect-circular-deps') !== -1;
const minimize
= process.argv.indexOf('-p') !== -1
@@ -155,6 +157,12 @@ const config = {
&& new BundleAnalyzerPlugin({
analyzerMode: 'disabled',
generateStatsFile: true
}),
detectCircularDeps
&& new CircularDependencyPlugin({
allowAsyncCycles: false,
exclude: /node_modules/,
failOnError: false
})
].filter(Boolean),
resolve: {