mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-11 01:00:20 +00:00
Compare commits
20 Commits
android-sd
...
saghul-pat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bf82b573c | ||
|
|
b4b4339a1a | ||
|
|
6773aed67f | ||
|
|
d740752522 | ||
|
|
d93b219c7f | ||
|
|
10cd150a07 | ||
|
|
a31f3c0c76 | ||
|
|
af39186a5f | ||
|
|
d4d1d0aa70 | ||
|
|
3a88f4939c | ||
|
|
fe221fe4be | ||
|
|
1caaa47f5e | ||
|
|
a2c4d17e4d | ||
|
|
ce1de9e1e7 | ||
|
|
3e7abf3da0 | ||
|
|
8b4f1789a6 | ||
|
|
444e2b90df | ||
|
|
7de88995a5 | ||
|
|
f0c6e934ce | ||
|
|
78b01d2c97 |
@@ -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.
|
||||
|
||||
22
config.js
22
config.js
@@ -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,
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
|
||||
@@ -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ă"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
2
modules/API/external/external_api.js
vendored
2
modules/API/external/external_api.js
vendored
@@ -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',
|
||||
|
||||
@@ -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
719
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
export * from './AspectRatioAware';
|
||||
export { default as DimensionsDetector } from './DimensionsDetector';
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -40,7 +40,7 @@ export default {
|
||||
notification: {
|
||||
backgroundColor: '#768898',
|
||||
flexDirection: 'row',
|
||||
height: 48,
|
||||
minHeight: 48,
|
||||
marginTop: 0.5 * BoxModel.margin
|
||||
},
|
||||
|
||||
|
||||
42
react/features/recording/actions.native.js
Normal file
42
react/features/recording/actions.native.js
Normal 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));
|
||||
};
|
||||
}
|
||||
27
react/features/recording/actions.web.js
Normal file
27
react/features/recording/actions.web.js
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
@@ -1,3 +1,4 @@
|
||||
// @flow
|
||||
|
||||
export { default as RecordingLabel } from './RecordingLabel';
|
||||
export { default as RecordingLimitNotificationDescription } from './RecordingLimitNotificationDescription';
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user