mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-14 18:17:47 +00:00
Compare commits
43 Commits
saghul-pat
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7529a7ae4 | ||
|
|
3c41791ed1 | ||
|
|
9a6babd70c | ||
|
|
b0ecd27d97 | ||
|
|
ae462f12e8 | ||
|
|
279638111a | ||
|
|
e5fb124baf | ||
|
|
c09719dabf | ||
|
|
41ba851275 | ||
|
|
db49f6fd45 | ||
|
|
144bc6529d | ||
|
|
ac84afae7c | ||
|
|
3f316dde56 | ||
|
|
c08a1d4b11 | ||
|
|
f132ed6c5c | ||
|
|
fc4186b052 | ||
|
|
80ad2c9d16 | ||
|
|
4f06c43eb7 | ||
|
|
5a11cd8801 | ||
|
|
d11a9daf41 | ||
|
|
6f4412db6e | ||
|
|
8ff122720d | ||
|
|
57b820f77f | ||
|
|
9b2b909b72 | ||
|
|
9a3917942f | ||
|
|
2d6be4ea27 | ||
|
|
cfd790731d | ||
|
|
04dc27b9e8 | ||
|
|
6534e6f277 | ||
|
|
5e58c9b133 | ||
|
|
2c43785f54 | ||
|
|
e672aaa35a | ||
|
|
95c420cab9 | ||
|
|
2d534bf2f7 | ||
|
|
d93b1c2195 | ||
|
|
5669d5f63e | ||
|
|
aac7606b32 | ||
|
|
716ceb0930 | ||
|
|
7574649887 | ||
|
|
e231d7815e | ||
|
|
d51c9eed6f | ||
|
|
2cc6953b60 | ||
|
|
751d1fae36 |
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@@ -208,6 +208,24 @@ jobs:
|
||||
-framework ios/sdk/out/ios-device.xcarchive/Products/Library/Frameworks/JitsiMeetSDK.framework \
|
||||
-output ios/sdk/out/JitsiMeetSDK.xcframework
|
||||
- run: ls -lR ios/sdk/out
|
||||
react-native-sdk-build:
|
||||
name: Build mobile SDK (React Native)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
|
||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f #v6.3.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
- name: Check Node / npm versions
|
||||
run: |
|
||||
node -v
|
||||
npm -v
|
||||
- run: npm install
|
||||
- run: |
|
||||
cd react-native-sdk
|
||||
node update_sdk_dependencies.js
|
||||
npm install
|
||||
debian-build:
|
||||
name: Test Debian packages build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
15
Makefile
15
Makefile
@@ -5,8 +5,8 @@ LIBJITSIMEET_DIR = node_modules/lib-jitsi-meet
|
||||
OLM_DIR = node_modules/@matrix-org/olm
|
||||
TF_WASM_DIR = node_modules/@tensorflow/tfjs-backend-wasm/dist/
|
||||
RNNOISE_WASM_DIR = node_modules/@jitsi/rnnoise-wasm/dist
|
||||
EXCALIDRAW_DIR = node_modules/@jitsi/excalidraw/dist/excalidraw-assets
|
||||
EXCALIDRAW_DIR_DEV = node_modules/@jitsi/excalidraw/dist/excalidraw-assets-dev
|
||||
EXCALIDRAW_DIR = node_modules/@jitsi/excalidraw/dist/prod
|
||||
EXCALIDRAW_DIR_DEV = node_modules/@jitsi/excalidraw/dist/dev
|
||||
TFLITE_WASM = react/features/stream-effects/virtual-background/vendor/tflite
|
||||
MEET_MODELS_DIR = react/features/stream-effects/virtual-background/vendor/models
|
||||
FACE_MODELS_DIR = node_modules/@vladmandic/human-models/models
|
||||
@@ -59,6 +59,7 @@ deploy-appbundle:
|
||||
$(BUILD_DIR)/close3.min.js \
|
||||
$(BUILD_DIR)/close3.min.js.map \
|
||||
$(DEPLOY_DIR) || true
|
||||
cp -r $(BUILD_DIR)/chunks $(DEPLOY_DIR)/chunks
|
||||
|
||||
deploy-lib-jitsi-meet:
|
||||
cp \
|
||||
@@ -86,14 +87,12 @@ deploy-tflite:
|
||||
$(DEPLOY_DIR)
|
||||
|
||||
deploy-excalidraw:
|
||||
cp -R \
|
||||
$(EXCALIDRAW_DIR) \
|
||||
$(DEPLOY_DIR)/
|
||||
mkdir -p $(DEPLOY_DIR)/excalidraw
|
||||
cp -R $(EXCALIDRAW_DIR)/fonts $(DEPLOY_DIR)/excalidraw/
|
||||
|
||||
deploy-excalidraw-dev:
|
||||
cp -R \
|
||||
$(EXCALIDRAW_DIR_DEV) \
|
||||
$(DEPLOY_DIR)/
|
||||
mkdir -p $(DEPLOY_DIR)/excalidraw
|
||||
cp -R $(EXCALIDRAW_DIR_DEV)/fonts $(DEPLOY_DIR)/excalidraw/
|
||||
|
||||
deploy-meet-models:
|
||||
cp \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# <p align="center">Jitsi Meet</p>
|
||||
# <p align="center">Jitsi Meet</p>
|
||||
|
||||
Jitsi Meet is a set of Open Source projects which empower users to use and deploy
|
||||
video conferencing platforms with state-of-the-art video quality and features.
|
||||
|
||||
@@ -389,6 +389,7 @@ export default {
|
||||
* Returns an object containing a promise which resolves with the created tracks &
|
||||
* the errors resulting from that process.
|
||||
* @param {object} options
|
||||
* @param {boolean} option.isBreakoutRoom - true if we are creating the initial local tracks in breakout room.
|
||||
* @param {boolean} options.startAudioOnly=false - if <tt>true</tt> then
|
||||
* only audio track will be created and the audio only mode will be turned
|
||||
* on.
|
||||
@@ -403,14 +404,16 @@ export default {
|
||||
*/
|
||||
createInitialLocalTracks(options = {}, recordTimeMetrics = false) {
|
||||
const errors = {};
|
||||
const { isBreakoutRoom = false } = options;
|
||||
|
||||
// Always get a handle on the audio input device so that we have statistics (such as "No audio input" or
|
||||
// "Are you trying to speak?" ) even if the user joins the conference muted.
|
||||
const initialDevices = config.startSilent || config.disableInitialGUM ? [] : [ MEDIA_TYPE.AUDIO ];
|
||||
const requestedAudio = !config.disableInitialGUM;
|
||||
const initialDevices
|
||||
= config.startSilent || (config.disableInitialGUM && !isBreakoutRoom) ? [] : [ MEDIA_TYPE.AUDIO ];
|
||||
const requestedAudio = !config.disableInitialGUM || isBreakoutRoom;
|
||||
let requestedVideo = false;
|
||||
|
||||
if (!config.disableInitialGUM
|
||||
if ((!config.disableInitialGUM || isBreakoutRoom)
|
||||
&& !options.startWithVideoMuted
|
||||
&& !options.startAudioOnly
|
||||
&& !options.startScreenSharing) {
|
||||
|
||||
@@ -1863,6 +1863,10 @@ var config = {
|
||||
// userLimit: 25,
|
||||
// // The url for more info about the whiteboard and its usage limitations.
|
||||
// limitUrl: 'https://example.com/blog/whiteboard-limits',
|
||||
|
||||
// //Backend URL for storing whiteboard scenes and images
|
||||
// //This backend service handles scene persistence and file uploads
|
||||
// storageBackendUrl: 'https://excalidraw-s3-storage-backend.example.com',
|
||||
// },
|
||||
|
||||
// The watchRTC initialize config params as described :
|
||||
|
||||
5
custom.d.ts
vendored
5
custom.d.ts
vendored
@@ -1,3 +1,8 @@
|
||||
declare module '*.css' {
|
||||
const content: Record<string, string>;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.svg' {
|
||||
const content: any;
|
||||
export default content;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
: pathname.substring(0, contextRootEndIndex + 1)
|
||||
);
|
||||
}
|
||||
window.EXCALIDRAW_ASSET_PATH = 'libs/';
|
||||
window.EXCALIDRAW_ASSET_PATH = 'libs/excalidraw/';
|
||||
// Dynamically generate the manifest location URL. It must be served from the document origin, and we may have
|
||||
// the base pointing to the CDN. This way we can generate a full URL which will bypass the base.
|
||||
document.querySelector('#manifest-placeholder').setAttribute('href', window.location.origin + contextRoot(window.location.pathname) + 'manifest.json');
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"copyLink": "Konferenzlink kopieren",
|
||||
"copyStream": "Livestreaminglink kopieren",
|
||||
"countryNotSupported": "Wir unterstützen dieses Land noch nicht.",
|
||||
"countryReminder": "Telefonnummer nicht in den USA? Bitte sicherstellen, dass die Telefonnummer mit dem Ländercode beginnt.",
|
||||
"countryReminder": "Telefonnummer nicht in den USA? Bitte sicherstellen, dass die Telefonnummer mit dem Ländercode beginnt!",
|
||||
"defaultEmail": "Ihre Standard-E-Mail",
|
||||
"disabled": "Sie können keine Personen einladen.",
|
||||
"failedToAdd": "Fehler beim Hinzufügen von Personen",
|
||||
@@ -359,7 +359,7 @@
|
||||
"error": "Fehler",
|
||||
"errorRoomCreationRestriction": "Sie haben versucht, zu schnell beizutreten, bitte versuchen Sie es gleich noch einmal.",
|
||||
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
|
||||
"grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?",
|
||||
"grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an {{participantName}} vergeben?",
|
||||
"grantModeratorTitle": "Moderationsrechte vergeben",
|
||||
"hide": "Ausblenden",
|
||||
"hideShareAudioHelper": "Diese Meldung nicht mehr anzeigen",
|
||||
@@ -385,6 +385,7 @@
|
||||
"login": "Anmelden",
|
||||
"loginFailed": "Anmeldung fehlgeschlagen.",
|
||||
"loginOnResume": "Ihre Anmeldung ist abgelaufen. Sie müssen sich neu anmelden um weiter an der Konferenz teilzunehmen.",
|
||||
"loginPopupBlocked": "Das Popup für die Anmeldung wurde von Ihrem Browser blockiert.",
|
||||
"loginQuestion": "Sind Sie sicher, dass sie sich anmelden und die Konferenz verlassen möchten?",
|
||||
"logoutQuestion": "Sind Sie sicher, dass Sie sich abmelden und die Konferenz verlassen möchten?",
|
||||
"logoutTitle": "Abmelden",
|
||||
@@ -395,7 +396,7 @@
|
||||
"micNotSendingData": "Gehen Sie zu den Einstellungen Ihres Computers, um die Stummschaltung Ihres Mikrofons aufzuheben und seinen Pegel einzustellen",
|
||||
"micNotSendingDataTitle": "Ihr Mikrofon ist durch Ihre Systemeinstellungen stumm geschaltet",
|
||||
"micPermissionDeniedError": "Die Berechtigung zur Verwendung des Mikrofons wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Personen können Sie nicht hören. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste, um die Berechtigungen zu erteilen.",
|
||||
"micTimeoutError": "Audioquelle konnte nicht gestartet werden. Zeitüberschreitung",
|
||||
"micTimeoutError": "Audioquelle konnte nicht gestartet werden. Zeitüberschreitung!",
|
||||
"micUnknownError": "Das Mikrofon kann aus einem unbekannten Grund nicht verwendet werden.",
|
||||
"moderationAudioLabel": "Erlaube Anwesenden die Stummschaltung für sich aufzuheben",
|
||||
"moderationDesktopLabel": "Erlaube Anwesenden ihren Bildschirm freizugeben",
|
||||
@@ -434,8 +435,8 @@
|
||||
"muteParticipantsVideoTitle": "Die Kamera von dieser Person ausschalten?",
|
||||
"noDropboxToken": "Kein gültiges Dropbox-Token",
|
||||
"password": "Passwort",
|
||||
"passwordLabel": "Diese Konferenz wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um der Konferenz beizutreten.",
|
||||
"passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
|
||||
"passwordLabel": "Diese Konferenz wurde gesichert. Bitte geben Sie das $t(lockRoomPassword) ein, um der Konferenz beizutreten.",
|
||||
"passwordNotSupported": "Das Festlegen eines $t(lockRoomPassword) wird nicht unterstützt.",
|
||||
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) nicht unterstützt",
|
||||
"passwordRequired": "$t(lockRoomPasswordUppercase) erforderlich",
|
||||
"permissionCameraRequiredError": "Der Zugriff auf die Kamera wird benötigt, um in Videokonferenzen teilzunehmen. Bitte in den Einstellungen zulassen",
|
||||
@@ -456,7 +457,7 @@
|
||||
"remoteControlStopMessage": "Die Fernsteuerung wurde beendet!",
|
||||
"remoteControlTitle": "Fernsteuerung",
|
||||
"remoteUserControls": "Remote Benutzersteuerung von {{username}}",
|
||||
"removePassword": "$t(lockRoomPasswordUppercase) entfernen",
|
||||
"removePassword": "$t(lockRoomPassword) entfernen",
|
||||
"removeSharedVideoMsg": "Sind Sie sicher, dass Sie das geteilte Video entfernen möchten?",
|
||||
"removeSharedVideoTitle": "Freigegebenes Video entfernen",
|
||||
"renameBreakoutRoomLabel": "Raumname",
|
||||
@@ -468,6 +469,8 @@
|
||||
"screenSharingFailed": "Ups! Beim Teilen des Bildschirms ist etwas schiefgegangen!",
|
||||
"screenSharingFailedTitle": "Bildschirmfreigabe fehlgeschlagen!",
|
||||
"screenSharingPermissionDeniedError": "Ups! Etwas stimmt nicht mit Ihren Berechtigungen zur Bildschirmfreigabe. Bitte neu laden und erneut versuchen.",
|
||||
"screenshareStoppedDiskSpace": "Dies passiert, wenn Sie die Bildschirmfreigabe über die macOS-Menüleiste gestoppt haben. Die Bildschirmfreigabe kann auch bei geringem freien Speicherplatz gestoppt werden.",
|
||||
"screenshareStoppedTitle": "Bildschirmfreigabe vom Betriebssystem gestoppt",
|
||||
"searchInSalesforce": "In Salesforce suchen",
|
||||
"searchResults": "Suchergebnisse({{count}})",
|
||||
"searchResultsDetailsError": "Beim Abrufen der Daten des Besitzers ist ein Fehler aufgetreten.",
|
||||
@@ -481,7 +484,7 @@
|
||||
"serviceUnavailable": "Dienst nicht verfügbar",
|
||||
"sessTerminated": "Konferenz beendet",
|
||||
"sessTerminatedReason": "Die Konferenz wurde beendet",
|
||||
"sessionRestarted": "Konferenz neugestartet",
|
||||
"sessionRestarted": "Konferenz neugestartet.",
|
||||
"shareAudio": "Fortfahren",
|
||||
"shareAudioAltText": "Um den gewünschten Inhalt zu teilen: Navigiere zu \"Browser tab\", wähle den Inhalt, aktiviere \"Audio teilen\" Kästchen Und klicke den “Teilen” schaltfläche",
|
||||
"shareAudioTitle": "Wie kann Audio geteilt werden",
|
||||
@@ -615,8 +618,8 @@
|
||||
},
|
||||
"info": {
|
||||
"accessibilityLabel": "Informationen anzeigen",
|
||||
"addPassword": "$t(lockRoomPasswordUppercase) hinzufügen",
|
||||
"cancelPassword": "$t(lockRoomPasswordUppercase) löschen",
|
||||
"addPassword": "$t(lockRoomPassword) hinzufügen",
|
||||
"cancelPassword": "$t(lockRoomPassword) löschen",
|
||||
"conferenceURL": "Link:",
|
||||
"copyNumber": "Nummer kopieren",
|
||||
"country": "Land",
|
||||
@@ -646,7 +649,7 @@
|
||||
"noRoom": "Keine Konferenz für die Einwahlinformationen angegeben.",
|
||||
"noWhiteboard": "Whiteboard konnte nicht geladen werden.",
|
||||
"numbers": "Einwahlnummern",
|
||||
"password": "$t(lockRoomPasswordUppercase):",
|
||||
"password": "$t(lockRoomPasswordUppercase): ",
|
||||
"reachedLimit": "Sie haben die Grenzen Ihres Tarifs erreicht.",
|
||||
"sip": "SIP-Adresse",
|
||||
"sipAudioOnly": "SIP-Adresse (nur Ton)",
|
||||
@@ -698,12 +701,13 @@
|
||||
"changeSignIn": "Konten wechseln.",
|
||||
"choose": "Livestream auswählen",
|
||||
"chooseCTA": "Streaming-Option auswählen. Sie sind aktuell als {{email}} angemeldet.",
|
||||
"chooseCTAWithChangeSignIn": "Streaming-Option auswählen. Sie sind aktuell als {{email}} angemeldet. <0>Account wechseln.</0>",
|
||||
"enterStreamKey": "Streamschlüssel für den YouTube-Livestream hier eingeben.",
|
||||
"error": "Das Livestreaming ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||
"errorAPI": "Beim Abrufen der YouTube-Livestreams ist ein Fehler aufgetreten. Bitte versuchen Sie, sich erneut anzumelden.",
|
||||
"errorLiveStreamNotEnabled": "Livestreaming ist für {{email}} nicht aktiviert. Aktivieren Sie das Livestreaming oder melden Sie sich bei einem Konto mit aktiviertem Livestreaming an.",
|
||||
"expandedOff": "Livestream wurde angehalten",
|
||||
"expandedOn": "Die Konferenz wird momentan an YouTube gestreamt.",
|
||||
"expandedOn": "Die Konferenz wird momentan an YouTube gestreamt",
|
||||
"expandedPending": "Livestream wird gestartet …",
|
||||
"failedToStart": "Livestream konnte nicht gestartet werden",
|
||||
"getStreamKeyManually": "Wir waren nicht in der Lage, Livestreams abzurufen. Versuchen Sie, Ihren Livestream-Schlüssel von YouTube zu erhalten.",
|
||||
@@ -726,6 +730,7 @@
|
||||
"streamIdHelp": "Was ist das?",
|
||||
"title": "Livestream",
|
||||
"unavailableTitle": "Livestreaming nicht verfügbar",
|
||||
"youTubeGoLiveWarning": "Denken Sie daran 'Go Live' im YouTube Studio auszuwählen, wenn Auto-Start/Auto-Stop deaktiviert ist. Ansonsten wird die Aufnahme nicht gestartet.",
|
||||
"youtubeTerms": "YouTube-Nutzungsbedingungen"
|
||||
},
|
||||
"lobby": {
|
||||
@@ -746,7 +751,7 @@
|
||||
"joinRejectedTitle": "Beitrittsanfrage abgelehnt.",
|
||||
"joinTitle": "Konferenz beitreten",
|
||||
"joinWithPasswordMessage": "Beitrittsversuch mit Passwort, bitte warten …",
|
||||
"joiningMessage": "Sie treten der Konferenz bei, sobald jemand Ihre Anfrage annimmt.",
|
||||
"joiningMessage": "Sie treten der Konferenz bei, sobald jemand Ihre Anfrage annimmt",
|
||||
"joiningTitle": "Beitritt anfragen …",
|
||||
"joiningWithPasswordTitle": "Mit Passwort beitreten …",
|
||||
"knockButton": "Beitritt anfragen",
|
||||
@@ -816,7 +821,7 @@
|
||||
"audioUnmuteBlockedTitle": "Stummschaltung kann nicht aufgehoben werden!",
|
||||
"chatMessages": "Chatnachrichten",
|
||||
"connectedOneMember": "{{name}} nimmt an der Konferenz teil",
|
||||
"connectedThreePlusMembers": "{{name}} und {{count}} andere Personen nehmen an der Konferenz teil",
|
||||
"connectedThreePlusMembers": "{{name}} und viele andere Personen nehmen an der Konferenz teil",
|
||||
"connectedTwoMembers": "{{first}} und {{second}} nehmen an der Konferenz teil",
|
||||
"connectionFailed": "Verbindung fehlgeschlagen. Bitte versuchen Sie es später noch einmal!",
|
||||
"dataChannelClosed": "Schlechte Videoqualität",
|
||||
@@ -834,7 +839,7 @@
|
||||
"focusFail": "{{component}} ist im Moment nicht verfügbar – wiederholen in {{ms}} Sekunden",
|
||||
"gifsMenu": "GIPHY",
|
||||
"groupTitle": "Benachrichtigungen",
|
||||
"hostAskedUnmute": "Die Moderation bittet Sie, das Mikrofon zu aktivieren",
|
||||
"hostAskedUnmute": "Die Moderation bittet Sie, das Mikrofon zu aktivieren.",
|
||||
"invalidTenant": "Ungültiger Mandant",
|
||||
"invalidTenantHyphenDescription": "Der gewählte Mandantenname ist ungültig (beginnt oder endet mit '-').",
|
||||
"invalidTenantLengthDescription": "Der gewählte Mandantenname ist zu lang.",
|
||||
@@ -855,21 +860,21 @@
|
||||
"localRecordingStarted": "{{name}} hat eine lokale Aufzeichnung gestartet.",
|
||||
"localRecordingStopped": "{{name}} hat eine lokale Aufzeichnung gestoppt.",
|
||||
"me": "Ich",
|
||||
"moderationInEffectCSDescription": "Bitte melden um ein Video zu teilen",
|
||||
"moderationInEffectCSDescription": "Bitte melden um ein Video zu teilen.",
|
||||
"moderationInEffectCSTitle": "Die Videofreigabe ist von der Moderation gesperrt",
|
||||
"moderationInEffectDescription": "Bitte melden um zu sprechen",
|
||||
"moderationInEffectDescription": "Bitte melden um zu sprechen.",
|
||||
"moderationInEffectTitle": "Das Mikrofon ist von der Moderation gesperrt",
|
||||
"moderationInEffectVideoDescription": "Bitte melden um die Kamera zu starten",
|
||||
"moderationInEffectVideoDescription": "Bitte melden um die Kamera zu starten.",
|
||||
"moderationInEffectVideoTitle": "Die Kamera ist von der Moderation gesperrt",
|
||||
"moderationRequestFromModerator": "Die Moderation bittet Sie, das Mikrofon zu aktivieren",
|
||||
"moderationRequestFromParticipant": "möchte sprechen",
|
||||
"moderationStartedTitle": "Moderation gestartet",
|
||||
"moderationStoppedTitle": "Moderation gestoppt",
|
||||
"moderationToggleDescription": "von {{participantDisplayName}}",
|
||||
"moderator": "Moderationsrechte vergeben!",
|
||||
"moderator": "Sie sind jetzt Teil der Moderation",
|
||||
"muted": "Der Konferenz wurde stumm beigetreten.",
|
||||
"mutedRemotelyDescription": "Sie können jederzeit die Stummschaltung aufheben, wenn Sie bereit sind zu sprechen. Wenn Sie fertig sind, können Sie sich wieder stummschalten, um Geräusche von der Konferenz fernzuhalten.",
|
||||
"mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet!",
|
||||
"mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet",
|
||||
"mutedTitle": "Stummschaltung aktiv!",
|
||||
"newDeviceAction": "Verwenden",
|
||||
"newDeviceAudioTitle": "Neues Audiogerät erkannt",
|
||||
@@ -880,26 +885,26 @@
|
||||
"noiseSuppressionStereoDescription": "Rauschunterdrückung unterstützt aktuell keinen Stereoton.",
|
||||
"oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ",
|
||||
"oldElectronClientDescription2": "aktuelle Version",
|
||||
"oldElectronClientDescription3": "!",
|
||||
"oldElectronClientDescription3": " jetzt!",
|
||||
"openChat": "Chat öffnen",
|
||||
"participantWantsToJoin": "Möchte an der Konferenz teilnehmen",
|
||||
"participantsWantToJoin": "Möchten an der Konferenz teilnehmen",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
|
||||
"raiseHandAction": "Melden",
|
||||
"raisedHand": "{{name}} möchte sprechen.",
|
||||
"raisedHand": "möchte sprechen.",
|
||||
"raisedHands": "{{participantName}} und {{raisedHands}} weitere möchten sprechen",
|
||||
"reactionSounds": "Interaktionstöne deaktivieren",
|
||||
"reactionSoundsForAll": "Interaktionstöne für alle deaktivieren",
|
||||
"screenShareNoAudio": "Die Option \"Audio freigeben\" wurde bei der Auswahl des Fensters nicht ausgewählt.",
|
||||
"screenShareNoAudioTitle": "Share audio was not checked",
|
||||
"screenShareNoAudioTitle": "Systemton konnte nicht geteilt werden!",
|
||||
"screenSharingAudioOnlyDescription": "Durch die Bildschirmfreigabe wird der Modus \"Beste Leistung\" beeinflusst und daher mehr Datenrate benötigt.",
|
||||
"screenSharingAudioOnlyTitle": "Modus \"Beste Leistung\"",
|
||||
"selfViewTitle": "Sie können die eigene Ansicht immer in den Einstellungen reaktivieren",
|
||||
"somebody": "Jemand",
|
||||
"startSilentDescription": "Treten Sie der Konferenz noch einmal bei, um Ihr Audio zu aktivieren",
|
||||
"startSilentTitle": "Sie sind ohne Audioausgabe beigetreten!",
|
||||
"suboptimalBrowserWarning": "Tut uns leid, aber die Konferenz wird mit {{appName}} kein großartiges Erlebnis. Wir versuchen immer die Situation zu verbessern, bis dahin empfehlen wir aber die Verwendung einer der <a href=\"{{recommendedBrowserPageLink}}\" target=\"_blank\">vollständig unterstützen Browser</a>.",
|
||||
"suboptimalBrowserWarning": "Tut uns leid, aber die Konferenz wird kein großartiges Erlebnis. Wir versuchen immer die Situation zu verbessern, bis dahin empfehlen wir aber die Verwendung einer der <a href=\"{{recommendedBrowserPageLink}}\" target=\"_blank\">vollständig unterstützen Browser</a>.",
|
||||
"suboptimalExperienceTitle": "Browserwarnung",
|
||||
"suggestRecordingAction": "Starten",
|
||||
"suggestRecordingDescription": "Möchten Sie eine Aufzeichnung starten?",
|
||||
@@ -908,7 +913,7 @@
|
||||
"unmuteScreen": "Bildschirmfreigabe starten",
|
||||
"unmuteVideo": "Kamera einschalten",
|
||||
"videoMutedRemotelyDescription": "Sie können sie jederzeit wieder einschalten.",
|
||||
"videoMutedRemotelyTitle": "Ihre Kamera wurde von {{participantDisplayName}} ausgeschaltet!",
|
||||
"videoMutedRemotelyTitle": "Ihre Kamera wurde von {{participantDisplayName}} ausgeschaltet",
|
||||
"videoUnmuteBlockedDescription": "Die Kamera und Bildschirmfreigabe kann aus Überlastungsschutzgründen temporär nicht eingeschaltet werden.",
|
||||
"videoUnmuteBlockedTitle": "Kamera und Bildschirmfreigabe kann nicht aktiviert werden!",
|
||||
"viewLobby": "Lobby ansehen",
|
||||
@@ -1002,7 +1007,7 @@
|
||||
},
|
||||
"results": {
|
||||
"changeVote": "Antwort ändern",
|
||||
"empty": "Es gibt bisher keine Umfragen in dieser Konferenz. Sie können hier eine Umfrage starten!",
|
||||
"empty": "Es gibt bisher keine Umfragen in dieser Konferenz.",
|
||||
"hideDetailedResults": "Details verbergen",
|
||||
"showDetailedResults": "Details anzeigen",
|
||||
"vote": "Vote"
|
||||
@@ -1011,7 +1016,7 @@
|
||||
"poweredby": "Betrieben von",
|
||||
"prejoin": {
|
||||
"audioAndVideoError": "Audio- und Videofehler:",
|
||||
"audioDeviceProblem": "Es gibt ein Problem mit Ihrem Audiogerät.",
|
||||
"audioDeviceProblem": "Es gibt ein Problem mit Ihrem Audiogerät",
|
||||
"audioOnlyError": "Audiofehler:",
|
||||
"audioTrackError": "Audiotrack konnte nicht erstellt werden.",
|
||||
"callMe": "Mich anrufen",
|
||||
@@ -1022,8 +1027,8 @@
|
||||
"connection": {
|
||||
"failed": "Verbindungstest fehlgeschlagen!",
|
||||
"good": "Ihre Internetverbindung sieht gut aus!",
|
||||
"nonOptimal": "Ihre Internetverbindung ist nicht optimal.",
|
||||
"poor": "Sie haben eine schlechte Internetverbindung.",
|
||||
"nonOptimal": "Ihre Internetverbindung ist nicht optimal",
|
||||
"poor": "Sie haben eine schlechte Internetverbindung",
|
||||
"running": "Verbindung wird getestet…"
|
||||
},
|
||||
"connectionDetails": {
|
||||
@@ -1050,7 +1055,7 @@
|
||||
"errorDialOutDisconnected": "Anruf fehlgeschlagen. Verbindungsabbruch",
|
||||
"errorDialOutFailed": "Anruf fehlgeschlagen. Anruf fehlgeschlagen",
|
||||
"errorDialOutStatus": "Fehler beim Abrufen des Anrufstatus",
|
||||
"errorMissingName": "Bitte geben Sie Ihren Namen ein, um der Konferenz beizutreten.",
|
||||
"errorMissingName": "Bitte geben Sie Ihren Namen ein, um der Konferenz beizutreten",
|
||||
"errorNoPermissions": "Sie müssen den Zugriff auf Mikrofon und Kamera erlauben",
|
||||
"errorStatusCode": "Anruf fehlgeschlagen. Statuscode: {{status}}",
|
||||
"errorValidation": "Nummerverifikation fehlgeschlagen",
|
||||
@@ -1062,7 +1067,7 @@
|
||||
"joinWithoutAudio": "Ohne Ton beitreten",
|
||||
"keyboardShortcuts": "Tastaturkurzbefehle aktivieren",
|
||||
"linkCopied": "Link in die Zwischenablage kopiert",
|
||||
"lookGood": "Alles scheint zu funktionieren.",
|
||||
"lookGood": "Alles scheint zu funktionieren",
|
||||
"or": "oder",
|
||||
"premeeting": "Vorschau",
|
||||
"proceedAnyway": "Trotzdem fortsetzen",
|
||||
@@ -1119,7 +1124,7 @@
|
||||
"error": "Die Aufzeichnung ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||
"errorFetchingLink": "Der Link zur Aufzeichnung konnte nicht geladen werden.",
|
||||
"expandedOff": "Aufzeichnung wurde gestoppt",
|
||||
"expandedOn": "Die Konferenz wird momentan aufgezeichnet.",
|
||||
"expandedOn": "Die Konferenz wird momentan aufgezeichnet",
|
||||
"expandedPending": "Aufzeichnung wird gestartet…",
|
||||
"failedToStart": "Die Aufnahme konnte nicht gestartet werden",
|
||||
"fileSharingdescription": "Aufzeichnung mit den Personen der Konferenz teilen",
|
||||
@@ -1144,9 +1149,11 @@
|
||||
"offBy": "{{name}} stoppte die Aufnahme",
|
||||
"on": "Aufnahme",
|
||||
"onBy": "{{name}} startete die Aufnahme",
|
||||
"onByWithTranscription": "{{name}} startete die Aufnahme. Ein Transkript wird nach der Konferenz verfügbar sein.",
|
||||
"onWithTranscription": "Aufnahme gestartet. Ein Transkript wird nach der Konferenz verfügbar sein.",
|
||||
"onlyRecordSelf": "Nur eigenes Kamerabild und Ton aufzeichnen",
|
||||
"pending": "Aufzeichnung der Konferenz wird vorbereitet…",
|
||||
"policyError": "Sie haben die Aufzeichnung zu schnell gestartet. Bitte versuchen Sie es später noch einmal.",
|
||||
"policyError": "Sie haben die Aufzeichnung zu schnell gestartet. Bitte versuchen Sie es später noch einmal!",
|
||||
"recordAudioAndVideo": "Kamera und Ton aufzeichnen",
|
||||
"recordTranscription": "Transkription aufzeichnen",
|
||||
"saveLocalRecording": "Aufzeichnung lokal abspeichern",
|
||||
@@ -1169,8 +1176,8 @@
|
||||
"pullToRefresh": "Ziehen, um zu aktualisieren"
|
||||
},
|
||||
"security": {
|
||||
"about": "Sie können Ihre Konferenz mit einem Passwort sichern. Personen müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
|
||||
"aboutReadOnly": "Mit Moderationsrechten kann die Konferenz mit einem Passwort gesichert werden. Personen müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
|
||||
"about": "Sie können Ihre Konferenz mit einem $t(lockRoomPassword) sichern. Personen müssen das $t(lockRoomPassword) eingeben, bevor sie an der Konferenz teilnehmen dürfen.",
|
||||
"aboutReadOnly": "Mit Moderationsrechten kann die Konferenz mit einem $t(lockRoomPassword) gesichert werden. Personen müssen das $t(lockRoomPassword) eingeben, bevor sie an der Konferenz teilnehmen dürfen.",
|
||||
"insecureRoomNameWarningNative": "Der Raumname ist unsicher. Unerwünschte Personen könnten Ihrer Konferenz beitreten. {{recommendAction}} Lernen Sie mehr über die Absicherung Ihrer Konferenz ",
|
||||
"insecureRoomNameWarningWeb": "Der Raumname ist unsicher. Unerwünschte Personen könnten Ihrer Konferenz beitreten {{recommendAction}} Lernen Sie <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">hier</a> mehr über die Absicherung Ihrer Konferenz.",
|
||||
"title": "Sicherheitsoptionen",
|
||||
@@ -1273,9 +1280,9 @@
|
||||
"displayEmotions": "Emotionen anzeigen",
|
||||
"fearful": "Ängstlich",
|
||||
"happy": "Fröhlich",
|
||||
"hours": "{{count}} Std. ",
|
||||
"hours": "{{count}} Std.",
|
||||
"labelTooltip": "Anzahl der Personen: {{count}}",
|
||||
"minutes": "{{count}} Min. ",
|
||||
"minutes": "{{count}} Min.",
|
||||
"name": "Name",
|
||||
"neutral": "Neutral",
|
||||
"sad": "Traurig",
|
||||
@@ -1358,7 +1365,7 @@
|
||||
"muteEveryoneElse": "Alle anderen stummschalten",
|
||||
"muteEveryoneElsesVideoStream": "Alle anderen Kameras ausschalten",
|
||||
"muteEveryonesVideoStream": "Alle Kameras ausschalten",
|
||||
"muteGUMPending": "Verbinde Ihr Mikrofon",
|
||||
"muteGUMPending": "Mikrofon verbinden",
|
||||
"noiseSuppression": "Rauschunterdrückung",
|
||||
"openChat": "Chat öffnen",
|
||||
"participants": "Anwesenheitsliste öffnen. {{participantsCount}} anwesend",
|
||||
@@ -1393,7 +1400,7 @@
|
||||
"unmute": "Stummschaltung aufheben",
|
||||
"videoblur": "Unscharfer Hintergrund ein-/ausschalten",
|
||||
"videomute": "Kamera stoppen",
|
||||
"videomuteGUMPending": "Verbinde Ihre Kamera",
|
||||
"videomuteGUMPending": "Kamera verbinden",
|
||||
"videounmute": "Kamera einschalten"
|
||||
},
|
||||
"addPeople": "Personen zur Konferenz hinzufügen",
|
||||
@@ -1463,7 +1470,7 @@
|
||||
"mute": "Audio stummschalten",
|
||||
"muteEveryone": "Alle stummschalten",
|
||||
"muteEveryonesVideo": "Alle Kameras ausschalten",
|
||||
"muteGUMPending": "Verbinde Ihre Kamera",
|
||||
"muteGUMPending": "Mikrofon verbinden",
|
||||
"noAudioSignalDesc": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stumm geschaltet haben, sollten Sie einen Wechsel des Geräts in Erwägung ziehen.",
|
||||
"noAudioSignalDescSuggestion": "Wenn Sie das Gerät nicht absichtlich über die Systemeinstellungen oder die Hardware stummgeschaltet haben, sollten Sie einen Wechsel auf das vorgeschlagene Gerät in Erwägung ziehen.",
|
||||
"noAudioSignalDialInDesc": "Sie können sich auch über die Einwahlnummer einwählen:",
|
||||
@@ -1500,7 +1507,7 @@
|
||||
"silence": "Stille",
|
||||
"speakerStats": "Sprechstatistik",
|
||||
"startScreenSharing": "Bildschirmfreigabe starten",
|
||||
"startSubtitles": "Untertitel einschalten",
|
||||
"startSubtitles": "Untertitel • {{language}}",
|
||||
"stopAudioSharing": "Audiofreigabe stoppen",
|
||||
"stopScreenSharing": "Bildschirmfreigabe stoppen",
|
||||
"stopSharedVideo": "Video stoppen",
|
||||
@@ -1512,15 +1519,19 @@
|
||||
"unmute": "Stummschaltung aufheben",
|
||||
"videoSettings": "Kamera-Einstellungen",
|
||||
"videomute": "Kamera stoppen",
|
||||
"videomuteGUMPending": "Verbinde Ihre Kamera",
|
||||
"videomuteGUMPending": "Kamera verbinden",
|
||||
"videounmute": "Kamera einschalten"
|
||||
},
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "Untertitel ein-/ausschalten",
|
||||
"expandedLabel": "Transkribieren ist derzeit eingeschaltet",
|
||||
"failed": "Transkribieren fehlgeschlagen",
|
||||
"labelTooltip": "Die Konferenz wird transkribiert",
|
||||
"labelTooltip": "Die Konferenz wird transkribiert.",
|
||||
"labelTooltipExtra": "Zusätzlich wird das Transkript später verfügbar sein.",
|
||||
"off": "Transkription gestoppt",
|
||||
"on": "Transkription gestartet",
|
||||
"onBy": "{{name}} startete die Transkription",
|
||||
"onWithRecording": "Ein Transkript wird nach der Konferenz 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 ",
|
||||
@@ -1562,7 +1573,7 @@
|
||||
"ldTooltip": "Video wird in niedriger Auflösung angezeigt",
|
||||
"lowDefinition": "Niedrige Auflösung",
|
||||
"performanceSettings": "Qualitätseinstellungen",
|
||||
"recording": "Aufnahme läuft",
|
||||
"recording": "Aufnahme läuft.",
|
||||
"sd": "SD",
|
||||
"sdTooltip": "Video wird in Standardauflösung angezeigt",
|
||||
"standardDefinition": "Standardauflösung",
|
||||
@@ -1603,6 +1614,7 @@
|
||||
"addBackground": "Hintergrund hinzufügen",
|
||||
"apply": "Anwenden",
|
||||
"backgroundEffectError": "Hintergrund konnte nicht aktiviert werden.",
|
||||
"backgroundLimitReached": "Maximale Zahl an Hintergründen erreicht",
|
||||
"blur": "Hintergrund unscharf",
|
||||
"deleteImage": "Bild löschen",
|
||||
"desktopShare": "Desktopfreigabe",
|
||||
@@ -1612,9 +1624,10 @@
|
||||
"image3": "Weißer leerer Raum",
|
||||
"image4": "Schwarze Stehlampe",
|
||||
"image5": "Berg",
|
||||
"image6": "Wald",
|
||||
"image6": "Wald ",
|
||||
"image7": "Sonnenaufgang",
|
||||
"none": "keiner",
|
||||
"oldestBackgroundRemoved": "Der älteste Hintergrund wurde gelöscht, um den Neuesten hinzuzufügen.",
|
||||
"pleaseWait": "Bitte warten…",
|
||||
"removeBackground": "Hintergrund entfernen",
|
||||
"slightBlur": "Hintergrund leicht unscharf",
|
||||
@@ -1629,7 +1642,7 @@
|
||||
"description": "Sie beobachten derzeit diese Konferenz.",
|
||||
"raiseHand": "Hand heben",
|
||||
"title": "Konferenz wird beigetreten",
|
||||
"wishToSpeak": "Wenn Sie sprechen möchten, heben Sie bitte unten Ihre Hand und warten Sie auf die Zustimmung der Moderation"
|
||||
"wishToSpeak": "Wenn Sie sprechen möchten, heben Sie bitte unten Ihre Hand und warten Sie auf die Zustimmung der Moderation."
|
||||
},
|
||||
"labelTooltip": "Anzahl Gäste: {{count}}",
|
||||
"notification": {
|
||||
@@ -1639,7 +1652,7 @@
|
||||
"noVisitorLobby": "Sie können nicht teilnehmen, solange die Lobby für diese Konferenz aktiviert ist.",
|
||||
"notAllowedPromotion": "Eine Person muss Ihre Anfrage erst erlauben.",
|
||||
"requestToJoin": "Hand gehoben",
|
||||
"requestToJoinDescription": "Ihre Anfrage wurde an die Moderation gesendet, bitte warten Sie.",
|
||||
"requestToJoinDescription": "Ihre Anfrage wurde an die Moderation gesendet, bitte warten Sie!",
|
||||
"title": "Sie sind Gast in der Konferenz"
|
||||
},
|
||||
"waitingMessage": "Sie werden der Konferenz beitreten, sobald sie gestartet ist!"
|
||||
|
||||
@@ -1494,6 +1494,10 @@
|
||||
"failed": "La transcription a échoué",
|
||||
"labelTooltip": "La transcription de la réunion est en cours",
|
||||
"labelTooltipExtra": "De plus, une transcription sera disponible plus tard.",
|
||||
"off": "Transcription arrêtée",
|
||||
"on": "Transcription démarrée",
|
||||
"onBy": "{{name}} a démarré la transcription",
|
||||
"onWithRecording": "Une transcription sera également disponible après la réunion.",
|
||||
"openClosedCaptions": "Ouvrir les sous-titres",
|
||||
"original": "Original",
|
||||
"sourceLanguageDesc": "Actuellement, la langue de la réunion est sélectionnée à <b>{{sourceLanguage}}</b>. <br/> Vous pouvez la changer à partir de ",
|
||||
|
||||
@@ -1518,6 +1518,10 @@
|
||||
"failed": "La transcription a échoué",
|
||||
"labelTooltip": "La transcription de la réunion est en cours",
|
||||
"labelTooltipExtra": "Une transcription sera disponible plus tard.",
|
||||
"off": "Transcription arrêtée",
|
||||
"on": "Transcription démarrée",
|
||||
"onBy": "{{name}} a démarré la transcription",
|
||||
"onWithRecording": "Une transcription sera également disponible après la réunion.",
|
||||
"openClosedCaptions": "Ouvrir les sous-titres",
|
||||
"original": "Original",
|
||||
"sourceLanguageDesc": "Actuellement, la langue de la réunion est sélectionnée à <b>{{sourceLanguage}}</b>. <br/> Vous pouvez la changer à partir de ",
|
||||
|
||||
@@ -871,6 +871,7 @@
|
||||
"or": "lub",
|
||||
"premeeting": "Przed spotkaniem",
|
||||
"proceedAnyway": "Kontynuuj mimo to",
|
||||
"recordingWarning": "Inni uczestnicy mogą nagrywać tę rozmowę",
|
||||
"screenSharingError": "Błąd udostępniania ekranu:",
|
||||
"startWithPhone": "Uruchom przez telefon",
|
||||
"unsafeRoomConsent": "Rozumiem ryzyko, chcę dołączyć do spotkania",
|
||||
|
||||
@@ -701,6 +701,7 @@
|
||||
"changeSignIn": "Switch accounts.",
|
||||
"choose": "Choose a live stream",
|
||||
"chooseCTA": "Choose a streaming option. You're currently logged in as {{email}}.",
|
||||
"chooseCTAWithChangeSignIn": "Choose a streaming option. You're currently logged in as {{email}}. <0>Switch accounts.</0>",
|
||||
"enterStreamKey": "Enter your YouTube live stream key here.",
|
||||
"error": "Live Streaming failed. Please try again.",
|
||||
"errorAPI": "An error occurred while accessing your YouTube broadcasts. Please try logging in again.",
|
||||
@@ -1148,6 +1149,8 @@
|
||||
"offBy": "{{name}} stopped the recording",
|
||||
"on": "Recording started",
|
||||
"onBy": "{{name}} started the recording",
|
||||
"onByWithTranscription": "{{name}} started the recording. A transcript will also be available after the meeting.",
|
||||
"onWithTranscription": "Recording started. A transcript will also be available after the meeting.",
|
||||
"onlyRecordSelf": "Record only my audio and video streams",
|
||||
"pending": "Preparing to record the meeting…",
|
||||
"policyError": "You tried to start a recording too quickly. Please try again later!",
|
||||
@@ -1525,6 +1528,10 @@
|
||||
"failed": "Transcribing failed",
|
||||
"labelTooltip": "This meeting is being transcribed.",
|
||||
"labelTooltipExtra": "In addition, a transcript will be available later.",
|
||||
"off": "Transcription stopped",
|
||||
"on": "Transcription started",
|
||||
"onBy": "{{name}} started the transcription",
|
||||
"onWithRecording": "A transcript will also be available after the meeting.",
|
||||
"openClosedCaptions": "Open closed captions",
|
||||
"original": "Original",
|
||||
"sourceLanguageDesc": "Currently the meeting language is set to <b>{{sourceLanguage}}</b>. <br/> You can change it from ",
|
||||
|
||||
@@ -1489,19 +1489,29 @@ class API {
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyReceivedChatMessage(
|
||||
{ body, from, nick, privateMessage, ts } = {}) {
|
||||
{ body, from, nick, privateMessage, ts, messageId, replyToMessageId } = {}) {
|
||||
if (APP.conference.isLocalId(from)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._sendEvent({
|
||||
const event = {
|
||||
name: 'incoming-message',
|
||||
from,
|
||||
message: body,
|
||||
nick,
|
||||
privateMessage,
|
||||
stamp: ts
|
||||
});
|
||||
};
|
||||
|
||||
if (typeof messageId === 'string' && messageId !== '') {
|
||||
event.messageId = messageId;
|
||||
}
|
||||
|
||||
if (typeof replyToMessageId === 'string' && replyToMessageId !== '') {
|
||||
event.replyToMessageId = replyToMessageId;
|
||||
}
|
||||
|
||||
this._sendEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
6
modules/API/external/external_api.js
vendored
6
modules/API/external/external_api.js
vendored
@@ -806,7 +806,11 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* {{
|
||||
* 'from': from,//JID of the user that sent the message
|
||||
* 'nick': nick,//the nickname of the user that sent the message
|
||||
* 'message': txt//the text of the message
|
||||
* 'message': txt,//the text of the message
|
||||
* 'privateMessage': privateMessage,//whether the message is private
|
||||
* 'stamp': stamp,//optional timestamp when available
|
||||
* 'messageId': messageId,//optional XMPP message id when available
|
||||
* 'replyToMessageId': replyToMessageId//optional XEP-0461 reply target message id when available
|
||||
* }}
|
||||
* {@code outgoingMessage} - receives event notifications about outgoing
|
||||
* messages. The listener will receive object with the following structure:
|
||||
|
||||
4242
package-lock.json
generated
4242
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@
|
||||
"@giphy/js-fetch-api": "4.9.3",
|
||||
"@giphy/react-components": "6.9.4",
|
||||
"@giphy/react-native-sdk": "4.1.0",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.19/jitsi-excalidraw-0.0.19.tgz",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/0.18.5/jitsi-excalidraw-v0.18.5.tgz",
|
||||
"@jitsi/js-utils": "2.6.7",
|
||||
"@jitsi/logger": "2.1.1",
|
||||
"@jitsi/rnnoise-wasm": "0.2.1",
|
||||
@@ -72,8 +72,8 @@
|
||||
"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/v2143.0.0+733e66c6/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.23",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2144.0.0+94d0e53e/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.18.1",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
"pixelmatch": "5.3.0",
|
||||
|
||||
@@ -1,6 +1 @@
|
||||
*.tgz
|
||||
tsconfig.json
|
||||
.npmrc
|
||||
.git
|
||||
.gitignore
|
||||
node_modules
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
"name": "@jitsi/react-native-sdk",
|
||||
"version": "0.0.0",
|
||||
"description": "React Native SDK for Jitsi Meet.",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"main": "index.tsx",
|
||||
"license": "Apache-2.0",
|
||||
"author": "",
|
||||
"homepage": "https://jitsi.org",
|
||||
@@ -93,27 +92,9 @@
|
||||
"@babel/plugin-proposal-optional-chaining": "0.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"postinstall": "node sdk_instructions.js",
|
||||
"prepare": "node prepare_sdk.js && npm run build"
|
||||
"prepare": "node prepare_sdk.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"android",
|
||||
"ios",
|
||||
"index.tsx",
|
||||
"jitsi-meet-rnsdk.podspec",
|
||||
"prepare_sdk.js",
|
||||
"sdk_instructions.js",
|
||||
"update_dependencies.js",
|
||||
"update_sdk_dependencies.js",
|
||||
"README.md",
|
||||
"images",
|
||||
"sounds",
|
||||
"lang",
|
||||
"modules",
|
||||
"react"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/jitsi/jitsi-meet/issues"
|
||||
},
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig.native.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"emitDeclarationOnly": false
|
||||
},
|
||||
"include": [
|
||||
"index.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -61,10 +61,10 @@ type NotifyClickButtonKey = string |
|
||||
ParticipantMenuButtonsWithNotifyClick;
|
||||
|
||||
export type NotifyClickButton = NotifyClickButtonKey |
|
||||
{
|
||||
key: NotifyClickButtonKey;
|
||||
preventExecution: boolean;
|
||||
};
|
||||
{
|
||||
key: NotifyClickButtonKey;
|
||||
preventExecution: boolean;
|
||||
};
|
||||
|
||||
export type Sounds = 'ASKED_TO_UNMUTE_SOUND' |
|
||||
'E2EE_OFF_SOUND' |
|
||||
@@ -145,6 +145,7 @@ export interface IWhiteboardConfig {
|
||||
collabServerBaseUrl?: string;
|
||||
enabled?: boolean;
|
||||
limitUrl?: string;
|
||||
storageBackendUrl?: string;
|
||||
userLimit?: number;
|
||||
}
|
||||
|
||||
@@ -196,7 +197,14 @@ export interface IConfig {
|
||||
opusMaxAverageBitrate?: number | null;
|
||||
stereo?: boolean;
|
||||
};
|
||||
/**
|
||||
* @deprecated Use `transcription.autoTranscribeOnRecord` instead.
|
||||
*/
|
||||
autoCaptionOnRecord?: boolean;
|
||||
|
||||
/**
|
||||
* @deprecated Use `lobby.autoKnock` instead.
|
||||
*/
|
||||
autoKnockLobby?: boolean;
|
||||
backgroundAlpha?: number;
|
||||
bosh?: string;
|
||||
@@ -287,13 +295,22 @@ export interface IConfig {
|
||||
disableCameraTintForeground?: boolean;
|
||||
disableChat?: boolean;
|
||||
disableChatSmileys?: boolean;
|
||||
/**
|
||||
* @deprecated Use `deeplinking.disabled` instead.
|
||||
*/
|
||||
disableDeepLinking?: boolean;
|
||||
disableFilmstripAutohiding?: boolean;
|
||||
disableFocus?: boolean;
|
||||
disableIframeAPI?: boolean;
|
||||
/**
|
||||
* @deprecated Use `disabledSounds` instead.
|
||||
*/
|
||||
disableIncomingMessageSound?: boolean;
|
||||
disableInitialGUM?: boolean;
|
||||
disableInviteFunctions?: boolean;
|
||||
/**
|
||||
* @deprecated Use `disabledSounds` instead.
|
||||
*/
|
||||
disableJoinLeaveSounds?: boolean;
|
||||
disableLocalVideoFlip?: boolean;
|
||||
disableModeratorIndicator?: boolean;
|
||||
@@ -303,9 +320,15 @@ export interface IConfig {
|
||||
disableReactions?: boolean;
|
||||
disableReactionsInChat?: boolean;
|
||||
disableReactionsModeration?: boolean;
|
||||
/**
|
||||
* @deprecated Use `disabledSounds` instead.
|
||||
*/
|
||||
disableRecordAudioNotification?: boolean;
|
||||
disableRemoteControl?: boolean;
|
||||
disableRemoteMute?: boolean;
|
||||
/**
|
||||
* @deprecated Use `raisedHands.disableRemoveRaisedHandOnFocus` instead.
|
||||
*/
|
||||
disableRemoveRaisedHandOnFocus?: boolean;
|
||||
disableResponsiveTiles?: boolean;
|
||||
disableRtx?: boolean;
|
||||
@@ -315,6 +338,9 @@ export interface IConfig {
|
||||
disableShortcuts?: boolean;
|
||||
disableShowMoreStats?: boolean;
|
||||
disableSimulcast?: boolean;
|
||||
/**
|
||||
* @deprecated Use `speakerStats.disableSearch` instead.
|
||||
*/
|
||||
disableSpeakerStatsSearch?: boolean;
|
||||
disableThirdPartyRequests?: boolean;
|
||||
disableTileEnlargement?: boolean;
|
||||
@@ -359,6 +385,9 @@ export interface IConfig {
|
||||
enableEncodedTransformSupport?: boolean;
|
||||
enableForcedReload?: boolean;
|
||||
enableInsecureRoomNameWarning?: boolean;
|
||||
/**
|
||||
* @deprecated Use `lobby.enableChat` instead.
|
||||
*/
|
||||
enableLobbyChat?: boolean;
|
||||
enableNoAudioDetection?: boolean;
|
||||
enableNoisyMicDetection?: boolean;
|
||||
@@ -368,6 +397,9 @@ export interface IConfig {
|
||||
enableTalkWhileMuted?: boolean;
|
||||
enableTcc?: boolean;
|
||||
enableWebHIDFeature?: boolean;
|
||||
/**
|
||||
* @deprecated Use `welcomePage.disabled` instead.
|
||||
*/
|
||||
enableWelcomePage?: boolean;
|
||||
etherpad_base?: string;
|
||||
faceLandmarks?: {
|
||||
@@ -379,7 +411,14 @@ export interface IConfig {
|
||||
faceCenteringThreshold?: number;
|
||||
};
|
||||
feedbackPercentage?: number;
|
||||
/**
|
||||
* @deprecated Use `recordingService.enabled` instead.
|
||||
*/
|
||||
fileRecordingsServiceEnabled?: boolean;
|
||||
|
||||
/**
|
||||
* @deprecated Use `recordingService.sharingEnabled` instead.
|
||||
*/
|
||||
fileRecordingsServiceSharingEnabled?: boolean;
|
||||
fileSharing?: {
|
||||
apiUrl?: string;
|
||||
@@ -414,18 +453,27 @@ export interface IConfig {
|
||||
baseUrl?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
/**
|
||||
* @deprecated Use `gravatar.baseUrl` instead.
|
||||
*/
|
||||
gravatarBaseURL?: string;
|
||||
guestDialOutStatusUrl?: string;
|
||||
guestDialOutUrl?: string;
|
||||
helpCentreURL?: string;
|
||||
hiddenDomain?: string;
|
||||
hiddenPremeetingButtons?: Array<'microphone' | 'camera' | 'select-background' | 'invite' | 'settings'>;
|
||||
/**
|
||||
* @deprecated Use `breakoutRooms.hideAddRoomButton` instead.
|
||||
*/
|
||||
hideAddRoomButton?: boolean;
|
||||
hideConferenceSubject?: boolean;
|
||||
hideConferenceTimer?: boolean;
|
||||
hideDisplayName?: boolean;
|
||||
hideDominantSpeakerBadge?: boolean;
|
||||
hideEmailInSettings?: boolean;
|
||||
/**
|
||||
* @deprecated Use `securityUi.hideLobbyButton` instead.
|
||||
*/
|
||||
hideLobbyButton?: boolean;
|
||||
hideLoginButton?: boolean;
|
||||
hideParticipantsStats?: boolean;
|
||||
@@ -462,6 +510,9 @@ export interface IConfig {
|
||||
termsLink?: string;
|
||||
validatorRegExpString?: string;
|
||||
};
|
||||
/**
|
||||
* @deprecated Use `liveStreaming.enabled` instead.
|
||||
*/
|
||||
liveStreamingEnabled?: boolean;
|
||||
lobby?: {
|
||||
autoKnock?: boolean;
|
||||
@@ -523,6 +574,9 @@ export interface IConfig {
|
||||
};
|
||||
preferBosh?: boolean;
|
||||
preferVisitor?: boolean;
|
||||
/**
|
||||
* @deprecated Use `transcription.preferredLanguage` instead.
|
||||
*/
|
||||
preferredTranscribeLanguage?: string;
|
||||
prejoinConfig?: {
|
||||
enabled?: boolean;
|
||||
@@ -585,6 +639,9 @@ export interface IConfig {
|
||||
disabled?: boolean;
|
||||
order?: Array<'role' | 'name' | 'hasLeft'>;
|
||||
};
|
||||
/**
|
||||
* @deprecated Use `speakerStats.order` instead.
|
||||
*/
|
||||
speakerStatsOrder?: Array<'role' | 'name' | 'hasLeft'>;
|
||||
startAudioMuted?: number;
|
||||
startAudioOnly?: boolean;
|
||||
@@ -627,7 +684,14 @@ export interface IConfig {
|
||||
initialTimeout?: number;
|
||||
timeout?: number;
|
||||
};
|
||||
/**
|
||||
* @deprecated Use `transcription.useAppLanguage` instead.
|
||||
*/
|
||||
transcribeWithAppLanguage?: boolean;
|
||||
|
||||
/**
|
||||
* @deprecated Use `transcription.enabled` instead.
|
||||
*/
|
||||
transcribingEnabled?: boolean;
|
||||
transcription?: {
|
||||
autoCaptionOnTranscribe?: boolean;
|
||||
|
||||
@@ -89,6 +89,7 @@ import { default as IconRemoteControlStop } from './stop-remote-control.svg';
|
||||
import { default as IconStop } from './stop.svg';
|
||||
import { default as IconSubtitles } from './subtitles.svg';
|
||||
import { default as IconTileView } from './tile-view.svg';
|
||||
import { default as IconTranscription } from './transcription.svg';
|
||||
import { default as IconTrash } from './trash.svg';
|
||||
import { default as IconUserDeleted } from './user-deleted.svg';
|
||||
import { default as IconUser } from './user.svg';
|
||||
@@ -183,6 +184,7 @@ export const DEFAULT_ICON: Record<string, any> = {
|
||||
IconPlus,
|
||||
IconRaiseHand,
|
||||
IconRecord,
|
||||
IconTranscription,
|
||||
IconRecordAccount,
|
||||
IconRecordContact,
|
||||
IconRecordLead,
|
||||
|
||||
@@ -74,6 +74,7 @@ const {
|
||||
IconPlus,
|
||||
IconRaiseHand,
|
||||
IconRecord,
|
||||
IconTranscription,
|
||||
IconRecordAccount,
|
||||
IconRecordContact,
|
||||
IconRecordLead,
|
||||
@@ -194,6 +195,7 @@ export {
|
||||
IconPlus,
|
||||
IconRaiseHand,
|
||||
IconRecord,
|
||||
IconTranscription,
|
||||
IconRecordAccount,
|
||||
IconRecordContact,
|
||||
IconRecordLead,
|
||||
|
||||
4
react/features/base/icons/svg/transcription.svg
Normal file
4
react/features/base/icons/svg/transcription.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5852 12.75L17.327 16.2909C17.5098 16.6626 17.9593 16.8157 18.331 16.6329C18.7027 16.4501 18.8558 16.0006 18.673 15.6289L12.6414 3.36696C12.2255 2.52157 11.017 2.53122 10.6146 3.38315L4.5718 16.1797C4.39494 16.5542 4.55519 16.9012 4.92974 17.0781C5.3043 17.255 5.65131 17.0947 5.82818 16.7202L7.85021 12.75H15.5852ZM14.8474 11.25L11.6388 4.72714L8.55854 11.25H14.8474Z" />
|
||||
<path d="M3 20.25C3 19.8358 3.33579 19.5 3.75 19.5H20.25C20.6642 19.5 21 19.8358 21 20.25C21 20.6642 20.6642 21 20.25 21H3.75C3.33579 21 3 20.6642 3 20.25Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 684 B |
@@ -54,9 +54,16 @@ class AudioTrack extends Component<IProps> {
|
||||
_ref: React.RefObject<HTMLAudioElement>;
|
||||
|
||||
/**
|
||||
* The current timeout ID for play() retries.
|
||||
* The current timeout ID for attach or play retries. Shared so that
|
||||
* {@link _detachTrack} can cancel whichever retry is pending.
|
||||
*/
|
||||
_playTimeout: number | undefined;
|
||||
_retryTimeout: number | undefined;
|
||||
|
||||
/**
|
||||
* Tracks how many full re-attach cycles (attach retries + play retries exhausted) have occurred
|
||||
* to prevent infinite recovery loops.
|
||||
*/
|
||||
_reattachCount = 0;
|
||||
|
||||
/**
|
||||
* Default values for {@code AudioTrack} component's properties.
|
||||
@@ -190,10 +197,11 @@ class AudioTrack extends Component<IProps> {
|
||||
* Calls into the passed in track to associate the track with the component's audio element.
|
||||
*
|
||||
* @param {Object} track - The redux representation of the {@code JitsiLocalTrack}.
|
||||
* @param {number} retryCount - The number of previously failed attach retries.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_attachTrack(track?: ITrack) {
|
||||
_attachTrack(track?: ITrack, retryCount = 0) {
|
||||
const { id } = this.props;
|
||||
|
||||
if (!track?.jitsiTrack) {
|
||||
@@ -209,13 +217,24 @@ class AudioTrack extends Component<IProps> {
|
||||
}
|
||||
|
||||
track.jitsiTrack.attach(this._ref.current)
|
||||
.then(() => {
|
||||
if (retryCount !== 0) {
|
||||
logger.info(`Successfully attached audio track on element ${id} after ${retryCount} retries`);
|
||||
}
|
||||
this._play();
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
logger.error(
|
||||
`Attaching the remote track ${track.jitsiTrack} to video with id ${id} has failed with `,
|
||||
`Attaching the remote track ${track.jitsiTrack} to audio with id ${id} has failed with `,
|
||||
error);
|
||||
})
|
||||
.finally(() => {
|
||||
this._play();
|
||||
|
||||
if (retryCount < 3) {
|
||||
this._retryTimeout = window.setTimeout(() => {
|
||||
this._attachTrack(track, retryCount + 1);
|
||||
}, 1000);
|
||||
} else {
|
||||
logger.error(`Failed to attach audio track on element ${id} after ${retryCount} retries`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,8 +248,8 @@ class AudioTrack extends Component<IProps> {
|
||||
*/
|
||||
_detachTrack(track?: ITrack) {
|
||||
if (this._ref?.current && track?.jitsiTrack) {
|
||||
clearTimeout(this._playTimeout);
|
||||
this._playTimeout = undefined;
|
||||
clearTimeout(this._retryTimeout);
|
||||
this._retryTimeout = undefined;
|
||||
track.jitsiTrack.detach(this._ref.current);
|
||||
}
|
||||
}
|
||||
@@ -272,22 +291,33 @@ class AudioTrack extends Component<IProps> {
|
||||
.then(() => {
|
||||
if (retries !== 0) {
|
||||
// success after some failures
|
||||
this._playTimeout = undefined;
|
||||
this._retryTimeout = undefined;
|
||||
sendAnalytics(createAudioPlaySuccessEvent(id));
|
||||
logger.info(`Successfully played audio track! retries: ${retries}`);
|
||||
}
|
||||
this._reattachCount = 0;
|
||||
}, e => {
|
||||
logger.error(`Failed to play audio track on audio element ${id}! retry: ${retries} ; Error:`, e);
|
||||
|
||||
if (retries < 3) {
|
||||
this._playTimeout = window.setTimeout(() => this._play(retries + 1), 1000);
|
||||
this._retryTimeout = window.setTimeout(() => this._play(retries + 1), 1000);
|
||||
|
||||
if (retries === 0) {
|
||||
// send only 1 error event.
|
||||
sendAnalytics(createAudioPlayErrorEvent(id));
|
||||
}
|
||||
} else {
|
||||
this._playTimeout = undefined;
|
||||
this._retryTimeout = undefined;
|
||||
|
||||
// Play retries exhausted — re-attach the track and try again (once).
|
||||
if (this._reattachCount < 1) {
|
||||
this._reattachCount++;
|
||||
logger.warn(`Play retries exhausted for audio element ${id}, re-attaching track`);
|
||||
this._detachTrack(this.props.audioTrack);
|
||||
this._attachTrack(this.props.audioTrack);
|
||||
} else {
|
||||
logger.error(`Audio recovery failed for element ${id} after re-attach`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,11 +146,18 @@ export function shouldRenderVideoTrack(
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getSoundFileSrc = (file: string, language: string): string => {
|
||||
if (!AudioSupportedLanguage[language as keyof typeof AudioSupportedLanguage]
|
||||
|| language === AudioSupportedLanguage.en) {
|
||||
if (!language) {
|
||||
return file;
|
||||
}
|
||||
|
||||
// Normalize language code: 'fr-CA' -> 'frCA' to match AudioSupportedLanguage enum and file naming
|
||||
const normalizedLanguage = language.replace('-', '');
|
||||
|
||||
if (!AudioSupportedLanguage[normalizedLanguage as keyof typeof AudioSupportedLanguage]
|
||||
|| normalizedLanguage === AudioSupportedLanguage.en) {
|
||||
return file;
|
||||
}
|
||||
const fileTokens = file.split('.');
|
||||
|
||||
return `${fileTokens[0]}_${language}.${fileTokens[1]}`;
|
||||
return `${fileTokens[0]}_${normalizedLanguage}.${fileTokens[1]}`;
|
||||
};
|
||||
|
||||
@@ -789,8 +789,9 @@ function _localRecordingUpdated({ dispatch, getState }: IStore, conference: IJit
|
||||
participantId: string, newValue: boolean) {
|
||||
const state = getState();
|
||||
const participant = getParticipantById(state, participantId);
|
||||
const currentValue = participant?.localRecording ?? false;
|
||||
|
||||
if (participant?.localRecording === newValue) {
|
||||
if (currentValue === newValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -370,6 +370,7 @@ export const colorMap = {
|
||||
recordingHighlightButtonIconDisabled: 'text03', // Recording highlight button disabled icon color
|
||||
recordingNotificationText: 'surface01', // Recording notification text color
|
||||
recordingNotificationAction: 'action01', // Recording notification action color
|
||||
transcriptionIndicator: 'success01', // Transcription indicator background
|
||||
|
||||
// Virtual Background
|
||||
virtualBackgroundBackground: 'ui01', // Virtual background picker background
|
||||
|
||||
@@ -53,12 +53,12 @@ const IconButton: React.FC<IIconButtonProps> = ({
|
||||
<TouchableHighlight
|
||||
accessibilityLabel = { accessibilityLabel }
|
||||
disabled = { disabled }
|
||||
id = { id }
|
||||
onPress = { onPress }
|
||||
style = { [
|
||||
iconButtonContainerStyles,
|
||||
style
|
||||
] as ViewStyle[] }
|
||||
testID = { id }
|
||||
underlayColor = { underlayColor }>
|
||||
<Icon
|
||||
color = { color }
|
||||
|
||||
@@ -164,6 +164,7 @@ const Input = forwardRef<TextInput, IProps>(({
|
||||
focused && styles.inputFocused,
|
||||
error && styles.inputError
|
||||
] as StyleProp<TextStyle> }
|
||||
testID = { id }
|
||||
textContentType = { textContentType }
|
||||
value = { typeof value === 'number' ? `${value}` : value } />
|
||||
{ clearable && !disabled && value !== '' && (
|
||||
|
||||
@@ -326,6 +326,7 @@ export interface IPalette {
|
||||
recordingNotificationAction: string;
|
||||
recordingNotificationText: string;
|
||||
recordingText: string;
|
||||
transcriptionIndicator: string;
|
||||
securityDialogBackground: string;
|
||||
securityDialogBorder: string;
|
||||
securityDialogSecondaryText: string;
|
||||
|
||||
@@ -2,15 +2,20 @@ import { merge } from 'lodash-es';
|
||||
|
||||
import * as jitsiTokens from './jitsiTokens.json';
|
||||
import * as tokens from './tokens.json';
|
||||
import { IPalette } from './types';
|
||||
|
||||
/**
|
||||
* Creates the color tokens based on the color theme and the association map.
|
||||
*
|
||||
* @param {Object} colorMap - A map between the token name and the actual color value.
|
||||
* @param {Object} customTokens - Optional custom token overrides to merge before resolution.
|
||||
* This allows custom branding colors to propagate through semantic token references.
|
||||
* For example, if customTokens contains { action01: '#custom' }, then any semantic token
|
||||
* that references 'action01' (like 'prejoinActionButtonPrimary') will resolve to '#custom'.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function createColorTokens(colorMap: Object): any {
|
||||
const allTokens = merge({}, tokens, jitsiTokens);
|
||||
export function createColorTokens(colorMap: Object, customTokens?: Partial<IPalette>): any {
|
||||
const allTokens = merge({}, tokens, jitsiTokens, customTokens || {});
|
||||
const result: any = {};
|
||||
|
||||
// First pass: resolve tokens that reference allTokens directly
|
||||
|
||||
@@ -1,11 +1,38 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { NIL, parse as parseUUID } from 'uuid';
|
||||
import zxcvbn from 'zxcvbn';
|
||||
|
||||
// The null UUID.
|
||||
const NIL_UUID = parseUUID(NIL);
|
||||
|
||||
const _zxcvbnCache = new Map();
|
||||
let _zxcvbn: ((password: string) => { score: number; }) | null = null;
|
||||
|
||||
/**
|
||||
* Triggers the asynchronous load of the zxcvbn library if not already loaded.
|
||||
* Can be called early (e.g. on config load) to ensure the library is ready
|
||||
* before the first call to {@link isInsecureRoomName}.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function preloadZxcvbn() {
|
||||
_ensureZxcvbn();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the asynchronous load of the zxcvbn library if not already loaded.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function _ensureZxcvbn() {
|
||||
if (_zxcvbn !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
import(/* webpackChunkName: "zxcvbn" */ 'zxcvbn').then((m: any) => {
|
||||
_zxcvbn = m.default ?? m;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given string is a valid UUID or not.
|
||||
@@ -27,16 +54,23 @@ function isValidUUID(str: string) {
|
||||
|
||||
/**
|
||||
* Checks a room name and caches the result.
|
||||
* Returns undefined if zxcvbn is not yet loaded.
|
||||
*
|
||||
* @param {string} roomName - The room name.
|
||||
* @returns {Object}
|
||||
* @returns {Object|undefined}
|
||||
*/
|
||||
function _checkRoomName(roomName = '') {
|
||||
if (_zxcvbnCache.has(roomName)) {
|
||||
return _zxcvbnCache.get(roomName);
|
||||
}
|
||||
|
||||
const result = zxcvbn(roomName);
|
||||
_ensureZxcvbn();
|
||||
|
||||
if (!_zxcvbn) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = _zxcvbn(roomName);
|
||||
|
||||
_zxcvbnCache.set(roomName, result);
|
||||
|
||||
@@ -45,6 +79,7 @@ function _checkRoomName(roomName = '') {
|
||||
|
||||
/**
|
||||
* Returns true if the room name is considered a weak (insecure) one.
|
||||
* Returns false (treats as secure) while the zxcvbn library is still loading.
|
||||
*
|
||||
* @param {string} roomName - The room name.
|
||||
* @returns {boolean}
|
||||
@@ -52,5 +87,5 @@ function _checkRoomName(roomName = '') {
|
||||
export default function isInsecureRoomName(roomName = ''): boolean {
|
||||
|
||||
// room names longer than 200 chars we consider secure
|
||||
return !isValidUUID(roomName) && (roomName.length < 200 && _checkRoomName(roomName).score < 3);
|
||||
return !isValidUUID(roomName) && (roomName.length < 200 && (_checkRoomName(roomName)?.score ?? 3) < 3);
|
||||
}
|
||||
|
||||
@@ -254,6 +254,7 @@ export function moveToRoom(roomId?: string) {
|
||||
}
|
||||
|
||||
APP.conference.joinRoom(_roomId, {
|
||||
isBreakoutRoom: true,
|
||||
startWithAudioMuted: isAudioMuted,
|
||||
startWithVideoMuted: isVideoMuted
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* displayName: string
|
||||
* hasRead: boolean,
|
||||
* id: string,
|
||||
* messageType: string,
|
||||
* messageType: ChatMessageType,
|
||||
* message: string,
|
||||
* timestamp: string,
|
||||
* }
|
||||
|
||||
@@ -42,7 +42,7 @@ import { ChatTabs } from './constants';
|
||||
* displayName: string,
|
||||
* hasRead: boolean,
|
||||
* message: string,
|
||||
* messageType: string,
|
||||
* messageType: ChatMessageType,
|
||||
* timestamp: string,
|
||||
* isReaction: boolean
|
||||
* }}
|
||||
|
||||
@@ -151,7 +151,7 @@ const useStyles = makeStyles<{
|
||||
|
||||
'*': {
|
||||
userSelect: 'text',
|
||||
'-webkit-user-select': 'text'
|
||||
WebkitUserSelect: 'text'
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -19,17 +19,17 @@ export const INCOMING_MSG_SOUND_ID = 'INCOMING_MSG_SOUND';
|
||||
/**
|
||||
* The {@code messageType} of error (system) messages.
|
||||
*/
|
||||
export const MESSAGE_TYPE_ERROR = 'error';
|
||||
export const MESSAGE_TYPE_ERROR = 'error' as const;
|
||||
|
||||
/**
|
||||
* The {@code messageType} of local messages.
|
||||
*/
|
||||
export const MESSAGE_TYPE_LOCAL = 'local';
|
||||
export const MESSAGE_TYPE_LOCAL = 'local' as const;
|
||||
|
||||
/**
|
||||
* The {@code messageType} of remote messages.
|
||||
*/
|
||||
export const MESSAGE_TYPE_REMOTE = 'remote';
|
||||
export const MESSAGE_TYPE_REMOTE = 'remote' as const;
|
||||
|
||||
export const SMALL_WIDTH_THRESHOLD = 580;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getParticipantById, isPrivateChatEnabled } from '../base/participants/f
|
||||
import { IParticipant } from '../base/participants/types';
|
||||
import { escapeRegexp } from '../base/util/helpers';
|
||||
import { arePollsDisabled } from '../conference/functions.any';
|
||||
import { getCustomPanelWidth } from '../custom-panel/functions';
|
||||
import { isFileSharingEnabled } from '../file-sharing/functions.any';
|
||||
import { getParticipantsPaneWidth } from '../participants-pane/functions';
|
||||
import { isCCTabEnabled } from '../subtitles/functions.any';
|
||||
@@ -311,7 +312,10 @@ export function isSendGroupChatDisabled(state: IReduxState): boolean {
|
||||
export function getChatMaxSize(state: IReduxState): number {
|
||||
const { clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
return Math.max(clientWidth - getParticipantsPaneWidth(state) - VIDEO_SPACE_MIN_SIZE, 0);
|
||||
return Math.max(
|
||||
clientWidth - getParticipantsPaneWidth(state) - getCustomPanelWidth(state) - VIDEO_SPACE_MIN_SIZE,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -406,7 +406,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||
/* eslint-disable max-params */
|
||||
(participantId: string, message: string, timestamp: number,
|
||||
displayName: string, isFromVisitor: boolean, messageId: string, source: string) => {
|
||||
displayName: string, isFromVisitor: boolean, messageId: string, source: string, replyToId?: string) => {
|
||||
/* eslint-enable max-params */
|
||||
_onConferenceMessageReceived(store, {
|
||||
// in case of messages coming from visitors we can have unknown id
|
||||
@@ -416,6 +416,7 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
displayName,
|
||||
isFromVisitor,
|
||||
messageId,
|
||||
replyToMessageId: replyToId,
|
||||
source,
|
||||
privateMessage: false
|
||||
});
|
||||
@@ -441,7 +442,8 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
|
||||
(participantId: string, message: string, timestamp: number, messageId: string, displayName?: string, isFromVisitor?: boolean) => {
|
||||
(participantId: string, message: string, timestamp: number, messageId: string, displayName?: string,
|
||||
isFromVisitor?: boolean, replyToId?: string) => {
|
||||
_onConferenceMessageReceived(store, {
|
||||
participantId,
|
||||
message,
|
||||
@@ -449,7 +451,8 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
displayName,
|
||||
messageId,
|
||||
privateMessage: true,
|
||||
isFromVisitor
|
||||
isFromVisitor,
|
||||
replyToMessageId: replyToId
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -468,9 +471,9 @@ function _addChatMsgListener(conference: IJitsiConference, store: IStore) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onConferenceMessageReceived(store: IStore,
|
||||
{ displayName, isFromVisitor, message, messageId, participantId, privateMessage, timestamp, source }: {
|
||||
{ displayName, isFromVisitor, message, messageId, participantId, privateMessage, replyToMessageId, timestamp, source }: {
|
||||
displayName?: string; isFromVisitor?: boolean; message: string; messageId?: string;
|
||||
participantId: string; privateMessage: boolean; source?: string; timestamp: number; }
|
||||
participantId: string; privateMessage: boolean; replyToMessageId?: string; source?: string; timestamp: number; }
|
||||
) {
|
||||
|
||||
const isGif = isGifEnabled(store.getState()) && isGifMessage(message);
|
||||
@@ -490,6 +493,7 @@ function _onConferenceMessageReceived(store: IStore,
|
||||
lobbyChat: false,
|
||||
timestamp,
|
||||
messageId,
|
||||
replyToMessageId,
|
||||
source
|
||||
}, true, isGif);
|
||||
}
|
||||
@@ -599,9 +603,10 @@ function getLobbyChatDisplayName(state: IReduxState, participantId: string) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
{ displayName, isFromVisitor, lobbyChat, message, messageId, participantId, privateMessage, source, timestamp }: {
|
||||
{ displayName, isFromVisitor, lobbyChat, message, messageId, participantId, privateMessage, replyToMessageId, source, timestamp }: {
|
||||
displayName?: string; isFromVisitor?: boolean; lobbyChat: boolean; message: string;
|
||||
messageId?: string; participantId: string; privateMessage: boolean; source?: string; timestamp: number; },
|
||||
messageId?: string; participantId: string; privateMessage: boolean; replyToMessageId?: string;
|
||||
source?: string; timestamp: number; },
|
||||
shouldPlaySound = true,
|
||||
isReaction = false
|
||||
) {
|
||||
@@ -652,6 +657,7 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
recipient: getParticipantDisplayName(state, localParticipant?.id ?? ''),
|
||||
timestamp: millisecondsTimestamp,
|
||||
messageId,
|
||||
replyToMessageId,
|
||||
isReaction,
|
||||
isFromVisitor,
|
||||
isFromGuest: source === 'guest'
|
||||
@@ -687,7 +693,9 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
from: participantId,
|
||||
nick: notificationDisplayName,
|
||||
privateMessage,
|
||||
ts: timestamp
|
||||
ts: timestamp,
|
||||
messageId: newMessage.messageId,
|
||||
replyToMessageId: newMessage.replyToMessageId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
|
||||
privateMessage: action.privateMessage,
|
||||
lobbyChat: action.lobbyChat,
|
||||
recipient: action.recipient,
|
||||
replyToMessageId: action.replyToMessageId,
|
||||
sentToVisitor: Boolean(action.sentToVisitor),
|
||||
timestamp: action.timestamp
|
||||
};
|
||||
|
||||
@@ -3,9 +3,20 @@ import { WithTranslation } from 'react-i18next';
|
||||
import { IStore } from '../app/types';
|
||||
import { IFileMetadata } from '../file-sharing/types';
|
||||
|
||||
import {
|
||||
MESSAGE_TYPE_ERROR,
|
||||
MESSAGE_TYPE_LOCAL,
|
||||
MESSAGE_TYPE_REMOTE
|
||||
} from './constants';
|
||||
|
||||
export type ChatMessageType =
|
||||
| typeof MESSAGE_TYPE_LOCAL
|
||||
| typeof MESSAGE_TYPE_ERROR
|
||||
| typeof MESSAGE_TYPE_REMOTE;
|
||||
|
||||
export interface IMessage {
|
||||
displayName: string;
|
||||
error?: Object;
|
||||
error?: unknown;
|
||||
fileMetadata?: IFileMetadata;
|
||||
isFromGuest?: boolean;
|
||||
isFromVisitor?: boolean;
|
||||
@@ -13,11 +24,15 @@ export interface IMessage {
|
||||
lobbyChat: boolean;
|
||||
message: string;
|
||||
messageId: string;
|
||||
messageType: string;
|
||||
messageType: ChatMessageType;
|
||||
participantId: string;
|
||||
privateMessage: boolean;
|
||||
reactions: Map<string, Set<string>>;
|
||||
recipient: string;
|
||||
/**
|
||||
* When set, XMPP message id of the message this one replies to (XEP-0461), from lib-jitsi-meet.
|
||||
*/
|
||||
replyToMessageId?: string;
|
||||
sentToVisitor?: boolean;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import { openHighlightDialog } from '../../../recording/actions.native';
|
||||
import HighlightButton from '../../../recording/components/Recording/native/HighlightButton';
|
||||
import RecordingLabel from '../../../recording/components/native/RecordingLabel';
|
||||
import TranscribingLabel from '../../../recording/components/native/TranscribingLabel';
|
||||
import { isLiveStreamingRunning } from '../../../recording/functions';
|
||||
import VisitorsCountLabel from '../../../visitors/components/native/VisitorsCountLabel';
|
||||
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
LABEL_ID_RAISED_HANDS_COUNT,
|
||||
LABEL_ID_RECORDING,
|
||||
LABEL_ID_STREAMING,
|
||||
LABEL_ID_TRANSCRIBING,
|
||||
LABEL_ID_VISITORS_COUNT,
|
||||
LabelHitSlop
|
||||
} from './constants';
|
||||
@@ -47,6 +49,11 @@ const AlwaysOnLabels = ({ createOnPress }: IProps) => {
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
</TouchableOpacity>
|
||||
}
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { createOnPress(LABEL_ID_TRANSCRIBING) } >
|
||||
<TranscribingLabel />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
hitSlop = { LabelHitSlop }
|
||||
onPress = { openHighlightDialogCallback }>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import RecordingExpandedLabel from '../../../recording/components/native/RecordingExpandedLabel';
|
||||
import TranscribingExpandedLabel from '../../../recording/components/native/TranscribingExpandedLabel';
|
||||
import VideoQualityExpandedLabel from '../../../video-quality/components/VideoQualityExpandedLabel.native';
|
||||
|
||||
import InsecureRoomNameExpandedLabel from './InsecureRoomNameExpandedLabel';
|
||||
@@ -22,6 +23,7 @@ export const EXPANDED_LABEL_TIMEOUT = 5000;
|
||||
export const LABEL_ID_QUALITY = 'quality';
|
||||
export const LABEL_ID_RECORDING = 'recording';
|
||||
export const LABEL_ID_STREAMING = 'streaming';
|
||||
export const LABEL_ID_TRANSCRIBING = 'transcribing';
|
||||
export const LABEL_ID_INSECURE_ROOM_NAME = 'insecure-room-name';
|
||||
export const LABEL_ID_RAISED_HANDS_COUNT = 'raised-hands-count';
|
||||
export const LABEL_ID_VISITORS_COUNT = 'visitors-count';
|
||||
@@ -56,6 +58,10 @@ export const EXPANDED_LABELS: {
|
||||
},
|
||||
alwaysOn: true
|
||||
},
|
||||
[LABEL_ID_TRANSCRIBING]: {
|
||||
component: TranscribingExpandedLabel,
|
||||
alwaysOn: true
|
||||
},
|
||||
[LABEL_ID_INSECURE_ROOM_NAME]: {
|
||||
component: InsecureRoomNameExpandedLabel
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import E2EELabel from '../../../e2ee/components/E2EELabel';
|
||||
import HighlightButton from '../../../recording/components/Recording/web/HighlightButton';
|
||||
import RecordingLabel from '../../../recording/components/web/RecordingLabel';
|
||||
import TranscribingLabel from '../../../recording/components/web/TranscribingLabel';
|
||||
import { showToolbox } from '../../../toolbox/actions.web';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.web';
|
||||
import VideoQualityLabel from '../../../video-quality/components/VideoQualityLabel.web';
|
||||
@@ -79,6 +80,7 @@ const COMPONENTS: Array<{
|
||||
<>
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
|
||||
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
|
||||
<TranscribingLabel />
|
||||
</>
|
||||
),
|
||||
id: 'recording'
|
||||
|
||||
@@ -12,3 +12,18 @@ export const CUSTOM_PANEL_OPEN = 'CUSTOM_PANEL_OPEN';
|
||||
* Action type to enable or disable the custom panel dynamically.
|
||||
*/
|
||||
export const SET_CUSTOM_PANEL_ENABLED = 'SET_CUSTOM_PANEL_ENABLED';
|
||||
|
||||
/**
|
||||
* Action type to set the custom panel width (responsive adjustments).
|
||||
*/
|
||||
export const SET_CUSTOM_PANEL_WIDTH = 'SET_CUSTOM_PANEL_WIDTH';
|
||||
|
||||
/**
|
||||
* Action type to set the user-preferred custom panel width (user drag).
|
||||
*/
|
||||
export const SET_USER_CUSTOM_PANEL_WIDTH = 'SET_USER_CUSTOM_PANEL_WIDTH';
|
||||
|
||||
/**
|
||||
* Action type to indicate whether the custom panel is being resized.
|
||||
*/
|
||||
export const SET_CUSTOM_PANEL_IS_RESIZING = 'SET_CUSTOM_PANEL_IS_RESIZING';
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import {
|
||||
CUSTOM_PANEL_CLOSE,
|
||||
CUSTOM_PANEL_OPEN,
|
||||
SET_CUSTOM_PANEL_ENABLED
|
||||
SET_CUSTOM_PANEL_ENABLED,
|
||||
SET_CUSTOM_PANEL_IS_RESIZING,
|
||||
SET_CUSTOM_PANEL_WIDTH,
|
||||
SET_USER_CUSTOM_PANEL_WIDTH
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Action to close the custom panel.
|
||||
*
|
||||
* @returns {Object} The action object.
|
||||
* NOTE: this action is used in the branding files.
|
||||
*/
|
||||
export function close() {
|
||||
return {
|
||||
@@ -38,3 +42,42 @@ export function setCustomPanelEnabled(enabled: boolean) {
|
||||
enabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the custom panel width (used for responsive adjustments).
|
||||
*
|
||||
* @param {number} width - The new width of the custom panel.
|
||||
* @returns {Object} The action object.
|
||||
*/
|
||||
export function setCustomPanelWidth(width: number) {
|
||||
return {
|
||||
type: SET_CUSTOM_PANEL_WIDTH,
|
||||
width
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user-preferred custom panel width (triggered by user drag).
|
||||
*
|
||||
* @param {number} width - The new width of the custom panel.
|
||||
* @returns {Object} The action object.
|
||||
*/
|
||||
export function setUserCustomPanelWidth(width: number) {
|
||||
return {
|
||||
type: SET_USER_CUSTOM_PANEL_WIDTH,
|
||||
width
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the user is currently resizing the custom panel.
|
||||
*
|
||||
* @param {boolean} resizing - Whether the panel is being resized.
|
||||
* @returns {Object} The action object.
|
||||
*/
|
||||
export function setCustomPanelIsResizing(resizing: boolean) {
|
||||
return {
|
||||
type: SET_CUSTOM_PANEL_IS_RESIZING,
|
||||
resizing
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,260 @@
|
||||
/**
|
||||
* Custom panel placeholder component.
|
||||
* This file is overridden by jitsi-meet-branding at build time
|
||||
* to provide the actual panel implementation with iframe content.
|
||||
*
|
||||
* @returns {null} This placeholder renders nothing.
|
||||
*/
|
||||
const CustomPanel = (): null => null;
|
||||
import { throttle } from 'lodash-es';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
export default CustomPanel;
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { isTouchDevice, shouldEnableResize } from '../../../base/environment/utils';
|
||||
import { setCustomPanelIsResizing, setUserCustomPanelWidth } from '../../actions.web';
|
||||
import {
|
||||
CUSTOM_PANEL_DRAG_HANDLE_HEIGHT,
|
||||
CUSTOM_PANEL_DRAG_HANDLE_OFFSET,
|
||||
CUSTOM_PANEL_DRAG_HANDLE_WIDTH,
|
||||
CUSTOM_PANEL_TOUCH_HANDLE_SIZE,
|
||||
DEFAULT_CUSTOM_PANEL_WIDTH
|
||||
} from '../../constants';
|
||||
import { getCustomPanelMaxSize, getCustomPanelOpen, isCustomPanelEnabled } from '../../functions';
|
||||
|
||||
import CustomPanelContent from './CustomPanelContent';
|
||||
|
||||
interface IStylesProps {
|
||||
|
||||
/**
|
||||
* Whether the panel is currently being resized.
|
||||
*/
|
||||
isResizing: boolean;
|
||||
|
||||
/**
|
||||
* Whether the device supports touch.
|
||||
*/
|
||||
isTouch: boolean;
|
||||
|
||||
/**
|
||||
* Whether resize is enabled.
|
||||
*/
|
||||
resizeEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The current width of the panel.
|
||||
*/
|
||||
width: number;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles<IStylesProps>()((theme, { isResizing, isTouch, resizeEnabled, width }) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.palette.ui01,
|
||||
flexShrink: 0,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
transition: isResizing ? undefined : 'width .16s ease-in-out',
|
||||
width: `${width}px`,
|
||||
zIndex: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
|
||||
// On non-touch devices (desktop), show handle on hover.
|
||||
// On touch devices, handle is always visible if resize is enabled.
|
||||
...(!isTouch && {
|
||||
'&:hover, &:focus-within': {
|
||||
'& .customPanelDragHandleContainer': {
|
||||
visibility: 'visible'
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
'@media (max-width: 580px)': {
|
||||
height: '100dvh',
|
||||
position: 'fixed',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: '100%',
|
||||
zIndex: 301
|
||||
}
|
||||
},
|
||||
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
|
||||
dragHandleContainer: {
|
||||
height: '100%',
|
||||
// Touch devices need larger hit target but positioned to not take extra space.
|
||||
width: isTouch ? `${CUSTOM_PANEL_TOUCH_HANDLE_SIZE}px` : `${CUSTOM_PANEL_DRAG_HANDLE_WIDTH}px`,
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
cursor: 'col-resize',
|
||||
display: resizeEnabled ? 'flex' : 'none',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
// On touch devices, always visible if resize enabled. On desktop, hidden by default.
|
||||
visibility: (isTouch && resizeEnabled) ? 'visible' : 'hidden',
|
||||
// Position on LEFT edge of panel (custom panel is rightmost in layout).
|
||||
left: isTouch
|
||||
? `${CUSTOM_PANEL_DRAG_HANDLE_OFFSET
|
||||
- Math.floor((CUSTOM_PANEL_TOUCH_HANDLE_SIZE - CUSTOM_PANEL_DRAG_HANDLE_WIDTH) / 2)}px`
|
||||
: `${CUSTOM_PANEL_DRAG_HANDLE_OFFSET}px`,
|
||||
top: 0,
|
||||
zIndex: 2,
|
||||
// Prevent touch scrolling while dragging.
|
||||
touchAction: 'none',
|
||||
|
||||
'&:hover': {
|
||||
'& .customPanelDragHandle': {
|
||||
backgroundColor: theme.palette.icon01
|
||||
}
|
||||
},
|
||||
|
||||
'&.visible': {
|
||||
visibility: 'visible',
|
||||
|
||||
'& .customPanelDragHandle': {
|
||||
backgroundColor: theme.palette.icon01
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
dragHandle: {
|
||||
// Keep the same visual appearance on all devices.
|
||||
backgroundColor: theme.palette.icon02,
|
||||
height: `${CUSTOM_PANEL_DRAG_HANDLE_HEIGHT}px`,
|
||||
width: `${CUSTOM_PANEL_DRAG_HANDLE_WIDTH / 3}px`,
|
||||
borderRadius: '1px',
|
||||
// Make more visible when actively shown on touch.
|
||||
...(isTouch && resizeEnabled && {
|
||||
backgroundColor: theme.palette.icon01
|
||||
})
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Custom panel container component that handles resize, close button,
|
||||
* and renders CustomPanelContent inside it.
|
||||
*
|
||||
* @returns {JSX.Element | null} The custom panel or null if not open.
|
||||
*/
|
||||
export default function CustomPanel(): JSX.Element | null {
|
||||
const dispatch = useDispatch();
|
||||
const enabled = useSelector(isCustomPanelEnabled);
|
||||
const paneOpen = useSelector(getCustomPanelOpen);
|
||||
const panelWidth = useSelector((state: IReduxState) =>
|
||||
state['features/custom-panel']?.width?.current ?? DEFAULT_CUSTOM_PANEL_WIDTH);
|
||||
const isResizing = useSelector((state: IReduxState) =>
|
||||
state['features/custom-panel']?.isResizing ?? false);
|
||||
const maxPanelWidth = useSelector(getCustomPanelMaxSize);
|
||||
|
||||
const isTouch = isTouchDevice();
|
||||
const resizeEnabled = shouldEnableResize();
|
||||
const { classes, cx } = useStyles({ isResizing, width: panelWidth, isTouch, resizeEnabled });
|
||||
|
||||
const [ isMouseDown, setIsMouseDown ] = useState(false);
|
||||
const [ mousePosition, setMousePosition ] = useState<number | null>(null);
|
||||
const [ dragPanelWidth, setDragPanelWidth ] = useState<number | null>(null);
|
||||
|
||||
/**
|
||||
* Handles pointer down on the drag handle.
|
||||
* Supports both mouse and touch events via Pointer Events API.
|
||||
*
|
||||
* @param {React.PointerEvent} e - The pointer down event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDragHandlePointerDown = useCallback((e: React.PointerEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Capture the pointer to ensure we receive all pointer events
|
||||
// even if the pointer moves outside the element.
|
||||
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
||||
|
||||
setIsMouseDown(true);
|
||||
setMousePosition(e.clientX);
|
||||
setDragPanelWidth(panelWidth);
|
||||
|
||||
dispatch(setCustomPanelIsResizing(true));
|
||||
|
||||
document.body.style.cursor = 'col-resize';
|
||||
document.body.style.userSelect = 'none';
|
||||
}, [ panelWidth, dispatch ]);
|
||||
|
||||
/**
|
||||
* Handles pointer up to end drag resize.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDragPointerUp = useCallback(() => {
|
||||
if (isMouseDown) {
|
||||
setIsMouseDown(false);
|
||||
dispatch(setCustomPanelIsResizing(false));
|
||||
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
}
|
||||
}, [ isMouseDown, dispatch ]);
|
||||
|
||||
/**
|
||||
* Handles pointer move during drag resize.
|
||||
* Handle is on the LEFT edge, so dragging left (negative diff) widens the panel.
|
||||
*
|
||||
* @param {PointerEvent} e - The pointermove event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onPanelResize = useCallback(throttle((e: PointerEvent) => {
|
||||
if (isMouseDown && mousePosition !== null && dragPanelWidth !== null) {
|
||||
const diff = e.clientX - mousePosition;
|
||||
|
||||
// Handle is on LEFT edge: dragging left (negative diff) increases width.
|
||||
const newWidth = Math.max(
|
||||
Math.min(dragPanelWidth - diff, maxPanelWidth),
|
||||
DEFAULT_CUSTOM_PANEL_WIDTH
|
||||
);
|
||||
|
||||
if (newWidth !== panelWidth) {
|
||||
dispatch(setUserCustomPanelWidth(newWidth));
|
||||
}
|
||||
}
|
||||
}, 50, {
|
||||
leading: true,
|
||||
trailing: false
|
||||
}), [ isMouseDown, mousePosition, dragPanelWidth, panelWidth, maxPanelWidth, dispatch ]);
|
||||
|
||||
// Set up global event listeners for drag tracking.
|
||||
useEffect(() => {
|
||||
document.addEventListener('pointerup', onDragPointerUp);
|
||||
document.addEventListener('pointermove', onPanelResize);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('pointerup', onDragPointerUp);
|
||||
document.removeEventListener('pointermove', onPanelResize);
|
||||
};
|
||||
}, [ onDragPointerUp, onPanelResize ]);
|
||||
|
||||
if (!enabled || !paneOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { classes.container }
|
||||
id = 'custom-panel'>
|
||||
<div
|
||||
className = { cx(
|
||||
classes.dragHandleContainer,
|
||||
(isMouseDown || isResizing) && 'visible',
|
||||
'customPanelDragHandleContainer'
|
||||
) }
|
||||
onPointerDown = { onDragHandlePointerDown }>
|
||||
<div className = { cx(classes.dragHandle, 'customPanelDragHandle') } />
|
||||
</div>
|
||||
<div className = { classes.contentContainer }>
|
||||
<CustomPanelContent />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Custom panel content placeholder component.
|
||||
* This file is overridden by jitsi-meet-branding at build time
|
||||
* to provide the actual panel content (e.g. iframe).
|
||||
*
|
||||
* @returns {null} This placeholder renders nothing.
|
||||
*/
|
||||
export default function CustomPanelContent(): null {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -2,3 +2,24 @@
|
||||
* Default width for the custom panel in pixels.
|
||||
*/
|
||||
export const DEFAULT_CUSTOM_PANEL_WIDTH = 315;
|
||||
|
||||
/**
|
||||
* Visual width of the drag handle in pixels.
|
||||
*/
|
||||
export const CUSTOM_PANEL_DRAG_HANDLE_WIDTH = 9;
|
||||
|
||||
/**
|
||||
* Visual height of the drag handle in pixels.
|
||||
*/
|
||||
export const CUSTOM_PANEL_DRAG_HANDLE_HEIGHT = 100;
|
||||
|
||||
/**
|
||||
* Touch target size for the drag handle on touch devices.
|
||||
* Provides adequate hit area (44px) for comfortable tapping.
|
||||
*/
|
||||
export const CUSTOM_PANEL_TOUCH_HANDLE_SIZE = 44;
|
||||
|
||||
/**
|
||||
* Offset from the panel edge for positioning the drag handle.
|
||||
*/
|
||||
export const CUSTOM_PANEL_DRAG_HANDLE_OFFSET = 4;
|
||||
|
||||
5
react/features/custom-panel/functions.custom.ts
Normal file
5
react/features/custom-panel/functions.custom.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Custom panel functions placeholder.
|
||||
* Override to add custom panel functionality.
|
||||
*/
|
||||
export {}; // we need this just to make TS happy!
|
||||
@@ -1,7 +1,12 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { CHAT_SIZE } from '../chat/constants';
|
||||
import { getParticipantsPaneWidth } from '../participants-pane/functions';
|
||||
import { VIDEO_SPACE_MIN_SIZE } from '../video-layout/constants';
|
||||
|
||||
import { DEFAULT_CUSTOM_PANEL_WIDTH } from './constants';
|
||||
|
||||
export * from './functions.custom';
|
||||
|
||||
/**
|
||||
* Returns whether the custom panel is enabled based on Redux state.
|
||||
* The feature is disabled by default and can be enabled dynamically via console.
|
||||
@@ -13,35 +18,6 @@ export function isCustomPanelEnabled(state: IReduxState): boolean {
|
||||
return Boolean(state['features/custom-panel']?.enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom panel URL.
|
||||
* Override to provide the actual URL.
|
||||
*
|
||||
* @returns {string} The custom panel URL.
|
||||
*/
|
||||
export function getCustomPanelUrl(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom panel button icon.
|
||||
* Override to provide the actual icon.
|
||||
*
|
||||
* @returns {Function | undefined} The icon component.
|
||||
*/
|
||||
export function getCustomPanelIcon(): Function | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured panel width.
|
||||
*
|
||||
* @returns {number} The panel width in pixels.
|
||||
*/
|
||||
export function getCustomPanelConfiguredWidth(): number {
|
||||
return DEFAULT_CUSTOM_PANEL_WIDTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the custom panel is currently open.
|
||||
*
|
||||
@@ -52,6 +28,17 @@ export function getCustomPanelOpen(state: IReduxState): boolean {
|
||||
return Boolean(state['features/custom-panel']?.isOpen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current configured width of the custom panel from Redux state.
|
||||
* Falls back to the default width if no dynamic width is set.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {number} The panel width in pixels.
|
||||
*/
|
||||
export function getCustomPanelConfiguredWidth(state: IReduxState): number {
|
||||
return state['features/custom-panel']?.width?.current ?? DEFAULT_CUSTOM_PANEL_WIDTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current panel width (0 if closed or disabled).
|
||||
*
|
||||
@@ -63,5 +50,20 @@ export function getCustomPanelWidth(state: IReduxState): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return getCustomPanelOpen(state) ? getCustomPanelConfiguredWidth() : 0;
|
||||
return getCustomPanelOpen(state) ? getCustomPanelConfiguredWidth(state) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the maximum width available for the custom panel based on the
|
||||
* current window size and other open UI panels.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {number} The maximum width in pixels. Returns 0 if no space is available.
|
||||
*/
|
||||
export function getCustomPanelMaxSize(state: IReduxState): number {
|
||||
const { clientWidth } = state['features/base/responsive-ui'];
|
||||
const { isOpen: isChatOpen, width: chatWidth } = state['features/chat'];
|
||||
const chatPanelWidth = isChatOpen ? (chatWidth?.current ?? CHAT_SIZE) : 0;
|
||||
|
||||
return Math.max(clientWidth - chatPanelWidth - getParticipantsPaneWidth(state) - VIDEO_SPACE_MIN_SIZE, 0);
|
||||
}
|
||||
|
||||
4
react/features/custom-panel/middleware.custom.web.ts
Normal file
4
react/features/custom-panel/middleware.custom.web.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Custom panel middleware placeholder.
|
||||
* Override to add custom panel functionality.
|
||||
*/
|
||||
@@ -1,4 +1,2 @@
|
||||
/**
|
||||
* Custom panel middleware placeholder.
|
||||
* Override to add custom panel functionality.
|
||||
*/
|
||||
import './middleware.custom.web';
|
||||
import './subscriber.web';
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import PersistenceRegistry from '../base/redux/PersistenceRegistry';
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
CUSTOM_PANEL_CLOSE,
|
||||
CUSTOM_PANEL_OPEN,
|
||||
SET_CUSTOM_PANEL_ENABLED
|
||||
SET_CUSTOM_PANEL_ENABLED,
|
||||
SET_CUSTOM_PANEL_IS_RESIZING,
|
||||
SET_CUSTOM_PANEL_WIDTH,
|
||||
SET_USER_CUSTOM_PANEL_WIDTH
|
||||
} from './actionTypes';
|
||||
import { DEFAULT_CUSTOM_PANEL_WIDTH } from './constants';
|
||||
|
||||
/**
|
||||
* The state of the custom panel feature.
|
||||
@@ -21,13 +26,48 @@ export interface ICustomPanelState {
|
||||
* Whether the custom panel is currently open.
|
||||
*/
|
||||
isOpen: boolean;
|
||||
|
||||
/**
|
||||
* Whether the user is currently resizing the custom panel.
|
||||
*/
|
||||
isResizing: boolean;
|
||||
|
||||
/**
|
||||
* The width state of the custom panel.
|
||||
*/
|
||||
width: {
|
||||
|
||||
/**
|
||||
* The current display width in pixels.
|
||||
*/
|
||||
current: number;
|
||||
|
||||
/**
|
||||
* The user-preferred width set via drag resize, or null if not set.
|
||||
*/
|
||||
userSet: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
const DEFAULT_STATE: ICustomPanelState = {
|
||||
enabled: false,
|
||||
isOpen: false
|
||||
isOpen: false,
|
||||
isResizing: false,
|
||||
width: {
|
||||
current: DEFAULT_CUSTOM_PANEL_WIDTH,
|
||||
userSet: null
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Persist only the width subtree so the user's preferred panel width
|
||||
* survives page reloads.
|
||||
*/
|
||||
PersistenceRegistry.register('features/custom-panel', {
|
||||
enabled: true,
|
||||
width: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Listen for actions that mutate the custom panel state.
|
||||
*/
|
||||
@@ -52,6 +92,30 @@ ReducerRegistry.register(
|
||||
enabled: action.enabled
|
||||
};
|
||||
|
||||
case SET_CUSTOM_PANEL_WIDTH:
|
||||
return {
|
||||
...state,
|
||||
width: {
|
||||
...state.width,
|
||||
current: action.width
|
||||
}
|
||||
};
|
||||
|
||||
case SET_USER_CUSTOM_PANEL_WIDTH:
|
||||
return {
|
||||
...state,
|
||||
width: {
|
||||
current: action.width,
|
||||
userSet: action.width
|
||||
}
|
||||
};
|
||||
|
||||
case SET_CUSTOM_PANEL_IS_RESIZING:
|
||||
return {
|
||||
...state,
|
||||
isResizing: action.resizing
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
74
react/features/custom-panel/subscriber.web.ts
Normal file
74
react/features/custom-panel/subscriber.web.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// @ts-ignore
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { clientResized } from '../base/responsive-ui/actions';
|
||||
|
||||
import { setCustomPanelWidth } from './actions.web';
|
||||
import { DEFAULT_CUSTOM_PANEL_WIDTH } from './constants';
|
||||
import { getCustomPanelMaxSize } from './functions';
|
||||
|
||||
interface IListenerState {
|
||||
clientWidth: number;
|
||||
isOpen: boolean;
|
||||
maxWidth: number;
|
||||
width: {
|
||||
current: number;
|
||||
userSet: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for changes in the client width and custom panel width
|
||||
* to determine when to adjust the panel size for responsive behavior.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => {
|
||||
return {
|
||||
clientWidth: state['features/base/responsive-ui']?.clientWidth,
|
||||
isOpen: state['features/custom-panel'].isOpen,
|
||||
width: state['features/custom-panel'].width,
|
||||
maxWidth: getCustomPanelMaxSize(state)
|
||||
};
|
||||
},
|
||||
/* listener */ (
|
||||
currentState: IListenerState,
|
||||
{ dispatch },
|
||||
previousState: IListenerState
|
||||
) => {
|
||||
if (currentState.isOpen
|
||||
&& (currentState.clientWidth !== previousState.clientWidth
|
||||
|| currentState.width !== previousState.width)) {
|
||||
const { userSet = 0 } = currentState.width;
|
||||
const { maxWidth } = currentState;
|
||||
let panelWidthChanged = false;
|
||||
|
||||
if (currentState.clientWidth !== previousState.clientWidth) {
|
||||
if (userSet !== null) {
|
||||
// If userSet is set, clamp it within the new bounds.
|
||||
// This handles the case when the screen gets smaller and
|
||||
// the user-set width exceeds the max, or when the screen
|
||||
// gets bigger and we can restore the user-set width.
|
||||
dispatch(setCustomPanelWidth(
|
||||
Math.max(Math.min(maxWidth, userSet), DEFAULT_CUSTOM_PANEL_WIDTH)
|
||||
));
|
||||
panelWidthChanged = true;
|
||||
}
|
||||
// else { // when userSet is null:
|
||||
// no-op. The custom panel width will be the default one which is the min too.
|
||||
// }
|
||||
} else {
|
||||
// Width changed (not clientWidth) — panel was resized by user.
|
||||
panelWidthChanged = true;
|
||||
}
|
||||
|
||||
if (panelWidthChanged) {
|
||||
const { innerWidth, innerHeight } = window;
|
||||
|
||||
// Recalculate videoSpaceWidth since it depends on the custom panel width.
|
||||
dispatch(clientResized(innerWidth, innerHeight));
|
||||
|
||||
// Recompute the large video size.
|
||||
VideoLayout.onResize();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -53,8 +53,13 @@ export function createMuiBrandingTheme(customTheme: Theme) {
|
||||
spacing: customSpacing
|
||||
} = customTheme;
|
||||
|
||||
const newPalette = createColorTokens(colorMap);
|
||||
// Pass customPalette to createColorTokens so that custom colors are merged
|
||||
// BEFORE token resolution. This ensures that semantic tokens (like prejoinActionButtonPrimary)
|
||||
// that reference base tokens (like action01) will resolve to the custom color values.
|
||||
const newPalette = createColorTokens(colorMap, customPalette);
|
||||
|
||||
// Also apply overwriteRecurrsive for any direct palette key overrides that may not be
|
||||
// handled through token resolution (e.g., if customer provides semantic token names directly).
|
||||
if (customPalette) {
|
||||
overwriteRecurrsive(newPalette, customPalette);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const generateDownloadUrl = async (url: string) => {
|
||||
|
||||
const blob = new Blob([ respBlob ]);
|
||||
|
||||
// @ts-ignore
|
||||
return URL.createObjectURL(blob);
|
||||
};
|
||||
|
||||
@@ -22,6 +23,8 @@ export const downloadFile = async (url: string, fileName: string) => {
|
||||
|
||||
// fix for certain browsers
|
||||
setTimeout(() => {
|
||||
|
||||
// @ts-ignore
|
||||
URL.revokeObjectURL(dowloadUrl);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import i18next from '../../../base/i18n/i18next';
|
||||
import { parseURLParams } from '../../../base/util/parseURLParams';
|
||||
import { DIAL_IN_INFO_PAGE_PATH_NAME } from '../../constants';
|
||||
import DialInSummary from '../dial-in-summary/web/DialInSummary';
|
||||
|
||||
import NoRoomError from './NoRoomError.web';
|
||||
|
||||
/**
|
||||
* TODO: This seems unused, so we can drop it.
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// @ts-ignore
|
||||
const { room } = parseURLParams(window.location, true, 'search');
|
||||
const { href } = window.location;
|
||||
const ix = href.indexOf(DIAL_IN_INFO_PAGE_PATH_NAME);
|
||||
const url = (ix > 0 ? href.substring(0, ix) : href) + room;
|
||||
|
||||
/* eslint-disable-next-line react/no-deprecated */
|
||||
ReactDOM.render(
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
{ room
|
||||
? <DialInSummary
|
||||
className = 'dial-in-page'
|
||||
clickableNumbers = { isMobileBrowser() }
|
||||
room = { decodeURIComponent(room) }
|
||||
url = { url } />
|
||||
: <NoRoomError className = 'dial-in-page' /> }
|
||||
</I18nextProvider>,
|
||||
document.getElementById('react')
|
||||
);
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
/* eslint-disable-next-line react/no-deprecated */
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById('react')!);
|
||||
});
|
||||
@@ -38,7 +38,16 @@ ReducerRegistry.register<IKeyboardShortcutsState>(STORE_NAME,
|
||||
enabled: false
|
||||
};
|
||||
case REGISTER_KEYBOARD_SHORTCUT: {
|
||||
const shortcutKey = action.shortcut.alt ? `:${action.shortcut.character}` : action.shortcut.character;
|
||||
const { alt, character, ctrl } = action.shortcut;
|
||||
let shortcutKey = character;
|
||||
|
||||
if (ctrl && alt) {
|
||||
shortcutKey = `-:${character}`;
|
||||
} else if (alt) {
|
||||
shortcutKey = `:${character}`;
|
||||
} else if (ctrl) {
|
||||
shortcutKey = `-${character}`;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
@@ -51,7 +60,15 @@ ReducerRegistry.register<IKeyboardShortcutsState>(STORE_NAME,
|
||||
};
|
||||
}
|
||||
case UNREGISTER_KEYBOARD_SHORTCUT: {
|
||||
const shortcutKey = action.alt ? `:${action.character}` : action.character;
|
||||
let shortcutKey = action.character;
|
||||
|
||||
if (action.ctrl && action.alt) {
|
||||
shortcutKey = `-:${action.character}`;
|
||||
} else if (action.alt) {
|
||||
shortcutKey = `:${action.character}`;
|
||||
} else if (action.ctrl) {
|
||||
shortcutKey = `-${action.character}`;
|
||||
}
|
||||
const shortcuts = new Map(state.shortcuts);
|
||||
|
||||
shortcuts.delete(shortcutKey);
|
||||
|
||||
@@ -6,6 +6,9 @@ export interface IKeyboardShortcut {
|
||||
// the character to be pressed that triggers the action
|
||||
character: string;
|
||||
|
||||
// whether or not the ctrl key must be pressed
|
||||
ctrl?: boolean;
|
||||
|
||||
// the function to be executed when the shortcut is pressed
|
||||
handler: Function;
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ export const getKeyboardKey = (e: KeyboardEvent): string => {
|
||||
|
||||
const replacedKey = code.replace('Key', '');
|
||||
|
||||
if (ctrlKey && altKey) {
|
||||
return `-:${replacedKey}`;
|
||||
}
|
||||
|
||||
if (altKey) {
|
||||
return `:${replacedKey}`;
|
||||
}
|
||||
|
||||
4
react/features/overlay/actionTypes.ts
Normal file
4
react/features/overlay/actionTypes.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* The type of the Redux action which sends page reload log.
|
||||
*/
|
||||
export const PAGE_RELOAD_APPLICATION_LOG = 'PAGE_RELOAD_APPLICATION_LOG';
|
||||
14
react/features/overlay/actions.any.ts
Normal file
14
react/features/overlay/actions.any.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { PAGE_RELOAD_APPLICATION_LOG } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Sends a page reload application log message.
|
||||
*
|
||||
* @param {string} reason - The reason for the reload.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function sendPageReloadApplicationLog(reason?: string) {
|
||||
return {
|
||||
type: PAGE_RELOAD_APPLICATION_LOG,
|
||||
reason
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
isFatalJitsiConferenceError,
|
||||
isFatalJitsiConnectionError
|
||||
} from '../../../base/lib-jitsi-meet/functions.web';
|
||||
import { sendPageReloadApplicationLog } from '../../actions.any';
|
||||
import logger from '../../logger';
|
||||
|
||||
import ReloadButton from './ReloadButton';
|
||||
@@ -152,13 +153,7 @@ export default class AbstractPageReloadOverlay<P extends IProps>
|
||||
* @returns {void}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
// FIXME: We should dispatch action for this.
|
||||
if (typeof APP !== 'undefined' && APP.conference?._room) {
|
||||
APP.conference._room.sendApplicationLog(JSON.stringify({
|
||||
name: 'page.reload',
|
||||
label: this.props.reason
|
||||
}));
|
||||
}
|
||||
this.props.dispatch(sendPageReloadApplicationLog(this.props.reason));
|
||||
|
||||
sendAnalytics(createPageReloadScheduledEvent(
|
||||
this.props.reason ?? '',
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { JitsiConferenceErrors, JitsiConnectionErrors } from '../base/lib-jitsi-meet';
|
||||
import {
|
||||
isFatalJitsiConferenceError,
|
||||
isFatalJitsiConnectionError
|
||||
} from '../base/lib-jitsi-meet/functions.any';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
|
||||
import { PAGE_RELOAD_APPLICATION_LOG } from './actionTypes';
|
||||
import { openPageReloadDialog } from './actions';
|
||||
import logger from './logger';
|
||||
|
||||
@@ -128,3 +131,25 @@ StateListenerRegistry.register(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Middleware for overlay specific actions.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(({ getState }) => next => action => {
|
||||
const result = next(action);
|
||||
|
||||
if (action.type === PAGE_RELOAD_APPLICATION_LOG) {
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state) ?? state['features/base/conference']?.leaving;
|
||||
|
||||
conference?.sendApplicationLog(JSON.stringify({
|
||||
name: 'page.reload',
|
||||
label: action.reason
|
||||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference/actionTypes';
|
||||
import { SET_CONFIG } from '../base/config/actionTypes';
|
||||
import { CONNECTION_FAILED } from '../base/connection/actionTypes';
|
||||
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../base/media/actionTypes';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
@@ -10,13 +11,14 @@ import {
|
||||
TRACK_ADDED,
|
||||
TRACK_NO_DATA_FROM_SOURCE
|
||||
} from '../base/tracks/actionTypes';
|
||||
import { preloadZxcvbn } from '../base/util/isInsecureRoomName';
|
||||
|
||||
import {
|
||||
setDeviceStatusOk,
|
||||
setDeviceStatusWarning,
|
||||
setJoiningInProgress
|
||||
} from './actions';
|
||||
import { isPrejoinPageVisible } from './functions.any';
|
||||
import { isPrejoinPageVisible, isUnsafeRoomWarningEnabled } from './functions.any';
|
||||
|
||||
/**
|
||||
* The redux middleware for {@link PrejoinPage}.
|
||||
@@ -26,6 +28,15 @@ import { isPrejoinPageVisible } from './functions.any';
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case SET_CONFIG: {
|
||||
const result = next(action);
|
||||
|
||||
if (isUnsafeRoomWarningEnabled(store.getState())) {
|
||||
preloadZxcvbn();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case SET_AUDIO_MUTED: {
|
||||
if (isPrejoinPageVisible(store.getState())) {
|
||||
store.dispatch(updateSettings({
|
||||
|
||||
@@ -256,14 +256,37 @@ export function showStartedRecordingNotification(
|
||||
const recordingSharingUrl = getRecordingSharingUrl(state);
|
||||
const iAmRecordingInitiator = getLocalParticipant(state)?.id === initiatorId;
|
||||
const { showRecordingLink } = state['features/base/config'].recordings || {};
|
||||
const isTranscribing = isRecorderTranscriptionsRunning(state);
|
||||
const isRecording = isRecordingRunning(state);
|
||||
|
||||
notifyProps.dialogProps = {
|
||||
customActionHandler: undefined,
|
||||
customActionNameKey: undefined,
|
||||
descriptionKey: participantName ? 'recording.onBy' : 'recording.on',
|
||||
descriptionArguments: { name: participantName },
|
||||
titleKey: 'dialog.recording'
|
||||
};
|
||||
// Case 1: Transcription only (no recording)
|
||||
if (isTranscribing && !isRecording) {
|
||||
notifyProps.dialogProps = {
|
||||
customActionHandler: undefined,
|
||||
customActionNameKey: undefined,
|
||||
descriptionKey: participantName ? 'transcribing.onBy' : 'transcribing.on',
|
||||
descriptionArguments: { name: participantName },
|
||||
titleKey: 'dialog.recording'
|
||||
};
|
||||
} else if (isTranscribing && isRecording) {
|
||||
// Case 2: Recording + transcription
|
||||
notifyProps.dialogProps = {
|
||||
customActionHandler: undefined,
|
||||
customActionNameKey: undefined,
|
||||
descriptionKey: participantName ? 'recording.onByWithTranscription' : 'recording.onWithTranscription',
|
||||
descriptionArguments: { name: participantName },
|
||||
titleKey: 'dialog.recording'
|
||||
};
|
||||
} else {
|
||||
// Case 3: Recording only (no transcription)
|
||||
notifyProps.dialogProps = {
|
||||
customActionHandler: undefined,
|
||||
customActionNameKey: undefined,
|
||||
descriptionKey: participantName ? 'recording.onBy' : 'recording.on',
|
||||
descriptionArguments: { name: participantName },
|
||||
titleKey: 'dialog.recording'
|
||||
};
|
||||
}
|
||||
|
||||
// fetch the recording link from the server for recording initiators in jaas meetings
|
||||
if (recordingSharingUrl
|
||||
|
||||
@@ -84,8 +84,7 @@ export function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const _isLivestreamingRunning = isLiveStreamingRunning(state);
|
||||
const _isVisible = isLiveStreamingLabel
|
||||
? _isLivestreamingRunning // this is the livestreaming label
|
||||
: isRecordingRunning(state) || isRemoteParticipantRecordingLocally(state)
|
||||
|| _isTranscribing; // this is the recording label
|
||||
: isRecordingRunning(state) || isRemoteParticipantRecordingLocally(state); // this is the recording label
|
||||
|
||||
return {
|
||||
_isVisible,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../app/types';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { translate } from '../../../../base/i18n/functions.web';
|
||||
import Dialog from '../../../../base/ui/components/web/Dialog';
|
||||
import Spinner from '../../../../base/ui/components/web/Spinner';
|
||||
import {
|
||||
@@ -275,19 +276,14 @@ class StartLiveStreamDialog
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: Ideally this help text would be one translation string
|
||||
* that also accepts the anchor. This can be done using the Trans
|
||||
* component of react-i18next but I couldn't get it working...
|
||||
*/
|
||||
helpText = (
|
||||
<div>
|
||||
{ `${t('liveStreaming.chooseCTA',
|
||||
{ email: _googleProfileEmail })} ` }
|
||||
<a onClick = { this._onRequestGoogleSignIn }>
|
||||
{ t('liveStreaming.changeSignIn') }
|
||||
</a>
|
||||
</div>
|
||||
<Trans
|
||||
components = { [ <a
|
||||
key = 'change-sign-in'
|
||||
onClick = { this._onRequestGoogleSignIn } /> ] }
|
||||
i18nKey = 'liveStreaming.chooseCTAWithChangeSignIn'
|
||||
t = { t }
|
||||
values = {{ email: _googleProfileEmail }} />
|
||||
);
|
||||
|
||||
break;
|
||||
|
||||
@@ -102,8 +102,8 @@ export default class AbstractStopRecordingDialog<P extends IProps>
|
||||
if (localRecordingVideoStop) {
|
||||
dispatch(setVideoMuted(true));
|
||||
}
|
||||
} else {
|
||||
_conference?.stopRecording(_fileRecordingSession?.id);
|
||||
} else if (_fileRecordingSession) {
|
||||
_conference?.stopRecording(_fileRecordingSession.id);
|
||||
this._toggleScreenshotCapture();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import ExpandedLabel, { IProps as AbstractProps } from '../../../base/label/components/native/ExpandedLabel';
|
||||
import { isRecorderTranscriptionsRunning } from '../../../transcribing/functions';
|
||||
|
||||
interface IProps extends AbstractProps, WithTranslation {
|
||||
|
||||
/**
|
||||
* Whether this meeting is being transcribed.
|
||||
*/
|
||||
_isTranscribing?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A react {@code Component} that implements an expanded label as tooltip-like
|
||||
* component to explain the meaning of the {@code TranscribingLabel}.
|
||||
*/
|
||||
class TranscribingExpandedLabel extends ExpandedLabel<IProps> {
|
||||
|
||||
/**
|
||||
* Returns the label specific text of this {@code ExpandedLabel}.
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
_getLabel() {
|
||||
const { _isTranscribing, t } = this.props;
|
||||
|
||||
if (_isTranscribing) {
|
||||
return t('transcribing.labelTooltip');
|
||||
}
|
||||
|
||||
return t('transcribing.expandedOff');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated
|
||||
* {@code TranscribingExpandedLabel}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _isTranscribing: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_isTranscribing: isRecorderTranscriptionsRunning(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(TranscribingExpandedLabel));
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconTranscription } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/native/Label';
|
||||
import { JitsiRecordingConstants } from '../../../base/lib-jitsi-meet';
|
||||
import { StyleType } from '../../../base/styles/functions.any';
|
||||
import { isRecorderTranscriptionsRunning } from '../../../transcribing/functions';
|
||||
import AbstractRecordingLabel, {
|
||||
IProps as AbstractProps
|
||||
} from '../AbstractRecordingLabel';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays the current state of
|
||||
* transcription.
|
||||
*
|
||||
* @augments {Component}
|
||||
*/
|
||||
class TranscribingLabel extends AbstractRecordingLabel<AbstractProps> {
|
||||
|
||||
/**
|
||||
* Renders the platform specific label component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderLabel() {
|
||||
const { _isTranscribing } = this.props;
|
||||
|
||||
if (!_isTranscribing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
icon = { IconTranscription }
|
||||
status = { 'on' }
|
||||
style = { styles.transcribingIndicatorStyle as StyleType } />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _mapStateToProps(state: any) {
|
||||
const _isTranscribing = isRecorderTranscriptionsRunning(state);
|
||||
|
||||
return {
|
||||
_isVisible: _isTranscribing,
|
||||
_iAmRecorder: Boolean(state['features/base/config'].iAmRecorder),
|
||||
_isTranscribing,
|
||||
mode: 'transcribing', // Custom mode for transcription
|
||||
_status: _isTranscribing ? JitsiRecordingConstants.status.ON : JitsiRecordingConstants.status.OFF
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(TranscribingLabel));
|
||||
@@ -14,5 +14,15 @@ export default createStyleSheet({
|
||||
marginLeft: 0,
|
||||
marginBottom: 0,
|
||||
backgroundColor: BaseTheme.palette.iconError
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the transcription indicator.
|
||||
*/
|
||||
transcribingIndicatorStyle: {
|
||||
marginRight: 4,
|
||||
marginLeft: 0,
|
||||
marginBottom: 0,
|
||||
backgroundColor: BaseTheme.palette.transcriptionIndicator
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import { IStore } from '../../../app/types';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconRecord, IconSites } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
@@ -12,6 +14,7 @@ import AbstractRecordingLabel, {
|
||||
IProps as AbstractProps,
|
||||
_mapStateToProps
|
||||
} from '../AbstractRecordingLabel';
|
||||
import StopRecordingDialog from '../Recording/web/StopRecordingDialog';
|
||||
|
||||
interface IProps extends AbstractProps {
|
||||
|
||||
@@ -20,6 +23,11 @@ interface IProps extends AbstractProps {
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,13 +52,33 @@ const styles = (theme: Theme) => {
|
||||
* @augments {Component}
|
||||
*/
|
||||
class RecordingLabel extends AbstractRecordingLabel<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code RecordingLabel} instance.
|
||||
*
|
||||
* @param {IProps} props - The props of the component.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking on the label.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
this.props.dispatch(openDialog('StopRecordingDialog', StopRecordingDialog));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific label component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _renderLabel() {
|
||||
const { _isTranscribing, _status, mode, t } = this.props;
|
||||
const { _status, mode, t } = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
const isRecording = mode === JitsiRecordingConstants.mode.FILE;
|
||||
const icon = isRecording ? IconRecord : IconSites;
|
||||
@@ -58,14 +86,8 @@ class RecordingLabel extends AbstractRecordingLabel<IProps> {
|
||||
|
||||
if (_status === JitsiRecordingConstants.status.ON) {
|
||||
content = t(isRecording ? 'videoStatus.recording' : 'videoStatus.streaming');
|
||||
|
||||
if (_isTranscribing) {
|
||||
content += ` ${t('transcribing.labelTooltipExtra')}`;
|
||||
}
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
return null;
|
||||
} else if (_isTranscribing) {
|
||||
content = t('transcribing.labelTooltip');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -76,7 +98,8 @@ class RecordingLabel extends AbstractRecordingLabel<IProps> {
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
className = { classes.record }
|
||||
icon = { icon } />
|
||||
icon = { icon }
|
||||
onClick = { this._onClick } />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
120
react/features/recording/components/web/TranscribingLabel.tsx
Normal file
120
react/features/recording/components/web/TranscribingLabel.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconTranscription } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import { isRecorderTranscriptionsRunning } from '../../../transcribing/functions';
|
||||
import AbstractRecordingLabel, {
|
||||
IProps as AbstractProps
|
||||
} from '../AbstractRecordingLabel';
|
||||
import StopRecordingDialog from '../Recording/web/StopRecordingDialog';
|
||||
|
||||
interface IProps extends AbstractProps {
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the styles for the component.
|
||||
*
|
||||
* @param {Object} theme - The current UI theme.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
transcribing: {
|
||||
background: theme.palette.transcriptionIndicator
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React {@link Component} which displays the current state of
|
||||
* transcription.
|
||||
*
|
||||
* @augments {Component}
|
||||
*/
|
||||
class TranscribingLabel extends AbstractRecordingLabel<IProps> {
|
||||
/**
|
||||
* Initializes a new {@code TranscribingLabel} instance.
|
||||
*
|
||||
* @param {IProps} props - The props of the component.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onClick = this._onClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking on the label.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClick() {
|
||||
this.props.dispatch(openDialog('StopRecordingDialog', StopRecordingDialog));
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific label component.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override _renderLabel() {
|
||||
const { _isTranscribing, t } = this.props;
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
|
||||
if (!_isTranscribing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = t('transcribing.labelTooltip');
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
content = { content }
|
||||
position = { 'bottom' }>
|
||||
<Label
|
||||
className = { classes.transcribing }
|
||||
icon = { IconTranscription }
|
||||
onClick = { this._onClick } />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const _isTranscribing = isRecorderTranscriptionsRunning(state);
|
||||
|
||||
return {
|
||||
_isVisible: _isTranscribing,
|
||||
_iAmRecorder: Boolean(state['features/base/config'].iAmRecorder),
|
||||
_isTranscribing,
|
||||
mode: 'transcribing' // Custom mode for transcription
|
||||
};
|
||||
}
|
||||
|
||||
export default withStyles(translate(connect(_mapStateToProps)(TranscribingLabel)), styles);
|
||||
@@ -36,6 +36,34 @@ export const RECORDING_OFF_SOUND_ID = 'RECORDING_OFF_SOUND';
|
||||
*/
|
||||
export const RECORDING_ON_SOUND_ID = 'RECORDING_ON_SOUND';
|
||||
|
||||
/**
|
||||
* The identifier of the sound to be played when transcription is stopped.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const TRANSCRIPTION_OFF_SOUND_ID = 'TRANSCRIPTION_OFF_SOUND';
|
||||
|
||||
/**
|
||||
* The identifier of the sound to be played when transcription is started.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const TRANSCRIPTION_ON_SOUND_ID = 'TRANSCRIPTION_ON_SOUND';
|
||||
|
||||
/**
|
||||
* The identifier of the sound to be played when recording and transcription are stopped.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const RECORDING_AND_TRANSCRIPTION_OFF_SOUND_ID = 'RECORDING_AND_TRANSCRIPTION_OFF_SOUND';
|
||||
|
||||
/**
|
||||
* The identifier of the sound to be played when recording and transcription are started.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const RECORDING_AND_TRANSCRIPTION_ON_SOUND_ID = 'RECORDING_AND_TRANSCRIPTION_ON_SOUND';
|
||||
|
||||
/**
|
||||
* Expected supported recording types.
|
||||
*
|
||||
|
||||
@@ -19,17 +19,25 @@ import LocalRecordingManager from './components/Recording/LocalRecordingManager'
|
||||
import {
|
||||
LIVE_STREAMING_OFF_SOUND_ID,
|
||||
LIVE_STREAMING_ON_SOUND_ID,
|
||||
RECORDING_AND_TRANSCRIPTION_OFF_SOUND_ID,
|
||||
RECORDING_AND_TRANSCRIPTION_ON_SOUND_ID,
|
||||
RECORDING_OFF_SOUND_ID,
|
||||
RECORDING_ON_SOUND_ID,
|
||||
RECORDING_STATUS_PRIORITIES,
|
||||
RECORDING_TYPES
|
||||
RECORDING_TYPES,
|
||||
TRANSCRIPTION_OFF_SOUND_ID,
|
||||
TRANSCRIPTION_ON_SOUND_ID
|
||||
} from './constants';
|
||||
import logger from './logger';
|
||||
import {
|
||||
LIVE_STREAMING_OFF_SOUND_FILE,
|
||||
LIVE_STREAMING_ON_SOUND_FILE,
|
||||
RECORDING_AND_TRANSCRIPTION_OFF_SOUND_FILE,
|
||||
RECORDING_AND_TRANSCRIPTION_ON_SOUND_FILE,
|
||||
RECORDING_OFF_SOUND_FILE,
|
||||
RECORDING_ON_SOUND_FILE
|
||||
RECORDING_ON_SOUND_FILE,
|
||||
TRANSCRIPTION_OFF_SOUND_FILE,
|
||||
TRANSCRIPTION_ON_SOUND_FILE
|
||||
} from './sounds';
|
||||
|
||||
/**
|
||||
@@ -384,6 +392,10 @@ export function unregisterRecordingAudioFiles(dispatch: IStore['dispatch']) {
|
||||
dispatch(unregisterSound(LIVE_STREAMING_ON_SOUND_FILE));
|
||||
dispatch(unregisterSound(RECORDING_OFF_SOUND_FILE));
|
||||
dispatch(unregisterSound(RECORDING_ON_SOUND_FILE));
|
||||
dispatch(unregisterSound(TRANSCRIPTION_OFF_SOUND_FILE));
|
||||
dispatch(unregisterSound(TRANSCRIPTION_ON_SOUND_FILE));
|
||||
dispatch(unregisterSound(RECORDING_AND_TRANSCRIPTION_OFF_SOUND_FILE));
|
||||
dispatch(unregisterSound(RECORDING_AND_TRANSCRIPTION_ON_SOUND_FILE));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -415,6 +427,22 @@ export function registerRecordingAudioFiles(dispatch: IStore['dispatch'], should
|
||||
dispatch(registerSound(
|
||||
RECORDING_ON_SOUND_ID,
|
||||
getSoundFileSrc(RECORDING_ON_SOUND_FILE, language)));
|
||||
|
||||
dispatch(registerSound(
|
||||
TRANSCRIPTION_OFF_SOUND_ID,
|
||||
getSoundFileSrc(TRANSCRIPTION_OFF_SOUND_FILE, language)));
|
||||
|
||||
dispatch(registerSound(
|
||||
TRANSCRIPTION_ON_SOUND_ID,
|
||||
getSoundFileSrc(TRANSCRIPTION_ON_SOUND_FILE, language)));
|
||||
|
||||
dispatch(registerSound(
|
||||
RECORDING_AND_TRANSCRIPTION_OFF_SOUND_ID,
|
||||
getSoundFileSrc(RECORDING_AND_TRANSCRIPTION_OFF_SOUND_FILE, language)));
|
||||
|
||||
dispatch(registerSound(
|
||||
RECORDING_AND_TRANSCRIPTION_ON_SOUND_ID,
|
||||
getSoundFileSrc(RECORDING_AND_TRANSCRIPTION_ON_SOUND_FILE, language)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,6 +51,8 @@ import LocalRecordingManager from './components/Recording/LocalRecordingManager'
|
||||
import {
|
||||
LIVE_STREAMING_OFF_SOUND_ID,
|
||||
LIVE_STREAMING_ON_SOUND_ID,
|
||||
RECORDING_AND_TRANSCRIPTION_OFF_SOUND_ID,
|
||||
RECORDING_AND_TRANSCRIPTION_ON_SOUND_ID,
|
||||
RECORDING_OFF_SOUND_ID,
|
||||
RECORDING_ON_SOUND_ID,
|
||||
START_RECORDING_NOTIFICATION_ID
|
||||
@@ -64,6 +66,11 @@ import {
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Map to track which recording sessions have transcription enabled.
|
||||
*/
|
||||
const sessionsWithTranscription = new Map<string, boolean>();
|
||||
|
||||
/**
|
||||
* StateListenerRegistry provides a reliable way to detect the leaving of a
|
||||
* conference, where we need to clean up the recording sessions.
|
||||
@@ -247,9 +254,23 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
sendAnalytics(createRecordingEvent('start', mode));
|
||||
|
||||
let soundID;
|
||||
const isTranscribing = isRecorderTranscriptionsRunning(state);
|
||||
const isRequestingTranscription = state['features/subtitles']._requestingSubtitles;
|
||||
const willTranscribe = isTranscribing || isRequestingTranscription;
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE && !isRecorderTranscriptionsRunning(state)) {
|
||||
soundID = RECORDING_ON_SOUND_ID;
|
||||
// Store whether transcription was enabled when recording started
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
const sessionId = action.sessionData.id;
|
||||
|
||||
if (sessionId) {
|
||||
sessionsWithTranscription.set(sessionId, willTranscribe);
|
||||
}
|
||||
|
||||
if (willTranscribe) {
|
||||
soundID = RECORDING_AND_TRANSCRIPTION_ON_SOUND_ID;
|
||||
} else {
|
||||
soundID = RECORDING_ON_SOUND_ID;
|
||||
}
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
soundID = LIVE_STREAMING_ON_SOUND_ID;
|
||||
}
|
||||
@@ -264,11 +285,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
}
|
||||
}
|
||||
} else if (updatedSessionData?.status === OFF && oldSessionData?.status !== OFF) {
|
||||
if (terminator) {
|
||||
dispatch(
|
||||
showStoppedRecordingNotification(
|
||||
mode, getParticipantDisplayName(state, getResourceId(terminator))));
|
||||
}
|
||||
const participantName = terminator
|
||||
? getParticipantDisplayName(state, getResourceId(terminator))
|
||||
: undefined;
|
||||
|
||||
dispatch(showStoppedRecordingNotification(mode, participantName));
|
||||
|
||||
let duration = 0, soundOff, soundOn;
|
||||
|
||||
@@ -278,9 +299,23 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
}
|
||||
sendAnalytics(createRecordingEvent('stop', mode, duration));
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE && !isRecorderTranscriptionsRunning(state)) {
|
||||
soundOff = RECORDING_OFF_SOUND_ID;
|
||||
soundOn = RECORDING_ON_SOUND_ID;
|
||||
// Check if transcription was enabled when the recording started
|
||||
const sessionId = action.sessionData.id;
|
||||
const wasWithTranscription = sessionId ? sessionsWithTranscription.get(sessionId) ?? false : false;
|
||||
|
||||
if (mode === JitsiRecordingConstants.mode.FILE) {
|
||||
if (wasWithTranscription) {
|
||||
soundOff = RECORDING_AND_TRANSCRIPTION_OFF_SOUND_ID;
|
||||
soundOn = RECORDING_AND_TRANSCRIPTION_ON_SOUND_ID;
|
||||
} else {
|
||||
soundOff = RECORDING_OFF_SOUND_ID;
|
||||
soundOn = RECORDING_ON_SOUND_ID;
|
||||
}
|
||||
|
||||
// Clean up the entry when recording stops
|
||||
if (sessionId) {
|
||||
sessionsWithTranscription.delete(sessionId);
|
||||
}
|
||||
} else if (mode === JitsiRecordingConstants.mode.STREAM) {
|
||||
soundOff = LIVE_STREAMING_OFF_SOUND_ID;
|
||||
soundOn = LIVE_STREAMING_ON_SOUND_ID;
|
||||
|
||||
@@ -25,3 +25,31 @@ export const RECORDING_OFF_SOUND_FILE = 'recordingOff.mp3';
|
||||
* @type {string}
|
||||
*/
|
||||
export const RECORDING_ON_SOUND_FILE = 'recordingOn.mp3';
|
||||
|
||||
/**
|
||||
* The name of the bundled audio file which will be played for when transcription is stopped.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const TRANSCRIPTION_OFF_SOUND_FILE = 'transcriptionOff.mp3';
|
||||
|
||||
/**
|
||||
* The name of the bundled audio file which will be played for when transcription is started.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const TRANSCRIPTION_ON_SOUND_FILE = 'transcriptionOn.mp3';
|
||||
|
||||
/**
|
||||
* The name of the bundled audio file which will be played for when recording and transcription are stopped.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const RECORDING_AND_TRANSCRIPTION_OFF_SOUND_FILE = 'recordingAndTranscriptionOff.mp3';
|
||||
|
||||
/**
|
||||
* The name of the bundled audio file which will be played for when recording and transcription are started.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const RECORDING_AND_TRANSCRIPTION_ON_SOUND_FILE = 'recordingAndTranscriptionOn.mp3';
|
||||
|
||||
@@ -7,7 +7,10 @@ import { playSound } from '../base/sounds/actions';
|
||||
import { showNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
|
||||
import { INotificationProps } from '../notifications/types';
|
||||
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../recording/constants';
|
||||
import {
|
||||
TRANSCRIPTION_OFF_SOUND_ID,
|
||||
TRANSCRIPTION_ON_SOUND_ID
|
||||
} from '../recording/constants';
|
||||
import { isLiveStreamingRunning, isRecordingRunning } from '../recording/functions';
|
||||
|
||||
import { isRecorderTranscriptionsRunning, isTranscribing } from './functions';
|
||||
@@ -17,7 +20,12 @@ import { isRecorderTranscriptionsRunning, isTranscribing } from './functions';
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ isRecorderTranscriptionsRunning,
|
||||
/* listener */ (isRecorderTranscriptionsRunningValue, { getState, dispatch }) => {
|
||||
/* listener */ (isRecorderTranscriptionsRunningValue, { getState, dispatch }, previousValue) => {
|
||||
// Only emit notifications on actual changes, not on initial state
|
||||
if (previousValue === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRecorderTranscriptionsRunningValue) {
|
||||
maybeEmitRecordingNotification(dispatch, getState, true);
|
||||
} else {
|
||||
@@ -27,7 +35,12 @@ StateListenerRegistry.register(
|
||||
);
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ isTranscribing,
|
||||
/* listener */ (isTranscribingValue, { getState }) => {
|
||||
/* listener */ (isTranscribingValue, { getState }, previousValue) => {
|
||||
// Only notify on actual changes, not on initial state
|
||||
if (previousValue === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTranscribingValue) {
|
||||
notifyTranscribingStatusChanged(getState, true);
|
||||
} else {
|
||||
@@ -56,14 +69,15 @@ function maybeEmitRecordingNotification(dispatch: IStore['dispatch'], getState:
|
||||
return;
|
||||
}
|
||||
|
||||
// Show transcription-specific notification when there's no recording
|
||||
const notifyProps: INotificationProps = {
|
||||
descriptionKey: on ? 'recording.on' : 'recording.off',
|
||||
descriptionKey: on ? 'transcribing.on' : 'transcribing.off',
|
||||
titleKey: 'dialog.recording'
|
||||
};
|
||||
|
||||
batch(() => {
|
||||
dispatch(showNotification(notifyProps, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
dispatch(playSound(on ? RECORDING_ON_SOUND_ID : RECORDING_OFF_SOUND_ID));
|
||||
dispatch(playSound(on ? TRANSCRIPTION_ON_SOUND_ID : TRANSCRIPTION_OFF_SOUND_ID));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { IReduxState, IStore } from '../../../app/types';
|
||||
import { getCurrentConference } from '../../../base/conference/functions';
|
||||
import { IJitsiConference } from '../../../base/conference/reducer';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { translate } from '../../../base/i18n/functions.native';
|
||||
import { IconCloseLarge } from '../../../base/icons/svg';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
|
||||
@@ -128,7 +128,7 @@ class Whiteboard extends PureComponent<IProps> {
|
||||
safeAreaInsets = { [ 'bottom', 'left', 'right' ] }
|
||||
style = { styles.backDrop }>
|
||||
<WebView
|
||||
domStorageEnabled = { false }
|
||||
domStorageEnabled = { true }
|
||||
incognito = { true }
|
||||
javaScriptEnabled = { true }
|
||||
nestedScrollEnabled = { true }
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ExcalidrawApp } from '@jitsi/excalidraw';
|
||||
import clsx from 'clsx';
|
||||
import i18next from 'i18next';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { Suspense, useCallback, useEffect, useRef } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector, useStore } from 'react-redux';
|
||||
|
||||
// @ts-expect-error
|
||||
import Filmstrip from '../../../../../modules/UI/videolayout/Filmstrip';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { getCurrentConference } from '../../../base/conference/functions';
|
||||
import { translate } from '../../../base/i18n/functions.web';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { getVerticalViewMaxWidth } from '../../../filmstrip/functions.web';
|
||||
import { getToolboxHeight } from '../../../toolbox/functions.web';
|
||||
@@ -17,10 +17,20 @@ import { WHITEBOARD_UI_OPTIONS } from '../../constants';
|
||||
import {
|
||||
getCollabDetails,
|
||||
getCollabServerUrl,
|
||||
getStorageBackendUrl,
|
||||
isWhiteboardOpen,
|
||||
isWhiteboardVisible
|
||||
} from '../../functions';
|
||||
|
||||
const LazyExcalidrawApp = React.lazy(async () => {
|
||||
const [ { ExcalidrawApp } ] = await Promise.all([
|
||||
import(/* webpackChunkName: "excalidraw" */ '@jitsi/excalidraw'),
|
||||
import(/* webpackChunkName: "excalidraw" */ '@jitsi/excalidraw/index.css')
|
||||
]);
|
||||
|
||||
return { default: ExcalidrawApp };
|
||||
});
|
||||
|
||||
/**
|
||||
* Space taken by meeting elements like the subject and the watermark.
|
||||
*/
|
||||
@@ -35,6 +45,12 @@ interface IDimensions {
|
||||
width: string;
|
||||
}
|
||||
|
||||
interface IMeetingDetails {
|
||||
jwt: string;
|
||||
roomJid: string;
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Whiteboard component.
|
||||
*
|
||||
@@ -42,7 +58,6 @@ interface IDimensions {
|
||||
* @returns {JSX.Element} - The React component.
|
||||
*/
|
||||
const Whiteboard = (props: WithTranslation): JSX.Element => {
|
||||
const excalidrawRef = useRef<any>(null);
|
||||
const excalidrawAPIRef = useRef<any>(null);
|
||||
const collabAPIRef = useRef<any>(null);
|
||||
|
||||
@@ -56,9 +71,23 @@ const Whiteboard = (props: WithTranslation): JSX.Element => {
|
||||
const filmstripWidth: number = useSelector(getVerticalViewMaxWidth);
|
||||
const collabDetails = useSelector(getCollabDetails);
|
||||
const collabServerUrl = useSelector(getCollabServerUrl);
|
||||
const storageBackendUrl = useSelector(getStorageBackendUrl);
|
||||
const { defaultRemoteDisplayName } = useSelector((state: IReduxState) => state['features/base/config']);
|
||||
const localParticipantName = useSelector(getLocalParticipant)?.name || defaultRemoteDisplayName || 'Fellow Jitster';
|
||||
|
||||
const jwt = useSelector((state: IReduxState) => state['features/base/jwt']).jwt || '';
|
||||
const store = useStore();
|
||||
const state = store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const sessionId = conference?.getMeetingUniqueId();
|
||||
const roomJid = conference?.room?.roomjid;
|
||||
|
||||
const meetingDetails: IMeetingDetails = {
|
||||
sessionId: sessionId ?? '',
|
||||
roomJid: roomJid ?? '',
|
||||
jwt: jwt
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!collabAPIRef.current) {
|
||||
return;
|
||||
@@ -141,23 +170,23 @@ const Whiteboard = (props: WithTranslation): JSX.Element => {
|
||||
{ props.t('whiteboard.accessibilityLabel.heading') }
|
||||
</span>
|
||||
}
|
||||
<ExcalidrawApp
|
||||
collabDetails = { collabDetails }
|
||||
collabServerUrl = { collabServerUrl }
|
||||
excalidraw = {{
|
||||
isCollaborating: true,
|
||||
langCode: i18next.language,
|
||||
|
||||
// @ts-ignore
|
||||
ref: excalidrawRef,
|
||||
theme: 'light',
|
||||
UIOptions: WHITEBOARD_UI_OPTIONS
|
||||
}}
|
||||
getCollabAPI = { getCollabAPI }
|
||||
getExcalidrawAPI = { getExcalidrawAPI } />
|
||||
<Suspense fallback = { null }>
|
||||
<LazyExcalidrawApp
|
||||
collabDetails = { collabDetails }
|
||||
collabServerUrl = { collabServerUrl }
|
||||
excalidraw = {{
|
||||
isCollaborating: true,
|
||||
langCode: i18next.language,
|
||||
theme: 'light',
|
||||
UIOptions: WHITEBOARD_UI_OPTIONS
|
||||
}}
|
||||
getCollabAPI = { getCollabAPI }
|
||||
getExcalidrawAPI = { getExcalidrawAPI }
|
||||
meetingDetails = { meetingDetails }
|
||||
storageBackendUrl = { storageBackendUrl } />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { generateCollaborationLinkData } from '@jitsi/excalidraw';
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
import BaseApp from '../../../base/app/components/BaseApp';
|
||||
@@ -33,6 +32,9 @@ export default class WhiteboardApp extends BaseApp<any> {
|
||||
|
||||
if (!roomId && !roomKey) {
|
||||
try {
|
||||
const { generateCollaborationLinkData } = await import(
|
||||
/* webpackChunkName: "excalidraw" */ '@jitsi/excalidraw'
|
||||
);
|
||||
const collabDetails = await generateCollaborationLinkData();
|
||||
|
||||
roomId = collabDetails.roomId;
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { ExcalidrawApp } from '@jitsi/excalidraw';
|
||||
import i18next from 'i18next';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import React, { Suspense, useCallback, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { WHITEBOARD_UI_OPTIONS } from '../../constants';
|
||||
import { getStorageBackendUrl } from '../../functions';
|
||||
|
||||
const LazyExcalidrawApp = React.lazy(async () => {
|
||||
const [ { ExcalidrawApp } ] = await Promise.all([
|
||||
import(/* webpackChunkName: "excalidraw" */ '@jitsi/excalidraw'),
|
||||
import(/* webpackChunkName: "excalidraw" */ '@jitsi/excalidraw/index.css')
|
||||
]);
|
||||
|
||||
return { default: ExcalidrawApp };
|
||||
});
|
||||
|
||||
/**
|
||||
* Whiteboard wrapper for mobile.
|
||||
@@ -23,9 +34,10 @@ const WhiteboardWrapper = ({
|
||||
collabServerUrl: string;
|
||||
localParticipantName: string;
|
||||
}) => {
|
||||
const excalidrawRef = useRef<any>(null);
|
||||
const excalidrawAPIRef = useRef<any>(null);
|
||||
const collabAPIRef = useRef<any>(null);
|
||||
const storageBackendUrl = useSelector(getStorageBackendUrl);
|
||||
const jwt = useSelector((state: IReduxState) => state['features/base/jwt']).jwt || '';
|
||||
|
||||
const getExcalidrawAPI = useCallback(excalidrawAPI => {
|
||||
if (excalidrawAPIRef.current) {
|
||||
@@ -45,24 +57,22 @@ const WhiteboardWrapper = ({
|
||||
return (
|
||||
<div className = { className }>
|
||||
<div className = 'excalidraw-wrapper'>
|
||||
<ExcalidrawApp
|
||||
collabDetails = { collabDetails }
|
||||
collabServerUrl = { collabServerUrl }
|
||||
detectScroll = { true }
|
||||
excalidraw = {{
|
||||
isCollaborating: true,
|
||||
langCode: i18next.language,
|
||||
|
||||
// @ts-ignore
|
||||
ref: excalidrawRef,
|
||||
theme: 'light',
|
||||
UIOptions: WHITEBOARD_UI_OPTIONS
|
||||
}}
|
||||
getCollabAPI = { getCollabAPI }
|
||||
getExcalidrawAPI = { getExcalidrawAPI } />
|
||||
<Suspense fallback = { null }>
|
||||
<LazyExcalidrawApp
|
||||
collabDetails = { collabDetails }
|
||||
collabServerUrl = { collabServerUrl }
|
||||
excalidraw = {{
|
||||
isCollaborating: true,
|
||||
langCode: i18next.language,
|
||||
theme: 'light',
|
||||
UIOptions: WHITEBOARD_UI_OPTIONS
|
||||
}}
|
||||
getCollabAPI = { getCollabAPI }
|
||||
getExcalidrawAPI = { getExcalidrawAPI }
|
||||
jwt = { jwt }
|
||||
storageBackendUrl = { storageBackendUrl } />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ export const WHITEBOARD_ID = 'whiteboard';
|
||||
export const WHITEBOARD_UI_OPTIONS = {
|
||||
canvasActions: {
|
||||
allowedShapes: [
|
||||
'arrow', 'diamond', 'ellipse', 'freedraw', 'line', 'rectangle', 'selection', 'text'
|
||||
'arrow', 'diamond', 'ellipse', 'freedraw', 'line', 'rectangle', 'selection', 'text', 'eraser'
|
||||
],
|
||||
allowedShortcuts: [
|
||||
'cut', 'deleteSelectedElements', 'redo', 'selectAll', 'undo'
|
||||
@@ -33,15 +33,18 @@ export const WHITEBOARD_UI_OPTIONS = {
|
||||
hideFontFamily: true,
|
||||
hideHelpDialog: true,
|
||||
hideIOActions: true,
|
||||
hideLaserOnCollaboration: true,
|
||||
hideLayers: true,
|
||||
hideLibraries: true,
|
||||
hideLockButton: true,
|
||||
hideOpacityInput: true,
|
||||
hideEmbedableTools: true,
|
||||
hideSharpness: true,
|
||||
hideStrokeStyle: true,
|
||||
hideTextAlign: true,
|
||||
hideThemeControls: true,
|
||||
hideUserList: true,
|
||||
hideWelcomeScreen: true,
|
||||
saveAsImageOptions: {
|
||||
defaultBackgroundValue: true,
|
||||
disableScale: true,
|
||||
|
||||
@@ -114,6 +114,16 @@ export const generateCollabServerUrl = (state: IReduxState): string | undefined
|
||||
export const getCollabServerUrl = (state: IReduxState): string | undefined =>
|
||||
getWhiteboardState(state).collabServerUrl;
|
||||
|
||||
/**
|
||||
* Returns the storage backend URL for saving whiteboard scenes and images.
|
||||
*
|
||||
* @param {IReduxState} state - The state from the Redux store.
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getStorageBackendUrl = (state: IReduxState): string | undefined =>
|
||||
getWhiteboardConfig(state).storageBackendUrl;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the whiteboard is visible on stage.
|
||||
*
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { generateCollaborationLinkData } from '@jitsi/excalidraw';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
@@ -142,6 +141,7 @@ function raiseWhiteboardNotification(status: WhiteboardStatus) {
|
||||
*/
|
||||
async function setNewWhiteboardOpen(store: IStore) {
|
||||
const { dispatch, getState } = store;
|
||||
const { generateCollaborationLinkData } = await import(/* webpackChunkName: "excalidraw" */ '@jitsi/excalidraw');
|
||||
const collabLinkData = await generateCollaborationLinkData();
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
@@ -42,7 +42,7 @@ end);
|
||||
local function shouldAllow(session)
|
||||
local token = session.auth_token;
|
||||
|
||||
if token ~= nil and session.jitsi_web_query_room and session.jitsi_web_query_prefix then
|
||||
if token ~= nil then
|
||||
-- cached tokens are banned
|
||||
if cache:get(token) then
|
||||
return false;
|
||||
|
||||
@@ -222,7 +222,20 @@ function process_main_muc_loaded(main_muc, host_module)
|
||||
local room = event.room;
|
||||
local json_msg = getMetadataJSON(room);
|
||||
|
||||
module:log('info', 'Metadata changed internally in room:%s,meeting_id:%s - broadcasting data:%s', room.jid, room._data.meetingId, json_msg);
|
||||
local log_json = json_msg;
|
||||
if room.jitsiMetadata and room.jitsiMetadata.transcription
|
||||
and room.jitsiMetadata.transcription.httpHeaders then
|
||||
local metadata_copy = table_shallow_copy(room.jitsiMetadata);
|
||||
local transcription_copy = table_shallow_copy(metadata_copy.transcription);
|
||||
local headers_redacted = {};
|
||||
for k, _ in pairs(transcription_copy.httpHeaders) do
|
||||
headers_redacted[k] = '***';
|
||||
end
|
||||
transcription_copy.httpHeaders = headers_redacted;
|
||||
metadata_copy.transcription = transcription_copy;
|
||||
log_json = getMetadataJSON(room, metadata_copy) or log_json;
|
||||
end
|
||||
module:log('info', 'Metadata changed internally in room:%s,meeting_id:%s - broadcasting data:%s', room.jid, room._data.meetingId, log_json);
|
||||
broadcastMetadata(room, json_msg);
|
||||
end);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local is_jibri = util.is_jibri;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local process_host_module = util.process_host_module;
|
||||
local is_transcriber_jigasi = util.is_transcriber_jigasi;
|
||||
local is_transcriber = util.is_transcriber;
|
||||
local jid_resource = require "util.jid".resource;
|
||||
local st = require "util.stanza";
|
||||
local socket = require "socket";
|
||||
@@ -226,7 +226,7 @@ function occupant_joined(event)
|
||||
|
||||
if is_healthcheck_room(room.jid)
|
||||
or is_admin(occupant.bare_jid)
|
||||
or is_transcriber_jigasi(stanza)
|
||||
or is_transcriber(occupant.jid)
|
||||
or is_jibri(occupant) then
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ local filter_identity_from_presence = util.filter_identity_from_presence;
|
||||
local is_admin = util.is_admin;
|
||||
local presence_check_status = util.presence_check_status;
|
||||
local process_host_module = util.process_host_module;
|
||||
local is_transcriber_jigasi = util.is_transcriber_jigasi;
|
||||
local is_jibri = util.is_jibri;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local json = require 'cjson.safe';
|
||||
|
||||
@@ -34,10 +34,6 @@ if main_muc_component_config == nil then
|
||||
return ;
|
||||
end
|
||||
|
||||
-- A list of domains which to be ignored for visitors. For occupants using those domain we do not propagate them
|
||||
-- to visitor nodes and we do not update them with presence changes
|
||||
local ignore_list = module:get_option_set('visitors_ignore_list', {});
|
||||
|
||||
-- Advertise the component for discovery via disco#items
|
||||
module:add_identity('component', 'visitors', 'visitors.'..module.host);
|
||||
|
||||
@@ -270,9 +266,8 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.dest_occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- filter focus and configured domains (used for jibri and transcribers)
|
||||
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil
|
||||
or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then
|
||||
-- filter focus and jibri
|
||||
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil or is_jibri(occupant.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -292,9 +287,9 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- ignore configured domains (jibri and transcribers)
|
||||
-- ignore jicofo and jibri
|
||||
if is_admin(occupant.bare_jid) or visitors_nodes[room.jid] == nil or visitors_nodes[room.jid].nodes == nil
|
||||
or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then
|
||||
or is_jibri(occupant.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -336,9 +331,8 @@ process_host_module(main_muc_component_config, function(host_module, host)
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- filter focus, ignore configured domains (jibri and transcribers)
|
||||
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil
|
||||
or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then
|
||||
-- filter focus, ignore jibri
|
||||
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil or is_jibri(occupant.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
@@ -512,8 +512,6 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
-- we skip any checks when auto-allow is enabled and room is live
|
||||
if (auto_allow_promotion or get_visitors_room_metadata(room).autoPromote and (is_live or is_live == nil))
|
||||
or ignore_list:contains(jid.host(stanza.attr.from)) -- jibri or other domains to ignore
|
||||
or is_sip_jigasi(stanza)
|
||||
or is_sip_jibri_join(stanza)
|
||||
or table_find(room._data.moderators, session.jitsi_meet_context_user and session.jitsi_meet_context_user.id)
|
||||
or table_find(room._data.participants, session.jitsi_meet_context_user and session.jitsi_meet_context_user.id) then
|
||||
if DEBUG then
|
||||
|
||||
@@ -503,27 +503,6 @@ function is_sip_jigasi(stanza)
|
||||
return stanza:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
|
||||
end
|
||||
|
||||
-- This requires presence stanza being passed
|
||||
function is_transcriber_jigasi(stanza)
|
||||
if not stanza then
|
||||
return false;
|
||||
end
|
||||
|
||||
local features = stanza:get_child('features');
|
||||
if not features then
|
||||
return false;
|
||||
end
|
||||
|
||||
for i = 1, #features do
|
||||
local feature = features[i];
|
||||
if feature.attr and feature.attr.var and feature.attr.var == 'http://jitsi.org/protocol/transcriber' then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function is_transcriber(jid)
|
||||
return starts_with_one_of(jid, TRANSCRIBER_PREFIXES);
|
||||
end
|
||||
@@ -738,7 +717,6 @@ return {
|
||||
is_sip_jibri_join = is_sip_jibri_join;
|
||||
is_sip_jigasi = is_sip_jigasi;
|
||||
is_transcriber = is_transcriber;
|
||||
is_transcriber_jigasi = is_transcriber_jigasi;
|
||||
is_vpaas = is_vpaas;
|
||||
get_focus_occupant = get_focus_occupant;
|
||||
get_ip = get_ip;
|
||||
|
||||
BIN
sounds/recordingAndTranscriptionOff.mp3
Normal file
BIN
sounds/recordingAndTranscriptionOff.mp3
Normal file
Binary file not shown.
BIN
sounds/recordingAndTranscriptionOff.opus
Normal file
BIN
sounds/recordingAndTranscriptionOff.opus
Normal file
Binary file not shown.
BIN
sounds/recordingAndTranscriptionOff_fr.mp3
Normal file
BIN
sounds/recordingAndTranscriptionOff_fr.mp3
Normal file
Binary file not shown.
BIN
sounds/recordingAndTranscriptionOff_fr.opus
Normal file
BIN
sounds/recordingAndTranscriptionOff_fr.opus
Normal file
Binary file not shown.
BIN
sounds/recordingAndTranscriptionOff_frCA.mp3
Normal file
BIN
sounds/recordingAndTranscriptionOff_frCA.mp3
Normal file
Binary file not shown.
BIN
sounds/recordingAndTranscriptionOff_frCA.opus
Normal file
BIN
sounds/recordingAndTranscriptionOff_frCA.opus
Normal file
Binary file not shown.
BIN
sounds/recordingAndTranscriptionOn.mp3
Normal file
BIN
sounds/recordingAndTranscriptionOn.mp3
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user