Compare commits

...

157 Commits

Author SHA1 Message Date
damencho
a24ddf205c feat(visitors): Adds enabled flag in the metadata. 2025-07-09 15:51:21 +03:00
damencho
7a97d15e89 feat(conference): Clears any error from previous attempts.
When you see the error, you may click join on pre-join again, which may succeeded, so clear previous errors.
2025-07-09 14:14:49 +03:00
damencho
1acb99d763 fix(av-moderation): Fixes auto starting av moderation, notify everyone. 2025-07-08 21:18:44 +03:00
damencho
adbe990867 fix(visitors): A join case with live rooms. 2025-07-08 19:10:28 +03:00
Saúl Ibarra Corretgé
a4367567ab fix(amplitude) adjust to new SDK API changes
Ref: https://amplitude.com/docs/sdks/analytics/browser/migrate-from-javascript-sdk-to-browser-sdk-2-0
2025-07-08 17:40:46 +02:00
damencho
7f56cbc4ce fix(av-moderation): Fixes auto starting av moderation.
There are some startMuted policies we set when starting it.
2025-07-08 16:18:57 +03:00
damencho
d636d084c8 fix(visitors): Fixes empty array case and wrong json.
When there is empty array cjson produces array: {} while prosody's json impl checks is it array and produces the correct value (array: []). Prosody impl is a little bit slower, but this is not a hot path and those are not huge json strings.
2025-07-08 13:31:05 +03:00
damencho
298567be48 fix(visitors): Updates docs, drops s2soutinjection.
That module was initially dropped with 354a3c002a.
2025-07-08 10:25:23 +03:00
Boris Grozev
c233629e51 fix: Do not merge participants and moderators into room metadata. 2025-07-08 06:00:35 +03:00
Saúl Ibarra Corretgé
75b5702a7e fix(file-sharing) fix resetting the state for share file input
Otherwise re-uploading the same file would not work because the input
element doesn't change state, as the value would remain the same.
2025-07-07 15:36:09 +02:00
benasm7
540f01d47e fix(virtual-background): Fix i18n for a device error.
* Reusing existing translation string for virtual background error notification, instead of current hardcoded english value.

* Update VirtualBackgroundPreview.tsx
2025-07-07 07:57:48 -05:00
Robert Oanta
5c7ed6a8b3 feat(av_moderation): handle av_can_unmute policy 2025-07-07 15:33:28 +03:00
damencho
3c5d33fefa fix(visitors): Avoid go live to overwrite other settings. 2025-07-07 15:33:14 +03:00
ltorje-8x8
be04236834 feat(visitors): Fixes nil error about 'get_visitors_room_metadata'
* Attempt to call a nil value (global 'get_visitors_room_metadata')

* make the linter happy

* more trailing whitespace + cleanup

* apply review

* use default false
2025-07-07 05:31:13 -05:00
Saúl Ibarra Corretgé
ec1bfe73b3 fix(amplitude) sync device ID on web too
Note the use of jitsiLocalStorage since we also need to consider the
case when local storage is performed in the host page when in an
iframe.
2025-07-07 11:39:04 +02:00
Saúl Ibarra Corretgé
d2ed9ffef6 fix(transcribing) fix overriding transcribing state
Skip updating the transcribing state when the 'audio-recording-enabled'
property is not provided.

This fixes a race when a transcriber is already in the room, we'll see
it before properties are updated (sometimes) and without checking for
undefined we'd flip the local value to false.
2025-07-04 17:15:06 +02:00
Saúl Ibarra Corretgé
6141ff78f8 fix(rn,embed) remove 8x8 apps from isEmbedded check
For all intents and purposes 8x8 apps are integrating the SDK as a 3rd
party.

