Compare commits

...

126 Commits

Author SHA1 Message Date
dependabot[bot]
0d32907178 chore(ci): bump actions/setup-node from 6.1.0 to 6.2.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](395ad32622...6044e13b5d)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:55:45 +00:00
Calinteodor
481b9a6e58 feat(tracks): add/remove "ended" listener when screen sharing
* Listen to the MediaStreamTrack "ended" event so local screen-share state is toggled off when capture is terminated externally.
2026-01-19 16:07:59 +02:00
damencho
fb3bc3c367 feat(dynamic-branding): Adds more options for overriding translations.
In branding you can add now
"labels-translation-languages": {
        "en": "/static/translation-overwritten-en.json"
    }

This allows overwriting strings from the translation-languages namespace, till now it was possible only for the main one.
2026-01-16 07:47:54 -06:00
zobadaniel
9fa5489154 add lower sorbian to the list of codes (#16844) 2026-01-16 07:39:02 -06:00
Anton
9499bf29ed fix(premeeting): device status indicator size when label is long (#16559) 2026-01-16 09:14:32 +01:00
José Luís Andrade
f605b5c487 lang: Update Portuguese translations in main-pt.json (#16790) 2026-01-15 22:04:30 -06:00
Nicolas
88fba5acab lang: Updates to Danish translation (#16781) 2026-01-15 22:01:23 -06:00
rassul
7bc79bc144 feat(lang): added kk translations 2026-01-15 21:50:00 -06:00
bgrozev
3e469019b5 test: Add a test for conference-request over XMPP. (#16838)
* test: Add a test for conference-request over XMPP.
2026-01-15 10:41:03 -06:00
bgrozev
d324935501 doc: Add a comment about JWT verification. (#16837) 2026-01-14 11:39:14 -06:00
Tobias
cd11cf6f65 ci: bump and pin Actions (#16553) 2026-01-14 10:24:41 +01:00
Matteo
5db3d529f4 lang: Update Italian translation 2026-01-13 18:40:46 -06:00
damencho
c7d2c9c204 fix(token): Keeps checks for allowlist only. 2026-01-12 15:51:55 -06:00
Harsh_w/n
9832c7a226 fix(recording): use cloned tracks for local recording; reset flags on stop (#16535)
Fixes: https://github.com/jitsi/jitsi-meet/issues/16491
2026-01-12 10:45:26 +01:00
mishraditi
12ee929499 docs: fix Contributing Guidelines link 2026-01-09 17:07:01 +01:00
vishal2005025
b9ed42613b fix(android): support String[] config overrides in mergeProps 2026-01-09 13:14:33 +02:00
Calinteodor
0d572b3bfb feat(participants-pane/native): fix participants sort (#13997)
* Make participants sort based on what web does.
2026-01-08 15:30:37 +02:00
damencho
609eab5f83 fix(chat): Clears private message notice if sender leaves.
If you are in a private conversation with someone and that participant leaves, the selection of messages switches to Everyone but the private message notice stays.
2026-01-07 12:33:38 -06:00
damencho
7bd0f479b9 fix(chat): Fixes sending messages after private message sender leaves. 2026-01-07 12:33:38 -06:00
Christoph Settgast
a634b6b2bc lang(de): Update chat permissions label as in #16807 2026-01-07 12:33:26 -06:00
Дамян Минков
0e53bd87ce fix(groupchat-polls-permissions): Adds UI setting to show the option. (#16807)
* fix(groupchat-polls-permissions): Adds UI setting to show the option.

Make sure we do not show the setting when everyone is a moderator in the room.

* squash: Change text and default value.

* squash: Defaults to false.

* squash: Set it in metadata.

* squash: Whitelist the config.
2026-01-05 16:27:32 -06:00
damencho
9e89c33796 fix(prosody): Fixes log message. 2026-01-05 13:18:36 -06:00
damencho
eaffd8b8f7 fix(breakout-rooms): Fix polls in main when breakout is enabled. 2026-01-05 13:18:36 -06:00
bgrozev
b8444d56ff fix: Fix nesting in the allure report (after allure 3 update). (#16816) 2026-01-05 13:15:47 -06:00
Christoph Settgast
42b2dd41c1 ci: fix android SDK build by removing unused preinstalled tools from github runner image (#16811)
* ci: try to fix android build by removing preinstalled stuff

The android SDK build anyway uses a docker image with react native and an android SDK inside, so remove android and haskell from the host runner

* More removals of preinstalled unused SDKs

dotnet is another 4GB, swift another 3GB
2025-12-30 14:58:22 -06:00
Christoph Settgast
229d1823fb lang: update German translation
Signed-off-by: Christoph Settgast <csett86_git@quicksands.de>
2025-12-30 12:20:45 -06:00
damencho
cd6e905b95 feat(test): Updates wdio to 9.22.0. 2025-12-29 13:40:50 -06:00
damencho
ddaf7a3180 fix(transcriptions): Drops not needed parameter.
Fixes an issue with iFrameAPI where toggleSubtitles will not do anything in case of async transcriptions turned on.
2025-12-29 12:24:47 -06:00
damencho
47aa51a58c fix(polls): Drops not needed check. 2025-12-29 12:24:36 -06:00
damencho
66f7b9de53 fix(polls): Updates polls validation. 2025-12-29 12:24:36 -06:00
emrah
bab87be9c9 fix(turnserver): no dtls 2025-12-23 14:02:53 +01:00
Jaya Allamsetty
6549d47233 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2116.0.0+40ad2744...v2118.0.0+67fd2c84
2025-12-19 15:15:16 -05:00
damencho
2063e66b8e fix(transcription): Fixes a UI bug where you cannot start transcription 2nd time. 2025-12-17 11:55:19 -06:00
Mihaela Dumitru
4dd241712d feat(external-api) expose mute remote command and participant muted event (#16768) 2025-12-17 14:14:30 +02:00
Calinteodor
a574d5ec79 feat(conference): apply reduce ui for web (#16763)
* Change stage view and use newly reducedUImainToolbarButtons config to show different custom buttons as main toolbar buttons for when web is in reduced UI.
2025-12-17 12:17:06 +02:00
Vishal Malyan
4b2b85bd12 fix(avatar) fix memory leak in preloadImage 2025-12-16 22:52:24 +01:00
Jaya Allamsetty
77ab1ea8ed fix(large-video) Fix auto-pinning of SS in large meetings. (#16773)
Fix(large-video) Fox auto pinning of screenshare in large meetings
2025-12-16 15:09:58 -05:00
Дамян Минков
05e47ade7c feat(lobby): Handle disabling lobby. (#16770)
* feat(lobby): Handle disabling lobby.

* squash: rename field.
2025-12-16 08:21:39 -06:00
damencho
6c78ec9099 fix(recording): Fixes inviting jigasi when backend recording is enabled. 2025-12-15 16:24:55 -06:00
Hristo Terezov
4c5afc0b5e chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2115.0.0+cc2f34c2...v2116.0.0+40ad2744
2025-12-08 18:52:57 -06:00
damencho
2e31ab9dca fix: Using recording dialog on asyncTranscription.
This partially reverts commit 02787b1 to avoid looping between request subtitles and start recording which results not closing the start recording dialog.
2025-12-08 14:00:51 -06:00
damencho
02787b1394 feat: Use recording dialog on asyncTranscription. 2025-12-05 16:10:24 -06:00
damencho
2476a06237 feat: Drops obsolete inviteJigasiOnBackendTranscribing. 2025-12-05 16:10:24 -06:00
Hristo Terezov
2d8909911e fix(pip): Fix PiP not working when enabled via overwriteConfig
Addresses multiple issues when enabling/disabling PiP dynamically:

1. External API: Replace dead config-overwrite event handler with
   interception in executeCommand. This properly manages the intersection
   observer and PiP state when pip config changes via overwriteConfig.

2. PiPVideoElement: Fix ref access pattern - access videoRef.current
   inside useEffects instead of capturing at render time. The captured
   value was null on first render, causing blur/focus listeners to never
   be set up when component mounted into a stable conference.

3. useCanvasAvatar: Return streamRef object instead of refs.current.stream
   so consumers can access .current inside their effects. The stream is
   created in an effect and wasn't available at render time.

4. Add on-mount focus check with loadedmetadata wait to handle PiP enable
   while app is in background, ensuring video source is ready before
   attempting to enter PiP mode.
2025-12-05 06:47:03 -06:00
Hristo Terezov
d06b847319 feat(pip): Add Picture-in-Picture support for Electron
Implements Picture-in-Picture functionality for the Electron wrapper to maintain video engagement when users are not actively focused on the conference window. This feature addresses the need to keep users visually connected to the conference even when multitasking.

Key features:
- Automatic PiP mode activation and deactivation based on user interaction
- Displays large video participant's stream or renders their avatar on canvas when video unavailable
- Provides audio/video mute controls via MediaSession API directly in PiP window
- Adds API events (_pip-requested) for Electron wrapper integration

Implementation includes new pip feature module with Redux architecture, canvas-based avatar rendering with custom backgrounds support, and integration with existing mute/unmute logic. Depends on jitsi-meet-electron-sdk#479 for proper user gesture handling in Electron.
2025-12-04 16:04:10 -06:00
Hristo Terezov
b517f614b3 fix(RN): Remove web files from build.
filmstrip/actions.web was imported in TileView native component.
filmstrip/actions.web was imported in config middleware.any.
2025-12-04 16:04:10 -06:00
Bastien Le Gall
10f77f1fbc fix(visitors): Fix s2sout host-unknown errors by skipping domain mapping 2025-12-04 14:26:56 -06:00
Vishal Malyan
77b89ece4a feat(api): add toolbarVisibilityChanged event to the IFrame API (#16659)
* Change toolbar background color from IFrame API #16468 fixed

* fix(toolbar #16468): implement toolbar background color via configOverwrite for web and mobile

* keep toolbarConfig defaults commented in config.js

* add trailing comma to commented toolbarConfig.backgroundColor

* fix: resolve linting errors

* feat(api): add toolbarVisibilityChanged event to IFrame API

* fix lint
2025-12-04 14:26:45 -06:00
Florian
129264c3c9 lang: Remove BETA label from noise suppression button (#16730)
Remove BETA label from noise suppression button as it was updated to a new stable RNNoise version
2025-12-03 08:19:33 -06:00
bgrozev
96c5a9abd1 fix: Fix reading transcription results from JVB. (#16725)
* fix: Fix reading transcription results from JVB.
2025-12-02 09:47:22 -06:00
Calinteodor
93ef2337ae dep(react-native): replace future deprecated SafeAreaView component (#16726)
* Replaced with react-native-safe-area-context component and fixed types around edges prop.
2025-12-02 16:34:57 +02:00
Дамян Минков
854a077684 fix(lobby): Send virtual jid for main room destroy. (#16724)
* fix(lobby): Send virtual jid for main room destroy.

* squash: Update and when manually disable lobby.
2025-12-01 12:57:26 -06:00
damencho
f903a7ae6e fix(prosody): Skips printing errors for reactions from visitors. 2025-12-01 09:26:29 -07:00
Saúl Ibarra Corretgé
9013881f76 chore(ci) clean Android build to save space
Add git clean command to CI workflow for Android.
2025-11-27 12:45:54 +01:00
dependabot[bot]
b6e7e0a19e chore(ci): bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 12:21:16 +01:00
dependabot[bot]
ae42e42534 chore(ci): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 11:17:58 +01:00
Saúl Ibarra Corretgé
21e2504cf9 chore(ci) add Dependabot configuration for GitHub Actions 2025-11-27 09:55:51 +01:00
dependabot[bot]
7a9ba79783 chore(deps): bump node-forge from 1.3.1 to 1.3.2
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.3.1 to 1.3.2.
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.2)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 08:51:30 +01:00
Damien Fetis
1f5a3b5b0f fix(recording): allow samesite iframe embeds to work with local recording
* fix(recording): allow samesite iframe embeds to work with local recording

Skip capture handle validation when inside an iframe to ensure local
recording works. This only applies if the iframe is served from the
same domain.

* fix(recording): add missing line breaks for better readability in LocalRecordingManager
2025-11-26 07:35:23 -07:00
bgrozev
fe2aff4f3c chore(deps) lib-jitsi-meet@latest (#16706)
https://github.com/jitsi/lib-jitsi-meet/compare/v2114.0.0+0e62818c...v2115.0.0+cc2f34c2
2025-11-25 11:57:53 -06:00
bgrozev
d847f6f96b feat: Accept transcription messages from non-participant entities. (#16631) 2025-11-25 11:06:16 -06:00
damencho
45ce467dcd feat(polls): Fixes support for breakout rooms.
Fixes #16693.
2025-11-25 09:30:10 -06:00
bgrozev
2b81fa6bd3 config(webpack): Listen on localhost by default. (#16703) 2025-11-25 08:42:19 -06:00
damencho
6f6100ceb2 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2113.0.0+ffcffaa7...v2114.0.0+0e62818c
2025-11-21 05:56:46 -06:00
Calin-Teodor
62cd1c29d7 align react native docker version with package json version 2025-11-21 13:45:53 +02:00
Дамян Минков
64869e8970 fix(deb): Adds Include on upgrade prosody. (#16687)
* fix(deb): Adds Include on upgrade prosody.

* squash: Restart if config has changed.
2025-11-20 16:13:29 -06:00
Saúl Ibarra Corretgé
29464e6886 Revert "chore(deps-dev): bump @react-native-community/cli from 15.0.1 to 17.0.1"
This reverts commit 421b21edeb.
2025-11-20 13:45:05 +01:00
Saúl Ibarra Corretgé
5ed92f2bc5 fix(deps) use Olm from npm
The Matrix GitLab repo was behind CF and thus affected by today's
outage.

Since they released the last Olm version to npm, let's consume that one.
2025-11-20 13:01:52 +01:00
srijan
048d12de24 feat(logging): replace console.* with centralized logger infrastructure (#16655)
* feat(logging): replace console.* with centralized logger infrastructure

* fix(logging): remove logger from size-constrained bundles
2025-11-19 18:31:35 -06:00
emrah
40c240c7ca fix(lang): add the missing translation (German) 2025-11-18 17:00:51 -06:00
Boris Grozev
289c1907e7 test: Skip iframe tests when the API is disabled. 2025-11-18 17:00:21 -06:00
damencho
35adea48ae fix(muc_rate_limit): Check connection when processing rate limited events.
If it happens that a connection was closed during waiting in the rate limited queue, we want to ignore those occupant events.
2025-11-18 14:24:26 -06:00
bgrozev
d72114d5bc test: Expect pin to have digits only, configure length. (#16670) 2025-11-18 12:00:06 -06:00
bgrozev
2f6b6ca837 fix: Fix transcription test expectation. (#16664) 2025-11-18 07:45:07 -06:00
bgrozev
615bbdc39b test: Order attachments by participant. (#16663) 2025-11-17 16:12:58 -06:00
bgrozev
ef97778158 test: Assert jaas visitors enabled. (#16662) 2025-11-17 16:12:45 -06:00
bgrozev
2885f39355 More test expectations (#16661)
* test: Add iframe API expectation.

* test: Add expectations for recording and live streaming.

* test: Remove iframe references from jaas/.

* test: Add a transcription expectation.
2025-11-17 16:12:34 -06:00
damencho
ae256b23b8 fix(cleanup_backend): Avoids cleanup when breakout rooms are active. 2025-11-17 12:56:00 -06:00
Дамян Минков
412aa83268 feat(jwt): Supports JWKS endpoint. (#16649)
* feat(jwt): Supports JWKS endpoint.

* squash: Allow setting just cache_keys_url.
2025-11-17 09:48:28 -06:00
damencho
f4c61e4760 fix(prosody): Order room-destroyed event.
Make sure we execute before prosody cleans it up from the list of room. If we try to look it up after that we will not find it. If we also add at 0 we cannot guarantee the order of hook execution.
2025-11-14 14:01:12 -06:00
Jaya Allamsetty
f313fb81d0 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2109.0.0+cb9d000c...v2113.0.0+ffcffaa7
2025-11-13 21:25:10 -05:00
Srijan
975af80e27 fix(chat): remove debug console.log statements from resize handlers 2025-11-13 16:52:24 -06:00
damencho
0a30a51bab feat(localstorage): Filter items. 2025-11-13 11:02:34 -06:00
Дамян Минков
54e28e223c fix(tests): Split participants presence. (#16642)
* fix(tests): Split participants presence.

* squash: Drop unused listener.
2025-11-12 11:42:43 -06:00
Edgars Voroboks
a4def96763 fix(lang): Update Latvian language translation 2025-11-12 11:42:29 -06:00
Hristo Terezov
dad4fb9e06 Revert "fix(large-video): Prevents unnecessary updates when container is hidden"
This reverts commit 6deb0a6385.
2025-11-11 12:48:16 -06:00
Vishal Malyan
3772b9a5ae feat(toolbar): implement toolbar background color via configOverwrite for web and mobile
* Change toolbar background color from IFrame API #16468 fixed

* fix(toolbar #16468): implement toolbar background color via configOverwrite for web and mobile

* keep toolbarConfig defaults commented in config.js

* add trailing comma to commented toolbarConfig.backgroundColor

* fix: resolve linting errors

Fixes #16468
2025-11-11 07:02:28 -06:00
bgrozev
89b9c75242 test: Default sort by test order in allure report. (#16636) 2025-11-10 14:07:23 -06:00
Дамян Минков
b24b60b735 fix(tests): Wait for transcriptions to be off via an event. (#16635) 2025-11-10 12:55:35 -06:00
Дамян Минков
486a1f6511 fix(tests): Avoids being blocked by notification when clicking toolbar buttons
* fix(tests): Avoids clicking UI buttons to avoid being blocked by notification.

In AV moderation tests sometimes clicking mute/unmute buttons is blocked by askedToUnmute notification.

* squash: fix waiting for button.

* squash: adds some docs.
2025-11-10 11:04:36 -06:00
Werner Fleischer
80b3f1d7d4 fix(mod_jitsi_permissions): Use correct session on moderator revocation
In the `process_set_affiliation` function, an undefined `session` variable was used when revoking moderator privileges. This prevented the `jitsi_meet_context_features` from being cleared for the occupant.
2025-11-10 05:07:47 -06:00
dependabot[bot]
421b21edeb chore(deps-dev): bump @react-native-community/cli from 15.0.1 to 17.0.1
Bumps [@react-native-community/cli](https://github.com/react-native-community/cli/tree/HEAD/packages/cli) from 15.0.1 to 17.0.1.
- [Release notes](https://github.com/react-native-community/cli/releases)
- [Changelog](https://github.com/react-native-community/cli/blob/main/packages/cli/CHANGELOG.md)
- [Commits](https://github.com/react-native-community/cli/commits/v17.0.1/packages/cli)

---
updated-dependencies:
- dependency-name: "@react-native-community/cli"
  dependency-version: 17.0.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-07 09:06:19 +01:00
Дамян Минков
a58b0d9a85 fix(tests): Update lobby test. (#16618)
* fix(tests): Update lobby test.
* test: Include name in "hangup" logs.
---------

Co-authored-by: Boris Grozev <boris@jitsi.org>
2025-11-06 14:56:43 -06:00
Дамян Минков
1aca8ab985 feat(dialog): Adds name to all dialogs. (#16626)
* feat(dialog): Adds name to all dialogs.

The name is used for debugging purposes to be added to logs.

* squash: Drop empty string.
2025-11-06 09:49:30 -06:00
Jaya Allamsetty
f9daba728f chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2101.0.0+8061f52a...v2109.0.0+cb9d000c
2025-11-05 15:45:04 -05:00
damencho
fbb6456317 fix(wait_for_host): Make sure the main room is set back to non-persistent. 2025-11-05 09:26:13 -06:00
damencho
52ead26bed feat(prosody): Adds a module to cleanup room with just service components in it. 2025-11-04 16:42:09 -06:00
damencho
8d1da83e3c fix(persistent_lobby): Avoids calling destroy twice. 2025-11-04 16:41:58 -06:00
Jaya Allamsetty
5453b615f5 fix(tests): Add missing helper function. 2025-11-04 11:04:54 -05:00
Jaya Allamsetty
81a7301a3e Revert "fix(large-video)pin prev speaker on stage when local user is dominant speaker. (#16511)"
This reverts commit 82d4628976.
2025-11-04 11:04:54 -05:00
Jaya Allamsetty
1138b7779b Revert "fix(filmstrip) Fixes an issue where remote tiles can disappear when SS is started"
This reverts commit 077602c427.
2025-11-04 11:04:54 -05:00
Calin-Teodor
2fd653d928 fix(chat): be explicit about screen navigation when polls are disabled 2025-11-04 15:35:24 +02:00
eastmancr
012c9fb329 feat(base): add WebRTC availability detection (#16608)
* feat(base/environment) add WebRTC availability detection

* feat(base/unsupported-browser) switch to JitsiMeetJS WebRTC detection

* fix(static/webrtcUnsupported) remove links
2025-11-03 14:25:10 -06:00
damencho
fdf95444e9 fix(lobby): Hide login button if authenticated(jwt is available). 2025-11-03 14:22:53 -06:00
Hristo Terezov
919c60b3d2 feat(chat): Add disableChat configuration option
Introduces a comprehensive disableChat config option that disables the entire chat feature including button visibility, notifications, sounds, private messages, and keyboard shortcuts. When disabled, the chat tab is hidden from the chat panel while allowing other tabs (polls, files, CC) to remain accessible.
2025-11-03 12:44:29 -06:00
Hristo Terezov
e02c4e8f7f feat(toolbox): Add polls and file sharing buttons to overflow menu
Adds dedicated buttons for polls and file sharing in the toolbar overflow menu, following the pattern of the CC button. Both buttons open the chat panel with their respective tab selected when clicked.
2025-11-03 12:44:29 -06:00
Дамян Минков
3fd9ce5f11 * fix(lobby): Updates metadata on destroy lobby room and let in participants on empty main.
* fix(lobby): Updates metadata on destroy lobby room.

* fix(visitors): Let people join lobby when main room is empty but with lobby.
2025-10-30 16:07:46 -05:00
bgrozev
93022b3281 feat: Filter transcription results. (#16606) 2025-10-30 15:38:00 -05:00
Jaya Allamsetty
5d63b31071 fix(video-layout) Possibly fixes auto-pinning of SS in a large call.
When a user joins a very large call with SS, sometime SS is not auto-pinned to stage. This may happen when lot of participant joins are processed at the same time and therefore the state for remoteScreenShares may not get updated in time. Added extra logging to help debug if this issue reproduces.
2025-10-30 15:21:29 -04:00
Jaya Allamsetty
4432f727a4 fix(tests) Check for continguous thunbnails in the filmstrip 2025-10-30 12:21:03 -04:00
Calin-Teodor
6f1bdb513a chore(android/sdk): invoke permissionListener immediatly 2025-10-30 10:52:13 +02:00
Jaya Allamsetty
ad144e6fd3 fix(tests) Check for the endpointID of the large-video (#16601) 2025-10-29 12:35:45 -04:00
Calin-Teodor
076d77a982 chore(android/sdk): handle foreground exceptions 2025-10-29 14:34:04 +02:00
damencho
5afdda7568 fix(tests): Give some time for recording events to be received. 2025-10-28 16:48:27 -05:00
Arvind Yadav.
6cb57c472c feat(avatar): Strip bracketed annotations from display names before generating initials
Fixes #16591.
2025-10-28 16:48:16 -05:00
bgrozev
e026bac42c feat(tests): Do not require WebhooksProxy for jaas dial-in test (#16595)
* Add a requireWebhooksProxy test property.

* test: Make the jaas dial-in test use but not require WH proxy.
2025-10-28 14:14:45 -05:00
Hugo Lavernhe
b29e48d471 fix(settings): Prevent enabling audio processing settings and stereo at the same time.
* Prevent enabling audio processing settings and stereo at the same time
2025-10-28 14:35:25 -04:00
Дамян Минков
99cb5e6c40 feat(metadata): Adds lobbyEnabled and visitorsEnabled to metadata. (#16583) 2025-10-28 09:20:37 -05:00
Calin-Teodor
01834903c2 feat(analytics): remove unused event 2025-10-28 16:11:48 +02:00
Avinash Alapati
2929317972 chore(editorconfig) extend to include Android and iOS indentation rules 2025-10-28 09:37:53 +01:00
damencho
57865d74c6 fix(tests): Fix hangup by avoiding not defined error.
ReferenceError: APP is not defined.
2025-10-23 13:29:25 -05:00
Дамян Минков
f8f331a576 fix(tests): Fix reference to APP in debug logs retrieval
Avoids `ReferenceError: APP is not defined` the failure may happen after hangup where APP is not defined.
2025-10-23 13:29:25 -05:00
Matteo
c610e955cd Update main-it.json 2025-10-23 13:29:17 -05:00
Boris Grozev
53899947a9 fix: Use latin M in logs. 2025-10-23 10:52:03 -05:00
Kerinlin
bf23107e7a fix(i18n): Fix Chinese language issues and hyphenated locale persistence
- Fix missing Chinese translations in main-zh-CN.json and main-zh-TW.json
- Fix language selection not persisting for hyphenated locales (zh-CN, zh-TW, es-US, fr-CA, pt-BR)
- Update normalizeCurrentLanguage to check exact match before normalization
2025-10-23 10:51:40 -05:00
bgrozev
6784921429 Minor test fixes (#16577)
* test: Increase password dialog timeout.
* test: Try to fix hangup().
2025-10-23 08:11:15 -05:00
284 changed files with 9405 additions and 3678 deletions

View File

@@ -14,3 +14,12 @@ trim_trailing_whitespace = false
[Makefile]
indent_style = tab
[*.{java,kt}]
indent_size = 4
[*.xml]
indent_size = 2
[*.{swift,m,mm,h}]
indent_size = 4

12
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
version: 2
updates:
# Monitor GitHub Actions versions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "chore(ci)"

View File

@@ -7,7 +7,7 @@ jobs:
name: Luacheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- name: Install luarocks
run: sudo apt-get --install-recommends -y install luarocks

View File

@@ -7,8 +7,8 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -42,8 +42,8 @@ jobs:
matrix:
os: [macos-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -59,8 +59,8 @@ jobs:
name: Build mobile bundle (Android)
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -74,8 +74,8 @@ jobs:
name: Build mobile bundle (iOS)
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -103,10 +103,21 @@ jobs:
android-sdk-build:
name: Build mobile SDK (Android)
runs-on: ubuntu-latest
container: reactnativecommunity/react-native-android:v18.0
container:
image: reactnativecommunity/react-native-android:v15.0
volumes:
- /usr:/host/usr
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Make space in image by removing preinstalled, but unused SDKs
run: |
df -h /
rm -rf /host/usr/local/lib/android
rm -rf /host/usr/local/.ghcup
rm -rf /host/usr/share/dotnet
rm -rf /host/usr/share/swift
df -h /
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -119,12 +130,15 @@ jobs:
cd android
./gradlew :sdk:clean
./gradlew :sdk:assembleRelease
- run: |
git config --global --add safe.directory /__w/jitsi-meet/jitsi-meet
git clean -dfx
ios-sdk-build:
name: Build mobile SDK (iOS)
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -146,7 +160,7 @@ jobs:
-workspace ios/jitsi-meet.xcworkspace \
-scheme JitsiMeetSDK
- name: setup-cocoapods
uses: ruby/setup-ruby@v1
uses: ruby/setup-ruby@4fc31e1c823882afd7ef55985266a526c589de90 #v1.282.0
with:
ruby-version: '3.4'
bundler-cache: true
@@ -173,8 +187,8 @@ jobs:
name: Test Debian packages build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 #v6.2.0
with:
node-version-file: '.nvmrc'
cache: 'npm'

View File

@@ -7,7 +7,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v8
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 #v10.1.0
with:
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'

View File

@@ -264,4 +264,4 @@ react/features/sample/
- [Jitsi Handbook](https://jitsi.github.io/handbook/) - Comprehensive documentation
- [Community Forum](https://community.jitsi.org/) - Ask questions and get support
- [Architecture Guide](https://jitsi.github.io/handbook/docs/architecture) - System overview
- [Contributing Guidelines](https://jitsi.github.io/handbook/docs/dev-guide/contributing) - Detailed contribution process
- [Contributing Guidelines](https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-contributing/) - Detailed contribution process

View File

@@ -37,7 +37,6 @@ public class JitsiMeetActivityDelegate {
* React Native module.
*/
private static PermissionListener permissionListener;
private static Callback permissionsCallback;
/**
* Tells whether or not the permissions request is currently in progress.
@@ -142,11 +141,6 @@ public class JitsiMeetActivityDelegate {
if (reactInstanceManager != null) {
reactInstanceManager.onHostResume(activity, new DefaultHardwareBackBtnHandlerImpl(activity));
}
if (permissionsCallback != null) {
permissionsCallback.invoke();
permissionsCallback = null;
}
}
/**
@@ -169,15 +163,10 @@ public class JitsiMeetActivityDelegate {
public static void onRequestPermissionsResult(
final int requestCode, final String[] permissions, final int[] grantResults) {
permissionsCallback = new Callback() {
@Override
public void invoke(Object... args) {
if (permissionListener != null
&& permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
permissionListener = null;
}
}
};
// Invoke the callback immediately
if (permissionListener != null && permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
permissionListener = null;
}
}
public static void requestPermissions(Activity activity, String[] permissions, int requestCode, PermissionListener listener) {

View File

@@ -99,6 +99,7 @@ public class JitsiMeetOngoingConferenceService extends Service implements Ongoin
public static void launch(Context context, HashMap<String, Object> extraData) {
List<String> permissionsList = new ArrayList<>();
Activity activity = (Activity) context;
PermissionListener listener = new PermissionListener() {
@Override
@@ -134,7 +135,7 @@ public class JitsiMeetOngoingConferenceService extends Service implements Ongoin
if (permissionsArray.length > 0) {
JitsiMeetActivityDelegate.requestPermissions(
(Activity) context,
activity,
permissionsArray,
PERMISSIONS_REQUEST_CODE,
listener
@@ -159,12 +160,20 @@ public class JitsiMeetOngoingConferenceService extends Service implements Ongoin
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
startForeground(NOTIFICATION_ID, notification);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
} else {
startForeground(NOTIFICATION_ID, notification);
}
} catch (Exception e) {
// Handle ForegroundServiceStartNotAllowedException when app is in background and cannot start foreground service.
// See: https://developer.android.com/develop/background-work/services/fgs/restrictions-bg-start#wiu-restrictions
JitsiMeetLogger.w(TAG + " Failed to start foreground service", e);
stopSelf();
return;
}
}

View File

@@ -30,6 +30,9 @@ import com.facebook.react.ReactRootView;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.ArrayList;
import java.util.Arrays;
public class JitsiMeetView extends FrameLayout {
@@ -84,6 +87,10 @@ public class JitsiMeetView extends FrameLayout {
result.putInt(key, (int)bValue);
} else if (valueType.contentEquals("Bundle")) {
result.putBundle(key, mergeProps((Bundle)aValue, (Bundle)bValue));
} else if (valueType.contentEquals("String[]")) {
// Convert String[] to ArrayList<String> for React Native bridge compatibility
String[] stringArray = (String[]) bValue;
result.putStringArrayList(key, new ArrayList<>(Arrays.asList(stringArray)));
} else {
throw new RuntimeException("Unsupported type: " + valueType);
}

View File

@@ -139,6 +139,9 @@ var config = {
// Disables polls feature.
// disablePolls: false,
// Disables chat feature entirely including notifications, sounds, and private messages.
// disableChat: false,
// Disables demote button from self-view
// disableSelfDemote: false,
@@ -148,6 +151,9 @@ var config = {
// Disables self-view settings in UI
// disableSelfViewSettings: false,
// Shows/hides the moderator setting for chat permissions.
// showChatPermissionsModeratorSetting: false,
// screenshotCapture : {
// Enables the screensharing capture feature.
// enabled: false,
@@ -517,9 +523,6 @@ var config = {
// // Note: Starting transcriptions from the recording dialog will still work.
// disableClosedCaptions: false,
// // Whether to invite jigasi when backend transcriptions are enabled (asyncTranscription is true in metadata).
// // By default, we invite it.
// inviteJigasiOnBackendTranscribing: true,
// },
// Misc
@@ -904,6 +907,8 @@ var config = {
// alwaysVisible: false,
// // Indicates whether the toolbar should still autohide when chat is open
// autoHideWhileChatIsOpen: false,
// // Default background color for the main toolbar. Accepts any valid CSS color.
// // backgroundColor: '#ffffff',
// },
// Overrides the buttons displayed in the main toolbar. Depending on the screen size the number of displayed
@@ -922,6 +927,11 @@ var config = {
// [ 'microphone', 'camera' ]
// ],
// Overrides the buttons displayed in the main toolbar for reduced UI.
// When there isn't an override for a certain configuration the default jitsi-meet configuration will be used.
// The order of the buttons in the array is preserved.
// reducedUImainToolbarButtons: [ 'microphone', 'camera' ],
// Toolbar buttons which have their click/tap event exposed through the API on
// `toolbarButtonClicked`. Passing a string for the button key will
// prevent execution of the click/tap routine; passing an object with `key` and

5
custom.d.ts vendored
View File

@@ -2,3 +2,8 @@ declare module '*.svg' {
const content: any;
export default content;
}
declare module '*.svg?raw' {
const content: string;
export default content;
}

View File

@@ -124,10 +124,17 @@ case "$1" in
ln -s $PROSODY_HOST_CONFIG /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua
fi
PROSODY_CREATE_JICOFO_USER="true"
fi
if ! grep -q "VirtualHost \"$JVB_HOSTNAME\"" $PROSODY_CONFIG_OLD; then
# on some distributions main prosody config doesn't include configs
# from conf.d folder enable it as this where we put our config by default
# also when upgrading to new prosody version from prosody repo we need to add it again
if ! grep -q "Include \"conf\.d\/\*\.cfg.lua\"" $PROSODY_CONFIG_OLD; then
echo -e "\nInclude \"conf.d/*.cfg.lua\"" >> $PROSODY_CONFIG_OLD
# trigger a restart
PROSODY_CONFIG_PRESENT="false"
fi
fi

View File

@@ -10,6 +10,7 @@ no-cli
no-loopback-peers
no-tcp-relay
no-tcp
no-dtls
listening-port=3478
tls-listening-port=5349
no-tlsv1

View File

@@ -31,6 +31,7 @@
"it": "Italiano",
"ja": "日本語",
"kab": "Taqbaylit",
"kk": "Қазақша",
"ko": "한국어",
"lt": "Lietuvių",
"lv": "Latviešu",

File diff suppressed because it is too large Load Diff

View File

@@ -109,9 +109,15 @@
}
},
"chat": {
"disabled": "Chat-Nachrichten sind deaktiviert.",
"enter": "Chat-Raum betreten",
"error": "Fehler: Ihre Nachricht wurde nicht versendet. Grund: {{error}}",
"everyone": "Alle",
"fieldPlaceHolder": "Geben Sie Ihre Nachricht hier ein",
"fileAccessibleTitle": "{{user}} hat eine Datei hochgeladen",
"fileAccessibleTitleMe": "Ich habe eine Datei hochgeladen",
"fileDeleted": "Eine Datei wurde gelöscht",
"guestsChatIndicator": "(Gast)",
"lobbyChatMessageTo": "Lobby-Nachricht an {{recipient}}",
"message": "Nachricht",
"messageAccessibleTitle": "{{user}} sagt:",
@@ -120,11 +126,20 @@
"messagebox": "Nachricht eingeben",
"newMessages": "Neue Nachrichten",
"nickname": {
"featureChat": "Chat",
"featureClosedCaptions": "Untertitel",
"featureFileSharing": "Dateien",
"featurePolls": "Umfragen",
"popover": "Wähle einen Alias",
"title": "Geben Sie einen Alias zum Chatten ein",
"titleWith1Features": "Geben Sie einen Alias ein, um {{feature1}} zu nutzen",
"titleWith2Features": "Geben Sie einen Alias ein, um {{feature1}} und {{feature2}} zu nutzen",
"titleWith3Features": "Geben Sie einen Alias ein, um {{feature1}}, {{feature2}} und {{feature3}} zu nutzen",
"titleWith4Features": "Geben Sie einen Alias ein, um {{feature1}}, {{feature2}}, {{feature3}} und {{feature4}} zu nutzen",
"titleWithCC": "Geben Sie einen Alias zum Chatten und für Untertitel ein",
"titleWithPolls": "Geben Sie einen Alias zum Chatten und für Umfragen ein",
"titleWithPollsAndCC": "Geben Sie einen Alias zum Chatten, für Umfragen und Untertitel ein"
"titleWithPollsAndCC": "Geben Sie einen Alias zum Chatten, für Umfragen und Untertitel ein",
"titleWithPollsAndCCAndFileSharing": "Geben Sie einen Alias zum Chatten, für Umfragen, Untertitel und Dateien ein"
},
"noMessagesMessage": "Es gibt noch keine Nachricht in dieser Konferenz. Starten Sie hier eine Unterhaltung!",
"privateNotice": "Private Nachricht an {{recipient}}",
@@ -132,14 +147,16 @@
"smileysPanel": "Emoji-Auswahl",
"systemDisplayName": "System",
"tabs": {
"chat": "Chatten",
"chat": "Chat",
"closedCaptions": "Untertitel",
"fileSharing": "Dateien",
"polls": "Umfragen"
},
"title": "Chatten",
"titleWithCC": "Chatten und Untertitel",
"titleWithPolls": "Chatten und Umfragen",
"titleWithPollsAndCC": "Chatten, Umfragen und Untertitel",
"title": "Chat",
"titleWithCC": "Untertitel",
"titleWithFeatures": "Chat und",
"titleWithFileSharing": "Dateien",
"titleWithPolls": "Umfragen",
"you": "Sie"
},
"chromeExtensionBanner": {
@@ -274,7 +291,6 @@
"Submit": "OK",
"Understand": "Verstanden, Stummschaltung beibehalten",
"UnderstandAndUnmute": "Verstanden, bitte Stummschaltung aufheben",
"WaitForHostMsg": "Die Konferenz wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
"WaitForHostNoAuthMsg": "Die Konferenz wurde noch nicht gestartet. Bitte warten Sie, bis die Konferenz gestartet wird.",
"WaitingForHostButton": "Auf Moderation warten",
"WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
@@ -296,6 +312,12 @@
"alreadySharedVideoTitle": "Nur ein geteiltes Video gleichzeitig",
"applicationWindow": "Anwendungsfenster",
"authenticationRequired": "Authentifizierung benötigt",
"cameraCaptureDialog": {
"description": "Ein Bild mit Ihrer Kamera aufnehmen und senden",
"ok": "Kamera starten",
"reject": "Jetzt nicht",
"title": "Ein Bild aufnehmen"
},
"cameraConstraintFailedError": "Ihre Kamera erfüllt die notwendigen Anforderungen nicht.",
"cameraNotFoundError": "Kamera nicht gefunden.",
"cameraNotSendingData": "Die Kamera ist nicht verfügbar. Bitte prüfen, ob eine andere Applikation die Kamera verwendet, eine andere Kamera vom Einstellungs-Menü auswählen oder die Applikation neu laden.",
@@ -371,22 +393,34 @@
"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",
"moderationVideoLabel": "Erlaube Anwesenden ihre Kamera einzuschalten",
"muteEveryoneDialog": "Wollen Sie wirklich alle stummschalten? Sie können deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.",
"muteEveryoneDialogModerationOn": "Die Anwesenden können eine Anfrage zum Sprechen jederzeit senden.",
"muteEveryoneElseDialog": "Einmal stummgeschaltet, können Sie deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.",
"muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?",
"muteEveryoneElsesDesktopDialog": "Sobald die Bildschirmfreigaben beendet sind, können Sie diese nicht mehr starten, aber die anderen können sie jederzeit wieder starten.",
"muteEveryoneElsesDesktopTitle": "Alle Bildschirmfreigaben außer {{whom}} beenden?",
"muteEveryoneElsesVideoDialog": "Sobald die Kamera für alle anderen Personen deaktiviert ist, können Sie diese nicht wieder für alle einschalten, die anderen Personen können ihre Kamera aber jederzeit wieder einschalten.",
"muteEveryoneElsesVideoTitle": "Die Kamera von allen außer {{whom}} ausschalten?",
"muteEveryoneSelf": "sich selbst",
"muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet",
"muteEveryoneTitle": "Alle stummschalten?",
"muteEveryonesDesktopDialog": "Die Anwesenden können ihren Bildschirm jederzeit freigeben.",
"muteEveryonesDesktopDialogModerationOn": "Die Anwesenden können jederzeit eine Anfrage zur Bildschirmfreigabe senden.",
"muteEveryonesDesktopTitle": "Alle Bildschirmfreigaben beenden?",
"muteEveryonesVideoDialog": "Sind Sie sicher, dass Sie die Kamera von allen Personen deaktivieren möchten? Sie können dies nicht wieder rückgängig machen, jede Personen kann ihre Kamera aber jederzeit wieder einschalten.",
"muteEveryonesVideoDialogModerationOn": "Die Anwesenden können jederzeit eine Anfrage senden, um ihre Kamera einzuschalten.",
"muteEveryonesVideoDialogOk": "deaktivieren",
"muteEveryonesVideoTitle": "Die Kamera von allen anderen ausschalten?",
"muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
"muteParticipantButton": "Stummschalten",
"muteParticipantsDesktopBody": "Sie können Bildschirmfreigaben von anderen nicht starten, sie können dies aber jederzeit.",
"muteParticipantsDesktopBodyModerationOn": "Weder Sie noch andere Anwesende können Bildschirmfreigaben starten.",
"muteParticipantsDesktopButton": "Bildschirmfreigabe beenden",
"muteParticipantsDesktopDialog": "Sind Sie sicher, dass Sie die Bildschirmfreigabe von dieser Person beenden möchten? Sie können diese nicht mehr starten, die Person aber jederzeit.",
"muteParticipantsDesktopDialogModerationOn": "Sind Sie sicher, dass Sie die Bildschirmfreigabe von dieser Person beenden möchten? Weder Sie noch die Person kann die Bildschirmfreigabe wieder starten.",
"muteParticipantsDesktopTitle": "Bildschirmfreigabe von dieser Person beenden?",
"muteParticipantsVideoBody": "Sie können die Kamera nicht wieder einschalten, die Person kann ihre Kamera aber jederzeit wieder einschalten.",
"muteParticipantsVideoBodyModerationOn": "Sie können die Kamera nicht wieder aktivieren und die Person selbst auch nicht.",
"muteParticipantsVideoButton": "Kamera ausschalten",
@@ -499,6 +533,7 @@
"tokenAuthFailedWithReasons": "Teilnahme an der Konferenz fehlgeschlagen. Möglicher Grund: {{reason}}",
"tokenAuthUnsupported": "Token-Authentifizierung wird nicht unterstützt.",
"transcribing": "Wird transkribiert",
"unauthenticatedAccessDisabled": "Zur Teilnahme an dieser Konferenz müssen Sie sich anmelden.",
"unlockRoom": "Konferenz$t(lockRoomPassword) entfernen",
"user": "Anmeldename",
"userIdentifier": "Benutzername",
@@ -539,6 +574,25 @@
"veryBad": "Sehr schlecht",
"veryGood": "Sehr gut"
},
"fileSharing": {
"downloadFailedDescription": "Bitte nochmals versuchen.",
"downloadFailedTitle": "Download fehlgeschlagen",
"downloadFile": "Download",
"downloadStarted": "Download gestartet",
"dragAndDrop": "Dateien hier oder irgendwo auf dem Bildschirm loslassen",
"fileAlreadyUploaded": "Datei wurde schon zur Konferenz hochgeladen.",
"fileRemovedByOther": "Ihre Datei '{{ fileName }}' wurde entfernt",
"fileTooLargeDescription": "Bitte stellen Sie sicher, dass Ihre Datei nicht die Maximalgröße von {{ maxFileSize }} überschreitet.",
"fileTooLargeTitle": "Die ausgewählte Datei ist zu groß",
"fileUploadProgress": "Datei wird hochgeladen",
"fileUploadedSuccessfully": "Datei erfolgreich hochgeladen",
"newFileNotification": "{{ participantName }} hat Datei '{{ fileName }}' hochgeladen",
"removeFile": "Entfernen",
"removeFileSuccess": "Datei erfolgreich entfernt",
"uploadFailedDescription": "Bitte versuchen Sie es erneut.",
"uploadFailedTitle": "Dateiupload fehlgeschlagen",
"uploadFile": "Datei hochladen"
},
"filmstrip": {
"accessibilityLabel": {
"heading": "Videominiaturen"
@@ -707,7 +761,8 @@
"notificationTitle": "Lobby",
"passwordJoinButton": "Beitreten",
"title": "Lobby",
"toggleLabel": "Lobby aktivieren"
"toggleLabel": "Lobby aktivieren",
"waitForModerator": "Die Konferenz wurde noch nicht gestartet, da noch keine Moderation anwesend ist. Wenn Sie zur Moderation gehören, melden Sie sich bitte an, ansonsten warten Sie bitte."
},
"localRecording": {
"clientState": {
@@ -750,8 +805,9 @@
"me": "ich",
"notify": {
"OldElectronAPPTitle": "Sicherheitslücke!",
"allowAll": "Alles einschalten",
"allowAudio": "Mikrofon einschalten",
"allowBoth": "Beides",
"allowDesktop": "Bildschirmfreigabe einschalten",
"allowVideo": "Kamera einschalten",
"allowedUnmute": "Sie können die Stummschaltung aufheben, Ihre Kamera einschalten oder Ihren Bildschirm teilen.",
"audioUnmuteBlockedDescription": "Díe Stummschaltung kann aus Überlastungsschutzgründen temporär nicht aufgehoben werden.",
@@ -765,6 +821,7 @@
"dataChannelClosedDescription": "Die Steuerungsverbindung (Bridge Channel) wurde unterbrochen, daher ist die Videoqulität auf die schlechteste Stufe limitiert.",
"dataChannelClosedDescriptionWithAudio": "Die Steuerungsverbindung (Bridge Channel) wurde unterbrochen, daher können Video- und Tonprobleme auftreten.",
"dataChannelClosedWithAudio": "Ton- und Videoqualität können beeinträchtigt sein",
"desktopMutedRemotelyTitle": "Ihre Bildschirmfreigabe wurde von {{participantDisplayName}} gestoppt",
"disabledIframe": "Die Einbettung ist nur für Demo-Zwecke vorgesehen. Diese Konferenz wird in {{timeout}} Minuten beendet.",
"disabledIframeSecondaryNative": "Die Einbettung von {{domain}} ist nur für Demo-Zwecke vorgesehen. Diese Konferenz wird in {{timeout}} Minuten beendet.",
"disabledIframeSecondaryWeb": "Die Einbettung von {{domain}} ist nur für Demo-Zwecke vorgesehen. Diese Konferenz wird in {{timeout}} Minuten beendet. Bitte nutzen Sie <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> für produktive Zwecke!",
@@ -822,6 +879,7 @@
"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": "!",
"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",
@@ -845,6 +903,7 @@
"suggestRecordingDescription": "Möchten Sie eine Aufzeichnung starten?",
"suggestRecordingTitle": "Konferenz aufzeichnen",
"unmute": "Stummschaltung aufheben",
"unmuteScreen": "Bildschirmfreigabe starten",
"unmuteVideo": "Kamera einschalten",
"videoMutedRemotelyDescription": "Sie können sie jederzeit wieder einschalten.",
"videoMutedRemotelyTitle": "Ihre Kamera wurde von {{participantDisplayName}} ausgeschaltet!",
@@ -864,11 +923,14 @@
"admit": "Zulassen",
"admitAll": "Alle zulassen",
"allow": "Anwesenden erlauben:",
"allowDesktop": "Bildschirm freizugeben",
"allowVideo": "Kamera einschalten",
"askDesktop": "Anfragen, Bildschirm freizugeben",
"askUnmute": "Anfragen, Stummschaltung aufzuheben",
"audioModeration": "Für sich selbst die Stummschaltung aufzuheben",
"blockEveryoneMicCamera": "Kamera und Mikrofon von allen sperren",
"breakoutRooms": "Breakout-Räume",
"desktopModeration": "Bildschirmfreigabe",
"goLive": "Live gehen",
"invite": "Person einladen",
"lowerAllHands": "Alle Hände senken",
@@ -880,6 +942,8 @@
"muteAll": "Alle stummschalten",
"muteEveryoneElse": "Alle anderen stummschalten",
"reject": "Ablehnen",
"stopDesktop": "Bildschirmfreigabe beenden",
"stopEveryonesDesktop": "Alle Bildschirmfreigaben beenden",
"stopEveryonesVideo": "Alle Kameras ausschalten",
"stopVideo": "Kamera ausschalten",
"unblockEveryoneMicCamera": "Kamera und Mikrofon von allen entsperren",
@@ -889,9 +953,11 @@
"headings": {
"lobby": "Lobby ({{count}})",
"participantsList": "Anwesende ({{count}})",
"viewerRequests": "Zuschaueranfragen {{count}}",
"visitorInQueue": " (Wartende Gäste {{count}})",
"visitorRequests": " (Anfragen {{count}})",
"visitors": "Gäste ({{count}})",
"visitorsList": "Zuschauer ({{count}})",
"waitingLobby": "In der Lobby ({{count}})"
},
"search": "Suche Anwesende",
@@ -912,6 +978,9 @@
"by": "Von {{ name }}",
"closeButton": "Umfrage schließen",
"create": {
"accessibilityLabel": {
"send": "Umfrage erstellen"
},
"addOption": "Antwort hinzufügen",
"answerPlaceholder": "Antwort {{index}}",
"cancel": "Abbrechen",
@@ -920,8 +989,7 @@
"pollQuestion": "Frage",
"questionPlaceholder": "Eine Frage stellen",
"removeOption": "Antwort entfernen",
"save": "Erstellen",
"send": "Senden"
"save": "Erstellen"
},
"errors": {
"notUniqueOption": "Optionen müssen einzigartig sein"
@@ -1122,7 +1190,7 @@
"signedIn": "Momentan wird auf Kalendertermine von {{email}} zugegriffen. Klicken Sie auf die folgende Schaltfläche „Trennen“, um den Zugriff auf die Kalendertermine zu stoppen.",
"title": "Kalender"
},
"chatWithPermissions": "Chat mit Freigaben",
"chatWithPermissions": "Chat nur für Moderation erlauben",
"desktopShareFramerate": "Framerate für Bildschirmfreigabe",
"desktopShareHighFpsWarning": "Eine höhere Framerate könnte sich auf Ihre Datenrate auswirken. Sie müssen die Bildschirmfreigabe neustarten, damit die Einstellung übernommen wird.",
"desktopShareWarning": "Sie müssen die Bildschirmfreigabe neustarten, damit die Einstellung übernommen wird.",
@@ -1328,6 +1396,20 @@
"videounmute": "Kamera einschalten"
},
"addPeople": "Personen zur Konferenz hinzufügen",
"advancedAudioSettings": {
"aec": {
"label": "Echounterdrückung"
},
"agc": {
"label": "Automatische Mikrofonlautstärke"
},
"ns": {
"label": "Rauschunterdrückung"
},
"stereo": {
"label": "Stereo"
}
},
"audioOnlyOff": "Modus „Nur Audio“ deaktivieren",
"audioOnlyOn": "Modus „Nur Audio“ aktivieren",
"audioRoute": "Audiogerät auswählen",
@@ -1355,6 +1437,7 @@
"exitFullScreen": "Vollbildmodus verlassen",
"exitTileView": "Kachelansicht ausschalten",
"feedback": "Feedback hinterlassen",
"fileSharing": "Dateien",
"giphy": "GIPHY ein-/ausschalten",
"hangup": "Konferenz verlassen",
"help": "Hilfe",
@@ -1390,6 +1473,7 @@
"openReactionsMenu": "Interaktionsmenü öffnen",
"participants": "Anwesende",
"pip": "Bild-in-Bild-Modus einschalten",
"polls": "Umfragen",
"privateMessage": "Private Nachricht senden",
"profile": "Profil bearbeiten",
"raiseHand": "Hand heben",
@@ -1399,6 +1483,7 @@
"reactionHeart": "Herz senden",
"reactionLaugh": "Lachen senden",
"reactionLike": "Daumen hoch senden",
"reactionLove": "Liebe senden",
"reactionSilence": "Stille senden",
"reactionSurprised": "Überrascht senden",
"reactions": "Interaktionen",
@@ -1484,6 +1569,8 @@
"connectionInfo": "Verbindungsinformationen",
"demote": "Zu Gästen verschieben",
"domute": "Stummschalten",
"domuteDesktop": "Bildschirmfreigabe beenden",
"domuteDesktopOfOthers": "Bildschirmfreigabe für alle beenden",
"domuteOthers": "Alle anderen stummschalten",
"domuteVideo": "Kamera ausschalten",
"domuteVideoOfOthers": "Alle anderen Kameras auschalten",
@@ -1548,6 +1635,8 @@
"noMainParticipantsTitle": "Diese Konferenz wurde noch nicht gestartet.",
"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.",
"title": "Sie sind Gast in der Konferenz"
},
"waitingMessage": "Sie werden der Konferenz beitreten, sobald sie gestartet ist!"

View File

@@ -114,6 +114,9 @@
"error": "Errore: il tuo messaggio non è stato inviato. Motivo: {{error}}",
"everyone": "Tutti",
"fieldPlaceHolder": "Scrivi qui il tuo messaggio",
"fileAccessibleTitle": "{{user}} ha caricato un file",
"fileAccessibleTitleMe": "ho caricato un file",
"fileDeleted": "Un file è stato eliminato",
"guestsChatIndicator": "(ospite)",
"lobbyChatMessageTo": "Messaggio a {{recipient}} in sala d'attesa",
"message": "Messaggio",
@@ -123,8 +126,16 @@
"messagebox": "Digita un messaggio",
"newMessages": "Nuovi messaggi",
"nickname": {
"featureChat": "la chat",
"featureClosedCaptions": "i sottotitoli",
"featureFileSharing": "la condivisione file",
"featurePolls": "i sondaggi",
"popover": "Scegli un nickname",
"title": "Inserisci un nickname per usare la chat",
"titleWith1Features": "Inserisci un nickname per usare {{feature1}}",
"titleWith2Features": "Inserisci un nickname per usare {{feature1}} e {{feature2}}",
"titleWith3Features": "Inserisci un nickname per usare {{feature1}}, {{feature2}} e {{feature3}}",
"titleWith4Features": "Inserisci un nickname per usare {{feature1}}, {{feature2}}, {{feature3}} e {{feature4}}",
"titleWithCC": "Inserisci un nickname per usare la chat e i sottotitoli",
"titleWithPolls": "Inserisci un nickname per usare la chat e i sondaggi",
"titleWithPollsAndCC": "Inserisci un nickname per usare la chat, i sondaggi e i sottotitoli",
@@ -156,7 +167,7 @@
"installExtensionText": "Installa un'estensione per integrare Google Calendar e Office 365"
},
"closedCaptionsTab": {
"emptyState": "Il contenuto dei sottotitoli sarà disponibile una volta che l'organizzatore lo attiverà",
"emptyState": "Il contenuto dei sottotitoli sarà disponibile una volta che un moderatore lo attiverà",
"startClosedCaptionsButton": "Attiva sottotitoli"
},
"connectingOverlay": {
@@ -280,10 +291,9 @@
"Submit": "Invia",
"Understand": "Accetto, mantieni microfono e videocamera disattivati per ora",
"UnderstandAndUnmute": "Accetto, riattiva microfono e videocamera",
"WaitForHostMsg": "La riunione non è ancora iniziata. Se sei l'organizzatore, per favore autenticati. Altrimenti, attendi l'arrivo dell'organizzatore.",
"WaitForHostNoAuthMsg": "La riunione non è ancora iniziata perché nessun organizzatore si è ancora collegato. Si prega di attendere.",
"WaitingForHostButton": "Attendi l'organizzatore",
"WaitingForHostTitle": "In attesa dell'organizzatore…",
"WaitForHostNoAuthMsg": "La riunione non è ancora iniziata perché nessun moderatore si è ancora collegato. Si prega di attendere.",
"WaitingForHostButton": "Attendi un moderatore",
"WaitingForHostTitle": "In attesa di un moderatore",
"Yes": "Sì",
"accessibilityLabel": {
"Cancel": "Annulla (chiudi dialogo)",
@@ -346,8 +356,8 @@
"error": "Errore",
"errorRoomCreationRestriction": "Hai provato ad accedere alla riunione troppo presto, torna tra un po'.",
"gracefulShutdown": "Il nostro servizio è al momento inattivo per manutenzione. Si prega di riprovare più tardi.",
"grantModeratorDialog": "Vuoi rendere relatore questo partecipante?",
"grantModeratorTitle": "Fai diventare relatore",
"grantModeratorDialog": "Desideri di concedere i permessi da moderatore a {{participantName}}?",
"grantModeratorTitle": "Concedi permessi da moderatore",
"hide": "Nascondi",
"hideShareAudioHelper": "Non mostrare più questa finestra",
"incorrectPassword": "Nome utente o password errati",
@@ -523,6 +533,7 @@
"tokenAuthFailedWithReasons": "Non sei autorizzato a partecipare a questa chiamata. Possibile motivo: {{reason}}",
"tokenAuthUnsupported": "Il token URL non è supportato.",
"transcribing": "Trascrizione in corso",
"unauthenticatedAccessDisabled": "Questa chiamata richiede l'autenticazione. Si prega di accedere per procedere.",
"unlockRoom": "Rimuovi la $t(lockRoomPassword) alla riunione",
"user": "Utente",
"userIdentifier": "Identificatore utente",
@@ -570,10 +581,12 @@
"downloadStarted": "Download del file iniziato",
"dragAndDrop": "Trascina e rilascia i file qui o da qualsiasi altra parte nella schermata",
"fileAlreadyUploaded": "Questo file è già stato caricato nella riunione.",
"fileRemovedByOther": "Il tuo file '{{ fileName }}' è stato rimosso",
"fileTooLargeDescription": "Assicurati che il file non superi {{ maxFileSize }}.",
"fileTooLargeTitle": "Il file selezionato è troppo grande",
"fileUploadProgress": "Caricamento del file in corso",
"fileUploadedSuccessfully": "Il file è stato caricato con successo",
"newFileNotification": "{{ participantName }} ha condiviso '{{ fileName }}'",
"removeFile": "Rimuovi",
"removeFileSuccess": "File rimosso con successo",
"uploadFailedDescription": "Si prega di riprovare.",
@@ -722,12 +735,12 @@
"emailField": "Inserisci il tuo indirizzo email",
"enableDialogPasswordField": "Imposta password (opzionale)",
"enableDialogSubmit": "Attiva",
"enableDialogText": "La sala d'attesa ti permette di proteggere la riunione concedendo l'accesso solo alle persone autorizzate da un organizzatore.",
"enableDialogText": "La sala d'attesa ti permette di proteggere la riunione concedendo l'accesso solo alle persone autorizzate da un moderatore.",
"enterPasswordButton": "Inserisci password riunione",
"enterPasswordTitle": "Inserisci la password per entrare nella riunione",
"errorMissingPassword": "Inserisci la password della riunione",
"invalidPassword": "Password errata",
"joinRejectedMessage": "La tua richiesta d'accesso è stata respinta da un organizzatore.",
"joinRejectedMessage": "La tua richiesta d'accesso è stata respinta da un moderatore.",
"joinRejectedTitle": "Richiesta d'accesso respinta.",
"joinTitle": "Entra nella riunione",
"joinWithPasswordMessage": "Tentativo di accesso con password in corso, si prega di attendere…",
@@ -748,7 +761,8 @@
"notificationTitle": "Sala d'attesa",
"passwordJoinButton": "Entra",
"title": "Sala d'attesa",
"toggleLabel": "Attiva sala d'attesa"
"toggleLabel": "Attiva sala d'attesa",
"waitForModerator": "La riunione non è ancora iniziata, perché non è arrivato alcun moderatore. Se vuoi diventarlo autenticati, altrimenti si prega di attendere."
},
"localRecording": {
"clientState": {
@@ -766,11 +780,11 @@
"me": "Io",
"messages": {
"engaged": "Registrazione avviata.",
"finished": "La registrazione della sessione {{token}} è terminata. Invia il file della registrazione all'organizzatore.",
"finishedModerator": "La registrazione della sessione {{token}} è terminata. La registrazione della traccia è stata salvata. Chiedi ai partecipanti di inviare le loro registrazioni.",
"notModerator": "Non sei un relatore. Non puoi avviare o interrompere la registrazione."
"finished": "La registrazione della sessione {{token}} è terminata. Invia il file della registrazione al moderatore.",
"finishedModerator": "La registrazione della sessione {{token}} è terminata. La registrazione della traccia locale è stata salvata. Si prega di chiedere ai partecipanti di inviare le loro registrazioni.",
"notModerator": "Non sei un moderatore. Non puoi avviare o interrompere la registrazione."
},
"moderator": "Relatore",
"moderator": "Moderatore",
"no": "No",
"participant": "Partecipante",
"participantStats": "Statistiche partecipante",
@@ -818,7 +832,7 @@
"focusFail": "{{component}} non disponibile - nuovo tentativo tra {{ms}} sec",
"gifsMenu": "GIPHY",
"groupTitle": "Notifiche",
"hostAskedUnmute": "Il relatore ti chiede di intervenire.",
"hostAskedUnmute": "Il moderatore ti chiede di intervenire.",
"invalidTenant": "Nome non valido",
"invalidTenantHyphenDescription": "Il nome che hai scelto non è valido (inizia o finisce con '-').",
"invalidTenantLengthDescription": "Il nome che hai scelto è troppo lungo.",
@@ -840,17 +854,17 @@
"localRecordingStopped": "{{name}} ha smesso di registrare.",
"me": "Io",
"moderationInEffectCSDescription": "Alza la mano, se vuoi condividere lo schermo.",
"moderationInEffectCSTitle": "La condivisione schermo è stata bloccata dal relatore",
"moderationInEffectCSTitle": "La condivisione schermo è stata bloccata dal moderatore",
"moderationInEffectDescription": "Alza la mano, se vuoi prendere la parola.",
"moderationInEffectTitle": "Il tuo microfono è stato spento dal relatore",
"moderationInEffectTitle": "Il tuo microfono è stato spento dal moderatore",
"moderationInEffectVideoDescription": "Alza la mano, se vuoi avviare la tua videocamera.",
"moderationInEffectVideoTitle": "La tua videocamera è stata spenta dal relatore",
"moderationRequestFromModerator": "Il relatore vorrebbe che tu accendessi il microfono",
"moderationInEffectVideoTitle": "La tua videocamera è stata spenta dal moderatore",
"moderationRequestFromModerator": "L'organizzatore ti chiede di accendere il microfono",
"moderationRequestFromParticipant": "Vuole parlare",
"moderationStartedTitle": "Moderazione in corso",
"moderationStoppedTitle": "Moderazione interrotta",
"moderationToggleDescription": "da {{participantDisplayName}}",
"moderator": "Ora sei un relatore!",
"moderator": "Ora sei un moderatore!",
"muted": "Hai iniziato la conversazione con il microfono disattivato.",
"mutedRemotelyDescription": "Puoi sempre attivare il microfono quando vuoi parlare. Spegni il microfono quando hai finito, per evitare rumori di fondo nella riunione.",
"mutedRemotelyTitle": "{{participantDisplayName}} ti ha spento il microfono",
@@ -964,6 +978,9 @@
"by": "Da {{ name }}",
"closeButton": "Chiudi sondaggio",
"create": {
"accessibilityLabel": {
"send": "Invia sondaggio"
},
"addOption": "Aggiungi opzione",
"answerPlaceholder": "Opzione {{index}}",
"cancel": "Annulla",
@@ -972,8 +989,7 @@
"pollQuestion": "Domanda del sondaggio",
"questionPlaceholder": "Fai una domanda",
"removeOption": "Elimina opzione",
"save": "Salva",
"send": "Invia"
"save": "Salva"
},
"errors": {
"notUniqueOption": "Le opzioni devono essere uniche"
@@ -1154,7 +1170,7 @@
},
"security": {
"about": "Puoi aggiungere una $t(lockRoomPassword) alla riunione. I partecipanti dovranno inserire la $t(lockRoomPassword) per accedere alla riunione.",
"aboutReadOnly": "Gli organizzatori possono aggiungere una $t(lockRoomPassword) alla riunione. I partecipanti dovranno inserire la $t(lockRoomPassword) per accedere alla riunione.",
"aboutReadOnly": "I moderatori possono aggiungere una $t(lockRoomPassword) alla riunione. I partecipanti dovranno inserire la $t(lockRoomPassword) per accedere alla riunione.",
"insecureRoomNameWarningNative": "Il nome della riunione è troppo semplice. Partecipanti non desiderati potrebbero entrare nella riunione. {{recommendAction}} Per saperne di più sulla sicurezza della tua riunione ",
"insecureRoomNameWarningWeb": "Il nome della riunione è troppo semplice. Partecipanti non desiderati potrebbero entrare nella riunione. {{recommendAction}} Per saperne di più sulla sicurezza della tua riunione <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">clicca qui</a>.",
"title": "Impostazioni di sicurezza",
@@ -1174,7 +1190,7 @@
"signedIn": "Accesso agli eventi del calendario per {{email}} in corso. Clicca sul pulsante Disconnetti per interrompere laccesso agli eventi sul calendario.",
"title": "Calendario"
},
"chatWithPermissions": "La chat richiede il permesso",
"chatWithPermissions": "Disattiva la chat per i partecipanti",
"desktopShareFramerate": "Frequenza di aggiornamento condivisone schermo",
"desktopShareHighFpsWarning": "Una frequenza di aggiornamento della condivisione dello schermo più alta può influire sulla tua connessione. Devi riavviare la condivisione schermo, per applicare le modifiche.",
"desktopShareWarning": "Devi riavviare la condivisione schermo, per applicare le modifiche.",
@@ -1187,8 +1203,8 @@
"loggedIn": "Connesso come {{name}}",
"maxStageParticipants": "Numero massimo di partecipanti che possono essere messi in evidenza nella schermata principale",
"microphones": "Microfoni",
"moderator": "Relatore",
"moderatorOptions": "Opzioni relatore",
"moderator": "Moderatore",
"moderatorOptions": "Opzioni moderatore",
"more": "Generali",
"name": "Nome",
"noDevice": "Nessuno",
@@ -1317,7 +1333,7 @@
"feedback": "Lascia un feedback",
"fullScreen": "Attiva modalità a schermo intero",
"giphy": "Mostra menu GIPHY",
"grantModerator": "Concedi permessi da relatore",
"grantModerator": "Concedi permessi da moderatore",
"hangup": "Lascia la riunione",
"heading": "Barra degli strumenti",
"help": "Aiuto",
@@ -1342,7 +1358,7 @@
"muteEveryoneElsesVideoStream": "Spegni la videocamera a tutti gli altri",
"muteEveryonesVideoStream": "Spegni la videocamera a tutti",
"muteGUMPending": "Connessione del microfono in corso",
"noiseSuppression": "Cancellazione del rumore (BETA)",
"noiseSuppression": "Cancellazione del rumore",
"openChat": "Apri chat",
"participants": "Apri pannello partecipanti. {{participantsCount}} partecipanti",
"pip": "Attiva modalità Picture-in-Picture",
@@ -1407,20 +1423,21 @@
"closeParticipantsPane": "Chiudi pannello partecipanti",
"closeReactionsMenu": "Chiudi menu reazioni",
"closedCaptions": "Sottotitoli",
"disableNoiseSuppression": "Disattiva cancellazione del rumore (BETA)",
"disableNoiseSuppression": "Disattiva cancellazione del rumore",
"disableReactionSounds": "Puoi disattivare i suoni delle reazioni in questa riunione",
"documentClose": "Chiudi documento condiviso",
"documentOpen": "Apri documento condiviso",
"download": "Scarica le nostre app",
"e2ee": "Crittografia End-to-End",
"embedMeeting": "Incorpora riunione",
"enableNoiseSuppression": "Attiva cancellazione del rumore (BETA)",
"enableNoiseSuppression": "Attiva cancellazione del rumore",
"endConference": "Termina la riunione per tutti",
"enterFullScreen": "Mostra a schermo intero",
"enterTileView": "Mostra vista a mosaico",
"exitFullScreen": "Esci dalla modalità a schermo intero",
"exitTileView": "Esci dalla vista a mosaico",
"feedback": "Lascia un feedback",
"fileSharing": "Condivisione file",
"giphy": "Menu GIPHY",
"hangup": "Lascia la riunione",
"help": "Aiuto",
@@ -1449,13 +1466,14 @@
"noAudioSignalDialInDesc": "Puoi anche chiamare usando:",
"noAudioSignalDialInLinkDesc": "Numeri di telefono",
"noAudioSignalTitle": "Nessun suono rilevato dal tuo microfono!",
"noiseSuppression": "Cancellazione del rumore (BETA)",
"noiseSuppression": "Cancellazione del rumore",
"noisyAudioInputDesc": "Sembra che il tuo microfono faccia rumore, si prega di spegnerlo o cambiarlo.",
"noisyAudioInputTitle": "Il tuo microfono sembra fare rumore!",
"openChat": "Apri chat",
"openReactionsMenu": "Apri il menu reazioni",
"participants": "Partecipanti",
"pip": "Abilita modalità Picture-in-Picture",
"polls": "Sondaggi",
"privateMessage": "Invia un messaggio privato",
"profile": "Modifica profilo",
"raiseHand": "Alza la mano",
@@ -1557,11 +1575,11 @@
"domuteVideo": "Disattiva videocamera",
"domuteVideoOfOthers": "Disattiva videocamera a tutti gli altri",
"flip": "Specchia",
"grantModerator": "Concedi permessi da relatore",
"grantModerator": "Concedi permessi da moderatore",
"hideSelfView": "Nascondi la tua immagine",
"kick": "Espelli",
"mirrorVideo": "Specchia il tuo video",
"moderator": "Relatore",
"moderator": "Moderatore",
"mute": "Il partecipante ha il microfono spento",
"muted": "Microfono spento",
"pinToStage": "Metti in primo piano",
@@ -1608,7 +1626,7 @@
"description": "Adesso sei uno spettatore in questa riunione.",
"raiseHand": "Alza la mano",
"title": "Ingresso nella riunione in corso",
"wishToSpeak": "Se vuoi parlare, si prega di alzare la mano sotto e aspettare l'autorizzazione del relatore."
"wishToSpeak": "Per parlare si prega di alzare la mano sotto e aspettare l'autorizzazione del moderatore."
},
"labelTooltip": "Numero di spettatori: {{count}}",
"notification": {
@@ -1617,6 +1635,8 @@
"noMainParticipantsTitle": "La riunione non è ancora iniziata.",
"noVisitorLobby": "Non puoi partecipare se la sala d'attesa è attiva per la riunione.",
"notAllowedPromotion": "Un partecipante deve autorizzare la tua richiesta prima.",
"requestToJoin": "Mano alzata",
"requestToJoinDescription": "La tua richiesta è stata inviata ai moderatori. Tieni duro!",
"title": "Sei uno spettatore nella riunione"
},
"waitingMessage": "Ti unirai alla riunione quando inizierà!"
@@ -1657,7 +1677,7 @@
"mobileDownLoadLinkAndroid": "Scarica applicazione per Android",
"mobileDownLoadLinkFDroid": "Scarica applicazione da F-Droid",
"mobileDownLoadLinkIos": "Scarica applicazione per iOS",
"moderatedMessage": "O <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">prepara un URL di riunione</a> in anticipo, quando sei l'unico organizzatore.",
"moderatedMessage": "O <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">prepara un URL di riunione</a> in anticipo, quando sei l'unico moderatore.",
"privacy": "Privacy",
"recentList": "Recenti",
"recentListDelete": "Cancella",

1703
lang/main-kk.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -114,6 +114,9 @@
"error": "Kļūda: Jūsu ziņa netika nosūtīta. Cēlonis: {{error}}",
"everyone": "Visi",
"fieldPlaceHolder": "Rakstiet ziņu šeit",
"fileAccessibleTitle": "{{user}} augšuplādēja failu",
"fileAccessibleTitleMe": "es augšuplādēju failu",
"fileDeleted": "Fails tika dzēsts",
"guestsChatIndicator": "(viesis)",
"lobbyChatMessageTo": "Vestibila tērzēšanas ziņa adresātam {{recipient}}",
"message": "Ziņa",
@@ -123,12 +126,20 @@
"messagebox": "Rakstiet ziņu",
"newMessages": "Jaunas ziņas",
"nickname": {
"popover": "Izvēlieties vārdu",
"title": "Ierakstiet vārdu, lai izmantotu tērzēšanā",
"titleWithCC": "Ievadiet segvārdu, lai izmantotu tērzēšanā un slēptos subtitros",
"titleWithPolls": "Ierakstiet segvārdu, lai izmantotu tērzēšanā un aptaujās",
"titleWithPollsAndCC": "Ievadiet segvārdu, lai izmantotu tērzēšanā, aptaujās un slēptos subtitros",
"titleWithPollsAndCCAndFileSharing": "Ievadiet segvārdu, lai izmantotu tērzēšanā, aptaujās, slēptos subtitros un failos"
"featureChat": "tērzētava",
"featureClosedCaptions": "slēgtie subtitri",
"featureFileSharing": "failu kopīgošana",
"featurePolls": "aptaujas",
"popover": "Izvēlieties segvārdu",
"title": "Ierakstiet segvārdu, lai izmantotu tērzēšanu",
"titleWith1Features": "Ievadiet segvārdu, lai izmantotu {{feature1}}",
"titleWith2Features": "Ievadiet segvārdu, lai izmantotu {{feature1}} un {{feature2}}",
"titleWith3Features": "Ievadiet segvārdu, lai izmantotu {{feature1}}, {{feature2}} un {{feature3}}",
"titleWith4Features": "Ievadiet segvārdu, lai izmantotu {{feature1}}, {{feature2}}, {{feature3}} un {{feature4}}",
"titleWithCC": "Ievadiet segvārdu, lai izmantotu tērzēšanu un slēgtos subtitrus",
"titleWithPolls": "Ierakstiet segvārdu, lai izmantotu tērzēšanu un aptaujas",
"titleWithPollsAndCC": "Ievadiet segvārdu, lai izmantotu tērzēšanu, aptaujas un slēgtos subtitrus",
"titleWithPollsAndCCAndFileSharing": "Ievadiet segvārdu, lai izmantotu tērzēšanu, aptaujas, slēgtos subtitrus un failus"
},
"noMessagesMessage": "Sapulcē pagaidām nav nevienas ziņas. Uzsāciet saraksti!",
"privateNotice": "Privāta ziņa adresātam {{recipient}}",
@@ -137,12 +148,12 @@
"systemDisplayName": "Sistēma",
"tabs": {
"chat": "Tērzēšana",
"closedCaptions": "Slēptie subtitri",
"closedCaptions": "Slēgtie subtitri",
"fileSharing": "Faili",
"polls": "Aptaujas"
},
"title": "Tērzēšana",
"titleWithCC": "Tērzēšana un Slēptie subtitri",
"titleWithCC": "Tērzēšana un Slēgtie subtitri",
"titleWithFeatures": "Tērzēšana un",
"titleWithFileSharing": "Faili",
"titleWithPolls": "Tērzēšana un Aptaujas",
@@ -156,8 +167,8 @@
"installExtensionText": "Uzstādīt spraudni Google kalendāra un Office 365 integrācijai"
},
"closedCaptionsTab": {
"emptyState": "Slēpto subtitru saturs būs pieejams, tiklīdz moderators uzsāks to.",
"startClosedCaptionsButton": "Uzsākt slēptos subtitrus"
"emptyState": "Slēgto subtitru saturs būs pieejams, tiklīdz moderators uzsāks to.",
"startClosedCaptionsButton": "Uzsākt slēgtos subtitrus"
},
"connectingOverlay": {
"joiningRoom": "Notiek pieslēgšanās jūsu sapulcei…"
@@ -280,7 +291,6 @@
"Submit": "Iesniegt",
"Understand": "Saprotu",
"UnderstandAndUnmute": "Es saprotu, lūdzu, ieslēdziet skaņu.",
"WaitForHostMsg": "Sapulce vēl nav sākusies, jo vēl nav ieradies neviens moderators. Lūdzu, autorizējieties, lai kļūtu par moderatoru. Pretējā gadījumā, lūdzu, uzgaidiet.",
"WaitForHostNoAuthMsg": "Sapulce vēl nav sākusies, jo vēl nav ieradies neviens moderators. Lūdzu, uzgaidiet.",
"WaitingForHostButton": "Gaidīt rīkotāju",
"WaitingForHostTitle": "Gaida rīkotāju…",
@@ -417,7 +427,7 @@
"muteParticipantsVideoDialog": "Vai tiešām vēlaties izslēgt šī dalībnieka kameru? Jūs nevarēsiet to ieslēgt atpakaļ, taču dalībnieks pats to varēs izdarīt jebkurā laikā.",
"muteParticipantsVideoDialogModerationOn": "Vai tiešām vēlaties izslēgt šī dalībnieka kameru? Ne Jūs, ne dalībnieks nevarēsiet to ieslēgt atpakaļ.",
"muteParticipantsVideoTitle": "Vai izslēgt šī dalībnieka video?",
"noDropboxToken": "Nav derīga Dropbox tokena",
"noDropboxToken": "Nav derīgas Dropbox pilnvaras",
"password": "Parole",
"passwordLabel": "Dalībnieks ir aizslēdzis sapulci. Lūdzu, ievadiet $t(lockRoomPassword), lai pievienotos.",
"passwordNotSupported": "Sapulces slēgšana ar $t(lockRoomPassword) netiek atbalstīta.",
@@ -501,7 +511,7 @@
"stopStreamingWarning": "Tiešām vēlaties beigt tiešraidi?",
"streamKey": "Tiešraides atslēga",
"thankYou": "Paldies, ka izmantojāt {{appName}}!",
"token": "tokens",
"token": "pilnvara",
"tokenAuthFailed": "Atvainojiet, jums nav atļauts pievienoties šim zvanam.",
"tokenAuthFailedReason": {
"audInvalid": "Nederīga `aud` vērtība. Tai vajadzētu būt `jitsi`.",
@@ -517,12 +527,13 @@
"nbfFuture": "`nbf` vērtība ir nākotnē.",
"nbfInvalid": "Nederīga `nbf` vērtība.",
"payloadNotFound": "Trūkst satura.",
"tokenExpired": "Token ir beidzies."
"tokenExpired": "Pilnvara ir beigusies."
},
"tokenAuthFailedTitle": "Autentifikācijas kļūda",
"tokenAuthFailedWithReasons": "Atvainojiet, jums nav atļauts pievienoties šim zvanam. Iespējamie iemesli: {{reason}}",
"tokenAuthUnsupported": "Token URL netiek atbalstīts.",
"tokenAuthUnsupported": "Pilnvaras URL nav atbalstīts.",
"transcribing": "Notiek atšifrējuma izveide",
"unauthenticatedAccessDisabled": "Šim zvanam nepieciešama autentifikācija. Lūdzu, piesakieties, lai turpinātu.",
"unlockRoom": "Noņemt $t(lockRoomPassword)",
"user": "Lietotājs",
"userIdentifier": "Lietotājvārds",
@@ -570,10 +581,12 @@
"downloadStarted": "Sākta faila lejuplāde",
"dragAndDrop": "Velciet un palaidiet failus šeit, vai jebkurā ekrāna vietā",
"fileAlreadyUploaded": "Fails jau ir augšuplādēts šajā sanāksmē.",
"fileRemovedByOther": "Jūsu fails '{{ fileName }}' tika noņemts",
"fileTooLargeDescription": "Lūdzu, pārliecinieties, vai faila lielums nepārsniedz {{ maxFileSize }}.",
"fileTooLargeTitle": "Izvēlētais fails ir pārāk liels",
"fileUploadProgress": "Faila augšuplādes gaita",
"fileUploadedSuccessfully": "Fails veiksmīgi augšuplādēts",
"newFileNotification": "{{ participantName }} kopīgoja '{{ fileName }}'",
"removeFile": "Noņemt",
"removeFileSuccess": "Fails veiksmīgi noņemts",
"uploadFailedDescription": "Lūdzu, mēģiniet vēlreiz.",
@@ -748,7 +761,8 @@
"notificationTitle": "Vestibils",
"passwordJoinButton": "Pievienoties",
"title": "Vestibils",
"toggleLabel": "Iespējot vestibilu"
"toggleLabel": "Iespējot vestibilu",
"waitForModerator": "Konference vēl nav sākusies, jo vēl nav ieradušies moderatori. Ja vēlaties kļūt par moderatoru, lūdzu, piesakieties. Pretējā gadījumā, lūdzu, uzgaidiet."
},
"localRecording": {
"clientState": {
@@ -775,7 +789,7 @@
"participant": "Dalībnieks",
"participantStats": "Dalībnieku statistika",
"selectTabTitle": "🎥 Lūdzu, atveriet šo cilni ierakstīšanai",
"sessionToken": "Sessijas tokens",
"sessionToken": "Sesijas Pilnvara",
"start": "Sākt ierakstu",
"stop": "Beigt ierakstu",
"stopping": "Ierakstīšanas pārtraukšana",
@@ -865,6 +879,7 @@
"oldElectronClientDescription1": "Izskatās, ka jūs izmantojat vecu Jitsi Meet klienta versiju, kurai ir zināmas drošības ievainojamības. Lūdzu, atjauniniet uz ",
"oldElectronClientDescription2": "jaunākā versija",
"oldElectronClientDescription3": "tagad!",
"openChat": "Atvērt tērzētavu",
"participantWantsToJoin": "Vēlas pievienoties sapulcei",
"participantsWantToJoin": "Vēlas pievienoties sapulcei",
"passwordRemovedRemotely": "Kāds dalībnieks noņēma $t(lockRoomPasswordUppercase).",
@@ -963,6 +978,9 @@
"by": "Pēc {{ name }} iniciatīvas",
"closeButton": "Slēgt aptauju",
"create": {
"accessibilityLabel": {
"send": "Nosūtīt aptauju"
},
"addOption": "Pievienot opciju",
"answerPlaceholder": "Opcija {{index}}",
"cancel": "Atcelt",
@@ -971,8 +989,7 @@
"pollQuestion": "Aptaujas Jautājums",
"questionPlaceholder": "Uzdod jautājumu",
"removeOption": "Noņemt opciju",
"save": "Saglabāt",
"send": "Nosūtīt"
"save": "Saglabāt"
},
"errors": {
"notUniqueOption": "Iespējām jābūt unikālām"
@@ -1091,7 +1108,7 @@
}
},
"recording": {
"authDropboxText": "Augšupielādēt uz Dropbox",
"authDropboxText": "Augšuplādēt uz Dropbox",
"availableSpace": "Pieejama vieta: {{spaceLeft}} MB (apmēram {{duration}} ieraksta minūtes)",
"beta": "BETA",
"busy": "Cenšamies nodrošināt ierakstam vairāk resursu. Lūdzu, pēc dažām minūtēm pamēģiniet vēlreiz.",
@@ -1145,7 +1162,7 @@
"title": "Ieraksts",
"unavailable": "Hmm! {{serviceName}} pašlaik nav pieejams. Mēs strādājam pie problēmas risināšanas. Lūdzu, pamēģiniet vēlreiz vēlāk.",
"unavailableTitle": "Ieraksts nav iespējams",
"uploadToCloud": "Augšupielādēt mākonī"
"uploadToCloud": "Augšuplādēt mākonī"
},
"screenshareDisplayName": "{{name}} ekrāns",
"sectionList": {
@@ -1300,7 +1317,7 @@
"closeChat": "Aizvērt tērzēšanu",
"closeMoreActions": "Aizvērt vairāk darbību izvēlni",
"closeParticipantsPane": "Aizvērt dalībnieku paneli",
"closedCaptions": "Slēptie subtitri",
"closedCaptions": "Slēgtie subtitri",
"collapse": "Sakļaut",
"document": "Kopīgotais dokuments (iesl./izsl.)",
"documentClose": "Aizvērt kopīgoto dokumentu",
@@ -1378,7 +1395,21 @@
"videomuteGUMPending": "Kameras pievienošana",
"videounmute": "Ieslēgt kameru"
},
"addPeople": "Pievienot cilvēkus savai sesijai/zvanam",
"addPeople": "Pievienot cilvēkus savam zvanam",
"advancedAudioSettings": {
"aec": {
"label": "Akustiskās atbalss slāpēšana"
},
"agc": {
"label": "Automātiska pastiprinājuma kontrole"
},
"ns": {
"label": "Trokšņu slāpēšana"
},
"stereo": {
"label": "Stereo"
}
},
"audioOnlyOff": "Atspējot kanāla/trafika taupības režīmu",
"audioOnlyOn": "Iespējot kanāla/trafika taupības režīmu",
"audioRoute": "Izvēlēties audioierīci",
@@ -1391,7 +1422,7 @@
"closeChat": "Aizvērt tērzētavu",
"closeParticipantsPane": "Aizvērt dalībnieku paneli",
"closeReactionsMenu": "Aizvērt reakciju izvēlni",
"closedCaptions": "Slēptie subtitri",
"closedCaptions": "Slēgtie subtitri",
"disableNoiseSuppression": "Atspējot trokšņu slāpēšanu",
"disableReactionSounds": "Šai sapulcei varat atspējot reakcijas skaņas",
"documentClose": "Aizvērt kopīgoto dokumentu",
@@ -1406,6 +1437,7 @@
"exitFullScreen": "Pilnekrāna režīms",
"exitTileView": "Tuvplāna režīms",
"feedback": "Atstāts atsauksmi",
"fileSharing": "Failu kopīgošana",
"giphy": "GIPHY izvēlne (rādīt/nerādīt)",
"hangup": "Iziet no sapulces",
"help": "Palīdzība",
@@ -1441,17 +1473,19 @@
"openReactionsMenu": "Atvērt reakciju izvēlni",
"participants": "Dalībnieki",
"pip": "Iesl. attēls attēlā (PIP) režīmu",
"polls": "Aptaujas",
"privateMessage": "Nosūtīt privātu ziņu",
"profile": "Rediģēt profilu",
"raiseHand": "Pacelt roku",
"raiseYourHand": "Pacelt roku",
"reactionBoo": "Nosūtīt būū reakciju",
"reactionClap": "Nosūtīt aplausu reakciju",
"reactionHeart": "Nosūtīt sirds reakciju",
"reactionLaugh": "Nosūtīt smieklu reakciju",
"reactionLike": "Nosūtīt īkšķi augšup reakciju",
"reactionSilence": "Nosūtīt klusuma reakciju",
"reactionSurprised": "Nosūtīt pārsteigts reakciju",
"reactionBoo": "Sūtīt būū reakciju",
"reactionClap": "Sūtīt aplausu reakciju",
"reactionHeart": "Sūtīt sirds reakciju",
"reactionLaugh": "Sūtīt smieklu reakciju",
"reactionLike": "Sūtīt īkšķis augšup reakciju",
"reactionLove": "Sūtīt mīlestības reakciju",
"reactionSilence": "Sūtīt klusuma reakciju",
"reactionSurprised": "Sūtīt pārsteiguma reakciju",
"reactions": "Reakcijas",
"security": "Drošības iespējas",
"selectBackground": "Izvēlēties fonu",
@@ -1484,7 +1518,7 @@
"failed": "Atšifrējuma izveide neizdevās",
"labelTooltip": "Šajā sapulcē notiek atšifrējuma izveide.",
"labelTooltipExtra": "Turklāt vēlāk būs pieejams atšifrējums.",
"openClosedCaptions": "Atvērt slēptos subtitrus",
"openClosedCaptions": "Atvērt slēgtos subtitrus",
"original": "Oriģināls",
"sourceLanguageDesc": "Pašlaik sapulces valoda ir iestatīta uz <b>{{sourceLanguage}}</b>. <br/> Varat to mainīt no ",
"sourceLanguageHere": "šeit",
@@ -1582,7 +1616,7 @@
"removeBackground": "Noņemt fonu",
"slightBlur": "Viegli izplūdis",
"title": "Virtuālie foni",
"uploadedImage": "Augšupielādēts attēls {{index}}",
"uploadedImage": "Augšuplādēts attēls {{index}}",
"webAssemblyWarning": "WebAssembly netiek atbalstīts",
"webAssemblyWarningDescription": "WebAssemb ir atspējots vai šī pārlūkprogramma to neatbalsta"
},
@@ -1601,6 +1635,8 @@
"noMainParticipantsTitle": "Šī sapulce vēl nav sākusies.",
"noVisitorLobby": "Jūs nevarat pievienoties, kamēr sapulcei ir iespējots vestibils.",
"notAllowedPromotion": "Dalībniekam vispirms ir jāatļauj jūsu pieprasījums.",
"requestToJoin": "Roka Pacelta",
"requestToJoinDescription": "Jūsu pieprasījums tika nosūtīts moderatoriem. Uzgaidiet!",
"title": "Jūs esat sapulces apmeklētājs"
},
"waitingMessage": "Jūs pievienosities sapulcei, tiklīdz tā sāksies!"

View File

@@ -114,6 +114,10 @@
"error": "Erro: a sua mensagem não foi enviada. Motivo: {{error}}",
"everyone": "Todos",
"fieldPlaceHolder": "Aa",
"fileAccessibleTitle": "{{user}} carregou um ficheiro",
"fileAccessibleTitleMe": "carreguei um arquivo",
"fileDeleted": "Um ficheiro foi eliminado",
"guestsChatIndicator": "(convidado)",
"lobbyChatMessageTo": "Mensagem de chat na sala de espera para {{recipient}}",
"message": "Mensagem",
"messageAccessibleTitle": "{{user}} disse:",
@@ -122,8 +126,16 @@
"messagebox": "Escreva uma mensagem",
"newMessages": "Novas mensagens",
"nickname": {
"featureChat": "chat",
"featureClosedCaptions": "legendas ocultas",
"featureFileSharing": "partilha de ficheiros",
"featurePolls": "sondagens",
"popover": "Escolha um apelido",
"title": "Introduza um apelido para usar o chat",
"titleWith1Features": "Insira um apelido para usar {{feature1}}",
"titleWith2Features": "Insira um apelido para usar {{feature1}} e {{feature2}}",
"titleWith3Features": "Insira um apelido para usar {{feature1}}, {{feature2}} e {{feature3}}",
"titleWith4Features": "Insira um apelido para usar {{feature1}}, {{feature2}}, {{feature3}} e {{feature4}}",
"titleWithCC": "Insira um apelido para usar o chat e as legendas ocultas",
"titleWithPolls": "Digite um apelido para usar o chat e as sondagens",
"titleWithPollsAndCC": "Insira um apelido para utilizar o chat, as sondagens e as legendas ocultas",
@@ -279,7 +291,6 @@
"Submit": "Submeter",
"Understand": "Entendo, mantenha-me em silêncio por enquanto.",
"UnderstandAndUnmute": "Entendo, por favor, desative o silêncio.",
"WaitForHostMsg": "A conferência ainda não começou porque ainda não chegaram moderadores. Se quiser ser um moderador, inicie a sessão. Caso contrário, aguarde.",
"WaitForHostNoAuthMsg": "A conferência ainda não começou porque ainda não chegaram os moderadores. Por favor, aguarde.",
"WaitingForHostButton": "Esperar pelo moderador",
"WaitingForHostTitle": "À espera de um moderador…",
@@ -522,6 +533,7 @@
"tokenAuthFailedWithReasons": "Lamentamos, mas não está autorizado a participar nesta chamada. Razões possíveis: {{reason}}",
"tokenAuthUnsupported": "O URL de token não é suportado.",
"transcribing": "Transcrição",
"unauthenticatedAccessDisabled": "Esta chamada requer autenticação. Por favor, inicie sessão para prosseguir.",
"unlockRoom": "Retirar reunião $t(lockRoomPassword)",
"user": "Utilizador",
"userIdentifier": "Identificador do utilizador",
@@ -566,13 +578,17 @@
"downloadFailedDescription": "Por favor, tente novamente.",
"downloadFailedTitle": "Falha no descarregar",
"downloadFile": "Descarregar",
"downloadStarted": "O download do ficheiro foi iniciado",
"dragAndDrop": "Arraste e solte os ficheiros aqui ou em qualquer lugar do ecrã",
"fileAlreadyUploaded": "O ficheiro já foi carregado para esta reunião.",
"fileRemovedByOther": "O seu ficheiro '{{ fileName }}' foi removido",
"fileTooLargeDescription": "Certifique-se de que o ficheiro não exceda {{ maxFileSize }}.",
"fileTooLargeTitle": "O ficheiro selecionado é muito grande",
"fileUploadProgress": "Progresso do envio do ficheiro",
"fileUploadedSuccessfully": "Ficheiro carregado com sucesso",
"newFileNotification": "{{ participantName }} partilhou '{{ fileName }}'",
"removeFile": "Remover",
"removeFileSuccess": "Ficheiro removido com sucesso",
"uploadFailedDescription": "Por favor, tente novamente.",
"uploadFailedTitle": "Falha ao carregar",
"uploadFile": "Partilhar ficheiro"
@@ -745,7 +761,8 @@
"notificationTitle": "Sala de espera",
"passwordJoinButton": "Solicitar",
"title": "Sala de espera",
"toggleLabel": "Ativar sala de espera"
"toggleLabel": "Ativar sala de espera",
"waitForModerator": "A conferência ainda não começou porque não chegou nenhum moderador. Se deseja tornar-se um moderador, faça login. Caso contrário, aguarde."
},
"localRecording": {
"clientState": {
@@ -862,6 +879,7 @@
"oldElectronClientDescription1": "Parece estar a utilizar uma versão antiga do cliente Jitsi Meet que tem vulnerabilidades de segurança conhecidas. Por favor, certifique-se de que actualiza a nossa ",
"oldElectronClientDescription2": "compilação mais recente",
"oldElectronClientDescription3": " agora!",
"openChat": "Abrir chat",
"participantWantsToJoin": "Deseja juntar-se à reunião",
"participantsWantToJoin": "Desejam juntar-se à reunião",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removido por outro participante",
@@ -960,6 +978,9 @@
"by": "Por {{ name }}",
"closeButton": "Fechar sondagem",
"create": {
"accessibilityLabel": {
"send": "Enviar sondagem"
},
"addOption": "Adicionar opção",
"answerPlaceholder": "Opção {{index}}",
"cancel": "Cancelar",
@@ -968,8 +989,7 @@
"pollQuestion": "Pergunta de Sondagem",
"questionPlaceholder": "Faça uma pergunta",
"removeOption": "Remover opção",
"save": "Guardar",
"send": "Enviar"
"save": "Guardar"
},
"errors": {
"notUniqueOption": "As opções devem ser únicas"
@@ -1338,7 +1358,7 @@
"muteEveryoneElsesVideo": "Parar o vídeo de todos os outros",
"muteEveryonesVideo": "Parar o vídeo de todos",
"muteGUMPending": "A ligar o seu microfone",
"noiseSuppression": "Supressão extra de ruído (BETA)",
"noiseSuppression": "Supressão extra de ruído",
"openChat": "Abrir chat",
"participants": "Abrir painel de participantes. {{participantsCount}} participantes",
"pip": "Mudar para o modo Picture-in-Picture",
@@ -1376,6 +1396,20 @@
"videounmute": "Iniciar câmara"
},
"addPeople": "Adicione pessoas à sua chamada",
"advancedAudioSettings": {
"aec": {
"label": "Cancelamento de eco acústico"
},
"agc": {
"label": "Controlo automático de ganho"
},
"ns": {
"label": "Supressão de ruído"
},
"stereo": {
"label": "Estéreo"
}
},
"audioOnlyOff": "Desativar modo de largura de banda baixa",
"audioOnlyOn": "Ativar modo de largura de banda baixa",
"audioRoute": "Selecionar o dispositivo de som",
@@ -1389,20 +1423,21 @@
"closeParticipantsPane": "Fechar painel de participantes",
"closeReactionsMenu": "Fechar menu de reações",
"closedCaptions": "Legendas ocultas",
"disableNoiseSuppression": "Desativar supressão de ruído extra (BETA)",
"disableNoiseSuppression": "Desativar supressão de ruído extra",
"disableReactionSounds": "Pode desactivar os sons de reacção para esta reunião",
"documentClose": "Fechar documento partilhado",
"documentOpen": "Abrir documento partilhado",
"download": "Descarregar as nossas aplicações",
"e2ee": "Criptografia ponta a ponta",
"embedMeeting": "Incorporar reunião",
"enableNoiseSuppression": "Ativar supressão extra de ruído (BETA)",
"enableNoiseSuppression": "Ativar supressão extra de ruído",
"endConference": "Terminar reunião para todos",
"enterFullScreen": "Ver em ecrã completo",
"enterTileView": "Ver em quadrícula",
"exitFullScreen": "Sair de ecrã completo",
"exitTileView": "Sair de quadrícula",
"feedback": "Deixar comentários",
"fileSharing": "Partilha de ficheiros",
"giphy": "Ativar/Desativar o menu GIPHY",
"hangup": "Sair da reunião",
"help": "Ajuda",
@@ -1431,13 +1466,14 @@
"noAudioSignalDialInDesc": "Também pode marcar usando:",
"noAudioSignalDialInLinkDesc": "Números de marcação",
"noAudioSignalTitle": "Não há nenhuma entrada vinda do seu microfone!",
"noiseSuppression": "Supressão extra de ruído (BETA)",
"noiseSuppression": "Supressão extra de ruído",
"noisyAudioInputDesc": "Parece que o seu microfone está a fazer barulho, por favor considere silenciar ou mudar de dispositivo.",
"noisyAudioInputTitle": "Seu microfone parece estar barulhento!",
"openChat": "Abrir chat",
"openReactionsMenu": "Abrir menu de reações",
"participants": "Participantes",
"pip": "Entrar no modo Picture-in-Picture",
"polls": "Sondagens",
"privateMessage": "Enviar mensagem privada",
"profile": "Editar o seu perfil",
"raiseHand": "Levantar a mão",
@@ -1447,6 +1483,7 @@
"reactionHeart": "Enviar reação com coração",
"reactionLaugh": "Enviar reação de risos",
"reactionLike": "Enviar reação de aprovado",
"reactionLove": "Enviar reação de amor",
"reactionSilence": "Enviar reação de silêncio",
"reactionSurprised": "Enviar reação de surpreendido",
"reactions": "Reações",
@@ -1598,6 +1635,8 @@
"noMainParticipantsTitle": "Esta reunião ainda não começou.",
"noVisitorLobby": "Não é possível aderir enquanto houver uma sala de espera activada para a reunião.",
"notAllowedPromotion": "É necessário que um participante autorize primeiro o seu pedido.",
"requestToJoin": "Mão levantada",
"requestToJoinDescription": "A sua solicitação foi enviada aos moderadores. Aguarde um momento!",
"title": "É um espectador na reunião"
},
"waitingMessage": "Participará na reunião assim que esta estiver em direto!"

View File

@@ -112,7 +112,12 @@
"disabled": "聊天已禁用",
"enter": "加入会议室",
"error": "错误:你的消息未发送。原因:{{error}}",
"everyone": "所有人",
"fieldPlaceHolder": "在这里输入你的信息",
"fileAccessibleTitle": "{{user}}上传了一个文件",
"fileAccessibleTitleMe": "我上传了一个文件",
"fileDeleted": "文件已被删除",
"guestsChatIndicator": "(访客)",
"lobbyChatMessageTo": "等候室聊天消息发送至{{recipient}}",
"message": "信息",
"messageAccessibleTitle": "{{user}}",
@@ -300,6 +305,12 @@
"alreadySharedVideoTitle": "同一时间只允许一个视频分享",
"applicationWindow": "应用程序窗口",
"authenticationRequired": "需要身份验证",
"cameraCaptureDialog": {
"description": "使用手机摄像头拍照并发送",
"ok": "打开相机",
"reject": "暂不使用",
"title": "拍照"
},
"cameraConstraintFailedError": "你的摄像头未满足某些必要条件。",
"cameraNotFoundError": "找不到摄像头",
"cameraNotSendingData": "我们无法访问你的摄像头,请检查是否有其他应用程序正在使用此设备,从设置菜单中选择另一个设备,或尝试重新加载应用程序。",
@@ -375,22 +386,34 @@
"micTimeoutError": "无法开启音频设备,发生超时!",
"micUnknownError": "由于未知原因,无法使用麦克风。",
"moderationAudioLabel": "允许参会者自己解除静音",
"moderationDesktopLabel": "允许非主持人共享屏幕",
"moderationVideoLabel": "允许参会者自己开启视频",
"muteEveryoneDialog": "参会者可以在任何时候解除自己的静音。",
"muteEveryoneDialogModerationOn": "参会者可以在任何时候请求发言。",
"muteEveryoneElseDialog": "静音后,你将无法为其解除静音,但是他们可以随时解除自己的静音。",
"muteEveryoneElseTitle": "除了{{whom}}以外的将所有人静音?",
"muteEveryoneElsesDesktopDialog": "一旦停止共享,你将无法重新开启他们的屏幕共享,但他们可以随时重新开启。",
"muteEveryoneElsesDesktopTitle": "停止除了{{whom}}以外所有人的屏幕共享?",
"muteEveryoneElsesVideoDialog": "一旦关闭,你将无法重新开启他们的摄像头,但他们随时可以重新开启。",
"muteEveryoneElsesVideoTitle": "除了{{whom}}以外,关闭所有人的摄像头?",
"muteEveryoneSelf": "你自己",
"muteEveryoneStartMuted": "现在所有人都已静音",
"muteEveryoneTitle": "静音所有人?",
"muteEveryonesDesktopDialog": "参会者可以随时共享他们的屏幕",
"muteEveryonesDesktopDialogModerationOn": "参会者可以随时请求共享他们的屏幕",
"muteEveryonesDesktopTitle": "停止所有人的屏幕共享?",
"muteEveryonesVideoDialog": "参会者可以随时开启他们的摄像头",
"muteEveryonesVideoDialogModerationOn": "参会者可以随时请求开启他们的摄像头",
"muteEveryonesVideoDialogOk": "关闭",
"muteEveryonesVideoTitle": "关闭所有人的摄像头?",
"muteParticipantBody": "你将无法为他们解除静音,但是他们可以随时解除自己的静音。",
"muteParticipantButton": "静音",
"muteParticipantsDesktopBody": "你无法重新开启他们的屏幕共享,但他们可以随时重新开启。",
"muteParticipantsDesktopBodyModerationOn": "你和他们都无法重新开启屏幕共享",
"muteParticipantsDesktopButton": "停止屏幕共享",
"muteParticipantsDesktopDialog": "你确定要停止这个参会者的屏幕共享吗?你将无法重新开启他们的屏幕共享,但他们可以随时重新开启。",
"muteParticipantsDesktopDialogModerationOn": "你确定要停止这个参会者的屏幕共享吗?你和他们都无法重新开启屏幕共享。",
"muteParticipantsDesktopTitle": "停止这个参会者的屏幕共享?",
"muteParticipantsVideoBody": "你无法重新开启摄像头,但他们随时可以重新开启。",
"muteParticipantsVideoBodyModerationOn": "你和他们都无法重新开启摄像头",
"muteParticipantsVideoButton": "关闭摄像头",
@@ -503,6 +526,7 @@
"tokenAuthFailedWithReasons": "抱歉,你无法加入此通话,原因:",
"tokenAuthUnsupported": "Token地址不支持",
"transcribing": "转录中",
"unauthenticatedAccessDisabled": "此会议需要身份验证,请先登录后继续。",
"unlockRoom": "移除会议$t(lockRoomPassword)",
"user": "用户",
"userIdentifier": "用户ID",
@@ -547,11 +571,17 @@
"downloadFailedDescription": "请稍后重试",
"downloadFailedTitle": "下载失败",
"downloadFile": "下载",
"downloadStarted": "文件下载已开始",
"dragAndDrop": "拖拽文件到此处上传",
"fileAlreadyUploaded": "文件已上传至本次会议",
"fileRemovedByOther": "你的文件{{fileName}}已被移除",
"fileTooLargeDescription": "请确保文件不超过 {{ maxFileSize }}",
"fileTooLargeTitle": "文件太大",
"fileUploadProgress": "文件上传进度",
"fileUploadedSuccessfully": "文件上传成功",
"newFileNotification": "{{participantName}}分享了{{fileName}}",
"removeFile": "移除",
"removeFileSuccess": "文件移除成功",
"uploadFailedDescription": "请稍后重试",
"uploadFailedTitle": "上传失败",
"uploadFile": "文件共享"
@@ -724,7 +754,8 @@
"notificationTitle": "等候室",
"passwordJoinButton": "加入",
"title": "等候室",
"toggleLabel": "开启等候室模式"
"toggleLabel": "开启等候室模式",
"waitForModerator": "会议尚未开始,暂无主持人入会。如需成为主持人请先登录,或耐心等待会议开始。"
},
"localRecording": {
"clientState": {
@@ -767,8 +798,10 @@
"me": "我",
"notify": {
"OldElectronAPPTitle": "安全漏洞!",
"allowAll": "允许全部",
"allowAudio": "允许开启麦克风",
"allowBoth": "允许音视频",
"allowDesktop": "允许屏幕共享",
"allowVideo": "允许开启摄像头",
"allowedUnmute": "你可以解除麦克风静音、启动摄像头或共享屏幕。",
"audioUnmuteBlockedDescription": "由于系统限制,麦克风解除静音操作被暂时阻止。",
@@ -782,6 +815,7 @@
"dataChannelClosedDescription": "桥接通道已断开,视频质量可能会被限制为最低设置",
"dataChannelClosedDescriptionWithAudio": "桥接通道已断开,音视频可能会出现卡顿或中断",
"dataChannelClosedWithAudio": "音视频质量可能受影响",
"desktopMutedRemotelyTitle": "你的屏幕共享已被{{participantDisplayName}}停止",
"disabledIframe": "嵌入仅用于演示,本次通话将在{{timeout}}分钟后自动断开",
"disabledIframeSecondaryNative": "嵌入{{domain}}仅用于演示,本次通话将在{{timeout}}分钟后自动断开",
"disabledIframeSecondaryWeb": "嵌入{{domain}}仅用于演示,本次通话将在{{timeout}}分钟后自动断开。如需在正式环境嵌入,请使用<a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi服务</a>",
@@ -839,6 +873,7 @@
"oldElectronClientDescription1": "你似乎正在使用存在已知安全漏洞的旧版Jitsi Meet客户端请确保您更新到我们的",
"oldElectronClientDescription2": "最新版本",
"oldElectronClientDescription3": "",
"openChat": "打开聊天",
"participantWantsToJoin": "想要加入会议",
"participantsWantToJoin": "想要加入会议",
"passwordRemovedRemotely": "其他参会者移除了$t(lockRoomPasswordUppercase)",
@@ -862,6 +897,7 @@
"suggestRecordingDescription": "是否需要录制本次会议?",
"suggestRecordingTitle": "录制会议",
"unmute": "解除静音",
"unmuteScreen": "开始屏幕共享",
"unmuteVideo": "开启摄像头",
"videoMutedRemotelyDescription": "你可随时重新开启视频",
"videoMutedRemotelyTitle": "{{participantDisplayName}}已关闭你的视频",
@@ -881,11 +917,14 @@
"admit": "同意加入",
"admitAll": "全部同意加入",
"allow": "允许参会者:",
"allowDesktop": "允许屏幕共享",
"allowVideo": "允许开启摄像头",
"askDesktop": "请求共享屏幕",
"askUnmute": "请求取消静音",
"audioModeration": "自行解除静音",
"blockEveryoneMicCamera": "禁用所有人的麦克风和摄像头",
"breakoutRooms": "分组讨论室",
"desktopModeration": "开始屏幕共享",
"goLive": "开始直播",
"invite": "邀请其他人",
"lowerAllHands": "取消全部举手",
@@ -897,6 +936,8 @@
"muteAll": "全体静音",
"muteEveryoneElse": "静音其他人",
"reject": "拒绝",
"stopDesktop": "停止屏幕共享",
"stopEveryonesDesktop": "停止所有人的屏幕共享",
"stopEveryonesVideo": "关闭所有人摄像头",
"stopVideo": "关闭摄像头",
"unblockEveryoneMicCamera": "允许所有人开启麦克风和摄像头",
@@ -906,9 +947,11 @@
"headings": {
"lobby": "等候室(({{count}}人)",
"participantsList": "会议参会者({{count}}人)",
"viewerRequests": "观众请求({{count}}人)",
"visitorInQueue": "(排队中:{{count}}人)",
"visitorRequests": "(请求加入:{{count}}人)",
"visitors": "观众(({{count}}人)",
"visitorsList": "观众({{count}}人)",
"waitingLobby": "在等候室等待({{count}}人)"
},
"search": "搜索参会者",
@@ -929,6 +972,9 @@
"by": "由{{ name }}发起",
"closeButton": "结束投票",
"create": {
"accessibilityLabel": {
"send": "发送投票"
},
"addOption": "添加选项",
"answerPlaceholder": "选项{{index}}",
"cancel": "取消",
@@ -1345,6 +1391,20 @@
"videounmute": "打开摄像头"
},
"addPeople": "添加成员到通话中",
"advancedAudioSettings": {
"aec": {
"label": "回声消除"
},
"agc": {
"label": "自动增益控制"
},
"ns": {
"label": "降噪"
},
"stereo": {
"label": "立体声"
}
},
"audioOnlyOff": "关闭省流模式",
"audioOnlyOn": "开启省流模式",
"audioRoute": "选择音频设备",
@@ -1416,6 +1476,7 @@
"reactionHeart": "发送爱心",
"reactionLaugh": "发送大笑",
"reactionLike": "发送点赞",
"reactionLove": "发送爱心",
"reactionSilence": "发送沉默",
"reactionSurprised": "发送惊讶",
"reactions": "互动表情",
@@ -1501,6 +1562,8 @@
"connectionInfo": "连接信息",
"demote": "设为观众",
"domute": "静音",
"domuteDesktop": "停止屏幕共享",
"domuteDesktopOfOthers": "停止屏幕共享给其他人",
"domuteOthers": "静音其他人",
"domuteVideo": "关闭摄像头",
"domuteVideoOfOthers": "关闭其他人摄像头",
@@ -1565,6 +1628,8 @@
"noMainParticipantsTitle": "会议尚未开始",
"noVisitorLobby": "当前会议已开启等候室,暂无法加入",
"notAllowedPromotion": "需由会议成员同意才能参与讨论",
"requestToJoin": "举手请求",
"requestToJoinDescription": "你的请求已发送给主持人,请稍候!",
"title": "你当前为会议观众"
},
"waitingMessage": "会议开始后将自动加入"

View File

@@ -112,7 +112,12 @@
"disabled": "聊天訊息已停用",
"enter": "加入聊天室",
"error": "錯誤:您的訊息未被傳送。原因:{{error}}",
"everyone": "所有人",
"fieldPlaceHolder": "在此輸入您的訊息",
"fileAccessibleTitle": "{{user}}上傳了一個檔案",
"fileAccessibleTitleMe": "我上傳了一個檔案",
"fileDeleted": "檔案已被刪除",
"guestsChatIndicator": "(訪客)",
"lobbyChatMessageTo": "大廳聊天訊息傳送至 {{recipient}}",
"message": "訊息",
"messageAccessibleTitle": "{{user}}",
@@ -300,6 +305,12 @@
"alreadySharedVideoTitle": "同一時間只允許一位影像分享",
"applicationWindow": "應用程式視窗",
"authenticationRequired": "需要驗證",
"cameraCaptureDialog": {
"description": "使用手機攝影機拍照並傳送",
"ok": "開啟相機",
"reject": "暫不使用",
"title": "拍照"
},
"cameraConstraintFailedError": "您的網路攝影機不符合要求。",
"cameraNotFoundError": "找不到網路攝影機。",
"cameraNotSendingData": "我們無法存取您的網路攝影機,請檢查是否有其他應用程式正在使用這個裝置,並從裝置選單裡選擇其他設備或者重新載入。",
@@ -375,22 +386,34 @@
"micTimeoutError": "無法啟動音訊裝置,連線逾時!",
"micUnknownError": "不明原因造成麥克風無法使用。",
"moderationAudioLabel": "允許與會者自我解除靜音",
"moderationDesktopLabel": "允許非主持人共享螢幕",
"moderationVideoLabel": "允許與會者開啟視訊",
"muteEveryoneDialog": "與會者可以隨時解除自己的靜音狀態。",
"muteEveryoneDialogModerationOn": "與會者可以隨時請求發言。",
"muteEveryoneElseDialog": "靜音後,您就不能再解除對方的靜音,但對方可以隨時解除自己的靜音狀態。",
"muteEveryoneElseTitle": "是否要讓除了 {{whom}} 以外的人靜音?",
"muteEveryoneElsesDesktopDialog": "一旦停止共享,您將無法重新開啟他們的螢幕共享,但他們可以隨時重新開啟。",
"muteEveryoneElsesDesktopTitle": "停止除了{{whom}}以外所有人的螢幕共享?",
"muteEveryoneElsesVideoDialog": "一旦停用,您就不能再重新開啟對方的網路攝影機,但對方隨時能重新開啟自己的網路攝影機。",
"muteEveryoneElsesVideoTitle": "是否要關閉除了 {{whom}} 以外的人的網路攝影機?",
"muteEveryoneSelf": "您自己",
"muteEveryoneStartMuted": "現在所有人皆已靜音",
"muteEveryoneTitle": "要將所有人靜音嗎?",
"muteEveryonesDesktopDialog": "與會者可以隨時共享他們的螢幕",
"muteEveryonesDesktopDialogModerationOn": "與會者可以隨時請求共享他們的螢幕",
"muteEveryonesDesktopTitle": "停止所有人的螢幕共享?",
"muteEveryonesVideoDialog": "與會者隨時可以重新開啟自己的網路攝影機。",
"muteEveryonesVideoDialogModerationOn": "與會者可以隨時傳送開啟視訊請求。",
"muteEveryonesVideoDialogOk": "停用",
"muteEveryonesVideoTitle": "要關閉所有人的網路攝影機嗎?",
"muteParticipantBody": "您無法對他們解除靜音,但是他們自己隨時可以解除靜音。",
"muteParticipantButton": "靜音",
"muteParticipantsDesktopBody": "您無法重新開啟他們的螢幕共享,但他們可以隨時重新開啟。",
"muteParticipantsDesktopBodyModerationOn": "您和他們都無法重新開啟螢幕共享",
"muteParticipantsDesktopButton": "停止螢幕分享",
"muteParticipantsDesktopDialog": "您確定要停止這位與會者的螢幕共享嗎?您將無法重新開啟他們的螢幕共享,但他們可以隨時重新開啟。",
"muteParticipantsDesktopDialogModerationOn": "您確定要停止這位與會者的螢幕共享嗎?您和他們都無法重新開啟螢幕共享。",
"muteParticipantsDesktopTitle": "停止這位與會者的螢幕共享?",
"muteParticipantsVideoBody": "您無法重新開啟,只有對方能自己重新開啟。",
"muteParticipantsVideoBodyModerationOn": "您和他都無法再將視訊重新開啟。",
"muteParticipantsVideoButton": "停用網路攝影機",
@@ -503,6 +526,7 @@
"tokenAuthFailedWithReasons": "抱歉,您無法參加這個通話,可能原因:{{reason}}",
"tokenAuthUnsupported": "不支援權杖網址。",
"transcribing": "轉錄中",
"unauthenticatedAccessDisabled": "此會議需要身份驗證,請先登入後繼續。",
"unlockRoom": "移除會議 $t(lockRoomPassword)",
"user": "使用者",
"userIdentifier": "使用者 ID",
@@ -547,11 +571,17 @@
"downloadFailedDescription": "請重試",
"downloadFailedTitle": "下載失敗",
"downloadFile": "下載",
"downloadStarted": "檔案下載已開始",
"dragAndDrop": "將檔案拖曳至此或畫面任一處上傳",
"fileAlreadyUploaded": "檔案已上傳至此會議",
"fileRemovedByOther": "您的檔案「{{fileName}}」已被移除",
"fileTooLargeDescription": "請確認檔案未超過 {{ maxFileSize }}",
"fileTooLargeTitle": "檔案過大",
"fileUploadProgress": "檔案上傳進度",
"fileUploadedSuccessfully": "檔案上傳成功",
"newFileNotification": "{{participantName}}分享了「{{fileName}}」",
"removeFile": "移除",
"removeFileSuccess": "檔案移除成功",
"uploadFailedDescription": "請重試",
"uploadFailedTitle": "上傳失敗",
"uploadFile": "分享檔案"
@@ -724,7 +754,8 @@
"notificationTitle": "大廳",
"passwordJoinButton": "加入",
"title": "大廳",
"toggleLabel": "啟用大廳模式"
"toggleLabel": "啟用大廳模式",
"waitForModerator": "會議尚未開始,暫無主持人入會。如需成為主持人請先登入,或耐心等待會議開始。"
},
"localRecording": {
"clientState": {
@@ -767,8 +798,10 @@
"me": "我",
"notify": {
"OldElectronAPPTitle": "安全漏洞!",
"allowAll": "允許全部",
"allowAudio": "允許音訊",
"allowBoth": "允許音訊與視訊",
"allowDesktop": "允許螢幕分享",
"allowVideo": "允許視訊",
"allowedUnmute": "您可以將麥克風解除靜音、開啟視訊,或是分享您的螢幕。",
"audioUnmuteBlockedDescription": "麥克風解除靜音操作由於系統限制而被暫時封鎖。",
@@ -782,6 +815,7 @@
"dataChannelClosedDescription": "橋接通道已斷開,視訊品質降至最低設定。",
"dataChannelClosedDescriptionWithAudio": "橋接通道已斷開,音訊和視訊可能會受到影響。",
"dataChannelClosedWithAudio": "音訊和視訊品質可能會降低。",
"desktopMutedRemotelyTitle": "您的螢幕分享已被{{participantDisplayName}}停止",
"disabledIframe": "嵌入僅供示範使用,此通話將於 {{timeout}} 分鐘後中斷連線。",
"disabledIframeSecondaryNative": "嵌入 {{domain}} 僅供示範,此通話將於 {{timeout}} 分鐘後中斷。",
"disabledIframeSecondaryWeb": "嵌入 {{domain}} 僅供示範,此通話將於 {{timeout}} 分鐘後中斷,請使用 <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi 服務</a> 來進行正式嵌入!",
@@ -839,6 +873,7 @@
"oldElectronClientDescription1": "您似乎正在使用存在已知安全漏洞的過時 Jitsi Meet 用戶端,請盡快更新至我們的",
"oldElectronClientDescription2": "最新版本",
"oldElectronClientDescription3": "",
"openChat": "開啟聊天",
"participantWantsToJoin": "希望加入會議",
"participantsWantToJoin": "希望加入會議",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) 已被其他與會者移除",
@@ -862,6 +897,7 @@
"suggestRecordingDescription": "是否要開始錄製這場會議?",
"suggestRecordingTitle": "錄製此會議",
"unmute": "取消靜音",
"unmuteScreen": "開始螢幕分享",
"unmuteVideo": "啟用視訊",
"videoMutedRemotelyDescription": "您隨時可以再次啟用。",
"videoMutedRemotelyTitle": "您的視訊已被 {{participantDisplayName}} 停用",
@@ -881,11 +917,14 @@
"admit": "準許",
"admitAll": "準許所有人",
"allow": "允許與會者能夠:",
"allowDesktop": "允許螢幕分享",
"allowVideo": "允許視訊",
"askDesktop": "請求共享螢幕",
"askUnmute": "要求解除靜音",
"audioModeration": "自我解除靜音",
"blockEveryoneMicCamera": "停用所有人的麥克風和網路攝影機",
"breakoutRooms": "分組討論室",
"desktopModeration": "開始螢幕分享",
"goLive": "開始直播",
"invite": "邀請他人",
"lowerAllHands": "全部取消舉手",
@@ -897,6 +936,8 @@
"muteAll": "靜音所有人",
"muteEveryoneElse": "靜音其他人",
"reject": "拒絕",
"stopDesktop": "停止螢幕分享",
"stopEveryonesDesktop": "停止所有人的螢幕分享",
"stopEveryonesVideo": "停用所有人的視訊",
"stopVideo": "停用視訊",
"unblockEveryoneMicCamera": "解除封鎖所有人的麥克風及網路攝影機",
@@ -906,9 +947,11 @@
"headings": {
"lobby": "大廳({{count}} 人)",
"participantsList": "會議與會者({{count}} 人)",
"viewerRequests": "觀眾請求({{count}}人)",
"visitorInQueue": "{{count}} 人等候中)",
"visitorRequests": "{{count}} 人申請",
"visitors": "訪客({{count}} 人)",
"visitorsList": "觀眾({{count}}人)",
"waitingLobby": "於大廳等候({{count}} 人)"
},
"search": "搜尋與會者",
@@ -929,6 +972,9 @@
"by": "由 {{ name }}",
"closeButton": "結束投票",
"create": {
"accessibilityLabel": {
"send": "傳送投票"
},
"addOption": "新增選項",
"answerPlaceholder": "選項 {{index}}",
"cancel": "取消",
@@ -1345,6 +1391,20 @@
"videounmute": "啟用網路攝影機"
},
"addPeople": "新增人員到您的通話中",
"advancedAudioSettings": {
"aec": {
"label": "回聲消除"
},
"agc": {
"label": "自動增益控制"
},
"ns": {
"label": "降噪"
},
"stereo": {
"label": "立體聲"
}
},
"audioOnlyOff": "停用低頻寬模式",
"audioOnlyOn": "啟用低頻寬模式",
"audioRoute": "選擇音訊裝置",
@@ -1416,6 +1476,7 @@
"reactionHeart": "傳送愛心反應",
"reactionLaugh": "傳送大笑反應",
"reactionLike": "傳送比讚反應",
"reactionLove": "傳送愛心",
"reactionSilence": "傳送沉默反應",
"reactionSurprised": "傳送驚訝反應",
"reactions": "反應",
@@ -1501,6 +1562,8 @@
"connectionInfo": "連線資訊",
"demote": "轉為訪客",
"domute": "靜音",
"domuteDesktop": "停止螢幕分享",
"domuteDesktopOfOthers": "停止螢幕分享給其他人",
"domuteOthers": "靜音其他人",
"domuteVideo": "停用網路攝影機",
"domuteVideoOfOthers": "停用其他人的網路攝影機",
@@ -1565,6 +1628,8 @@
"noMainParticipantsTitle": "會議尚未開始",
"noVisitorLobby": "此會議啟用大廳,暫時無法加入",
"notAllowedPromotion": "需由與會者同意您的申請",
"requestToJoin": "舉手請求",
"requestToJoinDescription": "您的請求已傳送給主持人,請稍候!",
"title": "您是會議中的訪客"
},
"waitingMessage": "會議開始後您將自動加入!"

View File

@@ -126,8 +126,16 @@
"messagebox": "Type a message",
"newMessages": "New messages",
"nickname": {
"featureChat": "chat",
"featureClosedCaptions": "closed captions",
"featureFileSharing": "file sharing",
"featurePolls": "polls",
"popover": "Choose a nickname",
"title": "Enter a nickname to use chat",
"titleWith1Features": "Enter a nickname to use {{feature1}}",
"titleWith2Features": "Enter a nickname to use {{feature1}} and {{feature2}}",
"titleWith3Features": "Enter a nickname to use {{feature1}}, {{feature2}} and {{feature3}}",
"titleWith4Features": "Enter a nickname to use {{feature1}}, {{feature2}}, {{feature3}} and {{feature4}}",
"titleWithCC": "Enter a nickname to use chat and closed captions",
"titleWithPolls": "Enter a nickname to use chat and polls",
"titleWithPollsAndCC": "Enter a nickname to use chat, polls and closed captions",
@@ -1182,7 +1190,7 @@
"signedIn": "Currently accessing calendar events for {{email}}. Click the Disconnect button below to stop accessing calendar events.",
"title": "Calendar"
},
"chatWithPermissions": "Chat requires permission",
"chatWithPermissions": "Disable chat for non-moderators",
"desktopShareFramerate": "Desktop sharing frame rate",
"desktopShareHighFpsWarning": "A higher frame rate for desktop sharing might affect your bandwidth. You need to restart the screen share for the new settings to take effect.",
"desktopShareWarning": "You need to restart the screen share for the new settings to take effect.",
@@ -1350,7 +1358,7 @@
"muteEveryoneElsesVideoStream": "Stop everyone else's video",
"muteEveryonesVideoStream": "Stop everyone's video",
"muteGUMPending": "Connecting your microphone",
"noiseSuppression": "Extra noise suppression (BETA)",
"noiseSuppression": "Extra noise suppression",
"openChat": "Open chat",
"participants": "Open participants panel. {{participantsCount}} participants",
"pip": "Toggle Picture-in-Picture mode",
@@ -1415,20 +1423,21 @@
"closeParticipantsPane": "Close participants pane",
"closeReactionsMenu": "Close reactions menu",
"closedCaptions": "Closed captions",
"disableNoiseSuppression": "Disable extra noise suppression (BETA)",
"disableNoiseSuppression": "Disable extra noise suppression",
"disableReactionSounds": "You can disable reaction sounds for this meeting",
"documentClose": "Close shared document",
"documentOpen": "Open shared document",
"download": "Download our apps",
"e2ee": "End-to-End Encryption",
"embedMeeting": "Embed meeting",
"enableNoiseSuppression": "Enable extra noise suppression (BETA)",
"enableNoiseSuppression": "Enable extra noise suppression",
"endConference": "End meeting for all",
"enterFullScreen": "View full screen",
"enterTileView": "Enter tile view",
"exitFullScreen": "Exit full screen",
"exitTileView": "Exit tile view",
"feedback": "Leave feedback",
"fileSharing": "File sharing",
"giphy": "Toggle GIPHY menu",
"hangup": "Leave the meeting",
"help": "Help",
@@ -1457,13 +1466,14 @@
"noAudioSignalDialInDesc": "You can also dial-in using:",
"noAudioSignalDialInLinkDesc": "Dial-in numbers",
"noAudioSignalTitle": "There is no input coming from your mic!",
"noiseSuppression": "Extra noise suppression (BETA)",
"noiseSuppression": "Extra noise suppression",
"noisyAudioInputDesc": "It sounds like your microphone is making noise, please consider muting or changing the device.",
"noisyAudioInputTitle": "Your microphone appears to be noisy!",
"openChat": "Open chat",
"openReactionsMenu": "Open reactions menu",
"participants": "Participants",
"pip": "Enter Picture-in-Picture mode",
"polls": "Polls",
"privateMessage": "Send private message",
"profile": "Edit your profile",
"raiseHand": "Raise your hand",

View File

@@ -107,9 +107,10 @@ import {
open as openParticipantsPane
} from '../../react/features/participants-pane/actions';
import { getParticipantsPaneOpen } from '../../react/features/participants-pane/functions';
import { hidePiP, showPiP } from '../../react/features/pip/actions';
import { startLocalVideoRecording, stopLocalVideoRecording } from '../../react/features/recording/actions.any';
import { grantRecordingConsent, grantRecordingConsentAndUnmute } from '../../react/features/recording/actions.web';
import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../react/features/recording/constants';
import { RECORDING_TYPES } from '../../react/features/recording/constants';
import { getActiveSession, supportsLocalRecording } from '../../react/features/recording/functions';
import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/features/screen-share/actions';
import { isScreenAudioSupported } from '../../react/features/screen-share/functions';
@@ -125,7 +126,7 @@ import { extractYoutubeIdOrURL } from '../../react/features/shared-video/functio
import { setRequestingSubtitles, toggleRequestingSubtitles } from '../../react/features/subtitles/actions';
import { isAudioMuteButtonDisabled } from '../../react/features/toolbox/functions';
import { setTileView, toggleTileView } from '../../react/features/video-layout/actions.any';
import { muteAllParticipants } from '../../react/features/video-menu/actions';
import { muteAllParticipants, muteRemote } from '../../react/features/video-menu/actions';
import { setVideoQuality } from '../../react/features/video-quality/actions';
import { toggleBackgroundEffect, toggleBlurredBackgroundEffect } from '../../react/features/virtual-background/actions';
import { VIRTUAL_BACKGROUND_TYPE } from '../../react/features/virtual-background/constants';
@@ -238,6 +239,17 @@ function initCommands() {
APP.store.dispatch(muteAllParticipants(exclude, muteMediaType));
},
'mute-remote-participant': (participantId, mediaType) => {
if (!isLocalParticipantModerator(APP.store.getState())) {
logger.error('Missing moderator rights to mute remote participant');
return;
}
const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
APP.store.dispatch(muteRemote(participantId, muteMediaType));
},
'toggle-lobby': isLobbyEnabled => {
APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));
},
@@ -340,6 +352,7 @@ function initCommands() {
APP.store.dispatch(setAssumedBandwidthBps(value));
},
'set-blurred-background': blurType => {
const tracks = APP.store.getState()['features/base/tracks'];
const videoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
@@ -777,10 +790,7 @@ function initCommands() {
}
if (transcription) {
APP.store.dispatch(setRequestingSubtitles(true, false, null, true));
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: true
});
APP.store.dispatch(setRequestingSubtitles(true, false, null));
}
},
@@ -803,9 +813,6 @@ function initCommands() {
if (transcription) {
APP.store.dispatch(setRequestingSubtitles(false, false, null));
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: false
});
}
if (mode === 'local') {
@@ -906,6 +913,12 @@ function initCommands() {
backgroundType: VIRTUAL_BACKGROUND_TYPE.IMAGE,
virtualSource: backgroundImage
}, jitsiTrack));
},
'show-pip': () => {
APP.store.dispatch(showPiP());
},
'hide-pip': () => {
APP.store.dispatch(hidePiP());
}
};
transport.on('event', ({ data, name }) => {
@@ -1241,6 +1254,20 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that the in-page toolbox
* visibility changed.
*
* @param {boolean} visible - True if the toolbox is visible, false otherwise.
* @returns {void}
*/
notifyToolbarVisibilityChanged(visible) {
this._sendEvent({
name: 'toolbar-visibility-changed',
visible
});
}
/**
* Notifies the external application (spot) that the local jitsi-participant
* has a status update.
@@ -1385,6 +1412,25 @@ class API {
});
}
/**
* Notify the external application that a participant's mute status changed.
*
* @param {string} participantId - The ID of the participant.
* @param {boolean} isMuted - True if muted, false if unmuted.
* @param {string} mediaType - Media type that was muted ('audio', 'video', or 'desktop').
* @param {boolean} isSelfMuted - True if participant muted themselves, false if muted by moderator.
* @returns {void}
*/
notifyParticipantMuted(participantId, isMuted, mediaType, isSelfMuted = true) {
this._sendEvent({
name: 'participant-muted',
id: participantId,
isMuted,
mediaType,
isSelfMuted
});
}
/**
* Notify the external app that a notification has been triggered.
*
@@ -2234,6 +2280,40 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that Picture-in-Picture was requested.
* Used by Electron to handle PiP requests with proper user gesture context.
*
* @returns {void}
*/
notifyPictureInPictureRequested() {
this._sendEvent({
name: '_pip-requested'
});
}
/**
* Notify external application (if API is enabled) that Picture-in-Picture mode was entered.
*
* @returns {void}
*/
notifyPictureInPictureEntered() {
this._sendEvent({
name: 'pip-entered'
});
}
/**
* Notify external application (if API is enabled) that Picture-in-Picture mode was exited.
*
* @returns {void}
*/
notifyPictureInPictureLeft() {
this._sendEvent({
name: 'pip-left'
});
}
/**
* Notify external application ( if API is enabled) that a participant menu button was clicked.
*

View File

@@ -2,6 +2,7 @@ import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
import EventEmitter from 'events';
import { urlObjectToString } from '../../../react/features/base/util/uri';
import { isPiPEnabled } from '../../../react/features/pip/external-api.shared';
import {
PostMessageTransportBackend,
Transport
@@ -46,6 +47,7 @@ const commands = {
localSubject: 'local-subject',
kickParticipant: 'kick-participant',
muteEveryone: 'mute-everyone',
muteRemoteParticipant: 'mute-remote-participant',
overwriteConfig: 'overwrite-config',
overwriteNames: 'overwrite-names',
password: 'password',
@@ -94,7 +96,9 @@ const commands = {
toggleTileView: 'toggle-tile-view',
toggleVirtualBackgroundDialog: 'toggle-virtual-background',
toggleVideo: 'toggle-video',
toggleWhiteboard: 'toggle-whiteboard'
toggleWhiteboard: 'toggle-whiteboard',
showPiP: 'show-pip',
hidePiP: 'hide-pip'
};
/**
@@ -102,6 +106,9 @@ const commands = {
* events expected by jitsi-meet.
*/
const events = {
'_pip-requested': '_pipRequested',
'pip-entered': 'pipEntered',
'pip-left': 'pipLeft',
'avatar-changed': 'avatarChanged',
'audio-availability-changed': 'audioAvailabilityChanged',
'audio-mute-status-changed': 'audioMuteStatusChanged',
@@ -144,6 +151,7 @@ const events = {
'participant-joined': 'participantJoined',
'participant-kicked-out': 'participantKickedOut',
'participant-left': 'participantLeft',
'participant-muted': 'participantMuted',
'participant-role-changed': 'participantRoleChanged',
'participants-pane-toggled': 'participantsPaneToggled',
'password-required': 'passwordRequired',
@@ -167,6 +175,7 @@ const events = {
'suspend-detected': 'suspendDetected',
'tile-view-changed': 'tileViewChanged',
'toolbar-button-clicked': 'toolbarButtonClicked',
'toolbar-visibility-changed': 'toolbarVisibilityChanged',
'transcribing-status-changed': 'transcribingStatusChanged',
'transcription-chunk-received': 'transcriptionChunkReceived',
'whiteboard-status-changed': 'whiteboardStatusChanged'
@@ -329,6 +338,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
this._myUserID = undefined;
this._onStageParticipant = undefined;
this._iAmvisitor = undefined;
this._pipConfig = configOverwrite?.pip;
this._setupListeners();
id++;
}
@@ -648,6 +658,56 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
this.emit(requestName, data, callback);
}
});
this._setupIntersectionObserver();
}
/**
* Sets up IntersectionObserver to monitor iframe visibility.
* Calls showPiP/hidePiP based on visibility.
*
* @private
* @returns {void}
*/
_setupIntersectionObserver() {
if (!isPiPEnabled(this._pipConfig)) {
return;
}
// Don't create duplicate observers.
if (this._intersectionObserver) {
return;
}
this._isIntersecting = true;
this._intersectionObserver = new IntersectionObserver(entries => {
const entry = entries[entries.length - 1];
const wasIntersecting = this._isIntersecting;
this._isIntersecting = entry.isIntersecting;
if (!entry.isIntersecting && wasIntersecting) {
this.showPiP();
} else if (entry.isIntersecting && !wasIntersecting) {
this.hidePiP();
}
});
this._intersectionObserver.observe(this._frame);
}
/**
* Tears down IntersectionObserver.
*
* @private
* @returns {void}
*/
_teardownIntersectionObserver() {
if (this._intersectionObserver) {
this._intersectionObserver.disconnect();
this._intersectionObserver = null;
}
}
/**
@@ -850,6 +910,8 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
this.emit('_willDispose');
this._transport.dispose();
this.removeAllListeners();
this._teardownIntersectionObserver();
if (this._frame && this._frame.parentNode) {
this._frame.parentNode.removeChild(this._frame);
}
@@ -878,10 +940,47 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return;
}
// Handle pip config changes locally.
// We update local state, send command to iframe, then handle PiP show/hide
// so the iframe config is updated before we try to show PiP.
let pipTransition = null;
if (name === 'overwriteConfig' && args[0]?.pip !== undefined) {
const wasEnabled = isPiPEnabled(this._pipConfig);
this._pipConfig = {
...this._pipConfig,
...args[0].pip
};
const isEnabled = isPiPEnabled(this._pipConfig);
if (!wasEnabled && isEnabled) {
this._setupIntersectionObserver();
pipTransition = 'enabled';
} else if (wasEnabled && !isEnabled) {
this._teardownIntersectionObserver();
pipTransition = 'disabled';
}
}
// Send command to iframe first.
this._transport.sendEvent({
data: args,
name: commands[name]
});
// Handle PiP state after command is sent so iframe config is updated.
if (pipTransition === 'enabled') {
// Show PiP if iframe is currently not visible.
if (!this._isIntersecting) {
this.showPiP();
}
} else if (pipTransition === 'disabled') {
// Hide any open PiP window.
this.hidePiP();
}
}
/**
@@ -1495,6 +1594,24 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
this.executeCommand('setVirtualBackground', enabled, backgroundImage);
}
/**
* Shows Picture-in-Picture window.
*
* @returns {void}
*/
showPiP() {
this.executeCommand('showPiP');
}
/**
* Hides Picture-in-Picture window.
*
* @returns {void}
*/
hidePiP() {
this.executeCommand('hidePiP');
}
/**
* Opens the desktop picker. This is invoked by the Electron SDK when gDM is used.
*

View File

@@ -158,11 +158,10 @@ const VideoLayout = {
return;
}
const state = APP.store.getState();
const currentContainer = largeVideo.getCurrentContainer();
const currentContainerType = largeVideo.getCurrentContainerType();
const isOnLarge = this.isCurrentlyOnLarge(id);
const state = APP.store.getState();
const participant = getParticipantById(state, id);
const videoTrack = getVideoTrackByParticipant(state, participant);
const videoStream = videoTrack?.jitsiTrack;

3290
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,7 @@
"@jitsi/js-utils": "2.6.7",
"@jitsi/logger": "2.1.1",
"@jitsi/rnnoise-wasm": "0.2.1",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@matrix-org/olm": "3.2.15",
"@microsoft/microsoft-graph-client": "3.0.1",
"@mui/material": "5.12.1",
"@react-native-async-storage/async-storage": "1.23.1",
@@ -72,7 +72,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -164,12 +164,12 @@
"@types/w3c-image-capture": "1.0.6",
"@types/w3c-web-hid": "1.0.3",
"@types/zxcvbn": "4.4.1",
"@wdio/allure-reporter": "9.16.0",
"@wdio/cli": "9.16.0",
"@wdio/globals": "9.16.0",
"@wdio/junit-reporter": "9.16.0",
"@wdio/local-runner": "9.16.0",
"@wdio/mocha-framework": "9.16.0",
"@wdio/allure-reporter": "9.22.0",
"@wdio/cli": "9.22.0",
"@wdio/globals": "9.17.0",
"@wdio/junit-reporter": "9.21.0",
"@wdio/local-runner": "9.22.0",
"@wdio/mocha-framework": "9.22.0",
"babel-loader": "9.1.0",
"babel-plugin-optional-require": "0.3.1",
"circular-dependency-plugin": "5.2.0",
@@ -193,7 +193,7 @@
"ts-loader": "9.4.2",
"typescript": "5.7.2",
"unorm": "1.6.0",
"webdriverio": "9.16.0",
"webdriverio": "9.22.0",
"webpack": "5.95.0",
"webpack-bundle-analyzer": "4.4.2",
"webpack-cli": "5.1.4",

View File

@@ -600,31 +600,6 @@ export function createRemoteVideoMenuButtonEvent(buttonName: string, attributes
};
}
/**
* The rtcstats websocket onclose event. We send this to amplitude in order
* to detect trace ws prematurely closing.
*
* @param {Object} closeEvent - The event with which the websocket closed.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createRTCStatsTraceCloseEvent(closeEvent: { code: string; reason: string; }) {
const event: {
action: string;
code?: string;
reason?: string;
source: string;
} = {
action: 'trace.onclose',
source: 'rtcstats'
};
event.code = closeEvent.code;
event.reason = closeEvent.reason;
return event;
}
/**
* Creates an event indicating that an action related to screen sharing
* occurred (e.g. It was started or stopped).

View File

@@ -5,6 +5,7 @@ import JitsiThemeProvider from '../../base/ui/components/JitsiThemeProvider.web'
import DialogContainer from '../../base/ui/components/web/DialogContainer';
import ChromeExtensionBanner from '../../chrome-extension-banner/components/ChromeExtensionBanner.web';
import OverlayContainer from '../../overlay/components/web/OverlayContainer';
import PiP from '../../pip/components/PiP';
import { AbstractApp } from './AbstractApp';
@@ -47,6 +48,7 @@ export class App extends AbstractApp {
<JitsiThemeProvider>
<GlobalStyles />
<ChromeExtensionBanner />
<PiP />
{ super._createMainElement(component, props) }
</JitsiThemeProvider>
);

View File

@@ -2,7 +2,6 @@ import '../analytics/middleware';
import '../authentication/middleware';
import '../av-moderation/middleware';
import '../base/conference/middleware';
import '../base/config/middleware';
import '../base/i18n/middleware';
import '../base/jwt/middleware';
import '../base/known-domains/middleware';

View File

@@ -1,3 +1,4 @@
import '../base/config/middleware';
import '../dynamic-branding/middleware';
import '../gifs/middleware';
import '../mobile/audio-mode/middleware';

View File

@@ -1,4 +1,5 @@
import '../base/app/middleware';
import '../base/config/middleware';
import '../base/connection/middleware';
import '../base/devices/middleware';
import '../base/media/middleware';
@@ -11,6 +12,7 @@ import '../no-audio-signal/middleware';
import '../notifications/middleware';
import '../noise-detection/middleware';
import '../old-client-notification/middleware';
import '../pip/middleware';
import '../power-monitor/middleware';
import '../prejoin/middleware';
import '../remote-control/middleware';

View File

@@ -8,6 +8,7 @@ import '../keyboard-shortcuts/reducer';
import '../no-audio-signal/reducer';
import '../noise-detection/reducer';
import '../participants-pane/reducer';
import '../pip/reducer';
import '../power-monitor/reducer';
import '../prejoin/reducer';
import '../remote-control/reducer';

View File

@@ -57,6 +57,7 @@ import { INoiseDetectionState } from '../noise-detection/reducer';
import { INoiseSuppressionState } from '../noise-suppression/reducer';
import { INotificationsState } from '../notifications/reducer';
import { IParticipantsPaneState } from '../participants-pane/reducer';
import { IPipState } from '../pip/reducer';
import { IPollsState } from '../polls/reducer';
import { IPollsHistoryState } from '../polls-history/reducer';
import { IPowerMonitorState } from '../power-monitor/reducer';
@@ -145,6 +146,7 @@ export interface IReduxState {
'features/noise-suppression': INoiseSuppressionState;
'features/notifications': INotificationsState;
'features/participants-pane': IParticipantsPaneState;
'features/pip': IPipState;
'features/polls': IPollsState;
'features/polls-history': IPollsHistoryState;
'features/power-monitor': IPowerMonitorState;

View File

@@ -139,7 +139,7 @@ function _upgradeRoleStarted(thenableWithCancel: Object) {
* @returns {Function}
*/
export function hideLoginDialog() {
return hideDialog(LoginDialog);
return hideDialog('LoginDialog', LoginDialog);
}
/**
@@ -199,7 +199,7 @@ export function enableModeratorLogin() {
* @returns {Action}
*/
export function openWaitForOwnerDialog() {
return openDialog(WaitForOwnerDialog);
return openDialog('WaitForOwnerDialog', WaitForOwnerDialog);
}
@@ -240,7 +240,7 @@ export function waitForOwner() {
* @returns {Action}
*/
export function openLoginDialog() {
return openDialog(LoginDialog);
return openDialog('LoginDialog', LoginDialog);
}
/**

View File

@@ -65,7 +65,7 @@ export function openTokenAuthUrl(tokenAuthServiceUrl: string): any {
// Show warning for leaving conference only when in a conference.
if (!browser.isElectron() && getState()['features/base/conference'].conference) {
dispatch(openDialog(LoginQuestionDialog, {
dispatch(openDialog('LoginQuestionDialog', LoginQuestionDialog, {
handler: () => {
// Give time for the dialog to close.
setTimeout(() => redirect(), 500);

View File

@@ -209,7 +209,7 @@ MiddlewareRegistry.register(store => next => action => {
case STOP_WAIT_FOR_OWNER:
_clearExistingWaitForOwnerTimeout(store);
store.dispatch(hideDialog(WaitForOwnerDialog));
store.dispatch(hideDialog('WaitForOwnerDialog', WaitForOwnerDialog));
break;
case UPGRADE_ROLE_FINISHED: {

View File

@@ -5,7 +5,18 @@ import Icon from '../../../icons/components/Icon';
import { pixelsToRem } from '../../../ui/functions.any';
import { isIcon } from '../../functions';
import { IAvatarProps } from '../../types';
import { PRESENCE_AVAILABLE_COLOR, PRESENCE_AWAY_COLOR, PRESENCE_BUSY_COLOR, PRESENCE_IDLE_COLOR } from '../styles';
import {
PRESENCE_AVAILABLE_COLOR,
PRESENCE_AWAY_COLOR,
PRESENCE_BUSY_COLOR,
PRESENCE_IDLE_COLOR
} from '../styles';
import {
AVATAR_DEFAULT_BACKGROUND_COLOR,
getAvatarFont,
getAvatarInitialsColor
} from './styles';
interface IProps extends IAvatarProps {
@@ -48,10 +59,10 @@ interface IProps extends IAvatarProps {
const useStyles = makeStyles()(theme => {
return {
avatar: {
backgroundColor: '#AAA',
backgroundColor: AVATAR_DEFAULT_BACKGROUND_COLOR,
borderRadius: '50%',
color: theme.palette?.text01 || '#fff',
...(theme.typography?.heading1 ?? {}),
color: getAvatarInitialsColor(theme),
...getAvatarFont(theme),
fontSize: 'inherit',
objectFit: 'cover',
textAlign: 'center',

View File

@@ -0,0 +1,26 @@
import { Theme } from '@mui/material/styles';
// Default avatar background color
export const AVATAR_DEFAULT_BACKGROUND_COLOR = '#AAA';
/**
* Returns the avatar font style from the theme.
*
* @param {Theme} theme - The MUI theme.
* @returns {Object} The font style object containing fontFamily, fontWeight, etc.
*/
export const getAvatarFont = (theme: Theme) => theme.typography?.heading1 ?? {};
/**
* Default text color for avatar initials.
*/
export const AVATAR_DEFAULT_INITIALS_COLOR = '#FFFFFF';
/**
* Returns the text color for avatar initials from the theme.
*
* @param {Theme} theme - The MUI theme.
* @returns {string} The text color.
*/
export const getAvatarInitialsColor = (theme: Theme): string =>
theme.palette?.text01 || AVATAR_DEFAULT_INITIALS_COLOR;

View File

@@ -55,6 +55,28 @@ function getFirstGraphemeUpper(word: string) {
return splitter.splitGraphemes(word)[0].toUpperCase();
}
/**
* Strips bracketed annotations from a display name. Handles multiple bracket types like (),
* [], and {}.
*
* @param {string} name - The display name to clean.
* @returns {string} The cleaned display name without bracketed annotations.
*/
function stripBracketedAnnotations(name: string): string {
// Match content within any of the bracket types at the end of the string
// This regex matches: (...) or [...] or {...} at the end
const bracketRegex = /\s*[([{][^)\]}]*[)\]}]$/;
let cleaned = name;
// Remove all trailing bracketed annotations (handle multiple occurrences)
while (bracketRegex.test(cleaned)) {
cleaned = cleaned.replace(bracketRegex, '');
}
return cleaned.trim();
}
/**
* Generates initials for a simple string.
*
@@ -64,7 +86,15 @@ function getFirstGraphemeUpper(word: string) {
export function getInitials(s?: string) {
// We don't want to use the domain part of an email address, if it is one
const initialsBasis = split(s, '@')[0];
const [ firstWord, ...remainingWords ] = initialsBasis.split(wordSplitRegex).filter(Boolean);
// Strip bracketed annotations (e.g., "(Department)", "[Team]", "{Org}")
// to prevent them from being considered as name parts
const cleanedName = stripBracketedAnnotations(initialsBasis);
// Fallback to original if cleaned name is empty
const nameForInitials = cleanedName || initialsBasis;
const [ firstWord, ...remainingWords ] = nameForInitials.split(wordSplitRegex).filter(Boolean);
return getFirstGraphemeUpper(firstWord) + getFirstGraphemeUpper(remainingWords.pop() || '');
}

View File

@@ -285,6 +285,7 @@ export interface IConfig {
disableAudioLevels?: boolean;
disableBeforeUnloadHandlers?: boolean;
disableCameraTintForeground?: boolean;
disableChat?: boolean;
disableChatSmileys?: boolean;
disableDeepLinking?: boolean;
disableFilmstripAutohiding?: boolean;
@@ -393,6 +394,7 @@ export interface IConfig {
disabled?: boolean;
initialWidth?: number;
minParticipantCountForTopPanel?: number;
stageFilmstripParticipants?: number;
};
flags?: {
ssrcRewritingEnabled: boolean;
@@ -515,6 +517,10 @@ export interface IConfig {
peopleSearchQueryTypes?: string[];
peopleSearchTokenLocation?: string;
peopleSearchUrl?: string;
pip?: {
disabled?: boolean;
showOnPrejoin?: boolean;
};
preferBosh?: boolean;
preferVisitor?: boolean;
preferredTranscribeLanguage?: string;
@@ -553,6 +559,7 @@ export interface IConfig {
skipConsentInMeeting?: boolean;
suggestRecording?: boolean;
};
reducedUImainToolbarButtons?: Array<string>;
remoteVideoMenu?: {
disableDemote?: boolean;
disableGrantModerator?: boolean;
@@ -575,6 +582,7 @@ export interface IConfig {
};
serviceUrl?: string;
sharedVideoAllowedURLDomains?: Array<string>;
showChatPermissionsModeratorSetting?: boolean;
sipInviteUrl?: string;
speakerStats?: {
disableSearch?: boolean;
@@ -616,6 +624,10 @@ export interface IConfig {
toolbarConfig?: {
alwaysVisible?: boolean;
autoHideWhileChatIsOpen?: boolean;
/**
* Background color for the main toolbar. Accepts any valid CSS color.
*/
backgroundColor?: string;
initialTimeout?: number;
timeout?: number;
};
@@ -626,7 +638,6 @@ export interface IConfig {
autoTranscribeOnRecord?: boolean;
disableClosedCaptions?: boolean;
enabled?: boolean;
inviteJigasiOnBackendTranscribing?: boolean;
preferredLanguage?: string;
translationLanguages?: Array<string>;
translationLanguagesHead?: Array<string>;

View File

@@ -94,6 +94,7 @@ export default [
'disableAudioLevels',
'disableBeforeUnloadHandlers',
'disableCameraTintForeground',
'disableChat',
'disableChatSmileys',
'disableDeepLinking',
'disabledNotifications',
@@ -198,6 +199,7 @@ export default [
'participantMenuButtonsWithNotifyClick',
'participantsPane',
'pcStatsInterval',
'pip',
'preferBosh',
'preferVisitor',
'prejoinConfig.enabled',
@@ -213,10 +215,12 @@ export default [
'recordings.showPrejoinWarning',
'recordings.showRecordingLink',
'recordings.suggestRecording',
'reducedUImainToolbarButtons',
'replaceParticipant',
'resolution',
'screenshotCapture',
'securityUi',
'showChatPermissionsModeratorSetting',
'speakerStats',
'startAudioMuted',
'startAudioOnly',

View File

@@ -2,7 +2,6 @@ import { AnyAction } from 'redux';
import { IStore } from '../../app/types';
import { SET_DYNAMIC_BRANDING_DATA } from '../../dynamic-branding/actionTypes';
import { setUserFilmstripWidth } from '../../filmstrip/actions.web';
import { getFeatureFlag } from '../flags/functions';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { updateSettings } from '../settings/actions';
@@ -80,29 +79,8 @@ function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyA
}));
}
const { initialWidth, stageFilmstripParticipants } = action.config.filmstrip || {};
if (stageFilmstripParticipants !== undefined) {
dispatch(updateSettings({
maxStageParticipants: stageFilmstripParticipants
}));
}
if (initialWidth) {
dispatch(setUserFilmstripWidth(initialWidth));
}
dispatch(updateConfig(config));
// FIXME On Web we rely on the global 'config' variable which gets altered
// multiple times, before it makes it to the reducer. At some point it may
// not be the global variable which is being modified anymore due to
// different merge methods being used along the way. The global variable
// must be synchronized with the final state resolved by the reducer.
if (typeof window.config !== 'undefined') {
window.config = state['features/base/config'];
}
return result;
}

View File

@@ -0,0 +1 @@
import './middleware.any';

View File

@@ -0,0 +1,48 @@
import { setUserFilmstripWidth } from '../../filmstrip/actions.web';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { updateSettings } from '../settings/actions';
import { SET_CONFIG } from './actionTypes';
import './middleware.any';
/**
* The middleware of the feature {@code base/config}.
*
* @param {Store} store - The redux store.
* @private
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case SET_CONFIG: {
const { initialWidth, stageFilmstripParticipants } = action.config.filmstrip || {};
const { dispatch, getState } = store;
const state = getState();
if (stageFilmstripParticipants !== undefined) {
dispatch(updateSettings({
maxStageParticipants: stageFilmstripParticipants
}));
}
if (initialWidth) {
dispatch(setUserFilmstripWidth(initialWidth));
}
// FIXME On Web we rely on the global 'config' variable which gets altered
// multiple times, before it makes it to the reducer. At some point it may
// not be the global variable which is being modified anymore due to
// different merge methods being used along the way. The global variable
// must be synchronized with the final state resolved by the reducer.
if (typeof window.config !== 'undefined') {
window.config = state['features/base/config'];
}
break;
}
}
return result;
});

View File

@@ -14,6 +14,7 @@ import logger from './logger';
/**
* Signals Dialog to close its dialog.
*
* @param {string|undefined} name - The name of the component for logging purposes.
* @param {Object} [component] - The {@code Dialog} component to close/hide. If
* {@code undefined}, closes/hides {@code Dialog} regardless of which
* component it's rendering; otherwise, closes/hides {@code Dialog} only if
@@ -23,8 +24,8 @@ import logger from './logger';
* component: (React.Component | undefined)
* }}
*/
export function hideDialog(component?: ComponentType<any>) {
logger.info(`Hide dialog: ${getComponentDisplayName(component)}`);
export function hideDialog(name?: string, component?: ComponentType<any>) {
logger.info(`Hide dialog: ${name}`);
return {
type: HIDE_DIALOG,
@@ -48,6 +49,7 @@ export function hideSheet() {
/**
* Signals Dialog to open dialog.
*
* @param {string} name - The name of the component for logging purposes.
* @param {Object} component - The component to display as dialog.
* @param {Object} [componentProps] - The React {@code Component} props of the
* specified {@code component}.
@@ -57,8 +59,8 @@ export function hideSheet() {
* componentProps: (Object | undefined)
* }}
*/
export function openDialog(component: ComponentType<any>, componentProps?: Object) {
logger.info(`Open dialog: ${getComponentDisplayName(component)}`);
export function openDialog(name: string, component: ComponentType<any>, componentProps?: Object) {
logger.info(`Open dialog: ${name}`);
return {
type: OPEN_DIALOG,
@@ -92,35 +94,18 @@ export function openSheet(component: ComponentType<any>, componentProps?: Object
* is not already open. If it is open, then Dialog is signaled to close its
* dialog.
*
* @param {string} name - The name of the component for logging purposes.
* @param {Object} component - The component to display as dialog.
* @param {Object} [componentProps] - The React {@code Component} props of the
* specified {@code component}.
* @returns {Function}
*/
export function toggleDialog(component: ComponentType<any>, componentProps?: Object) {
export function toggleDialog(name: string, component: ComponentType<any>, componentProps?: Object) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
if (isDialogOpen(getState, component)) {
dispatch(hideDialog(component));
dispatch(hideDialog(name, component));
} else {
dispatch(openDialog(component, componentProps));
dispatch(openDialog(name, component, componentProps));
}
};
}
/**
* Extracts a printable name for a dialog component.
*
* @param {Object} component - The component to extract the name for.
*
* @returns {string} The display name.
*/
function getComponentDisplayName(component?: ComponentType<any>) {
if (!component) {
return '';
}
const name = component.displayName ?? component.name ?? 'Component';
return name.replace('withI18nextTranslation(Connect(', '') // dialogs with translations
.replace('))', ''); // dialogs with translations suffix
}

View File

@@ -1,5 +1,6 @@
import React, { PureComponent, ReactNode } from 'react';
import { SafeAreaView, ScrollView, View, ViewStyle } from 'react-native';
import { ScrollView, View, ViewStyle } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { connect } from 'react-redux';
import { IStore } from '../../../../app/types';
@@ -122,6 +123,7 @@ class BottomSheet extends PureComponent<Props> {
style = { styles.sheetAreaCover } />
{ renderHeader?.() }
<SafeAreaView
edges = { [ 'left', 'right' ] }
style = { [
styles.sheetItemContainer,
renderHeader

View File

@@ -8,13 +8,14 @@ import i18next from './i18next';
*
* @param {string} language - The language e.g. 'en', 'fr'.
* @param {string} url - The url of the translation bundle.
* @param {string} ns - The namespace of the translation bundle.
* @returns {void}
*/
export async function changeLanguageBundle(language: string, url: string) {
export async function changeLanguageBundle(language: string, url: string, ns = 'main') {
const res = await fetch(url);
const bundle = await res.json();
i18next.addResourceBundle(language, 'main', bundle, true, true);
i18next.addResourceBundle(language, ns, bundle, true, true);
}
/**

View File

@@ -58,6 +58,14 @@ export const DEFAULT_LANGUAGE = 'en';
*/
export const TRANSLATION_LANGUAGES_HEAD: Array<string> = [ DEFAULT_LANGUAGE ];
/**
* The available/supported i18n namespaces.
*
* @public
* @type {Array<string>}
*/
export const SUPPORTED_NS = [ 'main', 'languages', 'countries', 'translation-languages' ];
/**
* The options to initialize i18next with.
*
@@ -80,8 +88,8 @@ const options: i18next.InitOptions = {
interpolation: {
escapeValue: false // not needed for react as it escapes by default
},
load: 'languageOnly',
ns: [ 'main', 'languages', 'countries', 'translation-languages' ],
load: 'all',
ns: SUPPORTED_NS,
react: {
// re-render when a new resource bundle is added
// @ts-expect-error. Fixed in i18next 19.6.1.

View File

@@ -4,7 +4,7 @@ import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { I18NEXT_INITIALIZED, LANGUAGE_CHANGED } from './actionTypes';
import { changeLanguageBundle } from './functions';
import i18next from './i18next';
import i18next, { SUPPORTED_NS } from './i18next';
import logger from './logger';
/**
@@ -19,9 +19,10 @@ MiddlewareRegistry.register(store => next => action => {
case LANGUAGE_CHANGED:
case SET_DYNAMIC_BRANDING_DATA: {
const { language } = i18next;
const { labels } = action.type === SET_DYNAMIC_BRANDING_DATA
const data = action.type === SET_DYNAMIC_BRANDING_DATA
? action.value
: store.getState()['features/dynamic-branding'];
const labels = data?.labels;
if (language && labels?.[language]) {
changeLanguageBundle(language, labels[language])
@@ -30,6 +31,17 @@ MiddlewareRegistry.register(store => next => action => {
});
}
SUPPORTED_NS.forEach(ns => {
const nsLabels = data?.[`labels-${ns}`];
if (language && nsLabels?.[language]) {
changeLanguageBundle(language, nsLabels[language], ns)
.catch(err => {
logger.log(`Error setting dynamic language bundle for ${ns}`, err);
});
}
});
// Update transcription language, if applicable.
if (action.type === SET_DYNAMIC_BRANDING_DATA) {
const { defaultTranscriptionLanguage } = action.value;

View File

@@ -3,13 +3,14 @@ import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { safeJsonParse } from '@jitsi/js-utils/json';
import { pick } from 'lodash-es';
import { browser } from '../lib-jitsi-meet';
import { isEmbedded } from '../util/embedUtils';
import { parseURLParams } from '../util/parseURLParams';
import logger from './logger';
import WHITELIST from './whitelist';
/**
* Handles changes of the fake local storage.
@@ -61,7 +62,7 @@ function setupJitsiLocalStorage() {
if (shouldUseHostPageLocalStorage(urlParams)) {
try {
const localStorageContent = safeJsonParse(urlParams['appData.localStorageContent']);
let localStorageContent = safeJsonParse(urlParams['appData.localStorageContent']);
// We need to disable the local storage before setting the data in case the browser local storage doesn't
// throw exception (in some cases when this happens the local storage may be cleared for every session.
@@ -71,6 +72,10 @@ function setupJitsiLocalStorage() {
jitsiLocalStorage.setLocalStorageDisabled(true);
if (typeof localStorageContent === 'object') {
if (!isEmbedded()) {
localStorageContent = pick(localStorageContent, WHITELIST);
}
Object.keys(localStorageContent).forEach(key => {
jitsiLocalStorage.setItem(key, localStorageContent[key]);
});

View File

@@ -0,0 +1,11 @@
/**
* Keys of localStorage that are used by jibri.
*/
export default [
'callStatsUserName',
'displayname',
'email',
'xmpp_username_override',
'xmpp_password_override',
'xmpp_conference_password_override'
];

View File

@@ -10,6 +10,10 @@ import { parseURLParams } from '../util/parseURLParams';
import { JWT_VALIDATION_ERRORS, MEET_FEATURES } from './constants';
import logger from './logger';
/**
* Note that this is just client-side code and it intentionally does not verify the signature of the JWT.
*/
/**
* Retrieves the JSON Web Token (JWT), if any, defined by a specific
* {@link URL}.

View File

@@ -26,7 +26,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
case SET_VIDEO_MUTED: {
if (LocalRecordingManager.isRecordingLocally() && LocalRecordingManager.selfRecording.on) {
if (action.muted && LocalRecordingManager.selfRecording.withVideo) {
dispatch(openDialog(StopRecordingDialog, { localRecordingVideoStop: true }));
dispatch(openDialog('StopRecordingDialog', StopRecordingDialog, { localRecordingVideoStop: true }));
return;
} else if (!action.muted && !LocalRecordingManager.selfRecording.withVideo) {

View File

@@ -80,3 +80,8 @@ export const LOWER_HAND_AUDIO_LEVEL = 0.2;
* Icon URL for the whiteboard participant.
*/
export const WHITEBOARD_PARTICIPANT_ICON = IconWhiteboard;
/**
* The ID used for non-participant (system) messages coming from a transcriber.
*/
export const TRANSCRIBER_ID = 'transcriber';

View File

@@ -62,6 +62,65 @@ const AVATAR_CHECKER_FUNCTIONS = [
];
/* eslint-enable arrow-body-style */
/**
* Returns the list of active speakers that should be moved to the top of the sorted list of participants so that the
* dominant speaker is visible always on the vertical filmstrip in stage layout.
*
* @param {Function | Object} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
* retrieve the state.
* @returns {Array<string>}
*/
export function getActiveSpeakersToBeDisplayed(stateful: IStateful) {
const state = toState(stateful);
const {
dominantSpeaker,
fakeParticipants,
sortedRemoteVirtualScreenshareParticipants,
speakersList
} = state['features/base/participants'];
const { visibleRemoteParticipants } = state['features/filmstrip'];
let activeSpeakers = new Map(speakersList);
// Do not re-sort the active speakers if dominant speaker is currently visible.
if (dominantSpeaker && visibleRemoteParticipants.has(dominantSpeaker)) {
return activeSpeakers;
}
let availableSlotsForActiveSpeakers = visibleRemoteParticipants.size;
if (activeSpeakers.has(dominantSpeaker ?? '')) {
activeSpeakers.delete(dominantSpeaker ?? '');
}
// Add dominant speaker to the beginning of the list (not including self) since the active speaker list is always
// alphabetically sorted.
if (dominantSpeaker && dominantSpeaker !== getLocalParticipant(state)?.id) {
const updatedSpeakers = Array.from(activeSpeakers);
updatedSpeakers.splice(0, 0, [ dominantSpeaker, getParticipantById(state, dominantSpeaker)?.name ?? '' ]);
activeSpeakers = new Map(updatedSpeakers);
}
// Remove screenshares from the count.
if (sortedRemoteVirtualScreenshareParticipants) {
availableSlotsForActiveSpeakers -= sortedRemoteVirtualScreenshareParticipants.size * 2;
for (const screenshare of Array.from(sortedRemoteVirtualScreenshareParticipants.keys())) {
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare as string);
activeSpeakers.delete(ownerId);
}
}
// Remove fake participants from the count.
if (fakeParticipants) {
availableSlotsForActiveSpeakers -= fakeParticipants.size;
}
const truncatedSpeakersList = Array.from(activeSpeakers).slice(0, availableSlotsForActiveSpeakers);
truncatedSpeakersList.sort((a: any, b: any) => a[1].localeCompare(b[1]));
return new Map(truncatedSpeakersList);
}
/**
* Resolves the first loadable avatar URL for a participant.
*
@@ -759,7 +818,7 @@ export const addPeopleFeatureControl = (stateful: IStateful) => {
* @param {Function} dispatch - The Redux dispatch function.
* @returns {Function}
*/
export const setShareDialogVisiblity = (addPeopleFeatureEnabled: boolean, dispatch: IStore['dispatch']) => {
export const setShareDialogVisiblity = (addPeopleFeatureEnabled: boolean | undefined, dispatch: IStore['dispatch']) => {
if (addPeopleFeatureEnabled) {
dispatch(toggleShareDialog(false));
} else {

View File

@@ -22,17 +22,35 @@ export function preloadImage(
return new Promise((resolve, reject) => {
const image = document.createElement('img');
// Cleanup function to release resources and prevent memory leaks
const cleanup = () => {
// Clear event handlers to break circular references
image.onload = null;
image.onerror = null;
// Clear src to stop any pending load and allow GC
image.src = '';
};
if (useCORS) {
image.setAttribute('crossOrigin', '');
}
image.onload = () => resolve({
src,
isUsingCORS: useCORS
});
image.onload = () => {
cleanup();
resolve({
src,
isUsingCORS: useCORS
});
};
image.onerror = error => {
cleanup();
if (tryOnce) {
reject(error);
} else {
// Retry with different CORS mode
preloadImage(src, !useCORS, true)
.then(resolve)
.catch(reject);

View File

@@ -27,6 +27,7 @@ import {
isRemoteScreenshareParticipant,
isScreenShareParticipant
} from './functions';
import logger from './logger';
import { FakeParticipant, ILocalParticipant, IParticipant, ISourceInfo } from './types';
/**
@@ -76,12 +77,12 @@ const DEFAULT_STATE = {
numberOfParticipantsNotSupportingE2EE: 0,
overwrittenNameList: {},
pinnedParticipant: undefined,
previousSpeakers: new Set<string>(),
raisedHandsQueue: [],
remote: new Map(),
remoteVideoSources: new Set<string>(),
sortedRemoteVirtualScreenshareParticipants: new Map(),
sortedRemoteParticipants: new Map()
sortedRemoteParticipants: new Map(),
speakersList: new Map()
};
export interface IParticipantsState {
@@ -94,12 +95,12 @@ export interface IParticipantsState {
numberOfParticipantsNotSupportingE2EE: number;
overwrittenNameList: { [id: string]: string; };
pinnedParticipant?: string;
previousSpeakers: Set<string>;
raisedHandsQueue: Array<{ hasBeenNotified?: boolean; id: string; raisedHandTimestamp: number; }>;
remote: Map<string, IParticipant>;
remoteVideoSources: Set<string>;
sortedRemoteParticipants: Map<string, string>;
sortedRemoteVirtualScreenshareParticipants: Map<string, string>;
speakersList: Map<string, string>;
}
/**
@@ -156,10 +157,22 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
const { participant } = action;
const { id, previousSpeakers = [] } = participant;
const { dominantSpeaker, local } = state;
const newSpeakers = [ id, ...previousSpeakers ];
const sortedSpeakersList: Array<Array<string>> = [];
// Build chronologically ordered Set of remote speakers (excluding local)
const previousSpeakersSet: Set<string>
= new Set(previousSpeakers.filter((speaker: string) => speaker !== local?.id));
for (const speaker of newSpeakers) {
if (speaker !== local?.id) {
const remoteParticipant = state.remote.get(speaker);
remoteParticipant
&& sortedSpeakersList.push(
[ speaker, _getDisplayName(state, remoteParticipant?.name) ]
);
}
}
// Keep the remote speaker list sorted alphabetically.
sortedSpeakersList.sort((a, b) => a[1].localeCompare(b[1]));
// Only one dominant speaker is allowed.
if (dominantSpeaker) {
@@ -169,8 +182,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
return {
...state,
dominantSpeaker: id,
previousSpeakers: previousSpeakersSet
dominantSpeaker: id, // @ts-ignore
speakersList: new Map(sortedSpeakersList)
};
}
@@ -352,6 +365,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
logger.debug('Remote screenshare participant joined', id);
}
// Exclude the screenshare participant from the fake participant count to avoid duplicates.
@@ -423,7 +438,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
}
// Remove the participant from the list of speakers.
state.previousSpeakers.delete(id);
state.speakersList.has(id) && state.speakersList.delete(id);
if (pinnedParticipant === id) {
state.pinnedParticipant = undefined;
@@ -436,6 +451,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
if (sortedRemoteVirtualScreenshareParticipants.has(id)) {
sortedRemoteVirtualScreenshareParticipants.delete(id);
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
logger.debug('Remote screenshare participant left', id);
}
if (oldParticipant && !oldParticipant.fakeParticipant && !isLocalScreenShare) {

View File

@@ -21,6 +21,7 @@ import {
getRemoteScreensharesBasedOnPresence,
getVirtualScreenshareParticipantOwnerId
} from './functions';
import logger from './logger';
import { FakeParticipant } from './types';
StateListenerRegistry.register(
@@ -69,14 +70,19 @@ function _createOrRemoveVirtualParticipants(
const addedScreenshareSourceNames = difference(newScreenshareSourceNames, oldScreenshareSourceNames);
if (removedScreenshareSourceNames.length) {
removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, {
fakeParticipant: FakeParticipant.RemoteScreenShare
})));
removedScreenshareSourceNames.forEach(id => {
logger.debug('Dispatching participantLeft for virtual screenshare', id);
dispatch(participantLeft(id, conference, {
fakeParticipant: FakeParticipant.RemoteScreenShare
}));
});
}
if (addedScreenshareSourceNames.length) {
addedScreenshareSourceNames.forEach(id => dispatch(
createVirtualScreenshareParticipant(id, false, conference)));
addedScreenshareSourceNames.forEach(id => {
logger.debug('Creating virtual screenshare participant', id);
dispatch(createVirtualScreenshareParticipant(id, false, conference));
});
}
}

View File

@@ -1,10 +1,10 @@
import React, { Component } from 'react';
import {
SectionList as ReactNativeSectionList,
SafeAreaView,
SectionListRenderItemInfo,
ViewStyle
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Item, Section } from '../../types';
@@ -76,6 +76,7 @@ export default class SectionList extends Component<IProps> {
override render() {
return (
<SafeAreaView
edges = { [ 'left', 'right' ] }
style = { styles.container as ViewStyle } >
<ReactNativeSectionList
ListEmptyComponent = { this.props.ListEmptyComponent }

View File

@@ -24,6 +24,7 @@ import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants';
* determine whether and how to render it.
*/
const REDUCED_UI_THRESHOLD = 300;
const WEB_REDUCED_UI_THRESHOLD = 320;
/**
* Indicates a resize of the window.
@@ -49,6 +50,8 @@ export function clientResized(clientWidth: number, clientHeight: number) {
}
availableWidth -= getParticipantsPaneWidth(state);
dispatch(setReducedUI(availableWidth, clientHeight));
}
batch(() => {
@@ -106,7 +109,10 @@ export function setAspectRatio(width: number, height: number) {
*/
export function setReducedUI(width: number, height: number) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const reducedUI = Math.min(width, height) < REDUCED_UI_THRESHOLD;
const threshold = navigator.product === 'ReactNative'
? REDUCED_UI_THRESHOLD
: WEB_REDUCED_UI_THRESHOLD;
const reducedUI = Math.min(width, height) < threshold;
if (reducedUI !== getState()['features/base/responsive-ui'].reducedUI) {
return dispatch({

View File

@@ -1,5 +1,7 @@
import { IReduxState, IStore } from '../../app/types';
import { isTrackStreamingStatusActive } from '../../connection-indicator/functions';
import { handleToggleVideoMuted } from '../../toolbox/actions.any';
import { muteLocal } from '../../video-menu/actions.any';
import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
import { getParticipantById, isScreenShareParticipant } from '../participants/functions';
import {
@@ -78,3 +80,43 @@ export function isRemoteVideoReceived({ getState }: IStore, id: string): boolean
return Boolean(videoTrack && !videoTrack.muted && isTrackStreamingStatusActive(videoTrack));
}
/**
* Mutes the local audio. Same as clicking the audio mute button.
*
* @param {IStore} store - The redux store.
* @returns {Promise} Resolves when the action is complete.
*/
export function audioMute({ dispatch }: IStore) {
return dispatch(muteLocal(true, MEDIA_TYPE.AUDIO));
}
/**
* Unmutes the local audio. Same as clicking the audio unmute button.
*
* @param {IStore} store - The redux store.
* @returns {Promise} Resolves when the action is complete.
*/
export function audioUnmute({ dispatch }: IStore) {
return dispatch(muteLocal(false, MEDIA_TYPE.AUDIO));
}
/**
* Mutes the local video. Same as clicking the video mute button.
*
* @param {IStore} store - The redux store.
* @returns {Promise} Resolves when the action is complete.
*/
export function videoMute({ dispatch }: IStore) {
return dispatch(handleToggleVideoMuted(true, true, true));
}
/**
* Unmutes the local video. Same as clicking the video unmute button.
*
* @param {IStore} store - The redux store.
* @returns {Promise} Resolves when the action is complete.
*/
export function videoUnmute({ dispatch }: IStore) {
return dispatch(handleToggleVideoMuted(false, true, true));
}

View File

@@ -8,11 +8,15 @@ import { getJitsiMeetGlobalNS } from '../util/helpers';
import { setConnectionState } from './actions';
import {
audioMute,
audioUnmute,
getLocalCameraEncoding,
getRemoteVideoType,
isLargeVideoReceived,
isRemoteVideoReceived,
isTestModeEnabled
isTestModeEnabled,
videoMute,
videoUnmute
} from './functions';
import logger from './logger';
@@ -85,10 +89,14 @@ function _bindTortureHelpers(store: IStore) {
// All torture helper methods go in here
getJitsiMeetGlobalNS().testing = {
audioMute: audioMute.bind(null, store),
audioUnmute: audioUnmute.bind(null, store),
getRemoteVideoType: getRemoteVideoType.bind(null, store),
isLargeVideoReceived: isLargeVideoReceived.bind(null, store),
getLocalCameraEncoding: getLocalCameraEncoding.bind(null, store),
isRemoteVideoReceived: isRemoteVideoReceived.bind(null, store)
isRemoteVideoReceived: isRemoteVideoReceived.bind(null, store),
videoMute: videoMute.bind(null, store),
videoUnmute: videoUnmute.bind(null, store),
};
}

View File

@@ -29,6 +29,7 @@ import {
TRACK_UPDATED,
TRACK_WILL_CREATE
} from './actionTypes';
import { toggleScreensharing } from './actions';
import {
createLocalTracksF,
getCameraFacingMode,
@@ -385,8 +386,22 @@ export function trackAdded(track: any) {
const mediaType = track.getVideoType() === VIDEO_TYPE.DESKTOP
? MEDIA_TYPE.SCREENSHARE
: track.getType();
let isReceivingData, noDataFromSourceNotificationInfo, participantId;
// Make screen share toggle off listen to MediaStreamTrack "ended" event
// when it's terminated via Android status bar chip.
if (navigator.product === 'ReactNative') {
const mediaStreamTrack = track?.getTrack?.();
if (mediaType === MEDIA_TYPE.SCREENSHARE) {
const onEnded = () => dispatch(toggleScreensharing(false));
mediaStreamTrack.addEventListener('ended', onEnded);
track._onEnded = onEnded;
}
}
if (local) {
// Reset the no data from src notification state when we change the track, as it's context is set
// on a per device basis.
@@ -568,6 +583,16 @@ export function trackRemoved(track: any): {
track.removeAllListeners(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED);
track.removeAllListeners(JitsiTrackEvents.NO_DATA_FROM_SOURCE);
// Remove MediaStreamTrack "ended" event.
if (navigator.product === 'ReactNative') {
const mediaStreamTrack = track?.getTrack?.();
if (track._onEnded) {
mediaStreamTrack.removeEventListener('ended', track._onEnded);
delete track._onEnded;
}
}
return {
type: TRACK_REMOVED,
track: {

View File

@@ -6,6 +6,7 @@ import { setScreenshareMuted } from '../media/actions';
import { addLocalTrack, replaceLocalTrack } from './actions.any';
import { getLocalDesktopTrack, getTrackState } from './functions.native';
import logger from './logger';
export * from './actions.any';
@@ -63,6 +64,6 @@ async function _startScreenSharing(dispatch: IStore['dispatch'], state: IReduxSt
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
} catch (error: any) {
console.log('ERROR creating screen-sharing stream ', error);
logger.error('Error creating screen-sharing stream', error);
}
}

View File

@@ -294,7 +294,7 @@ export function setCameraFacingMode(facingMode: string | undefined) {
* @returns {Object} - The open dialog action.
*/
export function openAllowToggleCameraDialog(onAllow: Function, initiatorId: string) {
return openDialog(AllowToggleCameraDialog, {
return openDialog('AllowToggleCameraDialog', AllowToggleCameraDialog, {
onAllow,
initiatorId
});

View File

@@ -211,18 +211,21 @@ export function getLocalJitsiAudioTrackSettings(state: IReduxState) {
const jitsiTrack = getLocalJitsiAudioTrack(state);
if (!jitsiTrack) {
const config = state['features/base/config'];
const disableAP = Boolean(config?.disableAP);
const disableAGC = Boolean(config?.disableAGC);
const disableAEC = Boolean(config?.disableAEC);
const disableNS = Boolean(config?.disableNS);
const stereo = Boolean(config?.audioQuality?.stereo);
const {
audioQuality,
disableAEC = false,
disableAGC = false,
disableAP = false,
disableNS = false
} = state['features/base/config'] || {};
const enableStereo = Boolean(audioQuality?.stereo);
return {
autoGainControl: !disableAP && !disableAGC,
channelCount: stereo ? 2 : 1,
echoCancellation: !disableAP && !disableAEC,
noiseSuppression: !disableAP && !disableNS
autoGainControl: enableStereo ? false : !disableAP && !disableAGC,
channelCount: enableStereo ? 2 : 1,
echoCancellation: enableStereo ? false : !disableAP && !disableAEC,
noiseSuppression: enableStereo ? false : !disableAP && !disableNS
};
}

View File

@@ -33,7 +33,6 @@ import {
isUserInteractionRequiredForUnmute,
setTrackMuted
} from './functions';
import './subscriber';
/**
* Middleware that captures LIB_DID_DISPOSE and LIB_DID_INIT actions and,

View File

@@ -38,6 +38,7 @@ import {
import { ITrack, ITrackOptions } from './types';
import './middleware.any';
import './subscriber.web';
/**
* Middleware that captures LIB_DID_DISPOSE and LIB_DID_INIT actions and,
@@ -142,6 +143,13 @@ MiddlewareRegistry.register(store => next => action => {
if (typeof action.track?.muted !== 'undefined' && participantID && !local) {
logTracksForParticipant(store.getState()['features/base/tracks'], participantID, 'Track updated');
// Notify external API when remote participant mutes/unmutes themselves
const mediaType = isVideoTrack
? (jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP ? 'desktop' : 'video')
: 'audio';
APP.API.notifyParticipantMuted(participantID, action.track.muted, mediaType, true);
}
return result;

View File

@@ -1,40 +0,0 @@
import { isEqual, sortBy } from 'lodash-es';
import { MEDIA_TYPE } from '../media/constants';
import { getScreenshareParticipantIds } from '../participants/functions';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { isLocalTrackMuted } from './functions';
/**
* Notifies when the list of currently sharing participants changes.
*/
StateListenerRegistry.register(
/* selector */ state => getScreenshareParticipantIds(state),
/* listener */ (participantIDs, store, previousParticipantIDs) => {
if (typeof APP !== 'object') {
return;
}
if (!isEqual(sortBy(participantIDs), sortBy(previousParticipantIDs))) {
APP.API.notifySharingParticipantsChanged(participantIDs);
}
}
);
/**
* Notifies when the local video mute state changes.
*/
StateListenerRegistry.register(
/* selector */ state => isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO),
/* listener */ (muted, store, previousMuted) => {
if (typeof APP !== 'object') {
return;
}
if (muted !== previousMuted) {
APP.API.notifyVideoMutedStatusChanged(muted);
}
}
);

View File

@@ -0,0 +1,52 @@
import { isEqual, sortBy } from 'lodash-es';
// @ts-expect-error
import VideoLayout from '../../../../modules/UI/videolayout/VideoLayout';
import { getAutoPinSetting } from '../../video-layout/functions.any';
import { MEDIA_TYPE } from '../media/constants';
import { getScreenshareParticipantIds } from '../participants/functions';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { isLocalTrackMuted } from './functions';
/**
* Notifies when the list of currently sharing participants changes.
*/
StateListenerRegistry.register(
/* selector */ state => getScreenshareParticipantIds(state),
/* listener */ (participantIDs, store, previousParticipantIDs) => {
if (getAutoPinSetting() && participantIDs !== previousParticipantIDs) {
const { participantId } = store.getState()['features/large-video'];
// Check if any new screenshare participants were added
const newParticipants = participantIDs.filter((id: string) => !previousParticipantIDs.includes(id));
// If the current large video participant is a new screensharer, update the display. This is needed when
// the track is created much later after the action for auto-pinning is dispatched. This usually happens in
// very large meetings if the screenshare was already ongoing when the participant joined. The track is
// signaled only after the receiver constraints with SS source id is processed by the bridge but the
// auto-pinning action is dispatched when the participant tile is created as soon as the presence is
// received.
if (participantId && newParticipants.includes(participantId)) {
VideoLayout.updateLargeVideo(participantId, true);
}
}
if (!isEqual(sortBy(participantIDs), sortBy(previousParticipantIDs))) {
APP.API.notifySharingParticipantsChanged(participantIDs);
}
}
);
/**
* Notifies when the local video mute state changes.
*/
StateListenerRegistry.register(
/* selector */ state => isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO),
/* listener */ (muted, store, previousMuted) => {
if (muted !== previousMuted) {
APP.API.notifyVideoMutedStatusChanged(muted);
}
}
);

View File

@@ -22,7 +22,6 @@ export function assignIfDefined(target: Object, source: Object) {
return to;
}
const MATCH_OPERATOR_REGEXP = /[|\\{}()[\]^$+*?.-]/g;
/**
@@ -79,6 +78,21 @@ export function getJitsiMeetGlobalNS() {
return window.JitsiMeetJS.app;
}
/**
* Gets the Electron-specific global namespace.
*
* @returns {Object} The Electron namespace.
*/
export function getElectronGlobalNS() {
const globalNS = getJitsiMeetGlobalNS();
if (!globalNS.electron) {
globalNS.electron = {};
}
return globalNS.electron;
}
/**
* Returns the object that stores the connection times.
*

View File

@@ -58,7 +58,7 @@ const BreakoutRoomContextMenu = ({ room, actions = ALL_ACTIONS }: IProps) => {
}, [ dispatch, room ]);
const onRenameBreakoutRoom = useCallback(() => {
dispatch(openDialog(BreakoutRoomNamePrompt, {
dispatch(openDialog('BreakoutRoomNamePrompt', BreakoutRoomNamePrompt, {
breakoutRoomJid: room.jid,
initialRoomName: room.name
}));

View File

@@ -22,7 +22,7 @@ export * from './actions.any';
* }}
*/
export function openUpdateCalendarEventDialog(eventId: string) {
return openDialog(UpdateCalendarEventDialog, { eventId });
return openDialog('UpdateCalendarEventDialog', UpdateCalendarEventDialog, { eventId });
}
/**

View File

@@ -220,6 +220,34 @@ export function openCCPanel() {
};
}
/**
* Opens the chat panel with polls tab active.
*
* @returns {Object} The redux action.
*/
export function openPollsPanel() {
return async (dispatch: IStore['dispatch']) => {
dispatch(setFocusedTab(ChatTabs.POLLS));
dispatch({
type: OPEN_CHAT
});
};
}
/**
* Opens the chat panel with file sharing tab active.
*
* @returns {Object} The redux action.
*/
export function openFileSharingPanel() {
return async (dispatch: IStore['dispatch']) => {
dispatch(setFocusedTab(ChatTabs.FILE_SHARING));
dispatch({
type: OPEN_CHAT
});
};
}
/**
* Initiates the sending of messages between a moderator and a lobby attendee.

View File

@@ -1,30 +1,34 @@
import { IStore } from '../app/types';
import { IParticipant } from '../base/participants/types';
import { navigate } from '../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../mobile/navigation/routes';
import { OPEN_CHAT } from './actionTypes';
import { setFocusedTab } from './actions.any';
import { ChatTabs } from './constants';
export * from './actions.any';
/**
* Displays the chat panel.
* Displays the chat panel with the CHAT tab active.
*
* @param {Object} participant - The recipient for the private chat.
* @param {boolean} disablePolls - Checks if polls are disabled.
*
* @returns {{
* participant: participant,
* type: OPEN_CHAT
* }}
* @returns {Function}
*/
export function openChat(participant?: IParticipant | undefined | Object, disablePolls?: boolean) {
if (disablePolls) {
navigate(screen.conference.chat);
}
navigate(screen.conference.chatandpolls.main);
return (dispatch: IStore['dispatch']) => {
if (disablePolls) {
navigate(screen.conference.chat);
} else {
navigate(screen.conference.chatandpolls.main);
}
return {
participant,
type: OPEN_CHAT
dispatch(setFocusedTab(ChatTabs.CHAT));
dispatch({
participant,
type: OPEN_CHAT
});
};
}

View File

@@ -8,12 +8,13 @@ import {
SET_CHAT_WIDTH,
SET_USER_CHAT_WIDTH
} from './actionTypes';
import { closeChat } from './actions.any';
import { closeChat, setFocusedTab } from './actions.any';
import { ChatTabs } from './constants';
export * from './actions.any';
/**
* Displays the chat panel.
* Displays the chat panel with the CHAT tab active.
*
* @param {Object} participant - The recipient for the private chat.
* @param {Object} _disablePolls - Used on native.
@@ -24,6 +25,7 @@ export * from './actions.any';
*/
export function openChat(participant?: Object, _disablePolls?: boolean) {
return function(dispatch: IStore['dispatch']) {
dispatch(setFocusedTab(ChatTabs.CHAT));
dispatch({
participant,
type: OPEN_CHAT

View File

@@ -10,7 +10,7 @@ import { arePollsDisabled } from '../../../conference/functions.any';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { getUnreadPollCount } from '../../../polls/functions';
import { getUnreadCount, getUnreadFilesCount } from '../../functions';
import { getUnreadCount, getUnreadFilesCount, isChatDisabled } from '../../functions';
interface IProps extends AbstractButtonProps {
@@ -65,7 +65,7 @@ class ChatButton extends AbstractButton<IProps> {
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState, ownProps: any) {
const enabled = getFeatureFlag(state, CHAT_ENABLED, true);
const enabled = getFeatureFlag(state, CHAT_ENABLED, true) && !isChatDisabled(state);
const { visible = enabled } = ownProps;
return {

View File

@@ -24,7 +24,7 @@ import {
toggleChat
} from '../../actions.web';
import { CHAT_SIZE, ChatTabs, OPTION_GROUPCHAT, SMALL_WIDTH_THRESHOLD } from '../../constants';
import { getChatMaxSize } from '../../functions';
import { getChatMaxSize, getFocusedTab, isChatDisabled } from '../../functions';
import { IChatProps as AbstractProps } from '../../types';
import ChatHeader from './ChatHeader';
@@ -41,13 +41,18 @@ interface IProps extends AbstractProps {
/**
* The currently focused tab.
*/
_focusedTab: ChatTabs;
_focusedTab?: ChatTabs;
/**
* True if the CC tab is enabled and false otherwise.
*/
_isCCTabEnabled: boolean;
/**
* True if chat is disabled.
*/
_isChatDisabled: boolean;
/**
* True if file sharing tab is enabled.
*/
@@ -73,6 +78,11 @@ interface IProps extends AbstractProps {
*/
_isResizing: boolean;
/**
* The indicator which determines whether the UI is reduced.
*/
_reducedUI: boolean;
/**
* Whether or not to block chat access with a nickname input form.
*/
@@ -217,10 +227,12 @@ const Chat = ({
_isOpen,
_isPollsEnabled,
_isCCTabEnabled,
_isChatDisabled,
_isFileSharingTabEnabled,
_focusedTab,
_isResizing,
_messages,
_reducedUI,
_unreadMessagesCount,
_unreadPollsCount,
_unreadFilesCount,
@@ -229,6 +241,11 @@ const Chat = ({
dispatch,
t
}: IProps) => {
// If no tabs are available, don't render the chat panel at all.
if (_isChatDisabled && !_isPollsEnabled && !_isCCTabEnabled && !_isFileSharingTabEnabled) {
return null;
}
const { classes, cx } = useStyles({ _isResizing, width: _width });
const [ isMouseDown, setIsMouseDown ] = useState(false);
const [ mousePosition, setMousePosition ] = useState<number | null>(null);
@@ -287,8 +304,6 @@ const Chat = ({
// Disable text selection during resize
document.body.style.userSelect = 'none';
console.log('Chat resize: Mouse down', { clientX: e.clientX, initialWidth: _width });
}, [ _width, dispatch ]);
/**
@@ -304,8 +319,6 @@ const Chat = ({
// Restore cursor and text selection
document.body.style.cursor = '';
document.body.style.userSelect = '';
console.log('Chat resize: Mouse up');
}
}, [ isMouseDown, dispatch ]);
@@ -316,7 +329,6 @@ const Chat = ({
* @returns {void}
*/
const onChatResize = useCallback(throttle((e: MouseEvent) => {
// console.log('Chat resize: Mouse move', { clientX: e.clientX, isMouseDown, mousePosition, _width });
if (isMouseDown && mousePosition !== null && dragChatWidth !== null) {
// For chat panel resizing on the left edge:
// - Dragging left (decreasing X coordinate) should make the panel wider
@@ -416,7 +428,7 @@ const Chat = ({
return (
<>
{renderTabs()}
<div
{!_isChatDisabled && (<div
aria-labelledby = { ChatTabs.CHAT }
className = { cx(
classes.chatPanel,
@@ -442,7 +454,7 @@ const Chat = ({
)}
<ChatInput
onSend = { onSendMessage } />
</div>
</div>) }
{ _isPollsEnabled && (
<>
<div
@@ -484,8 +496,18 @@ const Chat = ({
* @returns {ReactElement}
*/
function renderTabs() {
let tabs = [
{
// The only way focused tab will be undefined is when no tab is enabled. Therefore this function won't be
// executed because Chat component won't render anything. This should never happen but adding the check
// here to make TS happy (when passing the _focusedTab in the selected prop for Tabs).
if (!_focusedTab) {
return null;
}
let tabs = [];
// Only add chat tab if chat is not disabled.
if (!_isChatDisabled) {
tabs.push({
accessibilityLabel: t('chat.tabs.chat'),
countBadge:
_focusedTab !== ChatTabs.CHAT && _unreadMessagesCount > 0 ? _unreadMessagesCount : undefined,
@@ -493,8 +515,8 @@ const Chat = ({
controlsId: `${ChatTabs.CHAT}-panel`,
icon: IconMessage,
title: t('chat.tabs.chat')
}
];
});
}
if (_isPollsEnabled) {
tabs.push({
@@ -551,6 +573,10 @@ const Chat = ({
);
}
if (_reducedUI) {
return null;
}
return (
_isOpen ? <div
className = { classes.container }
@@ -564,6 +590,8 @@ const Chat = ({
{_showNamePrompt
? <DisplayNameForm
isCCTabEnabled = { _isCCTabEnabled }
isChatDisabled = { _isChatDisabled }
isFileSharingEnabled = { _isFileSharingTabEnabled }
isPollsEnabled = { _isPollsEnabled } />
: renderChat()}
<div
@@ -602,18 +630,21 @@ const Chat = ({
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { isOpen, focusedTab, messages, unreadMessagesCount, unreadFilesCount, width, isResizing } = state['features/chat'];
const { isOpen, messages, unreadMessagesCount, unreadFilesCount, width, isResizing } = state['features/chat'];
const { unreadPollsCount } = state['features/polls'];
const _localParticipant = getLocalParticipant(state);
const { reducedUI } = state['features/base/responsive-ui'];
return {
_isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
_isOpen: isOpen,
_isPollsEnabled: !arePollsDisabled(state),
_isCCTabEnabled: isCCTabEnabled(state),
_isChatDisabled: isChatDisabled(state),
_isFileSharingTabEnabled: isFileSharingEnabled(state),
_focusedTab: focusedTab,
_focusedTab: getFocusedTab(state),
_messages: messages,
_reducedUI: reducedUI,
_unreadMessagesCount: unreadMessagesCount,
_unreadPollsCount: unreadPollsCount,
_unreadFilesCount: unreadFilesCount,

View File

@@ -9,6 +9,7 @@ import { IconMessage } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { closeOverflowMenuIfOpen } from '../../../toolbox/actions.web';
import { toggleChat } from '../../actions.web';
import { isChatDisabled } from '../../functions';
import ChatCounter from './ChatCounter';
@@ -91,7 +92,8 @@ class ChatButton extends AbstractButton<IProps> {
*/
const mapStateToProps = (state: IReduxState) => {
return {
_chatOpen: state['features/chat'].isOpen
_chatOpen: state['features/chat'].isOpen,
visible: !isChatDisabled(state)
};
};

View File

@@ -2,12 +2,12 @@ import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import Icon from '../../../base/icons/components/Icon';
import { IconCloseLarge } from '../../../base/icons/svg';
import { isFileSharingEnabled } from '../../../file-sharing/functions.any';
import { toggleChat } from '../../actions.web';
import { ChatTabs } from '../../constants';
import { getFocusedTab, isChatDisabled } from '../../functions';
interface IProps {
@@ -40,7 +40,8 @@ interface IProps {
function ChatHeader({ className, isCCTabEnabled, isPollsEnabled }: IProps) {
const dispatch = useDispatch();
const { t } = useTranslation();
const { focusedTab } = useSelector((state: IReduxState) => state['features/chat']);
const _isChatDisabled = useSelector(isChatDisabled);
const focusedTab = useSelector(getFocusedTab);
const fileSharingTabEnabled = useSelector(isFileSharingEnabled);
const onCancel = useCallback(() => {
@@ -56,7 +57,7 @@ function ChatHeader({ className, isCCTabEnabled, isPollsEnabled }: IProps) {
let title = 'chat.title';
if (focusedTab === ChatTabs.CHAT) {
if (!_isChatDisabled && focusedTab === ChatTabs.CHAT) {
title = 'chat.tabs.chat';
} else if (isPollsEnabled && focusedTab === ChatTabs.POLLS) {
title = 'chat.tabs.polls';
@@ -64,6 +65,11 @@ function ChatHeader({ className, isCCTabEnabled, isPollsEnabled }: IProps) {
title = 'chat.tabs.closedCaptions';
} else if (fileSharingTabEnabled && focusedTab === ChatTabs.FILE_SHARING) {
title = 'chat.tabs.fileSharing';
} else {
// If the focused tab is not enabled, don't render the header.
// This should not happen in normal circumstances since Chat.tsx already checks
// if any tabs are available before rendering.
return null;
}
return (

View File

@@ -4,10 +4,12 @@ import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { openDialog } from '../../../base/dialog/actions';
import Icon from '../../../base/icons/components/Icon';
import { IconSubtitles } from '../../../base/icons/svg';
import Button from '../../../base/ui/components/web/Button';
import { groupMessagesBySender } from '../../../base/util/messageGrouping';
import { StartRecordingDialog } from '../../../recording/components/Recording';
import { setRequestingSubtitles } from '../../../subtitles/actions.any';
import LanguageSelector from '../../../subtitles/components/web/LanguageSelector';
import { canStartSubtitles } from '../../../subtitles/functions.any';
@@ -88,6 +90,8 @@ export default function ClosedCaptionsTab() {
const _canStartSubtitles = useSelector(canStartSubtitles);
const [ isButtonPressed, setButtonPressed ] = useState(false);
const subtitlesError = useSelector((state: IReduxState) => state['features/subtitles']._hasError);
const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) =>
state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription);
const filteredSubtitles = useMemo(() => {
// First, create a map of transcription messages by message ID
@@ -121,14 +125,21 @@ export default function ClosedCaptionsTab() {
groupMessagesBySender(filteredSubtitles), [ filteredSubtitles ]);
const startClosedCaptions = useCallback(() => {
if (isButtonPressed) {
return;
if (isAsyncTranscriptionEnabled) {
dispatch(openDialog('StartRecordingDialog', StartRecordingDialog, {
recordAudioAndVideo: false
}));
} else {
if (isButtonPressed) {
return;
}
dispatch(setRequestingSubtitles(true, false, null));
setButtonPressed(true);
}
dispatch(setRequestingSubtitles(true, false, null));
setButtonPressed(true);
}, [ dispatch, isButtonPressed, setButtonPressed ]);
if (subtitlesError && isButtonPressed) {
}, [ isAsyncTranscriptionEnabled, dispatch, isButtonPressed, openDialog, setButtonPressed ]);
if (subtitlesError && isButtonPressed && !isAsyncTranscriptionEnabled) {
setButtonPressed(false);
}
@@ -148,7 +159,7 @@ export default function ClosedCaptionsTab() {
);
}
if (isButtonPressed) {
if (isButtonPressed && !isAsyncTranscriptionEnabled) {
setButtonPressed(false);
}
@@ -165,7 +176,7 @@ export default function ClosedCaptionsTab() {
);
}
if (isButtonPressed) {
if (isButtonPressed && !isAsyncTranscriptionEnabled) {
setButtonPressed(false);
}

View File

@@ -25,6 +25,16 @@ interface IProps extends WithTranslation {
*/
isCCTabEnabled: boolean;
/**
* Whether chat is disabled.
*/
isChatDisabled: boolean;
/**
* Whether file sharing is enabled.
*/
isFileSharingEnabled: boolean;
/**
* Whether the polls feature is enabled or not.
*/
@@ -74,18 +84,31 @@ class DisplayNameForm extends Component<IProps, IState> {
* @returns {ReactElement}
*/
override render() {
const { isCCTabEnabled, isPollsEnabled, t } = this.props;
const { isCCTabEnabled, isChatDisabled, isFileSharingEnabled, isPollsEnabled, t } = this.props;
let title = 'chat.nickname.title';
// Build array of enabled feature names (translated).
const features = [
!isChatDisabled ? t('chat.nickname.featureChat') : '',
isPollsEnabled ? t('chat.nickname.featurePolls') : '',
isFileSharingEnabled ? t('chat.nickname.featureFileSharing') : '',
isCCTabEnabled ? t('chat.nickname.featureClosedCaptions') : ''
].filter(Boolean);
if (isCCTabEnabled && isPollsEnabled) {
title = 'chat.nickname.titleWithPollsAndCC';
} else if (isCCTabEnabled) {
title = 'chat.nickname.titleWithCC';
} else if (isPollsEnabled) {
title = 'chat.nickname.titleWithPolls';
// Return null if no features available - component won't render.
if (features.length === 0) {
return null;
}
// Build translation arguments dynamically: { feature1: "chat", feature2: "polls", ... }
const translationArgs = features.reduce((acc, feature, index) => {
acc[`feature${index + 1}`] = feature;
return acc;
}, {} as Record<string, string>);
// Use dynamic translation key: "titleWith1Features", "titleWith2Features", etc.
const title = t(`chat.nickname.titleWith${features.length}Features`, translationArgs);
return (
<div id = 'nickname'>
<form onSubmit = { this._onSubmit }>

View File

@@ -12,6 +12,7 @@ import Button from '../../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import { copyText } from '../../../base/util/copyText.web';
import { handleLobbyChatInitialized, openChat } from '../../actions.web';
import logger from '../../logger';
export interface IProps {
className?: string;
@@ -125,11 +126,11 @@ const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, en
setShowCopiedMessage(false);
}, 2000);
} else {
console.error('Failed to copy text');
logger.error('Failed to copy text');
}
})
.catch(error => {
console.error('Error copying text:', error);
.catch((error: Error) => {
logger.error('Error copying text', error);
});
handleClose();
}, [ message ]);

View File

@@ -12,11 +12,14 @@ import { isJwtFeatureEnabled } from '../base/jwt/functions';
import { getParticipantById, isPrivateChatEnabled } from '../base/participants/functions';
import { IParticipant } from '../base/participants/types';
import { escapeRegexp } from '../base/util/helpers';
import { arePollsDisabled } from '../conference/functions.any';
import { isFileSharingEnabled } from '../file-sharing/functions.any';
import { getParticipantsPaneWidth } from '../participants-pane/functions';
import { isCCTabEnabled } from '../subtitles/functions.any';
import { VIDEO_SPACE_MIN_SIZE } from '../video-layout/constants';
import { IVisitorChatParticipant } from '../visitors/types';
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
import { ChatTabs, MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
import { IMessage } from './types';
/**
@@ -153,6 +156,53 @@ export function areSmileysDisabled(state: IReduxState) {
return disableChatSmileys;
}
/**
* Returns whether the chat feature is disabled.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} True if chat is disabled, false otherwise.
*/
export function isChatDisabled(state: IReduxState): boolean {
return Boolean(state['features/base/config']?.disableChat);
}
/**
* Gets the default focused tab based on what features are enabled.
* Returns the first available tab in priority order: CHAT -> POLLS -> FILE_SHARING -> CLOSED_CAPTIONS.
*
* @param {IReduxState} state - The redux state.
* @returns {ChatTabs | undefined} The default focused tab.
*/
export function getDefaultFocusedTab(state: IReduxState): ChatTabs | undefined {
if (!isChatDisabled(state)) {
return ChatTabs.CHAT;
}
if (!arePollsDisabled(state)) {
return ChatTabs.POLLS;
}
if (isFileSharingEnabled(state)) {
return ChatTabs.FILE_SHARING;
}
if (isCCTabEnabled(state)) {
return ChatTabs.CLOSED_CAPTIONS;
}
return undefined;
}
/**
* Returns the currently focused tab or the default focused tab if none is set.
*
* @param {IReduxState} state - The redux state.
* @returns {ChatTabs | undefined} The focused tab or undefined if no tabs are available.
*/
export function getFocusedTab(state: IReduxState): ChatTabs | undefined {
return state['features/chat'].focusedTab || getDefaultFocusedTab(state);
}
/**
* Returns the timestamp to display for the message.
*

View File

@@ -0,0 +1,23 @@
import { useSelector } from 'react-redux';
import ChatButton from './components/web/ChatButton';
import { isChatDisabled } from './functions';
const chat = {
key: 'chat',
Content: ChatButton,
group: 2
};
/**
* A hook that returns the chat button if chat is not disabled.
*
* @returns {Object | undefined} - The chat button object or undefined.
*/
export function useChatButton() {
const _isChatDisabled = useSelector(isChatDisabled);
if (!_isChatDisabled) {
return chat;
}
}

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../base/logging/functions';
export default getLogger('app:chat');

View File

@@ -25,6 +25,8 @@ import { IParticipant } from '../base/participants/types';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
import { arePollsDisabled } from '../conference/functions.any';
import { isFileSharingEnabled } from '../file-sharing/functions.any';
import { addGif } from '../gifs/actions';
import { extractGifURL, getGifDisplayMode, isGifEnabled, isGifMessage } from '../gifs/function.any';
import { showMessageNotification } from '../notifications/actions';
@@ -34,6 +36,7 @@ import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
import { pushReactions } from '../reactions/actions.any';
import { ENDPOINT_REACTION_NAME } from '../reactions/constants';
import { getReactionMessageFromBuffer, isReactionsEnabled } from '../reactions/functions.any';
import { isCCTabEnabled } from '../subtitles/functions.any';
import { showToolbox } from '../toolbox/actions';
import { getDisplayName } from '../visitors/functions';
@@ -66,7 +69,9 @@ import {
} from './constants';
import {
getDisplayNameSuffix,
getFocusedTab,
getUnreadCount,
isChatDisabled,
isSendGroupChatDisabled,
isVisitorChatParticipant
} from './functions';
@@ -181,23 +186,28 @@ MiddlewareRegistry.register(store => next => action => {
case SET_FOCUSED_TAB:
case OPEN_CHAT: {
const focusedTab = action.tabId || getState()['features/chat'].focusedTab;
const state = store.getState();
const focusedTab = action.tabId || getFocusedTab(state);
if (focusedTab === ChatTabs.CHAT) {
// Don't allow opening chat if it's disabled AND user is trying to open the CHAT tab.
if (isChatDisabled(state)) {
return next(action);
}
unreadCount = 0;
if (typeof APP !== 'undefined') {
APP.API.notifyChatUpdated(unreadCount, true);
}
const { privateMessageRecipient } = store.getState()['features/chat'];
const { privateMessageRecipient } = state['features/chat'];
if (
isSendGroupChatDisabled(store.getState())
isSendGroupChatDisabled(state)
&& privateMessageRecipient
&& !action.participant
) {
const participant = getParticipantById(store.getState(), privateMessageRecipient.id);
const participant = getParticipantById(state, privateMessageRecipient.id);
if (participant) {
action.participant = participant;
@@ -207,7 +217,21 @@ MiddlewareRegistry.register(store => next => action => {
}
}
} else if (focusedTab === ChatTabs.POLLS) {
// Don't allow opening chat panel if polls are disabled AND user is trying to open the POLLS tab.
if (arePollsDisabled(state)) {
return next(action);
}
dispatch(resetUnreadPollsCount());
// Don't allow opening chat panel if file sharing is disabled AND user is trying to open the
// FILE_SHARING tab.
} else if (focusedTab === ChatTabs.FILE_SHARING && !isFileSharingEnabled(state)) {
return next(action);
// Don't allow opening chat panel if closed captions are disabled AND user is trying to open the
// CLOSED_CAPTIONS tab.
} else if (focusedTab === ChatTabs.CLOSED_CAPTIONS && !isCCTabEnabled(state)) {
return next(action);
}
break;
@@ -216,6 +240,13 @@ MiddlewareRegistry.register(store => next => action => {
case PARTICIPANT_JOINED:
case PARTICIPANT_LEFT:
case PARTICIPANT_UPDATED: {
if (action.type === PARTICIPANT_LEFT) {
const { privateMessageRecipient } = store.getState()['features/chat'];
if (action.participant?.id === privateMessageRecipient?.id) {
store.dispatch(setPrivateMessageRecipient());
}
}
if (_shouldNotifyPrivateRecipientsChanged(store, action)) {
const result = next(action);
@@ -231,7 +262,7 @@ MiddlewareRegistry.register(store => next => action => {
const conference = getCurrentConference(state);
if (conference) {
// There may be cases when we intend to send a private message but we forget to set the
// There may be cases when we intend to send a private message, but we forgot to set the
// recipient. This logic tries to mitigate this risk.
const shouldSendPrivateMessageTo = _shouldSendPrivateMessageTo(state, action);
@@ -239,35 +270,38 @@ MiddlewareRegistry.register(store => next => action => {
const participantExists = getParticipantById(state, shouldSendPrivateMessageTo.id);
if (participantExists || shouldSendPrivateMessageTo.isFromVisitor) {
dispatch(openDialog(ChatPrivacyDialog, {
dispatch(openDialog('ChatPrivacyDialog', ChatPrivacyDialog, {
message: action.message,
participantID: shouldSendPrivateMessageTo.id,
isFromVisitor: shouldSendPrivateMessageTo.isFromVisitor,
displayName: shouldSendPrivateMessageTo.name
}));
// the dialog will take care of sending the message after user confirmation
break;
}
}
// Sending the message if privacy notice doesn't need to be shown.
const { privateMessageRecipient, isLobbyChatActive, lobbyMessageRecipient }
= state['features/chat'];
if (typeof APP !== 'undefined') {
APP.API.notifySendingChatMessage(action.message, Boolean(privateMessageRecipient));
}
if (isLobbyChatActive && lobbyMessageRecipient) {
conference.sendLobbyMessage({
type: LOBBY_CHAT_MESSAGE,
message: action.message
}, lobbyMessageRecipient.id);
_persistSentPrivateMessage(store, lobbyMessageRecipient, action.message, true);
} else if (privateMessageRecipient) {
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message, 'body', isVisitorChatParticipant(privateMessageRecipient));
_persistSentPrivateMessage(store, privateMessageRecipient, action.message);
} else {
// Sending the message if privacy notice doesn't need to be shown.
const { privateMessageRecipient, isLobbyChatActive, lobbyMessageRecipient }
= state['features/chat'];
if (typeof APP !== 'undefined') {
APP.API.notifySendingChatMessage(action.message, Boolean(privateMessageRecipient));
}
if (isLobbyChatActive && lobbyMessageRecipient) {
conference.sendLobbyMessage({
type: LOBBY_CHAT_MESSAGE,
message: action.message
}, lobbyMessageRecipient.id);
_persistSentPrivateMessage(store, lobbyMessageRecipient, action.message, true);
} else if (privateMessageRecipient) {
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message, 'body', isVisitorChatParticipant(privateMessageRecipient));
_persistSentPrivateMessage(store, privateMessageRecipient, action.message);
} else {
conference.sendTextMessage(action.message);
}
conference.sendTextMessage(action.message);
}
}
break;
@@ -576,6 +610,11 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
const { isOpen: isChatOpen } = state['features/chat'];
const { soundsIncomingMessage: soundEnabled, userSelectedNotifications } = state['features/base/settings'];
// Don't play sound or show notifications if chat is disabled.
if (isChatDisabled(state)) {
return;
}
if (soundEnabled && shouldPlaySound && !isChatOpen) {
dispatch(playSound(INCOMING_MSG_SOUND_ID));
}

View File

@@ -35,7 +35,7 @@ const DEFAULT_STATE = {
privateMessageRecipient: undefined,
lobbyMessageRecipient: undefined,
isLobbyChatActive: false,
focusedTab: ChatTabs.CHAT,
focusedTab: undefined,
isResizing: false,
width: {
current: CHAT_SIZE,
@@ -44,7 +44,7 @@ const DEFAULT_STATE = {
};
export interface IChatState {
focusedTab: ChatTabs;
focusedTab?: ChatTabs;
groupChatWithPermissions: boolean;
isLobbyChatActive: boolean;
isOpen: boolean;

View File

@@ -22,7 +22,7 @@ export function notifyKickedOut(participant: any, submit?: Function) {
return;
}
dispatch(openDialog(AlertDialog, {
dispatch(openDialog('AlertDialog', AlertDialog, {
contentKey: {
key: participant ? 'dialog.kickTitle' : 'dialog.kickSystemTitle',
params: {
@@ -52,7 +52,7 @@ export function notifyConferenceFailed(reasonKey: string, submit?: Function) {
// we have to push the opening of the dialog to the queue
// so that we make sure it will be visible after the events
// of conference destroyed are done
setTimeout(() => dispatch(openDialog(AlertDialog, {
setTimeout(() => dispatch(openDialog('AlertDialog', AlertDialog, {
contentKey: {
key: reasonKey
},
@@ -60,7 +60,7 @@ export function notifyConferenceFailed(reasonKey: string, submit?: Function) {
},
onSubmit: () => {
submit?.();
dispatch(hideDialog(AlertDialog));
dispatch(hideDialog('AlertDialog', AlertDialog));
}
})));
};

View File

@@ -17,7 +17,7 @@ import logger from './logger';
*/
export function openLeaveReasonDialog(title?: string) {
return (dispatch: IStore['dispatch']): Promise<void> => new Promise(resolve => {
dispatch(openDialog(LeaveReasonDialog, {
dispatch(openDialog('LeaveReasonDialog', LeaveReasonDialog, {
onClose: resolve,
title
}));

View File

@@ -3,11 +3,10 @@ import React, { useCallback } from 'react';
import {
BackHandler,
NativeModules,
SafeAreaView,
View,
ViewStyle
} from 'react-native';
import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context';
import { Edge, EdgeInsets, SafeAreaView, withSafeAreaInsets } from 'react-native-safe-area-context';
import { connect, useDispatch } from 'react-redux';
import { appNavigate } from '../../../app/actions.native';
@@ -436,6 +435,7 @@ class Conference extends AbstractConference<IProps, State> {
</View>
<SafeAreaView
edges = { [ 'left', 'right', 'top' ] }
pointerEvents = 'box-none'
style = {
(_toolboxVisible
@@ -444,6 +444,7 @@ class Conference extends AbstractConference<IProps, State> {
<TitleBar _createOnPress = { this._createOnPress } />
</SafeAreaView>
<SafeAreaView
edges = { [ 'bottom', 'left', 'right', !_toolboxVisible && 'top' ].filter(Boolean) as Edge[] }
pointerEvents = 'box-none'
style = {
(_toolboxVisible

View File

@@ -90,6 +90,11 @@ interface IProps extends AbstractProps, WithTranslation {
*/
_overflowDrawer: boolean;
/**
* The indicator which determines whether the UI is reduced.
*/
_reducedUI: boolean;
/**
* Name for this conference room.
*/
@@ -226,12 +231,45 @@ class Conference extends AbstractConference<IProps, any> {
_layoutClassName,
_notificationsVisible,
_overflowDrawer,
_reducedUI,
_showLobby,
_showPrejoin,
_showVisitorsQueue,
t
} = this.props;
if (_reducedUI) {
return (
<div
id = 'layout_wrapper'
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }
onMouseMove = { this._onMouseMove }
ref = { this._setBackground }>
<Chat />
<div
className = { _layoutClassName }
id = 'videoconference_page'
onMouseMove = { isMobileBrowser() ? undefined : this._onShowToolbar }>
<ConferenceInfo />
<Notice />
<div
id = 'videospace'
onTouchStart = { this._onVideospaceTouchStart }>
<LargeVideo />
</div>
<span
aria-level = { 1 }
className = 'sr-only'
role = 'heading'>
{ t('toolbar.accessibilityLabel.heading') }
</span>
<Toolbox />
</div>
</div>
);
}
return (
<div
id = 'layout_wrapper'
@@ -418,6 +456,7 @@ class Conference extends AbstractConference<IProps, any> {
function _mapStateToProps(state: IReduxState) {
const { backgroundAlpha, mouseMoveCallbackInterval } = state['features/base/config'];
const { overflowDrawer } = state['features/toolbox'];
const { reducedUI } = state['features/base/responsive-ui'];
return {
...abstractMapStateToProps(state),
@@ -426,6 +465,7 @@ function _mapStateToProps(state: IReduxState) {
_layoutClassName: LAYOUT_CLASSNAMES[getCurrentLayout(state) ?? ''],
_mouseMoveCallbackInterval: mouseMoveCallbackInterval,
_overflowDrawer: overflowDrawer,
_reducedUI: reducedUI,
_roomName: getConferenceNameForTitle(state),
_showLobby: getIsLobbyVisible(state),
_showPrejoin: isPrejoinPageVisible(state),

View File

@@ -34,6 +34,11 @@ interface IProps {
autoHide?: string[];
};
/**
* The indicator which determines whether the UI is reduced.
*/
_reducedUI: boolean;
/**
* Indicates whether the component should be visible or not.
*/
@@ -194,6 +199,12 @@ class ConferenceInfo extends Component<IProps> {
* @returns {ReactElement}
*/
override render() {
const { _reducedUI } = this.props;
if (_reducedUI) {
return null;
}
return (
<div
className = 'details-container'
@@ -217,9 +228,12 @@ class ConferenceInfo extends Component<IProps> {
* }}
*/
function _mapStateToProps(state: IReduxState) {
const { reducedUI } = state['features/base/responsive-ui'];
return {
_conferenceInfo: getConferenceInfo(state),
_reducedUI: reducedUI,
_visible: isToolboxVisible(state),
_conferenceInfo: getConferenceInfo(state)
};
}

View File

@@ -25,9 +25,10 @@ const useStyles = makeStyles()(theme => {
const Notice = () => {
const message = useSelector((state: IReduxState) => state['features/base/config'].noticeMessage);
const { reducedUI } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const { classes } = useStyles();
if (!message) {
if (!message || reducedUI) {
return null;
}

View File

@@ -26,7 +26,7 @@ function SpeakerStatsLabel() {
const { t } = useTranslation();
const onClick = () => {
dispatch(openDialog(SpeakerStats, { conference }));
dispatch(openDialog('SpeakerStats', SpeakerStats, { conference }));
};
if (count <= 2 || _isSpeakerStatsDisabled) {

View File

@@ -327,7 +327,7 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
* @returns {void}
*/
_onOpenBandwidthDialog() {
dispatch(openDialog(BandwidthSettingsDialog));
dispatch(openDialog('BandwidthSettingsDialog', BandwidthSettingsDialog));
}
};
}

View File

@@ -18,7 +18,7 @@ type Options = {
export function showDesktopPicker(options: Options = {}, onSourceChoose: Function) {
const { desktopSharingSources } = options;
return openDialog(DesktopPicker, {
return openDialog('DesktopPicker', DesktopPicker, {
desktopSharingSources,
onSourceChoose
});

View File

@@ -259,6 +259,29 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
const newValue = name === 'channelCount' ? (checked ? 2 : 1) : checked;
if (name === 'channelCount' && newValue === 2) {
super._onChange({
audioSettings: {
autoGainControl: false,
channelCount: 2,
echoCancellation: false,
noiseSuppression: false
}
});
return;
} else if (name !== 'channelCount' && newValue === true) {
super._onChange({
audioSettings: {
...audioSettings,
[name]: newValue,
channelCount: 1
}
});
return;
}
super._onChange({
audioSettings: {
...audioSettings,

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