Compare commits

..

15 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
9bbf525060 fix(local-recordings) back to WebM format, fix duration
After a lot of back and forth, WebM seems to be the only option we
really have. In terms of containers and codecs, here is the rundown:

- WebM, any codec: the resulting file is not seekable
- MKV, any codec: the resulting file is not seekable
- MP4, vp9 + opus: video artifacts and audio clipping, file is seekable
- MP4, av1 + AAC: all good, but not supported on Linux :-/

MP4 looked very promising but there is no combination that leads to
something that works reliably everywhere, oh well. In addition, MP4
files can be opened with QuickTime on macOS, but not with the codec
combination we'd use, so that is somewhat a disadvantage.

So, we are back to where we started: WebM with VP8 and opus. But we need
to fix the duration in a potentially long file... the trick is to _only_
fix the duration. We can do that by inserting the right segment in the
metadata section. Something we cannot do without reading the whole file
is create cue points, but players like VLC seem to work well without
them.
2025-05-19 11:37:03 -05:00
Saúl Ibarra Corretgé
9dcad3e2fb fix(local-recordings) use the Matroska container with VP8 as a codec
In the 1st incarnation of local recordings we used to use VP8 as the
video encoder. Upon switching to MP4 that combiantion is not supported
for some reason, so I used VP9 instead.

Some anecdotal evidence suggests VP9 is behqaving more erratically, with
rendering errors and fixes.

Turns out Chrome also supports the Matroska container! And VP8 inside it
at that! The bonus we get from using it is that QuickTime on macOS won't
try to open it, thus avoiding some confusion with MP4 files, which it
recognizes, but cannot open due to the video codec.
2025-05-08 09:59:29 -05:00
Saúl Ibarra Corretgé
cf01abf72f fix(local-recordings) use constant bitrate for audio 2025-05-08 09:59:23 -05:00
Andrei Gavrilescu
6c6442f97d fix(popover): touch interaction closes overflow drawer without triggering action
* automatic drawer toolbox on mobile browser

* fix touch interaction on Popover
2025-05-06 16:20:58 +03:00
Saúl Ibarra Corretgé
2ea866d04e fix(local-recordings) make sure we have a gDM audio stream 2025-05-06 07:46:40 -05:00
Saúl Ibarra Corretgé
4da3648d6e fix(local-recordings) tweak audio constraints for local recordings 2025-05-06 07:46:27 -05:00
Saúl Ibarra Corretgé
33f61e5c23 feat(recording) add ability to skip consent in-meeting
When turned on, the consent dialog won't be displayed for the users who
are already in the meeting, it will only be displayed to those who join
after the recording was started.
2025-04-30 11:34:59 -04:00
Saúl Ibarra Corretgé
d8e91ad63c fix(local-recordings) fix data loss when MediaRecorder is stopped
Flush the file after the 'stop' event is emitted, which happens _after_
the last 'dataavailable' has been emitted, and thus when the
MediaRecorder is really done.

In addition, lower the time slice as added precaution against crashes.
2025-04-30 11:34:51 -04:00
Saúl Ibarra Corretgé
580a769ee3 fix(local-recordings) more resilient way to get local audio
It's OK if we don't have any local audio track, we'll add it to the
mixer later.

The original bug / limitation that prompted the previous code no longer
applies since we always have a MediaStream (with audio tracks) which
we are recording.
2025-04-30 11:34:41 -04:00
Saúl Ibarra Corretgé
e200ab64b3 fix(local-recordings) remove text mentioning time limit 2025-04-30 11:34:30 -04:00
Saúl Ibarra Corretgé
434f66030a feat(local-recordings) refactor how audio is captured
Capture the tab audio, which will include all participants and sound
effects, YouTube videos, anything playing in the tab.

This requires the `suppressLocalAudioPlayback` constraint since
otherwise the shared tab won't keep playing audio.

Local audio still needs to be injected seprarately, since it's not
played back to the local user.
2025-04-30 11:34:20 -04:00
Saúl Ibarra Corretgé
0908a51650 fix(local-recordings) style, for readability 2025-04-30 11:34:10 -04:00
Saúl Ibarra Corretgé
c6a49c8697 fix(local-recording) require setCaptureHandleConfig 2025-04-30 11:33:59 -04:00
Saúl Ibarra Corretgé
befef4af54 fix(recording) prevent multiple consent requests
A given recording should only trigger a single consent request.

The mechanism to notify about recording status updates may fire multiple
times since it's tied to XMPP presence and may send updates such as when
the live stream view URL is set.

Rather than trying to handle all possible corner cases to make sure we
only show the consent dialog once, keep track of the recording session
IDs for which we _have_ asked for consent and skip the dialog in case we
have done it already.
2025-04-30 11:33:43 -04:00
Jaya Allamsetty
7fb29693a2 fix(test): Fix codec selection test 2025-04-30 11:33:20 -04:00
194 changed files with 1500 additions and 4752 deletions

View File