Yes, we are a 1st party of sorts, but that's ok because 8x8.vc allows
embedding.
2025-07-04 15:31:14 +02:00
Saúl Ibarra Corretgé
c6a75fb9ed fix(file-sharing) hide upload button for visitors 2025-07-04 13:19:26 +02:00
Andrei Gavrilescu
3438438219 feat(recording): enable consent dialog on spot (#16179)
* enable consent dialog on spot

* lint fix

* move spot consent behind config flag

* revert copilot magic
2025-07-04 11:45:01 +03:00
Matteo
7cedea6740 lang: update Italian translation 2025-07-04 10:37:49 +02:00
Hristo Terezov
69f26c8a38 fix(participant-pane): Don't show the Viewers label twice. 2025-07-03 19:00:38 -05:00
Hristo Terezov
92a4750d0e fix(VisitorsList): use separate stomp instance. 2025-07-03 19:00:38 -05:00
Hristo Terezov
370a884765 fix(visitors): avoid lost deltas when subscribing 2025-07-03 19:00:38 -05:00
Hristo Terezov
877fc98eef feat(visitors-list): Add to participant pane. 2025-07-03 07:52:09 -05:00
Jaya Allamsetty
7bed0b36bd chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2024.0.0+006b25e4...v2025.0.0+49eb29a8
2025-07-02 22:30:40 -04:00
damencho
cd5aed37e9 feat(filesharing): Adds a nil check.
In case of file failing to upload we try to remove it, but there was nothing indicated as added before that.
2025-07-02 15:01:01 -05:00
damencho
b8dad082df chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2021.0.0+5a044f1a...v2024.0.0+006b25e4
2025-07-02 14:20:22 -05:00
damencho
f84f98e8e5 fix(visitors): Allow joining queue when not prefer to be visitor. 2025-07-02 14:20:22 -05:00
damencho
d1328d68f2 fix(visitors): Deny access when room is not live and there is a list of participants. 2025-07-02 14:20:22 -05:00
damencho
43d5c1e3ba feat(visitors): Adds allow promotion setting per room. 2025-07-02 14:20:22 -05:00
damencho
22ed00724d fix(visitors): Checks mainMeetingParticipants array to allow joins.
squash: Change checks in find table.
2025-07-02 14:20:22 -05:00
Horatiu Muresan
0b095f36eb fix(file-sharing) Keep original filename on file download (#16183) 2025-07-02 16:49:42 +03:00
Jaya Allamsetty
327376d85e chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2018.0.0+1773bcff...v2021.0.0+5a044f1a
2025-07-01 13:02:28 -04:00
Saúl Ibarra Corretgé
f28bd67ff4 fix(PressureObserver) adapt to API changes
Also set a sampling intervakl of 30s to avoid too chatty logs.
2025-07-01 16:47:15 +02:00
Horatiu Muresan
3a54c3418b feat(filmstrip) Add always visible resize bar and initial width (#16181) 2025-07-01 16:07:47 +03:00
TTG
b6026dcd04 fix(lang) Update zhCN & zhTW translations and fix English apostrophes (#16174)
* Update main-zhTW.json

* Update main-zhCN.json

* Update main.json
2025-06-30 03:42:52 -05:00
Jaya Allamsetty
2eff0d8f78 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2012.0.0+86b76227...v2018.0.0+1773bcff
2025-06-27 13:39:38 -04:00
Saúl Ibarra Corretgé
1ab7989a1a fix(file-sharing) fix computing file sharing percentage
Actually implement what the comment says: we want to cap the progress at
99% so we wait for the request to complete with 200 before going to
100%.
2025-06-27 16:34:10 +02:00
Calinteodor
b8c6c2381c dep(@amplitude): replace with web and native deps with new ones (#16169)
Replaced Amplitude related dependencies with https://github.com/amplitude/Amplitude-TypeScript
2025-06-27 15:39:33 +03:00
Avram Tudor
3f9202ce04 fix: hide chat controls and show disabled notice instead (#16168) 2025-06-26 17:49:48 +03:00
Avram Tudor
965b413d26 fix: hide create poll button rather than disabling it (#16167) 2025-06-26 15:32:29 +03:00
Avram Tudor
1cb2025951 fix: layout issue when only chat tab is visible (#16166) 2025-06-26 14:44:09 +03:00
Avram Tudor
4decb41a1e feat: allow private chats between users and moderators (#16165)
* fix context menu of a chat bubble containing the private message entry when private chat should be disabled
2025-06-26 14:43:54 +03:00
Saúl Ibarra Corretgé
8a79d200c8 fix(android) disable HW AV1 codec
We have observed terrible framerates due to the decoder getting
constantly restarted. Surprisingly, this only happens when using the
JVB, it does not happen when using P2P with Chromium as the other
endpoint.

We shall look into that from the bridge side.

At any rate, I'm disabling the HW video decoder for AV1, which means we
will be using dav1d, the software video decoder.
2025-06-26 13:29:54 +02:00
damencho
2f9436afb1 fix(tests): Skip url normalisation test when using tokens. 2025-06-25 08:18:35 -05:00
damencho
66dc158c22 feat(tests): Adds a debug print for document ready. 2025-06-25 08:18:35 -05:00
damencho
921ed99676 fix(permissions): Fixes grant owner for participant with token. 2025-06-24 12:49:40 -05:00
damencho
bd612ef8ea fix(tests): Fixes Firefox tests excludes. 2025-06-24 08:29:10 -05:00
damencho
b93c69c24e feat(tests): Adds url normalize test. 2025-06-24 08:29:10 -05:00
dependabot[bot]
cc6326a23f chore(deps): bump dompurify, @giphy/js-fetch-api and @giphy/react-components
Removes [dompurify](https://github.com/cure53/DOMPurify). It's no longer used after updating ancestor dependencies [dompurify](https://github.com/cure53/DOMPurify), [@giphy/js-fetch-api](https://github.com/Giphy/giphy-js) and [@giphy/react-components](https://github.com/Giphy/giphy-js). These dependencies need to be updated together.


Removes `dompurify`

Updates `@giphy/js-fetch-api` from 4.7.1 to 4.9.3
- [Release notes](https://github.com/Giphy/giphy-js/releases)
- [Commits](https://github.com/Giphy/giphy-js/compare/@giphy/js-fetch-api@4.7.1...@giphy/js-fetch-api@4.9.3)

Updates `@giphy/react-components` from 6.8.1 to 6.9.4
- [Release notes](https://github.com/Giphy/giphy-js/releases)
- [Commits](https://github.com/Giphy/giphy-js/compare/@giphy/react-components@6.8.1...@giphy/react-components@6.9.4)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: indirect
- dependency-name: "@giphy/js-fetch-api"
  dependency-type: direct:production
- dependency-name: "@giphy/react-components"
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-21 10:28:06 +02:00
Saúl Ibarra Corretgé
d6776f234d Revert "feat(android) drop support for x86 architecture"
This reverts commit 2763c4fdee.
2025-06-20 14:01:08 +03:00
Saúl Ibarra Corretgé
2763c4fdee feat(android) drop support for x86 architecture
It's only used by really old Chromebooks, and we provide a TWA for those
anyway.
2025-06-19 17:22:24 +03:00
Calin-Teodor
2aaf0ed543 feat(base/config): remove customToolbarButtons warning 2025-06-19 13:05:34 +03:00
Saúl Ibarra Corretgé
17f335f0c9 fix(ios) remove unused entitlement 2025-06-18 10:43:43 +02:00
damencho
e280d1d963 feat(transcribing): Use invite jigasi option only when async transcriptions are enabled. 2025-06-17 15:51:52 -05:00
damencho
a43472985b chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2006.0.0+04c7ab17...v2012.0.0+86b76227
2025-06-17 15:51:41 -05:00
damencho
e5187de9c3 feat(file-sharing): Uses a filesharing management from ljm.
squash: Change the way we pass the list of files from the backend.

squash: Remove actions to sync local state with remote.
2025-06-17 15:51:41 -05:00
damencho
b296776ed7 feat(prosody): Fixes log messages. 2025-06-17 15:51:41 -05:00
damencho
93bc4019ad feat(prosody): Simplifies modules that need to add identity. 2025-06-17 15:51:41 -05:00
Andrei Gavrilescu
a4c20469cd feat(API): expose recording consent to external api (#16141)
* expose recording consent to api

* Update react/features/recording/actions.web.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-17 14:44:00 +03:00
Saúl Ibarra Corretgé
196fd455cd fix(rn,dynamic-branding) don't override payload
Otherwise we'll miss any new properties. We just keep the sanitization
of avatarBackgrounds, which mobile needs.
2025-06-17 12:30:54 +03:00
Calin-Teodor
e75bacae4a feat(toolbox): use the same name for screen share 2025-06-17 09:16:26 +03:00
damencho
d238386085 feat(polls): Make sure we check for json messages with no namespace.
Missing namespace can interfere with polls logic.
2025-06-16 12:00:01 -05:00
damencho
a1634eb813 feat(iframeAPI): Fires transcribing events when subtitles are on. 2025-06-16 11:25:56 -05:00
Saúl Ibarra Corretgé
566d76a28d fix(android) drop old JSC dependency
We only use Hermes now.
2025-06-16 14:43:56 +03:00
Дамян Минков
494afde82a fix(config): Moves a property to dynamic branding. (#16138)
* fix(config): Moves a property to dynamic branding.

* squash: Remove unused.
2025-06-13 11:24:27 -05:00
Jaya Allamsetty
51ba5d31aa fix(tracks) Log when the MediaStream becomes 'inactive' 2025-06-12 13:57:54 -04:00
Дамян Минков
5f88b117ae fix(visitors): Fixes visitors count. (#16134)
* fix(visitors): Fixes visitors count.

* squash: Simplify logic with new function counting participants to display.
2025-06-11 12:51:33 -05:00
damencho
4500a5aba5 fix(conference): Handles promise rejection on conference failed.
For example when lobby is enabled.
2025-06-10 16:38:57 -05:00
damencho
fb64d1b68b feat(permissions): Clear up granted-permissions.
We do not need to keep granted permissions in separate field. We can always check the granted user-id or whether the current participant has a token (session.auth_token).
2025-06-10 12:20:56 -05:00
Wilson Furtado
a39d8d35a2 feat(base/avatar): Update the avatar to show the first and last name letters rather than first and second name (#15732)
* Update the avatar to select first and last name rather than first and second
2025-06-10 16:47:48 +03:00
Calinteodor
6bc12766f9 feat(react-native-sdk): fixed missing dependencies (#16102)
* feat(react-native-sdk): fixed missing dependencies errors
2025-06-10 13:37:08 +03:00
Mihaela Dumitru
ecf9bee7d0 fix(recordings) create missing local tracks when unmuting after consent (#16119)
* fix(recordings) create missing local tracks when unmuting after consent

* fix(conference) Avoid creating duplicate tracks on unmute

* squash: Ignore TS linter error

---------

Co-authored-by: Jaya Allamsetty <jaya.allamsetty@8x8.com>
2025-06-09 16:44:24 -04:00
Aaron van Meerten
6b5245be44 feat(webpack): rewrite CDN urls when proxying with webpack (#15938)
* feat(webpack): rewrite CDN urls when proxying with webpack

* fix lint

* squash: fix checking for existing file.

---------

Co-authored-by: Дамян Минков <damencho@jitsi.org>
2025-06-09 13:44:57 -05:00
damencho
6b71122cac feat(file-sharing): Update wrong fields.
Suppress a warning from external service for missing port and transport.
2025-06-06 13:42:21 -05:00
Дамян Минков
f060ab9d26 feat(visitors): Updates buttons visible to visitors.
* feat(visitors): Allow participants pane button.

* feat(visitors): Do not count the local participant when in visitor mode.

* feat(visitors): Use same buttons on web and native.

* feat(visitors): Always show the visitors count.

It was shown only for the main participants.

* feat(visitors): Skips showing local in participants pane when visitor.
2025-06-05 16:44:47 -05:00
dependabot[bot]
df3dd2b14e chore(deps-dev): bump undici from 6.21.1 to 6.21.3
Bumps [undici](https://github.com/nodejs/undici) from 6.21.1 to 6.21.3.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.21.1...v6.21.3)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.21.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-04 22:31:28 +02:00
Saúl Ibarra Corretgé
be9e1136de fix(ts) apply import linting rules to TS files too 2025-06-04 22:04:36 +02:00
Saúl Ibarra Corretgé
e2337e8db8 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1995.0.0+6de3a8ff...v2006.0.0+04c7ab17
2025-06-04 22:04:36 +02:00
Дамян Минков
293b6fa908 feat(jwt): New option to control user info check. (#16115)
* feat(jwt): New option to control user info check.

* squash: Rename.
2025-06-04 08:07:42 -05:00
dependabot[bot]
34da0ff99e chore(deps-dev): bump tar-fs from 3.0.8 to 3.0.9
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.0.8 to 3.0.9.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.8...v3.0.9)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 3.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-04 14:34:20 +02:00
bgrozev
06713a4ffa fix: Handling inviteJigasiOnBackendTranscribing. (#16113)
* fix: Handling inviteJigasiOnBackendTranscribing.

* squash: Remove extra whitespace.
2025-06-03 19:42:36 -05:00
Дамян Минков
94813163e8 feat(jwt): Delay loading of avatar. (#16111)
* feat(jwt): Delay loading of avatar.

Only load avatar when jwt has been validated server-side (after connected). The drawback is not loading the avatar on pre-join page only the first time.

* squash: fix comments.
2025-06-03 13:59:29 -05:00
damencho
bb1eade1f0 feat(prosody): Allow filter_rayo to be loaded in other virtual hosts. 2025-06-03 13:59:18 -05:00
May
2f0ca6c839 fix(conference): show hours in duration instead of undefined 2025-06-03 09:30:19 -05:00
Jaya Allamsetty
45bbf06a85 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1994.0.0+864d0f01...v1995.0.0+6de3a8ff
2025-06-02 11:54:38 -04:00
Jaya Allamsetty
042007adb5 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1993.0.0+e4763700...v1994.0.0+864d0f01
2025-05-30 10:46:15 -04:00
Jaya Allamsetty
dc73d1e328 feat(conference) force reload page on shard changes. 2025-05-30 10:05:05 -04:00
Дамян Минков
db4c9666c3 feat(transcribing): Switch state on audio-recording-enabled. (#16094)
* feat(transcribing): Switch state on audio-recording-enabled.

* squash: Simplifies check based on suggestion.
2025-05-30 08:15:05 -05:00
Дамян Минков
45cfc0e112 feat(metadata): Append moderators to the list of main participants for jicofo. (#16097)
* feat(metadata): Append moderators to the list of main participants for jicofo.

* squash: Drop not needed line.
2025-05-29 21:36:04 -05:00
Дамян Минков
fa9aab953d feat(transcribing): Adds config to skip inviting jigasi on transcribing. (#16095)
* feat(transcribing): Adds config to skip inviting jigasi on transcribing.

* squash: Changes config value name.
2025-05-29 21:35:46 -05:00
damencho
c16f652378 feat(conference): Adds check for non-moderators dropping files anywhere. 2025-05-29 08:32:43 -05:00
damencho
6f3ae47a2e fix(prosody): Updates client permissions on granting owner rights.
Setting granted permissions in session should happen before sending self-presence on muc-pre-set-affiliation.
2025-05-29 08:32:43 -05:00
damencho
6afbff9b36 feat(file-sharing): Uses short term token for operations. 2025-05-29 08:32:43 -05:00
damencho
c7b6af1df3 feat(file-sharing): Uses feature to determine is upload enabled. 2025-05-29 08:32:43 -05:00
damencho
308faf71bf feat(prosody): Adds a short-live token module.
Use xep-0215 to retrieve it.
2025-05-29 08:32:43 -05:00
damencho
78efddc447 fix(permissions): Fix grant the granted features. 2025-05-29 08:32:43 -05:00
zxshen
d370e79237 fix(chat): avoid emoji accidents when sending messages (#15854)
* fix(chat): avoid emoji accidents when send messages

* fix: resolve ESLint formatting issues

Signed-off-by: Zhenxing Shen <zxshen@amazon.com>

---------

Signed-off-by: Zhenxing Shen <zxshen@amazon.com>
2025-05-29 07:22:39 -05:00
Jaya Allamsetty
97b01b910e chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1991.0.0+80c2686a...v1993.0.0+e4763700
2025-05-28 14:23:08 -04:00
Дамян Минков
3bb28c4090 feat(metadata): Sends a list of main participants to jicofo. (#16088)
* feat(metadata): Sends a list of main participants to jicofo.

Drops adding metadata in the form, jicofo, jigasi and client are reading both and now we send initial message before the self-presence.

* squash: Fix error and make a shallow copy of metadata before sending to jicofo.
2025-05-28 11:01:59 -05:00
Saúl Ibarra Corretgé
589baab2ae fix(local-recording) defend against out of order events
We have observed some failed recordings which are lacking the EBML
header. The only way in which that seems plausible is if the
dataavailable event is received while processing the stop event. This is
allegedly not possible, but it's the only plausible explanation, so
let's defend against that.

Extend the timeslice back to 5s too.
2025-05-28 10:43:33 -05:00
Oğuzhan Selim Temiz
85fe5cf31e chore(deps): replace moment.js with day.js for locale handling (#15875)
* chore(deps): replace moment.js with day.js for locale handling and 
pin day.js version to 1.11.13 in package-lock.json

---------

Co-authored-by: Calin-Teodor <calin.chitu@8x8.com>
2025-05-28 15:29:32 +03:00
Saúl Ibarra Corretgé
7fabb33733 feat(android,ios) drop support for Firebase Dynamic Links
It's being sunset in August, after being deprecated for 2 years.

Ref: https://firebase.google.com/support/dynamic-links-faq?utm_source=deprecation_msa&utm_medium=email&utm_campaign=pone
2025-05-28 09:57:23 +03:00
damencho
5342712019 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1989.0.0+dcb588a2...v1991.0.0+80c2686a
2025-05-27 11:51:35 -05:00
Calin-Teodor
deb0e36f84 fix(settings): trying to access getMetadata() of undefined 2025-05-27 17:45:36 +03:00
Viktor-Asheim
65e3886d43 fix(config) fixed incorrect documentation about toolbar buttons 2025-05-26 14:31:29 +02:00
Calinteodor
337435e738 feat(conference): File sharing over conference (#16067)
* While making the file size limit configurable, we managed to have files dragged and dropped over the whole conference window.
2025-05-26 15:24:51 +03:00
damencho
7a1f5885d0 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1988.0.0+83c2ac30...v1989.0.0+dcb588a2
2025-05-24 15:09:52 -05:00
damencho
253f43ab9e fix(permissions): Adds an option to force-send permissions.
If backend modify permissions can force sending those on the initial presence.
2025-05-23 14:13:07 -05:00
damencho
f17a4387d9 feat(prosody): Check granted identity for recordings. 2025-05-23 11:58:28 -05:00
Saúl Ibarra Corretgé
7ac43abd03 fix(file-sharing) fix handling undefined metadata 2025-05-23 10:32:42 -05:00
Hristo Terezov
67b44f4406 fix(CC): Disable when config.transcription.enable=false
Disable the new CC tab when transcription.enable flag is false in config.
2025-05-22 20:13:43 -05:00
Saúl Ibarra Corretgé
7f601db349 feat(build,config) disable config whitelist in dev mode
Webpack will replace the code so the added condition because `if (true)`
in dev mode, which helps when one wants to override anything for
testing.
2025-05-22 16:43:31 +02:00
Saúl Ibarra Corretgé
26423f8e76 fix(file-sharing) rework sync
Rework sync so uploading multiple files at once or several moderators
uploading files simultaneously doesn't break synchronization.

The current room metadata plugin operates on <key,value> pairs and we
were using a generic "files" key and using a nested object as our value.
Since with every operation the entire object is replaced it's easy to
get out of sync because one needs to be sure to have the full state
before overwriting it.

This is not realistic.

We'll look into making the metadata plugin more flexible in order to
support add / delete operations also on nested objects, but for the time
being the following will suffice:

Use a key prefix, so each file has en entry in the room metadata, like
so: "files.<the file ID> -> file metadata". This means that when a file
is deleted we just empty the metadata. The metadata plugin doesn't
currently support removing existing keys.
2025-05-22 12:18:12 +02:00
Calinteodor
392d694563 feat(file-sharing): added author display name (#16059)
* feat(file-sharing): added shared file/files author display name
2025-05-22 12:52:43 +03:00
Hristo Terezov
107687583d doc(config): Add docs for transcription.disableClosedCaptions 2025-05-21 17:38:44 -05:00
daimoc
0085544a36 Fix lit error 2025-05-21 16:41:10 -05:00
daimoc
79cab9e7df Fix analytics loadscript call to match the new function signature 2025-05-21 16:41:10 -05:00
Zaid0412
1be9107ac7 Disable reactions in chat (#16029)
* feat: add disableReactionsInChat to config.js (default: false)

* fix: add override modifier to static contextType

Fixes #16028
2025-05-21 11:52:41 -05:00
Hristo Terezov
762d59a4ad chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1987.0.0+575f6738...v1988.0.0+83c2ac30
2025-05-21 11:39:45 -05:00
Calin-Teodor
b213c445de feat(file-sharing): sort shared files 2025-05-21 15:39:22 +03:00
Calin-Teodor
05079e5480 feat(file-sharing): ui fixes 2025-05-21 10:44:07 +03:00
damencho
b087b54575 fix(tests): Tests improvements. 2025-05-20 16:26:40 -05:00
Jaya Allamsetty
c586fd9592 fix(test) Add media checks for startMuted test. 2025-05-20 16:22:37 -04:00
Saúl Ibarra Corretgé
b966796d0c fix(file-sharing) don't mark upload as complete until response
Stay at 99% completion while we wait for the final reply to the request.
2025-05-20 15:09:48 +02:00
Saúl Ibarra Corretgé
734ab449a6 fix(local-recordings) back to WebM format, fix duration
After a lot of back and forth, WebM seems to be the only option we
really have. In terms of containers and codecs, here is the rundown:

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

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

So, we are back to where we started: WebM with VP8 and opus. But we need
to fix the duration in a potentially long file... the trick is to _only_
fix the duration. We can do that by inserting the right segment in the
metadata section. Something we cannot do without reading the whole file
is create cue points, but players like VLC seem to work well without
them.
2025-05-19 11:33:46 -05:00
Saúl Ibarra Corretgé
869a149ccd fix(file-sharing) fix file upload error handling 2025-05-19 15:07:42 +02:00
Saúl Ibarra Corretgé
90a831074e fix(file-sharing) fix styling on hover 2025-05-19 15:07:42 +02:00
Saúl Ibarra Corretgé
3be2a2d607 fix(file-sharing) don't pass token for downloads 2025-05-19 15:07:42 +02:00
Saúl Ibarra Corretgé
ec61563e8a fix(file-sharing) remove bogus error check 2025-05-19 15:07:42 +02:00
Matteo
8459e38b30 lang: Update Italian translation
-Implemented translation for file sharing
2025-05-17 06:59:18 -05:00
Saúl Ibarra Corretgé
916fd64178 fix(file-sharing) middleware is not async 2025-05-16 17:17:47 +02:00
Дамян Минков
34d7ff78c0 fix: Fix file-sharing swagger. 2025-05-16 09:03:21 -05:00
Saúl Ibarra Corretgé
a99e122801 feat(notifications) make sticky notifications duration configurable
Spot is non-interactive, so it will override a timeout.
2025-05-16 13:42:26 +02:00
Saúl Ibarra Corretgé
4735911b6c fix(spot) make Spot TV detection more resilient
Setting the UA string in Electron doesn't propagate the change to the
iframe where the meeting is loaded (🤦).

Thus make it more resilient by trying different things:

- A freshly introduced "iAmSpot" config option, similar to Jibri
- The app ID is present in the UA string, so we can test for that
- As a last-ditch effort, check if the display name is the default
  "Meeting Room"
2025-05-16 13:17:26 +02:00
Calin-Teodor
82f2060ebf feat(file-sharing) add support for file sharing
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
Co-authored-by: Lucian-Paul Torje <lucian-paul.torje@8x8.com>
2025-05-16 11:31:53 +02:00
damencho
0897db3efc feat(tests): Adds visitors go live test. 2025-05-15 21:09:11 -05:00
damencho
4d7d1a84bb feat(tests): Adds general visitors test. 2025-05-15 21:09:11 -05:00
damencho
9f133b3a28 feat(visitors): Disable stats broadcast for visitors.
This enables local stats for visitors, but disables just the broadcast of those stats.
2025-05-15 21:09:11 -05:00
Jaya Allamsetty
f24e02425f fix(conference) Mute user when startMuted policy update is received in conference meta data (#16025) 2025-05-15 19:38:23 -04:00
Edgars Voroboks
7d2771167a fix(lang): Update Latvian language translation 2025-05-15 14:38:19 -05:00
damencho
49fb2bbaa1 feat(av-moderation): Updates startMuted policy in metadata. 2025-05-15 12:56:30 -05:00
damencho
ca7ece97bd feat(av-moderation): Disable start muted settings when av moderation is on. 2025-05-15 12:48:55 -05:00
Saúl Ibarra Corretgé
0c68a1eb51 fix(ui) fix parsing typography tokens
They also need to be resolved from tokens.json, just like colors.
2025-05-15 16:51:35 +02:00
Saúl Ibarra Corretgé
8bab15d13a fix(recording) fix matching initiator
LJM will use either a JitsiParticipant object or a string for the
recording session initiator, handle both cases when checking if it's
ourselves.
2025-05-15 13:50:56 +02:00
damencho
914ace576a fix(breakout): Move visibility checks to the Security dialog.
This fixes some UI extra dividers shown. Disable password for breakout rooms.
2025-05-15 06:38:13 -05:00
damencho
9f80448f92 feat(metadata): Pushes metadata early before join. 2025-05-14 14:45:00 -05:00
Hristo Terezov
d5793c2445 Revert typography values in tokens to px from rem (#16026)
* Revert "feat(base/ui/native): Convert rem to px  (#15934)"

This reverts commit 057dc0e4d2.

* Revert "fix(StageParticipantNameLabel): size"

This reverts commit a01f4468a0.

* Revert "fix(subtitles): position part1"

This reverts commit 6c6ed8d7a8.

* Revert "fix(ITypographyType): wrong type of fontSize and lineHeight props"

This reverts commit bffcc9092b.

* revert(Tokens): font sizes and line heights back to px from rem

Turns out there are many places that does not expect rem. Temporary reverting this change from commit 6fa94b0bb4. We should bring it back along with proper handling of rem everywhere.
2025-05-13 18:34:25 -05:00
Hristo Terezov
805afd33d2 fix(CCTab): Reset start button state on error 2025-05-13 12:12:31 -05:00
Hristo Terezov
f22315cf92 fix(CC): Handle errors on start 2025-05-13 12:12:31 -05:00
Hristo Terezov
0ee2378ff0 feat(chat): Make chat panel resizeable 2025-05-12 16:34:09 -05:00
bgrozev
8157e528c3 fix(tests): Updated startMuted test (behaviour changed in jitsi/jicofo#1219. (#16019)
* Updated startMuted test (behaviour changed in jitsi/jicofo#1219.

* squash: more

---------

Co-authored-by: damencho <damencho@jitsi.org>
2025-05-12 16:24:58 -05:00
damencho
ca16f54dc9 feat(tests): Chat iframeApi tests. 2025-05-12 08:46:42 -05:00
Matteo
1825ab6c41 lang: Update Italian translation
- Translated 5 new strings
- Translated 2 edited strings
2025-05-09 15:12:24 -05:00
Hristo Terezov
3d5c08d86f ref(theme): revert spacing to number and not rem.
There are still many places where theme.spacing is expected to be number. Reverting parts of 057dc0e4d2 .
2025-05-09 13:45:56 -05:00
Hristo Terezov
f2babbf994 Revert "fix(subtitles): position part2"
This reverts commit 9361e3209b.
2025-05-09 13:45:56 -05:00
damencho
403b9043b6 feat(tests): Recording and live-streaming tests. 2025-05-08 13:29:58 -05:00
240 changed files with 8614 additions and 2916 deletions

View File

@@ -83,10 +83,8 @@ dependencies {
// Firebase
// - Crashlytics
// - Dynamic Links
implementation 'com.google.firebase:firebase-analytics:21.3.0'
implementation 'com.google.firebase:firebase-crashlytics:18.4.3'
implementation 'com.google.firebase:firebase-dynamic-links:21.1.0'
}
implementation project(':sdk')

View File

@@ -4,7 +4,6 @@ import android.net.Uri;
import android.util.Log;
import com.google.firebase.crashlytics.FirebaseCrashlytics;
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks;
import org.jitsi.meet.sdk.JitsiMeet;
import org.jitsi.meet.sdk.JitsiMeetActivity;
@@ -22,18 +21,6 @@ final class GoogleServicesHelper {
Log.d(activity.getClass().getSimpleName(), "Initializing Google Services");
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!JitsiMeet.isCrashReportingDisabled(activity));
FirebaseDynamicLinks.getInstance().getDynamicLink(activity.getIntent())
.addOnSuccessListener(activity, pendingDynamicLinkData -> {
Uri dynamicLink = null;
if (pendingDynamicLinkData != null) {
dynamicLink = pendingDynamicLinkData.getLink();
}
if (dynamicLink != null) {
activity.join(dynamicLink.toString());
}
});
}
}
}

View File

@@ -44,9 +44,6 @@ dependencies {
api "com.facebook.react:react-android:$rootProject.ext.rnVersion"
api "com.facebook.react:hermes-android:$rootProject.ext.rnVersion"
//noinspection GradleDynamicVersion
implementation 'org.webkit:android-jsc:+'
implementation 'com.facebook.fresco:animated-gif:2.5.0'
implementation 'com.dropbox.core:dropbox-core-sdk:4.0.1'
implementation 'com.jakewharton.timber:timber:5.0.1'

View File

@@ -10,13 +10,15 @@ package org.jitsi.meet.sdk;
* be found in the AUTHORS file in the root of the source tree.
*/
import android.media.MediaCodecInfo;
import androidx.annotation.Nullable;
import com.oney.WebRTCModule.webrtcutils.SoftwareVideoDecoderFactoryProxy;
import org.webrtc.EglBase;
import org.webrtc.HardwareVideoDecoderFactory;
import org.webrtc.PlatformSoftwareVideoDecoderFactory;
import org.webrtc.JitsiPlatformVideoDecoderFactory;
import org.webrtc.Predicate;
import org.webrtc.VideoCodecInfo;
import org.webrtc.VideoDecoder;
import org.webrtc.VideoDecoderFactory;
@@ -31,29 +33,34 @@ import java.util.LinkedHashSet;
public class JitsiVideoDecoderFactory implements VideoDecoderFactory {
private final VideoDecoderFactory hardwareVideoDecoderFactory;
private final VideoDecoderFactory softwareVideoDecoderFactory = new SoftwareVideoDecoderFactoryProxy();
private final @Nullable VideoDecoderFactory platformSoftwareVideoDecoderFactory;
private final VideoDecoderFactory platformSoftwareVideoDecoderFactory;
/**
* Predicate to filter out the AV1 hardware decoder, as we've seen decoding issues with it.
*/
private static final String GOOGLE_AV1_DECODER = "c2.google.av1";
private static final Predicate<MediaCodecInfo> hwCodecPredicate = arg -> {
// Filter out the Google AV1 codec.
return !arg.getName().startsWith(GOOGLE_AV1_DECODER);
};
private static final Predicate<MediaCodecInfo> swCodecPredicate = arg -> {
// Noop, just making sure we can customize it easily if needed.
return true;
};
/**
* Create decoder factory using default hardware decoder factory.
*/
public JitsiVideoDecoderFactory(@Nullable EglBase.Context eglContext) {
this.hardwareVideoDecoderFactory = new HardwareVideoDecoderFactory(eglContext);
this.platformSoftwareVideoDecoderFactory = new PlatformSoftwareVideoDecoderFactory(eglContext);
}
/**
* Create decoder factory using explicit hardware decoder factory.
*/
JitsiVideoDecoderFactory(VideoDecoderFactory hardwareVideoDecoderFactory) {
this.hardwareVideoDecoderFactory = hardwareVideoDecoderFactory;
this.platformSoftwareVideoDecoderFactory = null;
this.hardwareVideoDecoderFactory = new HardwareVideoDecoderFactory(eglContext, hwCodecPredicate);
this.platformSoftwareVideoDecoderFactory = new JitsiPlatformVideoDecoderFactory(eglContext, swCodecPredicate);
}
@Override
public @Nullable VideoDecoder createDecoder(VideoCodecInfo codecType) {
VideoDecoder softwareDecoder = softwareVideoDecoderFactory.createDecoder(codecType);
final VideoDecoder hardwareDecoder = hardwareVideoDecoderFactory.createDecoder(codecType);
if (softwareDecoder == null && platformSoftwareVideoDecoderFactory != null) {
if (softwareDecoder == null) {
softwareDecoder = platformSoftwareVideoDecoderFactory.createDecoder(codecType);
}
if (hardwareDecoder != null && softwareDecoder != null) {
@@ -70,10 +77,7 @@ public class JitsiVideoDecoderFactory implements VideoDecoderFactory {
supportedCodecInfos.addAll(Arrays.asList(softwareVideoDecoderFactory.getSupportedCodecs()));
supportedCodecInfos.addAll(Arrays.asList(hardwareVideoDecoderFactory.getSupportedCodecs()));
if (platformSoftwareVideoDecoderFactory != null) {
supportedCodecInfos.addAll(
Arrays.asList(platformSoftwareVideoDecoderFactory.getSupportedCodecs()));
}
supportedCodecInfos.addAll(Arrays.asList(platformSoftwareVideoDecoderFactory.getSupportedCodecs()));
return supportedCodecInfos.toArray(new VideoCodecInfo[supportedCodecInfos.size()]);
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2018 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.webrtc;
import android.media.MediaCodecInfo;
import androidx.annotation.Nullable;
/** Factory for Android platform software VideoDecoders. */
public class JitsiPlatformVideoDecoderFactory extends MediaCodecVideoDecoderFactory {
/**
* Default allowed predicate.
*/
private static final Predicate<MediaCodecInfo> defaultAllowedPredicate =
codecInfo -> {
// We only want to use the platform software codecs.
return MediaCodecUtils.isSoftwareOnly(codecInfo);
};
/**
* Creates a PlatformSoftwareVideoDecoderFactory that supports surface texture rendering.
*
* @param sharedContext The textures generated will be accessible from this context. May be null,
* this disables texture support.
*/
public JitsiPlatformVideoDecoderFactory(@Nullable EglBase.Context sharedContext) {
super(sharedContext, defaultAllowedPredicate);
}
public JitsiPlatformVideoDecoderFactory(@Nullable EglBase.Context sharedContext, @Nullable Predicate<MediaCodecInfo> codecAllowedPredicate) {
super(sharedContext, codecAllowedPredicate == null ? defaultAllowedPredicate : codecAllowedPredicate.and(defaultAllowedPredicate));
}
}

View File

@@ -3,7 +3,7 @@ rootProject.name = 'jitsi-meet'
include ':app', ':sdk'
include ':react-native-amplitude'
project(':react-native-amplitude').projectDir = new File(rootProject.projectDir, '../node_modules/@amplitude/react-native/android')
project(':react-native-amplitude').projectDir = new File(rootProject.projectDir, '../node_modules/@amplitude/analytics-react-native/android')
include ':react-native-async-storage'
project(':react-native-async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-async-storage/async-storage/android')
include ':react-native-background-timer'

View File

@@ -89,7 +89,7 @@ import {
setVideoMuted,
setVideoUnmutePermissions
} from './react/features/base/media/actions';
import { MEDIA_TYPE, VIDEO_TYPE } from './react/features/base/media/constants';
import { MEDIA_TYPE, VIDEO_MUTISM_AUTHORITY, VIDEO_TYPE } from './react/features/base/media/constants';
import {
getStartWithAudioMuted,
getStartWithVideoMuted,
@@ -131,7 +131,6 @@ import {
createLocalTracksF,
getLocalJitsiAudioTrack,
getLocalJitsiVideoTrack,
getLocalTracks,
getLocalVideoTrack,
isLocalTrackMuted,
isUserInteractionRequiredForUnmute
@@ -206,23 +205,6 @@ function sendData(command, value) {
room.sendCommand(command, { value });
}
/**
* Mute or unmute local audio stream if it exists.
* @param {boolean} muted - if audio stream should be muted or unmuted.
*/
function muteLocalAudio(muted) {
APP.store.dispatch(setAudioMuted(muted));
}
/**
* Mute or unmute local video stream if it exists.
* @param {boolean} muted if video stream should be muted or unmuted.
*
*/
function muteLocalVideo(muted) {
APP.store.dispatch(setVideoMuted(muted));
}
/**
* A queue for the async replaceLocalTrack action so that multiple audio
* replacements cannot happen simultaneously. This solves the issue where
@@ -709,11 +691,10 @@ export default {
* Simulates toolbar button click for audio mute. Used by shortcuts and API.
*
* @param {boolean} mute true for mute and false for unmute.
* @param {boolean} [showUI] when set to false will not display any error
* dialogs in case of media permissions error.
* @returns {Promise}
*/
async muteAudio(mute, showUI = true) {
async muteAudio(mute) {
const state = APP.store.getState();
if (!mute
@@ -732,47 +713,7 @@ export default {
return;
}
// Not ready to modify track's state yet
if (!this._localTracksInitialized) {
// This will only modify base/media.audio.muted which is then synced
// up with the track at the end of local tracks initialization.
muteLocalAudio(mute);
this.updateAudioIconEnabled();
return;
} else if (this.isLocalAudioMuted() === mute) {
// NO-OP
return;
}
const localAudio = getLocalJitsiAudioTrack(APP.store.getState());
if (!localAudio && !mute) {
const maybeShowErrorDialog = error => {
showUI && APP.store.dispatch(notifyMicError(error));
};
APP.store.dispatch(gumPending([ MEDIA_TYPE.AUDIO ], IGUMPendingState.PENDING_UNMUTE));
await createLocalTracksF({ devices: [ 'audio' ] })
.then(([ audioTrack ]) => audioTrack)
.catch(error => {
maybeShowErrorDialog(error);
// Rollback the audio muted status by using null track
return null;
})
.then(async audioTrack => {
await this._maybeApplyAudioMixerEffect(audioTrack);
return this.useAudioStream(audioTrack);
})
.finally(() => {
APP.store.dispatch(gumPending([ MEDIA_TYPE.AUDIO ], IGUMPendingState.NONE));
});
} else {
muteLocalAudio(mute);
}
await APP.store.dispatch(setAudioMuted(mute, true));
},
/**
@@ -802,10 +743,9 @@ export default {
/**
* Simulates toolbar button click for video mute. Used by shortcuts and API.
* @param mute true for mute and false for unmute.
* @param {boolean} [showUI] when set to false will not display any error
* dialogs in case of media permissions error.
*/
muteVideo(mute, showUI = true) {
muteVideo(mute) {
if (this.videoSwitchInProgress) {
logger.warn('muteVideo - unable to perform operations while video switch is in progress');
@@ -826,60 +766,7 @@ export default {
return;
}
// If not ready to modify track's state yet adjust the base/media
if (!this._localTracksInitialized) {
// This will only modify base/media.video.muted which is then synced
// up with the track at the end of local tracks initialization.
muteLocalVideo(mute);
this.setVideoMuteStatus();
return;
} else if (this.isLocalVideoMuted() === mute) {
// NO-OP
return;
}
const localVideo = getLocalJitsiVideoTrack(state);
if (!localVideo && !mute && !this.isCreatingLocalTrack) {
const maybeShowErrorDialog = error => {
showUI && APP.store.dispatch(notifyCameraError(error));
};
this.isCreatingLocalTrack = true;
APP.store.dispatch(gumPending([ MEDIA_TYPE.VIDEO ], IGUMPendingState.PENDING_UNMUTE));
// Try to create local video if there wasn't any.
// This handles the case when user joined with no video
// (dismissed screen sharing screen or in audio only mode), but
// decided to add it later on by clicking on muted video icon or
// turning off the audio only mode.
//
// FIXME when local track creation is moved to react/redux
// it should take care of the use case described above
createLocalTracksF({ devices: [ 'video' ] })
.then(([ videoTrack ]) => videoTrack)
.catch(error => {
// FIXME should send some feedback to the API on error ?
maybeShowErrorDialog(error);
// Rollback the video muted status by using null track
return null;
})
.then(videoTrack => {
logger.debug(`muteVideo: calling useVideoStream for track: ${videoTrack}`);
return this.useVideoStream(videoTrack);
})
.finally(() => {
this.isCreatingLocalTrack = false;
APP.store.dispatch(gumPending([ MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
});
} else {
// FIXME show error dialog if it fails (should be handled by react)
muteLocalVideo(mute);
}
APP.store.dispatch(setVideoMuted(mute, VIDEO_MUTISM_AUTHORITY.USER, true));
},
/**
@@ -1829,35 +1716,6 @@ export default {
onStartMutedPolicyChanged(audio, video));
}
);
room.on(JitsiConferenceEvents.STARTED_MUTED, () => {
const audioMuted = room.isStartAudioMuted();
const videoMuted = room.isStartVideoMuted();
const localTracks = getLocalTracks(APP.store.getState()['features/base/tracks']);
const promises = [];
APP.store.dispatch(setAudioMuted(audioMuted));
APP.store.dispatch(setVideoMuted(videoMuted));
// Remove the tracks from the peerconnection.
for (const track of localTracks) {
// Always add the track on Safari because of a known issue where audio playout doesn't happen
// if the user joins audio and video muted, i.e., if there is no local media capture.
if (audioMuted && track.jitsiTrack?.getType() === MEDIA_TYPE.AUDIO && !browser.isWebKitBased()) {
promises.push(this.useAudioStream(null));
}
if (videoMuted && track.jitsiTrack?.getType() === MEDIA_TYPE.VIDEO) {
promises.push(this.useVideoStream(null));
}
}
Promise.allSettled(promises)
.then(() => {
APP.store.dispatch(showNotification({
titleKey: 'notify.mutedTitle',
descriptionKey: 'notify.muted'
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
});
});
room.on(
JitsiConferenceEvents.DATA_CHANNEL_OPENED, () => {

View File

@@ -117,6 +117,11 @@ var config = {
// Will replace ice candidates IPs with invalid ones in order to fail ice.
// failICE: true,
// When running on Spot TV, this controls whether to show the recording consent dialog.
// If false (default), Spot instances will not show the recording consent dialog.
// If true, Spot instances will show the recording consent dialog like regular clients.
// showSpotConsentDialog: false,
},
// Disables moderator indicators.
@@ -128,6 +133,9 @@ var config = {
// Disables the reactions moderation feature.
// disableReactionsModeration: false,
// Disables the reactions in chat feature.
// disableReactionsInChat: false,
// Disables polls feature.
// disablePolls: false,
@@ -502,6 +510,15 @@ var config = {
// // Enables automatic request of subtitles when transcriber is present in the meeting, uses the default
// // language that is set
// autoCaptionOnTranscribe: false,
//
// // Disables everything related to closed captions - the tab in the chat area, the button in the menu,
// // subtitles on stage and the "Show subtitles on stage" checkbox in the settings.
// // 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
@@ -613,6 +630,7 @@ var config = {
// medium: 5000,
// long: 10000,
// extraLong: 60000,
// sticky: 0,
// },
// // Options for the recording limit notification.
@@ -830,8 +848,7 @@ var config = {
// some other values in config.js to be enabled. Also, the "profile" button will
// not display for users with a JWT.
// Notes:
// - it's impossible to choose which buttons go in the "More actions" menu
// - it's impossible to control the placement of buttons
// - it's possible to reorder the buttons in the maintoolbar by changing the order of the mainToolbarButtons
// - 'desktop' controls the "Share your screen" button
// - if `toolbarButtons` is undefined, we fallback to enabling all buttons on the UI
// toolbarButtons: [
@@ -1114,10 +1131,6 @@ var config = {
// The Amplitude APP Key:
// amplitudeAPPKey: '<APP_KEY>',
// Enables Amplitude UTM tracking:
// Default value is false.
// amplitudeIncludeUTM: false,
// Obfuscates room name sent to analytics (amplitude, rtcstats)
// Default value is false.
// obfuscateRoomName: false,
@@ -1261,9 +1274,6 @@ var config = {
// disableDeepLinking: false,
// The deeplinking config.
// For information about the properties of
// deeplinking.[ios/android].dynamicLink check:
// https://firebase.google.com/docs/dynamic-links/create-manually
// deeplinking: {
//
// // The desktop deeplinking config, disabled by default.
@@ -1292,13 +1302,6 @@ var config = {
// appScheme: 'org.jitsi.meet',
// // Custom URL for downloading ios mobile app.
// downloadLink: 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905',
// dynamicLink: {
// apn: 'org.jitsi.meet',
// appCode: 'w2atb',
// customDomain: undefined,
// ibi: 'com.atlassian.JitsiMeet.ios',
// isi: '1165103905'
// }
// },
// // The android deeplinking config.
@@ -1311,13 +1314,6 @@ var config = {
// // Android app package name.
// appPackage: 'org.jitsi.meet',
// fDroidUrl: 'https://f-droid.org/en/packages/org.jitsi.meet/',
// dynamicLink: {
// apn: 'org.jitsi.meet',
// appCode: 'w2atb',
// customDomain: undefined,
// ibi: 'com.atlassian.JitsiMeet.ios',
// isi: '1165103905'
// }
// }
// },
@@ -1365,18 +1361,11 @@ var config = {
// disableKick: true,
// // If set to true the 'Grant moderator' button will be disabled.
// disableGrantModerator: true,
// // If set to true the 'Send private message' button will be disabled.
// disablePrivateChat: true,
// // If set to 'all' the 'Private chat' button will be disabled for all participants.
// // If set to 'allow-moderator-chat' the 'Private chat' button will be available for chats with moderators.
// disablePrivateChat: 'all' | 'allow-moderator-chat',
// },
// Endpoint that enables support for salesforce integration with in-meeting resource linking
// This is required for:
// listing the most recent records - salesforceUrl/records/recents
// searching records - salesforceUrl/records?text=${text}
// retrieving record details - salesforceUrl/records/${id}?type=${type}
// and linking the meeting - salesforceUrl/sessions/${sessionId}/records/${id}
//
// salesforceUrl: 'https://api.example.com/',
// If set to true all muting operations of remote participants will be disabled.
// disableRemoteMute: true,
@@ -1401,6 +1390,13 @@ var config = {
logoClickUrl: 'https://example-company.org',
// The url used for the image used as logo
logoImageUrl: 'https://example.com/logo-img.png',
// Endpoint that enables support for salesforce integration with in-meeting resource linking
// This is required for:
// listing the most recent records - salesforceUrl/records/recents
// searching records - salesforceUrl/records?text=${text}
// retrieving record details - salesforceUrl/records/${id}?type=${type}
// and linking the meeting - salesforceUrl/sessions/${sessionId}/records/${id}
// salesforceUrl: 'https://api.example.com/',
// Overwrite for pool of background images for avatars
avatarBackgrounds: ['url(https://example.com/avatar-background-1.png)', '#FFF'],
// The lobby/prejoin screen background
@@ -1582,6 +1578,9 @@ var config = {
// tokenAuthUrlAutoRedirect: false
// An option to respect the context.tenant jwt field compared to the current tenant from the url
// tokenRespectTenant: false,
// An option to get for user info (name, picture, email) in the token outside the user context.
// Can be used with Firebase tokens.
// tokenGetUserInfoOutOfContext: false,
// You can put an array of values to target different entity types in the invite dialog.
// Valid values are "phone", "room", "sip", "user", "videosipgw" and "email"
@@ -1781,6 +1780,13 @@ var config = {
// // The minimum number of participants that must be in the call for
// // the top panel layout to be used.
// minParticipantCountForTopPanel: 50,
// // The width of the filmstrip on joining meeting. Can be resized afterwards.
// initialWidth: 400,
// // Whether the draggable resize bar of the filmstrip is always visible. Setting this to true will make
// // the filmstrip always visible in case `disableResizable` is false.
// alwaysShowResizeBar: true,
// },
// Tile view related config options.
@@ -1882,6 +1888,16 @@ var config = {
// If true remove the tint foreground on focused user camera in filmstrip
// disableCameraTintForeground: false,
// File sharign service.
// fileSharing: {
// // The URL of the file sharing service API. See resources/file-sharing.yaml for more details.
// apiUrl: 'https://example.com',
// // Whether the file sharing service is enabled or not.
// enabled: true,
// // Maximum file size limit (-1 value disables any file size limit check)
// maxFileSize: 50,
// },
};
// Set the default values for JaaS customers

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -58,28 +58,21 @@ VirtualHost "jitmeet.example.com"
key = "/etc/prosody/certs/jitmeet.example.com.key";
certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
}
av_moderation_component = "avmoderation.jitmeet.example.com"
speakerstats_component = "speakerstats.jitmeet.example.com"
end_conference_component = "endconference.jitmeet.example.com"
-- we need bosh
modules_enabled = {
"bosh";
"websocket";
"smacks";
"ping"; -- Enable mod_ping
"speakerstats";
"external_services";
"features_identity";
"conference_duration";
"end_conference";
"muc_lobby_rooms";
"muc_breakout_rooms";
"av_moderation";
"room_metadata";
}
c2s_require_encryption = false
lobby_muc = "lobby.jitmeet.example.com"
breakout_rooms_muc = "breakout.jitmeet.example.com"
room_metadata_component = "metadata.jitmeet.example.com"
main_muc = "conference.jitmeet.example.com"
-- muc_lobby_whitelist = { "recorder.jitmeet.example.com" } -- Here we can whitelist jibri to enter lobby enabled rooms
@@ -155,6 +148,9 @@ Component "endconference.jitmeet.example.com" "end_conference"
Component "avmoderation.jitmeet.example.com" "av_moderation_component"
muc_component = "conference.jitmeet.example.com"
Component "filesharing.jitmeet.example.com" "filesharing_component"
muc_component = "conference.jitmeet.example.com"
Component "lobby.jitmeet.example.com" "muc"
storage = "memory"
restrict_room_creation = true

View File

@@ -192,17 +192,6 @@ var interfaceConfig = {
// NATIVE_APP_NAME: 'Jitsi Meet',
/**
* Specify Firebase dynamic link properties for the mobile apps.
*/
// MOBILE_DYNAMIC_LINK: {
// APN: 'org.jitsi.meet',
// APP_CODE: 'w2atb',
// CUSTOM_DOMAIN: undefined,
// IBI: 'com.atlassian.JitsiMeet.ios',
// ISI: '1165103905'
// },
/**
* Hide the logo on the deep linking pages.
*/

View File

@@ -19,7 +19,6 @@ target 'JitsiMeet' do
pod 'Firebase/Analytics', '~> 8.0'
pod 'Firebase/Crashlytics', '~> 8.0'
pod 'Firebase/DynamicLinks', '~> 8.0'
end
target 'JitsiMeetSDK' do

View File

@@ -1,15 +1,11 @@
PODS:
- Amplitude (8.18.0):
- AnalyticsConnector (~> 1.0.0)
- amplitude-react-native (2.17.3):
- Amplitude (= 8.18.0)
- amplitude-react-native (1.4.13):
- React-Core
- AnalyticsConnector (1.0.3)
- AppAuth (1.7.5):
- AppAuth/Core (= 1.7.5)
- AppAuth/ExternalUserAgent (= 1.7.5)
- AppAuth/Core (1.7.5)
- AppAuth/ExternalUserAgent (1.7.5):
- AppAuth (1.7.6):
- AppAuth/Core (= 1.7.6)
- AppAuth/ExternalUserAgent (= 1.7.6)
- AppAuth/Core (1.7.6)
- AppAuth/ExternalUserAgent (1.7.6):
- AppAuth/Core
- boost (1.84.0)
- CocoaLumberjack (3.7.4):
@@ -27,9 +23,6 @@ PODS:
- Firebase/Crashlytics (8.15.0):
- Firebase/CoreOnly
- FirebaseCrashlytics (~> 8.15.0)
- Firebase/DynamicLinks (8.15.0):
- Firebase/CoreOnly
- FirebaseDynamicLinks (~> 8.15.0)
- FirebaseAnalytics (8.15.0):
- FirebaseAnalytics/AdIdSupport (= 8.15.0)
- FirebaseCore (~> 8.0)
@@ -64,8 +57,6 @@ PODS:
- GoogleUtilities/Environment (~> 7.7)
- nanopb (~> 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- FirebaseDynamicLinks (8.15.0):
- FirebaseCore (~> 8.0)
- FirebaseInstallations (8.15.0):
- FirebaseCore (~> 8.0)
- GoogleUtilities/Environment (~> 7.7)
@@ -141,18 +132,18 @@ PODS:
- hermes-engine (0.75.5):
- hermes-engine/Pre-built (= 0.75.5)
- hermes-engine/Pre-built (0.75.5)
- JitsiWebRTC (124.0.1)
- libwebp (1.3.2):
- libwebp/demux (= 1.3.2)
- libwebp/mux (= 1.3.2)
- libwebp/sharpyuv (= 1.3.2)
- libwebp/webp (= 1.3.2)
- libwebp/demux (1.3.2):
- JitsiWebRTC (124.0.2)
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
- libwebp/sharpyuv (= 1.5.0)
- libwebp/webp (= 1.5.0)
- libwebp/demux (1.5.0):
- libwebp/webp
- libwebp/mux (1.3.2):
- libwebp/mux (1.5.0):
- libwebp/demux
- libwebp/sharpyuv (1.3.2)
- libwebp/webp (1.3.2):
- libwebp/sharpyuv (1.5.0)
- libwebp/webp (1.5.0):
- libwebp/sharpyuv
- nanopb (2.30908.0):
- nanopb/decode (= 2.30908.0)
@@ -1791,14 +1782,13 @@ PODS:
- Yoga (0.0.0)
DEPENDENCIES:
- "amplitude-react-native (from `../node_modules/@amplitude/react-native`)"
- "amplitude-react-native (from `../node_modules/@amplitude/analytics-react-native`)"
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
- CocoaLumberjack (= 3.7.4)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- Firebase/Analytics (~> 8.0)
- Firebase/Crashlytics (~> 8.0)
- Firebase/DynamicLinks (~> 8.0)
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
- "giphy-react-native-sdk (from `../node_modules/@giphy/react-native-sdk`)"
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
@@ -1889,8 +1879,6 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- Amplitude
- AnalyticsConnector
- AppAuth
- CocoaLumberjack
- Firebase
@@ -1898,7 +1886,6 @@ SPEC REPOS:
- FirebaseCore
- FirebaseCoreDiagnostics
- FirebaseCrashlytics
- FirebaseDynamicLinks
- FirebaseInstallations
- Giphy
- GoogleAppMeasurement
@@ -1917,7 +1904,7 @@ SPEC REPOS:
EXTERNAL SOURCES:
amplitude-react-native:
:path: "../node_modules/@amplitude/react-native"
:path: "../node_modules/@amplitude/analytics-react-native"
boost:
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
DoubleConversion:
@@ -2095,10 +2082,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
Amplitude: 184def4f87aa26f94a93a7faa334e06b1cae704d
amplitude-react-native: 6b7a1d30627233fe6f03741109831561d0a5f69c
AnalyticsConnector: a53214d38ae22734c6266106c0492b37832633a9
AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa
amplitude-react-native: d0039a3ce502eb441ba818af1b8c8ba710ef16e7
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
boost: 4cb898d0bf20404aab1850c656dcea009429d6c1
CocoaLumberjack: 543c79c114dadc3b1aba95641d8738b06b05b646
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
@@ -2108,7 +2093,6 @@ SPEC CHECKSUMS:
FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1
FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb
FirebaseCrashlytics: feb07e4e9187be3c23c6a846cce4824e5ce2dd0b
FirebaseDynamicLinks: 1dc816ef789c5adac6fede0b46d11478175c70e4
FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
Giphy: 83628960ed04e1c3428ff1b4fb2b027f65e82f50
@@ -2121,8 +2105,8 @@ SPEC CHECKSUMS:
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
hermes-engine: c9fe5870af65876125fdbbf833071b6f329db30d
JitsiWebRTC: d0ae5fd6a81e771bfd82c2ee6c6bb542ebd65ee8
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
JitsiWebRTC: b47805ab5668be38e7ee60e2258f49badfe8e1d0
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
ObjectiveDropboxOfficial: fe206ce8c0bc49976c249d472db7fdbc53ebbd53
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
@@ -2209,6 +2193,6 @@ SPEC CHECKSUMS:
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
Yoga: 1dd9dabb9df8fe08f12cd522eae04a2da0e252eb
PODFILE CHECKSUM: 4f6abcf3cec0d9e8e1d5f5d81a35d99adde9ae45
PODFILE CHECKSUM: 8ff981f2324fa4b013b6690257898dcd2be9d3c3
COCOAPODS: 1.16.2

View File

@@ -539,7 +539,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if test \"$PRODUCT_BUNDLE_IDENTIFIER\" = \"com.atlassian.JitsiMeet.ios\"; then\n ENTITLEMENTS_PLIST=\"$PROJECT_DIR/app.entitlements\"\n \n /usr/libexec/PlistBuddy -c \"Add :com.apple.developer.avfoundation.multitasking-camera-access bool 1\" $ENTITLEMENTS_PLIST\nfi\n";
shellScript = "#if test \"$PRODUCT_BUNDLE_IDENTIFIER\" = \"com.atlassian.JitsiMeet.ios\"; then\n# ENTITLEMENTS_PLIST=\"$PROJECT_DIR/app.entitlements\"\n# \n# /usr/libexec/PlistBuddy -c \"Add :com.apple.developer.avfoundation.multitasking-camera-access bool 1\" $ENTITLEMENTS_PLIST\n#fi\n";
};
69BC5020DBE393B56BD76636 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;

View File

@@ -54,19 +54,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// MARK: Linking delegate methods
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
if self.appContainsRealServiceInfoPlist() {
let handled = DynamicLinks.dynamicLinks().handleUniversalLink(userActivity.webpageURL!) { dynamicLink, error in
if let firebaseUrl = self.extractURL(from: dynamicLink) {
userActivity.webpageURL = firebaseUrl
JitsiMeet.sharedInstance().application(application, continue: userActivity, restorationHandler: restorationHandler)
}
}
if handled {
return handled
}
}
return JitsiMeet.sharedInstance().application(application, continue: userActivity, restorationHandler: restorationHandler)
}
@@ -75,16 +62,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
return false
}
var openUrl = url
if self.appContainsRealServiceInfoPlist() {
if let dynamicLink = DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url),
let firebaseUrl = self.extractURL(from: dynamicLink) {
openUrl = firebaseUrl
}
}
return JitsiMeet.sharedInstance().application(app, open: openUrl, options: options)
return JitsiMeet.sharedInstance().application(app, open: url, options: options)
}
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
@@ -97,13 +75,4 @@ extension AppDelegate {
func appContainsRealServiceInfoPlist() -> Bool {
return InfoPlistUtil.containsRealServiceInfoPlist(in: Bundle.main)
}
func extractURL(from dynamicLink: DynamicLink?) -> URL? {
guard let dynamicLink = dynamicLink,
let dynamicLinkURL = dynamicLink.url,
dynamicLink.matchType == .unique || dynamicLink.matchType == .default else {
return nil
}
return dynamicLinkURL
}
}

View File

@@ -109,6 +109,7 @@
}
},
"chat": {
"disabled": "L'invio di messaggi in chat è disabilitato.",
"enter": "Entra nella conversazione",
"error": "Errore: il tuo messaggio non è stato inviato. Motivo: {{error}}",
"fieldPlaceHolder": "Scrivi qui il tuo messaggio",
@@ -124,7 +125,8 @@
"title": "Inserisci un nickname per usare la chat",
"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"
"titleWithPollsAndCC": "Inserisci un nickname per usare la chat, i sondaggi e i sottotitoli",
"titleWithPollsAndCCAndFileSharing": "Inserisci un nickname per usare la chat, i sondaggi, i sottotitoli e i file"
},
"noMessagesMessage": "Non ci sono ancora messaggi nella riunione. Inizia una conversazione, qui!",
"privateNotice": "Messaggio privato a {{recipient}}",
@@ -134,12 +136,14 @@
"tabs": {
"chat": "Chat",
"closedCaptions": "Sottotitoli",
"fileSharing": "File",
"polls": "Sondaggi"
},
"title": "Chat",
"titleWithCC": "Chat e sottotitoli",
"titleWithPolls": "Chat e sondaggi",
"titleWithPollsAndCC": "Chat, sondaggi e sottotitoli",
"titleWithCC": "Sottotitoli",
"titleWithFeatures": "Chat e",
"titleWithFileSharing": "File",
"titleWithPolls": "Sondaggi",
"you": "tu"
},
"chromeExtensionBanner": {
@@ -272,7 +276,8 @@
"Remove": "Rimuovi",
"Share": "Condividi",
"Submit": "Invia",
"Understand": "Accetto",
"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",
@@ -309,6 +314,7 @@
"conferenceReloadMsg": "Risoluzione del problema in corso. Tentativo di riconnessione tra {{seconds}} secondi…",
"conferenceReloadTitle": "Purtroppo qualcosa è andato storto.",
"confirm": "Conferma",
"confirmBack": "Indietro",
"confirmNo": "No",
"confirmYes": "Sì",
"connectError": "Ops! Qualcosa è andato storto e non è stato possibile collegarsi alla riunione.",
@@ -346,6 +352,7 @@
"kickParticipantTitle": "Espellere questo partecipante?",
"kickSystemTitle": "Oh! Sei stato espulso dalla riunione",
"kickTitle": "Oh! {{participantDisplayName}} ti ha espulso dalla riunione.",
"learnMore": "ulteriori informazioni",
"linkMeeting": "Collega la riunione",
"linkMeetingTitle": "Collega la riunione a Salesforce",
"liveStreaming": "Diretta",
@@ -403,7 +410,9 @@
"recentlyUsedObjects": "Oggetti usati di recente",
"recording": "Registrazione",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Impossibile durante una diretta.",
"recordingInProgressDescription": "La registrazione di questa riunione è in corso. I tuoi audio e video sono stati disattivati. Riattivandoli, acconsenti ad essere registrato.",
"recordingInProgressDescription": "Questa riunione sta venendo registrata e analizzata dall'IA. I tuoi audio e video sono stati disattivati. Riattivandoli, acconsenti ad essere registrato.",
"recordingInProgressDescriptionFirstHalf": "Questa riunione sta venendo registrata e analizzata dall'IA",
"recordingInProgressDescriptionSecondHalf": ". I tuoi audio e video sono stati disattivati. Riattivandoli, acconsenti ad essere registrato.",
"recordingInProgressTitle": "Registrazione in corso",
"rejoinNow": "Ricollegati ora",
"remoteControlAllowedMessage": "{{user}} ha accettato la tua richiesta di controllo remoto!",
@@ -534,6 +543,19 @@
"veryBad": "Pessima",
"veryGood": "Ottima"
},
"fileSharing": {
"downloadFailedDescription": "Si prega di riprovare.",
"downloadFailedTitle": "Download non riuscito",
"downloadFile": "Download",
"dragAndDrop": "Trascina e rilascia i file qui o da qualsiasi altra parte nella schermata",
"fileAlreadyUploaded": "Questo file è già stato caricato nella riunione.",
"fileTooLargeDescription": "Assicurati che il file non superi {{ maxFileSize }}.",
"fileTooLargeTitle": "Il file selezionato è troppo grande",
"removeFile": "Rimuovi",
"uploadFailedDescription": "Si prega di riprovare.",
"uploadFailedTitle": "Caricamento non riuscito",
"uploadFile": "Condividi file"
},
"filmstrip": {
"accessibilityLabel": {
"heading": "Miniatura video"
@@ -887,6 +909,7 @@
"visitorInQueue": " ({{count}} in attesa)",
"visitorRequests": " ({{count}} richiesta/e)",
"visitors": "Spettatori {{count}}",
"visitorsList": "Spettatori ({{count}})",
"waitingLobby": "({{count}}) in attesa"
},
"search": "Cerca partecipanti",
@@ -927,7 +950,7 @@
},
"results": {
"changeVote": "Cambia voto",
"empty": "Non ci sono ancora sondaggi in questa riunione. Crea un sondaggio qui!",
"empty": "Non ci sono ancora sondaggi in questa riunione.",
"hideDetailedResults": "Nascondi dettagli",
"showDetailedResults": "Mostra dettagli",
"vote": "Voti"

View File

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

View File

@@ -67,13 +67,18 @@
"renameBreakoutRoom": "重命名分组讨论室",
"sendToBreakoutRoom": "将参会者移至:"
},
"defaultName": "分组讨论室#{{index}}",
"breakoutList": "分组讨论室列表",
"buttonLabel": "分组讨论室",
"defaultName": "分组讨论室 #{{index}}",
"hideParticipantList": "隐藏成员列表",
"mainRoom": "主会议室",
"notifications": {
"joined": "正在加入“{{name}}”分组讨论室",
"joinedMainRoom": "正在加入主会议室",
"joinedTitle": "分组讨论室"
}
},
"showParticipantList": "显示成员列表",
"title": "分组讨论室"
},
"calendarSync": {
"addMeetingURL": "添加会议链接",
@@ -104,10 +109,11 @@
}
},
"chat": {
"disabled": "聊天已禁用",
"enter": "加入会议室",
"error": "错误:你的消息未发送。原因:{{error}}",
"fieldPlaceHolder": "在这里输入你的信息",
"lobbyChatMessageTo": "大厅聊天消息发送至{{recipient}}",
"lobbyChatMessageTo": "等候室聊天消息发送至{{recipient}}",
"message": "信息",
"messageAccessibleTitle": "{{user}}",
"messageAccessibleTitleMe": "我:",
@@ -116,19 +122,28 @@
"newMessages": "新信息",
"nickname": {
"popover": "选择一个昵称",
"title": "输入一个昵称用于聊天",
"titleWithPolls": "输入一个昵称用于聊天和投票"
"title": "输入昵称用于聊天",
"titleWithCC": "输入昵称用于聊天和字幕)",
"titleWithPolls": "输入昵称(用于聊天和投票)",
"titleWithPollsAndCC": "输入昵称(用于聊天、投票和字幕)",
"titleWithPollsAndCCAndFileSharing": "输入昵称(用于聊天、投票、字幕和文件共享)"
},
"noMessagesMessage": "会议中还没有消息,在这里开始谈话吧!",
"privateNotice": "与{{recipient}}的私聊",
"sendButton": "发送",
"smileysPanel": "表情符号面板",
"systemDisplayName": "系統",
"tabs": {
"chat": "聊天",
"closedCaptions": "字幕",
"fileSharing": "文件共享",
"polls": "投票"
},
"title": "聊天",
"titleWithPolls": "聊天和投票",
"titleWithCC": "字幕",
"titleWithFeatures": "聊天和",
"titleWithFileSharing": "文件",
"titleWithPolls": "投票",
"you": "你"
},
"chromeExtensionBanner": {
@@ -138,6 +153,10 @@
"dontShowAgain": "不要再问我了",
"installExtensionText": "安装用于Google日历和Office 365集成的扩展插件"
},
"closedCaptionsTab": {
"emptyState": "字幕将在主持人开启后显示",
"startClosedCaptionsButton": "开启字幕"
},
"connectingOverlay": {
"joiningRoom": "连接会议中……"
},
@@ -206,7 +225,7 @@
"descriptionNew": "没反应?我们已经尝试在{{app}}的电脑应用程序中加入会议。<br /><br />你可以重试,或在网页端中启动。",
"descriptionWithoutWeb": "没反应?我们已经尝试在{{app}}的电脑应用程序中加入会议。",
"downloadApp": "下载APP",
"downloadMobileApp": "App Store下载",
"downloadMobileApp": "前往App Store下载",
"ifDoNotHaveApp": "如果你还没有这个APP",
"ifHaveApp": "如果你已经拥有该APP",
"joinInApp": "使用APP加入此会议",
@@ -214,7 +233,9 @@
"joinInBrowser": "在浏览器中加入",
"launchMeetingLabel": "你希望如何加入此会议?",
"launchWebButton": "在网页中启动",
"noMobileApp": "还没安装APP",
"noDesktopApp": "还没安装桌面客户端",
"noMobileApp": "还没有安装手机App",
"or": "或",
"termsAndConditions": "继续操作即表示你同意我们的<a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>条款和条件。</a>",
"title": "正在{{app}}中加入会议……",
"titleNew": "正在启动你的会议……",
@@ -250,12 +271,16 @@
"dialog": {
"Back": "返回",
"Cancel": "取消",
"IamHost": "我是主持",
"IamHost": "登录主持",
"Ok": "确定",
"Remove": "移除",
"Share": "分享",
"Submit": "提交",
"WaitForHostMsg": "会议尚未开始。如果你是主持人,请进行身份验证。否则,请等待主持人加入。",
"Understand": "我已知晓,暂时保持静音",
"UnderstandAndUnmute": "我已知晓,请为我解除静音",
"WaitForHostMsg": "会议尚未开始,主持人还未入会。如需成为主持人请先登录,或耐心等待会议开始。",
"WaitForHostNoAuthMsg": "会议尚未开始,暂无主持人入会,请耐心等待",
"WaitingForHostButton": "等待主持人",
"WaitingForHostTitle": "正在等待主持人加入……",
"Yes": "是",
"accessibilityLabel": {
@@ -269,6 +294,8 @@
"addMeetingNote": "添加本次会议的备注",
"addOptionalNote": "添加备注(可选):",
"allow": "允许",
"allowToggleCameraDialog": "你是否允许{{initiatorName}}切换你的摄像头?",
"allowToggleCameraTitle": "允许切换摄像头?",
"alreadySharedVideoMsg": "其他参会者正在分享视频,本次会议仅支持同时分享一个视频。",
"alreadySharedVideoTitle": "同一时间只允许一个视频分享",
"applicationWindow": "应用程序窗口",
@@ -287,6 +314,7 @@
"conferenceReloadMsg": "我们正在努力修复此问题,将在{{seconds}}秒后重新连接……",
"conferenceReloadTitle": "糟了,好像有什么东西出错了。",
"confirm": "确认",
"confirmBack": "返回",
"confirmNo": "否",
"confirmYes": "是",
"connectError": "发生错误,无法连接至会议!",
@@ -295,19 +323,22 @@
"contactSupport": "联系支持",
"copied": "已复制",
"copy": "复制",
"demoteParticipantDialog": "确定将该成员调整为观众吗?",
"demoteParticipantTitle": "调整为观众",
"dismiss": "忽略",
"displayNameRequired": "嗨!你叫什么名字?",
"done": "完成",
"e2eeDescription": "请注意端到端加密目前处于实验阶段开启端到端加密将禁用部分服务器端提供的服务例如通过电话加入会议。另外通过网页版加入会议还需要使用支持Insertable Stream的浏览器。",
"e2eeDisabledDueToMaxModeDescription": "由于会议中的人数过多,无法开启端到端加密。",
"e2eeLabel": "开启端到端加密",
"e2eeWarning": "警告:本次会议中并不是所有的参会者都支持端到端加密。如果启它,他们将无法看到或听到你。",
"e2eeWarning": "警告:本次会议中并不是所有的参会者都支持端到端加密。如果启它,他们将无法看到或听到你。",
"e2eeWillDisableDueToMaxModeDescription": "警告:如果有更多人加入会议,端到端加密将自动禁用。",
"embedMeeting": "嵌入会议",
"enterDisplayName": "请在此输入你的名",
"enterDisplayName": "请输入你的名",
"error": "错误",
"errorRoomCreationRestriction": "加入过于频繁,请稍后再试。",
"gracefulShutdown": "我们的服务目前正在维护中,请稍后再试。",
"grantModeratorDialog": "确定要授予{{participantName}}主持人权限吗?",
"grantModeratorDialog": "确定{{participantName}}设为主持人吗?",
"grantModeratorTitle": "授予主持人权限",
"hide": "隐藏",
"hideShareAudioHelper": "不要再显示",
@@ -319,9 +350,11 @@
"kickParticipantButton": "移除",
"kickParticipantDialog": "你确定你要移除这位参会者吗?",
"kickParticipantTitle": "移除这位参会者?",
"kickSystemTitle": "你已被移出会议",
"kickTitle": "{{participantDisplayName}}将你从会议中移除",
"learnMore": "了解详情",
"linkMeeting": "关联会议",
"linkMeetingTitle": "将会议链接到Salesforce",
"linkMeetingTitle": "议关联到Salesforce",
"liveStreaming": "直播中",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "正在录制中,无法开启",
"localUserControls": "本地用户控制",
@@ -377,6 +410,10 @@
"recentlyUsedObjects": "你最近使用的对象",
"recording": "录制中",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "直播时无法使用",
"recordingInProgressDescription": "本会议正由AI录制并分析{{learnMore}},你已被静音。如需发言,视为同意录制。",
"recordingInProgressDescriptionFirstHalf": "本会议正由AI录制并分析",
"recordingInProgressDescriptionSecondHalf": ",你已被静音。如需发言,视为同意录制。",
"recordingInProgressTitle": "录制中",
"rejoinNow": "马上重新加入",
"remoteControlAllowedMessage": "{{user}}接受了你的远程控制请求!",
"remoteControlDeniedMessage": "{{user}}拒绝了你的远程控制请求!",
@@ -413,6 +450,7 @@
"sessTerminatedReason": "会议已经结束",
"sessionRestarted": "由于连接问题,呼叫重新启动。",
"shareAudio": "继续",
"shareAudioAltText": "如需分享内容,请切换到“浏览器标签页”,勾选“分享音频”并点击“分享”按钮",
"shareAudioTitle": "如何分享音频",
"shareAudioWarningD1": "你需要在分享你的音频之前停止共享屏幕。",
"shareAudioWarningD2": "你需要重新启动你的共享屏幕并勾选“共享音频”。",
@@ -423,7 +461,10 @@
"shareScreenWarningD2": "你需要停止音频共享后,重新启动你的共享屏幕并勾选“共享音频”。",
"shareScreenWarningH1": "如果你只想共享屏幕:",
"shareScreenWarningTitle": "在共享你的屏幕之前,你需要停止共享音频",
"shareVideoConfirmPlay": "即将打开外部网站,是否继续?",
"shareVideoConfirmPlayTitle": "需先停止音频共享才能分享屏幕",
"shareVideoLinkError": "请提供正确的视频链接。",
"shareVideoLinkStopped": "{{name}}向你分享了一个视频",
"shareVideoTitle": "分享视频",
"shareYourScreen": "共享你的屏幕",
"shareYourScreenDisabled": "共享屏幕已禁用。",
@@ -474,6 +515,10 @@
"viewUpgradeOptions": "查看升级选项",
"viewUpgradeOptionsContent": "要获取无限制的高级功能如录制、转录、RTMP 流等,请升级你的计划。",
"viewUpgradeOptionsTitle": "你发现了高级版功能!",
"whiteboardLimitContent": "同时在线白板人数已达上限,暂时无法继续使用",
"whiteboardLimitReference": "详情请访问",
"whiteboardLimitReferenceUrl": "我们的网站",
"whiteboardLimitTitle": "白板人数受限",
"yourEntireScreen": "你的整个屏幕"
},
"documentSharing": {
@@ -498,6 +543,19 @@
"veryBad": "非常差",
"veryGood": "非常好"
},
"fileSharing": {
"downloadFailedDescription": "请稍后重试",
"downloadFailedTitle": "下载失败",
"downloadFile": "下载",
"dragAndDrop": "拖拽文件到此处上传",
"fileAlreadyUploaded": "文件已上传至本次会议",
"fileTooLargeDescription": "请确保文件不超过 {{ maxFileSize }}",
"fileTooLargeTitle": "文件太大",
"removeFile": "移除",
"uploadFailedDescription": "请稍后重试",
"uploadFailedTitle": "上传失败",
"uploadFile": "文件共享"
},
"filmstrip": {
"accessibilityLabel": {
"heading": "视频缩略图"
@@ -545,13 +603,16 @@
"noNumbers": "无呼入号码。",
"noPassword": "无",
"noRoom": "没有指定要呼入的会议室。",
"noWhiteboard": "无法加载白板",
"numbers": "拨入号码",
"password": "$t(lockRoomPasswordUppercase)",
"reachedLimit": "你已达到套餐限制。",
"sip": "SIP地址",
"sipAudioOnly": "仅限SIP音频地址",
"title": "分享",
"tooltip": "分享此会议的链接和拨入信息",
"upgradeOptions": "请查看升级选项于"
"upgradeOptions": "请查看升级选项于",
"whiteboardError": "加载白板出错,请稍后重试"
},
"inlineDialogFailure": {
"msg": "我们似乎出了点小问题。",
@@ -615,6 +676,7 @@
"on": "直播已开始",
"onBy": "{{name}}已开始直播",
"pending": "直播启动中……",
"policyError": "你尝试开始直播的速度太快,请稍后再试!",
"serviceName": "直播服务",
"sessionAlreadyActive": "本次会议已经在录制或直播",
"signIn": "使用 Google 账号登录",
@@ -630,13 +692,13 @@
"lobby": {
"backToKnockModeButton": "请求加入",
"chat": "聊天",
"dialogTitle": "大厅模式",
"disableDialogContent": "大厅模式已开启,此功能确保未经授权的人士不能加入你的会议,确定要关闭吗?",
"dialogTitle": "等候室模式",
"disableDialogContent": "等候室模式已开启,此功能确保未经授权的人士不能加入你的会议,确定要关闭吗?",
"disableDialogSubmit": "关闭",
"emailField": "请输入你的邮箱",
"enableDialogPasswordField": "设置密码(可选)",
"enableDialogSubmit": "开启",
"enableDialogText": "在大厅模式下,参会者只有被主持人同意后才能加入,进而保护你的会议。",
"enableDialogText": "在等候室模式下,参会者只有被主持人同意后才能加入,进而保护你的会议。",
"enterPasswordButton": "输入会议密码",
"enterPasswordTitle": "输入密码加入会议",
"errorMissingPassword": "加入请求被拒绝。",
@@ -651,18 +713,18 @@
"knockButton": "请求加入",
"knockTitle": "有人想要加入会议",
"knockingParticipantList": "请求加入的参会者名单",
"lobbyChatStartedNotification": "{{moderator}}与{{attendee}}开始在大厅聊天",
"lobbyChatStartedTitle": "{{moderator}}已开始与你进行大厅聊天",
"nameField": "输入你的名字",
"lobbyChatStartedNotification": "{{moderator}}与{{attendee}}开始在等候室聊天",
"lobbyChatStartedTitle": "{{moderator}}已开始与你进行等候室聊天",
"lobbyClosed": "等候室已关闭",
"nameField": "输入你的姓名",
"notificationLobbyAccessDenied": "{{originParticipantName}}已拒绝{{targetParticipantName}}的加入请求",
"notificationLobbyAccessGranted": "{{originParticipantName}}已通过{{targetParticipantName}}的加入请求",
"notificationLobbyDisabled": "{{originParticipantName}}关闭了大厅模式",
"notificationLobbyEnabled": "{{originParticipantName}}开启了大厅模式",
"notificationTitle": "大厅",
"passwordField": "输入会议密码",
"notificationLobbyDisabled": "{{originParticipantName}}关闭了等候室模式",
"notificationLobbyEnabled": "{{originParticipantName}}开启了等候室模式",
"notificationTitle": "等候室",
"passwordJoinButton": "加入",
"title": "大厅",
"toggleLabel": "开启大厅模式"
"title": "等候室",
"toggleLabel": "开启等候室模式"
},
"localRecording": {
"clientState": {
@@ -705,7 +767,9 @@
"me": "我",
"notify": {
"OldElectronAPPTitle": "安全漏洞!",
"allowAction": "允许",
"allowAudio": "允许开启麦克风",
"allowBoth": "允许音视频",
"allowVideo": "允许开启摄像头",
"allowedUnmute": "你可以解除麦克风静音、启动摄像头或共享屏幕。",
"audioUnmuteBlockedDescription": "由于系统限制,麦克风解除静音操作被暂时阻止。",
"audioUnmuteBlockedTitle": "麦克风解除静音被阻止!",
@@ -713,9 +777,14 @@
"connectedOneMember": "{{name}}加入了会议",
"connectedThreePlusMembers": "{{name}}和其他人加入了会议",
"connectedTwoMembers": "{{first}}和{{second}}加入了会议",
"dataChannelClosed": "视频质量受损",
"dataChannelClosedDescription": "桥接通道已断开连接,因此视频质量限制为最低设置。",
"disabledIframe": "嵌入仅用于演示目的,因此此通话将在 {{timeout}} 分钟后断开连接。",
"connectionFailed": "连接失败,请稍后再试",
"dataChannelClosed": "视频质量可能受影响",
"dataChannelClosedDescription": "桥接通道已断开,视频质量可能会被限制为最低设置",
"dataChannelClosedDescriptionWithAudio": "桥接通道已断开,音视频可能会出现卡顿或中断",
"dataChannelClosedWithAudio": "音视频质量可能受影响",
"disabledIframe": "嵌入仅用于演示,本次通话将在{{timeout}}分钟后自动断开",
"disabledIframeSecondaryNative": "嵌入{{domain}}仅用于演示,本次通话将在{{timeout}}分钟后自动断开",
"disabledIframeSecondaryWeb": "嵌入{{domain}}仅用于演示,本次通话将在{{timeout}}分钟后自动断开。如需在正式环境嵌入,请使用<a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi服务</a>",
"disconnected": "已断开连接",
"displayNotifications": "显示通知",
"dontRemindMe": "不要提醒我",
@@ -723,7 +792,10 @@
"focusFail": "{{component}}不可用 - {{ms}}秒后重试",
"gifsMenu": "GIPHY",
"groupTitle": "通知",
"hostAskedUnmute": "主持人希望你发言",
"hostAskedUnmute": "主持人你发言",
"invalidTenant": "无效的组织空间",
"invalidTenantHyphenDescription": "你所使用的组织空间名无效(不能以‘-’开头或结尾)",
"invalidTenantLengthDescription": "你所使用的组织空间名过长",
"invitedOneMember": "已邀请{{name}}",
"invitedThreePlusMembers": "已邀请{{name}}以及另外{{count}}人",
"invitedTwoMembers": "已邀请{{first}}和{{second}}",
@@ -760,7 +832,8 @@
"newDeviceAction": "使用",
"newDeviceAudioTitle": "检测到新的音频设备",
"newDeviceCameraTitle": "检测到新的摄像头",
"noiseSuppressionDesktopAudioDescription": "在共享桌面音频时无法启用降噪功能,请禁用并重试。",
"nextToSpeak": "下一个轮到你发言",
"noiseSuppressionDesktopAudioDescription": "在共享桌面音频时无法开启降噪功能,请禁用并重试。",
"noiseSuppressionFailedTitle": "无法启动降噪功能",
"noiseSuppressionStereoDescription": "暂不支持立体声音频降噪。",
"oldElectronClientDescription1": "你似乎正在使用存在已知安全漏洞的旧版Jitsi Meet客户端请确保您更新到我们的",
@@ -784,45 +857,62 @@
"startSilentDescription": "重新加入会议以开启音频",
"startSilentTitle": "你加入时没有开启音频!",
"suboptimalBrowserWarning": "我们担心你本次会议体验欠佳,我们正在寻找改进的方法。与此同时,请尝试使用<a href='{{recommendedBrowserPageLink}}' target='_blank'>完全支持的浏览器</a>。",
"suboptimalExperienceTitle": "浏览器警告",
"suboptimalExperienceTitle": "浏览器提示",
"suggestRecordingAction": "开始录制",
"suggestRecordingDescription": "是否需要录制本次会议?",
"suggestRecordingTitle": "录制会议",
"unmute": "解除静音",
"videoMutedRemotelyDescription": "你随时可以重新打开。",
"unmuteVideo": "开启摄像头",
"videoMutedRemotelyDescription": "你可随时重新开启视频",
"videoMutedRemotelyTitle": "{{participantDisplayName}}已关闭你的视频",
"videoUnmuteBlockedDescription": "由于系统限制,开启摄像头共享屏幕操作已被暂时阻止。",
"videoUnmuteBlockedTitle": "开启摄像头和共享屏幕被阻止",
"viewLobby": "查看大厅",
"waitingParticipants": "{{waitingParticipants}}人"
"videoUnmuteBlockedDescription": "由于系统限制,暂时无法开启摄像头共享桌面",
"videoUnmuteBlockedTitle": "摄像头和桌面共享被禁用",
"viewLobby": "查看等候室",
"viewParticipants": "查看参会者",
"viewVisitors": "查看观众",
"waitingParticipants": "{{waitingParticipants}}人",
"waitingVisitors": "排队等候的观众:{{waitingVisitors}}人",
"waitingVisitorsTitle": "会议尚未开始!",
"whiteboardLimitDescription": "因用户数量已接近上限,白板即将关闭,请及时保存内容。",
"whiteboardLimitTitle": "白板使用限制"
},
"participantsPane": {
"actions": {
"admit": "同意",
"admitAll": "同意全部",
"admit": "同意加入",
"admitAll": "全部同意加入",
"allow": "允许参会者:",
"allowVideo": "允许视频",
"askUnmute": "请求解除静音",
"audioModeration": "自解除静音",
"allowVideo": "允许开启摄像头",
"askUnmute": "请求取消静音",
"audioModeration": "自解除静音",
"blockEveryoneMicCamera": "禁用所有人的麦克风和摄像头",
"breakoutRooms": "分组讨论室",
"goLive": "开始直播",
"invite": "邀请其他人",
"lowerAllHands": "取消全部举手",
"lowerHand": "取消举手",
"moreModerationActions": "更多主持人选项",
"moreModerationControls": "更多主持人控制",
"moreParticipantOptions": "更多参会者选项",
"mute": "静音",
"muteAll": "全体静音",
"muteEveryoneElse": "全体静音",
"muteEveryoneElse": "静音其他人",
"reject": "拒绝",
"stopEveryonesVideo": "禁用所有人视频",
"stopVideo": "禁用视频",
"unblockEveryoneMicCamera": "允许所有人麦克风和摄像头",
"videoModeration": "开启视频"
"stopEveryonesVideo": "关闭所有人摄像头",
"stopVideo": "关闭摄像头",
"unblockEveryoneMicCamera": "允许所有人开启麦克风和摄像头",
"videoModeration": "允许开启视频"
},
"close": "关闭",
"headings": {
"lobby": "大厅({{count}}人)",
"lobby": "等候室({{count}}人)",
"participantsList": "会议参会者({{count}}人)",
"visitors": "访客(({{count}}人)",
"waitingLobby": "在大厅等待{{count}}人)"
"visitorInQueue": "(排队中:{{count}}人)",
"visitorRequests": "请求加入:{{count}}人)",
"visitors": "观众(({{count}}人)",
"waitingLobby": "在等候室等待({{count}}人)"
},
"search": "搜索参会者",
"searchDescription": "输入关键词快速筛选参会者",
"title": "参会者"
},
"passwordDigitsOnly": "最多{{number}}位数字",
@@ -831,20 +921,24 @@
"pinnedParticipant": "参会者已固定",
"polls": {
"answer": {
"edit": "编辑",
"send": "发送",
"skip": "跳过",
"submit": "提交"
},
"by": "由{{ name }}",
"by": "由{{ name }}发起",
"closeButton": "结束投票",
"create": {
"addOption": "添加选项",
"answerPlaceholder": "选项{{index}}",
"cancel": "取消",
"create": "建投票",
"create": "建投票",
"pollOption": "投票选项{{index}}",
"pollQuestion": "投票内容",
"questionPlaceholder": "提出问题",
"removeOption": "除选项",
"send": "发送"
"questionPlaceholder": "请输入你的问题",
"removeOption": "除选项",
"save": "保存",
"send": "发布"
},
"errors": {
"notUniqueOption": "选项必须是唯一的"
@@ -866,61 +960,65 @@
"audioAndVideoError": "音频和视频错误:",
"audioDeviceProblem": "你的音频设备存在问题",
"audioOnlyError": "音频错误:",
"audioTrackError": "无法创建音轨。",
"callMe": "给我打电话",
"callMeAtNumber": "打电话给我,号码",
"calling": "正在拨号",
"audioTrackError": "无法创建音频轨道",
"callMe": "呼叫我",
"callMeAtNumber": "请拨打我的号码:",
"calling": "正在呼叫",
"configuringDevices": "正在配置设备……",
"connectedWithAudioQ": "你已连接音频?",
"connection": {
"failed": "连接测试失败!",
"good": "你的网络连接看起来很好!",
"nonOptimal": "你的网络连接不太理想",
"poor": "你的网络连接不太理想"
"poor": "你的网络连接较差",
"running": "正在测试连接……"
},
"connectionDetails": {
"audioClipping": "我们预计你的音频会有卡顿现象。",
"audioHighQuality": "我们预计你的音频质量会很好。",
"audioLowNoVideo": "我们预计你的音频质量会很差且没有视频画面",
"goodQuality": "太棒了!你的媒体质量会很好。",
"noMediaConnectivity": "我们无法建立连接,通常是防火墙或NAT的问题。",
"noVideo": "我们预计你的视频画会很糟糕",
"undetectable": "如果仍无法在浏览器中进行通话,我们建议你检查扬声器、麦克风和摄像头的设置,确定浏览器是否有使用麦克风和摄像头的权限,并将浏览器升级到最新版本。如果仍未解决问题,请与开发人员联系。",
"veryPoorConnection": "我们预计你的通话质量会非常糟糕。",
"videoFreezing": "我们预计你的视频会冻结、变黑并且呈像素化状态。",
"videoHighQuality": "我们预计你的视频质量会很好。",
"videoLowQuality": "我们预计你的视频帧率和分辨率会很低。",
"videoTearing": "我们预计你的视频会呈现像素化或有视觉伪影。"
"audioClipping": "预计你的音频可能会断断续续",
"audioHighQuality": "你的音频质量预计会很清晰",
"audioLowNoVideo": "预计你的音频质量较差且无法显示视频画面",
"goodQuality": "网络和设备状态都很好,体验会很流畅",
"noMediaConnectivity": "无法建立音视频连接通常是防火墙或NAT设置导致",
"noVideo": "预计你的视频画面可能会很糟糕",
"testFailed": "连接测试遇到异常,但不一定会影响实际体验",
"undetectable": "如果你仍无法正常通话,请检查扬声器、麦克风、摄像头设置,并确保浏览器有相关权限且为最新版本。如仍有问题,建议联系技术支持。",
"veryPoorConnection": "你的通话质量可能会非常差",
"videoFreezing": "预计你的视频会出现卡顿、黑屏或马赛克",
"videoHighQuality": "你的视频质量预计会很清晰",
"videoLowQuality": "预计你的视频帧率和分辨率会很低或画面不流畅",
"videoTearing": "预计你的视频画面会有马赛克或其它异常现象"
},
"copyAndShare": "复制并分享会议链接",
"dialInMeeting": "拨打会议电话",
"dialInPin": "拨打会议电话并输入PIN码",
"dialInMeeting": "拨电话接入会议",
"dialInPin": "电话接入会议并输入PIN码",
"dialing": "正在拨号",
"doNotShow": "不再显示",
"doNotShow": "不再显示此页面",
"errorDialOut": "无法拨出",
"errorDialOutDisconnected": "无法拨出,已断开连接",
"errorDialOutFailed": "无法拨出,呼叫失败",
"errorDialOutStatus": "获取拨出状态出错",
"errorMissingName": "请输入你的名字以加入会议",
"errorNoPermissions": "你需要允许访问麦克风和摄像头",
"errorStatusCode": "拨出错误,状态码:{{status}}",
"errorValidation": "号码验失败",
"iWantToDialIn": "我想拨打电话加入",
"initiated": "通话已发起",
"errorDialOutFailed": "无法拨出,通话失败",
"errorDialOutStatus": "获取拨出状态出错",
"errorMissingName": "请输入姓名后再加入会议",
"errorNoPermissions": "请开启麦克风和摄像头权限",
"errorStatusCode": "拨号失败,状态码:{{status}}",
"errorValidation": "号码验失败",
"iWantToDialIn": "我想通过电话加入",
"initiated": "呼叫已发起",
"joinAudioByPhone": "使用电话音频加入",
"joinMeeting": "加入会议",
"joinMeetingInLowBandwidthMode": "以省流模式加入",
"joinWithoutAudio": "无音频加入",
"keyboardShortcuts": "开启键盘快捷键",
"keyboardShortcuts": "开启快捷键",
"linkCopied": "链接已复制到剪贴板",
"lookGood": "你的麦克风工作正常",
"lookGood": "设备一切正常",
"or": "或",
"premeeting": "会前",
"premeeting": "会前设置",
"proceedAnyway": "仍然继续",
"screenSharingError": "共享屏幕错误:",
"startWithPhone": "以电话音频开始",
"unsafeRoomConsent": "我了解风险,我想加入会议",
"videoOnlyError": "视频错误:",
"videoTrackError": "无法创建视频轨道。",
"recordingWarning": "其他参会者可能正在录制本次会议",
"screenSharingError": "屏幕共享出错:",
"startWithPhone": "用电话音频开始",
"unsafeRoomConsent": "我了解风险,仍然加入会议",
"videoOnlyError": "视频出错:",
"videoTrackError": "无法创建视频轨道",
"viewAllNumbers": "查看所有号码"
},
"presenceStatus": {
@@ -981,7 +1079,6 @@
"limitNotificationDescriptionNative": "由于高需求,您的录制将限制在{{limit}}分钟内。若要无限制录制,请尝试<3>{{app}}</3>。",
"limitNotificationDescriptionWeb": "由于高需求,您的录制将限制在{{limit}}分钟内。若要无限制录制,请尝试<a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>。",
"linkGenerated": "我们已生成录制链接。",
"live": "直播中",
"localRecordingNoNotificationWarning": "系统不会主动通知参会者录制已开启,主持人需另行提醒。",
"localRecordingNoVideo": "视频未被录制",
"localRecordingStartWarning": "请确保在退出会议之前停止录制,以便保存录制内容。",
@@ -998,13 +1095,16 @@
"onBy": "{{name}}开启了录制",
"onlyRecordSelf": "仅录制我的音频和视频",
"pending": "正在准备录制会议……",
"rec": "录制中",
"policyError": "",
"recordAudioAndVideo": "",
"recordTranscription": "",
"saveLocalRecording": "本地保存录制文件(测试版)",
"serviceDescription": "录制将由录制服务保存",
"serviceDescriptionCloud": "云录制",
"serviceDescriptionCloudInfo": "录制的会议将在录制后的24小时内自动清除。",
"serviceName": "录制服务",
"sessionAlreadyActive": "此会议已经在录制或直播中",
"showAdvancedOptions": "高级选项",
"signIn": "登录",
"signOut": "注销",
"surfaceError": "请选择当前选项卡。",
@@ -1033,21 +1133,23 @@
"audio": "音频",
"buttonLabel": "设置",
"calendar": {
"about": "{{appName}}日历集成用于安全访问你的日历,读取即将到来的活动。",
"about": "{{appName}}日历集成安全访问你的日历,读取即将开始的会议。",
"disconnect": "断开连接",
"microsoftSignIn": "使用 Microsoft 登录",
"signedIn": "目前正在读取{{email}}的日历事件,点击下面的断开连接可以停止读取日历事件。",
"signedIn": "正在读取{{email}}的日历事件,点击下方“断开连接”可停止同步日历。",
"title": "日历"
},
"chatWithPermissions": "聊天需要相关权限",
"desktopShareFramerate": "共享屏幕帧率",
"desktopShareHighFpsWarning": "高帧率的共享屏幕的可能会影响你的网速,你需要重新启动共享屏幕以使新设置生效。",
"desktopShareWarning": "你需要重新启动共享屏幕以使新设置生效。",
"devices": "设备",
"followMe": "所有人跟随",
"framesPerSecond": "",
"incomingMessage": "新消息",
"followMe": "全员视角跟随",
"followMeRecorder": "录制画面跟随我",
"framesPerSecond": "帧率FPS",
"incomingMessage": "新消息提醒",
"language": "语言",
"loggedIn": "{{name}}登录",
"loggedIn": "已登录:{{name}}",
"maxStageParticipants": "可以固定的最大参会者人数",
"microphones": "麦克风",
"moderator": "主持人",
@@ -1057,52 +1159,55 @@
"noDevice": "无",
"notifications": "通知",
"participantJoined": "参会者已加入",
"participantKnocking": "参会者已进入大厅",
"participantKnocking": "参会者已进入等候室",
"participantLeft": "参会者已离开",
"playSounds": "播放提示音",
"reactions": "会议反应",
"sameAsSystem": "与系统相同{{label}}",
"selectAudioOutput": "音频输出",
"reactions": "会议互动表情",
"sameAsSystem": "与系统一致{{label}}",
"selectAudioOutput": "音频输出设备",
"selectCamera": "摄像头",
"selectMic": "麦克风",
"selfView": "本人视图",
"shortcuts": "快捷键",
"showSubtitlesOnStage": "主画面显示字幕",
"speakers": "扬声器",
"startAudioMuted": "所有人开始时静音",
"startReactionsMuted": "关闭所有人反应提示音",
"startVideoMuted": "所有人开始时隐藏视频画面",
"talkWhileMuted": "通话时静音",
"startAudioMuted": "所有人加入时静音",
"startReactionsMuted": "关闭所有人互动音效",
"startVideoMuted": "所有人加入时隐藏视频",
"talkWhileMuted": "静音时说话提醒",
"title": "设置",
"video": "视频"
},
"settingsView": {
"advanced": "高级",
"advanced": "高级设置",
"alertCancel": "取消",
"alertOk": "确认",
"alertTitle": "警告",
"alertURLText": "输入的服务器址无效",
"buildInfoSection": "生成信息",
"conferenceSection": "会议",
"disableCallIntegration": "禁用本地电话",
"disableCrashReporting": "禁用崩溃报告",
"disableCrashReportingWarning": "你确定要禁用崩溃报告吗?重启应用后生效。",
"disableP2P": "禁用点对点模式",
"alertURLText": "输入的服务器址无效",
"apply": "应用",
"buildInfoSection": "构建信息",
"conferenceSection": "会议设置",
"disableCallIntegration": "禁用系统通话集成",
"disableCrashReporting": "关闭崩溃报告",
"disableCrashReportingWarning": "确定要关闭崩溃报告吗?重启应用后设置才会生效",
"disableP2P": "关闭点对点模式",
"displayName": "显示名称",
"displayNamePlaceholderText": "例如:张三",
"email": "邮箱",
"emailPlaceholderText": "email@example.com",
"gavatarMessage": "如果你的邮箱已绑定Gravatar头像我们将自动为你显示头像",
"goTo": "前往",
"header": "设置",
"help": "帮助",
"links": "链接",
"links": "相关链接",
"privacy": "隐私",
"profileSection": "简介",
"profileSection": "个人信息",
"sdkVersion": "SDK版本",
"serverURL": "服务器址",
"serverURL": "服务器址",
"showAdvanced": "显示高级设置",
"startCarModeInLowBandwidthMode": "同时开启驾驶模式和省流模式",
"startWithAudioMuted": "关闭音频并启动",
"startWithVideoMuted": "关闭视频并启动",
"startCarModeInLowBandwidthMode": "省流模式下开启驾驶模式",
"startWithAudioMuted": "进入会议时音频静音",
"startWithVideoMuted": "进入会议时关闭视频",
"terms": "条款",
"version": "APP版本"
},
@@ -1113,20 +1218,22 @@
"speaker": "扬声器",
"speakerStats": {
"angry": "生气",
"disgusted": "呕吐",
"disgusted": "嫌弃",
"displayEmotions": "显示表情",
"fearful": "害怕",
"happy": "笑脸",
"happy": "开心",
"hours": "{{count}}时",
"labelTooltip": "参会者人数:{{count}}",
"minutes": "{{count}}分",
"name": "名字",
"neutral": "中",
"sad": "悲伤",
"neutral": "中",
"sad": "难过",
"search": "搜索",
"searchDescription": "输入关键词筛选参会者",
"searchHint": "搜索参会者",
"seconds": "{{count}}秒",
"speakerStats": "发言统计",
"speakerTime": "发言时",
"speakerStats": "参会者统计",
"speakerTime": "发言时",
"surprised": "惊讶"
},
"startupoverlay": {
@@ -1144,36 +1251,37 @@
},
"toggleTopPanelLabel": "切换顶部面板",
"toolbar": {
"Settings": "设置",
"Settings": "打开设置",
"accessibilityLabel": {
"Settings": "切换设置",
"audioOnly": "切换仅音频模式",
"audioRoute": "选择音频设备",
"boo": "嘘声",
"breakoutRoom": "加入/离开分组讨论室",
"callQuality": "管理视频质量",
"breakoutRooms": "分组讨论室",
"callQuality": "调整视频质量",
"carmode": "驾驶模式",
"cc": "切换字幕",
"chat": "打开/关闭聊天",
"clap": "鼓掌",
"closeChat": "关闭聊天",
"closeChat": "关闭聊天窗口",
"closeMoreActions": "关闭更多操作菜单",
"closeParticipantsPane": "关闭参会者面板",
"collapse": "折叠",
"closedCaptions": "隐藏字幕",
"collapse": "收起",
"document": "切换共享文档",
"documentClose": "关闭共享文档",
"documentOpen": "打开共享文档",
"download": "下载我们的APP",
"embedMeeting": "嵌入会议",
"endConference": "结束会议",
"endConference": "结束全体会议",
"enterFullScreen": "进入全屏模式",
"enterTileView": "进入平铺视图",
"enterTileView": "进入画廊视图",
"exitFullScreen": "退出全屏模式",
"exitTileView": "退出平铺视图",
"exitTileView": "退出画廊视图",
"expand": "展开",
"feedback": "提供反馈",
"fullScreen": "切换全屏模式",
"giphy": "切换GIPHY菜单",
"giphy": "切换表情动图菜单",
"grantModerator": "授予主持人权限",
"hangup": "离开会议",
"heading": "工具栏",
@@ -1183,30 +1291,32 @@
"kick": "移除参会者",
"laugh": "大笑",
"leaveConference": "离开会议",
"like": "竖起大拇指",
"linkToSalesforce": "链接到 Salesforce",
"lobbyButton": "开启/关闭大厅模式",
"localRecording": "切换本地录制控件",
"lockRoom": "开启/关闭会议密码",
"lowerHand": "放下手",
"like": "点赞",
"linkToSalesforce": "关联到Salesforce",
"lobbyButton": "开启/关闭等候室",
"localRecording": "切换本地录制",
"lockRoom": "设置/取消会议密码",
"love": "爱心",
"lowerHand": "取消举手",
"moreActions": "更多操作",
"moreActionsMenu": "更多操作菜单",
"moreOptions": "显示更多选项",
"mute": "静音",
"mute": "麦克风静音",
"muteEveryone": "将所有人静音",
"muteEveryoneElse": "将其他人静音",
"muteEveryoneElsesVideoStream": "停止其他人的视频",
"muteEveryonesVideoStream": "停止全部人的视频",
"muteGUMPending": "连接你的麦克风",
"noiseSuppression": "降噪",
"openChat": "打开聊天",
"participants": "打开参会者面板",
"muteEveryoneElse": "除自己外全部静音",
"muteEveryoneElsesVideoStream": "停止其他人摄像头",
"muteEveryonesVideoStream": "关闭所有人摄像头",
"muteGUMPending": "正在连接麦克风",
"noiseSuppression": "开启降噪(测试版)",
"openChat": "打开聊天窗口",
"participants": "打开参会者面板,参会者(共{{participantsCount}}人)",
"pip": "切换画中画模式",
"privateMessage": "发送私人消息",
"profile": "编辑你的个人资料",
"privateMessage": "发送私",
"profile": "编辑个人信息",
"raiseHand": "举手",
"reactions": "反应",
"reactionsMenu": "反应菜单",
"react": "消息表情",
"reactions": "互动表情",
"reactionsMenu": "互动表情菜单",
"recording": "切换录制",
"remoteMute": "静音参会者",
"remoteVideoMute": "禁用参会者摄像头",
@@ -1223,45 +1333,46 @@
"silence": "沉默",
"speakerStats": "切换参会者统计",
"stopScreenSharing": "停止屏幕共享",
"stopSharedVideo": "停止视频",
"stopSharedVideo": "停止视频共享",
"surprised": "惊讶",
"tileView": "切换画廊视图",
"toggleCamera": "切换摄像头",
"toggleFilmstrip": "切换幻灯片",
"unmute": "取消静音",
"videoblur": "切换视频模糊",
"toggleFilmstrip": "切换缩略图栏",
"unmute": "取消麦克风静音",
"videoblur": "切换虚化背景",
"videomute": "关闭摄像头",
"videomuteGUMPending": "连接你的摄像头",
"videounmute": "启动摄像头"
"videomuteGUMPending": "正在连接摄像头",
"videounmute": "打开摄像头"
},
"addPeople": "添加员到你的通话中",
"addPeople": "添加员到通话中",
"audioOnlyOff": "关闭省流模式",
"audioOnlyOn": "启省流模式",
"audioOnlyOn": "启省流模式",
"audioRoute": "选择音频设备",
"audioSettings": "音频设置",
"authenticate": "证",
"authenticate": "身份验证",
"boo": "嘘声",
"callQuality": "管理视频质量",
"callQuality": "调整视频质量",
"chat": "打开/关闭聊天",
"clap": "鼓掌",
"closeChat": "关闭聊天",
"closeParticipantsPane": "关闭参会者面板",
"closeReactionsMenu": "关闭反应菜单",
"disableNoiseSuppression": "关闭降噪",
"disableReactionSounds": "你可以禁用此会议的反应声音",
"closeParticipantsPane": "关闭参会者列表",
"closeReactionsMenu": "关闭互动表情菜单",
"closedCaptions": "关闭字幕",
"disableNoiseSuppression": "关闭降噪功能(测试版)",
"disableReactionSounds": "你可以禁用此会议的互动音效",
"documentClose": "关闭文件共享",
"documentOpen": "开启文件共享",
"download": "下载我们的APP",
"e2ee": "端到端加密",
"embedMeeting": "嵌入会议",
"enableNoiseSuppression": "启用降噪",
"endConference": "结束会议",
"enableNoiseSuppression": "开启降噪功能(测试版)",
"endConference": "结束全体会议",
"enterFullScreen": "进入全屏模式",
"enterTileView": "进入画廊视图",
"exitFullScreen": "退出全屏模式",
"exitTileView": "退出画廊视图",
"feedback": "提供反馈",
"giphy": "关闭GIPHY菜单",
"feedback": "意见反馈",
"giphy": "表情动画菜单",
"hangup": "离开会议",
"help": "帮助",
"hideWhiteboard": "隐藏白板",
@@ -1270,13 +1381,14 @@
"laugh": "大笑",
"leaveBreakoutRoom": "离开分组讨论室",
"leaveConference": "离开会议",
"like": "竖起大拇指",
"like": "点赞",
"linkToSalesforce": "链接到 Salesforce",
"lobbyButtonDisable": "关闭大厅模式",
"lobbyButtonEnable": "开启大厅模式",
"lobbyButtonDisable": "关闭等候室模式",
"lobbyButtonEnable": "开启等候室模式",
"login": "登录",
"logout": "注销",
"lowerYourHand": "放下手",
"love": "爱心",
"lowerYourHand": "取消举手",
"moreActions": "更多操作",
"moreOptions": "显示更多选项",
"mute": "静音",
@@ -1288,24 +1400,25 @@
"noAudioSignalDialInDesc": "你还可以拨打以下号码加入会议:",
"noAudioSignalDialInLinkDesc": "拨打电话号码",
"noAudioSignalTitle": "你的麦克风没有声音!",
"noiseSuppression": "降噪",
"noisyAudioInputDesc": "听起来你的麦克风在发出噪音,请考虑将其静音或更换设备。",
"noiseSuppression": "降噪(测试版)",
"noisyAudioInputDesc": "检测到你的麦克风有杂音,请考虑静音或更换设备。",
"noisyAudioInputTitle": "你的麦克风似乎很嘈杂!",
"openChat": "打开聊天",
"openReactionsMenu": "打开反应菜单",
"openReactionsMenu": "打开互动表情菜单",
"participants": "参会者",
"pip": "进入画中画模式",
"privateMessage": "发送私人消息",
"privateMessage": "发送私",
"profile": "编辑你的个人资料",
"raiseHand": "举手",
"raiseYourHand": "举手",
"reactionBoo": "发送嘘声反应",
"reactionClap": "发送鼓掌反应",
"reactionLaugh": "发送大笑反应",
"reactionLike": "发送竖起大拇指反应",
"reactionSilence": "发送沉默反应",
"reactionSurprised": "发送惊讶反应",
"reactions": "反应",
"reactionBoo": "发送嘘声",
"reactionClap": "发送鼓掌",
"reactionHeart": "发送爱心",
"reactionLaugh": "发送大笑",
"reactionLike": "发送点赞",
"reactionSilence": "发送沉默",
"reactionSurprised": "发送惊讶",
"reactions": "互动表情",
"security": "安全选项",
"selectBackground": "选择背景",
"shareRoom": "邀请他人",
@@ -1333,19 +1446,20 @@
},
"transcribing": {
"ccButtonTooltip": "开启/关闭字幕",
"error": "转录失败,请重试。",
"expandedLabel": "转录已开启",
"failedToStart": "开启转录失败",
"labelToolTip": "会议正在转录",
"off": "转录已停止",
"pending": "准备转录会议中……",
"failed": "转录失败",
"labelTooltip": "本次会议正在进行转录",
"labelTooltipExtra": "会后将提供转录文本",
"openClosedCaptions": "打开字幕",
"original": "原文",
"sourceLanguageDesc": "当前会议语言设置为<b>{{sourceLanguage}}</b><br/>你可以在这里",
"sourceLanguageHere": "更改",
"start": "开启显示字幕",
"stop": "停止显示字幕",
"subtitles": "字幕",
"subtitlesOff": "关",
"tr": "转录"
"subtitlesOff": "关",
"tr": "转录",
"translateTo": "翻译为"
},
"unpinParticipant": "{{participantName}} - 取消固定",
"userMedia": {
@@ -1385,15 +1499,16 @@
},
"videothumbnail": {
"connectionInfo": "连接信息",
"demote": "设为观众",
"domute": "静音",
"domuteOthers": "静音所有人",
"domuteOthers": "静音其他人",
"domuteVideo": "关闭摄像头",
"domuteVideoOfOthers": "关闭所有人的摄像头",
"flip": "翻转",
"domuteVideoOfOthers": "关闭其他人摄像头",
"flip": "翻转画面",
"grantModerator": "授予主持人权限",
"hideSelfView": "隐藏本人视图",
"kick": "移",
"mirrorVideo": "镜像我的视频",
"kick": "移出会议",
"mirrorVideo": "镜像我的画面",
"moderator": "主持人",
"mute": "参会者已被静音",
"muted": "已静音",
@@ -1436,12 +1551,23 @@
"webAssemblyWarningDescription": "此浏览器禁用或不支持WebAssembly"
},
"visitors": {
"chatIndicator": "访客",
"labelTooltip": "访客人数:{{count}}",
"chatIndicator": "观众",
"joinMeeting": {
"description": "你目前是本次会议的观众",
"raiseHand": "举手申请发言",
"title": "正在加入会议",
"wishToSpeak": "如需发言,请点击下方举手,等待主持人同意"
},
"labelTooltip": "观众人数:{{count}}",
"notification": {
"description": "要参与,请举手",
"title": "你是会议中的访客"
}
"demoteDescription": "你已被{{actor}}切换为观众身份,如需参与发言请举手申请",
"noMainParticipantsDescription": "暂无参会者开启会议,请稍后再试",
"noMainParticipantsTitle": "会议尚未开始",
"noVisitorLobby": "当前会议已开启等候室,暂无法加入",
"notAllowedPromotion": "需由会议成员同意才能参与讨论",
"title": "你当前为会议观众"
},
"waitingMessage": "会议开始后将自动加入"
},
"volumeSlider": "音量滑块",
"welcomepage": {
@@ -1499,6 +1625,7 @@
"whiteboard": {
"accessibilityLabel": {
"heading": "白板"
}
},
"screenTitle": "白板"
}
}

View File

@@ -109,6 +109,7 @@
}
},
"chat": {
"disabled": "聊天訊息已停用",
"enter": "加入聊天室",
"error": "錯誤:您的訊息未被傳送。原因:{{error}}",
"fieldPlaceHolder": "在此輸入您的訊息",
@@ -122,17 +123,26 @@
"nickname": {
"popover": "選擇名稱",
"title": "輸入名稱來使用聊天",
"titleWithPolls": "輸入名稱使用聊天與投票"
"titleWithCC": "輸入名稱使用聊天與即時字幕",
"titleWithPolls": "輸入名稱來使用聊天與投票",
"titleWithPollsAndCC": "輸入名稱以使用聊天、投票及即時字幕",
"titleWithPollsAndCCAndFileSharing": "輸入名稱以使用聊天、投票、即時字幕及檔案分享"
},
"noMessagesMessage": "此會議尚無訊息,在此開始對話聊天!",
"privateNotice": "傳送私人訊息至 {{recipient}}",
"sendButton": "傳送",
"smileysPanel": "Emoji 面板",
"systemDisplayName": "系統",
"tabs": {
"chat": "聊天",
"closedCaptions": "即時字幕",
"fileSharing": "檔案",
"polls": "投票"
},
"title": "聊天",
"titleWithCC": "即時字幕",
"titleWithFeatures": "聊天與",
"titleWithFileSharing": "檔案",
"titleWithPolls": "聊天與投票",
"you": "您"
},
@@ -143,6 +153,10 @@
"dontShowAgain": "不要再問了",
"installExtensionText": "安裝適用於 Google 行事曆及 Office 365 整合的擴充功能"
},
"closedCaptionsTab": {
"emptyState": "即時字幕內容將在主持人啟用後顯示",
"startClosedCaptionsButton": "啟動即時字幕"
},
"connectingOverlay": {
"joiningRoom": "正在將您連接至您的會議…"
},
@@ -219,7 +233,9 @@
"joinInBrowser": "在瀏覽器中加入",
"launchMeetingLabel": "您想如何加入此會議?",
"launchWebButton": "在瀏覽器開啟",
"noDesktopApp": "您尚未安裝桌面應用程式?",
"noMobileApp": "您尚未安裝該應用程式?",
"or": "或",
"termsAndConditions": "繼續操作即表示您同意我們的<a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>條款與條件。</a>",
"title": "正在 {{app}} 開啟您的會議…",
"titleNew": "正在開啟您的會議…",
@@ -260,7 +276,10 @@
"Remove": "移除",
"Share": "分享",
"Submit": "送出",
"Understand": "我了解,暫時保持靜音",
"UnderstandAndUnmute": "我了解,請為我解除靜音",
"WaitForHostMsg": "此會議尚未開始,如果您是會議主持人,請進行認證並以主持人身分開始會議。",
"WaitForHostNoAuthMsg": "此會議尚未開始,目前沒有主持人加入,請稍候。",
"WaitingForHostButton": "等待主持人",
"WaitingForHostTitle": "正在等候主持人加入…",
"Yes": "是",
@@ -295,6 +314,7 @@
"conferenceReloadMsg": "我們正試著修復狀況,將在 {{seconds}} 秒後重新連接…",
"conferenceReloadTitle": "喔哦!好像有東西壞掉囉。",
"confirm": "確認",
"confirmBack": "返回",
"confirmNo": "否",
"confirmYes": "是",
"connectError": "喔哦!發生錯誤,無法連接至會議。",
@@ -303,6 +323,8 @@
"contactSupport": "聯絡支援",
"copied": "已複製",
"copy": "複製",
"demoteParticipantDialog": "您確定要將此與會者轉為僅檢視模式嗎?",
"demoteParticipantTitle": "切換為檢視者",
"dismiss": "取消",
"displayNameRequired": "嗨!請問大名?",
"done": "完成",
@@ -314,6 +336,7 @@
"embedMeeting": "嵌入會議",
"enterDisplayName": "請在此輸入您自己的名字",
"error": "錯誤",
"errorRoomCreationRestriction": "您加入速度過快,請稍後再試。",
"gracefulShutdown": "服務目前正在維護中,請稍後再試。",
"grantModeratorDialog": "您確定要授予 {{participantName}} 主持人權限嗎?",
"grantModeratorTitle": "授予主持人權限",
@@ -327,7 +350,9 @@
"kickParticipantButton": "移除",
"kickParticipantDialog": "您確定要將這位與會者移除嗎?",
"kickParticipantTitle": "移除這位與會者?",
"kickSystemTitle": "您已被移出會議",
"kickTitle": "噢!{{participantDisplayName}} 將您從會議中移除",
"learnMore": "了解詳情",
"linkMeeting": "連結會議",
"linkMeetingTitle": "將會議連結至 Salesforce",
"liveStreaming": "直播串流中",
@@ -385,6 +410,10 @@
"recentlyUsedObjects": "您最近使用過的物件",
"recording": "錄製中",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "正在直播時無法使用",
"recordingInProgressDescription": "本會議正在錄製並由 AI 分析{{learnMore}},您的音訊與影像已靜音。如果您選擇取消靜音,即表示您同意被錄製。",
"recordingInProgressDescriptionFirstHalf": "本會議正在錄製並由 AI 分析",
"recordingInProgressDescriptionSecondHalf": ",您的音訊與影像已靜音。如果您選擇取消靜音,即表示您同意被錄製。",
"recordingInProgressTitle": "正在錄製",
"rejoinNow": "立即重新加入",
"remoteControlAllowedMessage": "{{user}} 接受您進行遠端控制的請求!",
"remoteControlDeniedMessage": "{{user}} 拒絕您進行遠端控制的請求!",
@@ -421,6 +450,7 @@
"sessTerminatedReason": "會議已經終止",
"sessionRestarted": "通話因連線問題重新啟動。",
"shareAudio": "繼續",
"shareAudioAltText": "如需分享內容,請選擇「瀏覽器分頁」,勾選「分享音訊」後再按「分享」",
"shareAudioTitle": "如何分享音訊",
"shareAudioWarningD1": "您必須先停用分享螢幕才能分享音訊。",
"shareAudioWarningD2": "您必須重新啟動螢幕分享並勾選「分享音訊」選項。",
@@ -431,7 +461,10 @@
"shareScreenWarningD2": "您必須先停用分享音訊後,重新啟動螢幕分享並勾選「分享音訊」選項。",
"shareScreenWarningH1": "如果您只要分享螢幕:",
"shareScreenWarningTitle": "您必須先停用分享音訊才能分享螢幕",
"shareVideoConfirmPlay": "您即將開啟外部網站,是否繼續?",
"shareVideoConfirmPlayTitle": "{{name}} 與您分享了一段影片。",
"shareVideoLinkError": "請提供正確的影片網址。",
"shareVideoLinkStopped": "{{name}} 的影片已停止播放",
"shareVideoTitle": "分享影像",
"shareYourScreen": "分享您的螢幕",
"shareYourScreenDisabled": "螢幕分享已停用。",
@@ -510,6 +543,19 @@
"veryBad": "極差",
"veryGood": "極好"
},
"fileSharing": {
"downloadFailedDescription": "請重試",
"downloadFailedTitle": "下載失敗",
"downloadFile": "下載",
"dragAndDrop": "將檔案拖曳至此或畫面任一處上傳",
"fileAlreadyUploaded": "檔案已上傳至此會議",
"fileTooLargeDescription": "請確認檔案未超過 {{ maxFileSize }}",
"fileTooLargeTitle": "檔案過大",
"removeFile": "移除",
"uploadFailedDescription": "請重試",
"uploadFailedTitle": "上傳失敗",
"uploadFile": "分享檔案"
},
"filmstrip": {
"accessibilityLabel": {
"heading": "視頻縮略圖"
@@ -557,6 +603,7 @@
"noNumbers": "無撥入號碼。",
"noPassword": "無",
"noRoom": "沒有會議室指定要撥入。",
"noWhiteboard": "無法載入白板",
"numbers": "撥入號碼",
"password": "$t(lockRoomPasswordUppercase)",
"reachedLimit": "您已達到您的方案上限",
@@ -564,7 +611,8 @@
"sipAudioOnly": "SIP 僅音訊位址",
"title": "分享",
"tooltip": "顯示此會議的連結及電話撥入號碼",
"upgradeOptions": "請查看升級選項於"
"upgradeOptions": "請查看升級選項於",
"whiteboardError": "載入白板時發生錯誤,請稍後再試。"
},
"inlineDialogFailure": {
"msg": "好像有點卡卡不順。",
@@ -628,6 +676,7 @@
"on": "直播串流已啟動",
"onBy": "{{name}} 啟動了直播串流",
"pending": "啟動直播串流…",
"policyError": "您啟動直播的過快,請稍後重試!",
"serviceName": "直播串流服務",
"sessionAlreadyActive": "已在錄製或直播此工作階段。",
"signIn": "使用 Google 帳號登入",
@@ -666,13 +715,13 @@
"knockingParticipantList": "請求加入的與會者名單",
"lobbyChatStartedNotification": "{{moderator}} 與 {{attendee}} 開始在大廳中聊天",
"lobbyChatStartedTitle": "{{moderator}} 與您開始在大廳中聊天。",
"lobbyClosed": "大廳已關閉",
"nameField": "輸入您的名字",
"notificationLobbyAccessDenied": "{{originParticipantName}} 拒絕了 {{targetParticipantName}} 的加入請求",
"notificationLobbyAccessGranted": "{{originParticipantName}} 同意了 {{targetParticipantName}} 的加入請求",
"notificationLobbyDisabled": "{{originParticipantName}} 已停用大廳模式",
"notificationLobbyEnabled": "{{originParticipantName}} 已啟用大廳模式",
"notificationTitle": "大廳",
"passwordField": "輸入會議密碼",
"passwordJoinButton": "加入",
"title": "大廳",
"toggleLabel": "啟用大廳模式"
@@ -718,7 +767,9 @@
"me": "我",
"notify": {
"OldElectronAPPTitle": "安全漏洞!",
"allowAction": "允許",
"allowAudio": "允許音訊",
"allowBoth": "允許音訊與視訊",
"allowVideo": "允許視訊",
"allowedUnmute": "您可以將麥克風解除靜音、開啟視訊,或是分享您的螢幕。",
"audioUnmuteBlockedDescription": "麥克風解除靜音操作由於系統限制而被暫時封鎖。",
"audioUnmuteBlockedTitle": "麥克風解除靜音遭封鎖!",
@@ -726,10 +777,14 @@
"connectedOneMember": "{{name}} 加入了會議",
"connectedThreePlusMembers": "{{name}} 與其他人加入了會議",
"connectedTwoMembers": "{{first}} 與{{second}} 加入了會議",
"connectionFailed": "連線失敗,請稍後重試!",
"dataChannelClosed": "視訊品質受限",
"dataChannelClosedDescription": "橋接通道已斷開,視訊品質降至最低設定。",
"dataChannelClosedDescriptionWithAudio": "橋接通道已斷開,音訊和視訊可能會受到影響。",
"dataChannelClosedWithAudio": "音訊和視訊品質可能會降低。",
"disabledIframe": "嵌入僅供示範使用,此通話將於 {{timeout}} 分鐘後中斷連線。",
"disabledIframeSecondary": "嵌 {{domain}} 僅為展示用途,此通話將 {{timeout}} 分鐘後中斷連線。請使用在正式環境使用 <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi 服務</a>來內嵌!",
"disabledIframeSecondaryNative": "嵌 {{domain}} 僅供示範,此通話將 {{timeout}} 分鐘後中斷",
"disabledIframeSecondaryWeb": "嵌入 {{domain}} 僅供示範,此通話將於 {{timeout}} 分鐘後中斷,請使用 <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi 服務</a> 來進行正式嵌入!",
"disconnected": "已經中斷連接",
"displayNotifications": "顯示通知給",
"dontRemindMe": "不要再提醒我",
@@ -738,6 +793,9 @@
"gifsMenu": "GIPHY",
"groupTitle": "通知",
"hostAskedUnmute": "主持人希望您發言",
"invalidTenant": "無效的租用者名稱",
"invalidTenantHyphenDescription": "您使用的租用者名稱無效(開頭或結尾不可為「-」)。",
"invalidTenantLengthDescription": "您使用的租用者名稱過長。",
"invitedOneMember": "{{name}} 已受邀請",
"invitedThreePlusMembers": "{{name}} 與 {{count}} 位人員已受邀請",
"invitedTwoMembers": "{{first}} 與 {{second}} 已受邀請",
@@ -774,6 +832,7 @@
"newDeviceAction": "使用",
"newDeviceAudioTitle": "偵測到新的音效裝置",
"newDeviceCameraTitle": "偵測到新的網路攝影機",
"nextToSpeak": "下一位輪到您發言",
"noiseSuppressionDesktopAudioDescription": "分享電腦音訊時無法啟用雜訊抑制,請停用後重試。",
"noiseSuppressionFailedTitle": "啟用雜訊抑制失敗",
"noiseSuppressionStereoDescription": "目前不支援立體聲降噪功能。",
@@ -799,13 +858,21 @@
"startSilentTitle": "您以無音訊輸出方式加入了會議!",
"suboptimalBrowserWarning": "我們恐怕您本次會議體驗不佳,我們會努力改善。在此期間,請嘗試使用<a href='{{recommendedBrowserPageLink}}' target='_blank'>支援的瀏覽器</a> 。",
"suboptimalExperienceTitle": "瀏覽器警告",
"suggestRecordingAction": "開始",
"suggestRecordingDescription": "是否要開始錄製這場會議?",
"suggestRecordingTitle": "錄製此會議",
"unmute": "取消靜音",
"unmuteVideo": "啟用視訊",
"videoMutedRemotelyDescription": "您隨時可以再次啟用。",
"videoMutedRemotelyTitle": "您的視訊已被 {{participantDisplayName}} 停用",
"videoUnmuteBlockedDescription": "啟用網路攝影機與分享螢幕由於系統限制而被暫時封鎖。",
"videoUnmuteBlockedTitle": "啟用網路攝影機與分享螢幕遭封鎖!",
"viewLobby": "檢視大廳",
"viewParticipants": "查看與會者",
"viewVisitors": "查看訪客",
"waitingParticipants": "{{waitingParticipants}} 人",
"waitingVisitors": "排隊中的訪客: {{waitingVisitors}} 人",
"waitingVisitorsTitle": "會議尚未開始!",
"whiteboardLimitDescription": "由於即將超出使用者限制,白板將關閉,請儲存您的進度。",
"whiteboardLimitTitle": "白板使用情況"
},
@@ -819,7 +886,10 @@
"audioModeration": "自我解除靜音",
"blockEveryoneMicCamera": "停用所有人的麥克風和網路攝影機",
"breakoutRooms": "分組討論室",
"goLive": "開始直播",
"invite": "邀請他人",
"lowerAllHands": "全部取消舉手",
"lowerHand": "取消舉手",
"moreModerationActions": "更多主持人選項",
"moreModerationControls": "更多主持人操作",
"moreParticipantOptions": "更多與會者選項",
@@ -836,10 +906,13 @@
"headings": {
"lobby": "大廳({{count}} 人)",
"participantsList": "會議與會者({{count}} 人)",
"visitorInQueue": "{{count}} 人等候中)",
"visitorRequests": "{{count}} 人申請",
"visitors": "訪客({{count}} 人)",
"waitingLobby": "於大廳等候({{count}} 人)"
},
"search": "搜尋與會者",
"searchDescription": "輸入關鍵字篩選與會者",
"title": "與會者"
},
"passwordDigitsOnly": "上限為 {{number}} 位數",
@@ -848,10 +921,13 @@
"pinnedParticipant": "與會者被釘選",
"polls": {
"answer": {
"edit": "編輯",
"send": "送出",
"skip": "跳過",
"submit": "送出"
},
"by": "由 {{ name }}",
"closeButton": "結束投票",
"create": {
"addOption": "新增選項",
"answerPlaceholder": "選項 {{index}}",
@@ -861,6 +937,7 @@
"pollQuestion": "投票問題",
"questionPlaceholder": "詢問問題",
"removeOption": "移除選項",
"save": "儲存",
"send": "傳送"
},
"errors": {
@@ -890,9 +967,11 @@
"configuringDevices": "設定裝置中…",
"connectedWithAudioQ": "您有連接音訊設備嗎?",
"connection": {
"good": "您的連線品質良好",
"failed": "連線測試失敗!",
"good": "您的連線品質良好!",
"nonOptimal": "您的連線品質不理想",
"poor": "您的連線品質不佳"
"poor": "您的連線品質不佳",
"running": "正在測試連線中…"
},
"connectionDetails": {
"audioClipping": "您的音訊將會斷斷續續。",
@@ -901,6 +980,7 @@
"goodQuality": "太好了!您的媒體品質良好。",
"noMediaConnectivity": "我們無法為此測試建立媒體連線,通常是防火牆或 NAT 的問題。",
"noVideo": "您的視訊畫質將會很糟糕。",
"testFailed": "連線測試遇到異常,但不一定會影響會議。",
"undetectable": "如果您仍無法在瀏覽器中進行通話,我們建議您檢查喇叭、麥克風、及網路攝影機的設置,確認是否允許瀏覽器存取麥克風及網路攝影機,並將瀏覽器更新到最新版本。如果以上步驟無法解決問題,請聯絡網頁程式的開發者。",
"veryPoorConnection": "您的通話品質將會非常糟糕。",
"videoFreezing": "您的視訊將會突然黑頻、卡住、或像素化。",
@@ -933,6 +1013,7 @@
"or": "或",
"premeeting": "會議前",
"proceedAnyway": "仍然繼續",
"recordingWarning": "其他與會者可能正在錄製此通話",
"screenSharingError": "螢幕分享錯誤:",
"startWithPhone": "使用手機音訊開始",
"unsafeRoomConsent": "我了解風險,我想要加入會議",
@@ -998,7 +1079,6 @@
"limitNotificationDescriptionNative": "由於目前流量過大,您的錄製時間被限制在 {{limit}} 分鐘。若要無限制的錄製,請試試 <3>{{app}}</3>。",
"limitNotificationDescriptionWeb": "由於目前流量過大,您的錄製時間被限制在 {{limit}} 分鐘。若要無限制的錄製,請試試 <a href={{url}}rel='noopener noreferrer' target='_blank'>{{app}}</a>。",
"linkGenerated": "我們建立了您的錄製檔案的連結。",
"live": "直播",
"localRecordingNoNotificationWarning": "系統不會主動知會與會者錄製已開啟,主持人需另行通知。",
"localRecordingNoVideo": "沒有錄製的視訊",
"localRecordingStartWarning": "請確保在退出會議之前停用錄製以便保存。",
@@ -1015,13 +1095,16 @@
"onBy": "{{name}} 開始了錄製",
"onlyRecordSelf": "僅錄製我的音訊和影片串流",
"pending": "正在準備錄製會議…",
"rec": "錄製中",
"policyError": "您啟動錄製的速度過快,請稍後再試!",
"recordAudioAndVideo": "錄製音訊和視訊",
"recordTranscription": "錄製字幕內容",
"saveLocalRecording": "將錄製檔案保存在本機(測試版)",
"serviceDescription": "您的錄製會由錄製服務儲存",
"serviceDescriptionCloud": "雲端錄製",
"serviceDescriptionCloudInfo": "已錄製的會議將在 24 小時後自動清除。",
"serviceName": "錄製服務",
"sessionAlreadyActive": "已在錄製或直播此工作階段。",
"showAdvancedOptions": "進階選項",
"signIn": "登入",
"signOut": "登出",
"surfaceError": "請選擇目前分頁",
@@ -1056,11 +1139,13 @@
"signedIn": "目前正在存取 {{email}} 的行事曆事件,點按下方中斷連接可以停用存取行事曆事件。",
"title": "行事曆"
},
"chatWithPermissions": "聊天功能需取得權限",
"desktopShareFramerate": "桌面螢幕分享影格率",
"desktopShareHighFpsWarning": "較高的桌面螢幕分享影格率可能會影響您的頻寬,您必須重新啟動桌面螢幕分享以套用新的設定。",
"desktopShareWarning": "您必須重新啟動桌面螢幕分享以套用新的設定。",
"devices": "裝置",
"followMe": "全部人跟隨我",
"followMeRecorder": "錄影將跟隨我的視角",
"framesPerSecond": "fps",
"incomingMessage": "新訊息",
"language": "語言",
@@ -1084,6 +1169,7 @@
"selectMic": "麥克風",
"selfView": "自身螢幕",
"shortcuts": "快捷鍵",
"showSubtitlesOnStage": "在主畫面顯示字幕",
"speakers": "喇叭",
"startAudioMuted": "所有人啟動時處於靜音",
"startReactionsMuted": "關閉所有人反應音效",
@@ -1137,11 +1223,13 @@
"fearful": "可怕",
"happy": "笑臉",
"hours": "{{count}} 小時",
"labelTooltip": "與會者人數:{{count}}",
"minutes": "{{count}} 分",
"name": "名字",
"neutral": "中立",
"sad": "悲傷",
"search": "搜尋",
"searchDescription": "輸入關鍵字篩選與會者",
"searchHint": "搜尋與會者",
"seconds": "{{count}} 秒",
"speakerStats": "發言統計",
@@ -1169,7 +1257,7 @@
"audioOnly": "切換僅音訊",
"audioRoute": "選擇音訊裝置",
"boo": "倒喝彩",
"breakoutRoom": "進入/離開分組討論室",
"breakoutRooms": "分組討論室",
"callQuality": "管理視訊品質",
"carmode": "行車模式",
"cc": "切換字幕",
@@ -1178,6 +1266,7 @@
"closeChat": "關閉聊天",
"closeMoreActions": "關閉更多操作選單",
"closeParticipantsPane": "關閉與會者窗格",
"closedCaptions": "停用即時字幕",
"collapse": "收回",
"document": "切換檔案分享",
"documentClose": "關閉檔案分享",
@@ -1207,6 +1296,7 @@
"lobbyButton": "啟用/停用大廳模式",
"localRecording": "切換本機錄製控制",
"lockRoom": "切換會議密碼",
"love": "愛心",
"lowerHand": "放下手",
"moreActions": "更多動作",
"moreActionsMenu": "更多動作選單",
@@ -1217,13 +1307,14 @@
"muteEveryoneElsesVideoStream": "停止其他人的視訊",
"muteEveryonesVideoStream": "停止所有人的視訊",
"muteGUMPending": "正在連接您的麥克風",
"noiseSuppression": "雜訊抑制",
"noiseSuppression": "雜訊抑制BETA",
"openChat": "打開聊天",
"participants": "打開與會者窗格",
"pip": "切換子母螢幕模式",
"privateMessage": "傳送私人訊息",
"profile": "編輯您的個人檔案",
"raiseHand": "舉手",
"react": "訊息反應",
"reactions": "反應",
"reactionsMenu": "反應選單",
"recording": "切換錄製",
@@ -1266,14 +1357,15 @@
"closeChat": "關閉聊天",
"closeParticipantsPane": "關閉與會者窗格",
"closeReactionsMenu": "關閉反應選單",
"disableNoiseSuppression": "停用雜訊抑制",
"closedCaptions": "停用即時字幕",
"disableNoiseSuppression": "停用雜訊抑制BETA",
"disableReactionSounds": "您可以停用此會議的反應音效",
"documentClose": "關閉分享檔案欄",
"documentOpen": "開啟分享檔案欄",
"download": "下載我們的應用程式",
"e2ee": "端對端加密",
"embedMeeting": "嵌入會議",
"enableNoiseSuppression": "開啟雜訊抑制",
"enableNoiseSuppression": "開啟雜訊抑制BETA",
"endConference": "結束會議(所有人)",
"enterFullScreen": "放大全螢幕",
"enterTileView": "進入畫廊檢視",
@@ -1295,6 +1387,7 @@
"lobbyButtonEnable": "啟用大廳模式",
"login": "登入",
"logout": "登出",
"love": "愛心",
"lowerYourHand": "放下您的手",
"moreActions": "更多動作",
"moreOptions": "更多選項",
@@ -1307,7 +1400,7 @@
"noAudioSignalDialInDesc": "您亦可使用下述方式撥入:",
"noAudioSignalDialInLinkDesc": "撥入號碼",
"noAudioSignalTitle": "您的麥克風沒有訊號!",
"noiseSuppression": "雜訊抑制",
"noiseSuppression": "雜訊抑制BETA",
"noisyAudioInputDesc": "噪音聽起來是從您的麥克風傳來的,請考慮靜音或更換裝置。",
"noisyAudioInputTitle": "您的麥克風疑似有雜音!",
"openChat": "開啟聊天",
@@ -1320,6 +1413,7 @@
"raiseYourHand": "舉手",
"reactionBoo": "傳送倒喝彩反應",
"reactionClap": "傳送鼓掌反應",
"reactionHeart": "傳送愛心反應",
"reactionLaugh": "傳送大笑反應",
"reactionLike": "傳送比讚反應",
"reactionSilence": "傳送沉默反應",
@@ -1352,21 +1446,22 @@
},
"transcribing": {
"ccButtonTooltip": "啟動/停用字幕",
"error": "轉錄失敗,請再試一次。",
"expandedLabel": "轉錄已開啟",
"failedToStart": "轉錄啟動失敗",
"labelToolTip": "此會議正在轉錄",
"off": "轉錄已停用",
"pending": "準備轉錄會議…",
"failed": "轉錄失敗",
"labelTooltip": "此會議正在轉錄",
"labelTooltipExtra": "稍後將提供完整轉錄紀錄",
"openClosedCaptions": "開啟字幕",
"original": "原文",
"sourceLanguageDesc": "會議語言目前設定為 <b>{{sourceLanguage}}</b><br/> 您可以在這裡",
"sourceLanguageHere": "修改",
"start": "開始顯示字幕",
"stop": "停用顯示字幕",
"subtitles": "字幕",
"subtitlesOff": "關",
"tr": "轉錄"
"tr": "轉錄",
"translateTo": "翻譯成"
},
"unpinParticipant": "",
"unpinParticipant": "{{participantName}} - 取消釘選",
"userMedia": {
"grantPermissions": "請允許使用您的網路攝影機和麥克風的權限."
},
@@ -1404,6 +1499,7 @@
},
"videothumbnail": {
"connectionInfo": "連線資訊",
"demote": "轉為訪客",
"domute": "靜音",
"domuteOthers": "靜音其他人",
"domuteVideo": "停用網路攝影機",
@@ -1456,11 +1552,22 @@
},
"visitors": {
"chatIndicator": "(訪客)",
"joinMeeting": {
"description": "您目前以訪客身分參與本會議",
"raiseHand": "舉手",
"title": "正在加入會議",
"wishToSpeak": "若您想發言,請先舉手並等候主持人同意"
},
"labelTooltip": "訪客數量:{{count}}",
"notification": {
"description": "若要參與請舉手",
"demoteDescription": "由 {{actor}} 調整為訪客,若要參與請舉手",
"noMainParticipantsDescription": "須有主要與會者啟動會議,請稍後再試",
"noMainParticipantsTitle": "會議尚未開始",
"noVisitorLobby": "此會議啟用大廳,暫時無法加入",
"notAllowedPromotion": "需由與會者同意您的申請",
"title": "您是會議中的訪客"
}
},
"waitingMessage": "會議開始後您將自動加入!"
},
"volumeSlider": "音量滑桿",
"welcomepage": {
@@ -1518,6 +1625,7 @@
"whiteboard": {
"accessibilityLabel": {
"heading": "白板"
}
},
"screenTitle": "白板"
}
}

View File

@@ -109,6 +109,7 @@
}
},
"chat": {
"disabled": "Sending chat messages is disabled.",
"enter": "Enter room",
"error": "Error: your message was not sent. Reason: {{error}}",
"fieldPlaceHolder": "Aa",
@@ -124,7 +125,8 @@
"title": "Enter a nickname to use chat",
"titleWithCC": "Enter a nickname to use chat and closed captions",
"titleWithPolls": "Enter a nickname to use chat and polls",
"titleWithPollsAndCC": "Enter a nickname to use chat, polls and closed captions"
"titleWithPollsAndCC": "Enter a nickname to use chat, polls and closed captions",
"titleWithPollsAndCCAndFileSharing": "Enter a nickname to use chat, polls, closed captions and files"
},
"noMessagesMessage": "There are no messages in the meeting yet. Start a conversation here!",
"privateNotice": "Private message to {{recipient}}",
@@ -134,19 +136,21 @@
"tabs": {
"chat": "Chat",
"closedCaptions": "CC",
"fileSharing": "Files",
"polls": "Polls"
},
"title": "Chat",
"titleWithCC": "Chat and CC",
"titleWithPolls": "Chat and Polls",
"titleWithPollsAndCC": "Chat, Polls and CC",
"titleWithCC": "CC",
"titleWithFeatures": "Chat and",
"titleWithFileSharing": "Files",
"titleWithPolls": "Polls",
"you": "you"
},
"chromeExtensionBanner": {
"buttonText": "Install Chrome Extension",
"buttonTextEdge": "Install Edge Extension",
"close": "Close",
"dontShowAgain": "Dont show me this again",
"dontShowAgain": "Don't show me this again",
"installExtensionText": "Install the extension for Google Calendar and Office 365 integration"
},
"closedCaptionsTab": {
@@ -229,8 +233,8 @@
"joinInBrowser": "Join in browser",
"launchMeetingLabel": "How do you want to join this meeting?",
"launchWebButton": "Launch in web",
"noDesktopApp": "You dont have the app?",
"noMobileApp": "You dont have the app?",
"noDesktopApp": "You don't have the app?",
"noMobileApp": "You don't have the app?",
"or": "OR",
"termsAndConditions": "By continuing you agree to our <a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>terms & conditions.</a>",
"title": "Launching your meeting in {{app}}…",
@@ -322,7 +326,7 @@
"demoteParticipantDialog": "Are you sure you want to move this participant to viewer?",
"demoteParticipantTitle": "Move to viewer",
"dismiss": "Dismiss",
"displayNameRequired": "Hi! Whats your name?",
"displayNameRequired": "Hi! What's your name?",
"done": "Done",
"e2eeDescription": "End-to-End Encryption is currently EXPERIMENTAL. Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.",
"e2eeDisabledDueToMaxModeDescription": "Cannot enable End-to-End Encryption due to large number of participants in the conference.",
@@ -428,7 +432,7 @@
"reservationErrorMsg": "Error code: {{code}}, message: {{msg}}",
"retry": "Retry",
"screenSharingAudio": "Share audio",
"screenSharingFailed": "Oops! Something went wrong, we werent able to start screen sharing!",
"screenSharingFailed": "Oops! Something went wrong, we weren't able to start screen sharing!",
"screenSharingFailedTitle": "Screen sharing failed!",
"screenSharingPermissionDeniedError": "Oops! Something went wrong with your screen sharing permissions. Please reload and try again.",
"searchInSalesforce": "Search in Salesforce",
@@ -457,7 +461,7 @@
"shareScreenWarningD2": "you need to stop audio sharing, start screen sharing and check the \"share audio\" option.",
"shareScreenWarningH1": "If you want to share just your screen:",
"shareScreenWarningTitle": "You need to stop audio sharing before sharing your screen",
"shareVideoConfirmPlay": "Youre about to open an external website. Do you want to continue?",
"shareVideoConfirmPlay": "You're about to open an external website. Do you want to continue?",
"shareVideoConfirmPlayTitle": "{{name}} has shared a video with you.",
"shareVideoLinkError": "Oops, this video cannot be played.",
"shareVideoLinkStopped": "The video from {{name}} was stopped.",
@@ -539,6 +543,19 @@
"veryBad": "Very Bad",
"veryGood": "Very Good"
},
"fileSharing": {
"downloadFailedDescription": "Please try again.",
"downloadFailedTitle": "Download failed",
"downloadFile": "Download",
"dragAndDrop": "Drag and drop files here or anywhere on screen",
"fileAlreadyUploaded": "File has already been uploaded to this meeting.",
"fileTooLargeDescription": "Please make sure the file does not exceed {{ maxFileSize }}.",
"fileTooLargeTitle": "The selected file is too large",
"removeFile": "Remove",
"uploadFailedDescription": "Please try again.",
"uploadFailedTitle": "Upload failed",
"uploadFile": "Share file"
},
"filmstrip": {
"accessibilityLabel": {
"heading": "Video thumbnails"
@@ -648,7 +665,7 @@
"expandedOn": "The meeting is currently being live streamed",
"expandedPending": "The live streaming is being started…",
"failedToStart": "Live Streaming failed to start",
"getStreamKeyManually": "We werent able to fetch any live streams. Try getting your live stream key from YouTube.",
"getStreamKeyManually": "We weren't able to fetch any live streams. Try getting your live stream key from YouTube.",
"googlePrivacyPolicy": "Google Privacy Policy",
"inProgress": "Recording or live streaming in progress",
"invalidStreamKey": "Live stream key may be incorrect.",
@@ -889,9 +906,11 @@
"headings": {
"lobby": "Lobby ({{count}})",
"participantsList": "Meeting participants ({{count}})",
"viewerRequests": "Viewers requests {{count}}",
"visitorInQueue": " (waiting {{count}})",
"visitorRequests": " (requests {{count}})",
"visitors": "Viewers {{count}}",
"visitorsList": "Viewers ({{count}})",
"waitingLobby": "Waiting in lobby ({{count}})"
},
"search": "Search participants",
@@ -932,7 +951,7 @@
},
"results": {
"changeVote": "Change vote",
"empty": "There are no polls in the meeting yet. Start a poll here!",
"empty": "There are no polls in the meeting yet.",
"hideDetailedResults": "Hide details",
"showDetailedResults": "Show details",
"vote": "Vote"
@@ -948,7 +967,7 @@
"callMeAtNumber": "Call me at this number:",
"calling": "Calling",
"configuringDevices": "Configuring devices…",
"connectedWithAudioQ": "Youre connected with audio?",
"connectedWithAudioQ": "You're connected with audio?",
"connection": {
"failed": "Connection test failed!",
"good": "Your internet connection looks good!",
@@ -1545,7 +1564,7 @@
"notification": {
"demoteDescription": "Sent here by {{actor}}, raise your hand to participate",
"noMainParticipantsDescription": "A participant needs to start the meeting. Please try again in a bit.",
"noMainParticipantsTitle": "This meeting hasnt started yet.",
"noMainParticipantsTitle": "This meeting hasn't started yet.",
"noVisitorLobby": "You cannot join while there is a lobby enabled for the meeting.",
"notAllowedPromotion": "A participant needs to allow your request first.",
"title": "You are a viewer in the meeting"

View File

@@ -108,6 +108,7 @@ import {
} from '../../react/features/participants-pane/actions';
import { getParticipantsPaneOpen, isForceMuted } from '../../react/features/participants-pane/functions';
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 { getActiveSession, supportsLocalRecording } from '../../react/features/recording/functions';
import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/features/screen-share/actions';
@@ -209,6 +210,10 @@ function initCommands() {
}
APP.store.dispatch(grantModerator(participantId));
},
'grant-recording-consent': unmute => {
unmute ? APP.store.dispatch(grantRecordingConsentAndUnmute())
: APP.store.dispatch(grantRecordingConsent());
},
'display-name': displayName => {
sendAnalytics(createApiEvent('display.name.changed'));
APP.store.dispatch(updateSettings({ displayName: getNormalizedDisplayName(displayName) }));
@@ -769,7 +774,7 @@ function initCommands() {
}
if (transcription) {
APP.store.dispatch(setRequestingSubtitles(true, false, null));
APP.store.dispatch(setRequestingSubtitles(true, false, null, true));
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
isTranscribingEnabled: true
});
@@ -1918,6 +1923,19 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that the recording consent dialog open state has changed.
*
* @param {boolean} open - True if the dialog is open, false otherwise.
* @returns {void}
*/
notifyRecordingConsentDialogOpen(open) {
this._sendEvent({
name: 'recording-consent-dialog-open',
open
});
}
/**
* Notify external application of the current meeting requiring a password
* to join.

View File

@@ -38,6 +38,7 @@ const commands = {
endConference: 'end-conference',
email: 'email',
grantModerator: 'grant-moderator',
grantRecordingConsent: 'grant-recording-consent',
hangup: 'video-hangup',
hideNotification: 'hide-notification',
initiatePrivateChat: 'initiate-private-chat',
@@ -151,6 +152,7 @@ const events = {
'proxy-connection-event': 'proxyConnectionEvent',
'raise-hand-updated': 'raiseHandUpdated',
'ready': 'ready',
'recording-consent-dialog-open': 'recordingConsentDialogOpen',
'recording-link-available': 'recordingLinkAvailable',
'recording-status-changed': 'recordingStatusChanged',
'participant-menu-button-clicked': 'participantMenuButtonClick',

View File

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

3190
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,12 +16,13 @@
"author": "",
"readmeFilename": "README.md",
"dependencies": {
"@amplitude/react-native": "2.17.3",
"@amplitude/analytics-browser": "2.17.12",
"@amplitude/analytics-react-native": "1.4.13",
"@braintree/sanitize-url": "7.0.0",
"@emotion/react": "11.10.6",
"@emotion/styled": "11.10.6",
"@giphy/js-fetch-api": "4.7.1",
"@giphy/react-components": "6.8.1",
"@giphy/js-fetch-api": "4.9.3",
"@giphy/react-components": "6.9.4",
"@giphy/react-native-sdk": "2.3.0",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.19/jitsi-excalidraw-0.0.19.tgz",
"@jitsi/js-utils": "2.2.1",
@@ -54,6 +55,7 @@
"bc-css-flags": "3.0.0",
"clipboard-copy": "4.0.1",
"clsx": "1.1.1",
"dayjs": "1.11.13",
"dropbox": "10.7.0",
"focus-visible": "5.1.0",
"grapheme-splitter": "1.0.4",
@@ -67,10 +69,8 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1984.0.0+dd4c41be/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2025.0.0+49eb29a8/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
"pixelmatch": "5.3.0",
@@ -111,6 +111,7 @@
"react-native-youtube-iframe": "2.3.0",
"react-redux": "7.2.9",
"react-textarea-autosize": "8.3.0",
"react-virtualized-auto-sizer": "^1.0.26",
"react-window": "1.8.6",
"react-youtube": "10.1.0",
"redux": "4.0.4",
@@ -118,6 +119,7 @@
"seamless-scroll-polyfill": "2.1.8",
"semver": "7.5.4",
"text-encoding": "0.7.0",
"ts-ebml": "^3.0.1",
"tss-react": "4.9.4",
"util": "0.12.1",
"uuid": "8.3.2",
@@ -130,7 +132,7 @@
"@babel/plugin-transform-private-methods": "7.25.9",
"@babel/preset-env": "7.25.9",
"@babel/preset-react": "7.25.9",
"@jitsi/eslint-config": "6.0.1",
"@jitsi/eslint-config": "6.0.4",
"@react-native/metro-config": "0.75.5",
"@types/amplitude-js": "8.16.5",
"@types/audioworklet": "0.0.29",
@@ -138,9 +140,7 @@
"@types/js-md5": "0.4.3",
"@types/jsonwebtoken": "9.0.7",
"@types/lodash-es": "4.17.12",
"@types/minimatch": "5.1.2",
"@types/mocha": "10.0.10",
"@types/moment-duration-format": "2.2.6",
"@types/offscreencanvas": "2019.7.2",
"@types/pixelmatch": "5.2.5",
"@types/pretty": "2.0.3",
@@ -157,12 +157,12 @@
"@types/w3c-image-capture": "1.0.6",
"@types/w3c-web-hid": "1.0.3",
"@types/zxcvbn": "4.4.1",
"@wdio/allure-reporter": "9.4.3",
"@wdio/cli": "9.4.3",
"@wdio/globals": "9.4.3",
"@wdio/junit-reporter": "9.4.3",
"@wdio/local-runner": "9.4.3",
"@wdio/mocha-framework": "9.4.3",
"@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",
"babel-loader": "9.1.0",
"babel-plugin-optional-require": "0.3.1",
"circular-dependency-plugin": "5.2.0",
@@ -187,7 +187,7 @@
"ts-loader": "9.4.2",
"typescript": "5.7.2",
"unorm": "1.6.0",
"webdriverio": "9.4.3",
"webdriverio": "9.16.0",
"webpack": "5.95.0",
"webpack-bundle-analyzer": "4.4.2",
"webpack-cli": "5.1.4",

View File

@@ -22,7 +22,9 @@
"@react-navigation/stack": "0.0.0",
"@stomp/stompjs": "0.0.0",
"@xmldom/xmldom": "0.0.0",
"abab": "0.0.0",
"base64-js": "0.0.0",
"dayjs": "0.0.0",
"grapheme-splitter": "0.0.0",
"i18n-iso-countries": "0.0.0",
"i18next": "0.0.0",
@@ -32,8 +34,6 @@
"jwt-decode": "0.0.0",
"lib-jitsi-meet": "0.0.0",
"lodash-es": "0.0.0",
"moment": "0.0.0",
"moment-duration-format": "0.0.0",
"optional-require": "0.0.0",
"promise.allsettled": "0.0.0",
"promise.withresolvers": "0.0.0",
@@ -57,7 +57,7 @@
"zxcvbn": "0.0.0"
},
"peerDependencies": {
"@amplitude/react-native": "0.0.0",
"@amplitude/analytics-react-native": "0.0.0",
"@giphy/react-native-sdk": "0.0.0",
"@react-native-async-storage/async-storage": "0.0.0",
"@react-native-clipboard/clipboard": "0.0.0",

View File

@@ -84,7 +84,6 @@ export async function createHandlers({ getState }: IStore) {
} = config;
const {
amplitudeAPPKey,
amplitudeIncludeUTM,
blackListedEvents,
scriptURLs,
matomoEndpoint,
@@ -94,7 +93,6 @@ export async function createHandlers({ getState }: IStore) {
const { group, user } = state['features/base/jwt'];
const handlerConstructorOptions = {
amplitudeAPPKey,
amplitudeIncludeUTM,
blackListedEvents,
envType: deploymentInfo?.envType || 'dev',
matomoEndpoint,
@@ -187,7 +185,6 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
overwritesDefaultLogoUrl?: boolean;
overwritesDeploymentUrls?: boolean;
overwritesLiveStreamingUrls?: boolean;
overwritesSalesforceUrl?: boolean;
overwritesSupportUrl?: boolean;
server?: string;
tenant?: string;
@@ -229,7 +226,6 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
// TODO: Temporary metric. To be removed once we don't need it.
permanentProperties.overwritesSupportUrl = 'interfaceConfig.SUPPORT_URL' in params;
permanentProperties.overwritesSalesforceUrl = 'config.salesforceUrl' in params;
permanentProperties.overwritesDefaultLogoUrl = 'config.defaultLogoUrl' in params;
const deploymentUrlsConfig = params['config.deploymentUrls'] ?? {};

View File

@@ -11,7 +11,6 @@ export interface IEvent {
interface IOptions {
amplitudeAPPKey?: string;
amplitudeIncludeUTM?: boolean;
blackListedEvents?: string[];
envType?: string;
group?: string;

View File

@@ -1,22 +1,21 @@
import { Identify } from '@amplitude/analytics-core';
import logger from '../logger';
import AbstractHandler, { IEvent } from './AbstractHandler';
import { fixDeviceID } from './amplitude/fixDeviceID';
import amplitude from './amplitude/lib';
import amplitude, { initAmplitude } from './amplitude/lib';
/**
* Analytics handler for Amplitude.
*/
export default class AmplitudeHandler extends AbstractHandler {
_deviceId: string;
_userId: Object;
/**
* Creates new instance of the Amplitude analytics handler.
*
* @param {Object} options - The amplitude options.
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API.
* @param {boolean} options.amplitudeIncludeUTM - Whether to include UTM parameters
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API
* in the Amplitude events.
*/
constructor(options: any) {
@@ -24,62 +23,38 @@ export default class AmplitudeHandler extends AbstractHandler {
const {
amplitudeAPPKey,
amplitudeIncludeUTM: includeUtm = true,
user
} = options;
this._enabled = true;
const onError = (e: Error) => {
logger.error('Error initializing Amplitude', e);
this._enabled = false;
};
// Forces sending all events on exit (flushing) via sendBeacon
const onExitPage = () => {
// @ts-ignore
amplitude.getInstance().sendEvents();
};
if (navigator.product === 'ReactNative') {
amplitude.getInstance().init(amplitudeAPPKey);
fixDeviceID(amplitude.getInstance()).then(() => {
amplitude.getInstance().getDeviceId()
// @ts-ignore
.then((deviceId: string) => {
this._deviceId = deviceId;
});
initAmplitude(amplitudeAPPKey, user)
.then(() => {
logger.info('Amplitude initialized');
fixDeviceID(amplitude);
})
.catch(e => {
logger.error('Error initializing Amplitude', e);
this._enabled = false;
});
} else {
const amplitudeOptions: any = {
includeReferrer: true,
includeUtm,
saveParamsReferrerOncePerSession: false,
onError,
onExitPage
};
// @ts-ignore
amplitude.getInstance().init(amplitudeAPPKey, undefined, amplitudeOptions);
fixDeviceID(amplitude.getInstance());
}
if (user) {
this._userId = user;
amplitude.getInstance().setUserId(user);
}
}
/**
* Sets the Amplitude user properties.
*
* @param {Object} userProps - The user portperties.
* @param {Object} userProps - The user properties.
* @returns {void}
*/
setUserProperties(userProps: any) {
if (this._enabled) {
amplitude.getInstance().setUserProperties(userProps);
const identify = new Identify();
// Set all properties
Object.entries(userProps).forEach(([ key, value ]) => {
identify.set(key, value as any);
});
amplitude.identify(identify);
}
}
@@ -96,8 +71,9 @@ export default class AmplitudeHandler extends AbstractHandler {
return;
}
// @ts-ignore
amplitude.getInstance().logEvent(this._extractName(event) ?? '', event);
const eventName = this._extractName(event) ?? '';
amplitude.track(eventName, event);
}
/**
@@ -106,21 +82,10 @@ export default class AmplitudeHandler extends AbstractHandler {
* @returns {Object}
*/
getIdentityProps() {
if (navigator.product === 'ReactNative') {
return {
deviceId: this._deviceId,
userId: this._userId
};
}
return {
sessionId: amplitude.getInstance().getSessionId(),
// @ts-ignore
deviceId: amplitude.getInstance().options.deviceId,
// @ts-ignore
userId: amplitude.getInstance().options.userId
sessionId: amplitude.getSessionId(),
deviceId: amplitude.getDeviceId(),
userId: amplitude.getUserId()
};
}
}

View File

@@ -1,4 +1,4 @@
import { Amplitude } from '@amplitude/react-native';
import { Types } from '@amplitude/analytics-react-native';
import DefaultPreference from 'react-native-default-preference';
import { getUniqueId } from 'react-native-device-info';
@@ -8,16 +8,16 @@ import logger from '../../logger';
/**
* Custom logic for setting the correct device id.
*
* @param {AmplitudeClient} amplitude - The amplitude instance.
* @param {Types.ReactNativeClient} amplitude - The amplitude instance.
* @returns {void}
*/
export async function fixDeviceID(amplitude: Amplitude) {
export async function fixDeviceID(amplitude: Types.ReactNativeClient) {
await DefaultPreference.setName('jitsi-preferences');
const current = await DefaultPreference.get('amplitudeDeviceId');
if (current) {
await amplitude.setDeviceId(current);
amplitude.setDeviceId(current);
} else {
const uid = await getUniqueId();
@@ -27,7 +27,7 @@ export async function fixDeviceID(amplitude: Amplitude) {
return;
}
await amplitude.setDeviceId(uid as string);
amplitude.setDeviceId(uid as string);
await DefaultPreference.set('amplitudeDeviceId', uid as string);
}
}

View File

@@ -1,11 +1,37 @@
import { AmplitudeClient } from 'amplitude-js';
import { Types } from '@amplitude/analytics-browser';
// @ts-ignore
import { jitsiLocalStorage } from '@jitsi/js-utils';
import logger from '../../logger';
/**
* Key used to store the device id in local storage.
*/
const DEVICE_ID_KEY = '__AMDID';
/**
* Custom logic for setting the correct device id.
*
* @param {AmplitudeClient} _amplitude - The amplitude instance.
* @param {Types.BrowserClient} amplitude - The amplitude instance.
* @returns {void}
*/
export function fixDeviceID(_amplitude: AmplitudeClient): Promise<any> {
return new Promise(resolve => resolve(true));
export function fixDeviceID(amplitude: Types.BrowserClient) {
const deviceId = jitsiLocalStorage.getItem(DEVICE_ID_KEY);
if (deviceId) {
// Set the device id in Amplitude.
try {
amplitude.setDeviceId(JSON.parse(deviceId));
} catch (error) {
logger.error('Failed to set device ID in Amplitude', error);
return Promise.resolve(false);
}
} else {
const newDeviceId = amplitude.getDeviceId();
if (newDeviceId) {
jitsiLocalStorage.setItem(DEVICE_ID_KEY, JSON.stringify(newDeviceId));
}
}
}

View File

@@ -1,3 +1,15 @@
import { Amplitude } from '@amplitude/react-native';
import amplitude from '@amplitude/analytics-react-native';
export default Amplitude;
export default amplitude;
/**
* Initializes the Amplitude instance.
*
* @param {string} amplitudeAPPKey - The Amplitude app key.
* @param {string | undefined} user - The user ID.
* @returns {Promise} The initialized Amplitude instance.
*/
export function initAmplitude(
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
return amplitude.init(amplitudeAPPKey, user, {}).promise;
}

View File

@@ -1,3 +1,38 @@
import amplitude from 'amplitude-js';
import { createInstance } from '@amplitude/analytics-browser';
const amplitude = createInstance();
export default amplitude;
/**
* Initializes the Amplitude instance.
*
* @param {string} amplitudeAPPKey - The Amplitude app key.
* @param {string | undefined} user - The user ID.
* @returns {Promise} The initialized Amplitude instance.
*/
export function initAmplitude(
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
// Forces sending all events on exit (flushing) via sendBeacon.
window.addEventListener('pagehide', () => {
// Set https transport to use sendBeacon API.
amplitude.setTransport('beacon');
// Send all pending events to server.
amplitude.flush();
});
const options = {
autocapture: {
attribution: true,
pageViews: true,
sessions: false,
fileDownloads: false,
formInteractions: false,
elementInteractions: false
},
defaultTracking: false
};
return amplitude.init(amplitudeAPPKey, user, options).promise;
}

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
import { MEDIA_TYPE } from '../base/media/constants';
import type { MediaType } from '../base/media/constants';
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
import {
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED

View File

@@ -24,20 +24,17 @@ MiddlewareRegistry.register(() => (next: Function) => (action: AnyAction) => {
case APP_WILL_MOUNT: {
// Disable it inside an iframe until Google fixes the origin trial for 3rd party sources:
// https://bugs.chromium.org/p/chromium/issues/detail?id=1504167
if (!isEmbedded() && 'PressureObserver' in globalThis) {
if (!isEmbedded() && 'PressureObserver' in window) {
pressureObserver = new window.PressureObserver(
(records: typeof window.PressureRecord) => {
logger.info('Compute pressure state changed:', JSON.stringify(records));
if (typeof APP !== 'undefined') {
APP.API.notifyComputePressureChanged(records);
}
},
{ sampleRate: 1 }
APP.API.notifyComputePressureChanged(records);
}
);
try {
pressureObserver
.observe('cpu')
.observe('cpu', { sampleInterval: 30_000 })
.catch((e: any) => logger.error('CPU pressure observer failed to start', e));
} catch (e: any) {
logger.error('CPU pressure observer failed to start', e);

View File

@@ -64,9 +64,9 @@ 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, secondWord ] = initialsBasis.split(wordSplitRegex).filter(Boolean);
const [ firstWord, ...remainingWords ] = initialsBasis.split(wordSplitRegex).filter(Boolean);
return getFirstGraphemeUpper(firstWord) + getFirstGraphemeUpper(secondWord);
return getFirstGraphemeUpper(firstWord) + getFirstGraphemeUpper(remainingWords.pop() || '');
}
/**

View File

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

View File

@@ -295,7 +295,7 @@ export function getVisitorOptions(stateful: IStateful, vnode: string, focusJid:
return {
hosts: config.oldConfig.hosts,
focusUserJid: focusJid,
disableLocalStats: false,
disableLocalStatsBroadcast: false,
bosh: config.oldConfig.bosh && appendURLParam(config.oldConfig.bosh, 'customusername', username),
p2p: config.oldConfig.p2p,
websocket: config.oldConfig.websocket
@@ -330,7 +330,7 @@ export function getVisitorOptions(stateful: IStateful, vnode: string, focusJid:
},
focusUserJid: focusJid,
disableFocus: true, // This flag disables sending the initial conference request
disableLocalStats: true,
disableLocalStatsBroadcast: true,
bosh: config.bosh && appendURLParam(config.bosh, 'vnode', vnode),
p2p: {
...config.p2p,
@@ -394,19 +394,6 @@ export function isP2pActive(stateful: IStateful): boolean | null {
return conference.isP2PActive();
}
/**
* Returns whether the current conference has audio recording property which is on.
*
* @param {IStateful} stateful - The redux store, state, or {@code getState} function.
* @returns {boolean|null}
*/
export function isConferenceAudioRecordingOn(stateful: IStateful): boolean | null {
const state = getConferenceState(toState(stateful));
// @ts-ignore
return state.properties?.['audio-recording-enabled'] === 'true';
}
/**
* Returns the stored room name.
*

View File

@@ -16,18 +16,20 @@ import { IStore } from '../../app/types';
import { removeLobbyChatParticipant } from '../../chat/actions.any';
import { openDisplayNamePrompt } from '../../display-name/actions';
import { isVpaasMeeting } from '../../jaas/functions';
import { showErrorNotification, showNotification } from '../../notifications/actions';
import { clearNotifications, showErrorNotification, showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { INotificationProps } from '../../notifications/types';
import { hasDisplayName } from '../../prejoin/utils';
import { stopLocalVideoRecording } from '../../recording/actions.any';
import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager';
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
import { iAmVisitor } from '../../visitors/functions';
import { overwriteConfig } from '../config/actions';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, CONNECTION_WILL_CONNECT } from '../connection/actionTypes';
import { connectionDisconnected, disconnect } from '../connection/actions';
import { validateJwt } from '../jwt/functions';
import { JitsiConferenceErrors, JitsiConferenceEvents, JitsiConnectionErrors } from '../lib-jitsi-meet';
import { MEDIA_TYPE } from '../media/constants';
import { PARTICIPANT_UPDATED, PIN_PARTICIPANT } from '../participants/actionTypes';
import { PARTICIPANT_ROLE } from '../participants/constants';
import {
@@ -70,6 +72,7 @@ import {
} from './functions';
import logger from './logger';
import { IConferenceMetadata } from './reducer';
import './subscriber';
/**
* Handler for before unload event.
@@ -96,6 +99,11 @@ MiddlewareRegistry.register(store => next => action => {
case CONNECTION_FAILED:
return _connectionFailed(store, next, action);
case CONNECTION_WILL_CONNECT:
// we are starting a new join process, let's clear the error notifications if any from any previous attempt
store.dispatch(clearNotifications());
break;
case CONFERENCE_SUBJECT_CHANGED:
return _conferenceSubjectChanged(store, next, action);
@@ -261,7 +269,9 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
_removeUnloadHandler(getState);
}
if (enableForcedReload && error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED) {
if (enableForcedReload
&& (error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED
|| error?.name === JitsiConnectionErrors.SHARD_CHANGED_ERROR)) {
dispatch(conferenceWillLeave(conference));
dispatch(reloadNow());
}
@@ -653,7 +663,7 @@ function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAct
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) {
async function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) {
const track = action.track;
// TODO All track swapping should happen here instead of conference.js.
@@ -661,7 +671,6 @@ function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction)
const { getState } = store;
const state = getState();
const conference = getCurrentConference(state);
let promise;
if (conference) {
const jitsiTrack = action.track.jitsiTrack;
@@ -670,14 +679,22 @@ function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction)
// If gUM is slow and tracks are created after the user has already joined the conference, avoid
// adding the tracks to the conference if the user is a visitor.
if (!iAmVisitor(state)) {
promise = _addLocalTracksToConference(conference, [ jitsiTrack ]);
const { desktopAudioTrack } = state['features/screen-share'];
// If the user is sharing their screen and has a desktop audio track, we need to replace that with
// the audio mixer effect so that the desktop audio is mixed in with the microphone audio.
if (typeof APP !== 'undefined' && desktopAudioTrack && track.mediaType === MEDIA_TYPE.AUDIO) {
await conference.replaceTrack(desktopAudioTrack, null);
const audioMixerEffect = new AudioMixerEffect(desktopAudioTrack);
await jitsiTrack.setEffect(audioMixerEffect);
await conference.replaceTrack(null, jitsiTrack);
} else {
await _addLocalTracksToConference(conference, [ jitsiTrack ]);
}
}
} else {
promise = _removeLocalTracksFromConference(conference, [ jitsiTrack ]);
}
if (promise) {
return promise.then(() => next(action));
await _removeLocalTracksFromConference(conference, [ jitsiTrack ]);
}
}
}

View File

@@ -8,7 +8,7 @@ import { isPrejoinPageVisible } from '../../prejoin/functions';
import { iAmVisitor } from '../../visitors/functions';
import { CONNECTION_DISCONNECTED, CONNECTION_ESTABLISHED } from '../connection/actionTypes';
import { hangup } from '../connection/actions.web';
import { JitsiConferenceErrors, browser } from '../lib-jitsi-meet';
import { JitsiConferenceErrors, JitsiConnectionErrors, browser } from '../lib-jitsi-meet';
import { gumPending, setInitialGUMPromise } from '../media/actions';
import { MEDIA_TYPE } from '../media/constants';
import { IGUMPendingState } from '../media/types';
@@ -117,7 +117,9 @@ MiddlewareRegistry.register(store => next => action => {
case CONFERENCE_FAILED: {
const errorName = action.error?.name;
if (enableForcedReload && errorName === JitsiConferenceErrors.CONFERENCE_RESTARTED) {
if (enableForcedReload
&& (errorName === JitsiConferenceErrors.CONFERENCE_RESTARTED
|| errorName === JitsiConnectionErrors.SHARD_CHANGED_ERROR)) {
dispatch(setSkipPrejoinOnReload(true));
}
@@ -188,7 +190,8 @@ MiddlewareRegistry.register(store => next => action => {
return APP.conference.startConference(jitsiTracks);
});
})
.catch(logger.error);
});
} else {
promise.then(({ tracks }) => {

View File

@@ -54,6 +54,19 @@ const DEFAULT_STATE = {
};
export interface IConferenceMetadata {
files: {
[fileId: string]: {
authorParticipantJid: string;
authorParticipantName: string;
conferenceFullName: string;
fileId: string;
fileName: string;
fileSize: number;
fileType: string;
progress?: number;
timestamp: number;
};
};
recording?: {
isTranscribingEnabled: boolean;
};
@@ -85,6 +98,7 @@ export interface IJitsiConference {
end: Function;
getBreakoutRooms: Function;
getConnection: Function;
getFileSharing: Function;
getLocalParticipantProperty: Function;
getLocalTracks: Function;
getMeetingUniqueId: Function;
@@ -94,6 +108,7 @@ export interface IJitsiConference {
getParticipantCount: Function;
getParticipants: Function;
getRole: Function;
getShortTermCredentials: Function;
getSpeakerStats: () => ISpeakerStats;
getSsrcByTrack: Function;
getTranscriptionStatus: Function;
@@ -105,8 +120,6 @@ export interface IJitsiConference {
isLobbySupported: Function;
isP2PActive: Function;
isSIPCallingSupported: Function;
isStartAudioMuted: Function;
isStartVideoMuted: Function;
join: Function;
joinLobby: Function;
kickParticipant: Function;

View File

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

View File

@@ -86,15 +86,6 @@ export type Sounds = 'ASKED_TO_UNMUTE_SOUND' |
'RECORDING_ON_SOUND' |
'TALK_WHILE_MUTED_SOUND';
export interface IMobileDynamicLink {
apn: string;
appCode: string;
customDomain?: string;
ibi: string;
isi: string;
}
export interface IDeeplinkingPlatformConfig {
appName: string;
appScheme: string;
@@ -103,7 +94,6 @@ export interface IDeeplinkingPlatformConfig {
export interface IDeeplinkingMobileConfig extends IDeeplinkingPlatformConfig {
appPackage?: string;
downloadLink: string;
dynamicLink?: IMobileDynamicLink;
fDroidUrl?: string;
}
@@ -182,7 +172,6 @@ export interface IConfig {
_screenshotHistoryRegionUrl?: number;
analytics?: {
amplitudeAPPKey?: string;
amplitudeIncludeUTM?: boolean;
blackListedEvents?: string[];
disabled?: boolean;
matomoEndpoint?: string;
@@ -308,6 +297,7 @@ export interface IConfig {
disablePolls?: boolean;
disableProfile?: boolean;
disableReactions?: boolean;
disableReactionsInChat?: boolean;
disableReactionsModeration?: boolean;
disableRecordAudioNotification?: boolean;
disableRemoteControl?: boolean;
@@ -387,11 +377,18 @@ export interface IConfig {
feedbackPercentage?: number;
fileRecordingsServiceEnabled?: boolean;
fileRecordingsServiceSharingEnabled?: boolean;
fileSharing?: {
apiUrl?: string;
enabled?: boolean;
maxFileSize?: number;
};
filmstrip?: {
alwaysShowResizeBar?: boolean;
disableResizable?: boolean;
disableStageFilmstrip?: boolean;
disableTopPanel?: boolean;
disabled?: boolean;
initialWidth?: number;
minParticipantCountForTopPanel?: number;
};
flags?: {
@@ -438,6 +435,7 @@ export interface IConfig {
};
iAmRecorder?: boolean;
iAmSipGateway?: boolean;
iAmSpot?: boolean;
ignoreStartMuted?: boolean;
inviteAppName?: string | null;
inviteServiceCallFlowsUrl?: string;
@@ -484,6 +482,7 @@ export interface IConfig {
long?: number;
medium?: number;
short?: number;
sticky?: number;
};
notifications?: Array<string>;
notifyOnConferenceDestruction?: boolean;
@@ -554,7 +553,7 @@ export interface IConfig {
disableDemote?: boolean;
disableGrantModerator?: boolean;
disableKick?: boolean;
disablePrivateChat?: boolean;
disablePrivateChat?: 'all' | 'allow-moderator-chat';
disabled?: boolean;
};
replaceParticipant?: string;
@@ -596,6 +595,7 @@ export interface IConfig {
failICE?: boolean;
noAutoPlayVideo?: boolean;
p2pTestMode?: boolean;
showSpotConsentDialog?: boolean;
skipInterimTranscriptions?: boolean;
testMode?: boolean;
};
@@ -605,8 +605,9 @@ export interface IConfig {
};
tokenAuthUrl?: string;
tokenAuthUrlAutoRedirect?: string;
tokenGetUserInfoOutOfContext?: boolean;
tokenLogoutUrl?: string;
tokenRespectTenant?: string;
tokenRespectTenant?: boolean;
toolbarButtons?: Array<ToolbarButton>;
toolbarConfig?: {
alwaysVisible?: boolean;
@@ -621,6 +622,7 @@ export interface IConfig {
autoTranscribeOnRecord?: boolean;
disableClosedCaptions?: boolean;
enabled?: boolean;
inviteJigasiOnBackendTranscribing?: boolean;
preferredLanguage?: string;
translationLanguages?: Array<string>;
translationLanguagesHead?: Array<string>;

View File

@@ -109,6 +109,7 @@ export default [
'disablePolls',
'disableProfile',
'disableReactions',
'disableReactionsInChat',
'disableReactionsModeration',
'disableRecordAudioNotification',
'disableRemoteControl',
@@ -150,6 +151,7 @@ export default [
'enableTcc',
'faceLandmarks',
'feedbackPercentage',
'fileSharing.enabled',
'filmstrip',
'flags',
'forceTurnRelay',
@@ -169,6 +171,7 @@ export default [
'hideLobbyButton',
'iAmRecorder',
'iAmSipGateway',
'iAmSpot',
'ignoreStartMuted',
'inviteAppName',
'liveStreaming.enabled',

View File

@@ -193,6 +193,13 @@ export function overrideConfigJSON(config: IConfig, interfaceConfig: any, json:
* that are whitelisted.
*/
export function getWhitelistedJSON(configName: 'interfaceConfig' | 'config', configJSON: any): Object {
// Disable whitelisting in dev mode.
if (typeof __DEV__ !== 'undefined' && __DEV__) {
logger.warn('Whitelisting is disabled in dev mode, accepting any overrides');
return configJSON;
}
if (configName === 'interfaceConfig') {
return pick(configJSON, INTERFACE_CONFIG_WHITELIST);
} else if (configName === 'config') {
@@ -364,12 +371,6 @@ export function setConfigFromURLParams(
logger.warn('Using liveStreaming config URL overwrite and/or LIVE_STREAMING_HELP_LINK interfaceConfig URL'
+ ' overwrite is deprecated. Please use liveStreaming from advanced branding!');
}
if ('config.customToolbarButtons' in params) {
logger.warn('Using customToolbarButtons config URL overwrite is deprecated.'
+ ' Please use liveStreaming from advanced branding!');
}
}
/* eslint-enable max-params */

View File

@@ -77,12 +77,6 @@ export function _setDeeplinkingDefaults(deeplinking: IDeeplinkingConfig) {
ios.appScheme = ios.appScheme || 'org.jitsi.meet';
ios.downloadLink = ios.downloadLink
|| 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905';
if (ios.dynamicLink) {
ios.dynamicLink.apn = ios.dynamicLink.apn || 'org.jitsi.meet';
ios.dynamicLink.appCode = ios.dynamicLink.appCode || 'w2atb';
ios.dynamicLink.ibi = ios.dynamicLink.ibi || 'com.atlassian.JitsiMeet.ios';
ios.dynamicLink.isi = ios.dynamicLink.isi || '1165103905';
}
android.appName = android.appName || 'Jitsi Meet';
android.appScheme = android.appScheme || 'org.jitsi.meet';
@@ -90,10 +84,4 @@ export function _setDeeplinkingDefaults(deeplinking: IDeeplinkingConfig) {
|| 'https://play.google.com/store/apps/details?id=org.jitsi.meet';
android.appPackage = android.appPackage || 'org.jitsi.meet';
android.fDroidUrl = android.fDroidUrl || 'https://f-droid.org/packages/org.jitsi.meet/';
if (android.dynamicLink) {
android.dynamicLink.apn = android.dynamicLink.apn || 'org.jitsi.meet';
android.dynamicLink.appCode = android.dynamicLink.appCode || 'w2atb';
android.dynamicLink.ibi = android.dynamicLink.ibi || 'com.atlassian.JitsiMeet.ios';
android.dynamicLink.isi = android.dynamicLink.isi || '1165103905';
}
}

View File

@@ -6,6 +6,5 @@ export default [
'customToolbarButtons',
'defaultLogoUrl',
'deploymentUrls',
'liveStreaming',
'salesforceUrl'
'liveStreaming'
];

View File

@@ -2,6 +2,7 @@ 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';
@@ -79,12 +80,18 @@ function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyA
}));
}
if (action.config.filmstrip?.stageFilmstripParticipants !== undefined) {
const { initialWidth, stageFilmstripParticipants } = action.config.filmstrip || {};
if (stageFilmstripParticipants !== undefined) {
dispatch(updateSettings({
maxStageParticipants: action.config.filmstrip.stageFilmstripParticipants
maxStageParticipants: stageFilmstripParticipants
}));
}
if (initialWidth) {
dispatch(setUserFilmstripWidth(initialWidth));
}
dispatch(updateConfig(config));
// FIXME On Web we rely on the global 'config' variable which gets altered

View File

@@ -18,8 +18,7 @@ import {
IConfig,
IDeeplinkingConfig,
IDeeplinkingDesktopConfig,
IDeeplinkingMobileConfig,
IMobileDynamicLink
IDeeplinkingMobileConfig
} from './configType';
import { _cleanupConfig, _setDeeplinkingDefaults } from './functions';
@@ -186,6 +185,15 @@ function _setConfig(state: IConfig, { config }: { config: IConfig; }) {
});
}
const { alwaysShowResizeBar, disableResizable } = config.filmstrip || {};
if (alwaysShowResizeBar && disableResizable) {
config.filmstrip = {
...config.filmstrip,
alwaysShowResizeBar: false
};
}
const newState = merge(
{},
config,
@@ -321,15 +329,6 @@ function _translateInterfaceConfig(oldValue: IConfig) {
};
if (typeof interfaceConfig === 'object') {
const mobileDynamicLink = interfaceConfig.MOBILE_DYNAMIC_LINK;
const dynamicLink: IMobileDynamicLink | undefined = mobileDynamicLink ? {
apn: mobileDynamicLink.APN,
appCode: mobileDynamicLink.APP_CODE,
ibi: mobileDynamicLink.IBI,
isi: mobileDynamicLink.ISI,
customDomain: mobileDynamicLink.CUSTOM_DOMAIN
} : undefined;
if (deeplinking.desktop) {
deeplinking.desktop.appName = interfaceConfig.NATIVE_APP_NAME;
}
@@ -340,14 +339,12 @@ function _translateInterfaceConfig(oldValue: IConfig) {
appScheme: interfaceConfig.APP_SCHEME,
downloadLink: interfaceConfig.MOBILE_DOWNLOAD_LINK_ANDROID,
appPackage: interfaceConfig.ANDROID_APP_PACKAGE,
fDroidUrl: interfaceConfig.MOBILE_DOWNLOAD_LINK_F_DROID,
dynamicLink
fDroidUrl: interfaceConfig.MOBILE_DOWNLOAD_LINK_F_DROID
};
deeplinking.ios = {
appName: interfaceConfig.NATIVE_APP_NAME,
appScheme: interfaceConfig.APP_SCHEME,
downloadLink: interfaceConfig.MOBILE_DOWNLOAD_LINK_IOS,
dynamicLink
downloadLink: interfaceConfig.MOBILE_DOWNLOAD_LINK_IOS
};
}
newValue.deeplinking = deeplinking;

View File

@@ -1,13 +1,14 @@
import { appNavigate } from '../../app/actions.native';
import { IStore } from '../../app/types';
import { getCustomerDetails } from '../../jaas/actions.any';
import { isVpaasMeeting, getJaasJWT } from '../../jaas/functions';
import { getJaasJWT, isVpaasMeeting } from '../../jaas/functions';
import { navigateRoot } from '../../mobile/navigation/rootNavigationContainerRef';
import { screen } from '../../mobile/navigation/routes';
import { setJWT } from '../jwt/actions';
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
import { _connectInternal } from './actions.native';
import logger from './logger';
export * from './actions.any';
@@ -34,6 +35,8 @@ export function connect(id?: string, password?: string) {
j && dispatch(setJWT(j));
return dispatch(_connectInternal(id, password));
}).catch(e => {
logger.error('Connection error', e);
});
}

View File

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

View File

@@ -1,65 +1,81 @@
import moment from 'moment';
import momentDurationFormatSetup from 'moment-duration-format';
import dayjs from 'dayjs';
import durationPlugin from 'dayjs/plugin/duration';
import localizedFormatPlugin from 'dayjs/plugin/localizedFormat';
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
import i18next from './i18next';
// allows for moment durations to be formatted
momentDurationFormatSetup(moment);
dayjs.extend(durationPlugin);
dayjs.extend(relativeTimePlugin);
dayjs.extend(localizedFormatPlugin);
// MomentJS uses static language bundle loading, so in order to support dynamic
// Day.js uses static language bundle loading, so in order to support dynamic
// language selection in the app we need to load all bundles that we support in
// the app.
require('moment/locale/af');
require('moment/locale/ar');
require('moment/locale/be');
require('moment/locale/bg');
require('moment/locale/ca');
require('moment/locale/cs');
require('moment/locale/da');
require('moment/locale/de');
require('moment/locale/el');
require('moment/locale/en-gb');
require('moment/locale/eo');
require('moment/locale/es-us');
require('moment/locale/es');
require('moment/locale/et');
require('moment/locale/eu');
require('moment/locale/fa');
require('moment/locale/fi');
require('moment/locale/fr-ca');
require('moment/locale/fr');
require('moment/locale/gl');
require('moment/locale/he');
require('moment/locale/hr');
require('moment/locale/hu');
require('moment/locale/hy-am');
require('moment/locale/id');
require('moment/locale/is');
require('moment/locale/it');
require('moment/locale/ja');
require('moment/locale/ko');
require('moment/locale/lt');
require('moment/locale/lv');
require('moment/locale/ml');
require('moment/locale/mn');
require('moment/locale/mr');
require('moment/locale/nb');
require('moment/locale/nl');
require('moment/locale/oc-lnc');
require('moment/locale/pl');
require('moment/locale/pt');
require('moment/locale/pt-br');
require('moment/locale/ro');
require('moment/locale/ru');
require('moment/locale/sk');
require('moment/locale/sl');
require('moment/locale/sr');
require('moment/locale/sv');
require('moment/locale/tr');
require('moment/locale/uk');
require('moment/locale/vi');
require('moment/locale/zh-cn');
require('moment/locale/zh-tw');
import 'dayjs/locale/af';
import 'dayjs/locale/ar';
import 'dayjs/locale/be';
import 'dayjs/locale/bg';
import 'dayjs/locale/ca';
import 'dayjs/locale/cs';
import 'dayjs/locale/da';
import 'dayjs/locale/de';
import 'dayjs/locale/el';
import 'dayjs/locale/eo';
import 'dayjs/locale/es';
import 'dayjs/locale/es-us';
import 'dayjs/locale/et';
import 'dayjs/locale/eu';
import 'dayjs/locale/fa';
import 'dayjs/locale/fi';
import 'dayjs/locale/fr';
import 'dayjs/locale/fr-ca';
import 'dayjs/locale/gl';
import 'dayjs/locale/he';
import 'dayjs/locale/hi';
import 'dayjs/locale/hr';
import 'dayjs/locale/hu';
import 'dayjs/locale/hy-am';
import 'dayjs/locale/id';
import 'dayjs/locale/is';
import 'dayjs/locale/it';
import 'dayjs/locale/ja';
import 'dayjs/locale/ko';
import 'dayjs/locale/lt';
import 'dayjs/locale/lv';
import 'dayjs/locale/ml';
import 'dayjs/locale/mn';
import 'dayjs/locale/mr';
import 'dayjs/locale/nb';
import 'dayjs/locale/nl';
import 'dayjs/locale/oc-lnc';
import 'dayjs/locale/pl';
import 'dayjs/locale/pt';
import 'dayjs/locale/pt-br';
import 'dayjs/locale/ro';
import 'dayjs/locale/ru';
import 'dayjs/locale/sk';
import 'dayjs/locale/sl';
import 'dayjs/locale/sq';
import 'dayjs/locale/sr';
import 'dayjs/locale/sv';
import 'dayjs/locale/te';
import 'dayjs/locale/tr';
import 'dayjs/locale/uk';
import 'dayjs/locale/vi';
import 'dayjs/locale/zh-cn';
import 'dayjs/locale/zh-tw';
const LOCALE_MAPPING: Record<string, string> = {
// i18next -> dayjs
'hy': 'hy-am',
'oc': 'oc-lnc',
'zhCN': 'zh-cn',
'zhTW': 'zh-tw',
'ptBR': 'pt-br',
'esUS': 'es-us',
'frCA': 'fr-ca'
};
/**
* Returns a localized date formatter initialized with a specific {@code Date}
@@ -71,7 +87,7 @@ require('moment/locale/zh-tw');
* @returns {Object}
*/
export function getLocalizedDateFormatter(dateOrTimeStamp: Date | number) {
return moment(dateOrTimeStamp).locale(_getSupportedLocale());
return dayjs(dateOrTimeStamp).locale(_getSupportedLocale());
}
/**
@@ -84,23 +100,17 @@ export function getLocalizedDateFormatter(dateOrTimeStamp: Date | number) {
* @returns {Object}
*/
export function getLocalizedDurationFormatter(duration: number) {
// FIXME The flow-type definition of moment is v2.3 while our package.json
// states v2.19 so maybe locale on moment's duration was introduced in
// between?
//
// If the conference is under an hour long we want to display it without
// showing the hour and we want to include the hour if the conference is
// more than an hour long
// @ts-ignore
if (moment.duration(duration).format('h') !== '0') {
// @ts-ignore
return moment.duration(duration).format('h:mm:ss');
const d = dayjs.duration(duration);
if (d.hours() !== 0) {
return d.format('H:mm:ss');
}
// @ts-ignore
return moment.duration(duration).format('mm:ss', { trim: false });
return d.format('mm:ss');
}
/**
@@ -110,9 +120,18 @@ export function getLocalizedDurationFormatter(duration: number) {
* @returns {string}
*/
function _getSupportedLocale() {
const availableLocales = Object.keys(dayjs.Ls);
const i18nLocale = i18next.language;
let supportedLocale;
if (LOCALE_MAPPING[i18nLocale]) {
return LOCALE_MAPPING[i18nLocale];
}
if (availableLocales.includes(i18nLocale)) {
return i18nLocale;
}
if (i18nLocale) {
const localeRegexp = new RegExp('^([a-z]{2,2})(-)*([a-z]{2,2})*$');
const localeResult = localeRegexp.exec(i18nLocale.toLowerCase());
@@ -123,11 +142,7 @@ function _getSupportedLocale() {
`^${localeResult[1]}(-)*${`(${localeResult[3]})*` || ''}`);
supportedLocale
// FIXME The flow-type definition of moment is v2.3 while our
// package.json states v2.19 so maybe locales on moment was
// introduced in between?
= moment.locales().find(lang => currentLocaleRegexp.exec(lang));
= availableLocales.find(lang => currentLocaleRegexp.exec(lang));
}
}

View File

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

View File

@@ -1,3 +1,13 @@
/**
* The type of redux action which set an avatar URL for delayed loading.
*
* {
* type: SET_DELAYED_LOAD_OF_AVATAR_URL
* avatarUrl: string
* }
*/
export const SET_DELAYED_LOAD_OF_AVATAR_URL = 'SET_DELAYED_LOAD_OF_AVATAR_URL';
/**
* The type of redux action which stores a specific JSON Web Token (JWT) into
* the redux store.
@@ -8,3 +18,13 @@
* }
*/
export const SET_JWT = 'SET_JWT';
/**
* The type of redux action which sets a known avatar URL.
*
* {
* type: SET_KNOWN_AVATAR_URL,
* avatarUrl: string
* }
*/
export const SET_KNOWN_AVATAR_URL = 'SET_KNOWN_AVATAR_URL';

View File

@@ -1,11 +1,27 @@
import { SET_JWT } from './actionTypes';
import { SET_DELAYED_LOAD_OF_AVATAR_URL, SET_JWT, SET_KNOWN_AVATAR_URL } from './actionTypes';
/**
* Sets an avatar URL for delayed loading.
*
* @param {string} avatarUrl - The avatar URL to set for delayed loading.
* @returns {{
* type: SET_DELAYED_LOAD_OF_AVATAR_URL,
* avatarUrl: string
* }}
*/
export function setDelayedLoadOfAvatarUrl(avatarUrl?: string) {
return {
type: SET_DELAYED_LOAD_OF_AVATAR_URL,
avatarUrl
};
}
/**
* Stores a specific JSON Web Token (JWT) into the redux store.
*
* @param {string} [jwt] - The JSON Web Token (JWT) to store.
* @returns {{
* type: SET_TOKEN_DATA,
* type: SET_JWT,
* jwt: (string|undefined)
* }}
*/
@@ -15,3 +31,19 @@ export function setJWT(jwt?: string) {
jwt
};
}
/**
* Sets a known avatar URL.
*
* @param {string} avatarUrl - The avatar URL to set as known.
* @returns {{
* type: SET_KNOWN_AVATAR_URL,
* avatarUrl: string
* }}
*/
export function setKnownAvatarUrl(avatarUrl: string) {
return {
type: SET_KNOWN_AVATAR_URL,
avatarUrl
};
}

View File

@@ -7,6 +7,7 @@ export const MEET_FEATURES: Record<string, ParticipantFeaturesKey> = {
BRANDING: 'branding',
CALENDAR: 'calendar',
CREATE_POLLS: 'create-polls',
FILE_UPLOAD: 'file-upload',
FLIP: 'flip',
INBOUND_CALL: 'inbound-call',
LIVESTREAMING: 'livestreaming',
@@ -17,6 +18,7 @@ export const MEET_FEATURES: Record<string, ParticipantFeaturesKey> = {
ROOM: 'room',
SCREEN_SHARING: 'screen-sharing',
SEND_GROUPCHAT: 'send-groupchat',
LIST_VISITORS: 'list-visitors',
SIP_INBOUND_CALL: 'sip-inbound-call',
SIP_OUTBOUND_CALL: 'sip-outbound-call',
TRANSCRIPTION: 'transcription'

View File

@@ -4,19 +4,33 @@ import { AnyAction } from 'redux';
import { IStore } from '../../app/types';
import { isVpaasMeeting } from '../../jaas/functions';
import { getCurrentConference } from '../conference/functions';
import { SET_CONFIG } from '../config/actionTypes';
import { SET_LOCATION_URL } from '../connection/actionTypes';
import { CONNECTION_ESTABLISHED, SET_LOCATION_URL } from '../connection/actionTypes';
import { participantUpdated } from '../participants/actions';
import { getLocalParticipant } from '../participants/functions';
import { IParticipant } from '../participants/types';
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { parseURIString } from '../util/uri';
import { SET_JWT } from './actionTypes';
import { setJWT } from './actions';
import { setDelayedLoadOfAvatarUrl, setJWT, setKnownAvatarUrl } from './actions';
import { parseJWTFromURLParams } from './functions';
import logger from './logger';
/**
* Set up a state change listener to perform maintenance tasks when the conference
* is left or failed, e.g. Clear any delayed load avatar url.
*/
StateListenerRegistry.register(
state => getCurrentConference(state),
(conference, { dispatch }, previousConference): void => {
if (conference !== previousConference) {
dispatch(setDelayedLoadOfAvatarUrl());
}
});
/**
* Middleware to parse token data upon setting a new room URL.
*
@@ -31,7 +45,18 @@ MiddlewareRegistry.register(store => next => action => {
// XXX The JSON Web Token (JWT) is not the only piece of state that we
// have decided to store in the feature jwt
return _setConfigOrLocationURL(store, next, action);
case CONNECTION_ESTABLISHED: {
const state = store.getState();
const delayedLoadOfAvatarUrl = state['features/base/jwt'].delayedLoadOfAvatarUrl;
if (delayedLoadOfAvatarUrl) {
_overwriteLocalParticipant(store, {
avatarURL: delayedLoadOfAvatarUrl
});
store.dispatch(setDelayedLoadOfAvatarUrl());
store.dispatch(setKnownAvatarUrl(delayedLoadOfAvatarUrl));
}
}
case SET_JWT:
return _setJWT(store, next, action);
}
@@ -140,6 +165,7 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
if (jwtPayload) {
const { context, iss, sub } = jwtPayload;
const { tokenGetUserInfoOutOfContext, tokenRespectTenant } = state['features/base/config'];
action.jwt = jwt;
action.issuer = iss;
@@ -155,7 +181,6 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
const newUser = user ? { ...user } : {};
let features = context.features;
const { tokenRespectTenant } = state['features/base/config'];
// eslint-disable-next-line max-depth
if (!isVpaasMeeting(state) && tokenRespectTenant && context.tenant) {
@@ -167,6 +192,16 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
features = context.tenant === tenant || tenant === '' ? features : {};
}
if (newUser.avatarURL) {
const { knownAvatarUrl } = state['features/base/jwt'];
if (knownAvatarUrl !== newUser.avatarURL) {
store.dispatch(setDelayedLoadOfAvatarUrl(newUser.avatarURL));
newUser.avatarURL = undefined;
}
}
_overwriteLocalParticipant(
store, { ...newUser,
features });
@@ -175,7 +210,8 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
if (context.user && context.user.role === 'visitor') {
action.preferVisitor = true;
}
} else if (jwtPayload.name || jwtPayload.picture || jwtPayload.email) {
} else if (tokenGetUserInfoOutOfContext
&& (jwtPayload.name || jwtPayload.picture || jwtPayload.email)) {
// there are some tokens (firebase) having picture and name on the main level.
_overwriteLocalParticipant(store, {
avatarURL: jwtPayload.picture,

View File

@@ -1,14 +1,18 @@
import PersistenceRegistry from '../redux/PersistenceRegistry';
import ReducerRegistry from '../redux/ReducerRegistry';
import { equals } from '../redux/functions';
import { SET_JWT } from './actionTypes';
import { SET_DELAYED_LOAD_OF_AVATAR_URL, SET_JWT, SET_KNOWN_AVATAR_URL } from './actionTypes';
import logger from './logger';
export interface IJwtState {
callee?: {
name: string;
};
delayedLoadOfAvatarUrl?: string;
group?: string;
jwt?: string;
knownAvatarUrl?: string;
server?: string;
tenant?: string;
user?: {
@@ -17,6 +21,10 @@ export interface IJwtState {
};
}
PersistenceRegistry.register('features/base/jwt', {
knownAvatarUrl: true
});
/**
* Reduces redux actions which affect the JSON Web Token (JWT) stored in the
* redux store.
@@ -30,15 +38,35 @@ ReducerRegistry.register<IJwtState>(
'features/base/jwt',
(state = {}, action): IJwtState => {
switch (action.type) {
case SET_DELAYED_LOAD_OF_AVATAR_URL: {
const nextState = {
...state,
delayedLoadOfAvatarUrl: action.avatarUrl
};
if (equals(state, nextState)) {
return state;
}
logger.info('JWT avatarURL temporarily not loaded till jwt is verified on connect');
return nextState;
}
case SET_JWT: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { type, ...payload } = action;
const nextState = {
...state,
...payload
};
return equals(state, nextState) ? state : nextState;
}
case SET_KNOWN_AVATAR_URL:
return {
...state,
knownAvatarUrl: action.avatarUrl
};
}
return state;

View File

@@ -1,7 +1,10 @@
import NetInfo from '@react-native-community/netinfo';
import type { NetInfoState, NetInfoSubscription } from '@react-native-community/netinfo';
import {
default as NetInfo,
type NetInfoState,
type NetInfoSubscription
} from '@react-native-community/netinfo';
// eslint-disable-next-line lines-around-comment
// @ts-expect-error
// @ts-ignore
import EventEmitter from 'events';
import { ONLINE_STATE_CHANGED_EVENT } from './events';

View File

@@ -5,6 +5,7 @@ import { IReduxState, IStore } from '../../app/types';
import { isStageFilmstripAvailable } from '../../filmstrip/functions';
import { isAddPeopleEnabled, isDialOutEnabled } from '../../invite/functions';
import { toggleShareDialog } from '../../share-room/actions';
import { iAmVisitor } from '../../visitors/functions';
import { IStateful } from '../app/types';
import { GRAVATAR_BASE_URL } from '../avatar/constants';
import { isCORSAvatarURL } from '../avatar/functions';
@@ -413,6 +414,22 @@ export function getParticipantCountWithFake(stateful: IStateful) {
return remote.size + (local ? 1 : 0) + (localScreenShare ? 1 : 0);
}
/**
* Returns a count of the known participants in the passed in redux state,
* including fake participants. Subtract 1 when the local participant is a visitor as we do not show a local thumbnail.
* The number used to display the participant count in the UI.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @returns {number}
*/
export function getParticipantCountForDisplay(stateful: IStateful) {
const _iAmVisitor = iAmVisitor(stateful);
return getParticipantCount(stateful) - (_iAmVisitor ? 1 : 0);
}
/**
* Returns participant's display name.
*
@@ -806,3 +823,25 @@ export const setShareDialogVisiblity = (addPeopleFeatureEnabled: boolean, dispat
dispatch(toggleShareDialog(true));
}
};
/**
* Checks if private chat is enabled for the given participant.
*
* @param {IParticipant|undefined} participant - The participant to check.
* @param {IReduxState} state - The Redux state.
* @returns {boolean} - True if private chat is enabled, false otherwise.
*/
export function isPrivateChatEnabled(participant: IParticipant | undefined, state: IReduxState) {
const { remoteVideoMenu = {} } = state['features/base/config'];
const { disablePrivateChat } = remoteVideoMenu;
if (participant?.local || state['features/visitors'].iAmVisitor || disablePrivateChat === 'all') {
return false;
}
if (disablePrivateChat === 'allow-moderator-chat') {
return isLocalParticipantModerator(state) || isParticipantModerator(participant);
}
return !disablePrivateChat;
}

View File

@@ -58,8 +58,10 @@ export interface IParticipantFeatures {
'branding'?: boolean | string;
'calendar'?: boolean | string;
'create-polls'?: boolean | string;
'file-upload'?: boolean | string;
'flip'?: boolean | string;
'inbound-call'?: boolean | string;
'list-visitors'?: boolean | string;
'livestreaming'?: boolean | string;
'lobby'?: boolean | string;
'moderation'?: boolean | string;

View File

@@ -54,7 +54,7 @@ class Message extends Component<IProps> {
// Tokenize the text in order to avoid emoji substitution for URLs
const tokens = text ? text.split(' ') : [];
const content = [];
const content: any[] = [];
const { gifEnabled } = this.props;
// check if the message is a GIF
@@ -72,7 +72,11 @@ class Message extends Component<IProps> {
// Bypass the emojification when urls or matrix ids are involved
content.push(token);
} else {
content.push(...toArray(token, { className: 'smiley' }));
const emojified = [ ...toArray(token, { className: 'smiley' }) ];
content.push(
...emojified.some(item => typeof item === 'string') ? [ token ] : emojified
);
}
content.push(' ');

View File

@@ -1,5 +1,4 @@
import { Action, combineReducers } from 'redux';
import type { Reducer } from 'redux';
import { Action, type Reducer, combineReducers } from 'redux';
/**
* The type of the dictionary/map which associates a reducer (function) with the

View File

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

View File

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

View File

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

View File

@@ -4,8 +4,10 @@ import Icon from '../../icons/components/Icon';
import Tooltip from '../../tooltip/components/Tooltip';
import ContextMenuItem from '../../ui/components/web/ContextMenuItem';
import AbstractToolboxItem from './AbstractToolboxItem';
import type { IProps as AbstractToolboxItemProps } from './AbstractToolboxItem';
import {
default as AbstractToolboxItem,
type IProps as AbstractToolboxItemProps
} from './AbstractToolboxItem';
interface IProps extends AbstractToolboxItemProps {

View File

@@ -421,12 +421,14 @@ export function trackAdded(track: any) {
}
track.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED,
() => dispatch({
type: TRACK_STOPPED,
track: {
jitsiTrack: track
}
}));
() => {
logger.debug(`Local track stopped: ${track}, removing it from the conference`);
dispatch({
type: TRACK_STOPPED,
track: {
jitsiTrack: track
} });
});
} else {
participantId = track.getParticipantId();
isReceivingData = true;

View File

@@ -2,7 +2,6 @@ import { batch } from 'react-redux';
import { IStore } from '../../app/types';
import { _RESET_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
import { isPrejoinPageVisible } from '../../prejoin/functions';
import { getCurrentConference } from '../conference/functions';
import {
SET_AUDIO_MUTED,
@@ -203,11 +202,8 @@ function _setMuted(store: IStore, { ensureTrack, muted }: {
setTrackMuted(jitsiTrack, muted, state, dispatch)
.catch(() => dispatch(trackMuteUnmuteFailed(localTrack, muted)));
}
} else if (!muted && ensureTrack && (typeof APP === 'undefined' || isPrejoinPageVisible(state))) {
} else if (!muted && ensureTrack) {
typeof APP !== 'undefined' && dispatch(gumPending([ mediaType ], IGUMPendingState.PENDING_UNMUTE));
// FIXME: This only runs on mobile now because web has its own way of
// creating local tracks. Adjust the check once they are unified.
dispatch(createLocalTracksA({ devices: [ mediaType ] })).then(() => {
typeof APP !== 'undefined' && dispatch(gumPending([ mediaType ], IGUMPendingState.NONE));
});

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import Icon from '../../../icons/components/Icon';
import { IconCloseCircle } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { IInputProps } from '../types';
import { HiddenDescription } from './HiddenDescription';
interface IProps extends IInputProps {

View File

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

View File

@@ -1,14 +0,0 @@
// Base font size in pixels (standard is 16px = 1rem)
const BASE_FONT_SIZE = 16;
/**
* Converts rem to pixels.
*
* @param {string} remValue - The value in rem units (e.g. '0.875rem').
* @returns {number}
*/
export function remToPixels(remValue: string): number {
const numericValue = parseFloat(remValue.replace('rem', ''));
return Math.round(numericValue * BASE_FONT_SIZE);
}

View File

@@ -1,35 +1,6 @@
import { DefaultTheme } from 'react-native-paper';
import { createColorTokens } from './utils';
import { remToPixels } from './functions.any';
export * from './functions.any';
/**
* Converts all rem to pixels in an object.
*
* @param {Object} obj - The object to convert rem values in.
* @returns {Object}
*/
function convertRemValues(obj: any): any {
const converted: { [key: string]: any; } = {};
if (typeof obj !== 'object' || obj === null) {
return obj;
}
Object.entries(obj).forEach(([ key, value ]) => {
if (typeof value === 'string' && value.includes('rem')) {
converted[key] = remToPixels(value);
} else if (typeof value === 'object' && value !== null) {
converted[key] = convertRemValues(value);
} else {
converted[key] = value;
}
});
return converted;
}
/**
* Creates a React Native Paper theme based on local UI tokens.
@@ -42,10 +13,10 @@ export function createNativeTheme({ font, colorMap, shape, spacing, typography }
...DefaultTheme,
palette: createColorTokens(colorMap),
shape,
spacing: spacing.map(remToPixels),
spacing,
typography: {
font,
...convertRemValues(typography)
...typography
}
};
}

View File

@@ -3,9 +3,7 @@ import { Theme, adaptV4Theme, createTheme } from '@mui/material/styles';
import { ITypography, IPalette as Palette1 } from '../ui/types';
import { createColorTokens } from './utils';
export * from './functions.any';
import { createColorTokens, createTypographyTokens } from './utils';
declare module '@mui/material/styles' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
@@ -20,7 +18,7 @@ interface ThemeProps {
colorMap: Object;
font: Object;
shape: Object;
spacing: Array<number | string>;
spacing: Array<number>;
typography: Object;
}
@@ -38,7 +36,7 @@ export function createWebTheme({ font, colorMap, shape, spacing, typography, bre
typography: {
// @ts-ignore
font,
...typography
...createTypographyTokens(typography)
},
breakpoints
}));
@@ -117,5 +115,5 @@ export function operatesWithEnterKey(element: Element): boolean {
* @returns {number}
*/
export function getVideospaceFloatingElementsBottomSpacing(theme: Theme, isToolbarVisible: boolean) {
return isToolbarVisible ? theme.spacing(12) : theme.spacing(6);
return parseInt(isToolbarVisible ? theme.spacing(12) : theme.spacing(6), 10);
}

View File

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

View File

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

View File

@@ -12,13 +12,7 @@ const JITSI_MEET_APPS = [
'org.jitsi.meet',
// Android debug app.
'org.jitsi.meet.debug',
// 8x8 Work (Android).
'org.vom8x8.sipua',
// 8x8 Work (iOS).
'com.yourcompany.Virtual-Office'
'org.jitsi.meet.debug'
];
/**

View File

@@ -8,10 +8,11 @@
export function loadScript(url: string): Promise<void> {
return new Promise((resolve, reject) =>
JitsiMeetJS.util.ScriptUtil.loadScript(
url,
/* async */ true,
/* prepend */ false,
/* relativeURL */ false,
/* loadCallback */ resolve,
/* errorCallback */ reject));
{ src: url,
async: true,
prepend: false,
relativeURL: false,
loadCallback: resolve,
errorCallback: reject
}));
}

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { Component } from 'react';
import { ReactReduxContext } from 'react-redux';
import { IMessage } from '../types';
@@ -16,6 +17,9 @@ export interface IProps {
* @augments PureComponent
*/
export default class AbstractMessageContainer<P extends IProps, S> extends Component<P, S> {
static override contextType = ReactReduxContext;
declare context: React.ContextType<typeof ReactReduxContext>;
static defaultProps = {
messages: [] as IMessage[]
};
@@ -33,9 +37,17 @@ export default class AbstractMessageContainer<P extends IProps, S> extends Compo
let currentGrouping: IMessage[] = [];
let currentGroupParticipantId;
const { store } = this.context;
const state = store.getState();
const { disableReactionsInChat } = state['features/base/config'];
for (let i = 0; i < messagesCount; i++) {
const message = this.props.messages[i];
if (message.isReaction && disableReactionsInChat) {
continue;
}
if (message.participantId === currentGroupParticipantId) {
currentGrouping.push(message);
} else {

View File

@@ -1,6 +1,7 @@
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { Platform, View, ViewStyle } from 'react-native';
import { Platform, TextStyle, View, ViewStyle } from 'react-native';
import { Text } from 'react-native-paper';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
@@ -10,9 +11,9 @@ import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
import IconButton from '../../../base/ui/components/native/IconButton';
import Input from '../../../base/ui/components/native/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { isSendGroupChatDisabled } from '../../functions';
import styles from './styles';
import { isSendGroupChatDisabled } from '../../functions';
interface IProps extends WithTranslation {
@@ -92,6 +93,18 @@ class ChatInputBar extends Component<IProps, IState> {
inputBarStyles = styles.inputBarNarrow;
}
if (this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId) {
return (
<View
id = 'no-messages-message'
style = { styles.disabledSendWrapper as ViewStyle }>
<Text style = { styles.emptyComponentText as TextStyle }>
{ this.props.t('chat.disabled') }
</Text>
</View>
);
}
return (
<View
id = 'chat-input'
@@ -112,8 +125,7 @@ class ChatInputBar extends Component<IProps, IState> {
returnKeyType = 'send'
value = { this.state.message } />
<IconButton
disabled = { !this.state.message
|| (this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId) }
disabled = { !this.state.message }
id = { this.props.t('chat.sendButton') }
onPress = { this._onSubmit }
src = { IconSend }

View File

@@ -38,6 +38,11 @@ export default {
flex: 1
},
chatDisabled: {
padding: BaseTheme.spacing[2],
textAlign: 'center'
},
emptyComponentText: {
color: BaseTheme.palette.text03,
textAlign: 'center'
@@ -115,6 +120,15 @@ export default {
maxWidth: '80%'
},
disabledSendWrapper: {
alignSelf: 'center',
flex: 0,
padding: BoxModel.padding,
paddingBottom: '8%',
paddingTop: '8%',
maxWidth: '80%'
},
/**
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
*/

View File

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

View File

@@ -1,10 +1,13 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
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';
interface IProps {
@@ -34,9 +37,11 @@ interface IProps {
*
* @returns {React$Element<any>}
*/
function ChatHeader({ className, isPollsEnabled, isCCTabEnabled }: IProps) {
function ChatHeader({ className, isCCTabEnabled, isPollsEnabled }: IProps) {
const dispatch = useDispatch();
const { t } = useTranslation();
const { focusedTab } = useSelector((state: IReduxState) => state['features/chat']);
const fileSharingTabEnabled = useSelector(isFileSharingEnabled);
const onCancel = useCallback(() => {
dispatch(toggleChat());
@@ -51,12 +56,14 @@ function ChatHeader({ className, isPollsEnabled, isCCTabEnabled }: IProps) {
let title = 'chat.title';
if (isCCTabEnabled && isPollsEnabled) {
title = 'chat.titleWithPollsAndCC';
} else if (isCCTabEnabled) {
title = 'chat.titleWithCC';
} else if (isPollsEnabled) {
title = 'chat.titleWithPolls';
if (focusedTab === ChatTabs.CHAT) {
title = 'chat.tabs.chat';
} else if (isPollsEnabled && focusedTab === ChatTabs.POLLS) {
title = 'chat.tabs.polls';
} else if (isCCTabEnabled && focusedTab === ChatTabs.CLOSED_CAPTIONS) {
title = 'chat.tabs.closedCaptions';
} else if (fileSharingTabEnabled && focusedTab === ChatTabs.FILE_SHARING) {
title = 'chat.tabs.fileSharing';
}
return (

View File

@@ -1,6 +1,8 @@
import { Theme } from '@mui/material';
import React, { Component, RefObject } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { withStyles } from 'tss-react/mui';
import { IReduxState, IStore } from '../../../app/types';
import { isMobileBrowser } from '../../../base/environment/utils';
@@ -8,10 +10,41 @@ import { translate } from '../../../base/i18n/functions';
import { IconFaceSmile, IconSend } from '../../../base/icons/svg';
import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
import { CHAT_SIZE } from '../../constants';
import { areSmileysDisabled, isSendGroupChatDisabled } from '../../functions';
import SmileysPanel from './SmileysPanel';
const styles = (_theme: Theme, { _chatWidth }: IProps) => {
return {
smileysPanel: {
bottom: '100%',
boxSizing: 'border-box' as const,
backgroundColor: 'rgba(0, 0, 0, .6) !important',
height: 'auto',
display: 'flex' as const,
overflow: 'hidden',
position: 'absolute' as const,
width: `${_chatWidth - 32}px`,
marginBottom: '5px',
marginLeft: '-5px',
transition: 'max-height 0.3s',
'& #smileysContainer': {
backgroundColor: '#131519',
borderTop: '1px solid #A4B8D1'
}
},
chatDisabled: {
borderTop: `1px solid ${_theme.palette.ui02}`,
boxSizing: 'border-box' as const,
padding: _theme.spacing(4),
textAlign: 'center' as const,
}
};
};
/**
* The type of the React {@code Component} props of {@link ChatInput}.
*/
@@ -22,6 +55,9 @@ interface IProps extends WithTranslation {
*/
_areSmileysDisabled: boolean;
_chatWidth: number;
/**
* Whether sending group chat messages is disabled.
*/
@@ -32,6 +68,11 @@ interface IProps extends WithTranslation {
*/
_privateMessageRecipientId?: string;
/**
* An object containing the CSS classes.
*/
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
/**
* Invoked to send chat messages.
*/
@@ -123,6 +164,17 @@ class ChatInput extends Component<IProps, IState> {
* @returns {ReactElement}
*/
override render() {
const classes = withStyles.getClasses(this.props);
const hideInput = this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId;
if (hideInput) {
return (
<div className = { classes.chatDisabled }>
{this.props.t('chat.disabled')}
</div>
);
}
return (
<div className = { `chat-input-container${this.state.message.trim().length ? ' populated' : ''}` }>
<div id = 'chat-input' >
@@ -130,7 +182,7 @@ class ChatInput extends Component<IProps, IState> {
<div
className = 'smiley-input'>
<div
className = 'smileys-panel' >
className = { classes.smileysPanel } >
<SmileysPanel
onSmileySelect = { this._onSmileySelect } />
</div>
@@ -150,8 +202,7 @@ class ChatInput extends Component<IProps, IState> {
value = { this.state.message } />
<Button
accessibilityLabel = { this.props.t('chat.sendButton') }
disabled = { !this.state.message.trim()
|| (this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId) }
disabled = { !this.state.message.trim() }
icon = { IconSend }
onClick = { this._onSubmitMessage }
size = { isMobileBrowser() ? 'large' : 'medium' } />
@@ -291,14 +342,15 @@ class ChatInput extends Component<IProps, IState> {
* }}
*/
const mapStateToProps = (state: IReduxState) => {
const { privateMessageRecipient } = state['features/chat'];
const { privateMessageRecipient, width } = state['features/chat'];
const isGroupChatDisabled = isSendGroupChatDisabled(state);
return {
_areSmileysDisabled: areSmileysDisabled(state),
_privateMessageRecipientId: privateMessageRecipient?.id,
_isSendGroupChatDisabled: isGroupChatDisabled
_isSendGroupChatDisabled: isGroupChatDisabled,
_chatWidth: width.current ?? CHAT_SIZE,
};
};
export default translate(connect(mapStateToProps)(ChatInput));
export default translate(connect(mapStateToProps)(withStyles(ChatInput, styles)));

View File

@@ -5,7 +5,7 @@ import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { getParticipantDisplayName } from '../../../base/participants/functions';
import { getParticipantById, getParticipantDisplayName, isPrivateChatEnabled } from '../../../base/participants/functions';
import Popover from '../../../base/popover/components/Popover.web';
import Message from '../../../base/react/components/web/Message';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
@@ -412,10 +412,12 @@ const ChatMessage = ({
*/
function _mapStateToProps(state: IReduxState, { message }: IProps) {
const { knocking } = state['features/lobby'];
const localParticipantId = state['features/base/participants'].local?.id;
const participant = getParticipantById(state, message.participantId);
const enablePrivateChat = isPrivateChatEnabled(participant, state);
return {
shouldDisplayChatMessageMenu: message.participantId !== localParticipantId,
shouldDisplayChatMessageMenu: enablePrivateChat,
knocking,
state
};

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import Icon from '../../../base/icons/components/Icon';
import { IconSubtitles } from '../../../base/icons/svg';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
@@ -15,7 +16,6 @@ import { ISubtitle } from '../../../subtitles/types';
import { isTranscribing } from '../../../transcribing/functions';
import { SubtitlesMessagesContainer } from './SubtitlesMessagesContainer';
import { IReduxState } from '../../../app/types';
/**
* The styles for the ClosedCaptionsTab component.
@@ -88,6 +88,7 @@ export default function ClosedCaptionsTab() {
const _isTranscribing = useSelector(isTranscribing);
const _canStartSubtitles = useSelector(canStartSubtitles);
const [ isButtonPressed, setButtonPressed ] = useState(false);
const subtitlesError = useSelector((state: IReduxState) => state['features/subtitles']._hasError);
const filteredSubtitles = useMemo(() => {
// First, create a map of transcription messages by message ID
@@ -128,6 +129,10 @@ export default function ClosedCaptionsTab() {
setButtonPressed(true);
}, [ dispatch, isButtonPressed, setButtonPressed ]);
if (subtitlesError && isButtonPressed) {
setButtonPressed(false);
}
if (!_isTranscribing) {
if (_canStartSubtitles) {
return (

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