Compare commits

..

29 Commits

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

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

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

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

We re-use room lock for lobby password.

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

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

* Adds destroy reason.

* Clears lobby room instance on destroy.

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

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

* Sends main room jid on lobby destroy.

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

* fix: Fixes using is_healthcheck_room.

* squash: Enables lobby rooms feature by default.

* chore(deps): Update lib-jitsi-meet, to enable lobby rooms.
2020-05-27 18:01:41 -05:00
Saúl Ibarra Corretgé
bf60be1654 style: fixup needlessly wrapped line 2020-05-27 16:45:11 +02:00
Saúl Ibarra Corretgé
5202a7e5b8 room-lock: use the proper text for the room lock prompt 2020-05-27 16:45:11 +02:00
Saúl Ibarra Corretgé
2af0c0ba17 rn: bump SDK version to 2.9.0 2020-05-27 15:35:58 +02:00
Saúl Ibarra Corretgé
fbb6486b5f deps: update react-native-watch-connectivity
It's back in active development and fixes a warning in iOS.
2020-05-27 15:35:58 +02:00
Saúl Ibarra Corretgé
a113151563 deps: update react-native-calendar-events
Rebase our patches (in PR) on top of latest master.

Sshould fix a crash when requesting permisssions.
2020-05-27 15:35:58 +02:00
Raider700
470fda3467 lang: add missing fields to German translation 2020-05-27 11:48:51 +02:00
Saúl Ibarra Corretgé
edea6316ab Update config.js 2020-05-27 08:43:48 +02:00
Saúl Ibarra Corretgé
adac9ee5f8 config: don't enable H.264 by default on P2P
We are not actively testing it and it currently crashes Chrome 83+ when insertable streams are used.
2020-05-27 08:43:48 +02:00
Saúl Ibarra Corretgé
af8bd876e6 deps: run npm audit fix
Skipped webpack, requires further investigation.
2020-05-27 00:16:20 +02:00
56 changed files with 2657 additions and 2160 deletions

View File

@@ -21,4 +21,4 @@ android.useAndroidX=true
android.enableJetifier=true
appVersion=20.3.0
sdkVersion=2.8.2
sdkVersion=2.9.0

View File

