Compare commits

...

85 Commits

Author SHA1 Message Date
Calinteodor
8a4990d9ae sdk(react-native-sdk): rnsdk screenshare android fix (#13884)
sdk(react-native-sdk): rnsdk screenshare android fix
2023-11-07 12:22:02 +02:00
Дамян Минков
0e55cbbda6 Clean up prosody modules with some extra checks (#14020)
* fix: Adds check for jitsi_meet_room not being string.

Oct 20 12:22:50 mod_bosh        error   Traceback[bosh]: /usr/share/jitsi-meet/prosody-plugins/token/util.lib.lua:336: bad argument #1 to 'lower' (string expected, got userdata)
        stack traceback:
        [C]: in function 'lower'
        /usr/share/jitsi-meet/prosody-plugins/token/util.lib.lua:336: in function 'verify_room'
        ...re/jitsi-meet/prosody-plugins/mod_token_verification.lua:78: in function 'verify_user'

* fix: Adds check for missing speaker stats for occupant.

error   Traceback[c2s]: ...itsi-meet/prosody-plugins/mod_speakerstats_component.lua:124: attempt to index field '?' (a nil value)
        stack traceback:
        ...itsi-meet/prosody-plugins/mod_speakerstats_component.lua:124: in function '?'

* fix: Nil check for breakout_rooms.

c2saaaad95a16c0 error   Traceback[c2s]: ...re/jitsi-meet/prosody-plugins/mod_muc_breakout_rooms.lua:345: attempt to index local 'main_room' (a nil value)
        stack traceback:
        ...re/jitsi-meet/prosody-plugins/mod_muc_breakout_rooms.lua:345: in function '?'
        /usr/share/lua/5.2/prosody/util/events.lua:81: in function </usr/share/lua/5.2/prosody/util/events.lua:77>
        (...tail calls...)
        /usr/lib/prosody/modules/muc/muc.lib.lua:496: in function </usr/lib/prosody/modules/muc/muc.lib.lua:492>

* fix: Adds nil check in allowners.

c2saaaae3024810 error   Traceback[c2s]: /usr/share/jitsi-meet/prosody-plugins/mod_muc_allowners.lua:171: attempt to index local 'room' (a nil value)
        stack traceback:
        /usr/share/jitsi-meet/prosody-plugins/mod_muc_allowners.lua:171: in function '?'
        /usr/share/lua/5.2/prosody/util/events.lua:81: in function </usr/share/lua/5.2/prosody/util/events.lua:77>

* fix: Adds nil check in lobby.

mod_bosh        error   Traceback[bosh]: ...share/jitsi-meet/prosody-plugins/mod_muc_lobby_rooms.lua:168: attempt to index local 'lobby_room' (a nil value)
        stack traceback:
        ...share/jitsi-meet/prosody-plugins/mod_muc_lobby_rooms.lua:168: in function '?'
        /usr/share/lua/5.2/prosody/util/filters.lua:25: in function 'filter'
        /usr/lib/prosody/modules/mod_bosh.lua:361: in function 'send'
        /usr/lib/prosody/modules/muc/mod_muc.lua:495: in function '?'

* fix: Fixes nil error in fmuc.

s2sinaaaaf2817260       error   Traceback[s2s]: /usr/share/jitsi-meet/prosody-plugins/mod_fmuc.lua:295: attempt to index local 'occupant' (a nil value)
        stack traceback:
        /usr/share/jitsi-meet/prosody-plugins/mod_fmuc.lua:295: in function '?'
        /usr/share/lua/5.2/prosody/util/events.lua:81: in function </usr/share/lua/5.2/prosody/util/events.lua:77>
        (...tail calls...)
        /usr/lib/prosody/modules/muc/muc.lib.lua:1201: in function </usr/lib/prosody/modules/muc/muc.lib.lua:1194>

* fix: Fixes nil occupant.

c2s55f4d5411dd0 error   Traceback[c2s]: /usr/share/jitsi-meet/prosody-plugins/mod_muc_flip.lua:120: attempt to index local 'kicked_occupant' (a nil value)
        stack traceback:
        /usr/share/jitsi-meet/prosody-plugins/mod_muc_flip.lua:120: in function '?'
        /usr/share/lua/5.2/prosody/util/events.lua:81: in function </usr/share/lua/5.2/prosody/util/events.lua:77>
        (...tail calls...)
        /usr/lib/prosody/modules/muc/muc.lib.lua:791: in function </usr/lib/prosody/modules/muc/muc.lib.lua:616>

* fix: Fixes caching main room.

Objects should not be set in room._data as this field is being serialized and we see errors like.

error   Traceback[c2s]: /usr/share/lua/5.2/prosody/util/serialization.lua:34: Can't serialize userdata
        stack traceback:
        [C]: in function 'error'
        /usr/share/lua/5.2/prosody/util/serialization.lua:34: in function </usr/share/lua/5.2/prosody/util/serialization.lua:33>
        (...tail calls...)
        /usr/share/lua/5.2/prosody/util/serialization.lua:199: in function 'serialize_table'
        /usr/share/lua/5.2/prosody/util/serialization.lua:197: in function 'serialize_table'
        /usr/share/lua/5.2/prosody/util/serialization.lua:197: in function 'serialize_table'
        /usr/share/lua/5.2/prosody/util/serialization.lua:219: in function </usr/share/lua/5.2/prosody/util/serialization.lua:217>
        (...tail calls...)
        /usr/lib/prosody/modules/mod_storage_memory.lua:42: in function </usr/lib/prosody/modules/mod_storage_memory.lua:40>
        (...tail calls...)
        ...re/jitsi-meet/prosody-plugins/mod_muc_breakout_rooms.lua:207: in function 'create_breakout_room'

* fix: Fixes calling save_occupant after changing its role.

* squash: Fixed passed value to type.
2023-11-06 15:31:59 -06:00
damencho
6da94aecf2 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1718.0.0+30be4f6f...v1719.0.0+f8a18cf0
2023-11-06 09:40:48 -06:00
Calinteodor
2a3c962e88 feat(recent-list): fix undefined error that breaks visitor joining (#14024)
* feat(recent-list): fix undefined error that breaks visitor joining

* feat(recent-list): revert variable name change

* feat(recent-list): fixed linter
2023-11-06 09:40:28 -06:00
AHMAD KADRI
34f1eb60f4 Accessibility: add validation warning on room name (#14009)
feat(accessibility): add validation warning on room name
2023-11-06 10:59:51 +02:00
Jaya Allamsetty
4115ebe856 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1717.0.0+7b6ce949...v1718.0.0+30be4f6f
2023-11-02 15:36:11 -04:00
Horatiu Muresan
d7dadfc157 feat(facing-mode) add config for initial camera facing mode (#14013) 2023-11-02 16:20:38 +02:00
Erin Yuki Schlarb
2851eeeab3 fix: Make room_metadata Prosody module depend on the required jitsi_session module
Without this room_metadata will silently discard all room metadata client requests assuming that they didn’t come from Jitsi meet clients.

Fixes #14001
2023-11-01 17:06:26 -05:00
Muhammed Ajmal M
84d75f2ae8 fix(screen-sharing) Self view of SC sized correctly initially (#13992) 2023-11-01 18:32:34 +02:00
damencho
73b3309adf feat: Adds leave rate limit to muc_rate_limit. 2023-10-31 15:59:23 -05:00
Jaya Allamsetty
e2de06f60d chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1716.0.0+93c167d3...v1717.0.0+7b6ce949
2023-10-31 14:35:31 -04:00
damencho
cdc7962d11 feat: Adds region parameter to dial out authorize requests. 2023-10-31 11:45:06 -05:00
Saúl Ibarra Corretgé
59242e1217 feat(external-api) introduce a "ready" event
It's fired when the API is ready, and it signals the embedder that they
can reveal the meeting from behind an overlay, for example.

The astute reader might notice we are currently sending a
'browser-support' event roughly at the same time. The reason for this
new event is plain simply semantics.

In addition the 'onload' handler is faked by calling it when the new
ready event fires. The original onload event is unreliable. It will be
called even when nothing was ever loaded (try loading a page without
internet and be amused).
2023-10-31 16:27:12 +01:00
Saúl Ibarra Corretgé
631e39d4fd feat(external-api) allow vh and vw values as parameters 2023-10-31 16:27:12 +01:00
Julian LADJANI
4290cdf53d fix(breakout-rooms, feature-flags): handle breakout button feature flag on participant pane footer component (#14003)
* fix(breakout-rooms, feature-flags): handle breakout button feature flag on participant pane footer component
2023-10-31 13:53:41 +02:00
damencho
84c1e20216 fix(moderated): Fixes moderators in moderated rooms without tenant. 2023-10-30 17:26:42 -05:00
Saúl Ibarra Corretgé
e6caeb86b0 chore(deps,rn) react-native-webrtc@111.0.6 2023-10-30 15:46:14 +01:00
Saúl Ibarra Corretgé
5854e38a09 fix(rn) allow default server URL to be set from native
On Android we support RestrictionManager, but that already sets it, so
make sure we always save it on the settings.

Editing will be restricted in the Settings dialog if changing it is
restricted anyway.

Fixes: https://github.com/jitsi/jitsi-meet/issues/13994
2023-10-30 15:12:05 +01:00
Saúl Ibarra Corretgé
3e9ee9451f fix(android) fix crash on Android 14
Fixes: https://github.com/jitsi/jitsi-meet/issues/13998
2023-10-30 15:11:19 +01:00
Calinteodor
29d02f0a2b feat(chat/native): fixed keyboard overlapping chat input bar (#13984)
* feat(chat/native): fixed keyboard overlapping chat input bar
2023-10-30 12:59:05 +02:00
David Hall
c780f9bbba Update main-sv.json
Fix typos.
2023-10-29 20:53:01 +01:00
Jaya Allamsetty
d5a0bac0a3 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1713.0.0+a1d7b0ea...v1716.0.0+93c167d3
2023-10-28 14:45:24 -04:00
5idereal
f0187cc0f8 lang: update zhTW translation (#13986)
* update zhTW translation

* Update main.json
2023-10-26 10:47:22 -05:00
damencho
4708d894cc fix: Adds a nil check in visitors module.
Sep 22 22:06:01 mod_bosh        error   Traceback[bosh]: /usr/share/jitsi-meet/prosody-plugins/mod_visitors.lua:305: attempt to index field '?' (a nil value)
        stack traceback:
        /usr/share/jitsi-meet/prosody-plugins/mod_visitors.lua:305: in function '?'
        /usr/share/lua/5.2/prosody/util/events.lua:81: in function </usr/share/lua/5.2/prosody/util/events.lua:77>
2023-10-26 09:38:23 -05:00
damencho
f38d120406 fix(visitors): Bumps queue size for waiting for jicofo.
500 is the maximum meeting participants we test and support.
2023-10-24 18:27:16 -05:00
damencho
53960baf76 fix(visitors): Fixes filtering initial msgs to main participants.
Filters initial msg for <subject/>.
2023-10-24 18:27:16 -05:00
Jaya Allamsetty
a0f061aa6f chore(deps): Update lib-jitsi-meet. 2023-10-23 17:42:32 -04:00
Jaya Allamsetty
f2fb525d0a ref(config) Drop forceJVB121Ratio from config.js 2023-10-23 17:42:32 -04:00
Jaya Allamsetty
5a59bee597 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1711.0.0+8ec3b736...v1712.0.0+540aed1e
2023-10-23 16:10:10 -04:00
Дамян Минков
07b903d887 feat(visitors): Adds an option to request to be visitor based on jwt. (#13977)
* feat(visitors): Adds an option to request to be visitor based on jwt.

* squash: Updates ljm.
2023-10-23 12:07:03 -05:00
Mihaela Dumitru
1a39315001 feat(whiteboard) expose the excalidraw api (#13974) 2023-10-23 09:22:42 +03:00
Gabriel Borlea
97e5f00dae fix(electron-screensharing): simplify the proccess (#13967)
* fix(electron-screensharing): simplify the proccess
2023-10-20 19:55:06 +03:00
Дамян Минков
bae77f21f8 feat: Adds event for parsed jwt and check for required token.context (#13973)
* squash: Remove tabs.

* feat: Adds a check for context required in jwt.

* feat: Adds an event to notify for parsed jwt.
2023-10-20 08:50:38 -05:00
José Luís Andrade
24d788f333 Update Portuguese translation 2023-10-20 06:50:20 -05:00
damencho
c4d553c605 feat: Filter iq rayo respects the actor of grant moderation. 2023-10-18 14:00:21 -05:00
Mehmet
fa64e2e67c fix(visitors): informs visitor nodes when a participant is kicked. (#13951)
* fix(visitors): informs visitor nodes when a participant is kicked.

* remove hooking muc-broadcast-presence event and create a stanza in muc-occupant-left event.
2023-10-17 07:54:02 -05:00
Damien Fetis
94c29180e4 fix(whiteboard) fix room id generation
Fixes: https://github.com/jitsi/jitsi-meet/issues/13921
2023-10-17 11:09:34 +02:00
dependabot[bot]
b864d91572 chore(deps): bump @babel/traverse in /react-native-sdk
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.15 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 17:57:49 +02:00
Saúl Ibarra Corretgé
2006182a2a fix(deps) run npm audit fix 2023-10-16 17:57:25 +02:00
Saúl Ibarra Corretgé
8fc3de416c feat(config) add ability to prefer BOSH over WebSocket
There might be cases where we'd want to enforce it.
2023-10-16 17:56:34 +02:00
Jaya Allamsetty
4c5787511e chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1698.0.0+03cb3ce8...v1704.0.0+d3efd197
2023-10-16 11:12:19 -04:00
Duduman Bogdan Vlad
8a2e4bc628 feat(screenshare) - add web security fix for electron (#13096)
use send the share screen sources using the external api

---------

Co-authored-by: Gabriel Borlea <gabriel.borlea@8x8.com>
2023-10-16 14:59:55 +03:00
Calinteodor
f78ebbb9a9 feat(settings/native/android): Fixed scroll inside conference settings screen (#13956)
* feat(settings/native/android): fixed scroll inside conference settings screen.
2023-10-16 14:10:52 +03:00
Calin-Teodor
4cc4c25691 feat(prejoin/native): fix display name input on prejoin 2023-10-16 13:13:30 +03:00
Avram Tudor
d02c7dc3a7 i18n: change iframe disabled message based on current hostname (#13950)
* i18n: change iframe disabled message based on current hostname

This will allow us to remove translation overrides from branded repo

* fix linter
2023-10-13 15:05:24 +03:00
Mihaela Dumitru
8741ee771e fix(language/config) deprecate defaultLanguage (#13949) 2023-10-13 14:06:59 +03:00
Mihaela Dumitru
006e8463cd feat(whiteboard) add user limit (#13870) 2023-10-13 13:41:31 +03:00
Saúl Ibarra Corretgé
86e295e9bc fix(conference) clear raised hands when conference changes
Fixes: https://github.com/jitsi/jitsi-meet-sdk-samples/issues/175
2023-10-12 15:03:59 +02:00
keremoge
07bade2557 feat(deps,rn) update React Native to version 0.69.12 2023-10-12 13:18:40 +03:00
Hristo Terezov
0becc890d8 feat(track-state): Log on add/remove/mute/owner. 2023-10-11 16:39:06 -05:00
Hristo Terezov
a1ce6f1ce5 fed(UI): remove UI.setAudioMuted 2023-10-11 16:39:06 -05:00
otbutz
43a7d00c63 web: enable http2 support 2023-10-11 18:22:12 +02:00
Calinteodor
9c04ba767c feat(breakout-rooms/native): separate breakout rooms from participants (#13920)
feat(breakout-rooms/native): separate breakout rooms from participants
2023-10-11 17:34:49 +03:00
Calinteodor
7e1d10fb4d sdk(react-native-sdk): update readme 2023-10-10 12:03:43 +02:00
Saúl Ibarra Corretgé
4ce2280e31 fix(rnsdk,build) run npm install after syncing deps 2023-10-09 15:21:32 +02:00
Saúl Ibarra Corretgé
2918a89d35 fix(rnsdk,build) don't commit the result after bumping version
Just like the other version bumping scripts
2023-10-09 15:21:32 +02:00
malik tekin
8f1c83edfd fix(lang) update Turkish translation
The Turkish translation of the "adjust for" is "ayarla". It was misspelled as "ayala".
2023-10-09 11:01:24 +02:00
eemehmet
106452d857 fix(visitors): Fixes duplicated messages sent from guest domain. 2023-10-06 10:33:23 -05:00
Horatiu Muresan
a4d3fb6c70 fix(notifications) Fix case when description is react component instance (#13919) 2023-10-06 15:20:56 +03:00
Saúl Ibarra Corretgé
a7af01b9e3 fix(screen-sharing) remove stop screen sharing icon 2023-10-06 10:31:47 +02:00
emrah
f7f434ab55 fix(config): add missing notification keys into the list 2023-10-06 10:31:23 +02:00
Esra Hatice YILMAZ
09c0854779 fix(breakout-rooms) fix race condition in timer handling 2023-10-05 22:56:53 +02:00
Horatiu Muresan
b4d12d74f7 fix(aot) remove dependency to store (#13910) 2023-10-05 13:05:15 +03:00
Gabriel Borlea
50b064907a fix(environment): optimal browsers list 2023-10-05 11:14:50 +03:00
dependabot[bot]
b9d6a0f269 chore(deps): bump postcss and css-loader
Bumps [postcss](https://github.com/postcss/postcss) to 8.4.31 and updates ancestor dependency [css-loader](https://github.com/webpack-contrib/css-loader). These dependencies need to be updated together.


Updates `postcss` from 7.0.39 to 8.4.31
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/7.0.39...8.4.31)

Updates `css-loader` from 3.6.0 to 6.8.1
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v3.6.0...v6.8.1)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: indirect
- dependency-name: css-loader
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-05 10:04:58 +02:00
Calinteodor
2414e57260 feat(shared-video/native): use local logger (#13886)
* feat(shared-video/native): use local logger
2023-10-04 13:13:54 +03:00
Horatiu Muresan
6c41ddb622 fix(aot) fix icons creating deps to store (#13901) 2023-10-03 17:31:04 +03:00
damencho
55e75d56fd fix: Fixes skipping the lobby for two times in a row for jibri.
An issue where a livestreaming is started for a second time in a meeting with lobby turned on.
2023-10-02 11:22:27 -05:00
Calinteodor
32ac299422 feat(authentication/native): hide login button for 8x8.vc (#13881)
* feat(authentication): hide login button for 8x8.vc
2023-10-02 18:03:57 +03:00
Christoph Settgast
cb7146f954 lang: update German translation 2023-09-29 12:28:22 -05:00
damencho
144c1ce4f4 fix: Fixes passing the hash params in state for token auth URL. 2023-09-29 12:11:52 -05:00
Gabriel Borlea
2102d6eda1 chore(deps): update js-utils to 2.2.1 2023-09-29 18:45:36 +03:00
damencho
1f8e3fe26f fix: Fixes wait for host to respect moderated tenants.
The correct place to check for tenant value is jitsi_meet_domain and not jitsi_meet_context_group.
2023-09-29 09:54:02 -05:00
Andrei Gavrilescu
8b0285a9d7 chore(deps) lib-jitsi-meet@latest (#13891)
https://github.com/jitsi/lib-jitsi-meet/compare/v1695.0.0+51c2187b...v1698.0.0+03cb3ce8
2023-09-29 17:02:31 +03:00
Mihaela Dumitru
b546d01c2d fix(prejoin) improve display name handling relative to configs (#13865) 2023-09-29 16:17:35 +03:00
Saúl Ibarra Corretgé
7bf3e7df1d fix(rn,polyfills) remove no longer needed polyfill
Fixes: https://github.com/jitsi/jitsi-meet/issues/13807
2023-09-28 12:23:09 +02:00
damencho
f9ac965e18 feat: Updates for jwt when room claim is not required.
This allows mod_token_verification to be used with token missing room claim (firebase) and jitsi tokens with the claim.
2023-09-27 19:29:16 -05:00
Gabriel Borlea
d70412166c chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1694.0.0+a0145343...v1695.0.0+51c2187b
2023-09-27 16:20:19 +03:00
Дамян Минков
a843406cb0 feat: Check jwt expiration and redirects to auth url if any. (#13879)
* feat: Check jwt expiration and redirects to auth url if any.

It may happen that the jwt had expired while being in the meeting and there is a network issue requiring to reload.

* squash: Fixes lint error.

* squash: Fixes comments.
2023-09-27 07:40:07 -05:00
AHMAD KADRI
58115477a2 Improve accessibility in breakout rooms list (#13669)
Improve accessibility in breakout rooms list
2023-09-27 12:13:03 +03:00
Hristo Terezov
e1dc573c3c fix(GUM):set deviceId only when the device exists 2023-09-26 12:53:08 -05:00
Hristo Terezov
c025102511 feat(devices): Filter MS Teams Audio device 2023-09-26 12:53:08 -05:00
Gabriel Borlea
54d052de73 chore: update js-utils with new ua-parser (#13877)
* chore: update js-utils with new ua-parser

* chore(deps) lib-jitsi-meet@latest
2023-09-26 19:31:26 +03:00
Javier García
7e633f0136 fix: Also check single quote on jitsi-meet-tokens install (#13869)
Fixes #13768.
2023-09-26 11:25:38 -05:00
Gabriel Borlea
4b4bc1c823 chore(deps) lib-jitsi-meet@latest (#13871)
https://github.com/jitsi/lib-jitsi-meet/compare/v1691.0.0+255d8f49...v1693.0.0+c3a086f8
2023-09-25 18:21:58 +03:00
168 changed files with 4229 additions and 2671 deletions

4
.gitignore vendored
View File

@@ -99,6 +99,10 @@ tsconfig.json
#
react-native-sdk/*.tgz
react-native-sdk/android/src
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JitsiMeetOngoingConferenceService.java
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JitsiMeetReactNativePackage.java
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/JMOngoingConferenceModule.java
!react-native-sdk/android/src/main/java/org/jitsi/meet/sdk/RNOngoingNotification.java
react-native-sdk/images
react-native-sdk/ios
react-native-sdk/lang

View File

@@ -65,7 +65,11 @@ import {
updateDeviceList
} from './react/features/base/devices/actions.web';
import {
areDevicesDifferent,
filterIgnoredDevices,
flattenAvailableDevices,
getDefaultDeviceId,
logDevices,
setAudioOutputDeviceId
} from './react/features/base/devices/functions.web';
import {
@@ -632,7 +636,7 @@ export default {
// so that the user can try unmute later on and add audio/video
// to the conference
if (!tracks.find(t => t.isAudioTrack())) {
this.setAudioMuteStatus(true);
this.updateAudioIconEnabled();
}
if (!tracks.find(t => t.isVideoTrack())) {
@@ -841,7 +845,7 @@ export default {
// 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.setAudioMuteStatus(mute);
this.updateAudioIconEnabled();
return;
} else if (this.isLocalAudioMuted() === mute) {
@@ -1390,7 +1394,7 @@ export default {
APP.store.dispatch(
replaceLocalTrack(oldTrack, newTrack, room))
.then(() => {
this.setAudioMuteStatus(this.isLocalAudioMuted());
this.updateAudioIconEnabled();
})
.then(resolve)
.catch(reject)
@@ -2241,19 +2245,28 @@ export default {
* @returns {Promise}
*/
async _onDeviceListChanged(devices) {
const oldDevices = APP.store.getState()['features/base/devices'].availableDevices;
const localAudio = getLocalJitsiAudioTrack(APP.store.getState());
const localVideo = getLocalJitsiVideoTrack(APP.store.getState());
const state = APP.store.getState();
const { filteredDevices, ignoredDevices } = filterIgnoredDevices(devices);
const oldDevices = state['features/base/devices'].availableDevices;
APP.store.dispatch(updateDeviceList(devices));
if (!areDevicesDifferent(flattenAvailableDevices(oldDevices), filteredDevices)) {
return Promise.resolve();
}
logDevices(ignoredDevices, 'Ignored devices on device list changed:');
const localAudio = getLocalJitsiAudioTrack(state);
const localVideo = getLocalJitsiVideoTrack(state);
APP.store.dispatch(updateDeviceList(filteredDevices));
// Firefox users can choose their preferred device in the gUM prompt. In that case
// we should respect that and not attempt to switch to the preferred device from
// our settings.
const newLabelsOnly = mediaDeviceHelper.newDeviceListAddedLabelsOnly(oldDevices, devices);
const newLabelsOnly = mediaDeviceHelper.newDeviceListAddedLabelsOnly(oldDevices, filteredDevices);
const newDevices
= mediaDeviceHelper.getNewMediaDevicesAfterDeviceListChanged(
devices,
filteredDevices,
localVideo,
localAudio,
newLabelsOnly);
@@ -2386,7 +2399,7 @@ export default {
return Promise.all(promises)
.then(() => {
APP.UI.onAvailableDevicesChanged(devices);
APP.UI.onAvailableDevicesChanged(filteredDevices);
});
},
@@ -2662,15 +2675,6 @@ export default {
APP.UI.setVideoMuted(this.getMyUserId());
},
/**
* Sets the audio muted status.
*
* @param {boolean} muted - New muted status.
*/
setAudioMuteStatus(muted) {
APP.UI.setAudioMuted(this.getMyUserId(), muted);
},
/**
* Dispatches the passed in feedback for submission. The submitted score
* should be a number inclusively between 1 through 5, or -1 for no score.

View File

@@ -51,6 +51,9 @@ var config = {
// Websocket URL (XMPP)
// websocket: 'wss://jitsi-meet.example.com/' + subdir + 'xmpp-websocket',
// Whether BOSH should be preferred over WebSocket if both are configured.
// preferBosh: false,
// The real JID of focus participant - can be overridden here
// Do not change username - FIXME: Make focus username configurable
// https://github.com/jitsi/jitsi-meet/issues/7376
@@ -215,6 +218,9 @@ var config = {
// Video
// Sets the default camera facing mode.
// cameraFacingMode: 'user',
// Sets the preferred resolution (height) for local video. Defaults to 720.
// resolution: 720,
@@ -632,6 +638,7 @@ var config = {
// hideDominantSpeakerBadge: false,
// Default language for the user interface. Cannot be overwritten.
// DEPRECATED! Use the `lang` iframe option directly instead.
// defaultLanguage: 'en',
// Disables profile and the edit of all fields from the profile settings (display name and email)
@@ -1509,7 +1516,6 @@ var config = {
disableLocalStats
disableNS
enableTalkWhileMuted
forceJVB121Ratio
forceTurnRelay
hiddenDomain
hiddenFromRecorderFeatureEnabled
@@ -1533,6 +1539,7 @@ var config = {
*/
// notifications: [
// 'connection.CONNFAIL', // shown when the connection fails,
// 'dialog.cameraConstraintFailedError', // shown when the camera failed
// 'dialog.cameraNotSendingData', // shown when there's no feed from user's camera
// 'dialog.kickTitle', // shown when user has been kicked
// 'dialog.liveStreaming', // livestreaming notifications (pending, on, off, limits)
@@ -1543,6 +1550,7 @@ var config = {
// 'dialog.recording', // recording notifications (pending, on, off, limits)
// 'dialog.remoteControlTitle', // remote control notifications (allowed, denied, start, stop, error)
// 'dialog.reservationError',
// 'dialog.screenSharingFailedTitle', // shown when the screen sharing failed
// 'dialog.serviceUnavailable', // shown when server is not reachable
// 'dialog.sessTerminated', // shown when there is a failed conference session
// 'dialog.sessionRestarted', // show when a client reload is initiated because of bridge migration
@@ -1555,37 +1563,45 @@ var config = {
// 'liveStreaming.unavailableTitle', // shown when livestreaming service is not reachable
// 'lobby.joinRejectedMessage', // shown when while in a lobby, user's request to join is rejected
// 'lobby.notificationTitle', // shown when lobby is toggled and when join requests are allowed / denied
// 'notify.audioUnmuteBlockedTitle', // shown when mic unmute blocked
// 'notify.chatMessages', // shown when receiving chat messages while the chat window is closed
// 'notify.disconnected', // shown when a participant has left
// 'notify.connectedOneMember', // show when a participant joined
// 'notify.connectedTwoMembers', // show when two participants joined simultaneously
// 'notify.connectedThreePlusMembers', // show when more than 2 participants joined simultaneously
// 'notify.leftOneMember', // show when a participant left
// 'notify.leftTwoMembers', // show when two participants left simultaneously
// 'notify.leftThreePlusMembers', // show when more than 2 participants left simultaneously
// 'notify.grantedTo', // shown when moderator rights were granted to a participant
// 'notify.connectedTwoMembers', // show when two participants joined simultaneously
// 'notify.dataChannelClosed', // shown when the bridge channel has been disconnected
// 'notify.hostAskedUnmute', // shown to participant when host asks them to unmute
// 'notify.invitedOneMember', // shown when 1 participant has been invited
// 'notify.invitedThreePlusMembers', // shown when 3+ participants have been invited
// 'notify.invitedTwoMembers', // shown when 2 participants have been invited
// 'notify.kickParticipant', // shown when a participant is kicked
// 'notify.leftOneMember', // show when a participant left
// 'notify.leftThreePlusMembers', // show when more than 2 participants left simultaneously
// 'notify.leftTwoMembers', // show when two participants left simultaneously
// 'notify.linkToSalesforce', // shown when joining a meeting with salesforce integration
// 'notify.moderationStartedTitle', // shown when AV moderation is activated
// 'notify.moderationStoppedTitle', // shown when AV moderation is deactivated
// 'notify.localRecordingStarted', // shown when the local recording has been started
// 'notify.localRecordingStopped', // shown when the local recording has been stopped
// 'notify.moderationInEffectCSTitle', // shown when user attempts to share content during AV moderation
// 'notify.moderationInEffectTitle', // shown when user attempts to unmute audio during AV moderation
// 'notify.moderationInEffectVideoTitle', // shown when user attempts to enable video during AV moderation
// 'notify.moderationInEffectCSTitle', // shown when user attempts to share content during AV moderation
// 'notify.moderator', // shown when user gets moderator privilege
// 'notify.mutedRemotelyTitle', // shown when user is muted by a remote party
// 'notify.mutedTitle', // shown when user has been muted upon joining,
// 'notify.newDeviceAudioTitle', // prompts the user to use a newly detected audio device
// 'notify.newDeviceCameraTitle', // prompts the user to use a newly detected camera
// 'notify.noiseSuppressionFailedTitle', // shown when failed to start noise suppression
// 'notify.participantWantsToJoin', // shown when lobby is enabled and participant requests to join meeting
// 'notify.participantsWantToJoin', // shown when lobby is enabled and participants request to join meeting
// 'notify.passwordRemovedRemotely', // shown when a password has been removed remotely
// 'notify.passwordSetRemotely', // shown when a password has been set remotely
// 'notify.raisedHand', // shown when a partcipant used raise hand,
// 'notify.screenShareNoAudio', // shown when the audio could not be shared for the selected screen
// 'notify.screenSharingAudioOnlyTitle', // shown when the best performance has been affected by screen sharing
// 'notify.selfViewTitle', // show "You can always un-hide the self-view from settings"
// 'notify.startSilentTitle', // shown when user joined with no audio
// 'notify.suboptimalExperienceTitle', // show the browser warning
// 'notify.unmute', // shown to moderator when user raises hand during AV moderation
// 'notify.videoMutedRemotelyTitle', // shown when user's video is muted by a remote party,
// 'notify.videoUnmuteBlockedTitle', // shown when camera unmute and desktop sharing are blocked
// 'prejoin.errorDialOut',
// 'prejoin.errorDialOutDisconnected',
// 'prejoin.errorDialOutFailed',
@@ -1685,6 +1701,11 @@ var config = {
// // The server used to support whiteboard collaboration.
// // https://github.com/jitsi/excalidraw-backend
// collabServerBaseUrl: 'https://excalidraw-backend.example.com',
// // The user access limit to the whiteboard, introduced as a means
// // to control the performance.
// userLimit: 25,
// // The url for more info about the whiteboard and its usage limitations.
// limitUrl: 'https://example.com/blog/whiteboard-limits,
// },
// The watchRTC initialize config params as described :

View File

@@ -61,6 +61,35 @@ body.welcome-page {
}
.not-allow-title-character-div {
color: #f03e3e;
background-color: #fff;
font-size: 12px;
font-weight: 600;
margin: 10px 0px 5px 0px;
text-align: $welcomePageHeaderTextAlign;
border-radius: 5px;
padding: 5px;
.not-allow-title-character-text {
float: right;
line-height: 1.9;
};
.jitsi-icon {
margin-right: 9px;
float: left;
svg {
fill:#f03e3e;
& > *:first-child {
fill: none !important;
}
}
}
}
.insecure-room-name-warning {
align-items: center;
color: rgb(215, 121, 118);

View File

@@ -39,7 +39,7 @@ case "$1" in
echo "Application secret is mandatory"
fi
# Not allowed unix special characters in secret: /, \, ", ', `
if echo "$RET" | grep -q '[/\\\"\`]' ; then
if echo "$RET" | grep -q "[/\\\"\`\']" ; then
echo "Application secret contains invalid characters: /, \\, \", ', \`"
exit 1
fi

View File

@@ -43,8 +43,8 @@ server {
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name jitsi-meet.example.com;
# Mozilla Guideline v5.4, nginx 1.17.7, OpenSSL 1.1.1d, intermediate configuration

View File

@@ -107,8 +107,8 @@ var interfaceConfig = {
// Names of browsers which should show a warning stating the current browser
// has a suboptimal experience. Browsers which are not listed as optimal or
// unsupported are considered suboptimal. Valid values are:
// chrome, chromium, edge, electron, firefox, nwjs, opera, safari
OPTIMAL_BROWSERS: [ 'chrome', 'chromium', 'firefox', 'nwjs', 'electron', 'safari' ],
// chrome, chromium, electron, firefox , safari, webkit
OPTIMAL_BROWSERS: [ 'chrome', 'chromium', 'firefox', 'electron', 'safari', 'webkit' ],
POLICY_LOGO: null,
PROVIDER_NAME: 'Jitsi',

View File

@@ -14,14 +14,14 @@ PODS:
- CocoaLumberjack/Core (= 3.7.2)
- CocoaLumberjack/Core (3.7.2)
- DoubleConversion (1.1.6)
- FBLazyVector (0.69.11)
- FBReactNativeSpec (0.69.11):
- FBLazyVector (0.69.12)
- FBReactNativeSpec (0.69.12):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTRequired (= 0.69.11)
- RCTTypeSafety (= 0.69.11)
- React-Core (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- RCTRequired (= 0.69.12)
- RCTTypeSafety (= 0.69.12)
- React-Core (= 0.69.12)
- React-jsi (= 0.69.12)
- ReactCommon/turbomodule/core (= 0.69.12)
- Firebase/Analytics (8.15.0):
- Firebase/Core
- Firebase/Core (8.15.0):
@@ -167,203 +167,203 @@ PODS:
- DoubleConversion
- fmt (~> 6.2.1)
- glog
- RCTRequired (0.69.11)
- RCTTypeSafety (0.69.11):
- FBLazyVector (= 0.69.11)
- RCTRequired (= 0.69.11)
- React-Core (= 0.69.11)
- React (0.69.11):
- React-Core (= 0.69.11)
- React-Core/DevSupport (= 0.69.11)
- React-Core/RCTWebSocket (= 0.69.11)
- React-RCTActionSheet (= 0.69.11)
- React-RCTAnimation (= 0.69.11)
- React-RCTBlob (= 0.69.11)
- React-RCTImage (= 0.69.11)
- React-RCTLinking (= 0.69.11)
- React-RCTNetwork (= 0.69.11)
- React-RCTSettings (= 0.69.11)
- React-RCTText (= 0.69.11)
- React-RCTVibration (= 0.69.11)
- React-bridging (0.69.11):
- RCTRequired (0.69.12)
- RCTTypeSafety (0.69.12):
- FBLazyVector (= 0.69.12)
- RCTRequired (= 0.69.12)
- React-Core (= 0.69.12)
- React (0.69.12):
- React-Core (= 0.69.12)
- React-Core/DevSupport (= 0.69.12)
- React-Core/RCTWebSocket (= 0.69.12)
- React-RCTActionSheet (= 0.69.12)
- React-RCTAnimation (= 0.69.12)
- React-RCTBlob (= 0.69.12)
- React-RCTImage (= 0.69.12)
- React-RCTLinking (= 0.69.12)
- React-RCTNetwork (= 0.69.12)
- React-RCTSettings (= 0.69.12)
- React-RCTText (= 0.69.12)
- React-RCTVibration (= 0.69.12)
- React-bridging (0.69.12):
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsi (= 0.69.11)
- React-callinvoker (0.69.11)
- React-Codegen (0.69.11):
- FBReactNativeSpec (= 0.69.11)
- React-jsi (= 0.69.12)
- React-callinvoker (0.69.12)
- React-Codegen (0.69.12):
- FBReactNativeSpec (= 0.69.12)
- RCT-Folly (= 2021.06.28.00-v2)
- RCTRequired (= 0.69.11)
- RCTTypeSafety (= 0.69.11)
- React-Core (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-Core (0.69.11):
- RCTRequired (= 0.69.12)
- RCTTypeSafety (= 0.69.12)
- React-Core (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- ReactCommon/turbomodule/core (= 0.69.12)
- React-Core (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.69.11)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-Core/Default (= 0.69.12)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/CoreModulesHeaders (0.69.11):
- React-Core/CoreModulesHeaders (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/Default (0.69.11):
- React-Core/Default (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/DevSupport (0.69.11):
- React-Core/DevSupport (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.69.11)
- React-Core/RCTWebSocket (= 0.69.11)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-jsinspector (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-Core/Default (= 0.69.12)
- React-Core/RCTWebSocket (= 0.69.12)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-jsinspector (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/RCTActionSheetHeaders (0.69.11):
- React-Core/RCTActionSheetHeaders (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/RCTAnimationHeaders (0.69.11):
- React-Core/RCTAnimationHeaders (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/RCTBlobHeaders (0.69.11):
- React-Core/RCTBlobHeaders (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/RCTImageHeaders (0.69.11):
- React-Core/RCTImageHeaders (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/RCTLinkingHeaders (0.69.11):
- React-Core/RCTLinkingHeaders (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/RCTNetworkHeaders (0.69.11):
- React-Core/RCTNetworkHeaders (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/RCTSettingsHeaders (0.69.11):
- React-Core/RCTSettingsHeaders (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/RCTTextHeaders (0.69.11):
- React-Core/RCTTextHeaders (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/RCTVibrationHeaders (0.69.11):
- React-Core/RCTVibrationHeaders (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-Core/RCTWebSocket (0.69.11):
- React-Core/RCTWebSocket (0.69.12):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.69.11)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsiexecutor (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-Core/Default (= 0.69.12)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsiexecutor (= 0.69.12)
- React-perflogger (= 0.69.12)
- Yoga
- React-CoreModules (0.69.11):
- React-CoreModules (0.69.12):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/CoreModulesHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- React-RCTImage (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-cxxreact (0.69.11):
- RCTTypeSafety (= 0.69.12)
- React-Codegen (= 0.69.12)
- React-Core/CoreModulesHeaders (= 0.69.12)
- React-jsi (= 0.69.12)
- React-RCTImage (= 0.69.12)
- ReactCommon/turbomodule/core (= 0.69.12)
- React-cxxreact (0.69.12):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-callinvoker (= 0.69.11)
- React-jsi (= 0.69.11)
- React-jsinspector (= 0.69.11)
- React-logger (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-runtimeexecutor (= 0.69.11)
- React-jsi (0.69.11):
- React-callinvoker (= 0.69.12)
- React-jsi (= 0.69.12)
- React-jsinspector (= 0.69.12)
- React-logger (= 0.69.12)
- React-perflogger (= 0.69.12)
- React-runtimeexecutor (= 0.69.12)
- React-jsi (0.69.12):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsi/Default (= 0.69.11)
- React-jsi/Default (0.69.11):
- React-jsi/Default (= 0.69.12)
- React-jsi/Default (0.69.12):
- boost (= 1.76.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsiexecutor (0.69.11):
- React-jsiexecutor (0.69.12):
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-jsinspector (0.69.11)
- React-logger (0.69.11):
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-perflogger (= 0.69.12)
- React-jsinspector (0.69.12)
- React-logger (0.69.12):
- glog
- react-native-background-timer (2.4.1):
- React-Core
@@ -391,77 +391,77 @@ PODS:
- react-native-video/Video (6.0.0-alpha.7):
- PromisesSwift
- React-Core
- react-native-webrtc (111.0.3):
- react-native-webrtc (111.0.6):
- JitsiWebRTC (~> 111.0.0)
- React-Core
- react-native-webview (13.5.1):
- React-Core
- React-perflogger (0.69.11)
- React-RCTActionSheet (0.69.11):
- React-Core/RCTActionSheetHeaders (= 0.69.11)
- React-RCTAnimation (0.69.11):
- React-perflogger (0.69.12)
- React-RCTActionSheet (0.69.12):
- React-Core/RCTActionSheetHeaders (= 0.69.12)
- React-RCTAnimation (0.69.12):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/RCTAnimationHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTBlob (0.69.11):
- RCTTypeSafety (= 0.69.12)
- React-Codegen (= 0.69.12)
- React-Core/RCTAnimationHeaders (= 0.69.12)
- React-jsi (= 0.69.12)
- ReactCommon/turbomodule/core (= 0.69.12)
- React-RCTBlob (0.69.12):
- RCT-Folly (= 2021.06.28.00-v2)
- React-Codegen (= 0.69.11)
- React-Core/RCTBlobHeaders (= 0.69.11)
- React-Core/RCTWebSocket (= 0.69.11)
- React-jsi (= 0.69.11)
- React-RCTNetwork (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTImage (0.69.11):
- React-Codegen (= 0.69.12)
- React-Core/RCTBlobHeaders (= 0.69.12)
- React-Core/RCTWebSocket (= 0.69.12)
- React-jsi (= 0.69.12)
- React-RCTNetwork (= 0.69.12)
- ReactCommon/turbomodule/core (= 0.69.12)
- React-RCTImage (0.69.12):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/RCTImageHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- React-RCTNetwork (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTLinking (0.69.11):
- React-Codegen (= 0.69.11)
- React-Core/RCTLinkingHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTNetwork (0.69.11):
- RCTTypeSafety (= 0.69.12)
- React-Codegen (= 0.69.12)
- React-Core/RCTImageHeaders (= 0.69.12)
- React-jsi (= 0.69.12)
- React-RCTNetwork (= 0.69.12)
- ReactCommon/turbomodule/core (= 0.69.12)
- React-RCTLinking (0.69.12):
- React-Codegen (= 0.69.12)
- React-Core/RCTLinkingHeaders (= 0.69.12)
- React-jsi (= 0.69.12)
- ReactCommon/turbomodule/core (= 0.69.12)
- React-RCTNetwork (0.69.12):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/RCTNetworkHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTSettings (0.69.11):
- RCTTypeSafety (= 0.69.12)
- React-Codegen (= 0.69.12)
- React-Core/RCTNetworkHeaders (= 0.69.12)
- React-jsi (= 0.69.12)
- ReactCommon/turbomodule/core (= 0.69.12)
- React-RCTSettings (0.69.12):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.69.11)
- React-Codegen (= 0.69.11)
- React-Core/RCTSettingsHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-RCTText (0.69.11):
- React-Core/RCTTextHeaders (= 0.69.11)
- React-RCTVibration (0.69.11):
- RCTTypeSafety (= 0.69.12)
- React-Codegen (= 0.69.12)
- React-Core/RCTSettingsHeaders (= 0.69.12)
- React-jsi (= 0.69.12)
- ReactCommon/turbomodule/core (= 0.69.12)
- React-RCTText (0.69.12):
- React-Core/RCTTextHeaders (= 0.69.12)
- React-RCTVibration (0.69.12):
- RCT-Folly (= 2021.06.28.00-v2)
- React-Codegen (= 0.69.11)
- React-Core/RCTVibrationHeaders (= 0.69.11)
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (= 0.69.11)
- React-runtimeexecutor (0.69.11):
- React-jsi (= 0.69.11)
- ReactCommon/turbomodule/core (0.69.11):
- React-Codegen (= 0.69.12)
- React-Core/RCTVibrationHeaders (= 0.69.12)
- React-jsi (= 0.69.12)
- ReactCommon/turbomodule/core (= 0.69.12)
- React-runtimeexecutor (0.69.12):
- React-jsi (= 0.69.12)
- ReactCommon/turbomodule/core (0.69.12):
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-bridging (= 0.69.11)
- React-callinvoker (= 0.69.11)
- React-Core (= 0.69.11)
- React-cxxreact (= 0.69.11)
- React-jsi (= 0.69.11)
- React-logger (= 0.69.11)
- React-perflogger (= 0.69.11)
- React-bridging (= 0.69.12)
- React-callinvoker (= 0.69.12)
- React-Core (= 0.69.12)
- React-cxxreact (= 0.69.12)
- React-jsi (= 0.69.12)
- React-logger (= 0.69.12)
- React-perflogger (= 0.69.12)
- RNCalendarEvents (2.2.0):
- React
- RNCAsyncStorage (1.19.3):
@@ -709,8 +709,8 @@ SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
CocoaLumberjack: b7e05132ff94f6ae4dfa9d5bce9141893a21d9da
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
FBLazyVector: 5c0975e66853436589eae7542f4b956c7e2ef465
FBReactNativeSpec: bb062293e84c33200005312d1807d8cb94a0d66a
FBLazyVector: 6fab494fa11340bd4206edaebed07279a6bafad4
FBReactNativeSpec: 76d7b03876b0ad0b86bc5c84d23af8e64db8e096
Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d
FirebaseAnalytics: 7761cbadb00a717d8d0939363eb46041526474fa
FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1
@@ -735,19 +735,19 @@ SPEC CHECKSUMS:
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
PromisesSwift: 28dca69a9c40779916ac2d6985a0192a5cb4a265
RCT-Folly: b9d9fe1fc70114b751c076104e52f3b1b5e5a95a
RCTRequired: 8e9a57dddc8f8e9e816c67c2d2537271a997137a
RCTTypeSafety: 2b19e268e2036a2c2f6db6deb1ac03e28b1d607a
React: f9478e6390f177ee6b67b87a3c6afea42b39523e
React-bridging: d405ecd3ff80e1d0a4059a11063eaa9ed7a00c58
React-callinvoker: c8ffa61f3f06f486ba6647769fc98f19e25d165a
React-Codegen: 73acfdac1495b91ad5efdd3ab005568263c5def6
React-Core: 7b7c75af4b73fe0ed4e5c3cdb7d79979e81148dc
React-CoreModules: cd6e7efb38162884f08c7afa16fffaf15ff28ae4
React-cxxreact: 51157cc600c9f436a7e623913a03b775305ef86c
React-jsi: 3eeb345c4828d7b132fd38064a305f31b46d4ec3
React-jsiexecutor: 5813455a4a908fb7284aa13307a9e0386e93b0bb
React-jsinspector: 9ca5bf73ed0a195397e45fdbcd507cf7d503c428
React-logger: 700340e325f21ba2a2d6413a61ef14268c7360aa
RCTRequired: b9e53f0512019150020156fa0dacd6583ab838be
RCTTypeSafety: 04b72202bef8302802610dee70bb9407a245b64c
React: 59288a7ca8104eb8002f01378606fe42eeabf4b5
React-bridging: b042b8c217f04e568409786de5f221793be49c31
React-callinvoker: c7b83d582112e2d5a049dc46abf4c64d871b5c45
React-Codegen: 5747238d0446e3ab1deb967e749a2bfde6a5c866
React-Core: d8e1250039d47112513757038d9d9f9b638565c6
React-CoreModules: 63cceb0040ec2b43a258113193be91f934b37f1b
React-cxxreact: 429404aac55d8bffca77328002452fc7fa8b29e8
React-jsi: a8f60feb519ac00085eb9a39d20eaa65c96b51ea
React-jsiexecutor: ce0b9aa647bdf94126eb2ee1f235d329eb8c0aec
React-jsinspector: f275698149311abc8c32ebb97797d6b97c44adde
React-logger: da69d7f1c9501c78cd60776d52a60d7fa5e4d9c2
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
react-native-get-random-values: dee677497c6a740b71e5612e8dbd83e7539ed5bb
react-native-keep-awake: afad8a51dfef9fe9655a6344771be32c8596d774
@@ -759,20 +759,20 @@ SPEC CHECKSUMS:
react-native-slider: 1cdd6ba29675df21f30544253bf7351d3c2d68c4
react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457
react-native-video: 967eead48aaa42c25a9e1d65c3b1ab30762a88df
react-native-webrtc: 4d1669c2ed29767fe70b0169428b4466589ecf8b
react-native-webrtc: 255a1172fd31525b952b36aef7b8e9a41de325e5
react-native-webview: 8baa0f5c6d336d6ba488e942bcadea5bf51f050a
React-perflogger: fdee2a0c512167ae4c19c4e230ccf6aa66a6aff0
React-RCTActionSheet: 1cf5fef4e372f1c877969710a51bea4bb25e78fe
React-RCTAnimation: 73816e3acd1f5e3f00166fc7eedb34f6b112f734
React-RCTBlob: 6976c838fb14a1daf75d7c8bb23bae9cbbf726bb
React-RCTImage: ab8a7498f215117f32271698591e4bd932dcf812
React-RCTLinking: e8e78aed2744ab9946cc8ba5716b4938c2efb1e0
React-RCTNetwork: 796f5aed4d932655d292bdc6b40f9502dcdb9542
React-RCTSettings: 7e1cd2a384b45c90caf67464572abe3833b9da3b
React-RCTText: fd6162890828f0761e03c59058fa23c3a21b2e10
React-RCTVibration: 302cfd5cc33669d7abdb7ec6790123baba66e62e
React-runtimeexecutor: 59407514818b2afbb1d7507e4e1ac834d24b0fbd
ReactCommon: b8487da74723562d7368dab27135fd182f00a91c
React-perflogger: 5ade0a1627352f1647d283e78331819bb46cceae
React-RCTActionSheet: 8e94f1e46e09c7035b81fe56c0ed8d78f3ccd340
React-RCTAnimation: bf2af72f03cf16528db9a830be69fa04b341a1b7
React-RCTBlob: 4d076b8bb55e631ad1280280ecba674fb1e46d16
React-RCTImage: 073dcc1689466851fe120c7f8a3cfe3db0196c9f
React-RCTLinking: 8872818dc894a17bf17cb4b120f76917bf2e9f0a
React-RCTNetwork: 1e9c873f4a210784a4fb752194cb595502112464
React-RCTSettings: 1475a717c54f4a9ed627dffffad2470c4b15a419
React-RCTText: ed34088172126f84130eea859d62fedca0dd7975
React-RCTVibration: c9cd9f21bbcb3b9c6deedbb66f13e373f57dd795
React-runtimeexecutor: ea78653fbc68bd6f2d3f5e7e311bc5a9dc8bfeca
ReactCommon: f4bb9e5209ea5c3c6ab25e100895119e58d6e50a
RNCalendarEvents: 7e65eb4a94f53c1744d1e275f7fafcfaa619f7a3
RNCAsyncStorage: c913ede1fa163a71cea118ed4670bbaaa4b511bb
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
@@ -784,7 +784,7 @@ SPEC CHECKSUMS:
RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852
RNSVG: ed492aaf3af9ca01bc945f7a149d76d62e73ec82
RNWatch: fd30ca40a5b5ef58dcbc195638e68219bc455236
Yoga: 7f5ad94937ba3fc58c151ad1b7bbada2c275b28e
Yoga: 8a90b50af67eaa9fe94fd03e550bfeab06096873
PODFILE CHECKSUM: 90720aee51cf2cab2e12611a28dbf55a688e969c

View File

@@ -67,13 +67,16 @@
"renameBreakoutRoom": "Breakout-Raum umbenennen",
"sendToBreakoutRoom": "Anwesende in Breakout-Raum verschieben:"
},
"breakoutList": "Breakout-Liste",
"defaultName": "Breakout-Raum #{{index}}",
"hideParticipantList": "Teilnehmerliste ausblenden",
"mainRoom": "Hauptraum",
"notifications": {
"joined": "Breakout-Raum \"{{name}}\" betreten",
"joinedMainRoom": "Hauptraum betreten",
"joinedTitle": "Breakout-Räume"
}
},
"showParticipantList": "Teilnehmerliste anzeigen"
},
"calendarSync": {
"addMeetingURL": "Konferenzlink hinzufügen",
@@ -256,6 +259,7 @@
"Share": "Teilen",
"Submit": "OK",
"WaitForHostMsg": "Die Konferenz wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
"WaitingForHostButton": "Auf Moderation warten",
"WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
"Yes": "Ja",
"accessibilityLabel": {
@@ -269,6 +273,8 @@
"addMeetingNote": "Notiz zu dieser Konferenz hinzufügen",
"addOptionalNote": "Notiz hinzufügen (optional):",
"allow": "Erlauben",
"allowToggleCameraDialog": "Wollen Sie {{initiatorName}} erlauben, Ihre Kameraauswahl zu ändern?",
"allowToggleCameraTitle": "Änderung der Kamera zulassen?",
"alreadySharedVideoMsg": "Eine andere Person gibt bereits ein Video weiter. Bei dieser Konferenz ist jeweils nur ein geteiltes Video möglich.",
"alreadySharedVideoTitle": "Nur ein geteiltes Video gleichzeitig",
"applicationWindow": "Anwendungsfenster",
@@ -329,6 +335,7 @@
"lockRoom": "Konferenz$t(lockRoomPassword) hinzufügen",
"lockTitle": "Sperren fehlgeschlagen",
"login": "Anmelden",
"loginQuestion": "Sind Sie sicher, dass sie sich anmelden und die Konferenz verlassen möchten?",
"logoutQuestion": "Sind Sie sicher, dass Sie sich abmelden und die Konferenz verlassen möchten?",
"logoutTitle": "Abmelden",
"maxUsersLimitReached": "Das Limit für die maximale Personenzahl ist erreicht. Die Konferenz ist voll. Bitte wenden Sie sich an die Konferenzleitung oder versuchen Sie es später noch einmal!",
@@ -409,6 +416,7 @@
"sendPrivateMessageTitle": "Privat antworten?",
"serviceUnavailable": "Dienst nicht verfügbar",
"sessTerminated": "Konferenz beendet",
"sessTerminatedReason": "Die Konferenz wurde beendet",
"sessionRestarted": "Konferenz neugestartet",
"shareAudio": "Fortfahren",
"shareAudioTitle": "Wie kann Audio geteilt werden",
@@ -440,7 +448,24 @@
"thankYou": "Danke für die Verwendung von {{appName}}!",
"token": "Token",
"tokenAuthFailed": "Sie sind nicht berechtigt, dieser Konferenz beizutreten.",
"tokenAuthFailedReason": {
"audInvalid": "Ungültiger `aud`-Wert. Erwartet wird `jitsi`.",
"contextNotFound": "Das `context`-Objekt fehlt.",
"expInvalid": "Ungültiger `exp`-Wert.",
"featureInvalid": "Ungültiges Feature: {{feature}}, noch nicht implementiert.",
"featureValueInvalid": "Ungültiger Wert für Feature: {{feature}}.",
"featuresNotFound": "Das `features`-Objekt fehlt.",
"headerNotFound": "Header fehlt.",
"issInvalid": "Ungültiger `iss`-Wert. Erwartet wird `chat`.",
"kidMismatch": "Die Key-ID (kid) passt nicht zum sub.",
"kidNotFound": "Fehlende Key-ID (kid).",
"nbfFuture": "Der `nbf`-Wert liegt in der Zukunft.",
"nbfInvalid": "Ungültiger `nbf`-Wert.",
"payloadNotFound": "Fehlende Payload.",
"tokenExpired": "Das Token ist abgelaufen."
},
"tokenAuthFailedTitle": "Authentifizierung fehlgeschlagen",
"tokenAuthFailedWithReasons": "Teilnahme an der Konferenz fehlgeschlagen. Möglicher Grund: {{reason}}",
"tokenAuthUnsupported": "Token-Authentifizierung wird nicht unterstützt.",
"transcribing": "Wird transkribiert",
"unlockRoom": "Konferenz$t(lockRoomPassword) entfernen",
@@ -530,6 +555,7 @@
"password": "$t(lockRoomPasswordUppercase):",
"reachedLimit": "Sie haben die Grenzen Ihres Tarifs erreicht.",
"sip": "SIP-Adresse",
"sipAudioOnly": "SIP-Adresse (nur Ton)",
"title": "Teilen",
"tooltip": "Freigabe-Link und Einwahlinformationen für dieses Meeting",
"upgradeOptions": "Bitte prüfen Sie Ihre Upgrade-Optionen auf"
@@ -636,13 +662,13 @@
"knockingParticipantList": "Liste anklopfender Personen",
"lobbyChatStartedNotification": "{{moderator}} hat einen Lobby-Chat mit {{attendee}} gestartet",
"lobbyChatStartedTitle": "{{moderator}} hat einen Lobby-Chat mit Ihnen gestartet.",
"lobbyClosed": "Die Lobby wurde geschlossen.",
"nameField": "Geben Sie Ihren Namen ein",
"notificationLobbyAccessDenied": "{{targetParticipantName}} wurde von {{originParticipantName}} der Zutritt verwehrt",
"notificationLobbyAccessGranted": "{{targetParticipantName}} wurde von {{originParticipantName}} der Zutritt gestattet",
"notificationLobbyDisabled": "{{originParticipantName}} hat die Lobby deaktiviert",
"notificationLobbyEnabled": "{{originParticipantName}} hat die Lobby aktiviert",
"notificationTitle": "Lobby",
"passwordField": "Konferenzpasswort eingeben",
"passwordJoinButton": "Beitreten",
"reject": "Ablehnen",
"rejectAll": "Alle ablehnen",
@@ -677,6 +703,8 @@
"sessionToken": "Sitzungs-Token",
"start": "Aufnahme starten",
"stop": "Aufnahme stoppen",
"stopping": "Aufnahme wird gestoppt",
"wait": "Bitte warten Sie während wir Ihre Aufnahme speichern",
"yes": "Ja"
},
"lockRoomPassword": "Passwort",
@@ -1059,6 +1087,7 @@
"alertOk": "OK",
"alertTitle": "Warnung",
"alertURLText": "Die angegebene Server-URL ist ungültig",
"apply": "Übernehmen",
"buildInfoSection": "Build-Informationen",
"conferenceSection": "Konferenz",
"disableCallIntegration": "Native Anrufintegration deaktivieren",
@@ -1069,6 +1098,7 @@
"displayNamePlaceholderText": "z.B. Erika Musterfrau",
"email": "E-Mail",
"emailPlaceholderText": "email@beispiel.de",
"gavatarMessage": "Wenn Sie ein Gravatar-Konto mit Ihrer Emailadresse haben, wird dieses als Ihr Profilfoto verwendet.",
"goTo": "Gehe zu",
"header": "Einstellungen",
"help": "Hilfe",

View File

@@ -67,13 +67,18 @@
"renameBreakoutRoom": "Renomear sala",
"sendToBreakoutRoom": "Enviar participante para:"
},
"breakoutList": "lista de salas",
"buttonLabel": "Salas simultâneas",
"defaultName": "Sala #{{index}}",
"hideParticipantList": "Ocultar lista de participantes",
"mainRoom": "Sala principal",
"notifications": {
"joined": "Entrada na sala \"{{name}}\"",
"joinedMainRoom": "Entrada na sala principal",
"joinedTitle": "Salas simultâneas"
}
},
"showParticipantList": "Mostrar lista de participantes",
"title": "Salas simultâneas"
},
"calendarSync": {
"addMeetingURL": "Adicionar um link da reunião",
@@ -477,6 +482,10 @@
"viewUpgradeOptions": "Ver opções de actualização",
"viewUpgradeOptionsContent": "Para obter acesso ilimitado a funcionalidades premium como gravação, transcrições, RTMP Streaming & mais, terá de actualizar o seu plano.",
"viewUpgradeOptionsTitle": "Descobriu uma característica premium!",
"whiteboardLimitContent": "Lamentamos, mas o limite de utilizadores do quadro branco foi atingido.",
"whiteboardLimitReference": "Para mais informações consultar",
"whiteboardLimitReferenceUrl": "o nosso sítio Web",
"whiteboardLimitTitle": "Restrição da utilização do quadro branco",
"yourEntireScreen": "O seu ecrã inteiro"
},
"documentSharing": {
@@ -724,6 +733,7 @@
"dataChannelClosed": "Deficiência na qualidade do vídeo",
"dataChannelClosedDescription": "O canal de ponte foi desconectado e, portanto, a qualidade do vídeo está limitada à sua configuração mais baixa.",
"disabledIframe": "A incorporação destina-se apenas a fins de demonstração, pelo que esta chamada será desligada em {{timeout}} minutos.",
"disabledIframeSecondary": "A incorporação de {{domain}} destina-se apenas a fins de demonstração, pelo que esta chamada será desligada em {{timeout}} minutos. Por favor, use <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> para incorporação em produção!",
"disconnected": "desconectado",
"displayNotifications": "Mostrar notificações para",
"dontRemindMe": "Não me lembre",
@@ -799,7 +809,9 @@
"videoUnmuteBlockedDescription": "A operação de ligar a câmara e partilhar o ambiente de trabalho foi temporariamente bloqueada devido aos limites do sistema.",
"videoUnmuteBlockedTitle": "Está bloqueado ligar a câmara e partilhar o ambiente de trabalho!",
"viewLobby": "Ver sala de espera",
"waitingParticipants": "{{waitingParticipants}} pessoas"
"waitingParticipants": "{{waitingParticipants}} pessoas",
"whiteboardLimitDescription": "Guarde o seu progresso, pois o limite de utilizadores será atingido em breve e o quadro branco será encerrado.",
"whiteboardLimitTitle": "Utilização do quadro branco"
},
"participantsPane": {
"actions": {
@@ -808,6 +820,7 @@
"askUnmute": "Pedir para ligar o som",
"audioModeration": "Ligar o microfone deles",
"blockEveryoneMicCamera": "Bloquear o microfone e a câmara de todos",
"breakoutRooms": "Salas simultâneas",
"invite": "Convidar alguém",
"moreModerationActions": "Mais opções de moderação",
"moreModerationControls": "Mais controlos de moderação",
@@ -1154,7 +1167,7 @@
"audioOnly": "Mudar para apenas áudio",
"audioRoute": "Selecionar o dispositivo de som",
"boo": "Vaia",
"breakoutRoom": "Entrar/Sair da sala",
"breakoutRooms": "Salas simultâneas",
"callQuality": "Gerir a qualidade do vídeo",
"carmode": "Modo de condução",
"cc": "Mudar legendas",

View File

@@ -391,7 +391,7 @@
"serviceUnavailable": "Tjänsten otillgänglig",
"sessTerminated": "Konferensen avslutades",
"sessionRestarted": "Samtal återstartat av bryggan",
"shareAudio": "Forstätt",
"shareAudio": "Fortsätt",
"shareAudioTitle": "Hur man delar ljud",
"shareAudioWarningD1": "Du måste avsluta din skärmdelning innan du kan dela ditt ljud",
"shareAudioWarningD2": "Du måste starta om din skärmdelning och därefter klicka på \"ljuddelning\"",
@@ -427,13 +427,13 @@
"user": "Användare",
"userIdentifier": "Användar-ID",
"userPassword": "Lösenord",
"verifyParticipantConfirm": "Dem matchar",
"verifyParticipantDismiss": "Dem matchar inte",
"verifyParticipantConfirm": "De matchar",
"verifyParticipantDismiss": "De matchar inte",
"verifyParticipantQuestion": "EXPERIMENTELLT: Fråga deltagaren; {{participantName}} om han/hon kan se samma innehåll, i samma ordning.",
"verifyParticipantTitle": "Användarverifikation",
"videoLink": "Videolänk",
"viewUpgradeOptions": "Se uppgraderings alternativ",
"viewUpgradeOptionsContent": "För att få obegränsad tillgång till premiumfunktioner som inspelning, transkriptioner, RTMP -streaming och mer måste du uppgradera din plan.",
"viewUpgradeOptionsContent": "För att få obegränsad tillgång till premiumfunktioner som inspelning, transkriptioner, RTMP-streaming och mer måste du uppgradera din plan.",
"viewUpgradeOptionsTitle": "Du upptäckte en premiumfunktion!",
"yourEntireScreen": "Helskärm"
},
@@ -458,7 +458,7 @@
},
"filmstrip": {
"accessibilityLabel": {
"heading": "Videomineatyrer"
"heading": "Videominiatyrer"
}
},
"giphy": {
@@ -1278,7 +1278,7 @@
"tileViewToggle": "Öppna eller stäng panelvyn",
"toggleCamera": "Byta kamera",
"unmute": "Slå på ljud",
"videoSettings": "Video inställningar",
"videoSettings": "Videoinställningar",
"videomute": "Inaktivera kameran",
"videounmute": "Aktivera kameran"
},
@@ -1346,9 +1346,9 @@
"videothumbnail": {
"connectionInfo": "Anslutningsinformation",
"domute": "Tysta",
"domuteOthers": "Inkativerad ljud för alla andra",
"domuteOthers": "Inaktivera ljud för alla andra",
"domuteVideo": "Inaktivera kamera",
"domuteVideoOfOthers": "Inkativera kamera för alla andra",
"domuteVideoOfOthers": "Inaktivera kamera för alla andra",
"flip": "Vänd",
"grantModerator": "Godkänn moderator",
"hideSelfView": "Dölj självvyn",

View File

@@ -1251,7 +1251,7 @@
"pending": "{{displayName}} davet edildi"
},
"videoStatus": {
"adjustFor": "Ayala:",
"adjustFor": "Ayarla:",
"audioOnly": "SES",
"audioOnlyExpanded": "Yalnızca ses modundasınız. Bu mod bant genişliğinden tasarruf sağlar, ancak başkalarının videolarını göremezsiniz.",
"bestPerformance": "En iyi performans",

View File

@@ -1,7 +1,7 @@
{
"addPeople": {
"accessibilityLabel": {
"meetingLink": "會議連結: {{url}}"
"meetingLink": "會議連結{{url}}"
},
"add": "邀請",
"addContacts": "邀請您的聯絡人",
@@ -16,7 +16,7 @@
"failedToAdd": "加入與會者失敗",
"googleEmail": "Google Email",
"inviteMoreHeader": "目前只有您一個人在會議中",
"inviteMoreMailSubject": "加入{{appName}}會議",
"inviteMoreMailSubject": "加入 {{appName}} 會議",
"inviteMorePrompt": "邀請更多人",
"linkCopied": "已將連結複製至剪貼簿",
"noResults": "沒有符合的搜尋結果",
@@ -29,7 +29,7 @@
"sipAddresses": "SIP 位址",
"telephone": "電話號碼:{{number}}",
"title": "邀請他人至會議",
"yahooEmail": "Yahoo! Email"
"yahooEmail": "Yahoo Email"
},
"audioDevices": {
"bluetooth": "藍牙",
@@ -56,24 +56,29 @@
},
"breakoutRooms": {
"actions": {
"add": "新增討論室",
"autoAssign": "自動分配至討論室",
"add": "新增分組討論室",
"autoAssign": "自動分配至分組討論室",
"close": "關閉",
"join": "加入",
"leaveBreakoutRoom": "離開討論室",
"leaveBreakoutRoom": "離開分組討論室",
"more": "更多",
"remove": "移除",
"rename": "重新命名",
"renameBreakoutRoom": "重新命名討論室",
"renameBreakoutRoom": "重新命名分組討論室",
"sendToBreakoutRoom": "將與會者移至:"
},
"breakoutList": "分組討論室列表",
"buttonLabel": "分組討論室",
"defaultName": "分組討論室 #{{index}}",
"hideParticipantList": "隱藏與會者列表",
"mainRoom": "主會議室",
"notifications": {
"joined": "正在加入「{{name}}」分組討論室",
"joinedMainRoom": "正在加入主會議室",
"joinedTitle": "分組討論室"
}
},
"showParticipantList": "顯示與會者列表",
"title": "分組討論室"
},
"calendarSync": {
"addMeetingURL": "增加會議連結",
@@ -107,11 +112,11 @@
"enter": "加入聊天室",
"error": "錯誤:您的訊息未被傳送。原因:{{error}}",
"fieldPlaceHolder": "在此輸入您的訊息",
"lobbyChatMessageTo": "大廳聊天訊息傳送至{{recipient}}",
"lobbyChatMessageTo": "大廳聊天訊息傳送至 {{recipient}}",
"message": "訊息",
"messageAccessibleTitle": "{{user}}",
"messageAccessibleTitleMe": "",
"messageTo": "傳送私人訊息至{{recipient}}",
"messageAccessibleTitleMe": "",
"messageTo": "傳送私人訊息至 {{recipient}}",
"messagebox": "輸入訊息",
"newMessages": "新訊息",
"nickname": {
@@ -120,7 +125,7 @@
"titleWithPolls": "輸入名稱來使用聊天與投票"
},
"noMessagesMessage": "此會議尚無訊息,在此開始對話聊天!",
"privateNotice": "傳送私人訊息至{{recipient}}",
"privateNotice": "傳送私人訊息至 {{recipient}}",
"sendButton": "傳送",
"smileysPanel": "Emoji 面板",
"tabs": {
@@ -133,7 +138,7 @@
},
"chromeExtensionBanner": {
"buttonText": "安裝 Chrome 擴充功能",
"buttonTextEdge": "安裝 Edge 外掛程式",
"buttonTextEdge": "安裝 Edge 擴充功能",
"close": "關閉",
"dontShowAgain": "不要再問了",
"installExtensionText": "安裝適用於 Google 行事曆及 Office 365 整合的擴充功能"
@@ -151,14 +156,14 @@
"DISCONNECTED": "已斷線",
"DISCONNECTING": "中斷連接中",
"ERROR": "錯誤",
"FETCH_SESSION_ID": "正在取得工作階段ID...",
"GET_SESSION_ID_ERROR": "取得工作階段ID時發生錯誤{{code}}",
"GOT_SESSION_ID": "正在取得工作階段ID...完成",
"LOW_BANDWIDTH": "已關閉{{displayName}}的視訊以節省流量"
"FETCH_SESSION_ID": "正在取得工作階段 ID...",
"GET_SESSION_ID_ERROR": "取得工作階段 ID 時發生錯誤:{{code}}",
"GOT_SESSION_ID": "正在取得工作階段 ID... 完成",
"LOW_BANDWIDTH": "已關閉 {{displayName}} 的視訊以節省頻寬"
},
"connectionindicator": {
"address": "位址:",
"audio_ssrc": "音訊SSRC",
"audio_ssrc": "音訊 SSRC",
"bandwidth": "預估頻寬:",
"bitrate": "連線速率:",
"bridgeCount": "伺服器數量:",
@@ -175,7 +180,7 @@
"more": "顯示更多",
"no": "否",
"packetloss": "封包遺失率:",
"participant_id": "與會者ID",
"participant_id": "與會者 ID",
"quality": {
"good": "良好",
"inactive": "閒置",
@@ -192,7 +197,7 @@
"status": "連線狀態:",
"transport": "傳輸協定:",
"transport_plural": "傳輸:",
"video_ssrc": "視訊SSRC",
"video_ssrc": "視訊 SSRC",
"yes": "是"
},
"dateUtils": {
@@ -201,33 +206,33 @@
"yesterday": "昨天"
},
"deepLinking": {
"appNotInstalled": "您需要在手機上安裝{{app}}行動應用程式才能加入這場會議。",
"description": "什麼事情都沒發生?我們已嘗試在您的{{app}}桌面應用程式開啟會議。請再試一次,或是在{{app}}網路應用程式開啟會議。",
"descriptionNew": "什麼事情都沒發生?我們已嘗試在您的{{app}}桌面應用程式開啟會議。<br /><br />您可以再試一次,或在網頁上啟動。",
"descriptionWithoutWeb": "什麼事情都沒發生?我們已試著將您的會議在桌面應用程式{{app}}中啟動。",
"downloadApp": "下載App",
"downloadMobileApp": "從App Store下載",
"ifDoNotHaveApp": "如果您尚未安裝App",
"ifHaveApp": "如果您已經安裝此App",
"joinInApp": "使用App加入會議",
"joinInAppNew": "在APP中加入",
"appNotInstalled": "您需要在手機上安裝 {{app}} 應用程式才能加入這場會議。",
"description": "什麼事情都沒發生?我們已嘗試在您的 {{app}} 桌面應用程式開啟會議。請再試一次,或是在 {{app}} 網頁應用程式開啟會議。",
"descriptionNew": "什麼事情都沒發生?我們已嘗試在您的 {{app}} 桌面應用程式開啟會議。<br /><br />您可以再試一次,或在網頁上啟動。",
"descriptionWithoutWeb": "什麼事情都沒發生?我們已試著將您的會議在桌面應用程式 {{app}} 中啟動。",
"downloadApp": "下載應用程式",
"downloadMobileApp": "從 App Store 下載",
"ifDoNotHaveApp": "如果您尚未安裝應用程式",
"ifHaveApp": "如果您已經安裝應用程式",
"joinInApp": "使用應用程式加入會議",
"joinInAppNew": "在應用程式中加入",
"joinInBrowser": "在瀏覽器中加入",
"launchMeetingLabel": "您想如何加入此會議?",
"launchWebButton": "在瀏覽器開啟",
"noMobileApp": "您尚未安裝該APP",
"noMobileApp": "您尚未安裝該應用程式",
"termsAndConditions": "繼續操作即表示您同意我們的<a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>條款與條件。</a>",
"title": "正在{{app}}發起您的會議...",
"titleNew": "正在啟您的會議...",
"title": "正在 {{app}} 開啟您的會議...",
"titleNew": "正在啟您的會議...",
"tryAgainButton": "在桌面上再試一次",
"unsupportedBrowser": "您似乎正在使用我們不支援的瀏覽器"
"unsupportedBrowser": "您似乎正在使用我們不支援的瀏覽器"
},
"defaultLink": "例如{{url}}",
"defaultNickname": "例如:Jane Pink",
"defaultLink": "例如 {{url}}",
"defaultNickname": "例如:王小明",
"deviceError": {
"cameraError": "無法存取您的網路攝影機",
"cameraPermission": "取網路攝影機權限時發生錯誤",
"cameraPermission": "取網路攝影機權限時發生錯誤",
"microphoneError": "無法存取您的麥克風",
"microphonePermission": "取麥克風權限時發生錯誤"
"microphonePermission": "取麥克風權限時發生錯誤"
},
"deviceSelection": {
"hid": {
@@ -239,13 +244,13 @@
"noPermission": "未取得權限",
"previewUnavailable": "無法預覽",
"selectADevice": "選擇裝置",
"testAudio": "播放測試聲音"
"testAudio": "測試"
},
"dialIn": {
"screenTitle": "通話記錄"
},
"dialOut": {
"statusMessage": "現在狀態為{{status}}"
"statusMessage": "現在狀態為 {{status}}"
},
"dialog": {
"Back": "返回",
@@ -256,6 +261,7 @@
"Share": "分享",
"Submit": "送出",
"WaitForHostMsg": "此會議尚未開始,如果您是會議主持人,請進行認證並以主持人身分開始會議。",
"WaitingForHostButton": "等待主持人",
"WaitingForHostTitle": "正在等候主持人加入...",
"Yes": "是",
"accessibilityLabel": {
@@ -269,6 +275,8 @@
"addMeetingNote": "新增此會議的備註",
"addOptionalNote": "新增備註(選填):",
"allow": "允許",
"allowToggleCameraDialog": "您要允許 {{initiatorName}} 切換您的鏡頭朝向嗎?",
"allowToggleCameraTitle": "要允許切換鏡頭嗎?",
"alreadySharedVideoMsg": "其他與會者正在分享影像,同一時間只有一個與會者可以分享影像螢幕。",
"alreadySharedVideoTitle": "同一時間只允許一位影像分享",
"applicationWindow": "應用程式視窗",
@@ -277,14 +285,14 @@
"cameraNotFoundError": "找不到網路攝影機。",
"cameraNotSendingData": "我們無法存取您的網路攝影機,請檢查是否有其他應用程式正在使用這個裝置,並從裝置選單裡選擇其他設備或者重新載入。",
"cameraNotSendingDataTitle": "無法存取網路攝影機",
"cameraPermissionDeniedError": "未取得網路攝影機的存取權,您仍可參加會議,但其他人無法看到。按一下網址列中的「攝影機」圖示 ,然後選取「一律允許」選項。",
"cameraPermissionDeniedError": "未取得網路攝影機的存取權,您仍可參加會議,但其他人無法看到。按一下網址列中的「攝影機」圖示 ,然後選取「一律允許」選項。",
"cameraTimeoutError": "無法啟動視訊裝置,連線逾時!",
"cameraUnknownError": "由於不明原因,無法存取網路攝影機。",
"cameraUnsupportedResolutionError": "您的網路攝影機不支援所需的影像解析度。",
"close": "關閉",
"conferenceDisconnectMsg": "請檢查一下網路連線,將在{{seconds}}秒後重新連接...",
"conferenceDisconnectMsg": "請檢查一下網路連線,將在 {{seconds}} 秒後重新連接...",
"conferenceDisconnectTitle": "您已經被中斷連接。",
"conferenceReloadMsg": "我們正試著修復狀況,將在{{seconds}}秒後重新連接...",
"conferenceReloadMsg": "我們正試著修復狀況,將在 {{seconds}} 秒後重新連接...",
"conferenceReloadTitle": "喔哦!好像有東西壞掉囉。",
"confirm": "確認",
"confirmNo": "否",
@@ -298,7 +306,7 @@
"dismiss": "取消",
"displayNameRequired": "嗨!請問大名?",
"done": "完成",
"e2eeDescription": "請注意端對端加密目前是實驗性功能啟用端對端加密將停用部分伺服器端提供的服務例如透過電話加入會議。另外透過網頁版加入會議還需要使用支援Insertable Streams的瀏覽器。",
"e2eeDescription": "請注意,端對端加密目前是實驗性功能,啟用端對端加密將停用部分伺服器端提供的服務,例如:透過電話加入會議。另外,透過網頁版加入會議還需要使用支援 Insertable Streams 的瀏覽器。",
"e2eeDisabledDueToMaxModeDescription": "由於會議中的人數過多,故無法啟用端對端加密。",
"e2eeLabel": "啟用端對端加密",
"e2eeWarning": "警告:看來此會議中不是每位與會者都啟用了端對端加密,如果您啟用了,他們可能無法看或聽到您。",
@@ -307,7 +315,7 @@
"enterDisplayName": "請在此輸入您自己的名字",
"error": "錯誤",
"gracefulShutdown": "服務目前正在維護中,請稍後再試。",
"grantModeratorDialog": "您確定要授予{{participantName}}主持人權限嗎?",
"grantModeratorDialog": "您確定要授予 {{participantName}} 主持人權限嗎?",
"grantModeratorTitle": "授予主持人權限",
"hide": "隱藏",
"hideShareAudioHelper": "不再顯示",
@@ -315,16 +323,16 @@
"incorrectRoomLockPassword": "密碼不符",
"internalError": "喔哦!出現了點問題,發生錯誤:{{error}}",
"internalErrorTitle": "內部錯誤",
"kickMessage": "您可以聯絡{{participantDisplayName}}取得更詳細資訊。",
"kickMessage": "您可以聯絡 {{participantDisplayName}} 取得更詳細資訊。",
"kickParticipantButton": "移除",
"kickParticipantDialog": "您確定要將這位與會者移除嗎?",
"kickParticipantTitle": "移除這位與會者?",
"kickTitle": "噢!{{participantDisplayName}}將您從會議中移除",
"kickTitle": "噢!{{participantDisplayName}} 將您從會議中移除",
"linkMeeting": "連結會議",
"linkMeetingTitle": "將會議連結至 Salesforce",
"liveStreaming": "直播串流中",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "正在錄製,無法使用",
"localUserControls": "本使用者控制",
"localUserControls": "本使用者控制",
"lockMessage": "無法鎖定會議。",
"lockRoom": "新增會議 $t(lockRoomPasswordUppercase)",
"lockTitle": "鎖定失敗",
@@ -346,9 +354,9 @@
"muteEveryoneDialog": "與會者可以隨時解除自己的靜音狀態。",
"muteEveryoneDialogModerationOn": "與會者可以隨時請求發言。",
"muteEveryoneElseDialog": "靜音後,您就不能再解除對方的靜音,但對方可以隨時解除自己的靜音狀態。",
"muteEveryoneElseTitle": "是否要讓除了{{whom}}以外的人靜音?",
"muteEveryoneElseTitle": "是否要讓除了 {{whom}} 以外的人靜音?",
"muteEveryoneElsesVideoDialog": "一旦停用,您就不能再重新開啟對方的網路攝影機,但對方隨時能重新開啟自己的網路攝影機。",
"muteEveryoneElsesVideoTitle": "是否要關閉除了{{whom}}以外的人的網路攝影機?",
"muteEveryoneElsesVideoTitle": "是否要關閉除了 {{whom}} 以外的人的網路攝影機?",
"muteEveryoneSelf": "您自己",
"muteEveryoneStartMuted": "現在所有人皆已靜音",
"muteEveryoneTitle": "要將所有人靜音嗎?",
@@ -364,12 +372,12 @@
"muteParticipantsVideoDialog": "確定要停用這位與會者的網路攝影機?您不能再重新開啟對方的網路攝影機,但他們隨時能重新開啟。",
"muteParticipantsVideoDialogModerationOn": "您確定要關閉此與會者的網路攝影機嗎?您和他都無法再將視訊重新開啟。",
"muteParticipantsVideoTitle": "要關閉此與會者的網路攝影機嗎?",
"noDropboxToken": "沒有有效的 Dropbox token",
"noDropboxToken": "沒有有效的 Dropbox 權杖",
"password": "密碼",
"passwordLabel": "會議已被一位與會者鎖定,請輸入$t(lockRoomPassword)以加入。",
"passwordNotSupported": "尚未支援設定會議$t(lockRoomPassword)。",
"passwordNotSupportedTitle": "尚未支援$t(lockRoomPasswordUppercase)",
"passwordRequired": "需要$t(lockRoomPasswordUppercase)",
"passwordLabel": "會議已被一位與會者鎖定,請輸入 $t(lockRoomPassword) 以加入。",
"passwordNotSupported": "尚未支援設定會議 $t(lockRoomPassword)。",
"passwordNotSupportedTitle": "尚未支援 $t(lockRoomPasswordUppercase)",
"passwordRequired": "需要 $t(lockRoomPasswordUppercase)",
"permissionCameraRequiredError": "參與視訊會議需要存取網路攝影機,請在設定中啟用權限",
"permissionErrorTitle": "需要權限",
"permissionMicRequiredError": "參與音訊會議需要存取麥克風,請在設定中啟用權限",
@@ -378,19 +386,19 @@
"recording": "錄製中",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "正在直播時無法使用",
"rejoinNow": "立即重新加入",
"remoteControlAllowedMessage": "{{user}}接受您進行遠端控制的請求!",
"remoteControlDeniedMessage": "{{user}}拒絕您進行遠端控制的請求!",
"remoteControlErrorMessage": "在嘗試向{{user}}請求遠端控制權限時發生錯誤!",
"remoteControlRequestMessage": "您要允許{{user}}遠端控制您的桌面嗎?",
"remoteControlAllowedMessage": "{{user}} 接受您進行遠端控制的請求!",
"remoteControlDeniedMessage": "{{user}} 拒絕您進行遠端控制的請求!",
"remoteControlErrorMessage": "在嘗試向 {{user}} 請求遠端控制權限時發生錯誤!",
"remoteControlRequestMessage": "您要允許 {{user}} 遠端控制您的桌面嗎?",
"remoteControlShareScreenWarning": "請注意:如果按下「允許」您將會分享自己的螢幕!",
"remoteControlStopMessage": "遠端控制會話已結束!",
"remoteControlTitle": "遠端桌面控制",
"remoteUserControls": "{{username}}的遠端使用者控制",
"remoteUserControls": "{{username}} 的遠端使用者控制",
"removePassword": "移除 $t(lockRoomPassword)",
"removeSharedVideoMsg": "您確定要移除您分享的影像嗎?",
"removeSharedVideoTitle": "移除分享的影像",
"renameBreakoutRoomLabel": "討論室名稱",
"renameBreakoutRoomTitle": "重新命名討論室",
"renameBreakoutRoomLabel": "分組討論室名稱",
"renameBreakoutRoomTitle": "重新命名分組討論室",
"reservationError": "預約系統錯誤",
"reservationErrorMsg": "錯誤代碼:{{code}},訊息:{{msg}}",
"retry": "重試",
@@ -410,6 +418,7 @@
"sendPrivateMessageTitle": "私人回覆?",
"serviceUnavailable": "服務無法使用",
"sessTerminated": "通話已經終止",
"sessTerminatedReason": "會議已經終止",
"sessionRestarted": "通話因連線問題重新啟動。",
"shareAudio": "繼續",
"shareAudioTitle": "如何分享音訊",
@@ -438,7 +447,7 @@
"stopRecordingWarning": "確定要停用錄製嗎?",
"stopStreamingWarning": "確定要停止直播串流嗎?",
"streamKey": "直播串流金鑰",
"thankYou": "感謝您使用{{appName}}",
"thankYou": "感謝您使用 {{appName}}",
"token": "token",
"tokenAuthFailed": "抱歉,您未被允許加入這個通話。",
"tokenAuthFailedReason": {
@@ -450,29 +459,33 @@
"featuresNotFound": "`context` 未在負載中找到。",
"headerNotFound": "標頭缺失。",
"issInvalid": "無效的`iss`值,應為`chat`。",
"kidMismatch": "金鑰IDkid與子項不符。",
"kidNotFound": "缺少金鑰IDkid。",
"kidMismatch": "金鑰 IDkid與子項不符。",
"kidNotFound": "缺少金鑰 IDkid。",
"nbfFuture": "`nbf`值在未來。",
"nbfInvalid": "無效的`nbf`值。",
"payloadNotFound": "未找到負載。",
"tokenExpired": "Token已過期。"
"payloadNotFound": "負載缺失。",
"tokenExpired": "權杖已過期。"
},
"tokenAuthFailedTitle": "驗證失敗",
"tokenAuthFailedWithReasons": "抱歉,您無法參加這個通話,可能原因:{{reason}}",
"tokenAuthUnsupported": "不支援的令牌位址。",
"tokenAuthUnsupported": "不支援權杖網址。",
"transcribing": "轉錄中",
"unlockRoom": "移除會議 $t(lockRoomPassword)",
"user": "使用者",
"userIdentifier": "使用者ID",
"userIdentifier": "使用者 ID",
"userPassword": "使用者密碼",
"verifyParticipantConfirm": "符合",
"verifyParticipantDismiss": "不符合",
"verifyParticipantQuestion": "實驗性功能:詢問與會者{{participantName}}是否以相同順序看到相同內容。",
"verifyParticipantQuestion": "實驗性功能:詢問與會者 {{participantName}} 是否以相同順序看到相同內容。",
"verifyParticipantTitle": "使用者驗證",
"videoLink": "影片連結",
"viewUpgradeOptions": "查看升級方案",
"viewUpgradeOptionsContent": "若要不受限制地使用錄製、逐字稿、RTMP 串流等進階版功能,您需要升級您的方案。",
"viewUpgradeOptionsTitle": "您找到了進階版功能!",
"whiteboardLimitContent": "抱歉,已達到白板使用者限制。",
"whiteboardLimitReference": "若要了解詳情,請前往",
"whiteboardLimitReferenceUrl": "我們的網站",
"whiteboardLimitTitle": "白板使用受限",
"yourEntireScreen": "您的整個螢幕"
},
"documentSharing": {
@@ -521,7 +534,7 @@
"copyNumber": "複製號碼",
"country": "國家",
"dialANumber": "若要參加您的會議,請撥打以下其中一支號碼,然後輸入 PIN 碼。",
"dialInConferenceID": "PIN碼",
"dialInConferenceID": "PIN 碼:",
"dialInNotSupported": "抱歉,目前不支援電話撥入。",
"dialInNumber": "撥入:",
"dialInSummaryError": "目前解析撥入資訊錯誤,請稍後再試一次。",
@@ -536,7 +549,7 @@
"inviteTextiOSPersonal": "{{name}}邀請您加入會議。",
"inviteTextiOSPhone": "若要透過電話加入,請使用此號碼:{{number}},,{{conferenceID}}#。如果您需要其他號碼,點擊此連結以檢視完整列表:{{didUrl}}。",
"inviteURLFirstPartGeneral": "您受邀參加會議。",
"inviteURLFirstPartPersonal": "{{name}}正在邀請您加入會議。\n",
"inviteURLFirstPartPersonal": "{{name}} 正在邀請您加入會議。\n",
"inviteURLSecondPart": "\n加入會議\n{{url}}\n",
"label": "撥入資訊",
"liveStreamURL": "直播串流:",
@@ -547,7 +560,8 @@
"numbers": "撥入號碼",
"password": "$t(lockRoomPasswordUppercase)",
"reachedLimit": "您已達到您的方案上限",
"sip": "SIP位址",
"sip": "SIP 位址",
"sipAudioOnly": "SIP 僅音訊位址",
"title": "分享",
"tooltip": "顯示此會議的連結及電話撥入號碼",
"upgradeOptions": "請查看升級選項於"
@@ -566,7 +580,7 @@
"searchPlaceholder": "與會者或電話號碼",
"send": "傳送"
},
"jitsiHome": "{{logo}}商標,首頁連結",
"jitsiHome": "{{logo}} 商標,首頁連結",
"keyboardShortcuts": {
"focusLocal": "聚焦於自己的影像",
"focusRemote": "聚焦於另一人的影像",
@@ -594,11 +608,11 @@
"busyTitle": "全部直播設備正在忙碌",
"changeSignIn": "切換帳號",
"choose": "選擇直播串流",
"chooseCTA": "請選擇直播串流選項,您目前是以{{email}}身份登入。",
"chooseCTA": "請選擇直播串流選項,您目前是以 {{email}} 身份登入。",
"enterStreamKey": "在此輸入您的 YouTube 直播串流金鑰。",
"error": "直播串流失敗,請重試。",
"errorAPI": "在存取您的 YouTube 直播時發生問題,請重新登入。",
"errorLiveStreamNotEnabled": "直播在{{email}}尚未啟用,請開啟直播串流或登入有啟用直播串流的帳號。",
"errorLiveStreamNotEnabled": "直播在 {{email}} 尚未啟用,請開啟直播串流或登入有啟用直播串流的帳號。",
"expandedOff": "直播已停用",
"expandedOn": "會議目前正在 YouTube 上直播。",
"expandedPending": "直播串流正被啟動...",
@@ -607,12 +621,12 @@
"googlePrivacyPolicy": "Google 隱私權政策",
"inProgress": "正在錄製或直播",
"invalidStreamKey": "直播串流金鑰可能不正確。",
"limitNotificationDescriptionNative": "您的最大直播長度將被限制在{{limit}}分鐘,若要不受限的直播,請使用{{app}}。",
"limitNotificationDescriptionWeb": "由於目前流量過大,您的最大直播長度將被限制在{{limit}}分鐘。若要不受限的直播,請使用<a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>。",
"limitNotificationDescriptionNative": "您的最大直播長度將被限制在 {{limit}} 分鐘,若要不受限的直播,請使用 {{app}}。",
"limitNotificationDescriptionWeb": "由於目前流量過大,您的最大直播長度將被限制在 {{limit}} 分鐘。若要不受限的直播,請使用 <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>。",
"off": "直播串流已停用",
"offBy": "{{name}}停用了直播串流",
"offBy": "{{name}} 停用了直播串流",
"on": "直播串流已啟動",
"onBy": "{{name}}啟動了直播串流",
"onBy": "{{name}} 啟動了直播串流",
"pending": "啟動直播串流...",
"serviceName": "直播串流服務",
"sessionAlreadyActive": "已在錄製或直播此工作階段。",
@@ -652,13 +666,13 @@
"knockButton": "請求加入",
"knockTitle": "有人想要加入會議",
"knockingParticipantList": "請求加入的與會者名單",
"lobbyChatStartedNotification": "{{moderator}}{{attendee}}開始在大廳中聊天",
"lobbyChatStartedTitle": "{{moderator}}與您開始在大廳中聊天。",
"lobbyChatStartedNotification": "{{moderator}}{{attendee}} 開始在大廳中聊天",
"lobbyChatStartedTitle": "{{moderator}} 與您開始在大廳中聊天。",
"nameField": "輸入您的名字",
"notificationLobbyAccessDenied": "{{originParticipantName}}拒絕了{{targetParticipantName}}的加入請求",
"notificationLobbyAccessGranted": "{{originParticipantName}}同意了{{targetParticipantName}}的加入請求",
"notificationLobbyDisabled": "{{originParticipantName}}已停用大廳模式",
"notificationLobbyEnabled": "{{originParticipantName}}已啟用大廳模式",
"notificationLobbyAccessDenied": "{{originParticipantName}} 拒絕了 {{targetParticipantName}} 的加入請求",
"notificationLobbyAccessGranted": "{{originParticipantName}} 同意了 {{targetParticipantName}} 的加入請求",
"notificationLobbyDisabled": "{{originParticipantName}} 已停用大廳模式",
"notificationLobbyEnabled": "{{originParticipantName}} 已啟用大廳模式",
"notificationTitle": "大廳",
"passwordField": "輸入會議密碼",
"passwordJoinButton": "加入",
@@ -673,19 +687,19 @@
"on": "開啟",
"unknown": "未知"
},
"dialogTitle": "本錄製控制",
"dialogTitle": "本錄製控制",
"duration": "時長",
"durationNA": "不適用",
"encoding": "編碼中",
"label": "本錄製",
"labelToolTip": "本錄製已啟用",
"localRecording": "本錄製中",
"label": "本錄製",
"labelToolTip": "本錄製已啟用",
"localRecording": "本錄製中",
"me": "我",
"messages": {
"engaged": "已啟用本錄製。",
"engaged": "已啟用本錄製。",
"finished": "錄製會話 {{token}} 已結束,請將傳送錄製檔案至主持人。",
"finishedModerator": "錄製階段{{token}}已完成,本錄製追蹤已存檔,請要求各與會者提供其錄製檔案。",
"notModerator": "您不是主持人,無法開始或停止本錄製。"
"finishedModerator": "錄製階段{{token}} 已完成,本錄製追蹤已存檔,請要求各與會者提供其錄製檔案。",
"notModerator": "您不是主持人,無法開始或停止本錄製。"
},
"moderator": "主持人",
"no": "否",
@@ -713,36 +727,37 @@
"audioUnmuteBlockedDescription": "麥克風解除靜音操作由於系統限制而被暫時封鎖。",
"audioUnmuteBlockedTitle": "麥克風解除靜音遭封鎖!",
"chatMessages": "聊天訊息",
"connectedOneMember": "{{name}}加入了會議",
"connectedThreePlusMembers": "{{name}}與其他人加入了會議",
"connectedTwoMembers": "{{first}}與{{second}}加入了會議",
"connectedOneMember": "{{name}} 加入了會議",
"connectedThreePlusMembers": "{{name}} 與其他人加入了會議",
"connectedTwoMembers": "{{first}} 與{{second}} 加入了會議",
"dataChannelClosed": "視訊品質受限",
"dataChannelClosedDescription": "橋接通道已斷開,視訊品質降至最低設定。",
"disabledIframe": "嵌入僅供示範使用,此通話將於 {{timeout}} 分鐘後中斷連線。",
"disabledIframeSecondary": "內嵌 {{domain}} 僅為展示用途,此通話將在 {{timeout}} 分鐘後中斷連線。請使用在正式環境使用 <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi 服務</a>來內嵌!",
"disconnected": "已經中斷連接",
"displayNotifications": "顯示通知給",
"dontRemindMe": "不要再提醒我",
"focus": "會議焦點",
"focusFail": "{{component}}無法使用 - {{ms}}秒後重試",
"focusFail": "{{component}} 無法使用 - {{ms}} 秒後重試",
"gifsMenu": "GIPHY",
"groupTitle": "通知",
"hostAskedUnmute": "主持人希望發言",
"invitedOneMember": "{{name}}已受邀請",
"invitedThreePlusMembers": "{{name}}{{count}}位人員已受邀請",
"invitedTwoMembers": "{{first}}{{second}}已受邀請",
"hostAskedUnmute": "主持人希望發言",
"invitedOneMember": "{{name}} 已受邀請",
"invitedThreePlusMembers": "{{name}}{{count}} 位人員已受邀請",
"invitedTwoMembers": "{{first}}{{second}} 已受邀請",
"joinMeeting": "加入",
"kickParticipant": "{{kicked}}已被{{kicker}}移除會議",
"leftOneMember": "{{name}}已離開會議",
"leftThreePlusMembers": "{{name}}與其他人已離開會議",
"leftTwoMembers": "{{first}}{{second}}已離開會議",
"kickParticipant": "{{kicked}} 已被 {{kicker}} 移除會議",
"leftOneMember": "{{name}} 已離開會議",
"leftThreePlusMembers": "{{name}} 與其他人已離開會議",
"leftTwoMembers": "{{first}}{{second}} 已離開會議",
"linkToSalesforce": "連結至 Salesforce",
"linkToSalesforceDescription": "您可以將會議摘要連結至 Salesforce 物件。",
"linkToSalesforceError": "無法將會議連結至 Salesforce",
"linkToSalesforceKey": "連結此會議",
"linkToSalesforceProgress": "正在將會議連結至 Salesforce...",
"linkToSalesforceSuccess": "會議已連結至 Salesforce",
"localRecordingStarted": "{{name}}已啟用本錄製",
"localRecordingStopped": "{{name}}已停用本錄製",
"localRecordingStarted": "{{name}} 已啟用本錄製",
"localRecordingStopped": "{{name}} 已停用本錄製",
"me": "我",
"moderationInEffectCSDescription": "若要分享您的螢幕,請舉手",
"moderationInEffectCSTitle": "分享螢幕已被主持人停用",
@@ -754,11 +769,11 @@
"moderationRequestFromParticipant": "想要發言",
"moderationStartedTitle": "開始主持",
"moderationStoppedTitle": "停止主持",
"moderationToggleDescription": "由{{participantDisplayName}}",
"moderationToggleDescription": "由 {{participantDisplayName}}",
"moderator": "主持人權限已經取得!",
"muted": "您已經啟動通話,處於靜音。",
"mutedRemotelyDescription": "當您準備好要發言,隨時可以取消靜音。當您結束之後再回復成靜音,保持會議安靜。",
"mutedRemotelyTitle": "您已經被{{participantDisplayName}}設為靜音!",
"mutedRemotelyTitle": "您已經被 {{participantDisplayName}} 設為靜音!",
"mutedTitle": "您目前處於靜音!",
"newDeviceAction": "使用",
"newDeviceAudioTitle": "偵測到新的音效裝置",
@@ -771,11 +786,11 @@
"oldElectronClientDescription3": "",
"participantWantsToJoin": "希望加入會議",
"participantsWantToJoin": "希望加入會議",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase)已被其他與會者移除",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase)由其他與會者設定",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) 已被其他與會者移除",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) 由其他與會者設定",
"raiseHandAction": "舉手發言",
"raisedHand": "{{name}}想要發言。",
"raisedHands": "{{participantName}}與其他{{raisedHands}}人",
"raisedHands": "{{participantName}} 與其他 {{raisedHands}} 人",
"reactionSounds": "停用音效",
"reactionSoundsForAll": "停用所有音效",
"screenShareNoAudio": "您未在選擇視窗時勾選分享音訊",
@@ -790,11 +805,13 @@
"suboptimalExperienceTitle": "瀏覽器警告",
"unmute": "取消靜音",
"videoMutedRemotelyDescription": "您隨時可以再次啟用。",
"videoMutedRemotelyTitle": "您的視訊已被{{participantDisplayName}}停用",
"videoMutedRemotelyTitle": "您的視訊已被 {{participantDisplayName}} 停用",
"videoUnmuteBlockedDescription": "啟用網路攝影機與分享螢幕由於系統限制而被暫時封鎖。",
"videoUnmuteBlockedTitle": "啟用網路攝影機與分享螢幕遭封鎖!",
"viewLobby": "檢視大廳",
"waitingParticipants": "{{waitingParticipants}}人"
"waitingParticipants": "{{waitingParticipants}} 人",
"whiteboardLimitDescription": "由於即將超出使用者限制,白板將關閉,請儲存您的進度。",
"whiteboardLimitTitle": "白板使用情況"
},
"participantsPane": {
"actions": {
@@ -803,6 +820,7 @@
"askUnmute": "要求解除靜音",
"audioModeration": "自我解除靜音",
"blockEveryoneMicCamera": "停用所有人的麥克風和網路攝影機",
"breakoutRooms": "分組討論室",
"invite": "邀請他人",
"moreModerationActions": "更多主持人選項",
"moreModerationControls": "更多主持人操作",
@@ -817,15 +835,15 @@
},
"close": "關閉",
"headings": {
"lobby": "大廳({{count}}人)",
"participantsList": "會議與會者({{count}}人)",
"visitors": "訪客({{count}}人)",
"waitingLobby": "於大廳等候({{count}}人)"
"lobby": "大廳({{count}} 人)",
"participantsList": "會議與會者({{count}} 人)",
"visitors": "訪客({{count}} 人)",
"waitingLobby": "於大廳等候({{count}} 人)"
},
"search": "搜尋與會者",
"title": "與會者"
},
"passwordDigitsOnly": "上限為{{number}}位數",
"passwordDigitsOnly": "上限為 {{number}} 位數",
"passwordSetRemotely": "由其他與會者設定",
"pinParticipant": "{{participantName}} - 釘選",
"pinnedParticipant": "與會者被釘選",
@@ -834,13 +852,13 @@
"skip": "跳過",
"submit": "送出"
},
"by": "由{{ name }}",
"by": "由 {{ name }}",
"create": {
"addOption": "新增選項",
"answerPlaceholder": "選項{{index}}",
"answerPlaceholder": "選項 {{index}}",
"cancel": "取消",
"create": "建立投票",
"pollOption": "選項{{index}}",
"pollOption": "選項 {{index}}",
"pollQuestion": "投票問題",
"questionPlaceholder": "詢問問題",
"removeOption": "移除選項",
@@ -879,7 +897,7 @@
"audioHighQuality": "您的音訊品質極佳。",
"audioLowNoVideo": "您的音訊品質較差,且無視訊功能。",
"goodQuality": "太好了!您的媒體品質良好。",
"noMediaConnectivity": "我們無法為此測試建立媒體連線通常是防火牆或NAT的問題。",
"noMediaConnectivity": "我們無法為此測試建立媒體連線,通常是防火牆或 NAT 的問題。",
"noVideo": "您的視訊畫質將會很糟糕。",
"undetectable": "如果您仍無法在瀏覽器中進行通話,我們建議您檢查喇叭、麥克風、及網路攝影機的設置,確認是否允許瀏覽器存取麥克風及網路攝影機,並將瀏覽器更新到最新版本。如果以上步驟無法解決問題,請聯絡網頁程式的開發者。",
"veryPoorConnection": "您的通話品質將會非常糟糕。",
@@ -958,8 +976,8 @@
},
"recording": {
"authDropboxText": "上傳至 Dropbox",
"availableSpace": "可用空間:{{spaceLeft}}MB錄製時間大約{{duration}}分鐘)",
"beta": "BETA",
"availableSpace": "可用空間:{{spaceLeft}} MB錄製時間大約 {{duration}} 分鐘)",
"beta": "測試版",
"busy": "我們正在釋放錄製資源,請過幾分鐘後再試。",
"busyTitle": "全部錄製目前忙碌",
"copyLink": "複製連結",
@@ -976,15 +994,15 @@
"highlightMomentSuccess": "已精選的時刻",
"highlightMomentSucessDescription": "您的精選時刻將新增至會議摘要。",
"inProgress": "正在錄製或直播",
"limitNotificationDescriptionNative": "由於目前流量過大,您的錄製時間被限制在{{limit}}分鐘。若要無限制的錄製,請試試 <3>{{app}}</3>。",
"limitNotificationDescriptionWeb": "由於目前流量過大,您的錄製時間被限制在{{limit}}分鐘。若要無限制的錄製,請試試 <a href={{url}}rel='noopener noreferrer' target='_blank'>{{app}}</a>。",
"limitNotificationDescriptionNative": "由於目前流量過大,您的錄製時間被限制在 {{limit}} 分鐘。若要無限制的錄製,請試試 <3>{{app}}</3>。",
"limitNotificationDescriptionWeb": "由於目前流量過大,您的錄製時間被限制在 {{limit}} 分鐘。若要無限制的錄製,請試試 <a href={{url}}rel='noopener noreferrer' target='_blank'>{{app}}</a>。",
"linkGenerated": "我們建立了您的錄製檔案的連結。",
"live": "直播",
"localRecordingNoNotificationWarning": "系統不會主動知會與會者錄製已開啟,主持人需另行通知。",
"localRecordingNoVideo": "沒有錄製的視訊",
"localRecordingStartWarning": "請確保在退出會議之前停用錄製以便保存。",
"localRecordingStartWarningTitle": "停用錄製以保存",
"localRecordingVideoStop": "關閉您的視訊也將停止本錄製,確定繼續嗎?",
"localRecordingVideoStop": "關閉您的視訊也將停止本錄製,確定繼續嗎?",
"localRecordingVideoWarning": "錄製視訊必須在開始時啟用",
"localRecordingWarning": "確保選擇目前的分頁以錄製正確的視訊和音訊。錄製目前限制為1GB約可錄製100分鐘。",
"loggedIn": "以 {{userName}} 登入",
@@ -997,7 +1015,7 @@
"onlyRecordSelf": "僅錄製我的音訊和影片串流",
"pending": "正在準備錄製會議...",
"rec": "錄製中",
"saveLocalRecording": "將錄製檔案保存在本BETA",
"saveLocalRecording": "將錄製檔案保存在本機(測試版",
"serviceDescription": "您的錄製會由錄製服務儲存",
"serviceDescriptionCloud": "雲端錄製",
"serviceDescriptionCloudInfo": "已錄製的會議將在 24 小時後自動清除。",
@@ -1007,19 +1025,19 @@
"signOut": "登出",
"surfaceError": "請選擇目前分頁",
"title": "錄製中",
"unavailable": "喔哦!{{serviceName}}目前無法使用,我們正在解決此問題,請稍後再試。",
"unavailable": "喔哦!{{serviceName}} 目前無法使用,我們正在解決此問題,請稍後再試。",
"unavailableTitle": "錄製無法使用",
"uploadToCloud": "上傳至雲端"
},
"screenshareDisplayName": "{{name}}的螢幕",
"screenshareDisplayName": "{{name}} 的螢幕",
"sectionList": {
"pullToRefresh": "下拉以重新整理"
},
"security": {
"about": "您可以新增$t(lockRoomPassword)至您的會議,與會者在加入會議前必須先輸入$t(lockRoomPassword)。",
"aboutReadOnly": "主持人可以新增$t(lockRoomPassword)至會議,與會者在加入會議前必須先輸入$t(lockRoomPassword)。",
"insecureRoomNameWarningNative": "房間名稱不安全,任何人都可能會加入您的會議。{{recommendAction}} 了解有關保護您的會議的更多信息。",
"insecureRoomNameWarningWeb": "房間名稱不安全,任何人都可能會加入您的會議。{{recommendAction}} 在此處了解有關保護您的會議的更多信息 <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">這裡</a>。",
"about": "您可以新增 $t(lockRoomPassword) 至您的會議,與會者在加入會議前必須先輸入$t(lockRoomPassword)。",
"aboutReadOnly": "主持人可以新增 $t(lockRoomPassword) 至會議,與會者在加入會議前必須先輸入$t(lockRoomPassword)。",
"insecureRoomNameWarningNative": "房間名稱不安全,任何人都可能會加入您的會議。{{recommendAction}} 了解有關保護您的會議的更多資訊。",
"insecureRoomNameWarningWeb": "房間名稱不安全,任何人都可能會加入您的會議。{{recommendAction}} 在此處了解有關保護您的會議的更多資訊 <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">這裡</a>。",
"title": "安全性選項",
"unsafeRoomActions": {
"meeting": "請考慮使用安全按鈕保護您的會議。",
@@ -1031,10 +1049,10 @@
"audio": "音訊",
"buttonLabel": "設定",
"calendar": {
"about": "{{appName}}行事曆整合功能可安全地存取您行事曆中即將舉行的活動。",
"about": "{{appName}} 行事曆整合功能可安全地存取您行事曆中即將舉行的活動。",
"disconnect": "中斷連接",
"microsoftSignIn": "使用Microsoft帳號登入",
"signedIn": "目前正在存取{{email}}的行事曆事件,點按下方中斷連接可以停用存取行事曆事件。",
"microsoftSignIn": "使用 Microsoft 帳號登入",
"signedIn": "目前正在存取 {{email}} 的行事曆事件,點按下方中斷連接可以停用存取行事曆事件。",
"title": "行事曆"
},
"desktopShareFramerate": "桌面螢幕分享影格率",
@@ -1079,6 +1097,7 @@
"alertOk": "確定",
"alertTitle": "警告",
"alertURLText": "輸入的伺服器網址無效",
"apply": "套用",
"buildInfoSection": "組建資訊",
"conferenceSection": "會議",
"disableCallIntegration": "停用原生通話整合",
@@ -1086,23 +1105,24 @@
"disableCrashReportingWarning": "您確定要停用錯誤回報功能嗎?變更將在重啟應用程式後生效。",
"disableP2P": "停用點對點模式",
"displayName": "顯示名稱",
"displayNamePlaceholderText": "例如:John Doe",
"displayNamePlaceholderText": "例如:王小明",
"email": "電子郵件",
"emailPlaceholderText": "email@example.com",
"gavatarMessage": "如果您的電子郵件地址與 Gravatar 帳號相關聯,我們會使用 Gravatar 上的個人檔案大頭貼。",
"goTo": "前往",
"header": "設定",
"help": "協助",
"links": "連結",
"privacy": "隱私權",
"profileSection": "簡介",
"sdkVersion": "SDK版本",
"sdkVersion": "SDK 版本",
"serverURL": "伺服器網址",
"showAdvanced": "顯示進階設定",
"startCarModeInLowBandwidthMode": "同時啟用行車模式與低頻寬模式",
"startWithAudioMuted": "啟動並靜音",
"startWithVideoMuted": "啟動並關閉影像",
"terms": "條款",
"version": "APP版本"
"version": "應用程式版本"
},
"share": {
"dialInfoText": "\n\n=====\n\n只想要透過手機撥打加入嗎\n\n撥打{{defaultDialInNumber}}或點此連結來查看此會議的電話撥入號碼\n{{dialInfoPageUrl}}",
@@ -1146,7 +1166,7 @@
"Settings": "切換設定",
"audioOnly": "切換僅音訊",
"audioRoute": "選擇音訊裝置",
"boo": "倒彩",
"boo": "倒彩",
"breakoutRoom": "進入/離開分組討論室",
"callQuality": "管理視訊品質",
"carmode": "行車模式",
@@ -1160,7 +1180,7 @@
"document": "切換檔案分享",
"documentClose": "關閉檔案分享",
"documentOpen": "打開檔案分享",
"download": "下載我們的APP",
"download": "下載我們的應用程式",
"embedMeeting": "嵌入會議",
"endConference": "結束會議(所有人)",
"enterFullScreen": "進入全螢幕",
@@ -1183,7 +1203,7 @@
"like": "比讚",
"linkToSalesforce": "連結至 Salesforce",
"lobbyButton": "啟用/停用大廳模式",
"localRecording": "切換本錄製控制",
"localRecording": "切換本錄製控制",
"lockRoom": "切換會議密碼",
"lowerHand": "放下手",
"moreActions": "更多動作",
@@ -1199,7 +1219,7 @@
"openChat": "打開聊天",
"participants": "打開與會者窗格",
"pip": "切換子母螢幕模式",
"privateMessage": "送私人訊息",
"privateMessage": "送私人訊息",
"profile": "編輯您的個人檔案",
"raiseHand": "舉手",
"reactions": "反應",
@@ -1237,7 +1257,7 @@
"audioRoute": "選擇音訊裝置",
"audioSettings": "音訊設定",
"authenticate": "驗證",
"boo": "倒彩",
"boo": "倒彩",
"callQuality": "管理影像品質",
"chat": "開啟/關閉聊天欄",
"clap": "鼓掌",
@@ -1248,7 +1268,7 @@
"disableReactionSounds": "您可以停用此會議的反應音效",
"documentClose": "關閉分享檔案欄",
"documentOpen": "開啟分享檔案欄",
"download": "下載我們的APP",
"download": "下載我們的應用程式",
"e2ee": "端對端加密",
"embedMeeting": "嵌入會議",
"enableNoiseSuppression": "開啟雜訊抑制",
@@ -1273,7 +1293,7 @@
"lobbyButtonEnable": "啟用大廳模式",
"login": "登入",
"logout": "登出",
"lowerYourHand": "放下的手",
"lowerYourHand": "放下的手",
"moreActions": "更多動作",
"moreOptions": "更多選項",
"mute": "靜音/解除靜音",
@@ -1294,9 +1314,9 @@
"pip": "進入子母螢幕模式",
"privateMessage": "傳送私人訊息",
"profile": "編輯您的個人資料",
"raiseHand": "舉起/放下的手",
"raiseHand": "舉起/放下的手",
"raiseYourHand": "舉手",
"reactionBoo": "傳送倒彩反應",
"reactionBoo": "傳送倒彩反應",
"reactionClap": "傳送鼓掌反應",
"reactionLaugh": "傳送大笑反應",
"reactionLike": "傳送比讚反應",
@@ -1336,7 +1356,7 @@
"labelToolTip": "此會議正在轉錄",
"off": "轉錄已停用",
"pending": "準備轉錄會議...",
"sourceLanguageDesc": "會議語言目前設定為<b>{{sourceLanguage}}</b><br/>您可以在這裡",
"sourceLanguageDesc": "會議語言目前設定為 <b>{{sourceLanguage}}</b><br/> 您可以在這裡",
"sourceLanguageHere": "修改",
"start": "開始顯示字幕",
"stop": "停用顯示字幕",
@@ -1360,12 +1380,12 @@
"videoSIPGW": {
"busy": "我們正在釋放資源,請過幾分鐘後再試。",
"busyTitle": "會議室服務目前忙碌中",
"errorAlreadyInvited": "{{displayName}}已經受邀",
"errorAlreadyInvited": "{{displayName}} 已經受邀",
"errorInvite": "會議尚未開始,請稍候再試。",
"errorInviteFailed": "我們正在努力解決這個問題,請稍後再試。",
"errorInviteFailedTitle": "邀請{{displayName}}失敗",
"errorInviteFailedTitle": "邀請 {{displayName}} 失敗",
"errorInviteTitle": "會議室邀請錯誤",
"pending": "已向{{displayName}}送邀請"
"pending": "已向 {{displayName}}送邀請"
},
"videoStatus": {
"adjustFor": "調整為:",
@@ -1415,7 +1435,7 @@
},
"virtualBackground": {
"accessibilityLabel": {
"currentBackground": "前背景:{{background}}",
"currentBackground": "前背景:{{background}}",
"selectBackground": "選擇背景"
},
"addBackground": "新增背景",
@@ -1438,8 +1458,8 @@
"slightBlur": "輕微模糊",
"title": "虛擬背景",
"uploadedImage": "已上傳的圖片 {{index}}",
"webAssemblyWarning": "不支援WebAssembly",
"webAssemblyWarningDescription": "WebAssembly已停用或不受此瀏覽器支援"
"webAssemblyWarning": "不支援 WebAssembly",
"webAssemblyWarningDescription": "WebAssembly 已停用或不受此瀏覽器支援"
},
"visitors": {
"chatIndicator": "(訪客)",
@@ -1456,14 +1476,14 @@
"roomname": "輸入會議室名稱"
},
"addMeetingName": "新增會議名稱",
"appDescription": "來吧,和您的整個團隊進行視訊會議。不,邀請所有您認識的人進行視訊會議。{{app}}是一套完全加密、100% 開放始碼的視訊會議解決方案。無需註冊帳號,無時無刻不分日夜均可免費使用。",
"appDescription": "來吧,和您的整個團隊進行視訊會議。不,邀請所有您認識的人進行視訊會議。{{app}} 是一套完全加密、100% 開放始碼的視訊會議解決方案。無需註冊帳號,無時無刻不分日夜均可免費使用。",
"audioVideoSwitch": {
"audio": "音訊",
"video": "視訊"
},
"calendar": "行事曆",
"connectCalendarButton": "連接您的行事曆",
"connectCalendarText": "連接您的行事曆來查看在{{app}}中的會議。此外,增加{{provider}}的會議至自己的行事曆,只要點按一下即可啟動。",
"connectCalendarText": "連接您的行事曆來查看在 {{app}} 中的會議。此外,增加 {{provider}} 的會議至自己的行事曆,只要點按一下即可啟動。",
"enterRoomTitle": "啟動新的會議",
"getHelp": "取得協助",
"go": "開始",
@@ -1482,25 +1502,25 @@
"policyLogo": "政策圖示"
},
"meetingsAccessibilityLabel": "會議",
"mobileDownLoadLinkAndroid": "下載 Android 版本的手機應用程式",
"mobileDownLoadLinkFDroid": "前往 F-Droid 下載 Android 版本的手機應用程式",
"mobileDownLoadLinkIos": "下載 iOS 版本的手機應用程式",
"mobileDownLoadLinkAndroid": "下載 Android 版本的應用程式",
"mobileDownLoadLinkFDroid": "前往 F-Droid 下載 Android 版本的應用程式",
"mobileDownLoadLinkIos": "下載 iOS 版本的應用程式",
"moderatedMessage": "或以主持人身份<a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">預先建立會議</a>。",
"privacy": "隱私權",
"recentList": "最近使用",
"recentList": "近期",
"recentListDelete": "刪除",
"recentListEmpty": "目前最近使用是空白的,與您的團隊成員聊天,即在此處找到最近使用過的會議。",
"recentMeetings": "您的最近會議",
"recentListEmpty": "您的近期列表目前是空白的,與您的團隊成員聊天,即在此處找到最近參與過的會議。",
"recentMeetings": "您近期的會議",
"reducedUIText": "歡迎使用{{app}}",
"roomNameAllowedChars": "會議室名稱不應包含以下字元:? & : ' \" % #",
"roomNameAllowedChars": "會議室名稱不應包含以下字元:?、&、:、'、\"、%、#。",
"roomname": "輸入會議室名稱",
"roomnameHint": "請輸入您想加入的會議室名稱或網址,您可以用一個名稱來建立會議室,只要其他人輸入相同的名稱就能加入會議室。",
"roomnameHint": "請輸入您想加入的會議室名稱或網址,您可以用一個名稱來建立會議室,只要其他人輸入相同的名稱就能加入會議室。",
"sendFeedback": "傳送回饋",
"settings": "設定",
"startMeeting": "開始會議",
"terms": "條款",
"title": "安全、功能齊全、完全免費的視訊會議",
"upcomingMeetings": "您即將的會議"
"upcomingMeetings": "您即將開始的會議"
},
"whiteboard": {
"accessibilityLabel": {

View File

@@ -67,13 +67,18 @@
"renameBreakoutRoom": "Rename breakout room",
"sendToBreakoutRoom": "Send participant to:"
},
"breakoutList": "Breakout list",
"buttonLabel": "Breakout rooms",
"defaultName": "Breakout room #{{index}}",
"hideParticipantList": "Hide participant list",
"mainRoom": "Main room",
"notifications": {
"joined": "Joining the \"{{name}}\" breakout room",
"joinedMainRoom": "Joining the main room",
"joinedTitle": "Breakout Rooms"
}
},
"showParticipantList": "Show participant list",
"title": "Breakout Rooms"
},
"calendarSync": {
"addMeetingURL": "Add a meeting link",
@@ -477,6 +482,10 @@
"viewUpgradeOptions": "View upgrade options",
"viewUpgradeOptionsContent": "To get unlimited access to premium features like recording, transcriptions, RTMP Streaming & more, you'll need to upgrade your plan.",
"viewUpgradeOptionsTitle": "You discovered a premium feature!",
"whiteboardLimitContent": "Sorry, the limit of conccurent whiteboard users has been reached.",
"whiteboardLimitReference": "For more information please visit",
"whiteboardLimitReferenceUrl": "our website",
"whiteboardLimitTitle": "Whiteboard usage restricted",
"yourEntireScreen": "Your entire screen"
},
"documentSharing": {
@@ -724,6 +733,7 @@
"dataChannelClosed": "Video quality impaired",
"dataChannelClosedDescription": "The bridge channel has been disconnected and thus video quality is limited to its lowest setting.",
"disabledIframe": "Embedding is only meant for demo purposes, so this call will disconnect in {{timeout}} minutes.",
"disabledIframeSecondary": "Embedding {{domain}} is only meant for demo purposes, so this call will disconnect in {{timeout}} minutes. Please use <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> for production embedding!",
"disconnected": "disconnected",
"displayNotifications": "Display notifications for",
"dontRemindMe": "Do not remind me",
@@ -799,7 +809,9 @@
"videoUnmuteBlockedDescription": "Camera unmute and desktop sharing operation have been temporarily blocked because of system limits.",
"videoUnmuteBlockedTitle": "Camera unmute and desktop sharing blocked!",
"viewLobby": "View lobby",
"waitingParticipants": "{{waitingParticipants}} people"
"waitingParticipants": "{{waitingParticipants}} people",
"whiteboardLimitDescription": "Please save your progress, as the user limit will soon be reached and the whiteboard will close.",
"whiteboardLimitTitle": "Whiteboard usage"
},
"participantsPane": {
"actions": {
@@ -808,6 +820,7 @@
"askUnmute": "Ask to unmute",
"audioModeration": "Unmute themselves",
"blockEveryoneMicCamera": "Block everyone's mic and camera",
"breakoutRooms": "Breakout rooms",
"invite": "Invite Someone",
"moreModerationActions": "More moderation options",
"moreModerationControls": "More moderation controls",
@@ -1154,7 +1167,7 @@
"audioOnly": "Toggle audio only",
"audioRoute": "Select the sound device",
"boo": "Boo",
"breakoutRoom": "Join/leave breakout room",
"breakoutRooms": "Breakout rooms",
"callQuality": "Manage video quality",
"carmode": "Car Mode",
"cc": "Toggle subtitles",

View File

@@ -993,7 +993,13 @@ function initCommands() {
callback(isP2pActive(APP.store.getState()));
break;
}
case '_new_electron_screensharing_supported': {
callback(true);
break;
}
default:
callback({ error: new Error('UnknownRequestError') });
return false;
}
@@ -1101,7 +1107,11 @@ class API {
this._enabled = true;
initCommands();
this.notifyBrowserSupport(isSupportedBrowser());
// Let the embedder know we are ready.
this._sendEvent({ name: 'ready' });
}
/**
@@ -1279,6 +1289,19 @@ class API {
});
}
/**
* Notify request desktop sources.
*
* @param {Object} options - Object with the options for desktop sources.
* @returns {void}
*/
requestDesktopSources(options) {
return transport.sendRequest({
name: '_request-desktop-sources',
options
});
}
/**
* Notify external application that the video quality setting has changed.
*

View File

@@ -145,6 +145,7 @@ const events = {
'prejoin-screen-loaded': 'prejoinScreenLoaded',
'proxy-connection-event': 'proxyConnectionEvent',
'raise-hand-updated': 'raiseHandUpdated',
'ready': 'ready',
'recording-link-available': 'recordingLinkAvailable',
'recording-status-changed': 'recordingStatusChanged',
'participant-menu-button-clicked': 'participantMenuButtonClick',
@@ -162,6 +163,10 @@ const events = {
'whiteboard-status-changed': 'whiteboardStatusChanged'
};
const requests = {
'_request-desktop-sources': '_requestDesktopSources'
};
/**
* Last id of api object.
*
@@ -269,10 +274,10 @@ function parseArguments(args) {
function parseSizeParam(value) {
let parsedValue;
// This regex parses values of the form 100px, 100em, 100pt or 100%.
// This regex parses values of the form 100px, 100em, 100pt, 100vh, 100vw or 100%.
// Values like 100 or 100px are handled outside of the regex, and
// invalid values will be ignored and the minimum will be used.
const re = /([0-9]*\.?[0-9]+)(em|pt|px|%)$/;
const re = /([0-9]*\.?[0-9]+)(em|pt|px|((d|l|s)?v)(h|w)|%)$/;
if (typeof value === 'string' && String(value).match(re) !== null) {
parsedValue = value;
@@ -361,7 +366,9 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
},
release
});
this._createIFrame(height, width, onload, sandbox);
this._createIFrame(height, width, sandbox);
this._transport = new Transport({
backend: new PostMessageTransportBackend({
postisOptions: {
@@ -371,9 +378,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
}
})
});
if (Array.isArray(invitees) && invitees.length > 0) {
this.invite(invitees);
}
this._onload = onload;
this._tmpE2EEKey = e2eeKey;
this._isLargeVideoVisible = false;
this._isPrejoinVideoVisible = false;
@@ -392,14 +402,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* parseSizeParam for format details.
* @param {number|string} width - The with of the iframe. Check
* parseSizeParam for format details.
* @param {Function} onload - The function that will listen
* for onload event.
* @param {string} sandbox - Sandbox directive for the created iframe, if desired.
* @returns {void}
*
* @private
*/
_createIFrame(height, width, onload, sandbox) {
_createIFrame(height, width, sandbox) {
const frameName = `jitsiConferenceFrame${id}`;
this._frame = document.createElement('iframe');
@@ -423,11 +431,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
this._frame.sandbox = sandbox;
}
if (onload) {
// waits for iframe resources to load
// and fires event when it is done
this._frame.onload = onload;
}
this._frame.src = this._url;
this._frame = this._parentNode.appendChild(this._frame);
@@ -576,6 +579,12 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
const userID = data.id;
switch (name) {
case 'ready': {
// Fake the iframe onload event because it's not reliable.
this._onload?.();
break;
}
case 'video-conference-joined': {
if (typeof this._tmpE2EEKey !== 'undefined') {
@@ -687,6 +696,18 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return false;
});
this._transport.on('request', (request, callback) => {
const requestName = requests[request.name];
const data = {
...request,
name: requestName
};
if (requestName) {
this.emit(requestName, data, callback);
}
});
}
/**
@@ -1258,6 +1279,17 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
});
}
/**
* Returns the state of availability electron share screen via external api.
*
* @returns {Promise}
*/
_isNewElectronScreensharingSupported() {
return this._transport.sendRequest({
name: '_new_electron_screensharing_supported'
});
}
/**
* Pins a participant's video on to the stage view.
*

View File

@@ -203,16 +203,6 @@ UI.toggleFilmstrip = function() {
APP.store.dispatch(setFilmstripVisible(!visible));
};
/**
* Sets muted audio state for participant
*/
UI.setAudioMuted = function(id) {
// FIXME: Maybe this can be removed!
if (APP.conference.isLocalId(id)) {
APP.conference.updateAudioIconEnabled();
}
};
/**
* Sets muted video state for participant
*/

View File

@@ -5,6 +5,7 @@ import {
notifyMicError
} from '../../react/features/base/devices/actions.web';
import {
flattenAvailableDevices,
getAudioOutputDeviceId
} from '../../react/features/base/devices/functions.web';
import { updateSettings } from '../../react/features/base/settings/actions';
@@ -186,7 +187,7 @@ export default {
* @returns {boolean}
*/
newDeviceListAddedLabelsOnly(oldDevices, newDevices) {
const oldDevicesFlattend = oldDevices.audioInput.concat(oldDevices.audioOutput).concat(oldDevices.videoInput);
const oldDevicesFlattend = flattenAvailableDevices(oldDevices);
if (oldDevicesFlattend.length !== newDevices.length) {
return false;

1947
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,8 +22,8 @@
"@giphy/js-fetch-api": "4.7.1",
"@giphy/react-components": "6.8.1",
"@giphy/react-native-sdk": "2.3.0",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.14/jitsi-excalidraw-0.0.14.tgz",
"@jitsi/js-utils": "2.1.3",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.15/jitsi-excalidraw-0.0.15.tgz",
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",
"@jitsi/rnnoise-wasm": "0.1.0",
"@jitsi/rtcstats": "9.5.1",
@@ -65,7 +65,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1691.0.0+255d8f49/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1719.0.0+f8a18cf0/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -79,7 +79,7 @@
"react-focus-on": "3.8.1",
"react-i18next": "10.11.4",
"react-linkify": "1.0.0-alpha",
"react-native": "0.69.11",
"react-native": "0.69.12",
"react-native-background-timer": "2.4.1",
"react-native-calendar-events": "2.2.0",
"react-native-callstats": "3.73.22",
@@ -90,7 +90,7 @@
"react-native-get-random-values": "1.9.0",
"react-native-immersive-mode": "2.0.1",
"react-native-keep-awake": "4.0.0",
"react-native-orientation-locker": "1.5.0",
"react-native-orientation-locker": "https://github.com/jitsi/react-native-orientation-locker/releases/download/v1.5.0-jitsi1/react-native-orientation-locker-1.5.0.tgz",
"react-native-pager-view": "6.2.0",
"react-native-paper": "5.10.3",
"react-native-performance": "5.0.0",
@@ -104,7 +104,7 @@
"react-native-url-polyfill": "2.0.0",
"react-native-video": "6.0.0-alpha.7",
"react-native-watch-connectivity": "1.1.0",
"react-native-webrtc": "111.0.3",
"react-native-webrtc": "111.0.6",
"react-native-webview": "13.5.1",
"react-native-youtube-iframe": "2.3.0",
"react-redux": "7.2.9",
@@ -140,7 +140,7 @@
"@types/react": "17.0.14",
"@types/react-dom": "17.0.14",
"@types/react-linkify": "1.0.1",
"@types/react-native": "0.69.20",
"@types/react-native": "0.69.22",
"@types/react-native-keep-awake": "2.0.3",
"@types/react-native-video": "5.0.14",
"@types/react-redux": "7.1.24",
@@ -157,7 +157,7 @@
"babel-plugin-optional-require": "0.3.1",
"circular-dependency-plugin": "5.2.0",
"clean-css-cli": "4.3.0",
"css-loader": "3.6.0",
"css-loader": "6.8.1",
"eslint": "8.40.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jsdoc": "37.0.3",

View File

@@ -73,6 +73,13 @@ cd ios && pod install && cd ..
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
```
- In `android/app/src/main/AndroidManifest.xml`, under the `</application>` tag, include
```xml
<service
android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService"
android:foregroundServiceType="mediaProjection" />
```
This will take care of the screen share feature.
If you want to test all the steps before applying them to your app, you can check our React Native SDK sample app here:
https://github.com/jitsi/jitsi-meet-sdk-samples/tree/master/react-native
@@ -87,3 +94,6 @@ https://github.com/jitsi/jitsi-meet-sdk-samples/tree/master/react-native
serverURL={'https://meet.jit.si/'}
token={'dkhalhfajhflahlfaahalhfahfsl'} />
```
For more details on how you can use React Native SDK with React Native app, you can follow this link:
https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-react-native-sdk

View File

@@ -0,0 +1,44 @@
package org.jitsi.meet.sdk;
import android.app.Notification;
import android.content.Context;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.module.annotations.ReactModule;
@ReactModule(name = JMOngoingConferenceModule.NAME)
class JMOngoingConferenceModule
extends ReactContextBaseJavaModule {
public static final String NAME = "JMOngoingConference";
public JMOngoingConferenceModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@ReactMethod
public void launch() {
Context context = getReactApplicationContext();
Context activityContext = context.getCurrentActivity();
JitsiMeetOngoingConferenceService.launch(context, activityContext);
}
@ReactMethod
public void abort() {
Context context = getReactApplicationContext();
JitsiMeetOngoingConferenceService.abort(context);
}
@NonNull
@Override
public String getName() {
return NAME;
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.HashMap;
/**
* This class implements an Android {@link Service}, a foreground one specifically, and it's
* responsible for presenting an ongoing notification when a conference is in progress.
* The service will help keep the app running while in the background.
*
* See: https://developer.android.com/guide/components/services
*/
public class JitsiMeetOngoingConferenceService extends Service {
private static final String TAG = JitsiMeetOngoingConferenceService.class.getSimpleName();
public static void launch(Context context, Context activityContext) {
RNOngoingNotification.createOngoingConferenceNotificationChannel(activityContext);
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
ComponentName componentName;
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
componentName = context.startForegroundService(intent);
} else {
componentName = context.startService(intent);
}
} catch (RuntimeException e) {
// Avoid crashing due to ForegroundServiceStartNotAllowedException (API level 31).
// See: https://developer.android.com/guide/components/foreground-services#background-start-restrictions
JitsiMeetLogger.w(TAG + " Ongoing conference service not started", e);
return;
}
if (componentName == null) {
JitsiMeetLogger.w(TAG + " Ongoing conference service not started");
}
}
public static void abort(Context context) {
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
context.stopService(intent);
}
@Override
public void onCreate() {
super.onCreate();
Notification notification = RNOngoingNotification.buildOngoingConferenceNotification(this);
if (notification == null) {
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
startForeground(RNOngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_NOT_STICKY;
}
}

View File

@@ -21,6 +21,7 @@ public class JitsiMeetReactNativePackage implements ReactPackage {
new AndroidSettingsModule(reactContext),
new AppInfoModule(reactContext),
new AudioModeModule(reactContext),
new JMOngoingConferenceModule(reactContext),
new JavaScriptSandboxModule(reactContext),
new LocaleDetector(reactContext),
new LogBridgeModule(reactContext),

View File

@@ -0,0 +1,97 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.Random;
/**
* Helper class for creating the ongoing notification which is used with
* {@link JitsiMeetOngoingConferenceService}. It allows the user to easily get back to the app
* and to hangup from within the notification itself.
*/
class RNOngoingNotification {
private static final String TAG = RNOngoingNotification.class.getSimpleName();
static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
static void createOngoingConferenceNotificationChannel(Context activityContext) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
if (activityContext == null) {
JitsiMeetLogger.w(TAG + " Cannot create notification channel: no current context");
return;
}
NotificationManager notificationManager
= (NotificationManager) activityContext.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel
= notificationManager.getNotificationChannel("JitsiOngoingConferenceChannel");
if (channel != null) {
// The channel was already created, no need to do it again.
return;
}
channel = new NotificationChannel("JitsiOngoingConferenceChannel", activityContext.getString(R.string.ongoing_notification_channel_name), NotificationManager.IMPORTANCE_DEFAULT);
channel.enableLights(false);
channel.enableVibration(false);
channel.setShowBadge(false);
notificationManager.createNotificationChannel(channel);
}
static Notification buildOngoingConferenceNotification(Context context) {
if (context == null) {
JitsiMeetLogger.w(TAG + " Cannot create notification: no current context");
return null;
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "JitsiOngoingConferenceChannel");
builder
.setCategory(NotificationCompat.CATEGORY_CALL)
.setContentTitle(context.getString(R.string.ongoing_notification_title))
.setContentText(context.getString(R.string.ongoing_notification_text))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setOngoing(true)
.setWhen(System.currentTimeMillis())
.setUsesChronometer(true)
.setAutoCancel(false)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOnlyAlertOnce(true)
.setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));
return builder.build();
}
}

View File

@@ -154,11 +154,11 @@
}
},
"node_modules/@babel/generator": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz",
"integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
"integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"dependencies": {
"@babel/types": "^7.22.15",
"@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -191,20 +191,20 @@
}
},
"node_modules/@babel/helper-environment-visitor": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz",
"integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-function-name": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
"integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"dependencies": {
"@babel/template": "^7.22.5",
"@babel/types": "^7.22.5"
"@babel/template": "^7.22.15",
"@babel/types": "^7.23.0"
},
"engines": {
"node": ">=6.9.0"
@@ -281,9 +281,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz",
"integrity": "sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==",
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
"engines": {
"node": ">=6.9.0"
}
@@ -323,9 +323,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.15.tgz",
"integrity": "sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==",
"bin": {
"parser": "bin/babel-parser.js"
},
@@ -358,18 +358,18 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.15.tgz",
"integrity": "sha512-DdHPwvJY0sEeN4xJU5uRLmZjgMMDIvMPniLuYzUVXj/GGzysPl0/fwt44JBkyUIzGJPV8QgHMcQdQ34XFuKTYQ==",
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
"dependencies": {
"@babel/code-frame": "^7.22.13",
"@babel/generator": "^7.22.15",
"@babel/helper-environment-visitor": "^7.22.5",
"@babel/helper-function-name": "^7.22.5",
"@babel/generator": "^7.23.0",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15",
"@babel/parser": "^7.23.0",
"@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -378,12 +378,12 @@
}
},
"node_modules/@babel/types": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.15.tgz",
"integrity": "sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"dependencies": {
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.15",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
},
"engines": {
@@ -3866,11 +3866,11 @@
}
},
"@babel/generator": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz",
"integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz",
"integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==",
"requires": {
"@babel/types": "^7.22.15",
"@babel/types": "^7.23.0",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"jsesc": "^2.5.1"
@@ -3896,17 +3896,17 @@
}
},
"@babel/helper-environment-visitor": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz",
"integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q=="
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz",
"integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA=="
},
"@babel/helper-function-name": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz",
"integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz",
"integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==",
"requires": {
"@babel/template": "^7.22.5",
"@babel/types": "^7.22.5"
"@babel/template": "^7.22.15",
"@babel/types": "^7.23.0"
}
},
"@babel/helper-hoist-variables": {
@@ -3959,9 +3959,9 @@
"integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw=="
},
"@babel/helper-validator-identifier": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz",
"integrity": "sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ=="
"version": "7.22.20",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A=="
},
"@babel/helper-validator-option": {
"version": "7.22.15",
@@ -3989,9 +3989,9 @@
}
},
"@babel/parser": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.15.tgz",
"integrity": "sha512-RWmQ/sklUN9BvGGpCDgSubhHWfAx24XDTDObup4ffvxaYsptOg2P3KG0j+1eWKLxpkX0j0uHxmpq2Z1SP/VhxA=="
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz",
"integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw=="
},
"@babel/runtime": {
"version": "7.22.5",
@@ -4012,29 +4012,29 @@
}
},
"@babel/traverse": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.15.tgz",
"integrity": "sha512-DdHPwvJY0sEeN4xJU5uRLmZjgMMDIvMPniLuYzUVXj/GGzysPl0/fwt44JBkyUIzGJPV8QgHMcQdQ34XFuKTYQ==",
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
"integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
"requires": {
"@babel/code-frame": "^7.22.13",
"@babel/generator": "^7.22.15",
"@babel/helper-environment-visitor": "^7.22.5",
"@babel/helper-function-name": "^7.22.5",
"@babel/generator": "^7.23.0",
"@babel/helper-environment-visitor": "^7.22.20",
"@babel/helper-function-name": "^7.23.0",
"@babel/helper-hoist-variables": "^7.22.5",
"@babel/helper-split-export-declaration": "^7.22.6",
"@babel/parser": "^7.22.15",
"@babel/types": "^7.22.15",
"@babel/parser": "^7.23.0",
"@babel/types": "^7.23.0",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.22.15",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.15.tgz",
"integrity": "sha512-X+NLXr0N8XXmN5ZsaQdm9U2SSC3UbIYq/doL++sueHOTisgZHoKaQtZxGuV2cUPQHMfjKEfg/g6oy7Hm6SKFtA==",
"version": "7.23.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz",
"integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==",
"requires": {
"@babel/helper-string-parser": "^7.22.5",
"@babel/helper-validator-identifier": "^7.22.15",
"@babel/helper-validator-identifier": "^7.22.20",
"to-fast-properties": "^2.0.0"
}
},

View File

@@ -97,4 +97,4 @@
"keywords": [
"react-native"
]
}
}

View File

@@ -6,7 +6,9 @@ const packageJSON = require('../package.json');
const SDKPackageJSON = require('./package.json');
const androidSourcePath = '../android/sdk/src/main/java/org/jitsi/meet/sdk';
const androidMainSourcePath = '../android/sdk/src/main/res';
const androidTargetPath = './android/src/main/java/org/jitsi/meet/sdk';
const androidMainTargetPath = './android/src/main/res';
const iosSrcPath = '../ios/sdk/src';
const iosDestPath = './ios/src';
@@ -169,6 +171,30 @@ copyFolderRecursiveSync(
`${androidSourcePath}/log`,
`${androidTargetPath}/log`
);
copyFolderRecursiveSync(
`${androidMainSourcePath}/values`,
`${androidMainTargetPath}`
);
copyFolderRecursiveSync(
`${androidMainSourcePath}/drawable-hdpi`,
`${androidMainTargetPath}`
);
copyFolderRecursiveSync(
`${androidMainSourcePath}/drawable-mdpi`,
`${androidMainTargetPath}`
);
copyFolderRecursiveSync(
`${androidMainSourcePath}/drawable-xhdpi`,
`${androidMainTargetPath}`
);
copyFolderRecursiveSync(
`${androidMainSourcePath}/drawable-xxhdpi`,
`${androidMainTargetPath}`
);
copyFolderRecursiveSync(
`${androidMainSourcePath}/drawable-xxxhdpi`,
`${androidMainTargetPath}`
);
copyFolderRecursiveSync(
`${androidSourcePath}/net`,
`${androidTargetPath}/log`

View File

@@ -4,6 +4,7 @@ import React, { Component } from 'react';
// in this environment (e.g. JitsiMeetJS or interfaceConfig)
import StatelessAvatar from '../base/avatar/components/web/StatelessAvatar';
import { getAvatarColor, getInitials } from '../base/avatar/functions';
import { DEFAULT_ICON } from '../base/icons/svg/constants';
import Toolbar from './Toolbar';
@@ -184,6 +185,7 @@ export default class AlwaysOnTop extends Component<any, IState> {
<div id = 'avatarContainer'>
<StatelessAvatar
color = { getAvatarColor(displayName, customAvatarBackgrounds) }
iconUser = { DEFAULT_ICON.IconUser }
id = 'avatar'
initials = { getInitials(displayName) }
url = { avatarURL } />)

View File

@@ -2,7 +2,8 @@ import React, { Component } from 'react';
// We need to reference these files directly to avoid loading things that are not available
// in this environment (e.g. JitsiMeetJS or interfaceConfig)
import { IconMic, IconMicSlash } from '../base/icons/svg';
import { DEFAULT_ICON } from '../base/icons/svg/constants';
import { IProps } from '../base/toolbox/components/AbstractButton';
import ToolbarButton from './ToolbarButton';
@@ -31,8 +32,8 @@ type Props = Partial<IProps>;
* Stateless "mute/unmute audio" button for the Always-on-Top windows.
*/
export default class AudioMuteButton extends Component<Props, IState> {
icon = IconMic;
toggledIcon = IconMicSlash;
icon = DEFAULT_ICON.IconMic;
toggledIcon = DEFAULT_ICON.IconMicSlash;
accessibilityLabel = 'Audio mute';
/**

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
// We need to reference these files directly to avoid loading things that are not available
// in this environment (e.g. JitsiMeetJS or interfaceConfig)
import { IconHangup } from '../base/icons/svg';
import { DEFAULT_ICON } from '../base/icons/svg/constants';
import { IProps } from '../base/toolbox/components/AbstractButton';
import ToolbarButton from './ToolbarButton';
@@ -17,7 +17,7 @@ type Props = Partial<IProps>;
export default class HangupButton extends Component<Props> {
accessibilityLabel = 'Hangup';
icon = IconHangup;
icon = DEFAULT_ICON.IconHangup;
/**
* Initializes a new {@code HangupButton} instance.

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react';
// We need to reference these files directly to avoid loading things that are not available
// in this environment (e.g. JitsiMeetJS or interfaceConfig)
import { IconVideo, IconVideoOff } from '../base/icons/svg';
import { DEFAULT_ICON } from '../base/icons/svg/constants';
import { IProps } from '../base/toolbox/components/AbstractButton';
import ToolbarButton from './ToolbarButton';
@@ -32,8 +32,8 @@ type State = {
*/
export default class VideoMuteButton extends Component<Props, State> {
icon = IconVideo;
toggledIcon = IconVideoOff;
icon = DEFAULT_ICON.IconVideo;
toggledIcon = DEFAULT_ICON.IconVideoOff;
accessibilityLabel = 'Video mute';
/**

View File

@@ -934,3 +934,27 @@ export function createGifSentEvent() {
action: 'gif.sent'
};
}
/**
* Creates an event which indicates the whiteboard was opened.
*
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createOpenWhiteboardEvent() {
return {
action: 'whiteboard.open'
};
}
/**
* Creates an event which indicates the whiteboard limit was enforced.
*
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createRestrictWhiteboardEvent() {
return {
action: 'whiteboard.restrict'
};
}

View File

@@ -224,7 +224,7 @@ export function initAnalytics(store: IStore, handlers: Array<Object>) {
// Set the handlers last, since this triggers emptying of the cache
analytics.setAnalyticsHandlers(handlers);
if (!isMobileBrowser() && browser.isChrome()) {
if (!isMobileBrowser() && browser.isChromiumBased()) {
const bannerCfg = state['features/base/config'].chromeExtensionBanner;
checkChromeExtensionsInstalled(bannerCfg).then(extensionsInstalled => {

View File

@@ -1,6 +1,14 @@
import { getLocationContextRoot } from '../base/util/uri';
// @ts-ignore
// eslint-disable-next-line
import { openTokenAuthUrl } from '../authentication/actions';
// @ts-ignore
import { getTokenAuthUrl, isTokenAuthEnabled } from '../authentication/functions';
import { getJwtExpirationDate } from '../base/jwt/functions';
import { getLocationContextRoot, parseURIString } from '../base/util/uri';
import { addTrackStateToURL } from './functions.any';
import logger from './logger';
import { IStore } from './types';
/**
@@ -83,4 +91,53 @@ export function reloadWithStoredParams() {
};
}
/**
* Checks whether tokenAuthUrl is set, we have a jwt token that will expire soon
* and redirect to the auth url to obtain new token if this is the case.
*
* @param {Dispatch} dispatch - The Redux dispatch function.
* @param {Function} getState - The Redux state.
* @param {Function} failureCallback - The callback on failure to obtain auth url.
* @returns {boolean} Whether we will redirect or not.
*/
export function maybeRedirectToTokenAuthUrl(
dispatch: IStore['dispatch'], getState: IStore['getState'], failureCallback: Function) {
const state = getState();
const config = state['features/base/config'];
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
if (!isTokenAuthEnabled(config)) {
return false;
}
// if tokenAuthUrl check jwt if is about to expire go through the url to get new token
const jwt = state['features/base/jwt'].jwt;
const expirationDate = getJwtExpirationDate(jwt);
// if there is jwt and its expiration time is less than 3 minutes away
// let's obtain new token
if (expirationDate && expirationDate.getTime() - Date.now() < 3 * 60 * 1000) {
const room = state['features/base/conference'].room;
const { tenant } = parseURIString(locationURL.href) || {};
getTokenAuthUrl(config, room, tenant, true, locationURL)
.then((tokenAuthServiceUrl: string | undefined) => {
if (!tokenAuthServiceUrl) {
logger.warn('Cannot handle login, token service URL is not set');
return Promise.reject();
}
return dispatch(openTokenAuthUrl(tokenAuthServiceUrl));
})
.catch(() => {
failureCallback();
});
return true;
}
return false;
}

View File

@@ -31,11 +31,11 @@ import { screen } from '../mobile/navigation/routes';
import { clearNotifications } from '../notifications/actions';
import { isUnsafeRoomWarningEnabled } from '../prejoin/functions';
import { maybeRedirectToTokenAuthUrl } from './actions.any';
import { addTrackStateToURL, getDefaultURL } from './functions.native';
import logger from './logger';
import { IReloadNowOptions, IStore } from './types';
export * from './actions.any';
/**
@@ -205,10 +205,18 @@ export function reloadNow() {
// @ts-ignore
const newURL = addTrackStateToURL(locationURL, state);
logger.info(`Reloading the conference using URL: ${locationURL}`);
const reloadAction = () => {
logger.info(`Reloading the conference using URL: ${locationURL}`);
dispatch(appNavigate(toURLString(newURL), {
hidePrejoin: true
}));
dispatch(appNavigate(toURLString(newURL), {
hidePrejoin: true
}));
};
if (maybeRedirectToTokenAuthUrl(dispatch, getState, reloadAction)) {
return;
}
reloadAction();
};
}

View File

@@ -15,6 +15,7 @@ import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants';
import { isWelcomePageEnabled } from '../welcome/functions';
import {
maybeRedirectToTokenAuthUrl,
redirectToStaticPage,
redirectWithStoredParams,
reloadWithStoredParams
@@ -170,8 +171,16 @@ export function reloadNow() {
const state = getState();
const { locationURL } = state['features/base/connection'];
logger.info(`Reloading the conference using URL: ${locationURL}`);
const reloadAction = () => {
logger.info(`Reloading the conference using URL: ${locationURL}`);
dispatch(reloadWithStoredParams());
dispatch(reloadWithStoredParams());
};
if (maybeRedirectToTokenAuthUrl(dispatch, getState, reloadAction)) {
return;
}
reloadAction();
};
}

View File

@@ -7,8 +7,7 @@ import SplashScreen from 'react-native-splash-screen';
import BottomSheetContainer from '../../base/dialog/components/native/BottomSheetContainer';
import DialogContainer from '../../base/dialog/components/native/DialogContainer';
import { updateFlags } from '../../base/flags/actions';
import { CALL_INTEGRATION_ENABLED, SERVER_URL_CHANGE_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import { CALL_INTEGRATION_ENABLED } from '../../base/flags/constants';
import { clientResized, setSafeAreaInsets } from '../../base/responsive-ui/actions';
import DimensionsDetector from '../../base/responsive-ui/components/DimensionsDetector.native';
import { updateSettings } from '../../base/settings/actions';
@@ -153,18 +152,13 @@ export class App extends AbstractApp<IProps> {
await rootNavigationReady;
// Check if serverURL is configured externally and not allowed to change.
const serverURLChangeEnabled = getState && getFeatureFlag(getState(), SERVER_URL_CHANGE_ENABLED, true);
// Update specified server URL.
if (typeof this.props.url !== 'undefined') {
// @ts-ignore
const { serverURL } = this.props.url;
if (!serverURLChangeEnabled) {
// As serverURL is provided externally, so we push it to settings.
if (typeof this.props.url !== 'undefined') {
// @ts-ignore
const { serverURL } = this.props.url;
if (typeof serverURL !== 'undefined') {
dispatch?.(updateSettings({ serverURL }));
}
if (typeof serverURL !== 'undefined') {
dispatch?.(updateSettings({ serverURL }));
}
}

View File

@@ -53,10 +53,10 @@ function _getWebConferenceRoute(state: IReduxState) {
if (!browser.isElectron() && config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
&& state['features/authentication'].tokenAuthUrlSuccessful
&& !state['features/base/jwt'].jwt && room) {
const { locationURL = { href: '' } } = state['features/base/connection'];
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
const { tenant } = parseURIString(locationURL.href) || {};
return getTokenAuthUrl(config, room, tenant)
return getTokenAuthUrl(config, room, tenant, false, locationURL)
.then((url: string | undefined) => {
route.href = url;

View File

@@ -17,6 +17,11 @@ interface IProps {
*/
_alternativeCancelText?: boolean;
/**
* Is confirm button hidden?
*/
_isConfirmHidden?: boolean;
/**
* Redux store dispatch function.
*/
@@ -56,11 +61,14 @@ class WaitForOwnerDialog extends Component<IProps> {
* @returns {ReactElement}
*/
render() {
const { _isConfirmHidden } = this.props;
return (
<ConfirmDialog
cancelLabel = { this.props._alternativeCancelText ? 'dialog.WaitingForHostButton' : 'dialog.Cancel' }
confirmLabel = 'dialog.IamHost'
descriptionKey = 'dialog.WaitForHostMsg'
isConfirmHidden = { _isConfirmHidden }
onCancel = { this._onCancel }
onSubmit = { this._onLogin } />
);
@@ -97,9 +105,11 @@ class WaitForOwnerDialog extends Component<IProps> {
*/
function mapStateToProps(state: IReduxState) {
const { membersOnly, lobbyWaitingForHost } = state['features/base/conference'];
const { locationURL } = state['features/base/connection'];
return {
_alternativeCancelText: membersOnly && lobbyWaitingForHost
_alternativeCancelText: membersOnly && lobbyWaitingForHost,
_isConfirmHidden: locationURL?.hostname?.includes('8x8.vc')
};
}

View File

@@ -16,12 +16,14 @@ export const isTokenAuthEnabled = (config: IConfig): boolean =>
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name if any.
* @param {boolean} skipPrejoin - Whether to skip pre-join page.
* @param {URL} locationURL - The location URL.
* @returns {Object} The state object.
*/
export const _getTokenAuthState = (
roomName: string | undefined,
tenant: string | undefined,
skipPrejoin: boolean | undefined = false): object => {
skipPrejoin: boolean | undefined = false,
locationURL: URL): object => {
const state = {
room: roomName,
roomSafe: getBackendSafeRoomName(roomName),
@@ -34,7 +36,7 @@ export const _getTokenAuthState = (
state['config.prejoinConfig.enabled'] = false;
}
const params = new URLSearchParams(window.location.search);
const params = new URLSearchParams(locationURL.hash);
for (const [ key, value ] of params) {
// we allow only config and interfaceConfig overrides in the state

View File

@@ -17,6 +17,7 @@ export * from './functions.any';
* @param {string} roomName - The name of the conference room for which the user will be authenticated.
* @param {string} tenant - The name of the conference tenant.
* @param {string} skipPrejoin - The name of the conference room for which the user will be authenticated.
* @param {URL} locationURL - The location URL.
*
* @returns {Promise<string|undefined>} - The URL pointing to JWT login service or
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
@@ -26,7 +27,9 @@ export const getTokenAuthUrl = (
config: IConfig,
roomName: string | undefined,
tenant: string | undefined,
skipPrejoin: boolean | undefined = false): Promise<string | undefined> => {
skipPrejoin: boolean | undefined = false,
// eslint-disable-next-line max-params
locationURL: URL): Promise<string | undefined> => {
let url = config.tokenAuthUrl;
@@ -35,7 +38,7 @@ export const getTokenAuthUrl = (
}
if (url.indexOf('{state}')) {
const state = _getTokenAuthState(roomName, tenant, skipPrejoin);
const state = _getTokenAuthState(roomName, tenant, skipPrejoin, locationURL);
// Append ios=true or android=true to the token URL.
// @ts-ignore

View File

@@ -35,6 +35,7 @@ function _cryptoRandom() {
* @param {string} roomName - The name of the conference room for which the user will be authenticated.
* @param {string} tenant - The name of the conference tenant.
* @param {string} skipPrejoin - The name of the conference room for which the user will be authenticated.
* @param {URL} locationURL - The current location URL.
*
* @returns {Promise<string|undefined>} - The URL pointing to JWT login service or
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
@@ -44,7 +45,9 @@ export const getTokenAuthUrl = (
config: IConfig,
roomName: string | undefined,
tenant: string | undefined,
skipPrejoin: boolean | undefined = false): Promise<string | undefined> => {
skipPrejoin: boolean | undefined = false,
// eslint-disable-next-line max-params
locationURL: URL): Promise<string | undefined> => {
let url = config.tokenAuthUrl;
@@ -53,7 +56,7 @@ export const getTokenAuthUrl = (
}
if (url.indexOf('{state}')) {
const state = _getTokenAuthState(roomName, tenant, skipPrejoin);
const state = _getTokenAuthState(roomName, tenant, skipPrejoin, locationURL);
if (browser.isElectron()) {
// @ts-ignore

View File

@@ -255,7 +255,7 @@ function _handleLogin({ dispatch, getState }: IStore) {
const state = getState();
const config = state['features/base/config'];
const room = state['features/base/conference'].room;
const { locationURL = { href: '' } } = state['features/base/connection'];
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
const { tenant } = parseURIString(locationURL.href) || {};
if (!room) {
@@ -270,7 +270,7 @@ function _handleLogin({ dispatch, getState }: IStore) {
return;
}
getTokenAuthUrl(config, room, tenant, true)
getTokenAuthUrl(config, room, tenant, true, locationURL)
.then((tokenAuthServiceUrl: string | undefined) => {
if (!tokenAuthServiceUrl) {
logger.warn('Cannot handle login, token service URL is not set');

View File

@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { IconUser } from '../../icons/svg';
import { getParticipantById } from '../../participants/functions';
import { IParticipant } from '../../participants/types';
import { getAvatarColor, getInitials, isCORSAvatarURL } from '../functions';
@@ -182,6 +183,7 @@ class Avatar<P extends IProps> extends PureComponent<P, IState> {
const avatarProps: AbstractProps & {
className?: string;
iconUser?: any;
id?: string;
status?: string;
testId?: string;
@@ -226,6 +228,10 @@ class Avatar<P extends IProps> extends PureComponent<P, IState> {
avatarProps.initials = initials;
}
if (navigator.product !== 'ReactNative') {
avatarProps.iconUser = IconUser;
}
return (
<StatelessAvatar
{ ...avatarProps } />

View File

@@ -2,7 +2,6 @@ import React, { useCallback } from 'react';
import { makeStyles } from 'tss-react/mui';
import Icon from '../../../icons/components/Icon';
import { IconUser } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { isIcon } from '../../functions';
import { IAvatarProps } from '../../types';
@@ -122,6 +121,7 @@ const useStyles = makeStyles()(theme => {
const StatelessAvatar = ({
className,
color,
iconUser,
id,
initials,
onAvatarLoadError,
@@ -212,7 +212,7 @@ const StatelessAvatar = ({
style = { _getAvatarStyle() }>
<Icon
size = { '50%' }
src = { IconUser } />
src = { iconUser } />
</div>
);
};

View File

@@ -5,6 +5,11 @@ export interface IAvatarProps {
*/
color?: string;
/**
* The user icon(browser only).
*/
iconUser?: any;
/**
* Initials to be used to render the initials based avatars.
*/

View File

@@ -16,6 +16,7 @@ import { removeLobbyChatParticipant } from '../../chat/actions.any';
import { openDisplayNamePrompt } from '../../display-name/actions';
import { showErrorNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { hasDisplayName } from '../../prejoin/utils';
import { stopLocalVideoRecording } from '../../recording/actions.any';
import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager';
import { iAmVisitor } from '../../visitors/functions';
@@ -276,7 +277,9 @@ function _conferenceJoined({ dispatch, getState }: IStore, next: Function, actio
if (requireDisplayName
&& !getLocalParticipant(getState)?.name
&& !conference.isHidden()) {
dispatch(openDisplayNamePrompt(undefined));
dispatch(openDisplayNamePrompt({
validateInput: hasDisplayName
}));
}
return result;

View File

@@ -157,6 +157,13 @@ export interface INoiseSuppressionConfig {
};
}
export interface IWhiteboardConfig {
collabServerBaseUrl?: string;
enabled?: boolean;
limitUrl?: string;
userLimit?: number;
}
export interface IWatchRTCConfiguration {
allowBrowserLogCollection?: boolean;
collectionInterval?: number;
@@ -246,6 +253,7 @@ export interface IConfig {
callStatsID?: string;
callStatsSecret?: string;
callUUID?: string;
cameraFacingMode?: string;
channelLastN?: number;
chromeExtensionBanner?: {
chromeExtensionsInfo?: Array<{ id: string; path: string; }>;
@@ -513,6 +521,7 @@ export interface IConfig {
pcStatsInterval?: number;
peopleSearchQueryTypes?: string[];
peopleSearchUrl?: string;
preferBosh?: boolean;
preferredTranscribeLanguage?: string;
prejoinConfig?: {
enabled?: boolean;
@@ -630,8 +639,5 @@ export interface IConfig {
customUrl?: string;
disabled?: boolean;
};
whiteboard?: {
collabServerBaseUrl?: string;
enabled?: boolean;
};
whiteboard?: IWhiteboardConfig;
}

View File

@@ -74,6 +74,7 @@ export default [
*/
'callUUID',
'cameraFacingMode',
'conferenceInfo',
'channelLastN',
'connectionIndicators',
@@ -158,7 +159,6 @@ export default [
'filmstrip',
'firefox_fake_device',
'flags',
'forceJVB121Ratio',
'forceTurnRelay',
'gatherStats',
'giphy',
@@ -197,6 +197,7 @@ export default [
'participantMenuButtonsWithNotifyClick',
'participantsPane',
'pcStatsInterval',
'preferBosh',
'prejoinConfig',
'prejoinPageEnabled',
'recordingService',

View File

@@ -27,6 +27,7 @@ import { ConnectionFailedError, IIceServers } from './types';
*/
interface IOptions extends IConfigState {
iceServersOverride?: IIceServers;
preferVisitor?: boolean;
}
/**
@@ -112,7 +113,7 @@ export function constructOptions(state: IReduxState) {
// redux store.
const options: IOptions = _.cloneDeep(state['features/base/config']);
const { locationURL } = state['features/base/connection'];
const { locationURL, preferVisitor } = state['features/base/connection'];
const params = parseURLParams(locationURL || '');
const iceServersOverride = params['iceServers.replace'];
@@ -120,7 +121,7 @@ export function constructOptions(state: IReduxState) {
options.iceServersOverride = iceServersOverride;
}
const { bosh } = options;
const { bosh, preferBosh } = options;
let { websocket } = options;
// TESTING: Only enable WebSocket for some percentage of users.
@@ -130,6 +131,10 @@ export function constructOptions(state: IReduxState) {
}
}
if (preferBosh) {
websocket = undefined;
}
// WebSocket is preferred over BOSH.
const serviceUrl = websocket || bosh;
@@ -151,6 +156,10 @@ export function constructOptions(state: IReduxState) {
}
}
if (preferVisitor) {
options.preferVisitor = true;
}
return options;
}

View File

@@ -1,4 +1,5 @@
import { SET_ROOM } from '../conference/actionTypes';
import { SET_JWT } from '../jwt/actionTypes';
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
import ReducerRegistry from '../redux/ReducerRegistry';
import { assign, set } from '../redux/functions';
@@ -26,6 +27,7 @@ export interface IConnectionState {
error?: ConnectionFailedError;
locationURL?: URL;
passwordRequired?: Object;
preferVisitor?: boolean;
showConnectionInfo?: boolean;
timeEstablished?: number;
}
@@ -49,6 +51,9 @@ ReducerRegistry.register<IConnectionState>(
case CONNECTION_WILL_CONNECT:
return _connectionWillConnect(state, action);
case SET_JWT:
return _setJWT(state, action);
case SET_LOCATION_URL:
return _setLocationURL(state, action);
@@ -84,6 +89,7 @@ function _connectionDisconnected(
return assign(state, {
connecting: undefined,
connection: undefined,
preferVisitor: undefined,
timeEstablished: undefined
});
}
@@ -141,7 +147,8 @@ function _connectionFailed(
error,
passwordRequired:
error.name === JitsiConnectionErrors.PASSWORD_REQUIRED
? connection : undefined
? connection : undefined,
preferVisitor: undefined
});
}
@@ -184,6 +191,22 @@ function _getCurrentConnection(baseConnectionState: IConnectionState): IConnecti
return baseConnectionState.connection || baseConnectionState.connecting;
}
/**
* Reduces a specific redux action {@link SET_JWT} of the feature
* base/connection.
*
* @param {IConnectionState} state - The redux state of the feature base/connection.
* @param {Action} action - The Redux action SET_JWT to reduce.
* @private
* @returns {Object} The new state of the feature base/connection after the
* reduction of the specified action.
*/
function _setJWT(state: IConnectionState, { preferVisitor }: { preferVisitor: boolean; }) {
return assign(state, {
preferVisitor
});
}
/**
* Reduces a specific redux action {@link SET_LOCATION_URL} of the feature
* base/connection.

View File

@@ -16,9 +16,13 @@ import {
} from './actionTypes';
import {
areDeviceLabelsInitialized,
areDevicesDifferent,
filterIgnoredDevices,
flattenAvailableDevices,
getDeviceIdByLabel,
getDeviceLabelById,
getDevicesFromURL,
logDevices,
setAudioOutputDeviceId
} from './functions';
import logger from './logger';
@@ -137,15 +141,21 @@ export function configureInitialDevices() {
* @returns {Function}
*/
export function getAvailableDevices() {
return (dispatch: IStore['dispatch']) => new Promise(resolve => {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => new Promise(resolve => {
const { mediaDevices } = JitsiMeetJS;
if (mediaDevices.isDeviceListAvailable()
&& mediaDevices.isDeviceChangeAvailable()) {
mediaDevices.enumerateDevices((devices: MediaDeviceInfo[]) => {
dispatch(updateDeviceList(devices));
const { filteredDevices, ignoredDevices } = filterIgnoredDevices(devices);
const oldDevices = flattenAvailableDevices(getState()['features/base/devices'].availableDevices);
resolve(devices);
if (areDevicesDifferent(oldDevices, filteredDevices)) {
logDevices(ignoredDevices, 'Ignored devices on device list changed:');
dispatch(updateDeviceList(filteredDevices));
}
resolve(filteredDevices);
});
} else {
resolve([]);

View File

@@ -0,0 +1,8 @@
/**
* Prefixes of devices that will be filtered from the device list.
*
* NOTE: Currently we filter only 'Microsoft Teams Audio Device' virtual device. It seems that it can't be set
* as default device on the OS level and this use case is not handled in the code. If we add more device prefixes that
* can be default devices we should make sure to handle the default device use case.
*/
export const DEVICE_LABEL_PREFIXES_TO_IGNORE = [ 'Microsoft Teams Audio Device' ];

View File

@@ -5,6 +5,7 @@ import { ISettingsState } from '../settings/reducer';
import { setNewAudioOutputDevice } from '../sounds/functions.web';
import { parseURLParams } from '../util/parseURLParams';
import { DEVICE_LABEL_PREFIXES_TO_IGNORE } from './constants';
import logger from './logger';
import { IDevicesState } from './types';
@@ -176,6 +177,74 @@ export function filterAudioDevices(devices: MediaDeviceInfo[]) {
return devices.filter(device => device.kind === 'audioinput');
}
/**
* Filters the devices that start with one of the prefixes from DEVICE_LABEL_PREFIXES_TO_IGNORE.
*
* @param {MediaDeviceInfo[]} devices - The devices to be filtered.
* @returns {MediaDeviceInfo[]} - The filtered devices.
*/
export function filterIgnoredDevices(devices: MediaDeviceInfo[] = []) {
const ignoredDevices: MediaDeviceInfo[] = [];
const filteredDevices = devices.filter(device => {
if (!device.label) {
return true;
}
if (DEVICE_LABEL_PREFIXES_TO_IGNORE.find(prefix => device.label?.startsWith(prefix))) {
ignoredDevices.push(device);
return false;
}
return true;
});
return {
filteredDevices,
ignoredDevices
};
}
/**
* Check if the passed device arrays are different.
*
* @param {MediaDeviceInfo[]} devices1 - Array with devices to be compared.
* @param {MediaDeviceInfo[]} devices2 - Array with devices to be compared.
* @returns {boolean} - True if the device arrays are different and false otherwise.
*/
export function areDevicesDifferent(devices1: MediaDeviceInfo[] = [], devices2: MediaDeviceInfo[] = []) {
if (devices1.length !== devices2.length) {
return true;
}
for (let i = 0; i < devices1.length; i++) {
const device1 = devices1[i];
const found = devices2.find(({ deviceId, groupId, kind, label }) =>
device1.deviceId === deviceId
&& device1.groupId === groupId
&& device1.kind === kind
&& device1.label === label
);
if (!found) {
return true;
}
}
return false;
}
/**
* Flattens the availableDevices from redux.
*
* @param {IDevicesState.availableDevices} devices - The available devices from redux.
* @returns {MediaDeviceInfo[]} - The flattened array of devices.
*/
export function flattenAvailableDevices(
{ audioInput = [], audioOutput = [], videoInput = [] }: IDevicesState['availableDevices']) {
return audioInput.concat(audioOutput).concat(videoInput);
}
/**
* We want to strip any device details that are not very user friendly, like usb ids put in brackets at the end.
*
@@ -240,6 +309,35 @@ export function getVideoDeviceIds(state: IReduxState) {
return state['features/base/devices'].availableDevices.videoInput?.map(({ deviceId }) => deviceId);
}
/**
* Converts an array of device info objects into string.
*
* @param {MediaDeviceInfo[]} devices - The devices.
* @returns {string}
*/
function devicesToStr(devices?: MediaDeviceInfo[]) {
return devices?.map(device => `\t\t${device.label}[${device.deviceId}]`).join('\n');
}
/**
* Logs an array of devices.
*
* @param {MediaDeviceInfo[]} devices - The array of devices.
* @param {string} title - The title that will be printed in the log.
* @returns {void}
*/
export function logDevices(devices: MediaDeviceInfo[], title = '') {
const deviceList = groupDevicesByKind(devices);
const audioInputs = devicesToStr(deviceList.audioInput);
const audioOutputs = devicesToStr(deviceList.audioOutput);
const videoInputs = devicesToStr(deviceList.videoInput);
logger.debug(`${title}:\n`
+ `audioInput:\n${audioInputs}\n`
+ `audioOutput:\n${audioOutputs}\n`
+ `videoInput:\n${videoInputs}`);
}
/**
* Set device id of the audio output device which is currently in use.
* Empty string stands for default device.

View File

@@ -33,11 +33,10 @@ import {
import {
areDeviceLabelsInitialized,
formatDeviceLabel,
groupDevicesByKind,
logDevices,
setAudioOutputDeviceId
} from './functions';
import logger from './logger';
import { IDevicesState } from './types';
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
microphone: {
@@ -62,25 +61,6 @@ const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
*/
let permissionsListener: Function | undefined;
/**
* Logs the current device list.
*
* @param {Object} deviceList - Whatever is returned by {@link groupDevicesByKind}.
* @returns {string}
*/
function logDeviceList(deviceList: IDevicesState['availableDevices']) {
const devicesToStr = (list?: MediaDeviceInfo[]) =>
list?.map(device => `\t\t${device.label}[${device.deviceId}]`).join('\n');
const audioInputs = devicesToStr(deviceList.audioInput);
const audioOutputs = devicesToStr(deviceList.audioOutput);
const videoInputs = devicesToStr(deviceList.videoInput);
logger.debug('Device list updated:\n'
+ `audioInput:\n${audioInputs}\n`
+ `audioOutput:\n${audioOutputs}\n`
+ `videoInput:\n${videoInputs}`);
}
/**
* Implements the middleware of the feature base/devices.
*
@@ -199,7 +179,7 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case UPDATE_DEVICE_LIST:
logDeviceList(groupDevicesByKind(action.devices));
logDevices(action.devices, 'Device list updated');
if (areDeviceLabelsInitialized(store.getState())) {
return _processPendingRequests(store, next, action);
}

View File

@@ -41,6 +41,11 @@ interface IProps extends AbstractProps, WithTranslation {
*/
isConfirmDestructive?: Boolean;
/**
* Whether or not the confirm button is hidden.
*/
isConfirmHidden?: Boolean;
/**
* Dialog title.
*/
@@ -60,7 +65,8 @@ class ConfirmDialog extends AbstractDialog<IProps> {
* @static
*/
static defaultProps = {
isConfirmDestructive: false
isConfirmDestructive: false,
isConfirmHidden: false
};
/**
@@ -95,6 +101,7 @@ class ConfirmDialog extends AbstractDialog<IProps> {
children,
confirmLabel,
isConfirmDestructive,
isConfirmHidden,
t,
title
} = this.props;
@@ -118,10 +125,12 @@ class ConfirmDialog extends AbstractDialog<IProps> {
label = { t(cancelLabel || 'dialog.confirmNo') }
onPress = { this._onCancel }
style = { styles.dialogButton } />
<Dialog.Button
label = { t(confirmLabel || 'dialog.confirmYes') }
onPress = { this._onSubmit }
style = { dialogButtonStyle } />
{
!isConfirmHidden && <Dialog.Button
label = { t(confirmLabel || 'dialog.confirmYes') }
onPress = { this._onSubmit }
style = { dialogButtonStyle } />
}
</Dialog.Container>
);
}

View File

@@ -20,6 +20,11 @@ interface IProps extends AbstractProps, WithTranslation {
*/
descriptionKey?: string;
/**
* Whether to display the cancel button.
*/
disableCancel?: boolean;
/**
* An optional initial value to initiate the field with.
*/
@@ -52,6 +57,11 @@ interface IState extends AbstractState {
* The current value of the field.
*/
fieldValue?: string;
/**
* The result of the input validation.
*/
isValid: boolean;
}
/**
@@ -68,6 +78,7 @@ class InputDialog extends AbstractDialog<IProps, IState> {
this.state = {
fieldValue: props.initialValue,
isValid: props.validateInput ? props.validateInput(props.initialValue) : true,
submitting: false
};
@@ -115,10 +126,11 @@ class InputDialog extends AbstractDialog<IProps, IState> {
</Dialog.Description>
)
}
<Dialog.Button
{!this.props.disableCancel && <Dialog.Button
label = { t('dialog.Cancel') }
onPress = { this._onCancel } />
onPress = { this._onCancel } />}
<Dialog.Button
disabled = { !this.state.isValid }
label = { t('dialog.Ok') }
onPress = { this._onSubmitValue } />
</Dialog.Container>
@@ -132,10 +144,14 @@ class InputDialog extends AbstractDialog<IProps, IState> {
* @returns {void}
*/
_onChangeText(fieldValue: string) {
if (this.props.validateInput && !this.props.validateInput(fieldValue)) {
if (this.props.validateInput) {
this.setState({
isValid: this.props.validateInput(fieldValue),
fieldValue
});
return;
}
this.setState({
fieldValue
});

View File

@@ -7,10 +7,11 @@ const { browser } = JitsiMeetJS.util;
const DEFAULT_OPTIMAL_BROWSERS = [
'chrome',
'chromium',
'electron',
'firefox',
'nwjs',
'safari'
'safari',
'webkit'
];
const DEFAULT_UNSUPPORTED_BROWSERS: string[] = [];
@@ -20,9 +21,8 @@ const browserNameToCheck = {
chromium: browser.isChromiumBased.bind(browser),
electron: browser.isElectron.bind(browser),
firefox: browser.isFirefox.bind(browser),
nwjs: browser.isNWJS.bind(browser),
opera: browser.isOpera.bind(browser),
safari: browser.isSafari.bind(browser)
safari: browser.isSafari.bind(browser),
webkit: browser.isWebKitBased.bind(browser)
};
/**

View File

@@ -23,6 +23,12 @@ export const AUDIO_MUTE_BUTTON_ENABLED = 'audio-mute.enabled';
*/
export const AUDIO_ONLY_BUTTON_ENABLED = 'audio-only.enabled';
/**
* Flag indicating that the Breakout Rooms button in the overflow menu is enabled.
* Default: enabled (true).
*/
export const BREAKOUT_ROOMS_BUTTON_ENABLED = 'breakout-rooms.enabled';
/**
* Flag indicating if calendar integration should be enabled.
* Default: enabled (true) on Android, auto-detected on iOS.

View File

@@ -85,7 +85,6 @@ import { default as IconSip } from './sip.svg';
import { default as IconSites } from './sites.svg';
import { default as IconRemoteControlStart } from './start-remote-control.svg';
import { default as IconRemoteControlStop } from './stop-remote-control.svg';
import { default as IconStopScreenshare } from './stop-screenshare.svg';
import { default as IconStop } from './stop.svg';
import { default as IconSubtitles } from './subtitles.svg';
import { default as IconTileView } from './tile-view.svg';
@@ -202,7 +201,6 @@ export const DEFAULT_ICON: Record<string, any> = {
IconSip,
IconSites,
IconStop,
IconStopScreenshare,
IconSubtitles,
IconTileView,
IconTrash,

View File

@@ -93,7 +93,6 @@ const {
IconSip,
IconSites,
IconStop,
IconStopScreenshare,
IconSubtitles,
IconTileView,
IconTrash,
@@ -213,7 +212,6 @@ export {
IconSip,
IconSites,
IconStop,
IconStopScreenshare,
IconSubtitles,
IconTileView,
IconTrash,

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.2803 2.78033C22.5732 2.48744 22.5732 2.01256 22.2803 1.71967C21.9874 1.42678 21.5126 1.42678 21.2197 1.71967L19.9114 3.02797C19.7769 3.00953 19.6396 3 19.5 3H4.5C2.84315 3 1.5 4.34315 1.5 6V17.25C1.5 18.3842 2.12941 19.3714 3.05799 19.8813L1.71967 21.2197C1.42678 21.5126 1.42678 21.9874 1.71967 22.2803C2.01256 22.5732 2.48744 22.5732 2.78033 22.2803L22.2803 2.78033ZM4.21616 18.7232L5.54187 17.3975C5.40631 17.4169 5.2687 17.4055 5.14194 17.3656C4.78878 17.2544 4.48676 16.9074 4.52225 16.4471C4.8467 12.239 8.62852 8.92199 13.327 8.60046V6.87396C13.327 6.08352 14.2729 5.67701 14.8464 6.22103L15.807 7.13235L18.4393 4.5H4.5C3.67157 4.5 3 5.17157 3 6V17.25C3 17.9814 3.52345 18.5905 4.21616 18.7232ZM12.7759 10.1634C10.0327 10.5417 7.82609 12.097 6.73783 14.1317C8.14978 12.9079 9.68868 12.2376 11.0531 11.8862L12.7759 10.1634Z" fill="white" />
<path d="M21 6.1607L22.283 4.87768C22.423 5.22437 22.5 5.60319 22.5 6V17.25C22.5 18.9069 21.1569 20.25 19.5 20.25H18.75C19.1642 20.25 19.5 20.5858 19.5 21C19.5 21.4142 19.1642 21.75 18.75 21.75H5.4107L8.4107 18.75H19.5C20.3284 18.75 21 18.0784 21 17.25V6.1607Z" fill="white" />
<path d="M13.327 13.8337V15.2002C13.327 15.9986 14.2893 16.4017 14.8583 15.8416L19.1656 11.6016C19.5284 11.2444 19.523 10.6577 19.1536 10.3073L17.9732 9.18747L16.9122 10.2485L17.6708 10.9682L14.827 13.7676V12.3337L13.327 13.8337Z" fill="white" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -186,3 +186,23 @@ export function validateJwt(jwt: string) {
return errors;
}
/**
* Extracts and returns the expiration date of jwt.
*
* @param {string|undefined} jwt - The jwt to check.
* @returns {Date} The expiration date of the jwt.
*/
export function getJwtExpirationDate(jwt: string | undefined) {
if (!jwt) {
return;
}
const payload = jwtDecode(jwt);
if (payload) {
const { exp } = payload;
return new Date(exp * 1000);
}
}

View File

@@ -153,6 +153,11 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
_overwriteLocalParticipant(
store, { ...newUser,
features: context.features });
// eslint-disable-next-line max-depth
if (context.user && context.user.role === 'visitor') {
action.preferVisitor = true;
}
} else if (jwtPayload.name || jwtPayload.picture || jwtPayload.email) {
// there are some tokens (firebase) having picture and name on the main level.
_overwriteLocalParticipant(store, {

View File

@@ -23,7 +23,12 @@ import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { getPropertyValue } from '../settings/functions.any';
import { TRACK_ADDED } from '../tracks/actionTypes';
import { destroyLocalTracks } from '../tracks/actions.any';
import { isLocalTrackMuted, isLocalVideoTrackDesktop, setTrackMuted } from '../tracks/functions.any';
import {
getCameraFacingMode,
isLocalTrackMuted,
isLocalVideoTrackDesktop,
setTrackMuted
} from '../tracks/functions.any';
import { ITrack } from '../tracks/types';
import {
@@ -40,7 +45,6 @@ import {
setVideoMuted
} from './actions';
import {
CAMERA_FACING_MODE,
MEDIA_TYPE,
SCREENSHARE_MUTISM_AUTHORITY,
VIDEO_MUTISM_AUTHORITY
@@ -233,7 +237,7 @@ function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAct
// the user i.e. the state of base/media. Eventually, practice/reality i.e.
// the state of base/tracks will or will not agree with the desires.
dispatch(setAudioMuted(audioMuted));
dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
dispatch(setCameraFacingMode(getCameraFacingMode(state)));
dispatch(setVideoMuted(videoMuted));
}

View File

@@ -203,6 +203,14 @@ export const SET_LOADABLE_AVATAR_URL = 'SET_LOADABLE_AVATAR_URL';
*/
export const LOCAL_PARTICIPANT_RAISE_HAND = 'LOCAL_PARTICIPANT_RAISE_HAND';
/**
* Clear the raise hand queue.
* {
* type: RAISE_HAND_CLEAR
* }
*/
export const RAISE_HAND_CLEAR = 'RAISE_HAND_CLEAR';
/**
* Updates participant in raise hand queue.
* {

View File

@@ -22,6 +22,7 @@ import {
PARTICIPANT_SOURCES_UPDATED,
PARTICIPANT_UPDATED,
PIN_PARTICIPANT,
RAISE_HAND_CLEAR,
RAISE_HAND_UPDATED,
SCREENSHARE_PARTICIPANT_NAME_CHANGED,
SET_LOADABLE_AVATAR_URL,
@@ -629,6 +630,19 @@ export function raiseHand(enabled: boolean) {
};
}
/**
* Clear the raise hand queue.
*
* @returns {{
* type: RAISE_HAND_CLEAR
* }}
*/
export function raiseHandClear() {
return {
type: RAISE_HAND_CLEAR
};
}
/**
* Update raise hand queue of participants.
*

View File

@@ -13,6 +13,7 @@ import {
PARTICIPANT_SOURCES_UPDATED,
PARTICIPANT_UPDATED,
PIN_PARTICIPANT,
RAISE_HAND_CLEAR,
RAISE_HAND_UPDATED,
SCREENSHARE_PARTICIPANT_NAME_CHANGED,
SET_LOADABLE_AVATAR_URL
@@ -465,6 +466,12 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
return { ...state };
}
case RAISE_HAND_CLEAR: {
return {
...state,
raisedHandsQueue: []
};
}
case RAISE_HAND_UPDATED: {
return {
...state,

View File

@@ -210,6 +210,5 @@ function _getUserSelectedDeviceId(options: {
return strippedDeviceLabel === strippedCandidateLabel;
});
return foundMatchBasedOnLabel
? foundMatchBasedOnLabel.deviceId : userSelectedDeviceId;
return foundMatchBasedOnLabel?.deviceId;
}

View File

@@ -35,6 +35,7 @@ import {
} from './actionTypes';
import {
createLocalTracksF,
getCameraFacingMode,
getLocalTrack,
getLocalTracks,
getLocalVideoTrack,
@@ -140,6 +141,7 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
getState
};
const promises = [];
const state = getState();
// The following executes on React Native only at the time of this
// writing. The effort to port Web's createInitialLocalTracks
@@ -153,7 +155,7 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
// device separately.
for (const device of devices) {
if (getLocalTrack(
getState()['features/base/tracks'],
state['features/base/tracks'],
device as MediaType,
/* includePending */ true)) {
throw new Error(`Local track for ${device} already exists`);
@@ -165,7 +167,7 @@ export function createLocalTracksA(options: ITrackOptions = {}) {
cameraDeviceId: options.cameraDeviceId,
devices: [ device ],
facingMode:
options.facingMode || CAMERA_FACING_MODE.USER,
options.facingMode || getCameraFacingMode(state),
micDeviceId: options.micDeviceId
},
store)

View File

@@ -4,7 +4,7 @@ import {
} from '../config/functions.any';
import { JitsiTrackErrors, browser } from '../lib-jitsi-meet';
import { gumPending } from '../media/actions';
import { MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants';
import { CAMERA_FACING_MODE, MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants';
import { IMediaState } from '../media/reducer';
import { IGUMPendingState } from '../media/types';
import {
@@ -224,6 +224,17 @@ export function getTrackByMediaTypeAndParticipant(
);
}
/**
* Returns track for specified participant id.
*
* @param {ITrack[]} tracks - List of all tracks.
* @param {string} participantId - Participant ID.
* @returns {(Track[]|undefined)}
*/
export function getTrackByParticipantId(tracks: ITrack[], participantId: string) {
return tracks.filter(t => t.participantId === participantId);
}
/**
* Returns screenshare track of given virtualScreenshareParticipantId.
*
@@ -333,8 +344,7 @@ export function isLocalVideoTrackDesktop(state: IReduxState) {
* @returns {boolean}
*/
export function isRemoteTrackMuted(tracks: ITrack[], mediaType: MediaType, participantId: string) {
const track = getTrackByMediaTypeAndParticipant(
tracks, mediaType, participantId);
const track = getTrackByMediaTypeAndParticipant(tracks, mediaType, participantId);
return !track || track.muted;
}
@@ -416,3 +426,34 @@ export function setTrackMuted(track: any, muted: boolean, state: IReduxState | I
}
});
}
/**
* Logs the current track state for a participant.
*
* @param {ITrack[]} tracksState - The tracks from redux.
* @param {string} participantId - The ID of the participant.
* @param {string} reason - The reason for the track change.
* @returns {void}
*/
export function logTracksForParticipant(tracksState: ITrack[], participantId: string, reason?: string) {
if (!participantId) {
return;
}
const tracks = getTrackByParticipantId(tracksState, participantId);
const logStringPrefix = `Track state for participant ${participantId} changed`;
const trackStateStrings = tracks.map(t => `{type: ${t.mediaType}, videoType: ${t.videoType}, muted: ${
t.muted}, isReceivingData: ${t.isReceivingData}, jitsiTrack: ${t.jitsiTrack?.toString()}}`);
const tracksLogMsg = trackStateStrings.length > 0 ? `\n${trackStateStrings.join('\n')}` : ' No tracks available!';
logger.debug(`${logStringPrefix}${reason ? `(reason: ${reason})` : ''}:${tracksLogMsg}`);
}
/**
* Gets the default camera facing mode.
*
* @param {Object} state - The redux state.
* @returns {string} - The camera facing mode.
*/
export function getCameraFacingMode(state: IReduxState) {
return state['features/base/config'].cameraFacingMode ?? CAMERA_FACING_MODE.USER;
}

View File

@@ -1,6 +1,7 @@
import { IStore } from '../../app/types';
import JitsiMeetJS from '../lib-jitsi-meet';
import { getCameraFacingMode } from './functions.any';
import { ITrackOptions } from './types';
export * from './functions.any';
@@ -39,6 +40,7 @@ export function createLocalTracksF(options: ITrackOptions = {}, store: IStore) {
// Copy array to avoid mutations inside library.
devices: options.devices?.slice(0),
facingMode: options.facingMode || getCameraFacingMode(state),
micDeviceId,
resolution
});

View File

@@ -12,6 +12,7 @@ import {
getUserSelectedMicDeviceId
} from '../settings/functions.web';
import { getCameraFacingMode } from './functions.any';
import loadEffects from './loadEffects';
import logger from './logger';
import { ITrackOptions } from './types';
@@ -82,6 +83,7 @@ export function createLocalTracksF(options: ITrackOptions = {}, store?: IStore)
// Copy array to avoid mutations inside library.
devices: options.devices?.slice(0),
effects,
facingMode: options.facingMode || getCameraFacingMode(state),
firefox_fake_device, // eslint-disable-line camelcase
firePermissionPromptIsShownEvent,
micDeviceId,

View File

@@ -15,6 +15,7 @@ import {
TRACK_ADDED,
TRACK_MUTE_UNMUTE_FAILED,
TRACK_NO_DATA_FROM_SOURCE,
TRACK_OWNER_CHANGED,
TRACK_REMOVED,
TRACK_STOPPED,
TRACK_UPDATED
@@ -25,7 +26,7 @@ import {
trackNoDataFromSourceNotificationInfoChanged
} from './actions.web';
import {
getTrackByJitsiTrack
getTrackByJitsiTrack, logTracksForParticipant
} from './functions.web';
import { ITrack } from './types';
@@ -48,9 +49,17 @@ MiddlewareRegistry.register(store => next => action => {
// were granted and a local video track is added by umuting the video.
if (local) {
store.dispatch(getAvailableDevices());
break;
}
break;
const result = next(action);
const participantId = action.track?.participantId;
if (participantId) {
logTracksForParticipant(store.getState()['features/base/tracks'], participantId, 'Track added');
}
return result;
}
case TRACK_NO_DATA_FROM_SOURCE: {
const result = next(action);
@@ -62,7 +71,32 @@ MiddlewareRegistry.register(store => next => action => {
case TRACK_REMOVED: {
_removeNoDataFromSourceNotification(store, action.track);
break;
const result = next(action);
const participantId = action.track?.jitsiTrack?.getParticipantId();
if (participantId && !action.track?.jitsiTrack?.isLocal()) {
logTracksForParticipant(store.getState()['features/base/tracks'], participantId, 'Track removed');
}
return result;
}
case TRACK_OWNER_CHANGED: {
const oldTrack = getTrackByJitsiTrack(store.getState()['features/base/tracks'], action.track?.jitsiTrack);
const oldOwner = oldTrack?.participantId;
const result = next(action);
const newOwner = action.track?.participantId;
if (oldOwner) {
logTracksForParticipant(store.getState()['features/base/tracks'], oldOwner, 'Owner changed');
}
if (newOwner) {
logTracksForParticipant(store.getState()['features/base/tracks'], newOwner, 'Owner changed');
}
return result;
}
case TRACK_MUTE_UNMUTE_FAILED: {
@@ -75,7 +109,7 @@ MiddlewareRegistry.register(store => next => action => {
} else if (isVideoTrack) {
APP.conference.setVideoMuteStatus();
} else {
APP.conference.setAudioMuteStatus(!muted);
APP.conference.updateAudioIconEnabled();
}
break;
@@ -102,20 +136,22 @@ MiddlewareRegistry.register(store => next => action => {
}
const { jitsiTrack } = action.track;
const muted = jitsiTrack.isMuted();
const participantID = jitsiTrack.getParticipantId();
const isVideoTrack = jitsiTrack.type !== MEDIA_TYPE.AUDIO;
const local = jitsiTrack.isLocal();
if (isVideoTrack) {
if (jitsiTrack.isLocal() && !(jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP)) {
if (local && !(jitsiTrack.getVideoType() === VIDEO_TYPE.DESKTOP)) {
APP.conference.setVideoMuteStatus();
} else if (!jitsiTrack.isLocal()) {
} else if (!local) {
APP.UI.setVideoMuted(participantID);
}
} else if (jitsiTrack.isLocal()) {
APP.conference.setAudioMuteStatus(muted);
} else {
APP.UI.setAudioMuted(participantID, muted);
} else if (local) {
APP.conference.updateAudioIconEnabled();
}
if (typeof action.track?.muted !== 'undefined' && participantID && !local) {
logTracksForParticipant(store.getState()['features/base/tracks'], participantID, 'Track updated');
}
return result;

View File

@@ -136,6 +136,7 @@ export interface IProps {
description?: string;
disableBackdropClose?: boolean;
disableEnter?: boolean;
disableEscape?: boolean;
onClose?: () => void;
size?: 'large' | 'medium';
submit?: () => void;
@@ -150,6 +151,7 @@ const BaseDialog = ({
description,
disableBackdropClose,
disableEnter,
disableEscape,
onClose,
size = 'medium',
submit,
@@ -166,7 +168,7 @@ const BaseDialog = ({
}, [ disableBackdropClose, onClose ]);
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (e.key === 'Escape' && !disableEscape) {
onClose?.();
}
if (e.key === 'Enter' && !disableEnter) {

View File

@@ -1,4 +1,6 @@
import React, { ReactNode, useEffect, useLayoutEffect, useRef, useState } from 'react';
import React, { KeyboardEvent, ReactNode,
useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { FocusOn } from 'react-focus-on';
import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
@@ -9,6 +11,7 @@ import participantsPaneTheme from '../../../components/themes/participantsPaneTh
import { withPixelLineHeight } from '../../../styles/functions.web';
import { spacing } from '../../Tokens';
/**
* Get a style property from a style declaration as a float.
*
@@ -45,6 +48,11 @@ interface IProps {
*/
accessibilityLabel?: string;
/**
* To activate the FocusOn component.
*/
activateFocusTrap?: boolean;
/**
* Children of the context menu.
*/
@@ -163,6 +171,7 @@ const useStyles = makeStyles()(theme => {
const ContextMenu = ({
accessibilityLabel,
activateFocusTrap = false,
children,
className,
entity,
@@ -227,6 +236,133 @@ const ContextMenu = ({
}
}, [ hidden ]);
const handleKeyDown = useCallback((event: KeyboardEvent) => {
const { current: listRef } = containerRef;
const currentFocusElement = document.activeElement;
const moveFocus = (
list: Element | null,
currentFocus: Element | null,
traversalFunction: (
list: Element | null,
currentFocus: Element | null
) => Element | null
) => {
let wrappedOnce = false;
let nextFocus = traversalFunction(list, currentFocus);
/* eslint-disable no-unmodified-loop-condition */
while (list && nextFocus) {
// Prevent infinite loop.
if (nextFocus === list.firstChild) {
if (wrappedOnce) {
return;
}
wrappedOnce = true;
}
// Same logic as useAutocomplete.js
const nextFocusDisabled
/* eslint-disable no-extra-parens */
= (nextFocus as HTMLInputElement).disabled
|| nextFocus.getAttribute('aria-disabled') === 'true';
if (!nextFocus.hasAttribute('tabindex') || nextFocusDisabled) {
// Move to the next element.
nextFocus = traversalFunction(list, nextFocus);
} else {
/* eslint-disable no-extra-parens */
(nextFocus as HTMLElement).focus();
return;
}
}
};
const previousItem = (
list: Element | null,
item: Element | null
): Element | null => {
/**
* To find the last child of the list.
*
* @param {Element | null} element - Element.
* @returns {Element | null}
*/
function lastChild(element: Element | null): Element | null {
while (element?.lastElementChild) {
/* eslint-disable no-param-reassign */
element = element.lastElementChild;
}
return element;
}
if (!list) {
return null;
}
if (list === item) {
return list.lastElementChild;
}
if (item?.previousElementSibling) {
return lastChild(item.previousElementSibling);
}
if (item && item?.parentElement !== list) {
return item.parentElement;
}
return lastChild(list.lastElementChild);
};
const nextItem = (
list: Element | null,
item: Element | null
): Element | null => {
if (!list) {
return null;
}
if (list === item) {
return list.firstElementChild;
}
if (item?.firstElementChild) {
return item.firstElementChild;
}
if (item?.nextElementSibling) {
return item.nextElementSibling;
}
while (item && item.parentElement !== list) {
/* eslint-disable no-param-reassign */
item = item.parentElement;
if (item?.nextElementSibling) {
return item.nextElementSibling;
}
}
return list?.firstElementChild;
};
if (event.key === 'Escape') {
// Close the menu
event.preventDefault();
setIsHidden(true);
} else if (event.key === 'ArrowUp') {
// Move focus to the previous menu item
event.preventDefault();
moveFocus(listRef, currentFocusElement, previousItem);
} else if (event.key === 'ArrowDown') {
// Move focus to the next menu item
event.preventDefault();
moveFocus(listRef, currentFocusElement, nextItem);
}
}, [ containerRef ]);
const removeFocus = useCallback(() => {
onDrawerClose?.();
}, [ onMouseLeave ]);
if (_overflowDrawer && inDrawer) {
return (<div
className = { styles.drawer }
@@ -247,23 +383,32 @@ const ContextMenu = ({
</div>
</Drawer>
</JitsiPortal>
: <div
{ ...aria }
aria-label = { accessibilityLabel }
className = { cx(styles.contextMenu,
: <FocusOn
// Use the `enabled` prop instead of conditionally rendering ReactFocusOn
// to prevent UI stutter on dialog appearance. It seems the focus guards generated annoy
// our DialogPortal positioning calculations.
enabled = { activateFocusTrap && !isHidden }
onClickOutside = { removeFocus }
onEscapeKey = { removeFocus }>
<div
{ ...aria }
aria-label = { accessibilityLabel }
className = { cx(styles.contextMenu,
isHidden && styles.contextMenuHidden,
className
) }
id = { id }
onClick = { onClick }
onKeyDown = { onKeyDown }
onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave }
ref = { containerRef }
role = { role }
tabIndex = { tabIndex }>
{children}
</div>;
) }
id = { id }
onClick = { onClick }
onKeyDown = { onKeyDown ?? handleKeyDown }
onMouseEnter = { onMouseEnter }
onMouseLeave = { onMouseLeave }
ref = { containerRef }
role = { role }
tabIndex = { tabIndex }>
{children}
</div>
</FocusOn >;
};
export default ContextMenu;

View File

@@ -85,7 +85,7 @@ export interface IProps {
* If no onClick handler is provided, we assume the context menu item is
* not interactive and no role will be set.
*/
role?: 'tab' | 'button';
role?: 'tab' | 'button' | 'menuitem';
/**
* Whether the item is marked as selected.

View File

@@ -92,6 +92,7 @@ const Dialog = ({
disableBackdropClose,
hideCloseButton,
disableEnter,
disableEscape,
ok = { translationKey: 'dialog.Ok' },
onCancel,
onSubmit,
@@ -125,6 +126,7 @@ const Dialog = ({
description = { description }
disableBackdropClose = { disableBackdropClose }
disableEnter = { disableEnter }
disableEscape = { disableEscape }
onClose = { onClose }
size = { size }
submit = { submit }
@@ -170,7 +172,7 @@ const Dialog = ({
id = 'modal-dialog-ok-button'
isSubmit = { true }
labelKey = { ok.translationKey }
onClick = { submit } />}
{ ...(!ok.disabled && { onClick: submit }) } />}
</div>
</BaseDialog>
);

View File

@@ -18,6 +18,11 @@ interface IProps {
*/
className?: string;
/**
* The breakout name for aria-label.
*/
defaultName?: string;
/**
* Whether or not the actions should be hidden.
*/
@@ -111,7 +116,15 @@ const useStyles = makeStyles()(theme => {
},
highlighted: {
backgroundColor: theme.palette.ui02
backgroundColor: theme.palette.ui02,
'& .actions': {
display: 'flex',
position: 'relative',
top: 'auto',
boxShadow: `-15px 0px 10px -5px ${theme.palette.ui02}`,
backgroundColor: theme.palette.ui02
}
},
detailsContainer: {
@@ -179,6 +192,7 @@ const useStyles = makeStyles()(theme => {
const ListItem = ({
actions,
className,
defaultName,
icon,
id,
hideActions = false,
@@ -229,6 +243,7 @@ const ListItem = ({
return (
<div
aria-label = { defaultName }
className = { cx('list-item-container',
classes.container,
isHighlighted && classes.highlighted,
@@ -237,6 +252,7 @@ const ListItem = ({
data-testid = { testId }
id = { id }
onClick = { onClick }
role = 'listitem'
{ ...(isMobile
? {
onTouchEnd: _onTouchEnd,

View File

@@ -69,7 +69,6 @@ const useContextMenu = <T>(): [(force?: boolean | Object) => void,
const menuLeave = useCallback(() => {
isMouseOverMenu.current = false;
lowerMenu();
}, [ lowerMenu ]);
return [ lowerMenu, raiseMenu, toggleMenu, menuEnter, menuLeave, raiseContext ];

View File

@@ -1,9 +1,9 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Button from '../../../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../../../base/ui/constants.native';
import { createBreakoutRoom } from '../../../../../breakout-rooms/actions';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { createBreakoutRoom } from '../../actions';
import styles from './styles';

View File

@@ -1,9 +1,9 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Button from '../../../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../../../base/ui/constants.native';
import { autoAssignToBreakoutRooms } from '../../../../../breakout-rooms/actions';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { autoAssignToBreakoutRooms } from '../../actions';
import styles from './styles';

View File

@@ -4,22 +4,23 @@ import { TouchableOpacity, ViewStyle } from 'react-native';
import { Text } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux';
import { createBreakoutRoomsEvent } from '../../../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../../../analytics/functions';
import { hideSheet, openDialog } from '../../../../../base/dialog/actions';
import BottomSheet from '../../../../../base/dialog/components/native/BottomSheet';
import Icon from '../../../../../base/icons/components/Icon';
import { IconCloseLarge, IconEdit, IconRingGroup } from '../../../../../base/icons/svg';
import { isLocalParticipantModerator } from '../../../../../base/participants/functions';
import { closeBreakoutRoom, moveToRoom, removeBreakoutRoom } from '../../../../../breakout-rooms/actions';
import { getBreakoutRoomsConfig } from '../../../../../breakout-rooms/functions';
import { IRoom } from '../../../../../breakout-rooms/types';
import { isBreakoutRoomRenameAllowed } from '../../../../functions';
import { BREAKOUT_CONTEXT_MENU_ACTIONS as ACTIONS } from '../../../../types';
import styles from '../../../native/styles';
import { createBreakoutRoomsEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { hideSheet, openDialog } from '../../../base/dialog/actions';
import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
import Icon from '../../../base/icons/components/Icon';
import { IconCloseLarge, IconEdit, IconRingGroup } from '../../../base/icons/svg';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import styles from '../../../participants-pane/components/native/styles';
import { isBreakoutRoomRenameAllowed } from '../../../participants-pane/functions';
import { BREAKOUT_CONTEXT_MENU_ACTIONS as ACTIONS } from '../../../participants-pane/types';
import { closeBreakoutRoom, moveToRoom, removeBreakoutRoom } from '../../actions';
import { getBreakoutRoomsConfig } from '../../functions';
import { IRoom } from '../../types';
import BreakoutRoomNamePrompt from './BreakoutRoomNamePrompt';
/**
* An array with all possible breakout rooms actions.
*/
@@ -85,7 +86,8 @@ const BreakoutRoomContextMenu = ({ room, actions = ALL_ACTIONS }: IProps) => {
</TouchableOpacity>
)
}
{!room?.isMainRoom && actions.includes(ACTIONS.RENAME) && _isBreakoutRoomRenameAllowed
{
!room?.isMainRoom && actions.includes(ACTIONS.RENAME) && _isBreakoutRoomRenameAllowed
&& <TouchableOpacity
onPress = { onRenameBreakoutRoom }
style = { styles.contextMenuItem as ViewStyle }>
@@ -95,7 +97,8 @@ const BreakoutRoomContextMenu = ({ room, actions = ALL_ACTIONS }: IProps) => {
<Text style = { styles.contextMenuItemText }>{t('breakoutRooms.actions.rename')}</Text>
</TouchableOpacity>
}
{!room?.isMainRoom && isLocalModerator && actions.includes(ACTIONS.REMOVE)
{
!room?.isMainRoom && isLocalModerator && actions.includes(ACTIONS.REMOVE)
&& (room?.participants && Object.keys(room.participants).length > 0
? <TouchableOpacity
onPress = { onCloseBreakoutRoom }

View File

@@ -1,9 +1,10 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import InputDialog from '../../../../../base/dialog/components/native/InputDialog';
import { renameBreakoutRoom } from '../../../../../breakout-rooms/actions';
import { IBreakoutRoomNamePromptProps as IProps } from '../../../../types';
import InputDialog from '../../../base/dialog/components/native/InputDialog';
import { IBreakoutRoomNamePromptProps as IProps } from '../../../participants-pane/types';
import { renameBreakoutRoom } from '../../actions';
/**
* Implements a component to render a breakout room name prompt.

View File

@@ -1,11 +1,11 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../../../../../app/types';
import { isLocalParticipantModerator, isParticipantModerator } from '../../../../../base/participants/functions';
import { IRoom } from '../../../../../breakout-rooms/types';
import { showRoomParticipantMenu } from '../../../../actions.native';
import ParticipantItem from '../../../native/ParticipantItem';
import { IReduxState } from '../../../app/types';
import { isLocalParticipantModerator, isParticipantModerator } from '../../../base/participants/functions';
import { showRoomParticipantMenu } from '../../../participants-pane/actions.native';
import ParticipantItem from '../../../participants-pane/components/native/ParticipantItem';
import { IRoom } from '../../types';
interface IProps {

View File

@@ -0,0 +1,68 @@
import React, { useCallback } from 'react';
import { FlatList } from 'react-native';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { isLocalParticipantModerator } from '../../../base/participants/functions';
import { equals } from '../../../base/redux/functions';
import {
getBreakoutRooms,
getCurrentRoomId,
isAddBreakoutRoomButtonVisible,
isAutoAssignParticipantsVisible,
isInBreakoutRoom
} from '../../functions';
import AddBreakoutRoomButton from './AddBreakoutRoomButton';
import AutoAssignButton from './AutoAssignButton';
import { CollapsibleRoom } from './CollapsibleRoom';
import LeaveBreakoutRoomButton from './LeaveBreakoutRoomButton';
import styles from './styles';
const BreakoutRooms = () => {
const currentRoomId = useSelector(getCurrentRoomId);
const inBreakoutRoom = useSelector(isInBreakoutRoom);
const isBreakoutRoomsSupported = useSelector((state: IReduxState) =>
state['features/base/conference'].conference?.getBreakoutRooms()?.isSupported());
const isLocalModerator = useSelector(isLocalParticipantModerator);
const keyExtractor = useCallback((e: undefined, i: number) => i.toString(), []);
const rooms = Object.values(useSelector(getBreakoutRooms, equals))
.filter(room => room.id !== currentRoomId)
.sort((p1, p2) => (p1?.name || '').localeCompare(p2?.name || ''));
const showAddBreakoutRoom = useSelector(isAddBreakoutRoomButtonVisible);
const showAutoAssign = useSelector(isAutoAssignParticipantsVisible);
return (
<JitsiScreen
footerComponent = { isLocalModerator && showAddBreakoutRoom
? AddBreakoutRoomButton : undefined }
style = { styles.breakoutRoomsContainer }>
{ /* Fixes warning regarding nested lists */ }
<FlatList
/* eslint-disable react/jsx-no-bind */
ListHeaderComponent = { () => (
<>
{ showAutoAssign && <AutoAssignButton /> }
{ inBreakoutRoom && <LeaveBreakoutRoomButton /> }
{
isBreakoutRoomsSupported
&& rooms.map(room => (<CollapsibleRoom
key = { room.id }
room = { room }
roomId = { room.id } />))
}
</>
) }
data = { [] as ReadonlyArray<undefined> }
keyExtractor = { keyExtractor }
renderItem = { null }
windowSize = { 2 } />
</JitsiScreen>
);
};
export default BreakoutRooms;

View File

@@ -0,0 +1,53 @@
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import {
BREAKOUT_ROOMS_BUTTON_ENABLED
} from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { translate } from '../../../base/i18n/functions';
import { IconRingGroup } from '../../../base/icons/svg';
import AbstractButton,
{
IProps as AbstractButtonProps
} from '../../../base/toolbox/components/AbstractButton';
import {
navigate
} from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
/**
* Implements an {@link AbstractButton} to open the breakout room screen.
*/
class BreakoutRoomsButton extends AbstractButton<AbstractButtonProps> {
accessibilityLabel = 'toolbar.accessibilityLabel.breakoutRooms';
icon = IconRingGroup;
label = 'breakoutRooms.buttonLabel';
/**
* Handles clicking / pressing the button and opens the breakout rooms screen.
*
* @private
* @returns {void}
*/
_handleClick() {
return navigate(screen.conference.breakoutRooms);
}
}
/**
* Maps part of the redux state to the component's props.
*
* @param {IReduxState} state - The Redux state.
* @returns {Object}
*/
function _mapStateToProps(state: IReduxState) {
const enabled = getFeatureFlag(state, BREAKOUT_ROOMS_BUTTON_ENABLED, true);
return {
visible: enabled
};
}
export default translate(connect(_mapStateToProps)(BreakoutRoomsButton));

View File

@@ -3,11 +3,9 @@ import { useTranslation } from 'react-i18next';
import { FlatList } from 'react-native';
import { useDispatch } from 'react-redux';
import { openSheet } from '../../../../../base/dialog/actions';
import { IRoom } from '../../../../../breakout-rooms/types';
import { participantMatchesSearch } from '../../../../functions';
import CollapsibleList from '../../../native/CollapsibleList';
import styles from '../../../native/styles';
import { openSheet } from '../../../base/dialog/actions';
import CollapsibleList from '../../../participants-pane/components/native/CollapsibleList';
import { IRoom } from '../../types';
import BreakoutRoomContextMenu from './BreakoutRoomContextMenu';
import BreakoutRoomParticipantItem from './BreakoutRoomParticipantItem';
@@ -19,10 +17,7 @@ interface IProps {
*/
room: IRoom;
/**
* Participants search string.
*/
searchString: string;
roomId: string;
}
/**
@@ -35,7 +30,7 @@ function _keyExtractor(item: any) {
return item.jid;
}
export const CollapsibleRoom = ({ room, searchString }: IProps) => {
export const CollapsibleRoom = ({ room, roomId }: IProps) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const _openContextMenu = useCallback(() => {
@@ -46,29 +41,22 @@ export const CollapsibleRoom = ({ room, searchString }: IProps) => {
= `${room.name
|| t('breakoutRooms.mainRoom')} (${roomParticipantsNr})`;
// Regarding the fact that we have 3 sections, we apply
// a certain height percentage for every section in order for all to fit
// inside the participants pane container
const containerStyle
= roomParticipantsNr > 2 ? styles.collapsibleRoomContainer : undefined;
return (
<CollapsibleList
containerStyle = { containerStyle }
onLongPress = { _openContextMenu }
title = { title }>
<FlatList
bounces = { false }
data = { Object.values(room.participants || {}) }
horizontal = { false }
keyExtractor = { _keyExtractor }
listKey = { roomId }
// eslint-disable-next-line react/jsx-no-bind, no-confusing-arrow
renderItem = { ({ item: participant }) => participantMatchesSearch(participant, searchString)
? <BreakoutRoomParticipantItem
renderItem = { ({ item: participant }) => (
<BreakoutRoomParticipantItem
item = { participant }
room = { room } />
: null }
scrollEnabled = { true }
) }
scrollEnabled = { false }
showsHorizontalScrollIndicator = { false }
windowSize = { 2 } />
</CollapsibleList>

View File

@@ -1,11 +1,11 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { createBreakoutRoomsEvent } from '../../../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../../../analytics/functions';
import Button from '../../../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../../../base/ui/constants.native';
import { moveToRoom } from '../../../../../breakout-rooms/actions';
import { createBreakoutRoomsEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import Button from '../../../base/ui/components/native/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { moveToRoom } from '../../actions';
import styles from './styles';

View File

@@ -1,12 +1,5 @@
import BaseTheme from '../../../../../base/ui/components/BaseTheme.native';
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
const baseButton = {
borderRadius: BaseTheme.shape.borderRadius,
height: BaseTheme.spacing[7],
marginTop: BaseTheme.spacing[3],
marginLeft: BaseTheme.spacing[3],
marginRight: BaseTheme.spacing[3]
};
/**
* The styles of the native components of the feature {@code breakout rooms}.
@@ -14,16 +7,8 @@ const baseButton = {
export default {
button: {
marginTop: BaseTheme.spacing[3],
marginLeft: BaseTheme.spacing[3],
marginRight: BaseTheme.spacing[3]
},
collapsibleRoom: {
...baseButton,
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
marginBottom: BaseTheme.spacing[4],
marginHorizontal: BaseTheme.spacing[2]
},
collapsibleList: {
@@ -32,7 +17,7 @@ export default {
display: 'flex',
flexDirection: 'row',
height: BaseTheme.spacing[7],
marginHorizontal: BaseTheme.spacing[3],
marginHorizontal: BaseTheme.spacing[2],
marginTop: BaseTheme.spacing[3]
},
@@ -68,5 +53,24 @@ export default {
alignSelf: 'center',
justifyContent: 'center',
marginTop: BaseTheme.spacing[3]
},
breakoutRoomsContainer: {
backgroundColor: BaseTheme.palette.ui01,
flex: 1,
flexDirection: 'column',
height: 'auto',
paddingHorizontal: BaseTheme.spacing[3]
},
inputContainer: {
marginLeft: BaseTheme.spacing[2],
marginRight: BaseTheme.spacing[2],
marginTop: BaseTheme.spacing[4]
},
centerInput: {
paddingRight: BaseTheme.spacing[3],
textAlign: 'center'
}
};

View File

@@ -59,13 +59,17 @@ class Chat extends Component<IProps> {
return (
<JitsiScreen
disableForcedKeyboardDismiss = { true }
/* eslint-disable react/jsx-no-bind */
footerComponent = { () =>
<ChatInputBar onSend = { this._onSendMessage } />
}
hasBottomTextInput = { true }
hasTabNavigator = { true }
style = { styles.chatContainer }>
{/* @ts-ignore */}
<MessageContainer messages = { _messages } />
<MessageRecipient privateMessageRecipient = { privateMessageRecipient } />
<ChatInputBar onSend = { this._onSendMessage } />
</JitsiScreen>
);
}

View File

@@ -1,18 +1,26 @@
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { Platform, ViewStyle } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Platform, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconSend } from '../../../base/icons/svg';
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 styles from './styles';
interface IProps extends WithTranslation {
/**
* Application's aspect ratio.
*/
aspectRatio: Symbol;
/**
* Callback to invoke on message send.
*/
@@ -66,11 +74,18 @@ class ChatInputBar extends Component<IProps, IState> {
* @inheritdoc
*/
render() {
let inputBarStyles;
if (this.props.aspectRatio === ASPECT_RATIO_WIDE) {
inputBarStyles = styles.inputBarWide;
} else {
inputBarStyles = styles.inputBarNarrow;
}
return (
<SafeAreaView
edges = { [ 'bottom' ] }
<View
style = { [
styles.inputBar,
inputBarStyles,
this.state.addPadding ? styles.extraBarPadding : null
] as ViewStyle[] }>
<Input
@@ -88,9 +103,8 @@ class ChatInputBar extends Component<IProps, IState> {
disabled = { !this.state.message }
onPress = { this._onSubmit }
src = { IconSend }
style = { styles.sendButton }
type = { BUTTON_TYPES.PRIMARY } />
</SafeAreaView>
</View>
);
}
@@ -137,4 +151,19 @@ class ChatInputBar extends Component<IProps, IState> {
}
}
export default translate(ChatInputBar);
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The redux state.
* @private
* @returns {IProps}
*/
function _mapStateToProps(state: IReduxState) {
const { aspectRatio } = state['features/base/responsive-ui'];
return {
aspectRatio
};
}
export default translate(connect(_mapStateToProps)(ChatInputBar));

View File

@@ -14,6 +14,12 @@ const recipientContainer = {
padding: BaseTheme.spacing[2]
};
const inputBar = {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between'
};
/**
* The styles of the feature chat.
*
@@ -116,12 +122,16 @@ export default {
paddingBottom: 30
},
inputBar: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
marginLeft: BaseTheme.spacing[3],
width: '100%'
inputBarNarrow: {
...inputBar,
height: 112,
marginHorizontal: BaseTheme.spacing[3]
},
inputBarWide: {
...inputBar,
height: 88,
marginHorizontal: BaseTheme.spacing[9]
},
customInputContainer: {
@@ -157,10 +167,6 @@ export default {
flexDirection: 'row'
},
sendButton: {
marginRight: BaseTheme.spacing[5]
},
/**
* Style modifier for system (error) messages.
*/

View File

@@ -153,7 +153,7 @@ class ChromeExtensionBanner extends PureComponent<IProps, IState> {
*/
_isSupportedEnvironment() {
return interfaceConfig.SHOW_CHROME_EXTENSION_BANNER
&& browser.isChrome()
&& browser.isChromiumBased()
&& !browser.isTwa()
&& !isMobileBrowser()
&& !this.props.isVpaas;

View File

@@ -19,7 +19,7 @@ import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
import { translateToHTML } from '../base/i18n/functions';
import i18next from '../base/i18n/i18next';
import { browser } from '../base/lib-jitsi-meet';
import { pinParticipant } from '../base/participants/actions';
import { pinParticipant, raiseHandClear } from '../base/participants/actions';
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { SET_REDUCED_UI } from '../base/responsive-ui/actionTypes';
@@ -90,6 +90,9 @@ StateListenerRegistry.register(
// remaining pinned, since it's not destroyed across runs.
dispatch(pinParticipant(null));
// Clear raised hands.
dispatch(raiseHandClear());
// XXX I wonder if there is a better way to do this. At this stage
// we do know what dialogs we want to keep but the list of those
// we want to hide is a lot longer. Thus we take a bit of a shortcut
@@ -174,11 +177,30 @@ function _checkIframe(state: IReduxState, dispatch: IStore['dispatch']) {
if (inIframe() && state['features/base/config'].disableIframeAPI && !browser.isElectron()
&& !isVpaasMeeting(state) && !allowIframe) {
// show sticky notification and redirect in 5 minutes
const { locationURL } = state['features/base/connection'];
let translationKey = 'notify.disabledIframe';
const hostname = locationURL?.hostname ?? '';
let domain = '';
const mapping: Record<string, string> = {
'8x8.vc': 'https://jaas.8x8.vc',
'meet.jit.si': 'https://jitsi.org/jaas'
};
const jaasDomain = mapping[hostname];
if (jaasDomain) {
translationKey = 'notify.disabledIframeSecondary';
domain = hostname;
}
dispatch(showWarningNotification({
description: translateToHTML(
i18next.t.bind(i18next),
'notify.disabledIframe',
translationKey,
{
domain,
jaasDomain,
timeout: IFRAME_DISABLED_TIMEOUT_MINUTES
}
)

View File

@@ -7,20 +7,12 @@ import { hideDialog } from '../../base/dialog/actions';
import { translate } from '../../base/i18n/functions';
import Dialog from '../../base/ui/components/web/Dialog';
import Tabs from '../../base/ui/components/web/Tabs';
import { THUMBNAIL_SIZE } from '../constants';
import { obtainDesktopSources } from '../functions';
import logger from '../logger';
import DesktopPickerPane from './DesktopPickerPane';
/**
* The size of the requested thumbnails.
*
* @type {Object}
*/
const THUMBNAIL_SIZE = {
height: 300,
width: 300
};
/**
* The sources polling interval in ms.
*
@@ -182,6 +174,7 @@ class DesktopPicker extends PureComponent<IProps, IState> {
this._stopPolling();
}
/**
* Implements React's {@link Component#render()}.
*
@@ -322,14 +315,14 @@ class DesktopPicker extends PureComponent<IProps, IState> {
_onTabSelected(id: string) {
const { sources } = this.state;
this._selectedTabType = id;
// When we change tabs also reset the screenShareAudio state so we don't
// use the option from one tab when sharing from another.
this.setState({
screenShareAudio: false,
selectedSource: this._getSelectedSource(sources),
selectedTab: id
// select type `window` or `screen` from id
selectedTab: id.split('-')[0]
});
}
@@ -405,23 +398,23 @@ class DesktopPicker extends PureComponent<IProps, IState> {
*/
_updateSources() {
const { types } = this.state;
const options = {
types,
thumbnailSize: THUMBNAIL_SIZE
};
if (types.length > 0) {
obtainDesktopSources(
this.state.types,
{ thumbnailSize: THUMBNAIL_SIZE }
)
.then((sources: any) => {
const selectedSource = this._getSelectedSource(sources);
obtainDesktopSources(options)
.then((sources: any) => {
const selectedSource = this._getSelectedSource(sources);
// TODO: Maybe check if we have stopped the timer and unmounted
// the component.
this.setState({
sources,
selectedSource
});
})
.catch(() => { /* ignore */ });
this.setState({
sources,
selectedSource
});
})
.catch((error: any) => logger.log(error));
}
}
}

View File

@@ -74,12 +74,7 @@ class DesktopSourcePreview extends Component<IProps> {
className = { displayClasses }
onClick = { this._onClick }
onDoubleClick = { this._onDoubleClick }>
<div className = 'desktop-source-preview-image-container'>
<img
alt = { this.props.t('welcomepage.logo.desktopPreviewThumbnail') }
className = 'desktop-source-preview-thumbnail'
src = { this.props.source.thumbnail.toDataURL() } />
</div>
{this._renderThumbnailImageContainer()}
<div className = 'desktop-source-preview-label'>
{ this.props.source.name }
</div>
@@ -87,6 +82,43 @@ class DesktopSourcePreview extends Component<IProps> {
);
}
/**
* Render thumbnail screenshare image.
*
* @returns {Object} - Thumbnail image.
*/
_renderThumbnailImageContainer() {
// default data URL for thumnbail image
let srcImage = this.props.source.thumbnail.dataUrl;
// legacy thumbnail image
if (typeof this.props.source.thumbnail.toDataURL === 'function') {
srcImage = this.props.source.thumbnail.toDataURL();
}
return (
<div className = 'desktop-source-preview-image-container'>
{ this._renderThumbnailImage(srcImage) }
</div>
);
}
/**
* Render thumbnail screenshare image.
*
* @param {string} src - Of the image.
* @returns {Object} - Thumbnail image.
*/
_renderThumbnailImage(src: string) {
return (
<img
alt = { this.props.t('welcomepage.logo.desktopPreviewThumbnail') }
className = 'desktop-source-preview-thumbnail'
src = { src } />
);
}
/**
* Invokes the passed in onClick callback.
*

View File

@@ -0,0 +1,9 @@
/**
* The size of the requested thumbnails.
*
* @type {Object}
*/
export const THUMBNAIL_SIZE = {
height: 300,
width: 300
};

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