@@ -131,6 +131,7 @@ import {
createLocalTracksF,
getLocalJitsiAudioTrack,
getLocalJitsiVideoTrack,
getLocalTracks,
getLocalVideoTrack,
isLocalTrackMuted,
isUserInteractionRequiredForUnmute
@@ -1828,6 +1829,35 @@ export default {
onStartMutedPolicyChanged(audio, video));
}
);
room.on(JitsiConferenceEvents.STARTED_MUTED, () => {
const audioMuted = room.isStartAudioMuted();
const videoMuted = room.isStartVideoMuted();
const localTracks = getLocalTracks(APP.store.getState()['features/base/tracks']);
const promises = [];
APP.store.dispatch(setAudioMuted(audioMuted));
APP.store.dispatch(setVideoMuted(videoMuted));
// Remove the tracks from the peerconnection.
for (const track of localTracks) {
// Always add the track on Safari because of a known issue where audio playout doesn't happen
// if the user joins audio and video muted, i.e., if there is no local media capture.
if (audioMuted && track.jitsiTrack?.getType() === MEDIA_TYPE.AUDIO && !browser.isWebKitBased()) {
promises.push(this.useAudioStream(null));
}
if (videoMuted && track.jitsiTrack?.getType() === MEDIA_TYPE.VIDEO) {
promises.push(this.useVideoStream(null));
}
}
Promise.allSettled(promises)
.then(() => {
APP.store.dispatch(showNotification({
titleKey: 'notify.mutedTitle',
descriptionKey: 'notify.muted'
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
});
});
room.on(
JitsiConferenceEvents.DATA_CHANNEL_OPENED, () => {
@@ -2039,7 +2069,8 @@ export default {
_initDeviceList(setDeviceListChangeHandler = false) {
const { mediaDevices } = JitsiMeetJS;
if (mediaDevices.isDeviceChangeAvailable()) {
if (mediaDevices.isDeviceListAvailable()
&& mediaDevices.isDeviceChangeAvailable()) {
if (setDeviceListChangeHandler) {
this.deviceChangeListener = devices =>
window.setTimeout(() => this._onDeviceListChanged(devices), 0);

View File

@@ -403,8 +403,6 @@ var config = {
// // requireConsent: true,
// // If true consent will be skipped for users who are already in the meeting.
// // skipConsentInMeeting: true,
// // Link for the recording consent dialog's "Learn more" link.
// // consentLearnMoreLink: 'https://jitsi.org/meet/consent',
// },
// recordingService: {
@@ -613,7 +611,6 @@ var config = {
// medium: 5000,
// long: 10000,
// extraLong: 60000,
// sticky: 0,
// },
// // Options for the recording limit notification.
@@ -1883,14 +1880,6 @@ var config = {
// If true remove the tint foreground on focused user camera in filmstrip
// disableCameraTintForeground: false,
// File sharign service.
// fileSharing: {
// // The URL of the file sharing service API. See resources/file-sharing.yaml for more details.
// apiUrl: 'https://example.com',
// // Whether the file sharing service is enabled or not.
// enabled: true,
// },
};
// Set the default values for JaaS customers

View File

@@ -141,6 +141,32 @@
left: 0;
}
.smileys-panel {
bottom: 100%;
box-sizing: border-box;
background-color: rgba(0, 0, 0, .6) !important;
height: auto;
display: flex;
overflow: hidden;
position: absolute;
width: calc(#{$sidebarWidth} - 32px);
margin-bottom: 5px;
margin-left: -5px;
/**
* CSS transitions do not apply for auto dimensions. So to produce the css
* accordion effect for showing and hiding the smiley-panel, while allowing
* for variable panel, height, use a very large max-height and animate off
* of that.
*/
transition: max-height 0.3s;
#smileysContainer {
background-color: $chatBackgroundColor;
border-top: 1px solid #A4B8D1;
}
}
#smileysContainer .smiley {
font-size: 1.625rem;
}

View File

@@ -4,3 +4,9 @@
border-radius: 3px;
}
}
.mobile-browser.shift-right {
.participants_pane {
z-index: -1;
}
}

View File

@@ -60,3 +60,21 @@
}
}
}
.desktop-browser {
&.shift-right {
@media only screen and (max-width: $verySmallScreen + $sidebarWidth) {
#videoResolutionLabel {
display: none;
}
.vertical-filmstrip .filmstrip {
display: none;
}
.chrome-extension-banner {
display: none;
}
}
}
}

View File

@@ -22,6 +22,7 @@ $newToolbarSizeWithPadding: calc(#{$newToolbarSize} + 24px);
* Chat
*/
$chatBackgroundColor: #131519;
$sidebarWidth: 315px;
/**
* Misc.

View File

@@ -91,3 +91,15 @@
}
}
}
.shift-right .remote-videos > div {
/**
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants,
* from which we subtract the chat size.
*/
@media only screen and (max-width: calc(500px + #{$sidebarWidth})) {
video {
object-fit: cover;
}
}
}

View File

@@ -835,6 +835,7 @@
"or": "أو",
"premeeting": "ما قبل المُلتقى",
"screenSharingError": "خطأ في مشاركة الشاشة:",
"showScreen": "تفعيل واجهة ما قبل المُلتقى",
"startWithPhone": "البدء مع جهاز الصوت من الجوال",
"videoOnlyError": "خطأ في الفيديو:",
"videoTrackError": "لم نتمكن من إنشاء ملف الفيديو",

View File

@@ -842,6 +842,7 @@
"or": "o",
"premeeting": "Prereunió",
"screenSharingError": "Error en compartir la pantalla:",
"showScreen": "Activa la pantalla de prereunió",
"startWithPhone": "Comença amb àudio de telèfon",
"videoOnlyError": "Error del vídeo:",
"videoTrackError": "No s'ha pogut crear la pista de vídeo.",

View File

@@ -976,6 +976,7 @@
"proceedAnyway": "Přesto pokračujte",
"recordingWarning": "Ostatní účastníci mohou tento hovor nahrávat",
"screenSharingError": "Chyba sdílení obrazovky:",
"showScreen": "Zapnout obrazovku před setkáním",
"startWithPhone": "Začít se zvukem přes telefon",
"unsafeRoomConsent": "Chápu rizika, chci se připojit k setkání",
"videoOnlyError": "Chyba videa:",

View File

@@ -122,9 +122,7 @@
"nickname": {
"popover": "Wähle einen Alias",
"title": "Geben Sie einen Alias zum Chatten ein",
"titleWithCC": "Geben Sie einen Alias zum Chatten und für Untertitel ein",
"titleWithPolls": "Geben Sie einen Alias zum Chatten und für Umfragen ein",
"titleWithPollsAndCC": "Geben Sie einen Alias zum Chatten, für Umfragen und Untertitel ein"
"titleWithPolls": "Geben Sie einen Alias zum Chatten ein"
},
"noMessagesMessage": "Es gibt noch keine Nachricht in dieser Konferenz. Starten Sie hier eine Unterhaltung!",
"privateNotice": "Private Nachricht an {{recipient}}",
@@ -133,13 +131,10 @@
"systemDisplayName": "System",
"tabs": {
"chat": "Chatten",
"closedCaptions": "Untertitel",
"polls": "Umfragen"
},
"title": "Chatten",
"titleWithCC": "Chatten und Untertitel",
"titleWithPolls": "Chatten und Umfragen",
"titleWithPollsAndCC": "Chatten, Umfragen und Untertitel",
"you": "Sie"
},
"chromeExtensionBanner": {
@@ -149,10 +144,6 @@
"dontShowAgain": "Hinweis nicht mehr anzeigen",
"installExtensionText": "Installieren Sie die Erweiterung für die Integration von Google Calendar und Office 365"
},
"closedCaptionsTab": {
"emptyState": "Die Untertitel sind verfügbar, sobald sie von der Moderation gestartet wurden",
"startClosedCaptionsButton": "Untertitel starten"
},
"connectingOverlay": {
"joiningRoom": "Eine Verbindung zu Ihrer Konferenz wird hergestellt…"
},
@@ -272,8 +263,7 @@
"Remove": "Entfernen",
"Share": "Teilen",
"Submit": "OK",
"Understand": "Verstanden, Stummschaltung beibehalten",
"UnderstandAndUnmute": "Verstanden, bitte Stummschaltung aufheben",
"Understand": "Verstanden",
"WaitForHostMsg": "Die Konferenz wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
"WaitForHostNoAuthMsg": "Die Konferenz wurde noch nicht gestartet. Bitte warten Sie, bis die Konferenz gestartet wird.",
"WaitingForHostButton": "Auf Moderation warten",
@@ -310,7 +300,6 @@
"conferenceReloadMsg": "Wir versuchen das zu beheben. Verbinde in {{seconds}} Sekunden …",
"conferenceReloadTitle": "Leider ist etwas schiefgegangen.",
"confirm": "Bestätigen",
"confirmBack": "Zurück",
"confirmNo": "Nein",
"confirmYes": "Ja",
"connectError": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden.",
@@ -348,7 +337,6 @@
"kickParticipantTitle": "Person entfernen?",
"kickSystemTitle": "Autsch! Sie wurden aus der Konferenz geworfen",
"kickTitle": "Autsch! {{participantDisplayName}} hat Sie aus der Konferenz geworfen",
"learnMore": "Mehr erfahren",
"linkMeeting": "Konferenz verlinken",
"linkMeetingTitle": "Konferenz mit Salesforce verlinken",
"liveStreaming": "Livestreaming",
@@ -406,9 +394,7 @@
"recentlyUsedObjects": "Ihre zuletzt verwendeten Objekte",
"recording": "Aufnahme",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Während eines Livestreams nicht möglich",
"recordingInProgressDescription": "Diese Konferenz wird aufgezeichnet und von KI analysiert {{learnMore}}. Ihr Ton und Video ist deaktiviert, wenn Sie es aktivieren, stimmen Sie der Aufzeichnung zu.",
"recordingInProgressDescriptionFirstHalf": "Diese Konferenz wird aufgezeichnet und von KI analysiert",
"recordingInProgressDescriptionSecondHalf": ". Ihr Ton und Video ist deaktiviert, wenn Sie es aktivieren, stimmen Sie der Aufzeichnung zu.",
"recordingInProgressDescription": "Diese Konferenz wird aufgezeichnet. Ihr Ton und Video ist deaktiviert, wenn Sie es aktivieren, stimmen Sie der Aufzeichnung zu.",
"recordingInProgressTitle": "Aufnahme läuft",
"rejoinNow": "Jetzt erneut beitreten",
"remoteControlAllowedMessage": "{{user}} hat die Anfrage zur Fernsteuerung angenommen!",
@@ -998,6 +984,7 @@
"proceedAnyway": "Trotzdem fortsetzen",
"recordingWarning": "Diese Konferenz wird möglicherweise von anderen Personen aufgezeichnet",
"screenSharingError": "Fehler bei Bildschirmfreigabe:",
"showScreen": "Konferenzvorschau aktivieren",
"startWithPhone": "Mit Telefonaudio starten",
"unsafeRoomConsent": "Ich verstehe das Risiko und möchte der Konferenz beitreten",
"videoOnlyError": "Videofehler:",
@@ -1152,7 +1139,6 @@
"selectMic": "Mikrofon",
"selfView": "Eigene Ansicht",
"shortcuts": "Tastaturkürzel",
"showSubtitlesOnStage": "Untertitel in Hauptansicht anzeigen",
"speakers": "Lautsprecher",
"startAudioMuted": "Alle Personen treten stummgeschaltet bei",
"startReactionsMuted": "Interaktionstöne für alle deaktivieren",
@@ -1249,7 +1235,6 @@
"closeChat": "Chat schließen",
"closeMoreActions": "„Weitere Einstellungen“ schließen",
"closeParticipantsPane": "Anwesendenliste schließen",
"closedCaptions": "Untertitel",
"collapse": "Einklappen",
"document": "Geteiltes Dokument schließen",
"documentClose": "Geteiltes Dokument schließen",
@@ -1340,7 +1325,6 @@
"closeChat": "Chat schließen",
"closeParticipantsPane": "Anwesenheitsliste schließen",
"closeReactionsMenu": "Interaktionsmenü schließen",
"closedCaptions": "Untertitel",
"disableNoiseSuppression": "Rauschunterdrückung deaktivieren",
"disableReactionSounds": "Sie können die Interaktionstöne für diese Konferenz deaktivieren",
"documentClose": "Geteiltes Dokument schließen",
@@ -1433,16 +1417,13 @@
"failed": "Transkribieren fehlgeschlagen",
"labelTooltip": "Die Konferenz wird transkribiert",
"labelTooltipExtra": "Zusätzlich wird das Transkript später verfügbar sein.",
"openClosedCaptions": "Untertitel öffnen",
"original": "Original",
"sourceLanguageDesc": "Aktuell ist die Sprache der Konferenz auf <b>{{sourceLanguage}}</b> eingestellt. <br/> Sie könne dies hier ",
"sourceLanguageHere": "ändern",
"start": "Anzeige der Untertitel starten",
"stop": "Anzeige der Untertitel stoppen",
"subtitles": "Untertitel",
"subtitlesOff": "Ausschalten",
"tr": "TR",
"translateTo": "Übersetzen in"
"tr": "TR"
},
"unpinParticipant": "{{participantName}} - Nicht mehr anheften",
"userMedia": {

View File

@@ -845,6 +845,7 @@
"or": "abo",
"premeeting": "naglěd",
"screenSharingError": "zmólenje pśi sobuźělenju monitora:",
"showScreen": "naglěd konferency aktiwěrowaś",
"startWithPhone": "zachopiś z telefonowym audio",
"videoOnlyError": "zmólenje wideo:",
"videoTrackError": "Sćažka wideo njejo mógła se załožyś.",

View File

@@ -862,6 +862,7 @@
"or": "ή",
"premeeting": "Προ σύσκεψη",
"screenSharingError": "Σφάλμα διαμοιρασμού οθόνης:",
"showScreen": "Ενεργοποίηση οθόνης προ σύσκεψης",
"startWithPhone": "Ξεκινήστε με ήχο τηλεφώνου",
"videoOnlyError": "Σφάλμα βίντεο:",
"videoTrackError": "Δεν ήταν δυνατή η δημιουργία κομματιού βίντεο.",

View File

@@ -935,6 +935,7 @@
"premeeting": "Antaŭkunveno",
"proceedAnyway": "Daŭrigi",
"screenSharingError": "Eraro kun la ekrandividado:",
"showScreen": "Ebligu antaŭkunvenon ekranon",
"startWithPhone": "Komencu kun la telefona sono",
"unsafeRoomConsent": "Akceptu la riskojn, kaj daŭrigi",
"videoOnlyError": "Eraro kun la videaĵo:",

View File

@@ -886,6 +886,7 @@
"premeeting": "Pre-reunión",
"proceedAnyway": "Continuar de todos modos",
"screenSharingError": "Error al compartir pantalla:",
"showScreen": "Habilitar pantalla pre-reunión",
"startWithPhone": "Iniciar con audio de llamada telefónica",
"unsafeRoomConsent": "Comprendo los riesgos, quiero unirme a la reunión",
"videoOnlyError": "Error con el vídeo:",

View File

@@ -759,6 +759,7 @@
"or": "o",
"premeeting": "Pre-reunión",
"screenSharingError": "Error al compartir pantalla:",
"showScreen": "Habilitar pantalla pre-reunión",
"startWithPhone": "Iniciar con audio de llamada telefónica",
"videoOnlyError": "Error con el video:",
"videoTrackError": "No se pudo crear la pista de video.",

View File

@@ -646,6 +646,7 @@
"or": "edo",
"premeeting": "Aurre-bilera",
"screenSharingError": "Errorea pantaila partekatzean:",
"showScreen": "Aktibatu bileraren aurreko pantaila",
"startWithPhone": "Telefono diearen audioarekin hasi",
"videoOnlyError": "Errorea bideoan:",
"videoTrackError": "Ezin izan da bideo pista sortu.",

View File

@@ -894,6 +894,7 @@
"premeeting": "پیش‌جلسه",
"proceedAnyway": "در هر صورت انجام شود",
"screenSharingError": "خطا در هم‌رسانی صفحه:",
"showScreen": "فعال‌سازی صفحهٔ پیش‌جلسه",
"startWithPhone": "شروع با صدای گوشی",
"unsafeRoomConsent": "من خطر احتمالی را درک می‌کنم؛ می‌خواهم به جلسه بپیوندم",
"videoOnlyError": "خطای ویدیو:",

View File

@@ -976,6 +976,7 @@
"proceedAnyway": "Continuer quand même",
"recordingWarning": "D'autres participants peuvent enregistrer cet appel",
"screenSharingError": "Erreur de partage d'écran:",
"showScreen": "Activer l'écran de pré-séance",
"startWithPhone": "Commencez avec l'audio du téléphone",
"unsafeRoomConsent": "Je comprends les risques et je veux quand même rejoindre cette réunion",
"videoOnlyError": "Erreur vidéo:",

View File

@@ -954,6 +954,7 @@
"premeeting": "Pré-séance",
"proceedAnyway": "Continuer quand même",
"screenSharingError": "Erreur de partage d'écran:",
"showScreen": "Activer l'écran de pré-séance",
"startWithPhone": "Commencez avec l'audio du téléphone",
"unsafeRoomConsent": "Je comprends les risques et je veux quand même rejoindre cette réunion",
"videoOnlyError": "Erreur vidéo:",

View File

@@ -622,6 +622,7 @@
"or": "या",
"premeeting": "प्री मीटिंग",
"screenSharingError": "स्क्रीन शेयरिंग त्रुटि:",
"showScreen": "प्री मीटिंग स्क्रीन सक्षम करें",
"startWithPhone": "फोन ऑडियो से शुरू करें",
"videoOnlyError": "वीडियो त्रुटि:",
"videoTrackError": "वीडियो ट्रैक नहीं बना सका",

View File

@@ -840,6 +840,7 @@
"or": "ili",
"premeeting": "Predsastanak",
"screenSharingError": "Greška dijeljenja ekrana:",
"showScreen": "Uključi ekran predsastanka",
"startWithPhone": "Počni s telefonom",
"videoOnlyError": "Greška videa:",
"videoTrackError": "Nije bilo moguće stvoriti videosnimku.",

View File

@@ -824,6 +824,7 @@
"or": "abo",
"premeeting": "předstwa",
"screenSharingError": "zmylk při dopušćenju wužiwanja monitora:",
"showScreen": "konferencnu předstwu aktiwěrować",
"startWithPhone": "z telefoniskim awdijom startować",
"videoOnlyError": "widejowy zmylk:",
"videoTrackError": "widejowy trak njebě móžny",

View File

@@ -683,6 +683,7 @@
"or": "vagy",
"premeeting": "Csatlakozás előtt",
"screenSharingError": "Képernyő megosztás hiba:",
"showScreen": "Csatlakozás előtti kamerakép",
"startWithPhone": "Kezdés telefonhanggal",
"videoOnlyError": "Videó hiba:",
"videoTrackError": "Nem sikerült a videó megjelenítés.",

View File

@@ -955,6 +955,7 @@
"proceedAnyway": "Lanjutkan saja",
"recordingWarning": "Peserta lain mungkin sedang merekam panggilan ini",
"screenSharingError": "Kesalahan berbagi layar:",
"showScreen": "Aktifkan layar pra pertemuan",
"startWithPhone": "Mulai dengan audio ponsel",
"unsafeRoomConsent": "Saya memahami risikonya, saya ingin bergabung dengan pertemuan",
"videoOnlyError": "Kesalahan video:",

View File

@@ -936,6 +936,7 @@
"premeeting": "Á undan fundi",
"proceedAnyway": "Halda samt áfram",
"screenSharingError": "Villa í skjádeilingu:",
"showScreen": "Virkja skjá á undan fundi",
"startWithPhone": "Byrja með símahljóði",
"unsafeRoomConsent": "Ég skil áhættuna, ég vil taka þátt í fundinum",
"videoOnlyError": "Villa í myndmerki:",

File diff suppressed because it is too large Load Diff

View File

@@ -784,6 +784,7 @@
"or": "または",
"premeeting": "プレミーティング",
"screenSharingError": "画面共有のエラー:",
"showScreen": "プレミーティング画面を有効",
"startWithPhone": "音声通話を開始",
"videoOnlyError": "ビデオのエラー:",
"videoTrackError": "ビデオトラックを生成できませんでした。",

View File

@@ -736,6 +736,7 @@
"or": "neɣ",
"premeeting": "Timlilit tuzwirt",
"screenSharingError": "Tuccḍa deg beṭṭu n ugdil:",
"showScreen": "Rmed agdil n temlilit tuzwirt",
"startWithPhone": "Bdu s umeslaw n tiliɣri",
"videoOnlyError": "Tuccḍa deg tvidyut:",
"videoTrackError": "Asnulfu n track n tvidyut ulamek.",

View File

@@ -975,6 +975,7 @@
"proceedAnyway": "그래도 진행",
"recordingWarning": "다른 참가자가 이 통화를 녹화하고 있을 수 있습니다",
"screenSharingError": "화면 공유 오류:",
"showScreen": "회의 전 화면 활성화",
"startWithPhone": "전화 오디오로 시작",
"unsafeRoomConsent": "위험을 이해하며 회의에 참여하고 싶습니다",
"videoOnlyError": "비디오 오류:",

View File

@@ -122,9 +122,7 @@
"nickname": {
"popover": "Izvēlieties vārdu",
"title": "Ierakstiet vārdu, lai izmantotu tērzēšanā",
"titleWithCC": "Ievadiet segvārdu, lai izmantotu tērzēšanā un slēptos subtitros",
"titleWithPolls": "Ierakstiet segvārdu, lai izmantotu tērzēšanā un aptaujās",
"titleWithPollsAndCC": "Ievadiet segvārdu, lai izmantotu tērzēšanā, aptaujās un slēptos subtitros"
"titleWithPolls": "Ierakstiet vārdu, lai izmantotu tērzēšanā un aptaujās"
},
"noMessagesMessage": "Sapulcē pagaidām nav nevienas ziņas. Uzsāciet saraksti!",
"privateNotice": "Privāta ziņa adresātam {{recipient}}",
@@ -133,13 +131,10 @@
"systemDisplayName": "Sistēma",
"tabs": {
"chat": "Tērzēšana",
"closedCaptions": "Slēptie subtitri",
"polls": "Aptaujas"
},
"title": "Tērzēšana",
"titleWithCC": "Tērzēšana un slēptie subtitri",
"titleWithPolls": "Tērzēšana un Aptaujas",
"titleWithPollsAndCC": "Tērzēšana, Aptaujas un Slēptie subtitri",
"you": "jūs"
},
"chromeExtensionBanner": {
@@ -149,10 +144,6 @@
"dontShowAgain": "Nerādīt man šo vēlreiz",
"installExtensionText": "Uzstādīt spraudni Google kalendāra un Office 365 integrācijai"
},
"closedCaptionsTab": {
"emptyState": "Slēpto subtitru saturs būs pieejams, tiklīdz moderators uzsāks to.",
"startClosedCaptionsButton": "Uzsākt slēptos subtitrus"
},
"connectingOverlay": {
"joiningRoom": "Notiek pieslēgšanās jūsu sapulcei…"
},
@@ -273,7 +264,6 @@
"Share": "Kopīgot",
"Submit": "Iesniegt",
"Understand": "Saprotu",
"UnderstandAndUnmute": "Es saprotu, lūdzu, ieslēdziet skaņu.",
"WaitForHostMsg": "Sapulce vēl nav sākusies, jo vēl nav ieradies neviens moderators. Lūdzu, autorizējieties, lai kļūtu par moderatoru. Pretējā gadījumā, lūdzu, uzgaidiet.",
"WaitForHostNoAuthMsg": "Sapulce vēl nav sākusies, jo vēl nav ieradies neviens moderators. Lūdzu, uzgaidiet.",
"WaitingForHostButton": "Gaidīt rīkotāju",
@@ -310,7 +300,6 @@
"conferenceReloadMsg": "Cenšamies to labot. Atkārtota savienojuma izveide pēc {{seconds}} sek….",
"conferenceReloadTitle": "Diemžēl kaut kas nogāja greizi.",
"confirm": "Apstiprināt",
"confirmBack": "Atpakaļ",
"confirmNo": "Nē",
"confirmYes": "Jā",
"connectError": "Hmm! Radās problēma, un mēs nevarējām izveidot savienojumu ar sapulci.",
@@ -348,7 +337,6 @@
"kickParticipantTitle": "Izraidīt šo dalībnieku?",
"kickSystemTitle": "Ak! Jūs izraidīja no sapulces",
"kickTitle": "Ak! {{participantDisplayName}} izraidīja jūs no sapulces",
"learnMore": "uzzināt vairāk",
"linkMeeting": "Sasaistīt sapulci",
"linkMeetingTitle": "Sasaistīt sapulci ar Salesforce",
"liveStreaming": "Tiešraides straumēšana",
@@ -407,8 +395,6 @@
"recording": "Ieraksts",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Nav iespējams, kamēr ir aktīva tiešraides straume",
"recordingInProgressDescription": "Šī sapulce tiek ierakstīta. Jūsu audio un video ir izslēgti. Ja izvēlaties ieslēgt skaņu vai video, jūs piekrītat ierakstīšanai.",
"recordingInProgressDescriptionFirstHalf": "Šo sanāksmi ieraksta un analizē mākslīgais intelekts",
"recordingInProgressDescriptionSecondHalf": ". Jūsu audio un video skaņa ir izslēgta. Ja izvēlaties ieslēgt skaņu, jūs piekrītat ierakstīšanai.",
"recordingInProgressTitle": "Notiek ierakstīšana",
"rejoinNow": "Pieslēgties no jauna",
"remoteControlAllowedMessage": "{{user}} apstiprināja jūsu attālās pārvaldības pieprasījumu!",
@@ -766,8 +752,7 @@
"dataChannelClosedDescriptionWithAudio": "Savienojuma kanāls nedarbojas, tāpēc var rasties audio un video traucējumi.",
"dataChannelClosedWithAudio": "Audio un video kvalitāte var būt traucēta",
"disabledIframe": "Iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks atvienots pēc {{timeout}} minūtēm.",
"disabledIframeSecondaryNative": "Domēna {{domain}} iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks pārtraukts pēc {{timeout}} minūtēm.",
"disabledIframeSecondaryWeb": "Domēna {{domain}} iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks pārtraukts pēc {{timeout}} minūtēm. Lūdzu, produkcijas videi izmantojiet <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a>!",
"disabledIframeSecondary": "{{domain}} iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks atvienots pēc {{timeout}} minūtēm. Lūdzu, izmantojiet <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi kā Pakalpojums</a> produkcijas iegulšanai!",
"disconnected": "savienojums pārtraukts",
"displayNotifications": "Rādīt paziņojumus",
"dontRemindMe": "Neatgādināt man",
@@ -895,7 +880,6 @@
"waitingLobby": "Gaida vestibilā ({{count}})"
},
"search": "Meklēt dalībniekus",
"searchDescription": "Sāciet rakstīt, lai atlasītu dalībnieks",
"title": "Dalībnieki"
},
"passwordDigitsOnly": "Līdz {{number}} cipariem",
@@ -998,6 +982,7 @@
"proceedAnyway": "Tik un tā turpināt",
"recordingWarning": "Citi dalībnieki var ierakstīt šo zvanu",
"screenSharingError": "Ekrāna koplietošanas kļūda:",
"showScreen": "Iespējot ekrānu pirms sapulces",
"startWithPhone": "Sākt ar tālruņa audio",
"unsafeRoomConsent": "Es saprotu riskus, vēlos pievienoties sapulcei",
"videoOnlyError": "Video kļūda:",
@@ -1122,7 +1107,6 @@
"signedIn": "Pašreiz ir piekļuve e-pasta adreses {{email}} kalendāra notikumiem. Noklikšķiniet uz pogas |Atslēgt|, lai izslēgtu piekļuvi šiem kalendāra pasākumiem.",
"title": "Kalendārs"
},
"chatWithPermissions": "Tērzēšanai nepieciešama atļauja",
"desktopShareFramerate": "Darbvirsmas koplietošanas kadru ātrums",
"desktopShareHighFpsWarning": "Lielāks kadru nomaiņas ātrums darbvirsmas koplietošanai var ietekmēt joslas platumu. Lai jaunie iestatījumi stātos spēkā, ir jārestartē ekrāna kopīgošana.",
"desktopShareWarning": "Lai jaunie iestatījumi stātos spēkā, ir jārestartē ekrāna kopīgošana.",
@@ -1152,7 +1136,6 @@
"selectMic": "Mikrofons",
"selfView": "Pašskats",
"shortcuts": "Īsceļi",
"showSubtitlesOnStage": "Rādīt subtitrus galvenajā skatā",
"speakers": "Skaļruņi",
"startAudioMuted": "Dalībnieki pievienojas ar izslēgtu mikrofonu",
"startReactionsMuted": "Izslēgt reakcijas skaņas visiem",
@@ -1212,7 +1195,6 @@
"neutral": "Neitrāls",
"sad": "Bēdīgs",
"search": "Meklēt",
"searchDescription": "Sāciet rakstīt, lai atlasītu dalībnieks",
"searchHint": "Meklēt dalībniekus",
"seconds": "{{count}}s",
"speakerStats": "Dalībnieka uzstāšanās statistika",
@@ -1249,7 +1231,6 @@
"closeChat": "Aizvērt tērzēšanu",
"closeMoreActions": "Aizvērt vairāk darbību izvēlni",
"closeParticipantsPane": "Aizvērt dalībnieku paneli",
"closedCaptions": "Slēptie subtitri",
"collapse": "Sakļaut",
"document": "Kopīgotais dokuments (iesl./izsl.)",
"documentClose": "Aizvērt kopīgoto dokumentu",
@@ -1340,7 +1321,6 @@
"closeChat": "Aizvērt tērzētavu",
"closeParticipantsPane": "Aizvērt dalībnieku paneli",
"closeReactionsMenu": "Aizvērt reakciju izvēlni",
"closedCaptions": "Slēptie subtitri",
"disableNoiseSuppression": "Atspējot trokšņu slāpēšanu",
"disableReactionSounds": "Šai sapulcei varat atspējot reakcijas skaņas",
"documentClose": "Aizvērt kopīgoto dokumentu",
@@ -1433,16 +1413,13 @@
"failed": "Atšifrējuma izveide neizdevās",
"labelTooltip": "Šajā sapulcē notiek atšifrējuma izveide.",
"labelTooltipExtra": "Turklāt vēlāk būs pieejams atšifrējums.",
"openClosedCaptions": "Atvērt slēptos subtitrus",
"original": "Oriģināls",
"sourceLanguageDesc": "Pašlaik sapulces valoda ir iestatīta uz <b>{{sourceLanguage}}</b>. <br/> Varat to mainīt no ",
"sourceLanguageHere": "šeit",
"start": "Iesl. subtitru rādīšanu",
"stop": "Izsl. subtitru rādīšanu",
"subtitles": "Subtitri",
"subtitlesOff": "Izslēgts",
"tr": "TR",
"translateTo": "Tulkot uz"
"tr": "TR"
},
"unpinParticipant": "{{participantName}} — atspraust",
"userMedia": {

View File

@@ -602,6 +602,7 @@
"or": "അല്ലെങ്കിൽ",
"premeeting": "പ്രീ മീറ്റിംഗ്",
"screenSharingError": "സ്ക്രീൻ പങ്കിടൽ പിശക്:",
"showScreen": "പ്രീ മീറ്റിംഗ് സ്ക്രീൻ പ്രാപ്തമാക്കുക",
"startWithPhone": "ഫോൺ ഓഡിയോ ഉപയോഗിച്ച് ആരംഭിക്കുക",
"videoOnlyError": "വീഡിയോ പിശക്:",
"videoTrackError": "വീഡിയോ ട്രാക്ക് സൃഷ്ടിക്കാൻ കഴിഞ്ഞില്ല.",

View File

@@ -872,6 +872,7 @@
"or": "эсвэл",
"premeeting": "Уулзалтын өмнө",
"screenSharingError": "Дэлгэц хуваалцахын алдаа:",
"showScreen": "Уулзалтын өмгөх дэлгэц идэвхижүүлэх",
"startWithPhone": "Утасны дуугаар холбогдох",
"videoOnlyError": "Видео дамжуулалтын алдаа:",
"videoTrackError": "Видео бичлэг үүсгэж чадсангүй.",

View File

@@ -976,6 +976,7 @@
"proceedAnyway": "Fortsett likevel",
"recordingWarning": "Andre deltakere kan ta opp denne samtalen",
"screenSharingError": "Feil ved skjermdeling:",
"showScreen": "Aktiver skjerm før møtet",
"startWithPhone": "Start med telefonlyd",
"unsafeRoomConsent": "Jeg forstår risikoen, jeg vil bli med i møtet",
"videoOnlyError": "Video feil:",

View File

@@ -723,6 +723,7 @@
"or": "of",
"premeeting": "Voorbeeldscherm",
"screenSharingError": "Fout bij schermdeling:",
"showScreen": "Voorbeeldscherm inschakelen",
"startWithPhone": "Starten met telefoonaudio",
"videoOnlyError": "Videofout:",
"videoTrackError": "Kon videotrack niet aanmaken.",

View File

@@ -976,6 +976,7 @@
"proceedAnyway": "Fortsett likevel",
"recordingWarning": "Andre deltakere kan ta opp denne samtalen",
"screenSharingError": "Feil ved skjermdeling:",
"showScreen": "Aktiver skjerm før møtet",
"startWithPhone": "Start med telefonlyd",
"unsafeRoomConsent": "Jeg forstår risikoen, jeg vil bli med i møtet",
"videoOnlyError": "Video feil:",

View File

@@ -976,6 +976,7 @@
"proceedAnyway": "Contunhar malgrat tot",
"recordingWarning": "D'autres participants pòdon enregistrar aquesta sonada",
"screenSharingError": "Error de partatge decran:",
"showScreen": "Activar l'ecran de prereünion",
"startWithPhone": "Començar amb làudio del telefòn",
"unsafeRoomConsent": "Compreni lo risc e vòli çaquelà participar a la reünion",
"videoOnlyError": "Error vidèo:",

View File

@@ -874,6 +874,7 @@
"premeeting": "Przed spotkaniem",
"proceedAnyway": "Kontynuuj mimo to",
"screenSharingError": "Błąd udostępniania ekranu:",
"showScreen": "Tryb osobistej poczekalni przed spotkaniem",
"startWithPhone": "Uruchom przez telefon",
"unsafeRoomConsent": "Rozumiem ryzyko, chcę dołączyć do spotkania",
"videoOnlyError": "Błąd wideo:",

View File

@@ -964,6 +964,7 @@
"proceedAnyway": "Continuar na mesma",
"recordingWarning": "Outros participantes podem estar a gravar esta chamada",
"screenSharingError": "Erro de partilha de ecrã:",
"showScreen": "Ativar o ecrã de pré-reunião",
"startWithPhone": "Iniciar com o áudio do telefone",
"unsafeRoomConsent": "Compreendo os riscos, quero participar na reunião",
"videoOnlyError": "Erro de vídeo:",

View File

@@ -935,6 +935,7 @@
"premeeting": "Pré-reunião",
"proceedAnyway": "Prosseguir mesmo assim",
"screenSharingError": "Erro de compartilhamento de tela:",
"showScreen": "Habilitar tela pré-reunião",
"startWithPhone": "Iniciar com o áudio da ligação",
"unsafeRoomConsent": "Eu entendo os riscos, desejo ingressar na reunião",
"videoOnlyError": "Erro de vídeo:",

View File

@@ -950,6 +950,7 @@
"proceedAnyway": "Продолжить в любом случае",
"recordingWarning": "Другие участники могут записывать этот звонок",
"screenSharingError": "Ошибка показа экрана:",
"showScreen": "Включить экран перед подключением",
"startWithPhone": "Начать с телефонной связью",
"unsafeRoomConsent": "Я понимаю риски и хочу присоединиться к встрече",
"videoOnlyError": "Ошибка видео:",

View File

@@ -842,6 +842,7 @@
"or": "opuru",
"premeeting": "Pre-riunione",
"screenSharingError": "Faddina in sa cumpartzidura de s'ischermu",
"showScreen": "Ativa ischermu de pre-riunione",
"startWithPhone": "Avia imperende s'àudio de su telèfonu",
"videoOnlyError": "Faddina de vìdeu:",
"videoTrackError": "Impossìbile creare una rasta de vìdeu.",

View File

@@ -737,6 +737,7 @@
"or": "ali",
"premeeting": "Pred sestanek",
"screenSharingError": "Napaka deljenja zaslona:",
"showScreen": "Omogoči zaslon pred sestankom",
"startWithPhone": "Začni z zvokom telefona",
"videoOnlyError": "Napaka videa:",
"videoTrackError": "Ni bilo mogoče ustvariti videa.",

View File

@@ -975,6 +975,7 @@
"proceedAnyway": "Vazhdo, sido qoftë",
"recordingWarning": "Këtë thirrje pjesëmarrës të tjerë mund ta regjistrojnë",
"screenSharingError": "Gabim ndarjeje ekrani me të tjerë:",
"showScreen": "Aktivizoni skenë para takimit",
"startWithPhone": "Nise me audio telefoni",
"unsafeRoomConsent": "I kuptoj rreziqet, dëshiroj të marr pjesë te takimi",
"videoOnlyError": "Gabim video:",

View File

@@ -483,6 +483,7 @@
"or": "или",
"premeeting": "Пред придруживањем",
"screenSharingError": "Грешка дијељења екрана:",
"showScreen": "Укључити екран 'пред придруживњем'.",
"startWithPhone": "Започети са телефонском везом.",
"videoOnlyError": "Грешка видеа:",
"videoTrackError": "Креирање видео траке није успјело.",

View File

@@ -976,6 +976,7 @@
"proceedAnyway": "Fortsätt ändå",
"recordingWarning": "",
"screenSharingError": "Skärmdelningsfel:",
"showScreen": "Aktivera skärmen före mötet",
"startWithPhone": "Börja med telefonljud",
"unsafeRoomConsent": "Jag förstår riskerna, jag vill vara med på mötet",
"videoOnlyError": "Videofel:",

View File

@@ -634,6 +634,7 @@
"or": "లేదా",
"premeeting": "Pre meeting",
"screenSharingError": "Screen sharing error:",
"showScreen": "Enable pre meeting screen",
"startWithPhone": "Start with phone audio",
"videoOnlyError": "Video error:",
"videoTrackError": "Could not create video track.",

View File

@@ -970,6 +970,7 @@
"proceedAnyway": "Yine de devam et",
"recordingWarning": "Diğer katılımcılar bu çağrıyı kaydediyor olabilir",
"screenSharingError": "Ekran paylaşma hatası:",
"showScreen": "Toplantı öncesi ekranını etkinleştir",
"startWithPhone": "Telefon sesiyle başlayın",
"unsafeRoomConsent": "Riskleri anlıyorum, toplantıya katılmak istiyorum",
"videoOnlyError": "Video hatası:",

View File

@@ -870,6 +870,7 @@
"or": "або",
"premeeting": "Перед приєднанням",
"screenSharingError": "Помилка спільного перегляду екрана:",
"showScreen": "Увімкнути вхідну панель",
"startWithPhone": "Почати в режимі телефону",
"videoOnlyError": "Помилка відео:",
"videoTrackError": "Не вдалося створити трек відео.",

View File

@@ -948,6 +948,7 @@
"proceedAnyway": "Tiếp tục dù sao",
"recordingWarning": "Có thể có người tham gia khác đang ghi lại cuộc gọi này",
"screenSharingError": "Lỗi chia sẻ màn hình:",
"showScreen": "Kích hoạt màn hình trước cuộc họp",
"startWithPhone": "Bắt đầu với âm thanh điện thoại",
"unsafeRoomConsent": "Tôi hiểu rủi ro, tôi muốn tham gia cuộc họp",
"videoOnlyError": "Lỗi video:",

View File

@@ -917,6 +917,7 @@
"premeeting": "会前",
"proceedAnyway": "仍然继续",
"screenSharingError": "共享屏幕错误:",
"showScreen": "开启会前屏幕",
"startWithPhone": "以电话音频开始",
"unsafeRoomConsent": "我了解风险,我想加入会议",
"videoOnlyError": "视频错误:",

View File

@@ -934,6 +934,7 @@
"premeeting": "會議前",
"proceedAnyway": "仍然繼續",
"screenSharingError": "螢幕分享錯誤:",
"showScreen": "啟用會議前螢幕",
"startWithPhone": "使用手機音訊開始",
"unsafeRoomConsent": "我了解風險,我想要加入會議",
"videoOnlyError": "視訊錯誤:",

View File

@@ -124,8 +124,7 @@
"title": "Enter a nickname to use chat",
"titleWithCC": "Enter a nickname to use chat and closed captions",
"titleWithPolls": "Enter a nickname to use chat and polls",
"titleWithPollsAndCC": "Enter a nickname to use chat, polls and closed captions",
"titleWithPollsAndCCAndFileSharing": "Enter a nickname to use chat, polls, closed captions and files"
"titleWithPollsAndCC": "Enter a nickname to use chat, polls and closed captions"
},
"noMessagesMessage": "There are no messages in the meeting yet. Start a conversation here!",
"privateNotice": "Private message to {{recipient}}",
@@ -135,14 +134,12 @@
"tabs": {
"chat": "Chat",
"closedCaptions": "CC",
"fileSharing": "Files",
"polls": "Polls"
},
"title": "Chat",
"titleWithCC": "CC",
"titleWithFeatures": "Chat and",
"titleWithFileSharing": "Files",
"titleWithPolls": "Polls",
"titleWithCC": "Chat and CC",
"titleWithPolls": "Chat and Polls",
"titleWithPollsAndCC": "Chat, Polls and CC",
"you": "you"
},
"chromeExtensionBanner": {
@@ -275,8 +272,7 @@
"Remove": "Remove",
"Share": "Share",
"Submit": "Submit",
"Understand": "I understand, keep me muted for now",
"UnderstandAndUnmute": "I understand, please unmute me",
"Understand": "I understand",
"WaitForHostMsg": "The conference has not yet started because no moderators have yet arrived. If you'd like to become a moderator please log-in. Otherwise, please wait.",
"WaitForHostNoAuthMsg": "The conference has not yet started because no moderators have yet arrived. Please wait.",
"WaitingForHostButton": "Wait for moderator",
@@ -313,7 +309,6 @@
"conferenceReloadMsg": "We're trying to fix this. Reconnecting in {{seconds}} sec…",
"conferenceReloadTitle": "Unfortunately, something went wrong.",
"confirm": "Confirm",
"confirmBack": "Back",
"confirmNo": "No",
"confirmYes": "Yes",
"connectError": "Oops! Something went wrong and we couldn't connect to the conference.",
@@ -351,7 +346,6 @@
"kickParticipantTitle": "Kick this participant?",
"kickSystemTitle": "Ouch! You were kicked out of the meeting",
"kickTitle": "Ouch! {{participantDisplayName}} kicked you out of the meeting",
"learnMore": "learn more",
"linkMeeting": "Link meeting",
"linkMeetingTitle": "Link meeting to Salesforce",
"liveStreaming": "Live Streaming",
@@ -409,9 +403,7 @@
"recentlyUsedObjects": "Your recently used objects",
"recording": "Recording",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Not possible while a live stream is active",
"recordingInProgressDescription": "This meeting is being recorded and analyzed by AI{{learnMore}}. Your audio and video have been muted. If you choose to unmute, you consent to being recorded.",
"recordingInProgressDescriptionFirstHalf": "This meeting is being recorded and analyzed by AI",
"recordingInProgressDescriptionSecondHalf": ". Your audio and video have been muted. If you choose to unmute, you consent to being recorded.",
"recordingInProgressDescription": "This meeting is being recorded. Your audio and video have been muted. If you choose to unmute, you consent to being recorded.",
"recordingInProgressTitle": "Recording in progress",
"rejoinNow": "Rejoin now",
"remoteControlAllowedMessage": "{{user}} accepted your remote control request!",
@@ -542,17 +534,6 @@
"veryBad": "Very Bad",
"veryGood": "Very Good"
},
"fileSharing": {
"downloadFailedDescription": "Please try again.",
"downloadFailedTitle": "Download failed",
"downloadFile": "Download",
"dragAndDrop": "Drag and drop files here",
"fileAlreadyUploaded": "File has already been uploaded to this meeting",
"removeFile": "Remove",
"uploadFailedDescription": "Please try again.",
"uploadFailedTitle": "Upload failed",
"uploadFile": "Share file"
},
"filmstrip": {
"accessibilityLabel": {
"heading": "Video thumbnails"
@@ -1012,6 +993,7 @@
"proceedAnyway": "Proceed anyway",
"recordingWarning": "Other participants may be recording this call",
"screenSharingError": "Screen sharing error:",
"showScreen": "Enable pre meeting screen",
"startWithPhone": "Start with phone audio",
"unsafeRoomConsent": "I understand the risks, I want to join the meeting",
"videoOnlyError": "Video error:",

View File

@@ -11,6 +11,7 @@ import {
getAvailableDevices,
getCurrentDevices,
isDeviceChangeAvailable,
isDeviceListAvailable,
isMultipleAudioInputSupported,
setAudioInputDevice,
setAudioOutputDevice,
@@ -988,15 +989,10 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* Returns Promise that resolves with true if the device list is available
* and with false if not.
*
* @deprecated
*
* @returns {Promise}
*/
isDeviceListAvailable() {
console.warn('isDeviceListAvailable is deprecated and will be removed in the future. '
+ 'It always returns true');
return Promise.resolve(true);
return isDeviceListAvailable(this._transport);
}
/**

View File

@@ -56,6 +56,21 @@ export function isDeviceChangeAvailable(transport, deviceType) {
});
}
/**
* Returns Promise that resolves with true if the device list is available
* and with false if not.
*
* @param {Transport} transport - The @code{Transport} instance responsible for
* the external communication.
* @returns {Promise}
*/
export function isDeviceListAvailable(transport) {
return transport.sendRequest({
type: 'devices',
name: 'isDeviceListAvailable'
});
}
/**
* Returns Promise that resolves with true if multiple audio input is supported
* and with false if not.

View File

@@ -478,11 +478,9 @@ export default class LargeVideoManager {
if (isOpen && window.innerWidth > 580) {
/**
* If chat state is open, we re-compute the container width
* by subtracting the chat width, which may be resized by the user.
* by subtracting the default width of the chat.
*/
const chatWidth = state['features/chat'].width?.current ?? CHAT_SIZE;
widthToUse -= chatWidth;
widthToUse -= CHAT_SIZE;
}
if (resizableFilmstrip && visible && filmstripWidth.current >= FILMSTRIP_BREAKPOINT) {

10
package-lock.json generated
View File

@@ -61,7 +61,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1988.0.0+83c2ac30/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1979.0.0+9da20d5f/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -17091,8 +17091,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1988.0.0+83c2ac30/lib-jitsi-meet.tgz",
"integrity": "sha512-0vhCwUToOCnT0X8qiW2FyTC6+9DfuB5+1geUYryDlvHgU+LrLM6HFsh/tHHpQFoHY8qZlRMZN4wHDSgu5h2sDQ==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1979.0.0+9da20d5f/lib-jitsi-meet.tgz",
"integrity": "sha512-Gz3TpqGMdpGbUAaL82SpllprDoz+kWmTK0YUEKy47OwWdd+OYO/SNKTGwLFvw/3fmpJPuD4wPNM2SU3xjqQgJA==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.2.1",
@@ -37723,8 +37723,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1988.0.0+83c2ac30/lib-jitsi-meet.tgz",
"integrity": "sha512-0vhCwUToOCnT0X8qiW2FyTC6+9DfuB5+1geUYryDlvHgU+LrLM6HFsh/tHHpQFoHY8qZlRMZN4wHDSgu5h2sDQ==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1979.0.0+9da20d5f/lib-jitsi-meet.tgz",
"integrity": "sha512-Gz3TpqGMdpGbUAaL82SpllprDoz+kWmTK0YUEKy47OwWdd+OYO/SNKTGwLFvw/3fmpJPuD4wPNM2SU3xjqQgJA==",
"requires": {
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",

View File

@@ -67,7 +67,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1988.0.0+83c2ac30/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1979.0.0+9da20d5f/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",

View File

@@ -1,5 +1,5 @@
diff --git a/node_modules/@giphy/js-analytics/dist/send-pingback.js b/node_modules/@giphy/js-analytics/dist/send-pingback.js
index 989f0ff..52471cb 100644
index 989f0ff..149e77c 100644
--- a/node_modules/@giphy/js-analytics/dist/send-pingback.js
+++ b/node_modules/@giphy/js-analytics/dist/send-pingback.js
@@ -10,6 +10,9 @@ var global_1 = __importDefault(require("./global"));

View File

@@ -22,6 +22,5 @@ import '../toolbox/middleware';
import '../face-landmarks/middleware';
import '../gifs/middleware';
import '../whiteboard/middleware.web';
import '../file-sharing/middleware.web';
import './middlewares.any';

View File

@@ -17,6 +17,5 @@ import '../screenshot-capture/reducer';
import '../talk-while-muted/reducer';
import '../virtual-background/reducer';
import '../web-hid/reducer';
import '../file-sharing/reducer';
import './reducers.any';

View File

@@ -38,7 +38,6 @@ import { IE2EEState } from '../e2ee/reducer';
import { IEtherpadState } from '../etherpad/reducer';
import { IFaceLandmarksState } from '../face-landmarks/reducer';
import { IFeedbackState } from '../feedback/reducer';
import { IFileSharingState } from '../file-sharing/reducer';
import { IFilmstripState } from '../filmstrip/reducer';
import { IFollowMeState } from '../follow-me/reducer';
import { IGifsState } from '../gifs/reducer';
@@ -129,7 +128,6 @@ export interface IReduxState {
'features/etherpad': IEtherpadState;
'features/face-landmarks': IFaceLandmarksState;
'features/feedback': IFeedbackState;
'features/file-sharing': IFileSharingState;
'features/filmstrip': IFilmstripState;
'features/follow-me': IFollowMeState;
'features/full-screen': IFullScreenState;

View File

@@ -72,15 +72,11 @@ export function getInitials(s?: string) {
/**
* Checks if the passed URL should be loaded with CORS.
*
* @param {string | Function} url - The URL (on mobile we use a specific Icon component for avatars).
* @param {string} url - The URL.
* @param {Array<string>} corsURLs - The URL pattern that matches a URL that needs to be handled with CORS.
* @returns {boolean}
* @returns {void}
*/
export function isCORSAvatarURL(url: string | Function, corsURLs: Array<string> = []): boolean {
if (typeof url === 'function') {
return false;
}
export function isCORSAvatarURL(url: string, corsURLs: Array<string> = []): boolean {
return corsURLs.some(pattern => url.startsWith(pattern));
}

View File

@@ -1,5 +1,6 @@
import { createStartMutedConfigurationEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState, IStore } from '../../app/types';
import { readyToClose } from '../../mobile/external-api/actions';
import { transcriberJoined, transcriberLeft } from '../../transcribing/actions';
import { setIAmVisitor } from '../../visitors/actions';
import { iAmVisitor } from '../../visitors/functions';
@@ -10,7 +11,9 @@ import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection/constants';
import { hasAvailableDevices } from '../devices/functions.any';
import JitsiMeetJS, { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet';
import {
setAudioMuted,
setAudioUnmutePermissions,
setVideoMuted,
setVideoUnmutePermissions
} from '../media/actions';
import { MEDIA_TYPE, MediaType } from '../media/constants';
@@ -23,11 +26,12 @@ import {
participantSourcesUpdated,
participantUpdated
} from '../participants/actions';
import { getLocalParticipant, getNormalizedDisplayName, getParticipantByIdOrUndefined } from '../participants/functions';
import { getNormalizedDisplayName, getParticipantByIdOrUndefined } from '../participants/functions';
import { IJitsiParticipant } from '../participants/types';
import { toState } from '../redux/functions';
import {
destroyLocalTracks,
replaceLocalTrack,
trackAdded,
trackRemoved
} from '../tracks/actions.any';
@@ -139,24 +143,7 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
conference.on(
JitsiConferenceEvents.KICKED,
(participant: any, reason: any, isReplaced: boolean) => {
if (isReplaced) {
const localParticipant = getLocalParticipant(state);
dispatch(participantUpdated({
conference,
// @ts-ignore
id: localParticipant.id,
isReplaced
}));
dispatch(readyToClose());
} else {
dispatch(kickedOut(conference, participant));
}
});
(participant: any) => dispatch(kickedOut(conference, participant)));
conference.on(
JitsiConferenceEvents.PARTICIPANT_KICKED,
@@ -176,6 +163,39 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
// Dispatches into features/base/media follow:
conference.on(
JitsiConferenceEvents.STARTED_MUTED,
() => {
const audioMuted = Boolean(conference.isStartAudioMuted());
const videoMuted = Boolean(conference.isStartVideoMuted());
const localTracks = getLocalTracks(state['features/base/tracks']);
sendAnalytics(createStartMutedConfigurationEvent('remote', audioMuted, videoMuted));
logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${videoMuted ? 'video' : ''}`);
// XXX Jicofo tells lib-jitsi-meet to start with audio and/or video
// muted i.e. Jicofo expresses an intent. Lib-jitsi-meet has turned
// Jicofo's intent into reality by actually muting the respective
// tracks. The reality is expressed in base/tracks already so what
// is left is to express Jicofo's intent in base/media.
// TODO Maybe the app needs to learn about Jicofo's intent and
// transfer that intent to lib-jitsi-meet instead of lib-jitsi-meet
// acting on Jicofo's intent without the app's knowledge.
dispatch(setAudioMuted(audioMuted));
dispatch(setVideoMuted(videoMuted));
// Remove the tracks from peerconnection as well.
for (const track of localTracks) {
const trackType = track.jitsiTrack.getType();
// Do not remove the audio track on RN. Starting with iOS 15 it will fail to unmute otherwise.
if ((audioMuted && trackType === MEDIA_TYPE.AUDIO && navigator.product !== 'ReactNative')
|| (videoMuted && trackType === MEDIA_TYPE.VIDEO)) {
dispatch(replaceLocalTrack(track.jitsiTrack, null, conference));
}
}
});
conference.on(
JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED,
(disableAudioMuteChange: boolean) => {
@@ -788,8 +808,10 @@ export function nonParticipantMessageReceived(id: string, json: Object) {
/**
* Updates the known state of start muted policies.
*
* @param {boolean} audioMuted - Whether or not members will join the conference as audio muted.
* @param {boolean} videoMuted - Whether or not members will join the conference as video muted.
* @param {boolean} audioMuted - Whether or not members will join the conference
* as audio muted.
* @param {boolean} videoMuted - Whether or not members will join the conference
* as video muted.
* @returns {{
* type: SET_START_MUTED_POLICY,
* startAudioMutedPolicy: boolean,
@@ -1000,8 +1022,10 @@ export function setRoom(room?: string) {
/**
* Sets whether or not members should join audio and/or video muted.
*
* @param {boolean} startAudioMuted - Whether or not members will join the conference as audio muted.
* @param {boolean} startVideoMuted - Whether or not members will join the conference as video muted.
* @param {boolean} startAudioMuted - Whether or not members will join the
* conference as audio muted.
* @param {boolean} startVideoMuted - Whether or not members will join the
* conference as video muted.
* @returns {Function}
*/
export function setStartMutedPolicy(
@@ -1013,6 +1037,9 @@ export function setStartMutedPolicy(
audio: startAudioMuted,
video: startVideoMuted
});
dispatch(
onStartMutedPolicyChanged(startAudioMuted, startVideoMuted));
};
}

View File

@@ -295,7 +295,7 @@ export function getVisitorOptions(stateful: IStateful, vnode: string, focusJid:
return {
hosts: config.oldConfig.hosts,
focusUserJid: focusJid,
disableLocalStatsBroadcast: false,
disableLocalStats: false,
bosh: config.oldConfig.bosh && appendURLParam(config.oldConfig.bosh, 'customusername', username),
p2p: config.oldConfig.p2p,
websocket: config.oldConfig.websocket
@@ -330,7 +330,7 @@ export function getVisitorOptions(stateful: IStateful, vnode: string, focusJid:
},
focusUserJid: focusJid,
disableFocus: true, // This flag disables sending the initial conference request
disableLocalStatsBroadcast: true,
disableLocalStats: true,
bosh: config.bosh && appendURLParam(config.bosh, 'vnode', vnode),
p2p: {
...config.p2p,

View File

@@ -70,7 +70,6 @@ import {
} from './functions';
import logger from './logger';
import { IConferenceMetadata } from './reducer';
import './subscriber';
/**
* Handler for before unload event.

View File

@@ -54,19 +54,6 @@ const DEFAULT_STATE = {
};
export interface IConferenceMetadata {
files: {
[fileId: string]: {
authorParticipantJid: string;
authorParticipantName: string;
conferenceFullName: string;
fileId: string;
fileName: string;
fileSize: number;
fileType: string;
progress?: number;
timestamp: number;
};
};
recording?: {
isTranscribingEnabled: boolean;
};
@@ -118,6 +105,8 @@ export interface IJitsiConference {
isLobbySupported: Function;
isP2PActive: Function;
isSIPCallingSupported: Function;
isStartAudioMuted: Function;
isStartVideoMuted: Function;
join: Function;
joinLobby: Function;
kickParticipant: Function;

View File

@@ -1,61 +0,0 @@
import { IStore } from '../../app/types';
import { showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { setAudioMuted, setVideoMuted } from '../media/actions';
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
let hasShownNotification = false;
/**
* Handles changes in the start muted policy for audio and video tracks in the meta data set for the conference.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/conference'].startAudioMutedPolicy,
/* listener */ (startAudioMutedPolicy, store) => {
_updateTrackMuteState(store, true);
});
StateListenerRegistry.register(
/* selector */ state => state['features/base/conference'].startVideoMutedPolicy,
/* listener */(startVideoMutedPolicy, store) => {
_updateTrackMuteState(store, false);
});
/**
* Updates the mute state of the track based on the start muted policy.
*
* @param {IStore} store - The redux store.
* @param {boolean} isAudio - Whether the track is audio or video.
* @returns {void}
*/
function _updateTrackMuteState(store: IStore, isAudio: boolean) {
const { dispatch, getState } = store;
const mutedPolicyKey = isAudio ? 'startAudioMutedPolicy' : 'startVideoMutedPolicy';
const mutedPolicyValue = getState()['features/base/conference'][mutedPolicyKey];
// Currently, the policy only supports force muting others, not unmuting them.
if (!mutedPolicyValue) {
return;
}
let muteStateUpdated = false;
const { muted } = isAudio ? getState()['features/base/media'].audio : getState()['features/base/media'].video;
if (isAudio && !Boolean(muted)) {
dispatch(setAudioMuted(mutedPolicyValue, true));
muteStateUpdated = true;
} else if (!isAudio && !Boolean(muted)) {
// TODO: Add a new authority for video mutism for the moderator case.
dispatch(setVideoMuted(mutedPolicyValue, VIDEO_MUTISM_AUTHORITY.USER, true));
muteStateUpdated = true;
}
if (!hasShownNotification && muteStateUpdated) {
hasShownNotification = true;
dispatch(showNotification({
titleKey: 'notify.mutedTitle',
descriptionKey: 'notify.muted'
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
}
}

View File

@@ -387,10 +387,6 @@ export interface IConfig {
feedbackPercentage?: number;
fileRecordingsServiceEnabled?: boolean;
fileRecordingsServiceSharingEnabled?: boolean;
fileSharing?: {
apiUrl?: string;
enabled?: boolean;
};
filmstrip?: {
disableResizable?: boolean;
disableStageFilmstrip?: boolean;
@@ -442,7 +438,6 @@ export interface IConfig {
};
iAmRecorder?: boolean;
iAmSipGateway?: boolean;
iAmSpot?: boolean;
ignoreStartMuted?: boolean;
inviteAppName?: string | null;
inviteServiceCallFlowsUrl?: string;
@@ -489,7 +484,6 @@ export interface IConfig {
long?: number;
medium?: number;
short?: number;
sticky?: number;
};
notifications?: Array<string>;
notifyOnConferenceDestruction?: boolean;
@@ -548,7 +542,6 @@ export interface IConfig {
};
recordingSharingUrl?: string;
recordings?: {
consentLearnMoreLink?: string;
recordAudioAndVideo?: boolean;
requireConsent?: boolean;
showPrejoinWarning?: boolean;

View File

@@ -150,7 +150,6 @@ export default [
'enableTcc',
'faceLandmarks',
'feedbackPercentage',
'fileSharing.enabled',
'filmstrip',
'flags',
'forceTurnRelay',
@@ -170,7 +169,6 @@ export default [
'hideLobbyButton',
'iAmRecorder',
'iAmSipGateway',
'iAmSpot',
'ignoreStartMuted',
'inviteAppName',
'liveStreaming.enabled',

View File

@@ -1,14 +1,10 @@
import { appNavigate } from '../../app/actions.native';
import { IStore } from '../../app/types';
import { getCustomerDetails } from '../../jaas/actions.any';
import { isVpaasMeeting, getJaasJWT } from '../../jaas/functions';
import { navigateRoot } from '../../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../../mobile/navigation/routes';
import { setJWT } from '../jwt/actions';
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
import { _connectInternal } from './actions.native';
import logger from './logger';
import { _connectInternal } from './actions.any';
export * from './actions.any';
@@ -20,34 +16,12 @@ export * from './actions.any';
* @returns {Function}
*/
export function connect(id?: string, password?: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { jwt } = state['features/base/jwt'];
if (isVpaasMeeting(state)) {
return dispatch(getCustomerDetails())
.then(() => {
if (!jwt) {
return getJaasJWT(state);
}
})
.then(j => {
j && dispatch(setJWT(j));
return dispatch(_connectInternal(id, password));
}).catch(e => {
logger.error('Connection error', e);
});
}
dispatch(_connectInternal(id, password))
return (dispatch: IStore['dispatch']) => dispatch(_connectInternal(id, password))
.catch(error => {
if (error === JitsiConnectionErrors.NOT_LIVE_ERROR) {
navigateRoot(screen.visitorsQueue);
}
});
};
}
/**

View File

@@ -11,7 +11,6 @@ import LocalRecordingManager from '../../recording/components/Recording/LocalRec
import { setJWT } from '../jwt/actions';
import { _connectInternal } from './actions.any';
import logger from './logger';
export * from './actions.any';
@@ -39,8 +38,6 @@ export function connect(id?: string, password?: string) {
j && dispatch(setJWT(j));
return dispatch(_connectInternal(id, password));
}).catch(e => {
logger.error('Connection error', e);
});
}

View File

@@ -150,7 +150,8 @@ export function getAvailableDevices() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => new Promise(resolve => {
const { mediaDevices } = JitsiMeetJS;
if (mediaDevices.isDeviceChangeAvailable()) {
if (mediaDevices.isDeviceListAvailable()
&& mediaDevices.isDeviceChangeAvailable()) {
mediaDevices.enumerateDevices((devices: MediaDeviceInfo[]) => {
const { filteredDevices, ignoredDevices } = filterIgnoredDevices(devices);
const oldDevices = flattenAvailableDevices(getState()['features/base/devices'].availableDevices);

View File

@@ -40,7 +40,6 @@ export default class AbstractDialog<P extends IProps, S extends IState = IState>
super(props);
// Bind event handlers so they are only bound once per instance.
this._onBack = this._onBack.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._onSubmitFulfilled = this._onSubmitFulfilled.bind(this);
@@ -76,14 +75,6 @@ export default class AbstractDialog<P extends IProps, S extends IState = IState>
return this.props.dispatch(hideDialog());
}
_onBack() {
const { backDisabled = false, onBack } = this.props;
if (!backDisabled && (!onBack || onBack())) {
this._hide();
}
}
/**
* Dispatches a redux action to hide this dialog when it's canceled.
*

View File

@@ -16,11 +16,6 @@ import styles from './styles';
*/
interface IProps extends AbstractProps, WithTranslation {
/**
* The i18n key of the text label for the back button.
*/
backLabel?: string;
/**
* The i18n key of the text label for the cancel button.
*/
@@ -41,11 +36,6 @@ interface IProps extends AbstractProps, WithTranslation {
*/
descriptionKey?: string | { key: string; params: string; };
/**
* Whether the back button is hidden.
*/
isBackHidden?: Boolean;
/**
* Whether the cancel button is hidden.
*/
@@ -65,11 +55,6 @@ interface IProps extends AbstractProps, WithTranslation {
* Dialog title.
*/
title?: string;
/**
* Renders buttons vertically.
*/
verticalButtons?: boolean;
}
/**
@@ -117,17 +102,14 @@ class ConfirmDialog extends AbstractDialog<IProps> {
*/
override render() {
const {
backLabel,
cancelLabel,
children,
confirmLabel,
isBackHidden = true,
isCancelHidden,
isConfirmDestructive,
isConfirmHidden,
t,
title,
verticalButtons
title
} = this.props;
const dialogButtonStyle
@@ -137,7 +119,6 @@ class ConfirmDialog extends AbstractDialog<IProps> {
return (
<Dialog.Container
coverScreen = { false }
verticalButtons = { verticalButtons }
visible = { true }>
{
title && <Dialog.Title>
@@ -146,12 +127,6 @@ class ConfirmDialog extends AbstractDialog<IProps> {
}
{ this._renderDescription() }
{ children }
{
!isBackHidden && <Dialog.Button
label = { t(backLabel || 'dialog.confirmBack') }
onPress = { this._onBack }
style = { styles.dialogButton } />
}
{
!isCancelHidden && <Dialog.Button
label = { t(cancelLabel || 'dialog.confirmNo') }

View File

@@ -2,16 +2,6 @@ import { ReactNode } from 'react';
export type DialogProps = {
/**
* Whether back button is disabled. Enabled by default.
*/
backDisabled?: boolean;
/**
* Optional i18n key to change the back button title.
*/
backKey?: string;
/**
* Whether cancel button is disabled. Enabled by default.
*/
@@ -37,11 +27,6 @@ export type DialogProps = {
*/
okKey?: string;
/**
* The handler for onBack event.
*/
onBack?: Function;
/**
* The handler for onCancel event.
*/

View File

@@ -72,11 +72,6 @@ interface IProps extends IIconProps {
*/
id?: string;
/**
* On click handler.
*/
onClick?: (e?: any) => void;
/**
* Keydown handler.
*/

View File

@@ -88,11 +88,6 @@ interface IProps {
*/
position: string;
/**
* The ARIA role.
*/
role?: string;
/**
* Whether the trigger for open/ close should be click or hover.
*/
@@ -488,24 +483,20 @@ class Popover extends Component<IProps, IState> {
* @returns {ReactElement}
*/
_renderContent() {
const { content, position, trigger, headingId, headingLabel, role = 'dialog' } = this.props;
const isFocusLockEnabled = this.state.enableFocusLock;
const isDialogRole = role === 'dialog';
const { content, position, trigger, headingId, headingLabel } = this.props;
return (
<div className = { `popover ${trigger}` }>
<div
className = { `popover-content ${position.split('-')[0]}` }
data-autofocus = { isFocusLockEnabled }
data-autofocus = { this.state.enableFocusLock }
onKeyDown = { this._onEscKey }
{ ...(isFocusLockEnabled && {
role,
tabIndex: -1
}) }
{ ...(isFocusLockEnabled && isDialogRole && {
{ ...(this.state.enableFocusLock && {
'aria-modal': true,
'aria-label': !headingId && headingLabel ? headingLabel : undefined,
'aria-labelledby': headingId ? headingId : undefined
'aria-labelledby': headingId,
role: 'dialog',
tabIndex: -1
}) }>
{ content }
</div>

View File

@@ -2,7 +2,8 @@ import { batch } from 'react-redux';
import { IStore } from '../../app/types';
import { CHAT_SIZE } from '../../chat/constants';
import { getParticipantsPaneWidth } from '../../participants-pane/functions';
import { getParticipantsPaneOpen } from '../../participants-pane/functions';
import theme from '../components/themes/participantsPaneTheme.json';
import {
CLIENT_RESIZED,
@@ -42,23 +43,25 @@ export function clientResized(clientWidth: number, clientHeight: number) {
if (navigator.product !== 'ReactNative') {
const state = getState();
const { isOpen: isChatOpen, width } = state['features/chat'];
const { isOpen: isChatOpen } = state['features/chat'];
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
if (isChatOpen) {
availableWidth -= width?.current ?? CHAT_SIZE;
availableWidth -= CHAT_SIZE;
}
availableWidth -= getParticipantsPaneWidth(state);
if (isParticipantsPaneOpen) {
availableWidth -= theme.participantsPaneWidth;
}
}
batch(() => {
dispatch({
type: CLIENT_RESIZED,
clientHeight,
clientWidth,
videoSpaceWidth: availableWidth
clientWidth: availableWidth
});
dispatch(setAspectRatio(availableWidth, clientHeight));
dispatch(setAspectRatio(clientWidth, clientHeight));
});
};
}

View File

@@ -18,11 +18,3 @@ export const ASPECT_RATIO_WIDE = Symbol('ASPECT_RATIO_WIDE');
* Smallest supported mobile width.
*/
export const SMALL_MOBILE_WIDTH = '320';
/**
* The width for desktop that we start hiding elements from the UI (video quality label, filmstrip, etc).
* This should match the value for $verySmallScreen in _variables.scss.
*
* @type {number}
*/
export const SMALL_DESKTOP_WIDTH = 500;

View File

@@ -1,20 +0,0 @@
import { IStateful } from '../app/types';
import { isMobileBrowser } from '../environment/utils';
import { toState } from '../redux/functions';
import { SMALL_DESKTOP_WIDTH } from './constants';
/**
* Determines if the screen is narrow with the chat panel open. If the function returns true video quality label,
* filmstrip, etc will be hidden.
*
* @param {IStateful} stateful - The stateful object representing the application state.
* @returns {boolean} - True if the screen is narrow with the chat panel open, otherwise `false`.
*/
export function isNarrowScreenWithChatOpen(stateful: IStateful) {
const state = toState(stateful);
const isDesktopBrowser = !isMobileBrowser();
const { isOpen, width } = state['features/chat'];
const { clientWidth } = state['features/base/responsive-ui'];
return isDesktopBrowser && isOpen && (width?.current + SMALL_DESKTOP_WIDTH) > clientWidth;
}

View File

@@ -29,9 +29,9 @@ MiddlewareRegistry.register(store => next => action => {
break;
case CONFERENCE_JOINED: {
const { clientHeight = 0, clientWidth = 0, videoSpaceWidth = 0 } = store.getState()['features/base/responsive-ui'];
const { clientHeight = 0, clientWidth = 0 } = store.getState()['features/base/responsive-ui'];
if (!clientHeight && !clientWidth && !videoSpaceWidth) {
if (!clientHeight && !clientWidth) {
const {
innerHeight,
innerWidth

View File

@@ -25,8 +25,7 @@ const DEFAULT_STATE = {
clientWidth: innerWidth,
isNarrowLayout: false,
reducedUI: false,
contextMenuOpened: false,
videoSpaceWidth: innerWidth
contextMenuOpened: false
};
export interface IResponsiveUIState {
@@ -42,7 +41,6 @@ export interface IResponsiveUIState {
right: number;
top: number;
};
videoSpaceWidth: number;
}
ReducerRegistry.register<IResponsiveUIState>('features/base/responsive-ui',
@@ -52,8 +50,7 @@ ReducerRegistry.register<IResponsiveUIState>('features/base/responsive-ui',
return {
...state,
clientWidth: action.clientWidth,
clientHeight: action.clientHeight,
videoSpaceWidth: action.videoSpaceWidth
clientHeight: action.clientHeight
};
}

View File

@@ -1,5 +1,7 @@
import { IStore } from '../../app/types';
import { PREJOIN_INITIALIZED } from '../../prejoin/actionTypes';
import { setPrejoinPageVisibility } from '../../prejoin/actions';
import { APP_WILL_MOUNT } from '../app/actionTypes';
import { getJwtName } from '../jwt/functions';
import { MEDIA_TYPE } from '../media/constants';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
@@ -24,6 +26,9 @@ MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case APP_WILL_MOUNT:
_initializeShowPrejoin(store);
break;
case PREJOIN_INITIALIZED:
_maybeUpdateDisplayName(store);
break;
@@ -35,6 +40,21 @@ MiddlewareRegistry.register(store => next => action => {
return result;
});
/**
* Overwrites the showPrejoin flag based on cached used selection for showing prejoin screen.
*
* @param {Store} store - The redux store.
* @private
* @returns {void}
*/
function _initializeShowPrejoin({ dispatch, getState }: IStore) {
const { userSelectedSkipPrejoin } = getState()['features/base/settings'];
if (userSelectedSkipPrejoin) {
dispatch(setPrejoinPageVisibility(false));
}
}
/**
* Updates the display name to the one in JWT if there is one.
*

View File

@@ -48,7 +48,8 @@ const DEFAULT_STATE: ISettingsState = {
userSelectedNotifications: {
'notify.chatMessages': true
},
userSelectedMicDeviceLabel: undefined
userSelectedMicDeviceLabel: undefined,
userSelectedSkipPrejoin: undefined
};
export interface ISettingsState {
@@ -87,6 +88,7 @@ export interface ISettingsState {
userSelectedNotifications?: {
[key: string]: boolean;
};
userSelectedSkipPrejoin?: boolean;
videoSettingsVisible?: boolean;
visible?: boolean;
}

View File

@@ -118,8 +118,8 @@ export const colorMap = {
export const font = {
weightRegular: '400',
weightSemiBold: '600'
weightRegular: 400,
weightSemiBold: 600
};
export const shape = {
@@ -128,7 +128,8 @@ export const shape = {
boxShadow: 'inset 0px -1px 0px rgba(255, 255, 255, 0.15)'
};
export const spacing = [ 0, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 ];
export const spacing
= [ '0rem', '0.25rem', '0.5rem', '1rem', '1.5rem', '2rem', '2.5rem', '3rem', '3.5rem', '4rem', '4.5rem', '5rem', '5.5rem', '6rem', '6.5rem', '7rem', '7.5rem', '8rem' ];
export const typography = {
labelRegular: 'label01',
@@ -136,64 +137,64 @@ export const typography = {
labelBold: 'labelBold01',
bodyShortRegularSmall: {
fontSize: 10,
lineHeight: 16,
fontSize: '0.625rem',
lineHeight: '1rem',
fontWeight: font.weightRegular,
letterSpacing: 0
},
bodyShortRegular: {
fontSize: 14,
lineHeight: 20,
fontSize: '0.875rem',
lineHeight: '1.25rem',
fontWeight: font.weightRegular,
letterSpacing: 0
},
bodyShortBold: {
fontSize: 14,
lineHeight: 20,
fontSize: '0.875rem',
lineHeight: '1.25rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
bodyShortRegularLarge: {
fontSize: 16,
lineHeight: 22,
fontSize: '1rem',
lineHeight: '1.375rem',
fontWeight: font.weightRegular,
letterSpacing: 0
},
bodyShortBoldLarge: {
fontSize: 16,
lineHeight: 22,
fontSize: '1rem',
lineHeight: '1.375rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
bodyLongRegular: {
fontSize: 14,
lineHeight: 24,
fontSize: '0.875rem',
lineHeight: '1.5rem',
fontWeight: font.weightRegular,
letterSpacing: 0
},
bodyLongRegularLarge: {
fontSize: 16,
lineHeight: 26,
fontSize: '1rem',
lineHeight: '1.625rem',
fontWeight: font.weightRegular,
letterSpacing: 0
},
bodyLongBold: {
fontSize: 14,
lineHeight: 24,
fontSize: '0.875rem',
lineHeight: '1.5rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
bodyLongBoldLarge: {
fontSize: 16,
lineHeight: 26,
fontSize: '1rem',
lineHeight: '1.625rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
@@ -203,29 +204,29 @@ export const typography = {
heading2: 'heading02',
heading3: {
fontSize: 32,
lineHeight: 40,
fontSize: '2rem',
lineHeight: '2.5rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
heading4: {
fontSize: 28,
lineHeight: 36,
fontSize: '1.75rem',
lineHeight: '2.25rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
heading5: {
fontSize: 20,
lineHeight: 28,
fontSize: '1.25rem',
lineHeight: '1.75rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
},
heading6: {
fontSize: 16,
lineHeight: 26,
fontSize: '1rem',
lineHeight: '1.625rem',
fontWeight: font.weightSemiBold,
letterSpacing: 0
}

View File

@@ -1,3 +1,5 @@
import { GestureResponderEvent } from 'react-native';
import { BUTTON_TYPES } from '../constants.any';
export interface IButtonProps {
@@ -30,7 +32,7 @@ export interface IButtonProps {
/**
* Click callback.
*/
onClick?: (e?: any) => void;
onClick?: (e?: React.MouseEvent<HTMLButtonElement> | GestureResponderEvent) => void;
/**
* Key press callback.

View File

@@ -213,7 +213,7 @@ const ContextMenu = ({
if (offsetTop + height > offsetHeight + scrollTop && height > offsetTop) {
// top offset and + padding + border
container.style.maxHeight = `${offsetTop - ((spacing[2] * 2) + 2)}px`;
container.style.maxHeight = `calc(${offsetTop}px - (${spacing[2]} * 2 + 2px))`;
}
// get the height after style changes

View File

@@ -224,7 +224,7 @@ const ContextMenuItem = ({
tabIndex = selected ? 0 : -1;
}
if ((role === 'button' || role === 'menuitem') && !disabled) {
if (role === 'button' && !disabled) {
tabIndex = 0;
}

View File

@@ -15,11 +15,6 @@ interface IProps {
* The children of the component.
*/
children?: ReactNode;
/**
* The optional role of the component.
*/
role?: string;
}
const useStyles = makeStyles()(theme => {
@@ -46,15 +41,12 @@ const useStyles = makeStyles()(theme => {
const ContextMenuItemGroup = ({
actions,
children,
...rest
children
}: IProps) => {
const { classes: styles } = useStyles();
return (
<div
className = { styles.contextMenuItemGroup }
{ ...rest }>
<div className = { styles.contextMenuItemGroup }>
{children}
{actions?.map(actionProps => (
<ContextMenuItem

View File

@@ -173,16 +173,16 @@ const DialogWithTabs = ({
const [ selectedTab, setSelectedTab ] = useState<string | undefined>(defaultTab ?? tabs[0].name);
const [ userSelected, setUserSelected ] = useState(false);
const [ tabStates, setTabStates ] = useState(tabs.map(tab => tab.props));
const videoSpaceWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].videoSpaceWidth);
const clientWidth = useSelector((state: IReduxState) => state['features/base/responsive-ui'].clientWidth);
const [ isMobile, setIsMobile ] = useState(false);
useEffect(() => {
if (videoSpaceWidth <= MOBILE_BREAKPOINT) {
if (clientWidth <= MOBILE_BREAKPOINT) {
!isMobile && setIsMobile(true);
} else {
isMobile && setIsMobile(false);
}
}, [ videoSpaceWidth, isMobile ]);
}, [ clientWidth, isMobile ]);
useEffect(() => {
if (isMobile) {

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useEffect } from 'react';
import { makeStyles } from 'tss-react/mui';
import { isMobileBrowser } from '../../../environment/utils';
import Icon from '../../../icons/components/Icon';
import { withPixelLineHeight } from '../../../styles/functions.web';
interface ITabProps {
@@ -15,9 +14,8 @@ interface ITabProps {
controlsId: string;
countBadge?: number;
disabled?: boolean;
icon?: Function;
id: string;
label?: string;
label: string;
}>;
}
@@ -76,10 +74,6 @@ const useStyles = makeStyles()(theme => {
borderRadius: '100%',
backgroundColor: theme.palette.warning01,
marginLeft: theme.spacing(2)
},
icon: {
marginRight: theme.spacing(1)
}
};
});
@@ -140,9 +134,6 @@ const Tabs = ({
onKeyDown = { onKeyDown(index) }
role = 'tab'
tabIndex = { selected === tab.id ? undefined : -1 }>
{tab.icon && <Icon
className = { classes.icon }
src = { tab.icon } />}
{tab.label}
{tab.countBadge && <span className = { classes.badge }>{tab.countBadge}</span>}
</button>

View File

@@ -2,6 +2,47 @@ import { DefaultTheme } from 'react-native-paper';
import { createColorTokens } from './utils';
// Base font size in pixels (standard is 16px = 1rem)
const BASE_FONT_SIZE = 16;
/**
* Converts rem to pixels.
*
* @param {string} remValue - The value in rem units (e.g. '0.875rem').
* @returns {number}
*/
function remToPixels(remValue: string): number {
const numericValue = parseFloat(remValue.replace('rem', ''));
return Math.round(numericValue * BASE_FONT_SIZE);
}
/**
* Converts all rem to pixels in an object.
*
* @param {Object} obj - The object to convert rem values in.
* @returns {Object}
*/
function convertRemValues(obj: any): any {
const converted: { [key: string]: any; } = {};
if (typeof obj !== 'object' || obj === null) {
return obj;
}
Object.entries(obj).forEach(([ key, value ]) => {
if (typeof value === 'string' && value.includes('rem')) {
converted[key] = remToPixels(value);
} else if (typeof value === 'object' && value !== null) {
converted[key] = convertRemValues(value);
} else {
converted[key] = value;
}
});
return converted;
}
/**
* Creates a React Native Paper theme based on local UI tokens.
*
@@ -13,10 +54,10 @@ export function createNativeTheme({ font, colorMap, shape, spacing, typography }
...DefaultTheme,
palette: createColorTokens(colorMap),
shape,
spacing,
spacing: spacing.map(remToPixels),
typography: {
font,
...typography
...convertRemValues(typography)
}
};
}

View File

@@ -3,7 +3,7 @@ import { Theme, adaptV4Theme, createTheme } from '@mui/material/styles';
import { ITypography, IPalette as Palette1 } from '../ui/types';
import { createColorTokens, createTypographyTokens } from './utils';
import { createColorTokens } from './utils';
declare module '@mui/material/styles' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -18,7 +18,7 @@ interface ThemeProps {
colorMap: Object;
font: Object;
shape: Object;
spacing: Array<number>;
spacing: Array<number | string>;
typography: Object;
}
@@ -36,7 +36,7 @@ export function createWebTheme({ font, colorMap, shape, spacing, typography, bre
typography: {
// @ts-ignore
font,
...createTypographyTokens(typography)
...typography
},
breakpoints
}));

View File

@@ -1,6 +1,6 @@
interface ITypographyType {
fontSize: number;
fontWeight: number; // TODO: revisit this.
fontWeight: string;
letterSpacing: number;
lineHeight: number;
}

View File

@@ -19,24 +19,3 @@ export function createColorTokens(colorMap: Object): any {
return Object.assign(result, { [token]: color });
}, {});
}
/**
* Create the typography tokens based on the typography theme and the association map.
*
* @param {Object} typography - A map between the token name and the actual typography value.
* @returns {Object}
*/
export function createTypographyTokens(typography: Object): any {
const allTokens = merge({}, tokens, jitsiTokens);
return Object.entries(typography)
.reduce((result, [ token, value ]: [any, any]) => {
let typographyValue = value;
if (typeof value === 'string') {
typographyValue = allTokens[value as keyof typeof allTokens] || value;
}
return Object.assign(result, { [token]: typographyValue });
}, {});
}

View File

@@ -1,16 +1,9 @@
import { IReduxState } from '../../app/types';
/**
* Checks if Jitsi Meet is running on Spot TV.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} Whether or not Jitsi Meet is running on Spot TV.
*/
export function isSpotTV(state: IReduxState): boolean {
const { defaultLocalDisplayName, iAmSpot } = state['features/base/config'] || {};
return iAmSpot
|| navigator.userAgent.includes('JitsiSpot/') // Jitsi Spot app
|| navigator.userAgent.includes('8x8MeetingRooms/') // 8x8 Meeting Rooms app
|| defaultLocalDisplayName === 'Meeting Room';
export function isSpotTV(): boolean {
return navigator.userAgent.includes('SpotElectron/');
}

View File

@@ -115,51 +115,23 @@ export const SET_FOCUSED_TAB = 'SET_FOCUSED_TAB';
* type: SET_LOBBY_CHAT_RECIPIENT
* }
*/
export const SET_LOBBY_CHAT_RECIPIENT = 'SET_LOBBY_CHAT_RECIPIENT';
export const SET_LOBBY_CHAT_RECIPIENT = 'SET_LOBBY_CHAT_RECIPIENT';
/**
* The type of action sets the state of lobby messaging status.
*
* {
* type: SET_LOBBY_CHAT_ACTIVE_STATE
* payload: boolean
* }
*/
export const SET_LOBBY_CHAT_ACTIVE_STATE = 'SET_LOBBY_CHAT_ACTIVE_STATE';
/**
* The type of action sets the state of lobby messaging status.
*
* {
* type: SET_LOBBY_CHAT_ACTIVE_STATE
* payload: boolean
* }
*/
export const SET_LOBBY_CHAT_ACTIVE_STATE = 'SET_LOBBY_CHAT_ACTIVE_STATE';
/**
* The type of action removes the lobby messaging from participant.
*
* {
* type: REMOVE_LOBBY_CHAT_PARTICIPANT
* }
*/
export const REMOVE_LOBBY_CHAT_PARTICIPANT = 'REMOVE_LOBBY_CHAT_PARTICIPANT';
/**
* The type of action which signals to set the width of the chat panel.
*
* {
* type: SET_CHAT_WIDTH,
* width: number
* }
*/
export const SET_CHAT_WIDTH = 'SET_CHAT_WIDTH';
/**
* The type of action which sets the width for the chat panel (user resized).
* {
* type: SET_USER_CHAT_WIDTH,
* width: number
* }
*/
export const SET_USER_CHAT_WIDTH = 'SET_USER_CHAT_WIDTH';
/**
* The type of action which sets whether the user is resizing the chat panel or not.
* {
* type: SET_CHAT_IS_RESIZING,
* resizing: boolean
* }
*/
export const SET_CHAT_IS_RESIZING = 'SET_CHAT_IS_RESIZING';
/**
* The type of action removes the lobby messaging from participant.
*
* {
* type: REMOVE_LOBBY_CHAT_PARTICIPANT
* }
*/
export const REMOVE_LOBBY_CHAT_PARTICIPANT = 'REMOVE_LOBBY_CHAT_PARTICIPANT';

View File

@@ -2,12 +2,7 @@
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import { IStore } from '../app/types';
import {
OPEN_CHAT,
SET_CHAT_IS_RESIZING,
SET_CHAT_WIDTH,
SET_USER_CHAT_WIDTH
} from './actionTypes';
import { OPEN_CHAT } from './actionTypes';
import { closeChat } from './actions.any';
export * from './actions.any';
@@ -50,48 +45,3 @@ export function toggleChat() {
VideoLayout.onResize();
};
}
/**
* Sets the chat panel's width.
*
* @param {number} width - The new width of the chat panel.
* @returns {{
* type: SET_CHAT_WIDTH,
* width: number
* }}
*/
export function setChatWidth(width: number) {
return {
type: SET_CHAT_WIDTH,
width
};
}
/**
* Sets the chat panel's width and the user preferred width.
*
* @param {number} width - The new width of the chat panel.
* @returns {{
* type: SET_USER_CHAT_WIDTH,
* width: number
* }}
*/
export function setUserChatWidth(width: number) {
return {
type: SET_USER_CHAT_WIDTH,
width
};
}
/**
* Sets whether the user is resizing the chat panel or not.
*
* @param {boolean} resizing - Whether the user is resizing or not.
* @returns {Object}
*/
export function setChatIsResizing(resizing: boolean) {
return {
type: SET_CHAT_IS_RESIZING,
resizing
};
}

View File

@@ -1,21 +1,18 @@
import React, { useCallback, useEffect, useState } from 'react';
import { connect, useSelector } from 'react-redux';
import React, { useCallback } from 'react';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { throttle } from 'lodash-es';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { getLocalParticipant } from '../../../base/participants/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Tabs from '../../../base/ui/components/web/Tabs';
import { arePollsDisabled } from '../../../conference/functions.any';
import FileSharing from '../../../file-sharing/components/web/FileSharing';
import { isFileSharingEnabled } from '../../../file-sharing/functions.any';
import PollsPane from '../../../polls/components/web/PollsPane';
import { isCCTabEnabled } from '../../../subtitles/functions.any';
import { setChatIsResizing, setUserChatWidth, sendMessage, setFocusedTab, toggleChat } from '../../actions.web';
import { sendMessage, setFocusedTab, toggleChat } from '../../actions.web';
import { CHAT_SIZE, ChatTabs, SMALL_WIDTH_THRESHOLD } from '../../constants';
import { IChatProps as AbstractProps } from '../../types';
import { IconMessage, IconInfo, IconSubtitles, IconShareDoc } from '../../../base/icons/svg';
import ChatHeader from './ChatHeader';
import ChatInput from './ChatInput';
@@ -24,7 +21,6 @@ import DisplayNameForm from './DisplayNameForm';
import KeyboardAvoider from './KeyboardAvoider';
import MessageContainer from './MessageContainer';
import MessageRecipient from './MessageRecipient';
import { getChatMaxSize } from '../../functions';
interface IProps extends AbstractProps {
@@ -38,11 +34,6 @@ interface IProps extends AbstractProps {
*/
_isCCTabEnabled: boolean;
/**
* True if file sharing tab is enabled.
*/
_isFileSharingTabEnabled: boolean;
/**
* Whether the chat is opened in a modal or not (computed based on window width).
*/
@@ -58,11 +49,6 @@ interface IProps extends AbstractProps {
*/
_isPollsEnabled: boolean;
/**
* Whether the user is currently resizing the chat panel.
*/
_isResizing: boolean;
/**
* Number of unread poll messages.
*/
@@ -98,30 +84,19 @@ interface IProps extends AbstractProps {
* Whether or not to block chat access with a nickname input form.
*/
_showNamePrompt: boolean;
/**
* The current width of the chat panel.
*/
_width: number;
}
const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme, { _isResizing, width }) => {
const useStyles = makeStyles()(theme => {
return {
container: {
backgroundColor: theme.palette.ui01,
flexShrink: 0,
overflow: 'hidden',
position: 'relative',
transition: _isResizing ? undefined : 'width .16s ease-in-out',
width: `${width}px`,
transition: 'width .16s ease-in-out',
width: `${CHAT_SIZE}px`,
zIndex: 300,
'&:hover, &:focus-within': {
'& .dragHandleContainer': {
visibility: 'visible'
}
},
'@media (max-width: 580px)': {
height: '100dvh',
position: 'fixed',
@@ -148,8 +123,7 @@ const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme,
alignItems: 'center',
boxSizing: 'border-box',
color: theme.palette.text01,
...theme.typography.heading6,
fontWeight: theme.typography.heading6.fontWeight as any,
...withPixelLineHeight(theme.typography.heading6),
'.jitsi-icon': {
cursor: 'pointer'
@@ -172,48 +146,6 @@ const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme,
pollsPanel: {
// extract header + tabs height
height: 'calc(100% - 110px)'
},
resizableChat: {
flex: 1,
display: 'flex',
flexDirection: 'column',
width: '100%'
},
dragHandleContainer: {
height: '100%',
width: '9px',
backgroundColor: 'transparent',
position: 'absolute',
cursor: 'col-resize',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
visibility: 'hidden',
right: '4px',
top: 0,
'&:hover': {
'& .dragHandle': {
backgroundColor: theme.palette.icon01
}
},
'&.visible': {
visibility: 'visible',
'& .dragHandle': {
backgroundColor: theme.palette.icon01
}
}
},
dragHandle: {
backgroundColor: theme.palette.icon02,
height: '100px',
width: '3px',
borderRadius: '1px'
}
};
});
@@ -223,9 +155,7 @@ const Chat = ({
_isOpen,
_isPollsEnabled,
_isCCTabEnabled,
_isFileSharingTabEnabled,
_focusedTab,
_isResizing,
_messages,
_nbUnreadMessages,
_nbUnreadPolls,
@@ -234,100 +164,10 @@ const Chat = ({
_onToggleChatTab,
_onTogglePollsTab,
_showNamePrompt,
_width,
dispatch,
t
}: IProps) => {
const { classes, cx } = useStyles({ _isResizing, width: _width });
const [ isMouseDown, setIsMouseDown ] = useState(false);
const [ mousePosition, setMousePosition ] = useState<number | null>(null);
const [ dragChatWidth, setDragChatWidth ] = useState<number | null>(null);
const maxChatWidth = useSelector(getChatMaxSize);
/**
* Handles mouse down on the drag handle.
*
* @param {MouseEvent} e - The mouse down event.
* @returns {void}
*/
const onDragHandleMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
// Store the initial mouse position and chat width
setIsMouseDown(true);
setMousePosition(e.clientX);
setDragChatWidth(_width);
// Indicate that resizing is in progress
dispatch(setChatIsResizing(true));
// Add visual feedback that we're dragging
document.body.style.cursor = 'col-resize';
// Disable text selection during resize
document.body.style.userSelect = 'none';
console.log('Chat resize: Mouse down', { clientX: e.clientX, initialWidth: _width });
}, [ _width, dispatch ]);
/**
* Drag handle mouse up handler.
*
* @returns {void}
*/
const onDragMouseUp = useCallback(() => {
if (isMouseDown) {
setIsMouseDown(false);
dispatch(setChatIsResizing(false));
// Restore cursor and text selection
document.body.style.cursor = '';
document.body.style.userSelect = '';
console.log('Chat resize: Mouse up');
}
}, [ isMouseDown, dispatch ]);
/**
* Handles drag handle mouse move.
*
* @param {MouseEvent} e - The mousemove event.
* @returns {void}
*/
const onChatResize = useCallback(throttle((e: MouseEvent) => {
// console.log('Chat resize: Mouse move', { clientX: e.clientX, isMouseDown, mousePosition, _width });
if (isMouseDown && mousePosition !== null && dragChatWidth !== null) {
// For chat panel resizing on the left edge:
// - Dragging left (decreasing X coordinate) should make the panel wider
// - Dragging right (increasing X coordinate) should make the panel narrower
const diff = e.clientX - mousePosition;
const newWidth = Math.max(
Math.min(dragChatWidth + diff, maxChatWidth),
CHAT_SIZE
);
// Update the width only if it has changed
if (newWidth !== _width) {
dispatch(setUserChatWidth(newWidth));
}
}
}, 50, {
leading: true,
trailing: false
}), [ isMouseDown, mousePosition, dragChatWidth, _width, maxChatWidth, dispatch ]);
// Set up event listeners when component mounts
useEffect(() => {
document.addEventListener('mouseup', onDragMouseUp);
document.addEventListener('mousemove', onChatResize);
return () => {
document.removeEventListener('mouseup', onDragMouseUp);
document.removeEventListener('mousemove', onChatResize);
};
}, [ onDragMouseUp, onChatResize ]);
const { classes, cx } = useStyles();
/**
* Sends a text message.
@@ -389,10 +229,7 @@ const Chat = ({
aria-labelledby = { ChatTabs.CHAT }
className = { cx(
classes.chatPanel,
!_isPollsEnabled
&& !_isCCTabEnabled
&& !_isFileSharingTabEnabled
&& classes.chatPanelNoTabs,
!_isPollsEnabled && !_isCCTabEnabled && classes.chatPanelNoTabs,
_focusedTab !== ChatTabs.CHAT && 'hide'
) }
id = { `${ChatTabs.CHAT}-panel` }
@@ -425,14 +262,6 @@ const Chat = ({
tabIndex = { 2 }>
<ClosedCaptionsTab />
</div> }
{ _isFileSharingTabEnabled && <div
aria-labelledby = { ChatTabs.FILE_SHARING }
className = { cx(classes.chatPanel, _focusedTab !== ChatTabs.FILE_SHARING && 'hide') }
id = { `${ChatTabs.FILE_SHARING}-panel` }
role = 'tabpanel'
tabIndex = { 3 }>
<FileSharing />
</div> }
</>
);
}
@@ -452,7 +281,7 @@ const Chat = ({
_focusedTab !== ChatTabs.CHAT && _nbUnreadMessages > 0 ? _nbUnreadMessages : undefined,
id: ChatTabs.CHAT,
controlsId: `${ChatTabs.CHAT}-panel`,
icon: IconMessage
label: t('chat.tabs.chat')
}
];
@@ -462,7 +291,7 @@ const Chat = ({
countBadge: _focusedTab !== ChatTabs.POLLS && _nbUnreadPolls > 0 ? _nbUnreadPolls : undefined,
id: ChatTabs.POLLS,
controlsId: `${ChatTabs.POLLS}-panel`,
icon: IconInfo
label: t('chat.tabs.polls')
});
}
@@ -472,32 +301,13 @@ const Chat = ({
countBadge: undefined,
id: ChatTabs.CLOSED_CAPTIONS,
controlsId: `${ChatTabs.CLOSED_CAPTIONS}-panel`,
icon: IconSubtitles
});
}
if (_isFileSharingTabEnabled) {
tabs.push({
accessibilityLabel: t('chat.tabs.fileSharing'),
countBadge: undefined,
id: ChatTabs.FILE_SHARING,
controlsId: `${ChatTabs.FILE_SHARING}-panel`,
icon: IconShareDoc
label: t('chat.tabs.closedCaptions')
});
}
return (
<Tabs
accessibilityLabel = { _isPollsEnabled || _isCCTabEnabled || _isFileSharingTabEnabled
? t('chat.titleWithFeatures', {
features: [
_isPollsEnabled ? t('chat.titleWithPolls') : '',
_isCCTabEnabled ? t('chat.titleWithCC') : '',
_isFileSharingTabEnabled ? t('chat.titleWithFileSharing') : ''
].filter(Boolean).join(', ')
})
: t('chat.title')
}
accessibilityLabel = { t(_isPollsEnabled ? 'chat.titleWithPolls' : 'chat.title') }
onChange = { onChangeTab }
selected = { _focusedTab }
tabs = { tabs } />
@@ -519,15 +329,6 @@ const Chat = ({
isCCTabEnabled = { _isCCTabEnabled }
isPollsEnabled = { _isPollsEnabled } />
: renderChat()}
<div
className = { cx(
classes.dragHandleContainer,
(isMouseDown || _isResizing) && 'visible',
'dragHandleContainer'
) }
onMouseDown = { onDragHandleMouseDown }>
<div className = { cx(classes.dragHandle, 'dragHandle') } />
</div>
</div> : null
);
};
@@ -548,13 +349,11 @@ const Chat = ({
* _messages: Array<Object>,
* _nbUnreadMessages: number,
* _nbUnreadPolls: number,
* _showNamePrompt: boolean,
* _width: number,
* _isResizing: boolean
* _showNamePrompt: boolean
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { isOpen, focusedTab, messages, nbUnreadMessages, width, isResizing } = state['features/chat'];
const { isOpen, focusedTab, messages, nbUnreadMessages } = state['features/chat'];
const { nbUnreadPolls } = state['features/polls'];
const _localParticipant = getLocalParticipant(state);
@@ -563,14 +362,11 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
_isOpen: isOpen,
_isPollsEnabled: !arePollsDisabled(state),
_isCCTabEnabled: isCCTabEnabled(state),
_isFileSharingTabEnabled: isFileSharingEnabled(state),
_focusedTab: focusedTab,
_messages: messages,
_nbUnreadMessages: nbUnreadMessages,
_nbUnreadPolls: nbUnreadPolls,
_showNamePrompt: !_localParticipant?.name,
_width: width?.current || CHAT_SIZE,
_isResizing: isResizing
_showNamePrompt: !_localParticipant?.name
};
}

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