@@ -22,7 +22,6 @@ import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import com.calendarevents.CalendarEventsPackage;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactContext;
@@ -167,13 +166,7 @@ public class JitsiMeetActivityDelegate {
}
public static void onRequestPermissionsResult(
final int requestCode,
final String[] permissions,
final int[] grantResults) {
CalendarEventsPackage.onRequestPermissionsResult(
requestCode,
permissions,
grantResults);
final int requestCode, final String[] permissions, final int[] grantResults) {
permissionsCallback = new Callback() {
@Override
public void invoke(Object... args) {

View File

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

View File

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

View File

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

View File

@@ -287,7 +287,7 @@ PODS:
- React-jsinspector (0.61.5-jitsi.1)
- react-native-background-timer (2.1.1):
- React
- react-native-calendar-events (1.7.3):
- react-native-calendar-events (2.0.0):
- React
- react-native-keep-awake (4.0.0):
- React
@@ -365,7 +365,7 @@ PODS:
- React
- RNSVG (9.7.1):
- React
- RNWatch (0.2.0):
- RNWatch (0.4.3):
- React
- Yoga (1.14.0)
@@ -566,7 +566,7 @@ SPEC CHECKSUMS:
React-jsiexecutor: de1c37cf59ae9adcbf2be82eea0e090dc3f3205e
React-jsinspector: b76c4e84a7833bb4c90549d59ed53ec299ff912b
react-native-background-timer: 0d34748e53a972507c66963490c775321a88f6f2
react-native-calendar-events: 2fe35a9294af05de0ed819d3a1b5dac048d2c010
react-native-calendar-events: 1442fad71a00388f933cfa25512588fec300fcf8
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
react-native-webrtc: 86d841823e66d68cc1f86712db1c2956056bf0c2
@@ -586,7 +586,7 @@ SPEC CHECKSUMS:
RNGoogleSignin: 39336070b35fc4cea6a98cf111e00480317be0ae
RNSound: c980916b596cc15c8dcd2f6ecd3b13c4881dbe20
RNSVG: aac12785382e8fd4f28d072fe640612e34914631
RNWatch: 09738b339eceb66e4d80a2371633ca5fb380fa42
RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
Yoga: 7b4209fda2441f99d54dd6cf4c82b094409bb68f
PODFILE CHECKSUM: 082858daebbe170e7a490de433e7f2a99e0c3701

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.8.1</string>
<string>2.9.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

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

View File

@@ -1,11 +1,20 @@
{
"addPeople": {
"add": "Einladen",
"addContacts": "Laden Sie Ihre Kontakte ein",
"copyInvite": "Sitzungseinladung kopieren",
"copyLink": "Meeting-Link kopieren",
"copyStream": "Live-Streaming-Link kopieren",
"countryNotSupported": "Wir unterstützen dieses Land noch nicht.",
"countryReminder": "Telefonnummer nicht in den USA? Bitte sicherstellen, dass die Telefonnummer mit dem Ländercode beginnt.",
"defaultEmail": "Ihre Standard-E-Mail",
"disabled": "Sie können keine Teilnehmer einladen.",
"failedToAdd": "Fehler beim Hinzufügen von Teilnehmern",
"footerText": "Abgehender Ruf ist deaktiviert.",
"inviteMoreHeader": "Sie sind alleine in der Sitzung",
"inviteMoreMailSubject": "An {{appName}} Meeting teilnehmen",
"inviteMorePrompt": "Mehr Leute einladen",
"linkCopied": "Link in die Zwischenablage kopiert",
"loading": "Suche nach Teilnehmern und Telefonnummern",
"loadingNumber": "Telefonnummer wird überprüft",
"loadingPeople": "Suche nach einzuladenden Teilnehmern",
@@ -14,6 +23,9 @@
"searchNumbers": "Telefonnummern hinzufügen",
"searchPeople": "Nach Teilnehmern suchen",
"searchPeopleAndNumbers": "Nach Teilnehmen suchen oder deren Telefonnummern hinzufügen",
"shareInvite": "Einladung zur Versammlung teilen",
"shareLink": "Teilen Sie den Meeting-Link, um andere einzuladen",
"shareStream": "Den Live-Streaming-Link freigeben",
"telephone": "Telefon: {{number}}",
"title": "Teilnehmer zu dieser Konferenz einladen"
},
@@ -518,6 +530,11 @@
"sectionList": {
"pullToRefresh": "Ziehen, um zu aktualisieren"
},
"security": {
"about": "Sie können einen Passwort zu Ihrer Sitzung hinzufügen. Die Teilnehmer müssen dieses ebenfalls eingeben, bevor sie an der Sitzung teilnehmen dürfen",
"insecureRoomNameWarning": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten",
"securityOptions": "Sicherheitsoptionen"
},
"settings": {
"calendar": {
"about": "Die Kalenderintegration von {{appName}} wird verwendet, um ein sicheres Zugreifen auf Ihren Kalender und Auslesen der bevorstehenden Termine zu ermöglichen.",
@@ -612,6 +629,7 @@
"raiseHand": "„Melden“ ein-/ausschalten",
"recording": "Aufzeichnung ein-/ausschalten",
"remoteMute": "Teilnehmer stummschalten",
"security": "Sicherheitsoptionen",
"Settings": "Einstellungen ein-/ausschalten",
"sharedvideo": "YouTube-Videofreigabe ein-/ausschalten",
"shareRoom": "Person einladen",
@@ -663,6 +681,7 @@
"profile": "Profil bearbeiten",
"raiseHand": "Hand erheben",
"raiseYourHand": "Melden",
"security": "Sicherheitsoptionen",
"Settings": "Einstellungen",
"sharedvideo": "YouTube-Video teilen",
"shareRoom": "Person einladen",

View File

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

View File

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

View File

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

View File

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

View File

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

3150
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -50,13 +50,13 @@
"i18next-xhr-backend": "3.0.0",
"jQuery-Impromptu": "github:trentrichardson/jQuery-Impromptu#v6.0.0",
"jitsi-meet-logger": "github:jitsi/jitsi-meet-logger#5ec92357570dc8f0b7ffc1528820721c84c6af8b",
"jquery": "3.4.0",
"jquery": "3.5.1",
"jquery-contextmenu": "2.4.5",
"jquery-i18next": "1.2.1",
"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",
@@ -69,7 +69,7 @@
"react-linkify": "1.0.0-alpha",
"react-native": "github:jitsi/react-native#efd2aff5661d75a230e36406b698cfe0ee545be2",
"react-native-background-timer": "2.1.1",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#902e6e92d6bae450a6052f76ba4d02f977ffd8f2",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#928a80e2ffef0d7e84936d7e7e0acc4f53ee8470",
"react-native-callstats": "3.61.0",
"react-native-collapsible": "1.5.1",
"react-native-default-preference": "1.4.2",
@@ -80,7 +80,7 @@
"react-native-svg": "9.7.1",
"react-native-svg-transformer": "0.13.0",
"react-native-swipeout": "2.3.6",
"react-native-watch-connectivity": "0.2.0",
"react-native-watch-connectivity": "0.4.3",
"react-native-webrtc": "1.75.3",
"react-native-webview": "7.4.1",
"react-redux": "7.1.0",
@@ -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",
@@ -124,14 +125,14 @@
"imports-loader": "0.7.1",
"jetifier": "1.6.4",
"metro-react-native-babel-preset": "0.56.0",
"node-sass": "4.13.1",
"node-sass": "4.14.1",
"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.1.2",
"webpack-dev-server": "3.8.2"
"webpack-cli": "3.3.11",
"webpack-dev-server": "3.11.0"
},
"engines": {
"node": ">=8.0.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -77,7 +77,7 @@ class RoomLockPrompt extends Component<Props> {
return (
<InputDialog
contentKey = 'dialog.passwordLabel'
contentKey = 'security.about'
onCancel = { this._onCancel }
onSubmit = { this._onSubmit }
textInputProps = { textInputProps }
@@ -129,10 +129,7 @@ class RoomLockPrompt extends Component<Props> {
_validateInput(value: string) {
// we want only digits, but both number-pad and numeric add ',' and '.' as symbols
if (this.props.passwordNumberOfDigits
&& value.length > 0
&& !/^\d+$/.test(value)) {
if (this.props.passwordNumberOfDigits && value.length > 0 && !/^\d+$/.test(value)) {
return false;
}

View File

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

View File

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

View File

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

View File

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

View File

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