Compare commits

...

151 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
1433a1ee5d feat(rn,filmstrip) add 1on1 mode
When there are only 2 participants in a call, don't show the remote thumbnail,
unless the `disable1On1Mode` config option is set or the local participant pin
themselves.
2021-08-06 13:31:00 +02:00
dependabot[bot]
834ee22bc3 chore(deps): bump browserslist in /resources/load-test
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.7.3 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.7.3...4.16.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-06 10:02:48 +02:00
Pierre
d6b5687828 fix(misc) fix typeof comparison to undefined 2021-08-06 09:51:54 +02:00
Jaya Allamsetty
6b496d4def chore(deps) lib-jitsi-meet@latest
* fix(ConnectionQuality): Do not show red/yellow GSM bars on join. When the user first unmutes their video, the connection quality is shown as poor until the local stats are available. Calculate the connection quality only after the stats are available, i.e., assume 100% until pcStatsInterval has elapsed.
* feat(non-participant-messages) Add a new JitiConferenceEvent for messages ignored by ENDPOINT_MESSAGE_RECEIVED
* fix(precall) respect custom callstats script url for precall test

9e632a77c5...6a3df11ffa
2021-08-05 17:07:43 -04:00
Jaya Allamsetty
22cc56ce8d chore(deps) lib-jitsi-meet@latest
* feat(BridgeChannel): Signal a new videoType for high fps screenshare. This lets the bridge adjust the bitrate allocation for this source so that layers with higher fps are prioritized over layers with higher resolution. As a result, endpoints with restricted downlink will receive a high fps low resolution share as opposed to a high resolution low fps screenshare.
* fix(log) lower severity of overly verbose logs (2)

fa834c2923...9e632a77c5
2021-08-05 14:16:52 -04:00
Saúl Ibarra Corretgé
0419c5a15b fix(rn,video-quality) fix not selecting any endpoint on mobile
The concept of "visible participants" is not yet implemented.
2021-08-05 17:28:33 +02:00
hmuresan
dda1f3c5ba fix(recording-label) fix recording label behavior 2021-08-05 17:56:12 +03:00
hmuresan
6f41ef75d7 fix(notifications) Fix hiding audio/video lost notification 2021-08-05 16:36:13 +03:00
Saúl Ibarra Corretgé
46cbc0ff49 chore(rn,versions) set mobile app and SDK versions 2021-08-05 13:43:29 +02:00
Saúl Ibarra Corretgé
53a695da90 fix(twa) update masked icon
Contrary to what I thought the expected icon is an icon that can be masked, not
the mask itself.
2021-08-05 13:42:58 +02:00
Calin Chitu
8bbee7d1dc feat(toolbox) updated menu items order, dividers for mobile 2021-08-05 11:56:44 +02:00
Дамян Минков
72d4aa7dd5 fix: Fix av moderation enable-disable sequence.
When you enable and then disable av-moderation just the audio moderation is disabled and video moderation disabling is not signalled to moderated clients.
2021-08-04 19:41:37 +03:00
Saúl Ibarra Corretgé
8161309e28 deps(rn) update WebRTC to M92 2021-08-04 15:42:37 +02:00
Saúl Ibarra Corretgé
465e7f1458 fix(conference) unify conference options
Some options were missing on the mobile side, notably calltsts
enableDisplayNameInStats and enableEmailInStats. Now the same logic will be used
in web and mobile.
2021-08-04 15:32:03 +02:00
Calinteodor
4e43a31ec9 fix(rn,bottom-sheet) fixed surface color
Also fix properly showing the remote menu.
2021-08-04 15:11:32 +02:00
Christoph Settgast
70c5ea04b1 fix(lang) update German translation
Signed-off-by: Christoph Settgast <csett86@web.de>
2021-08-04 11:00:34 +02:00
Calinteodor
ca25be7314 fix(config) comment out enabledReactions 2021-08-04 10:58:07 +02:00
Calinteodor
3c2ad24652 fix(shared-video,video-menu) add ability to stop shared video from video menu
Specifically, in the bottom sheet (on mobile) and participants pane.
2021-08-04 10:51:05 +02:00
Avram Tudor
e421a119e1 feat(share-video) Allow sharing direct video links (mp4 etc) on mobile (#9511)
* feat(share-video) Allow sharing direct video links (mp4 etc) on mobile

* fix linting

* code review
2021-08-02 15:55:52 +03:00
Hristo Terezov
619acaca24 fix(Filmstrip): Send only visible endpoints to jvb 2021-07-30 11:44:16 -05:00
Calin Chitu
bc9f5773fb feat(participants-pane) changed to standard header 2021-07-30 16:07:48 +02:00
Andrei Gavrilescu
d0be8dcf9d fix(external-api): persist audio output device 2021-07-30 14:51:47 +03:00
Alex Bumbu
af9958ad66 feat(ios) support for destroying & reinitializing the react native bridge 2021-07-30 11:53:30 +02:00
Calinteodor
efc5c9dabe feat(participants-pane) hide admit all if less than 2 participants
- Fixed admitMultiple action for mobile
- Added token color for button
- Hide Admit all button if less than 2 knocking participants
2021-07-30 11:48:06 +02:00
Calinteodor
d22fc88ae3 feat(participants-pane) context menu ui fixes
- Fixed background color for all participants context menus
- Removed connection status from ReactVideoMenu and added it for local participants
- Removed AVModeration comments on mobile
- Show on stage option visible only when participants pane is closed
2021-07-30 11:46:49 +02:00
Mihai-Andrei Uscat
9ee75038b6 fix(Toolbar): Fix toolbar not hiding on mobile 2021-07-30 11:37:45 +03:00
Saúl Ibarra Corretgé
09af88088d fix(logging) reduce overly vebose logging
It huurts performance on mobile. It can still be enabled by setting the level to
debug / trace.
2021-07-28 23:27:54 +02:00
Saúl Ibarra Corretgé
2e539ba010 chore(deps) lib-jitsi-meet@latest
* fix(log) lower severity of overly verbose logs
* e2ee: remove legacy apis (#1653)

b815157a22...fa834c2923
2021-07-28 23:14:08 +02:00
José Luís Andrade
87b3ec2cc0 fix(lang) update portuguese translaation 2021-07-28 22:22:10 +02:00
Pawel Domas
907b51925d fix(prejoin): disposed track was added to the conference
It is not 100% clear to me when it happens, but I think it could happen
in some race condition where a track is unmuted when it's being disposed
or something around those lines.

The fact is that any muted tracks are disposed by replaceLocalTrack(track.jitsiTrack, null) and they should not be used anymore.

Supposedly fixes a crash:

Failed to add local track to conference Track has been already disposed
2021-07-28 09:00:54 -05:00
Saúl Ibarra Corretgé
643340c4a6 fix(deps,rn) bump @react-native-async-storage/async-storage
In version 1.15 the storage backend was rewritten, which hopefully allows us to
fix this crash on Android:

Caused by java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase: /data/user/0/org.jitsi.meet/databases/RKStorage
       at android.database.sqlite.SQLiteClosable.acquireReference(SQLiteClosable.java:55)
       at android.database.sqlite.SQLiteDatabase.queryWithFactory(SQLiteDatabase.java:1160)
       at android.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1036)
       at android.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1204)
       at com.reactnativecommunity.asyncstorage.AsyncStorageModule$1.doInBackgroundGuarded(AsyncStorageModule.java:159)
       at com.reactnativecommunity.asyncstorage.AsyncStorageModule$1.doInBackgroundGuarded(AsyncStorageModule.java:146)
       at com.facebook.react.bridge.GuardedAsyncTask.doInBackground(GuardedAsyncTask.java:35)
       at com.facebook.react.bridge.GuardedAsyncTask.doInBackground(GuardedAsyncTask.java:19)
       at android.os.AsyncTask$2.call(AsyncTask.java:305)
       at java.util.concurrent.FutureTask.run(FutureTask.java:237)
       at com.reactnativecommunity.asyncstorage.AsyncStorageModule$SerialExecutor$1.run(AsyncStorageModule.java:63)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
       at java.lang.Thread.run(Thread.java:760)
2021-07-28 15:26:00 +02:00
Calinteodor
d6c821d524 feat(participants-pane) updates
- Fixed react native community slider to work on both android and ios
- Removed InviteButton from native menus
- Fixed buttons spacing in native OverflowMenu
- Participant context menu details are shown only for remote participants
2021-07-27 16:08:33 +02:00
Saúl Ibarra Corretgé
eb16f93153 fix(rn,polyfills) remove no longer needed hack
With react-native-webrtc 1.89.2 the remote SDP is properly updated before
onaddstream is fired so it's no longer needed.

Also, for readability, IPv6 address synthesis has been moved to a standalone
utils file.
2021-07-26 16:16:27 +02:00
Saúl Ibarra Corretgé
47576aebba chore(deps) react-native-webrtc@1.89.2
THe new version fixed a longstanding problem with RN not updating the JS side
SDP representation properly. This will allow us to remove a hack we currently
have to sidestep this.
2021-07-26 16:16:27 +02:00
Saúl Ibarra Corretgé
bac0a55421 fix(config) add missing buttons to default constants
- Remove button list from interface_config.js since it has been deprecated for a
  while
- Alphabetically sort buttons in config.js and constants.js to make it easier to
  add / remove items
- Add missing invite and toggle-camera buttons to default constants
- Remove no longer existing "fodeviceselection" button

Fixes: https://github.com/jitsi/jitsi-meet/issues/9605
2021-07-26 15:33:38 +02:00
hmuresan
1c8103c444 fix(dropbox-recording) Prevent start recording when no dropbox token 2021-07-26 16:20:05 +03:00
Mihai-Andrei Uscat
4e83e93eb6 fix(virtual-background): Refactor CSS to accommodate smaller screens 2021-07-26 11:57:39 +03:00
Calin Chitu
0f8fa4f059 feat(participants-pane) removed getIsParticipantAudioMuted 2021-07-22 12:54:16 -05:00
Hristo Terezov
becaf0806a fix(ShareDesktopButton): typo. 2021-07-22 11:32:44 -05:00
hmuresan
5b77d722d7 fix(toolbox) add back toggle camera button 2021-07-22 16:20:45 +03:00
hmuresan
f4cde2192e fix(toolbar-buttons) Attempt fix Meet in integration 2021-07-22 16:18:56 +03:00
Hristo Terezov
e91df47d1b fix(ShareDesktopButton): getParticipants reference 2021-07-22 07:37:50 -05:00
robertpin
2d04f3852c fix(reactions) Moved reactions behind feature flag 2021-07-22 13:17:42 +03:00
Mihai-Andrei Uscat
2209394d09 feat(Filmstrip): Collapse filmstrip to avoid toolbar overlap on mobile 2021-07-22 09:37:44 +03:00
Jaya Allamsetty
1e76dc0aa2 chore(deps) lib-jitsi-meet@latest
* feat(JingleSessionPC): Enable unfied plan by default for chrome p2p.
* fix(JingleSessionPC): Fix startMuted cases for p2p unified plan. Chrome doesn't create a decoder for ssrc in the remote description when there is no local source and the endpoint is offerer. Initiating a renegotiation with the endpoint as a responder fixes this issue. Add a workaround until Chrome fixes this bug.
* fix: Missed SSRCs in Unified Plan with several "ssrc-group:FID" groups. (#1658)

e6648fac96...b815157a22
2021-07-21 16:47:12 -04:00
George Politis
75edfc1fab fix: Normalize the tenant part of the URL. (#9577)
This PR normalises the tenant part of the URL. For example, the following URL

    https://jitsi-meet.example.com/something@example.com/something@example.com

is converted to

    https://jitsi-meet.example.com/somethingexample.com/somethingexample.com

whereas before it was converted to

    https://jitsi-meet.example.com/something@example.com/somethingexample.com

i.e. the tenant part was not normalised
2021-07-21 18:48:08 +01:00
Calin Chitu
8c20dd8e47 feat(native-participants-pane) removed everyonemoderator from footer context menu 2021-07-21 12:32:10 -05:00
Calin Chitu
fefe451180 feat(native-participants-pane) updated slider volume private prop 2021-07-21 12:32:10 -05:00
Calin Chitu
b268e01a42 feat(native-participants-pane) rebase, resolved conflicts pt. 2 2021-07-21 12:32:10 -05:00
Calin Chitu
d62e378528 feat(native-participants-pane) rebase, resolved conflicts pt. 1 2021-07-21 12:32:10 -05:00
Calin Chitu
e8ad2365b6 feat(native-participants-pane) rebase and Podfile.lock file updates 2021-07-21 12:32:10 -05:00
Calin Chitu
b7389e1c31 feat(native-participants-pane) implemented review remarks pt.4 2021-07-21 12:32:10 -05:00
Calin Chitu
eeddf6b350 feat(native-participants-pane) fixed lint errors 2021-07-21 12:32:10 -05:00
Calin Chitu
665b7730ee feat(native-participants-pane) implemented review remarks pt. 3 2021-07-21 12:32:10 -05:00
Calin Chitu
7854437e31 feat(native-participants-pane) slider ui fixes 2021-07-21 12:32:10 -05:00
Calin Chitu
600af62945 feat(native-participants-pane) updated podfile 2021-07-21 12:32:10 -05:00
Calin Chitu
88ddb8d9b4 feat(native-participants-pane) volume level state fix 2021-07-21 12:32:10 -05:00
Calin Chitu
5182a720f9 feat(native-participants-pane) volume slider refactoring 2021-07-21 12:32:10 -05:00
Calin Chitu
415562c315 feat(native-participants-pane) updated translations and added throttle for slider 2021-07-21 12:32:10 -05:00
Calin Chitu
53d0a892b5 feat(native-participants-pane) review remarks pt 2 volume slider 2021-07-21 12:32:10 -05:00
Calin Chitu
9b220f3870 feat(native-participants-pane) fixed comment typos and reworks on volume slider 2021-07-21 12:32:10 -05:00
Calin Chitu
c6e50ad439 feat(native-participants-pane) implemented review remarks pt. 1 2021-07-21 12:32:10 -05:00
Calin Chitu
36cb896680 feat(native-participants-pane) resolved rebase conflicts and updated import paths 2021-07-21 12:32:10 -05:00
Calin Chitu
249515ac60 feat(native-participants-pane) removed console.log 2021-07-21 12:32:10 -05:00
Calin Chitu
80b49266ab feat(native-participants-pane) removed unused prop and added onPress condition 2021-07-21 12:32:10 -05:00
Calin Chitu
1afae50923 feat(native-participants-pane) dialog for blocking audio/video 2021-07-21 12:32:10 -05:00
Calin Chitu
b332fb474b feat(native-participants-pane) Updated comments 2021-07-21 12:32:10 -05:00
Calin Chitu
a12ad99ecf feat(native-participants-pane) participants pane open/close fixed 2021-07-21 12:32:10 -05:00
Calin Chitu
400f47963d feat(native-participants-pane) open/close pane native actions 2021-07-21 12:32:10 -05:00
Calin Chitu
65fbc6f256 feat(native-participants-pane) fixed slider error on android 2021-07-21 12:32:10 -05:00
Calin Chitu
e7a324185f feat(native-participants-pane) fixed lint errors 2021-07-21 12:32:10 -05:00
Calin Chitu
14a5c45fa3 feat(native-participants-pane) removed mock data 2021-07-21 12:32:10 -05:00
Calin Chitu
05e6dde341 feat(native-participants-pane) ui fixes 2021-07-21 12:32:10 -05:00
Calin Chitu
e13473d42f feat(native-participants-pane) fixed lint error 2021-07-21 12:32:10 -05:00
Calin Chitu
4b72fefd7e feat(native-participants-pane) created admitAll action 2021-07-21 12:32:10 -05:00
Calin Chitu
ba9398a1e2 feat(native-participants-pane) fixed slider and hid dialog when chat is open 2021-07-21 12:32:10 -05:00
Calin Chitu
8d4cf7165e feat(native-participants-pane) added action dialogs for context menu participant details and native community slider 2021-07-21 12:32:10 -05:00
Calin Chitu
0b3991d9e1 feat(native-participants-pane) context menu for meeting participant 2021-07-21 12:32:10 -05:00
Calin Chitu
47be509d17 feat(native-participants-pane) fixed import order lint error 2021-07-21 12:32:10 -05:00
Calin Chitu
ba64d3e0c8 feat(native-participants-pane) context menu for more btn and reject lobby participant 2021-07-21 12:32:10 -05:00
Calin Chitu
cd05c34d19 feat(native-participants-pane) rendered participant name 2021-07-21 12:32:10 -05:00
Calin Chitu
24550777c6 feat(native-participants-pane) simplified props for hiding header with navigation 2021-07-21 12:32:10 -05:00
Calin Chitu
ee101f8947 feat(native-participants-pane) fixed mute all content styles and added doInvitePeople action 2021-07-21 12:32:10 -05:00
Calin Chitu
8ca85f9e1c feat(native-participants-pane) token updates and added mute all event 2021-07-21 12:32:10 -05:00
Calin Chitu
34ccd56691 feat(native-participants-pane) updated styles for meeting participant list 2021-07-21 12:32:10 -05:00
Calin Chitu
f49c05c666 feat(native-participants-pane) created meeting participant list 2021-07-21 12:32:10 -05:00
Calin Chitu
e7280e5040 feat(native-participants-pane) ui fixes 2021-07-21 12:32:10 -05:00
Calin Chitu
eb1add681f feat(native-participants-pane) admit/reject all buttons 2021-07-21 12:32:10 -05:00
Calin Chitu
8419dc725c feat(native-participants-pane) ui updates for participant item 2021-07-21 12:32:10 -05:00
Calin Chitu
f984faef3f feat(native-participants-pane) first lobbyparticipantlist steps 2021-07-21 12:32:10 -05:00
Calin Chitu
0c76d7532c feat(native-participants-pane) fixed lint errors 2021-07-21 12:32:10 -05:00
Calin Chitu
cb0b68f840 feat(native-participants-pane) removed button component, fixed icons inside paper button component and reverted actions.js files 2021-07-21 12:32:10 -05:00
Calin Chitu
08a4da22f3 feat(native-participants-pane) reverted podfile changes 2021-07-21 12:32:10 -05:00
Calin Chitu
bdd6638067 feat(native-participants-pane) fixed lint errors 2021-07-21 12:32:10 -05:00
Calin Chitu
8b44e06f2c feat(native-participants-pane) separated native actions from web actions and created any actions for the common ones 2021-07-21 12:32:10 -05:00
Calin Chitu
79edc1b358 feat(native-participants-pane) updated styles for button component and participant item related components 2021-07-21 12:32:10 -05:00
Calin Chitu
6597bfc2aa feat(native-participants-pane) added showHeaderWithNavigation prop to JitsiModal and created close button 2021-07-21 12:32:10 -05:00
Calin Chitu
e0a2320d75 feat(native-participants-panel) renamed ParticipantsPanel to ParticipantsPane and created modal 2021-07-21 12:32:10 -05:00
Calin Chitu
81e9fca03b feat(native-participants-panel) created participants panel overflowmenu button 2021-07-21 12:32:10 -05:00
robertpin
76f8302aeb fix(recording-label) Make REC label visible at all times (#9578) 2021-07-21 11:46:49 +03:00
Mihai-Andrei Uscat
7263829763 feat(DominantSpeakerName): Implement 2021-07-21 09:05:47 +03:00
Mihai-Andrei Uscat
b7ab3ea052 fix(Thumbnail, Drawer): Remove hover state; Prevent outside propagation 2021-07-21 09:05:47 +03:00
robertpin
c657f360e1 Fix recording for public access (#9584) 2021-07-21 08:52:04 +03:00
José Luís Andrade
ae5edf5a62 Update toolbarButtons list
Add 'participants-pane' option
2021-07-20 16:57:12 -04:00
hmuresan
2bac757ca6 feat(branding): Add custom avatar backgrounds 2021-07-20 18:56:06 +03:00
robertpin
c10805f81b feat(sound-settings) Added ability to control sounds 2021-07-20 14:56:57 +03:00
robertpin
251eec19cd fix(reactions) Batch events before sending 2021-07-20 14:31:49 +03:00
robertpin
4276f82c03 feat(billing-counter) Removed iframe billing-counter callbacks (#9537)
* Removed iframe billing-counter callbacks

* Moved remaining items to jaas

* Fixed import path

* Removed billingCounter condition

* Use getvpaasTenant in middleware

* Removed billingId

* Path fix

* Removed jwt from isVpaasMeeting

* Fix isVpaas
2021-07-20 11:58:42 +03:00
Jaya Allamsetty
4c3aae1e28 chore(deps) lib-jitsi-meet@latest
* fix(TPC): Fix the screenshare issue when user starts video muted on chrome. Munge 3 ssrcs in the SDP for chrome in unified plan always for the simulcast case.

053a26604d...e6648fac96
2021-07-19 12:17:20 -04:00
Tudor-Ovidiu Avram
12be14bd4b fix(jaas) do not deeplink jaas users on default 8x8 app scheme 2021-07-16 17:05:21 +03:00
Tudor-Ovidiu Avram
420a7d8110 fix(jaas) fix vpaas condition 2021-07-16 15:49:57 +03:00
hmuresan
17f77a4246 fix(support-url): Hide contact support button when SUPPORT_URL is empty
- fix issue on error notification on invite contacts
2021-07-16 14:53:26 +03:00
hmuresan
6f9944a2d0 fix(iOS-mailto): Send no new line invite on default email
- addresses this issue: https://developer.apple.com/forums/thread/681023
2021-07-16 13:27:29 +03:00
Avram Tudor
73328810e4 fix(jaas) hide chrome banner regardless of jwt (#9565) 2021-07-16 13:17:28 +03:00
tudordan7
bb8c30a6c9 fix(virtual-background): Add logs on virtual background actions. 2021-07-16 10:31:35 +03:00
Hristo Terezov
c5438ecd0c fix(AOT): participant selection. 2021-07-15 18:03:17 -05:00
Jaya Allamsetty
e22a25b216 chore(deps) lib-jitsi-meet@latest
* fix(JingleSessionPC): Disable unified-plan for p2p chrome. Do not enable unified plan for p2p chrome by default until StartMutedTest is fixed. Fix media direction for case when there are no local and remote sources, should be set to 'inactive' in that case.

3a313a244d...053a26604d
2021-07-15 18:03:24 -04:00
Дамян Минков
4075e5deb7 fix: Fix speakerstats reports. 2021-07-15 17:03:05 +03:00
robertpin
ea0d953d1c feat(reactions-webhook) Added reactions backend call for webhook (#9534)
* Added backend call for reactions webhook

* Updated webhook url

* Fixed linting error

* Code review fixes

* Fixed linting errors
2021-07-15 15:23:48 +03:00
hmuresan
b3e03fe50c fix(aot): Remove dependency to translate from StatelessAvatar.js
- translate uses interfaceConfig object which does not exist in AOT context
2021-07-15 14:57:33 +03:00
Mircea Sotan
8f81a75a61 fix(android): Fix install error on application compiled against Android S - Targeting S+ (version 10000 and above) requires that an explicit value for android:exported be defined when intent filters are present 2021-07-15 14:48:46 +03:00
Tudor-Ovidiu Avram
0ab905bf75 code review 2021-07-15 14:44:28 +03:00
Tudor-Ovidiu Avram
5a3607f63f fix*(jaas) redirect to plan limit page on connection failed 2021-07-15 14:44:28 +03:00
Дамян Минков
d57e202d19 chore(deps) lib-jitsi-meet@latest
* fix(codec-selection): Fix VP9 codec switching issue in Chrome unified-plan. Munge only the m-line that corresponds to the source that the browser will be sending. Do not select VP9 on Firefox. Detect support for RTCRtpTransceiver#setCodecPreferences correctly.

89a7e2d9cd...3a313a244d
2021-07-15 13:12:53 +03:00
robertpin
1223c63f69 fix(reactions) Updated list of reactions & disabled incoming message sound (#9550)
* Changed reactions

* Disable incoming message sound on reactions
2021-07-15 10:26:27 +03:00
tmoldovan8x8
25b4887f74 chore(rn): set mobile SDK versions 3.7.0 2021-07-15 09:36:41 +03:00
Avram Tudor
e8ee65db82 fix(jaas) always hide chrome banner (#9546) 2021-07-14 16:16:21 +03:00
Дамян Минков
9956ca6551 chore(deps) lib-jitsi-meet@latest
* fix(RTC): In unified-plan mode, disable the low resolution streams for low fps SS. In unified plan impl, it is not possible to enable/disable simulcast during the call since the same sender is re-used for all local video tracks. Therefore, disable the low resolution simulcast streams for low fps screensharing so that the bridge forwards only the highest resolution stream which is important for low fps screensharing.

f974007ca6...89a7e2d9cd
2021-07-14 16:11:46 +03:00
Calin Chitu
03a96d0be2 fix(css) moved to the left moderation text 2021-07-14 11:47:46 +03:00
Дамян Минков
3fa7c01f19 chore(deps) lib-jitsi-meet@latest
* fix(JingleSessionPC): Do not check if the ssrc already exists in the RD when adding a ssrc-group from source-add.
* feat: Switch to unified plan on chrome by default unless explicitly disabled.
* fix(VADAudioAnalyser): NPE error evaluating this._vadEmitter.on (#1652)
* Small fix in tokens doc.

b43a9fa0ee...f974007ca6
2021-07-14 11:47:37 +03:00
Avram Tudor
5cba6c7bc7 fix(jaas) fix jaas customer details endpoint (#9542) 2021-07-14 11:26:04 +03:00
tudordan7
52ee9b5151 fix(virtual-background): Prevent memory leak when desktop share as a virtual background is applied. 2021-07-13 17:30:29 +03:00
SiderealArt
d5f379a97c Update Traditional Chinese translation 2021-07-13 15:58:51 +03:00
SiderealArt
84cdd31731 Update Traditional Chinese translation 2021-07-13 12:54:52 +03:00
MaikHannemann
b7eba915af Added missing german translation 2021-07-13 12:54:38 +03:00
José Luís Andrade
7d0722c031 Update of the translation to portuguese language 2021-07-13 12:54:27 +03:00
robertpin
7d3953de51 Fixed (#9533) 2021-07-13 11:55:07 +03:00
robertpin
601ee219e7 feat(reactions) Added Reactions (#9465)
* Created desktop reactions menu

Moved raise hand functionality to reactions menu

* Added reactions to chat

* Added animations

* Added reactions to the web mobile version

Redesigned the overflow menu. Added the reactions menu and reactions animations

* Make toolbar visible on animation start

* Bug fix

* Cleanup

* Fixed overflow menu desktop

* Revert mobile menu changes

* Removed unused CSS

* Fixed iOS safari issue

* Fixed overflow issue on mobile

* Added keyboard shortcuts for reactions

* Disabled double tap zoom on reaction buttons

* Refactored actions

* Updated option symbol for keyboard shortcuts

* Actions refactor

* Refactor

* Fixed linting errors

* Updated BottomSheet

* Added reactions on native

* Code cleanup

* Code review refactor

* Color fix

* Hide reactions on one participant

* Removed console log

* Lang fix

* Update schortcuts
2021-07-13 09:50:08 +03:00
tudordan7
8db3a341b3 fix(virtual-background): Set x scale to default value on desktop share as a virtual background deactivation. 2021-07-12 15:32:58 +03:00
Tudor D. Pop
8d562b9d59 fix(share-youtube-video): Validate youtube url. 2021-07-09 10:16:35 -04:00
damencho
80e2c05219 fix(av-moderation): Fixes sending wrong whitelist in json. 2021-07-09 15:41:08 +03:00
Hristo Terezov
0bdc7d42c5 feat: Participants optimisations (#9515)
* fix(participants): Change from array to Map

* fix(unload): optimise

* feat: Introduces new states for e2ee feature.

Stores everyoneSupportsE2EE and everyoneEnabledE2EE to minimize looping through participants list.

squash: Uses participants map and go over the elements only once.

* feat: Optimizes isEveryoneModerator to do less frequent checks in all participants.

* fix: Drops deep equal from participants pane and uses the map.

* fix(SharedVideo): isVideoPlaying

* fix(participants): Optimise isEveryoneModerator

* fix(e2e): Optimise everyoneEnabledE2EE

* fix: JS errors.

* ref(participants): remove getParticipants

* fix(participants): Prepare for PR.

* fix: Changes participants pane to be component.

The functional component was always rendered:
`prev props: {} !== {} :next props`.

* feat: Optimization to skip participants list on pane closed.

* fix: The participants list shows and the local participant.

* fix: Fix wrong action name for av-moderation.

* fix: Minimizes the number of render calls of av moderation notification.

* fix: Fix iterating over remote participants.

* fix: Fixes lint error.

* fix: Reflects participant updates for av-moderation.

* fix(ParticipantPane): to work with IDs.

* fix(av-moderation): on PARTCIPANT_UPDATE

* fix(ParticipantPane): close delay.

* fix: address code review comments

* fix(API): mute-everyone

* fix: bugs

* fix(Thumbnail): on mobile.

* fix(ParticipantPane): Close context menu on click.

* fix: Handles few error when local participant is undefined.

* feat: Hides AV moderation if not supported.

* fix: Show mute all video.

* fix: Fixes updating participant for av moderation.

Co-authored-by: damencho <damencho@jitsi.org>
2021-07-09 15:36:19 +03:00
Tudor D. Pop
d87a40e77e fix(virtual-background): Style adjustments on virtual background dial… (#9484)
* fix(virtual-background): Style adjustments on virtual background dialog on small screens.

* fix(virtual-background): Style adjustments on virtual background dialog on small screens.

Co-authored-by: tudordan7 <tudor.pop@decagon.tech>
2021-07-09 10:26:55 +03:00
robertpin
bad58f6508 feat(JaaS-JWT) Get JWT for JaaS (#9512)
* Added get JWT for JaaS

* Code review fix
2021-07-09 09:30:49 +03:00
Hristo Terezov
dc60418613 fix(settings): imports that break mobile. 2021-07-09 06:56:14 +03:00
hmuresan
f2f545a57f fix(connection-indicator) Fix detecting local/remote participant 2021-07-08 20:28:39 +03:00
Tudor D. Pop
d72b27d46d fix(virtual-background): Fix mirror behavior for remote participants. 2021-07-08 13:20:12 -04:00
Hristo Terezov
e1fef8d848 fix(tile-view): when iAmRecorder=true. 2021-07-08 09:06:53 -05:00
Mihai-Andrei Uscat
33f1199fc8 fix(Filmstrip): Fix scrollbars around filmstrip (#9519) 2021-07-08 16:43:15 +03:00
Avram Tudor
62c78950cd feat(toolbox) allow any toolbox button to be displayed in main toolbar (#9488)
* feat(toolbox) allow any toolbox button to be displayed as main

fixes the previous behaviour where only a certain set of buttons were whitelisted for being displayed in the main toolbar

* code review

* code review - fix avatar icon position
2021-07-08 16:42:07 +03:00
Jaya Allamsetty
074a783bd9 feat: Enable unified plan support for p2p chrome calls. 2021-07-07 14:55:07 -04:00
310 changed files with 11118 additions and 4119 deletions

View File

@@ -25,5 +25,5 @@ android.enableDexingArtifactTransform.desugaring=false
android.useAndroidX=true
android.enableJetifier=true
appVersion=21.2.0
sdkVersion=3.6.0
appVersion=21.3.0
sdkVersion=3.8.0

View File

@@ -70,11 +70,13 @@ dependencies {
implementation project(':react-native-default-preference')
implementation project(':react-native-immersive')
implementation project(':react-native-keep-awake')
implementation project(':react-native-slider')
implementation project(':react-native-sound')
implementation project(':react-native-splash-screen')
implementation project(':react-native-svg')
implementation project(':react-native-video')
implementation project(':react-native-webrtc')
implementation project(':react-native-webview')
implementation project(':react-native-splash-screen')
testImplementation 'junit:junit:4.12'
}

View File

@@ -40,7 +40,8 @@
<service
android:name=".ConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>

View File

@@ -187,9 +187,11 @@ class ReactInstanceManagerHolder {
new com.ocetnik.timer.BackgroundTimerPackage(),
new com.reactnativecommunity.asyncstorage.AsyncStoragePackage(),
new com.reactnativecommunity.netinfo.NetInfoPackage(),
new com.reactnativecommunity.slider.ReactSliderPackage(),
new com.reactnativecommunity.webview.RNCWebViewPackage(),
new com.rnimmersive.RNImmersivePackage(),
new com.zmxv.RNSound.RNSoundPackage(),
new com.brentvatne.react.ReactVideoPackage(),
new ReactPackageAdapter() {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {

View File

@@ -19,13 +19,17 @@ include ':react-native-immersive'
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
include ':react-native-keep-awake'
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keep-awake/android')
include ':react-native-slider'
project(':react-native-slider').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/slider/android')
include ':react-native-sound'
project(':react-native-sound').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sound/android')
include ':react-native-splash-screen'
project(':react-native-splash-screen').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-splash-screen/android')
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-video/android')
include ':react-native-webrtc'
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
include ':react-native-webview'
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')

View File

@@ -39,6 +39,7 @@ import {
conferenceWillJoin,
conferenceWillLeave,
dataChannelOpened,
getConferenceOptions,
kickedOut,
lockStateChanged,
onStartMutedPolicyChanged,
@@ -111,7 +112,6 @@ import {
trackRemoved
} from './react/features/base/tracks';
import { downloadJSON } from './react/features/base/util/downloadJSON';
import { getConferenceOptions } from './react/features/conference/functions';
import { showDesktopPicker } from './react/features/desktop-picker';
import { appendSuffix } from './react/features/display-name';
import {
@@ -132,6 +132,7 @@ import { setScreenAudioShareState, isScreenAudioShared } from './react/features/
import { toggleScreenshotCaptureEffect } from './react/features/screenshot-capture';
import { AudioMixerEffect } from './react/features/stream-effects/audio-mixer/AudioMixerEffect';
import { createPresenterEffect } from './react/features/stream-effects/presenter';
import { createRnnoiseProcessor } from './react/features/stream-effects/rnnoise';
import { endpointMessageReceived } from './react/features/subtitles';
import UIEvents from './service/UI/UIEvents';
@@ -1357,7 +1358,11 @@ export default {
},
_getConferenceOptions() {
return getConferenceOptions(APP.store.getState());
const options = getConferenceOptions(APP.store.getState());
options.createVADProcessor = createRnnoiseProcessor;
return options;
},
/**

View File

@@ -70,6 +70,9 @@ var config = {
// callStatsThreshold: 5 // enable callstats for 5% of the users.
},
// Enables reactions feature.
// enableReactions: false,
// Disables ICE/UDP by filtering out local and remote UDP candidates in
// signalling.
// webrtcIceUdpDisable: false,
@@ -459,11 +462,38 @@ var config = {
// - 'desktop' controls the "Share your screen" button
// - if `toolbarButtons` is undefined, we fallback to enabling all buttons on the UI
// toolbarButtons: [
// 'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
// 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
// 'livestreaming', 'etherpad', 'sharedvideo', 'shareaudio', 'settings', 'raisehand',
// 'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
// 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
// 'camera',
// 'chat',
// 'closedcaptions',
// 'desktop',
// 'download',
// 'embedmeeting',
// 'etherpad',
// 'feedback',
// 'filmstrip',
// 'fullscreen',
// 'hangup',
// 'help',
// 'invite',
// 'livestreaming',
// 'microphone',
// 'mute-everyone',
// 'mute-video-everyone',
// 'participants-pane',
// 'profile',
// 'raisehand',
// 'recording',
// 'security',
// 'select-background',
// 'settings',
// 'shareaudio',
// 'sharedvideo',
// 'shortcuts',
// 'stats',
// 'tileview',
// 'toggle-camera',
// 'videoquality',
// '__end'
// ],
// Stats
@@ -514,6 +544,9 @@ var config = {
// connection.
enabled: true,
// Enable unified plan implementation support on Chromium for p2p connection.
// enableUnifiedOnChrome: false,
// Sets the ICE transport policy for the p2p connection. At the time
// of this writing the list of possible values are 'all' and 'relay',
// but that is subject to change in the future. The enum is defined in
@@ -600,6 +633,9 @@ var config = {
// conference (if set to true, these sounds will not be played).
// disableJoinLeaveSounds: false,
// Disables the sounds that play when a chat message is received.
// disableIncomingMessageSound: false,
// Information for the chrome extension banner
// chromeExtensionBanner: {
// // The chrome extension to be installed address
@@ -729,6 +765,9 @@ var config = {
// Hides the conference subject
// hideConferenceSubject: true,
// Hides the recording label
// hideRecordingLabel: false,
// Hides the conference timer.
// hideConferenceTimer: true,

View File

@@ -11,11 +11,13 @@ import {
connectionFailed
} from './react/features/base/connection/actions';
import { openDialog } from './react/features/base/dialog/actions';
import { setJWT } from './react/features/base/jwt';
import {
isFatalJitsiConnectionError,
JitsiConnectionErrors,
JitsiConnectionEvents
} from './react/features/base/lib-jitsi-meet';
import { isVpaasMeeting, getJaasJWT } from './react/features/jaas/functions';
import { setPrejoinDisplayNameRequired } from './react/features/prejoin/actions';
const logger = Logger.getLogger(__filename);
@@ -82,9 +84,16 @@ function checkForAttachParametersAndConnect(id, password, connection) {
* @returns {Promise<JitsiConnection>} connection if
* everything is ok, else error.
*/
export function connect(id, password, roomName) {
export async function connect(id, password, roomName) {
const connectionConfig = Object.assign({}, config);
const { jwt } = APP.store.getState()['features/base/jwt'];
const state = APP.store.getState();
let { jwt } = state['features/base/jwt'];
const { iAmRecorder, iAmSipGateway } = state['features/base/config'];
if (!iAmRecorder && !iAmSipGateway && !jwt && isVpaasMeeting(state)) {
jwt = await getJaasJWT(state);
APP.store.dispatch(setJWT(jwt));
}
// Use Websocket URL for the web app if configured. Note that there is no 'isWeb' check, because there's assumption
// that this code executes only on web browsers/electron. This needs to be changed when mobile and web are unified.

View File

@@ -100,12 +100,18 @@
}
.audio-preview > div:nth-child(2),
.video-preview > div:nth-child(2) {
.video-preview > div:nth-child(2),
.reactions-menu-popup > div:nth-child(2) {
margin-bottom: 4px;
outline: none;
padding: 0;
}
.reactions-menu-popup > div:nth-child(2) {
margin-bottom: 6px;
box-shadow: none;
}
/**
* The following selectors keep the chat modal full-size anywhere between 100px
* and 580px for desktop or 680px for mobile.

View File

@@ -4,17 +4,28 @@
right: 0;
bottom: 0;
z-index: $drawerZ;
border-radius: 16px 16px 0 0;
}
.drawer-portal::after {
content: '';
background-color: $participantsPaneBgColor;
margin-bottom: env(safe-area-inset-bottom, 0);
}
.drawer-menu-container {
height: 100vh;
display: flex;
align-items: flex-end;
}
.drawer-menu {
max-height: 50vh;
max-height: calc(80vh - 64px);
background: #242528;
border-radius: 16px 16px 0 0;
overflow-y: auto;
&.expanded {
max-height: 80vh;
}
overflow-y: hidden;
margin-bottom: env(safe-area-inset-bottom, 0);
width: 100%;
.drawer-toggle {
display: flex;
@@ -42,6 +53,8 @@
font-size: 1.2em;
list-style-type: none;
padding: 0;
height: calc(80vh - 144px - 64px);
overflow-y: auto;
.overflow-menu-item {
box-sizing: border-box;

189
css/_reactions-menu.scss Normal file
View File

@@ -0,0 +1,189 @@
@use 'sass:math';
.reactions-menu {
width: 280px;
background: #292929;
box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.6), 0px 0px 4px 1px rgba(0, 0, 0, 0.25);
border-radius: 3px;
padding: 16px;
&.overflow {
width: auto;
padding-bottom: max(env(safe-area-inset-bottom, 0), 16px);
background-color: #141414;
box-shadow: none;
border-radius: 0;
position: relative;
.toolbox-icon {
width: 48px;
height: 48px;
span.emoji {
width: 48px;
height: 48px;
}
}
.reactions-row {
display: flex;
flex-direction: row;
justify-content: space-around;
.toolbox-button {
margin-right: 0;
}
}
}
.toolbox-icon {
width: 40px;
height: 40px;
border-radius: 6px;
span.emoji {
width: 40px;
height: 40px;
font-size: 22px;
display: flex;
align-items: center;
justify-content: center;
}
}
.reactions-row {
.toolbox-button {
margin-right: 8px;
touch-action: manipulation;
}
.toolbox-button:last-of-type {
margin-right: 0;
}
}
.raise-hand-row {
margin-top: 16px;
.toolbox-button {
width: 100%;
}
.toolbox-icon {
width: 100%;
flex-direction: row;
align-items: center;
span.text {
font-style: normal;
font-weight: 600;
font-size: 14px;
line-height: 24px;
margin-left: 8px;
}
}
}
}
.reactions-animations-container {
position: absolute;
width: 20%;
bottom: 0;
left: 40%;
height: 0;
}
.reactions-menu-popup-container,
.reactions-menu-popup {
display: inline-block;
position: relative;
}
$reactionCount: 20;
@function random($min, $max) {
@return math.random() * ($max - $min) + $min;
}
.reaction-emoji {
position: absolute;
font-size: 24px;
line-height: 32px;
width: 32px;
height: 32px;
top: 0;
left: 20px;
opacity: 0;
z-index: 1;
&.reaction-0 {
animation: flowToRight 5s forwards ease-in-out;
}
@for $i from 1 through $reactionCount {
&.reaction-#{$i} {
animation: animation-#{$i} 5s forwards ease-in-out;
top: #{random(-40, 10)}px;
left: #{random(0, 30)}px;
}
}
}
@keyframes flowToRight {
0% {
transform: translate(0px, 0px) scale(0.6);
opacity: 1;
}
70% {
transform: translate(40px, -70vh) scale(1.5);
opacity: 1;
}
75% {
transform: translate(40px, -70vh) scale(1.5);
opacity: 1;
}
100% {
transform: translate(140px, -50vh) scale(1);
opacity: 0;
}
}
@mixin animation-list {
@for $i from 1 through $reactionCount {
$topX: random(-100, 100);
$topY: random(65, 75);
$bottomX: random(150, 200);
$bottomY: random(40, 50);
@if $topX < 0 {
$bottomX: -$bottomX;
}
@keyframes animation-#{$i} {
0% {
transform: translate(0, 0) scale(0.6);
opacity: 1;
}
70% {
transform: translate(#{$topX}px, -#{$topY}vh) scale(1.5);
opacity: 1;
}
75% {
transform: translate(#{$topX}px, -#{$topY}vh) scale(1.5);
opacity: 1;
}
100% {
transform: translate(#{$bottomX}px, -#{$bottomY}vh) scale(1);
opacity: 0;
}
}
}
}
@include animation-list;

View File

@@ -9,8 +9,31 @@
z-index: $zindex3;
&.visible {
top: 0px;
top: 0;
}
&.recording {
top: 0;
.subject-details-container {
opacity: 0;
transition: opacity .3s ease-in;
}
.subject-info-container .show-always {
transition: margin-left .3s ease-in;
}
&.visible {
.subject-details-container {
opacity: 1;
}
}
}
}
.subject-details-container {
display: flex;
}
.subject-info-container {

View File

@@ -105,16 +105,23 @@
margin: 0 auto;
max-width: 100%;
pointer-events: all;
border-radius: 6px;
}
.toolbox-content-wrapper::after {
content: '';
background: $newToolbarBackgroundColor;
padding-bottom: env(safe-area-inset-bottom, 0);
}
.toolbox-content-items {
background: $newToolbarBackgroundColor;
box-shadow: 0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15);
border-radius: 6px;
margin: 0 auto;
padding: 6px;
text-align: center;
pointer-events: all;
box-shadow: 0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15);
>div {
margin-left: 8px;
@@ -278,6 +285,10 @@
}
}
.profile-button-avatar {
align-items: center;
}
/**
* START of fade in animation for main toolbar
*/

View File

@@ -16,8 +16,8 @@
z-index: $subtitlesZ;
&.lifted {
// Lift subtitle above toolbar+invite box.
bottom: $newToolbarSize + 112px + 40px;
// Lift subtitle above toolbar+dominant speaker box.
bottom: $newToolbarSize + 36px + 40px;
}
span {

View File

@@ -46,6 +46,7 @@ $menuBG:#242528;
$newToolbarFontSize: 24px;
$newToolbarHangupFontSize: 32px;
$newToolbarSize: 48px;
$newToolbarSizeMobile: 60px;
$newToolbarSizeWithPadding: calc(#{$newToolbarSize} + 24px);
$toolbarTitleFontSize: 19px;
$overflowMenuItemColor: #fff;

View File

@@ -122,11 +122,11 @@ body.welcome-page {
#moderated-meetings {
max-width: calc(100% - 40px);
padding: 16px 0 39px 0;
margin: $welcomePageEnterRoomMargin;
width: $welcomePageEnterRoomWidth;
p {
color: $welcomePageDescriptionColor;
float: left;
text-align: $welcomePageHeaderTextAlign;
a {

View File

@@ -35,6 +35,7 @@
display: flex;
justify-content: center;
align-items: center;
transition: margin-bottom .3s ease-in;
}
.filmstrip {
@@ -52,11 +53,23 @@
margin-left: $sidebarWidth;
width: calc(100% - #{$sidebarWidth});
.remote-videos{
.remote-videos {
width: calc(100vw - #{$sidebarWidth});
}
}
}
&.collapse {
#remoteVideos {
height: calc(100% - #{$newToolbarSizeMobile}) !important;
margin-bottom: $newToolbarSizeMobile;
}
.remote-videos {
// !important is needed here as overflow is set via element.style in a FixedSizeGrid.
overflow: hidden auto !important;
}
}
}
/**
@@ -120,3 +133,7 @@
}
}
}
.indicator-icon-container {
display: inline-block;
}

View File

@@ -28,7 +28,7 @@
flex-direction: column-reverse;
height: 100%;
width: 100%;
padding: ($desktopAppDragBarHeight - 5px) 5px 10px;
padding: ($desktopAppDragBarHeight - 5px) 5px calc(env(safe-area-inset-bottom, 0) + 10px);
/**
* fixed positioning is necessary for remote menus and tooltips to pop
* out of the scrolling filmstrip. AtlasKit dialogs and tooltips use

View File

@@ -106,6 +106,7 @@ $flagsImagePath: "../images/";
@import 'connection-status';
@import 'drawer';
@import 'participants-pane';
@import 'reactions-menu';
@import 'plan-limit';
/* Modules END */

View File

@@ -1,5 +1,5 @@
.virtual-background-dialog {
margin-left:-10px;
margin-left: -10px;
position: relative;
max-height: 300px;
color: white;
@@ -7,16 +7,16 @@
grid-template-columns: auto auto auto auto auto;
column-gap: 9px;
cursor: pointer;
.desktop-share:hover, .thumbnail:hover, .blur:hover, .slight-blur:hover, .virtual-background-none:hover{
opacity: .5;
.desktop-share:hover,
.thumbnail:hover,
.blur:hover,
.slight-blur:hover,
.virtual-background-none:hover {
opacity: 0.5;
border: 2px solid #99bbf3;
@media (min-width: 432px) and (min-width: 432px) and (max-width: 632px) {
height: 56px;
width: 56px;
}
@media (max-width: 432px){
height: 56px;
width: 56px;
@media (max-width: 632px) {
height: 60px;
width: 60px;
}
}
.background-option {
@@ -40,28 +40,28 @@
}
.thumbnail-selected {
object-fit: cover;
border: 2px solid #246FE5;
border: 2px solid #246fe5;
}
.blur{
.blur {
box-shadow: inset 0 0 12px #000000;
background: #7E8287;
background: #7e8287;
padding: 0 10px;
}
.blur-selected {
box-shadow: inset 0 0 12px #000000;
background: #7E8287;
border: 2px solid #246FE5;
background: #7e8287;
border: 2px solid #246fe5;
padding: 0 10px;
}
.slight-blur{
.slight-blur {
box-shadow: inset 0 0 12px #000000;
background: #A4A4A4;
background: #a4a4a4;
padding: 0 10px;
}
.slight-blur-selected{
.slight-blur-selected {
box-shadow: inset 0 0 12px #000000;
background: #A4A4A4;
border: 2px solid #246FE5;
background: #a4a4a4;
border: 2px solid #246fe5;
padding: 0 10px;
}
.virtual-background-none {
@@ -70,29 +70,40 @@
}
.none-selected {
background: #525252;
border: 2px solid #246FE5;
border: 2px solid #246fe5;
padding: 0 10px;
}
.desktop-share{
.desktop-share {
background: #525252;
}
.desktop-share-selected{
.desktop-share-selected {
background: #525252;
border: 2px solid #246FE5;
border: 2px solid #246fe5;
padding: 0 10px;
}
@media (min-width: 432px) and (max-width: 632px) {
@media (max-width: 632px) {
font-size: 1.5vw;
.desktop-share,
.virtual-background-none,
.thumbnail,
.blur,
.slight-blur {
height: 60px;
width: 60px;
}
.desktop-share-selected,
.thumbnail-selected,
.none-selected,
.blur-selected,
.slight-blur-selected {
height: 60px;
width: 60px;
}
}
@media (max-width: 432px){
@media (max-width: 360px) {
grid-template-columns: auto auto auto;
font-size: 1.5vw;
}
@media (max-width: 320px){
grid-template-columns: auto auto auto;
font-size: 1.5vw;
}
}
@@ -109,14 +120,14 @@
.file-upload-btn {
display: none;
}
.file-upload-label{
.file-upload-label {
font-size: 14px;
font-weight: 600;
line-height: 20px;
margin-left: -10px;
margin-top: 16px;
margin-bottom: 8px;
color: #669AEC;
color: #669aec;
display: inline-flex;
cursor: pointer;
}
@@ -127,8 +138,8 @@
display: none;
left: 96;
bottom: 51;
@media (min-width: 432px) and (max-width: 632px) {
left: 51px
@media (max-width: 632px) {
left: 51px;
}
}
@@ -139,57 +150,54 @@
.thumbnail-container {
position: relative;
&:focus-within {
.thumbnail ~ .delete-image-icon{
display: block;
}
.thumbnail ~ .delete-image-icon {
display: block;
}
}
}
.add-background{
.add-background {
margin-right: 8px;
}
.apply-background-btn{
.apply-background-btn {
margin-top: 16px;
float: right;
}
}
.video-background-preview-entry{
.video-background-preview-entry {
margin-left: -10px;
height: 250px;
width: 570px;
margin-bottom: 8px;
z-index: 2;
@media (min-width: 432px) and (max-width: 632px) {
@media (max-width: 632px) {
max-width: 336;
}
@media (max-width: 432px){
max-width: 336;
}
}
}
.virtual-background-preview-video{
.virtual-background-preview-video {
margin-left: -10;
border-radius: 6px;
height: 100%;
object-fit: cover;
width: 100%;
}
.video-preview-loader{
border-radius: 6px;
background-color: transparent;
height: 250px;
margin-bottom: 8px;
width: 572px;
position: fixed;
z-index: 2;
@media (min-width: 432px) and (max-width: 632px) {
width: 340px;
}
}
}
.video-preview-loader {
border-radius: 6px;
background-color: transparent;
height: 250px;
margin-bottom: 8px;
width: 572px;
position: fixed;
z-index: 2;
@media (min-width: 432px) and (max-width: 632px) {
width: 340px;
}
}
.video-preview-loader svg{
.video-preview-loader svg {
position: absolute;
top: 40%;
left: 45%;
}
}

View File

@@ -174,7 +174,7 @@ var interfaceConfig = {
RECENT_LIST_ENABLED: true,
REMOTE_THUMBNAIL_RATIO: 1, // 1:1
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar', 'sounds' ],
/**
* Specify which sharing features should be displayed. If the value is not set
@@ -208,13 +208,7 @@ var interfaceConfig = {
* DEPRECATED!
* This config was moved to config.js as `toolbarButtons`.
*/
// TOOLBAR_BUTTONS: [
// 'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
// 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
// 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
// 'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
// 'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
// ],
// TOOLBAR_BUTTONS: [],
TOOLBAR_TIMEOUT: 4000,

View File

@@ -58,7 +58,9 @@ target 'JitsiMeetSDK' do
pod 'react-native-calendar-events', :path => '../node_modules/react-native-calendar-events'
pod 'react-native-keep-awake', :path => '../node_modules/react-native-keep-awake'
pod 'react-native-netinfo', :path => '../node_modules/@react-native-community/netinfo'
pod 'react-native-slider', :path => '../node_modules/@react-native-community/slider'
pod 'react-native-splash-screen', :path => '../node_modules/react-native-splash-screen'
pod 'react-native-video', :path => '../node_modules/react-native-video/react-native-video.podspec'
pod 'react-native-webview', :path => '../node_modules/react-native-webview'
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage'

View File

@@ -1,9 +1,9 @@
PODS:
- AppAuth (1.2.0):
- AppAuth/Core (= 1.2.0)
- AppAuth/ExternalUserAgent (= 1.2.0)
- AppAuth/Core (1.2.0)
- AppAuth/ExternalUserAgent (1.2.0)
- AppAuth (1.4.0):
- AppAuth/Core (= 1.4.0)
- AppAuth/ExternalUserAgent (= 1.4.0)
- AppAuth/Core (1.4.0)
- AppAuth/ExternalUserAgent (1.4.0)
- boost-for-react-native (1.63.0)
- CocoaLumberjack (3.5.3):
- CocoaLumberjack/Core (= 3.5.3)
@@ -48,7 +48,7 @@ PODS:
- GoogleUtilities/Environment (~> 6.7)
- GoogleUtilities/Logger (~> 6.7)
- nanopb (~> 1.30906.0)
- FirebaseCrashlytics (4.6.1):
- FirebaseCrashlytics (4.6.2):
- FirebaseCore (~> 6.10)
- FirebaseInstallations (~> 1.6)
- GoogleDataTransport (~> 7.2)
@@ -77,9 +77,9 @@ PODS:
- GoogleUtilities/Network (~> 6.7)
- "GoogleUtilities/NSData+zlib (~> 6.7)"
- nanopb (~> 1.30906.0)
- GoogleDataTransport (7.4.0):
- GoogleDataTransport (7.5.1):
- nanopb (~> 1.30906.0)
- GoogleSignIn (5.0.1):
- GoogleSignIn (5.0.2):
- AppAuth (~> 1.2)
- GTMAppAuth (~> 1.0)
- GTMSessionFetcher/Core (~> 1.1)
@@ -102,21 +102,17 @@ PODS:
- GoogleUtilities/Logger
- GoogleUtilities/UserDefaults (6.7.2):
- GoogleUtilities/Logger
- GTMAppAuth (1.0.0):
- AppAuth/Core (~> 1.0)
- GTMSessionFetcher (~> 1.1)
- GTMSessionFetcher (1.2.2):
- GTMSessionFetcher/Full (= 1.2.2)
- GTMSessionFetcher/Core (1.2.2)
- GTMSessionFetcher/Full (1.2.2):
- GTMSessionFetcher/Core (= 1.2.2)
- GTMAppAuth (1.2.2):
- AppAuth/Core (~> 1.4)
- GTMSessionFetcher/Core (~> 1.5)
- GTMSessionFetcher/Core (1.6.1)
- nanopb (1.30906.0):
- nanopb/decode (= 1.30906.0)
- nanopb/encode (= 1.30906.0)
- nanopb/decode (1.30906.0)
- nanopb/encode (1.30906.0)
- ObjectiveDropboxOfficial (3.9.4)
- PromisesObjC (1.2.10)
- PromisesObjC (1.2.12)
- RCTRequired (0.61.5-jitsi.2)
- RCTTypeSafety (0.61.5-jitsi.2):
- FBLazyVector (= 0.61.5-jitsi.2)
@@ -288,9 +284,16 @@ PODS:
- React
- react-native-netinfo (4.1.5):
- React
- react-native-slider (3.0.3):
- React
- react-native-splash-screen (3.2.0):
- React
- react-native-webrtc (1.89.1):
- react-native-video (5.1.1):
- React-Core
- react-native-video/Video (= 5.1.1)
- react-native-video/Video (5.1.1):
- React-Core
- react-native-webrtc (1.92.0):
- React-Core
- react-native-webview (11.0.2):
- React-Core
@@ -348,8 +351,8 @@ PODS:
- React-jsi (= 0.61.5-jitsi.2)
- ReactCommon/jscallinvoker (= 0.61.5-jitsi.2)
- ReactCommon/turbomodule/core (= 0.61.5-jitsi.2)
- RNCAsyncStorage (1.13.2):
- React
- RNCAsyncStorage (1.15.5):
- React-Core
- RNDefaultPreference (1.4.2):
- React
- RNDeviceInfo (8.0.0):
@@ -394,7 +397,9 @@ DEPENDENCIES:
- react-native-calendar-events (from `../node_modules/react-native-calendar-events`)
- react-native-keep-awake (from `../node_modules/react-native-keep-awake`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- react-native-splash-screen (from `../node_modules/react-native-splash-screen`)
- react-native-video (from `../node_modules/react-native-video/react-native-video.podspec`)
- react-native-webrtc (from `../node_modules/react-native-webrtc`)
- react-native-webview (from `../node_modules/react-native-webview`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
@@ -475,8 +480,12 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-keep-awake"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-slider:
:path: "../node_modules/@react-native-community/slider"
react-native-splash-screen:
:path: "../node_modules/react-native-splash-screen"
react-native-video:
:path: "../node_modules/react-native-video/react-native-video.podspec"
react-native-webrtc:
:path: "../node_modules/react-native-webrtc"
react-native-webview:
@@ -519,7 +528,7 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
AppAuth: bce82c76043657c99d91e7882e8a9e1a93650cd4
AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
CocoaLumberjack: 2f44e60eb91c176d471fdba43b9e3eae6a721947
DoubleConversion: 5805e889d232975c086db112ece9ed034df7a0b2
@@ -529,20 +538,20 @@ SPEC CHECKSUMS:
FirebaseAnalytics: 5dd088bd2e67bb9d13dbf792d1164ceaf3052193
FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd
FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1
FirebaseCrashlytics: 5777d3462fb8c3ab9e80a2473bd7d667a2e8411c
FirebaseCrashlytics: 1a747c9cc084a24dc6d9511c991db1cd078154eb
FirebaseDynamicLinks: 6eac37d86910382eafb6315d952cc44c9e176094
FirebaseInstallations: 466c7b4d1f58fe16707693091da253726a731ed2
Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51
glog: 1f3da668190260b06b429bb211bfbee5cd790c28
GoogleAppMeasurement: 966e88df9d19c15715137bb2ddaf52373f111436
GoogleDataTransport: b7f406340a291370045a270c599e53c6fa6ec20f
GoogleSignIn: 3a51b9bb8e48b635fd7f4272cee06ca260345b86
GoogleDataTransport: f56af7caa4ed338dc8e138a5d7c5973e66440833
GoogleSignIn: 7137d297ddc022a7e0aa4619c86d72c909fa7213
GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3
GTMAppAuth: 4deac854479704f348309e7b66189e604cf5e01e
GTMSessionFetcher: 61bb0f61a4cb560030f1222021178008a5727a23
GTMAppAuth: ad5c2b70b9a8689e1a04033c9369c4915bfcbe89
GTMSessionFetcher: 36689134877faeb055b27dfa4ccc9ceaa42e029e
nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc
ObjectiveDropboxOfficial: a5afefc83f6467c42c45f2253f583f2ad1ffc701
PromisesObjC: b14b1c6b68e306650688599de8a45e49fae81151
PromisesObjC: 3113f7f76903778cf4a0586bd1ab89329a0b7b97
RCTRequired: a686731276578c125dff205f08b6ec9cee6ede32
RCTTypeSafety: 88e5500e801c00d16a3d1895e3470d13beed6584
React: 8b2bcf6a93846e47a7a365a54ec6edeb78b37701
@@ -556,8 +565,10 @@ SPEC CHECKSUMS:
react-native-calendar-events: 1442fad71a00388f933cfa25512588fec300fcf8
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
react-native-slider: b733e17fdd31186707146debf1f04b5d94aa1a93
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
react-native-webrtc: ccb0c21eb4fb04326648fbdb4a5d49977e2cf274
react-native-video: 1574074179ecaf6a9dd067116c8f31bf9fec15c8
react-native-webrtc: bbb644859dcc37ccb7edaec860ca62ed47bf996c
react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6
@@ -569,7 +580,7 @@ SPEC CHECKSUMS:
React-RCTText: 4f1b99f228278d2a5e9008eced8dc9c974c4a270
React-RCTVibration: c1041024893fdfdb8371e7c720c437751b711676
ReactCommon: 18014e1d98dbeb9141e935cfe35fc93bd511ffb6
RNCAsyncStorage: bc2f81cc1df90c267ce9ed30bb2dbc93b945a8ee
RNCAsyncStorage: 8324611026e8dc3706f829953aa6e3899f581589
RNDefaultPreference: 56a405ce61033ac77b95004dccd7ac54c2eb50d1
RNDeviceInfo: 72ded653ce636b3f03571e90bed99309a714944e
RNGoogleSignin: 39336070b35fc4cea6a98cf111e00480317be0ae
@@ -578,6 +589,6 @@ SPEC CHECKSUMS:
RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c
PODFILE CHECKSUM: d059cebf82da14a53940a16c24c3330752d4b0c8
PODFILE CHECKSUM: f4db44d934caeae7212dbaa33abe62ed164363e8
COCOAPODS: 1.10.1

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>21.2.0</string>
<string>21.3.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSExtension</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>21.2.0</string>
<string>21.3.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>21.2.0</string>
<string>21.3.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UISupportedInterfaceOrientations</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>21.2.0</string>
<string>21.3.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CLKComplicationPrincipalClass</key>

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>3.6.0</string>
<string>3.8.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

@@ -59,6 +59,16 @@
#pragma mark - Utility methods
/**
* Once the react native bridge is destroyed you are responsible for reinstantiating it back. Use this method to do so.
*/
- (void)instantiateReactNativeBridge;
/**
* Helper method to destroy the react native bridge, cleaning up resources in the process. Once the react native bridge is destroyed you are responsible for reinstantiating it back using `instantiateReactNativeBridge` method.
*/
- (void)destroyReactNativeBridge;
- (JitsiMeetConferenceOptions *_Nonnull)getInitialConferenceOptions;
- (BOOL)isCrashReportingDisabled;

View File

@@ -28,7 +28,6 @@
#import <RNGoogleSignin/RNGoogleSignin.h>
#import <WebRTC/RTCLogging.h>
@implementation JitsiMeet {
RCTBridgeWrapper *_bridgeWrapper;
NSDictionary *_launchOptions;
@@ -50,7 +49,7 @@
- (instancetype)init {
if (self = [super init]) {
// Initialize the on and only bridge for interfacing with React Native.
// Initialize the one and only bridge for interfacing with React Native.
_bridgeWrapper = [[RCTBridgeWrapper alloc] init];
// Initialize the listener for handling start/stop screensharing notifications.
@@ -119,6 +118,18 @@
#pragma mark - Utility methods
- (void)instantiateReactNativeBridge {
if (_bridgeWrapper != nil) {
return;
};
_bridgeWrapper = [[RCTBridgeWrapper alloc] init];
}
- (void)destroyReactNativeBridge {
_bridgeWrapper = nil;
}
- (JitsiMeetConferenceOptions *)getInitialConferenceOptions {
if (_launchOptions[UIApplicationLaunchOptionsURLKey]) {
NSURL *url = _launchOptions[UIApplicationLaunchOptionsURLKey];

View File

@@ -24,7 +24,7 @@
"shareInvite": "Einladung zur Versammlung teilen",
"shareLink": "Teilen Sie den Konferenzlink, um andere einzuladen",
"shareStream": "Den Livestreaminglink freigeben",
"sip": "SIP: {{address}}",
"sipAddresses": "SIP-Adressen",
"telephone": "Telefon: {{number}}",
"title": "Personen zu dieser Konferenz einladen",
"yahooEmail": "Yahoo-E-Mail"
@@ -215,6 +215,7 @@
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
"grantModeratorDialog": "Möchten Sie wirklich Moderationsrechte an diese Person vergeben?",
"grantModeratorTitle": "Moderationsrechte vergeben",
"hideShareAudioHelper": "Diese Meldung nicht mehr anzeigen",
"IamHost": "Ich leite das Meeting",
"incorrectRoomLockPassword": "Falsches Passwort",
"incorrectPassword": "Name oder Passwort ungültig",
@@ -257,16 +258,21 @@
"muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
"muteParticipantButton": "Stummschalten",
"muteParticipantDialog": "Wollen Sie diese Person wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, die Person kann dies aber jederzeit selbst tun.",
"muteParticipantsVideoDialog": "Wollen Sie die Kamera dieser Person wirklich deaktivieren? Sie können die Kamera nicht wieder aktivieren, die Person kann dies aber jederzeit selbst tun.",
"muteParticipantTitle": "Person stummschalten?",
"muteParticipantsVideoButton": "Kamera ausschalten",
"muteParticipantsVideoTitle": "Die Kamera von dieser Person ausschalten?",
"muteParticipantsVideoBody": "Sie können die Kamera nicht wieder aktivieren, die Teilnehmer können dies aber jederzeit wieder ändern.",
"noDropboxToken": "Kein gültiges Dropbox-Token",
"Ok": "OK",
"password": "Passwort",
"passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
"passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) nicht unterstützt",
"passwordRequired": "$t(lockRoomPasswordUppercase) erforderlich",
"permissionErrorTitle": "Berechtigung benötigt",
"permissionCameraRequiredError": "Der Zugriff auf die Kamera wird benötigt, um in Videokonferenzen teilzunehmen. Bitte in den Einstellungen zulassen",
"permissionMicRequiredError": "Der Zugriff auf das Mikrofon wird benötigt, um an Konferenzen mit Ton teilzunehmen. Bitte in den Einstellungen zulassen",
"popupError": "Ihr Browser blockiert Pop-ups von dieser Website. Bitte aktivieren Sie Pop-ups in den Sicherheitseinstellungen des Browsers und versuchen Sie es erneut.",
"popupErrorTitle": "Pop-up blockiert",
"readMore": "mehr",
@@ -300,6 +306,13 @@
"sessTerminated": "Konferenz beendet",
"sessionRestarted": "Konferenz neugestartet",
"Share": "Teilen",
"shareAudio": "Fortfahren",
"shareAudioTitle" : "Wie kann Audio geteilt werden",
"shareAudioWarningTitle": "Sie müssen die Bildschirmfreigabe ausschalten, bevor Sie Audio teilen können",
"shareAudioWarningH1": "Wenn Sie Ihr Audio teilen wollen:",
"shareAudioWarningD1": "müssen Sie Ihre Bildschirmfreigabe stoppen, bevor Sie Audio teilen können.",
"shareAudioWarningD2": "müssen Sie Ihre Bildschirmfreigabe neustarten und die Option \"Audio freigeben\" auswählen.",
"shareMediaWarningGenericH2": "Wenn Sie Ihren Bildschirm und Audio teilen wollen",
"shareVideoLinkError": "Bitte einen gültigen YouTube-Link angeben.",
"shareVideoTitle": "Video teilen",
"shareYourScreen": "Bildschirmfreigabe ein-/ausschalten",
@@ -307,6 +320,10 @@
"startLiveStreaming": "Livestream starten",
"startRecording": "Aufnahme starten",
"startRemoteControlErrorMessage": "Beim Versuch, die Fernsteuerung zu starten, ist ein Fehler aufgetreten!",
"shareScreenWarningTitle": "Sie müssen die Audiofreigabe beenden, bevor Sie den Bildschirm freigeben können",
"shareScreenWarningH1": "Wenn Sie Ihren Bildschirm freigeben wollen:",
"shareScreenWarningD1": "müssen Sie Ihre Audiofreigabe stoppen, bevor Sie ihren Bildschirm freigeben.",
"shareScreenWarningD2": "müssen Sie Ihre Audiofreigabe stoppen und dann die Bildschirmfreigabe mit der Option \"Audio freigeben\" starten.",
"stopLiveStreaming": "Livestream stoppen",
"stopRecording": "Aufnahme stoppen",
"stopRecordingWarning": "Sind Sie sicher, dass Sie die Aufnahme stoppen möchten?",
@@ -323,6 +340,9 @@
"userIdentifier": "Benutzername",
"userPassword": "Passwort",
"videoLink": "Video-Link",
"viewUpgradeOptions": "Upgradeoptionen anzeigen",
"viewUpgradeOptionsContent": "Sie müssen Ihren Tarif erweitern, um Premium-Features wie Aufnahme, Transkription, RTMP-Streaming und mehr zu nutzen.",
"viewUpgradeOptionsTitle": "Sie haben ein Premium-Feature entdeckt!",
"WaitForHostMsg": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
"WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
"WaitingForHostTitle": "Warten auf den Beginn der Konferenz …",
@@ -344,11 +364,11 @@
"title": "Diese Konferenz einbetten"
},
"virtualBackground": {
"apply": "Anwenden",
"title": "Hintergründe",
"blur": "Hintergrund unscharf",
"slightBlur": "Hintergrund leicht unscharf",
"removeBackground": "Hintergrund entfernen",
"uploadImage": "Bild hochladen",
"addBackground": "Hintergrund hinzufügen",
"pleaseWait": "Bitte warten...",
"none": "keiner",
@@ -362,7 +382,8 @@
"image6" : "Wald",
"image7" : "Sonnenaufgang",
"desktopShareError": "Desktop konnte nicht freigegeben werden",
"desktopShare":"Desktopfreigabe"
"desktopShare": "Desktopfreigabe",
"webAssemblyWarning": "WebAssembly wird nicht unterstützt"
},
"feedback": {
"average": "Durchschnittlich",
@@ -399,6 +420,10 @@
"invitePhone": "Wenn Sie stattdessen per Telefon beitreten möchten, wählen sie: {{number}},,{{conferenceID}}#\n",
"invitePhoneAlternatives": "Suchen Sie nach einer anderen Einwahlnummer ?\nEinwahlnummern der Konferenz anzeigen: {{url}}\n\n\nWenn Sie sich auch über ein Raumtelefon einwählen, nehmen Sie teil, ohne sich mit dem Ton zu verbinden: {{silentUrl}}",
"inviteSipEndpoint": "Um mit SIP teilzunehmen, folgende Adresse nutzen: {{sipUri}}",
"inviteTextiOSPersonal": "{{name}} lädt Sie zu einem Meeting ein.",
"inviteTextiOSJoinSilent": "Wenn Sie über ein Konferenztelefon teilnehmen, können Sie diesen Link nutzen um ohne Ton an der Konferenz teilzunehmen: {{silentUrl}}.",
"inviteTextiOSInviteUrl": "Am Meeting teilnehmen: {{inviteUrl}}.",
"inviteTextiOSPhone": "Nutzen Sie folgende Nummer um via Telefon teilzunehmen: {{number}},,{{conferenceID}}#. Wenn Sie nach einer anderen Einwahlnummer suchen, finden Sie die vollständige Liste hier: {{didUrl}}.",
"inviteURLFirstPartGeneral": "Sie wurden zur Teilnahme an einem Meeting eingeladen.",
"inviteURLFirstPartPersonal": "{{name}} lädt Sie zu einem Meeting ein.\n",
"inviteURLSecondPart": "\nAm Meeting teilnehmen:\n{{url}}\n",
@@ -409,6 +434,7 @@
"noRoom": "Keine Konferenz für die Einwahlinformationen angegeben.",
"numbers": "Einwahlnummern",
"password": "$t(lockRoomPasswordUppercase):",
"sip": "SIP-Adresse",
"title": "Teilen",
"tooltip": "Freigabe-Link und Einwahlinformationen für dieses Meeting",
"label": "Einwahlinformationen"
@@ -519,6 +545,7 @@
"focus": "Konferenzleitung",
"focusFail": "{{component}} ist im Moment nicht verfügbar wiederholen in {{ms}} Sekunden",
"grantedTo": "Moderationsrechte an {{to}} vergeben!",
"hostAskedUnmute": "Die Moderation bittet Sie, das Mikrofon zu aktivieren",
"invitedOneMember": "{{name}} wurde eingeladen",
"invitedThreePlusMembers": "{{name}} und {{count}} andere wurden eingeladen",
"invitedTwoMembers": "{{first}} und {{second}} wurden eingeladen",
@@ -534,7 +561,7 @@
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
"raisedHand": "{{name}} möchte sprechen.",
"screenShareNoAudio": " Share audio box was not checked in the window selection screen.",
"screenShareNoAudio": "Die Option \"Audio freigeben\" wurde bei der Auswahl des Fensters nicht ausgewählt.",
"screenShareNoAudioTitle": "Share audio was not checked",
"somebody": "Jemand",
"startSilentTitle": "Sie sind ohne Audioausgabe beigetreten!",
@@ -549,28 +576,40 @@
"oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ",
"oldElectronClientDescription2": "aktuelle Version",
"oldElectronClientDescription3": "!",
"moderationInEffectDescription": "Bitte melden um zu sprechen",
"moderationInEffectCSDescription": "Bitte melden um ein Video zu teilen",
"moderationInEffectVideoDescription": "Bitte melden um die Kamera zu starten",
"moderationInEffectTitle": "Das Mikrofon ist von der Moderation gesperrt",
"moderationInEffectCSTitle": "Die Videofreigabe ist von der Moderation gesperrt",
"moderationInEffectVideoTitle": "Die Kamera ist von der Moderation gesperrt",
"moderationRequestFromModerator": "Die Moderation bittet Sie, das Mikrofon zu aktivieren",
"moderationRequestFromParticipant": "möchte sprechen",
"moderationStartedTitle": "Moderation gestartet",
"moderationStoppedTitle": "Moderation gestoppt",
"moderationToggleDescription": "von {{participantDisplayName}}",
"raiseHandAction": "Melden",
"groupTitle": "Benachrichtigungen"
},
"participantsPane": {
"close": "Schließen",
"header": "Anwesende",
"headings": {
"lobby": "Lobby ({{count}})",
"participantsList": "Teilnehmer ({{count}})"
},
"actions": {
"muteAll": "Alle stummschalten",
"stopVideo": "Video stoppen"
}
},
"participantsPane": {
"headings": {
"lobby": "Lobby ({{count}})",
"participantsList": "Anwesende ({{count}})"
"participantsList": "Anwesende ({{count}})",
"waitingLobby": "In der Lobby ({{count}})"
},
"actions": {
"allow": "Anwesenden erlauben:",
"blockEveryoneMicCamera": "Kamera und Mikrofon von allen sperren",
"invite": "Person einladen",
"askUnmute": "Anfragen, Stummschaltung aufzuheben",
"mute": "Stummschalten",
"muteAll": "Alle stummschalten",
"stopVideo": "Kamera ausschalten"
"muteEveryoneElse": "Alle anderen stummschalten",
"startModeration": "Stummschaltung aufheben oder Kamera aktivieren",
"stopEveryonesVideo": "Alle Kameras ausschalten",
"stopVideo": "Kamera ausschalten",
"unblockEveryoneMicCamera": "Kamera und Mikrofon von allen entsperren"
}
},
"passwordSetRemotely": "von einer anderen Person gesetzt",
@@ -625,9 +664,9 @@
"linkCopied": "Link in die Zwischenablage kopiert",
"lookGood": "Ihr Mikrofon scheint zu funktionieren.",
"or": "oder",
"keyboardShortcuts" : "Tastaturkurzbefehle aktivieren",
"premeeting": "Vorschau",
"showScreen": "Konferenzvorschau aktivieren",
"keyboardShortcuts" : "Tastaturkurzbefehle aktivieren",
"startWithPhone": "Mit Telefonaudio starten",
"screenSharingError": "Fehler bei Bildschirmfreigabe:",
"videoOnlyError": "Videofehler:",
@@ -664,12 +703,15 @@
"beta": "BETA",
"busy": "Es werden Ressourcen für eine Aufnahme bereitgestellt. Bitte in ein paar Minuten erneut versuchen.",
"busyTitle": "Alle Aufnahme-Instanzen sind in Gebrauch",
"copyLink": "Link kopieren",
"error": "Die Aufzeichnung ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
"errorFetchingLink": "Der Link zur Aufzeichnung konnte nicht geladen werden.",
"expandedOff": "Aufzeichnung wurde gestoppt",
"expandedOn": "Das Meeting wird momentan aufgezeichnet.",
"expandedPending": "Aufzeichnung wird gestartet…",
"failedToStart": "Die Aufnahme konnte nicht gestartet werden",
"fileSharingdescription": "Aufzeichnung mit den Personen der Konferenz teilen",
"linkGenerated": "Link zur Aufzeichnung wurde generiert.",
"live": "LIVE",
"loggedIn": "Als {{userName}} angemeldet",
"off": "Aufnahme gestoppt",
@@ -684,7 +726,8 @@
"signIn": "Anmelden",
"signOut": "Abmelden",
"unavailable": "Oh! Der {{serviceName}} ist aktuell nicht verfügbar. Wir arbeiten an der Behebung des Problems. Bitte versuchen Sie es später noch einmal.",
"unavailableTitle": "Aufnahme nicht verfügbar"
"unavailableTitle": "Aufnahme nicht verfügbar",
"uploadToCloud": "In die Cloud hochladen"
},
"sectionList": {
"pullToRefresh": "Ziehen, um zu aktualisieren"
@@ -703,8 +746,13 @@
"signedIn": "Momentan wird auf Kalendertermine von {{email}} zugegriffen. Klicken Sie auf die folgende Schaltfläche „Trennen“, um den Zugriff auf die Kalendertermine zu stoppen.",
"title": "Kalender"
},
"desktopShareFramerate": "Framerate für Bildschirmfreigabe",
"desktopShareWarning": "Sie müssen die Bildschirmfreigabe neustarten, damit die Einstellung übernommen wird.",
"desktopShareHighFpsWarning": "Eine höhere Framerate könnte sich auf Ihre Datenrate auswirken. Sie müssen die Bildschirmfreigabe neustarten, damit die Einstellung übernommen wird.",
"devices": "Geräte",
"followMe": "Follow-me für alle Personen",
"framesPerSecond": "FPS",
"incomingMessage": "Eingehende Nachricht",
"language": "Sprache",
"loggedIn": "Als {{name}} angemeldet",
"microphones": "Mikrofon",
@@ -712,12 +760,18 @@
"more": "Mehr",
"name": "Name",
"noDevice": "Kein",
"participantJoined": "Neue Person nimmt teil",
"participantLeft": "Person verlässt die Konferenz",
"playSounds": "Hinweistöne aktiviert",
"sameAsSystem": "Wie System ({{label}})",
"selectAudioOutput": "Audioausgabe",
"selectCamera": "Kamera",
"selectMic": "Mikrofon",
"sounds": "Hinweistöne",
"speakers": "Lautsprecher",
"startAudioMuted": "Alle Personen treten stumm geschaltet bei",
"startAudioMuted": "Alle Personen treten stummgeschaltet bei",
"startVideoMuted": "Alle Personen treten ohne Video bei",
"talkWhileMuted": "Wenn bei Stummschaltung gesprochen wird",
"title": "Einstellungen"
},
"settingsView": {
@@ -766,12 +820,14 @@
"title": "Die Konferenz wurde unterbrochen, weil der Standby-Modus aktiviert wurde."
},
"toolbar": {
"accessibilityLabel": {
"accessibilityLabel": {
"audioOnly": "„Nur Audio“ ein-/ausschalten",
"audioRoute": "Audiogerät auswählen",
"boo": "Buhen",
"callQuality": "Qualitätseinstellungen",
"cc": "Untertitel ein-/ausschalten",
"chat": "Chatfenster öffnen / schließen",
"clap": "Klatschen",
"document": "Geteiltes Dokument schließen",
"download": "Unsere Apps herunterladen",
"embedMeeting": "Konferenz einbetten",
@@ -782,6 +838,8 @@
"help": "Hilfe",
"invite": "Person einladen",
"kick": "Person entfernen",
"laugh": "Lachen",
"like": "Daumen nach oben",
"lobbyButton": "Lobbymodus ein-/ausschalten",
"localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
"lockRoom": "Konferenzpasswort ein-/ausschalten",
@@ -794,10 +852,12 @@
"muteEveryonesVideo": "Alle Kameras ausschalten",
"muteEveryoneElsesVideo": "Alle anderen Kameras ausschalten",
"participants": "Anwesende",
"party": "Konfetti",
"pip": "Bild-in-Bild-Modus ein-/ausschalten",
"privateMessage": "Private Nachricht senden",
"profile": "Profil bearbeiten",
"raiseHand": "Hand erheben / senken",
"reactionsMenu": "Interaktionsmenü öffnen / schließen",
"recording": "Aufzeichnung ein-/ausschalten",
"remoteMute": "Personen stummschalten",
"remoteVideoMute": "Kamera von dieser Person ausschalten",
@@ -810,23 +870,29 @@
"shortcuts": "Tastenkombinationen ein-/ausblenden",
"show": "Im Vordergrund anzeigen",
"speakerStats": "Sprechstatistik ein-/ausblenden",
"surprised": "Überrascht",
"tileView": "Kachelansicht ein-/ausschalten",
"toggleCamera": "Kamera wechseln",
"toggleFilmstrip": "Miniaturansichten ein-/ausschalten",
"videomute": "„Video stummschalten“ ein-/ausschalten",
"videoblur": "Unscharfer Hintergrund ein-/ausschalten",
"selectBackground": "Hintergrund auswählen",
"expand": "Ausklappen",
"collapse": "Einklappen"
},
"addPeople": "Personen zur Konferenz hinzufügen",
"audioSettings": "Ton-Einstellungen",
"videoSettings": "Kameraeinstellungen",
"audioOnlyOff": "Modus „Nur Audio“ deaktivieren",
"audioOnlyOn": "Modus „Nur Audio“ aktivieren",
"audioRoute": "Audiogerät auswählen",
"authenticate": "Anmelden",
"boo": "Buhen",
"callQuality": "Qualitätseinstellungen",
"chat": "Chat öffnen / schließen",
"clap": "Klatschen",
"closeChat": "Chat schließen",
"closeReactionsMenu": "Interationsmenü schließen",
"documentClose": "Geteiltes Dokument schließen",
"documentOpen": "Geteiltes Dokument öffnen",
"download": "Unsere Apps herunterladen",
@@ -840,6 +906,8 @@
"hangup": "Konferenz verlassen",
"help": "Hilfe",
"invite": "Personen einladen",
"laugh": "Lachen",
"like": "Daumen hoch",
"lobbyButtonDisable": "Lobbymodus deaktivieren",
"lobbyButtonEnable": "Lobbymodus aktivieren",
"login": "Anmelden",
@@ -858,12 +926,20 @@
"noisyAudioInputTitle": "Ihr Mikrofon scheint lärmintensiv zu sein!",
"noisyAudioInputDesc": "Es klingt, als ob Ihr Mikrofon Störgeräusche verursacht. Bitte überlegen Sie, ob Sie das Gerät stummschalten oder austauschen wollen.",
"openChat": "Chat öffnen",
"openReactionsMenu": "Interationsmenü öffnen",
"participants": "Anwesende",
"party": "Konfetti",
"pip": "Bild-in-Bild-Modus einschalten",
"privateMessage": "Private Nachricht senden",
"profile": "Profil bearbeiten",
"raiseHand": "Hand erheben / senken",
"raiseYourHand": "Melden",
"reactionBoo": "Buhen senden",
"reactionClap": "Klatschen senden",
"reactionLaugh": "Lachen senden",
"reactionLike": "Daumen hoch senden",
"reactionParty": "Konfetti senden",
"reactionSurprised": "Überrascht senden",
"security": "Sicherheitsoptionen",
"Settings": "Einstellungen",
"shareaudio": "Audio teilen",
@@ -873,14 +949,15 @@
"speakerStats": "Sprechstatistik",
"startScreenSharing": "Bildschirmfreigabe starten",
"startSubtitles": "Untertitel einschalten",
"stopAudioSharing": "Audiofreigabe stoppen",
"stopScreenSharing": "Bildschirmfreigabe stoppen",
"stopSubtitles": "Untertitel ausschalten",
"stopSharedVideo": "YouTube-Video stoppen",
"surprised": "Überrascht",
"talkWhileMutedPopup": "Versuchen Sie zu sprechen? Ihr Mikrofon ist stummgeschaltet.",
"tileViewToggle": "Kachelansicht ein-/ausschalten",
"toggleCamera": "Kamera wechseln",
"videomute": "Kamera starten / stoppen",
"videoSettings": "Video-Einstellungen",
"selectBackground": "Hintergrund auswählen"
},
"transcribing": {
@@ -974,10 +1051,10 @@
"info": "Einwahlinformationen",
"join": "ERSTELLEN / BEITRETEN",
"jitsiOnMobile": "Jitsi unterwegs einfach unsere Apps herunterladen und Meetings von überall starten",
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, die nur Sie moderieren.",
"mobileDownLoadLinkIos": "iOS App Download",
"mobileDownLoadLinkAndroid": "Android App Download",
"mobileDownLoadLinkFDroid": "F-Droid App Download",
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, die nur Sie moderieren.",
"privacy": "Datenschutz",
"recentList": "Verlauf",
"recentListDelete": "Eintrag löschen",
@@ -1008,6 +1085,7 @@
},
"lobby": {
"admit": "Zulassen",
"admitAll": "Alle zulassen",
"knockingParticipantList": "Liste anklopfender Personen",
"allow": "Annehmen",
"backToKnockModeButton": "Kein Passwort, stattdessen Beitritt anfragen",
@@ -1038,6 +1116,7 @@
"passwordField": "Konferenzpasswort eingeben",
"passwordJoinButton": "Beitreten",
"reject": "Ablehnen",
"rejectAll": "Alle ablehnen",
"toggleLabel": "Lobby aktivieren"
}
}

View File

@@ -1,22 +1,33 @@
{
"addPeople": {
"add": "Convidar",
"countryNotSupported": "Ainda não suportamos este destino.",
"countryReminder": "Está a ligar de fora dos EUA? Por favor, certifique-se de começar com o código do país!",
"disabled": "Você não pode convidar pessoas.",
"addContacts": "Convidar os seus contactos",
"copyInvite": "Cópia do convite para reunião",
"copyLink": "Cópia do link da reunião",
"copyStream": "Copiar do link de transmissão em direto",
"contacts": "contactos",
"countryNotSupported": "Ainda não temos suporte para este destino.",
"countryReminder": "Está a telefonar para fora dos EUA? Por favor, certifique-se de que começa com o código do país!",
"defaultEmail": "O seu e-mail predefinido",
"disabled": "Não pode convidar outras pessoas.",
"failedToAdd": "Falha ao adicionar participantes",
"footerText": "Digitação está desativada.",
"inviteMorePrompt": "Convide mais pessoas",
"loading": "A procurar por pessoas e números de telefone",
"loadingNumber": "A validar o número de telefone",
"loadingPeople": "A procurar pessoas para convidar",
"noResults": "Nenhum resultado de busca correspondente",
"noValidNumbers": "Por favor, digite um número de telefone",
"searchNumbers": "Adicionar números de telefone",
"searchPeople": "Pesquisar pessoas",
"searchPeopleAndNumbers": "Pesquisar por pessoas ou adicionar os seus números de telefone",
"footerText": "A marcação está desactivada.",
"googleEmail": "E-mail do Google",
"inviteMoreHeader": "Você é o único na reunião",
"inviteMoreMailSubject": "Participar na reunião {{appName}}",
"inviteMorePrompt": "Convidar mais pessoas",
"linkCopied": "Link copiado para a área de transferência",
"noResults": "Sem resultados de pesquisa correspondentes",
"outlookEmail": "E-mail do Outlook",
"phoneNumbers": "números de telefone",
"searching": "A pesquisar...",
"shareInvite": "Partilhar convite de reunião",
"shareLink": "Partilhar o link da reunião para convidar outras pessoas",
"shareStream": "Partilhar o link de transmissão em direto",
"sipAddresses": "endereços SIP",
"telephone": "Telefone: {{number}}",
"title": "Convide pessoas para sua reunião"
"title": "Convidar pessoas para esta reunião",
"yahooEmail": "E-mail do Yahoo"
},
"audioDevices": {
"bluetooth": "Bluetooth",
@@ -171,7 +182,7 @@
"dismiss": "Dispensar",
"displayNameRequired": "Olá! Qual é o seu nome?",
"done": "Feito",
"e2eeDescription": "A encriptação de ponta-a-ponta é actualmente EXPERIMENTAL. Tenha em mente que ligar a encriptação de ponta a ponta irá efectivamente desactivar os serviços fornecidos do lado do servidor, tais como: gravação, transmissão em directo e participação telefónica. Tenha também em mente que o encontro só funcionará para pessoas que se juntem a partir de browsers com suporte para \"insertable streams\".",
"e2eeDescription": "A encriptação de ponta a ponta é actualmente EXPERIMENTAL. Tenha em mente que ligar a encriptação de ponta a ponta irá efectivamente desactivar os serviços fornecidos do lado do servidor, tais como: gravação, transmissão em directo e participação telefónica. Tenha também em mente que o encontro só funcionará para pessoas que se juntem a partir de browsers com suporte para \"insertable streams\".",
"e2eeLabel": "Habilitar encriptação de ponta a ponta",
"e2eeWarning": "AVISO: Nem todos os participantes neste encontro parecem ter apoio para a encriptação de ponta a ponta. Se o permitir, eles não o poderão ver nem ouvir.",
"enterDisplayName": "Digite o seu nome aqui",
@@ -271,10 +282,10 @@
"shareYourScreen": "Partilhe o seu ecrã",
"shareYourScreenDisabled": "Partilha de ecrã desactivada.",
"startLiveStreaming": "Iniciar a transmissão em directo",
"startRecording": "Iniciar a gravação",
"startRecording": "Iniciar gravação",
"startRemoteControlErrorMessage": "Ocorreu um erro ao tentar iniciar a sessão de controlo remoto!",
"stopLiveStreaming": "Parar a transmissão em direto",
"stopRecording": "Parar a gravação",
"stopRecording": "Parar gravação",
"stopRecordingWarning": "Tem a certeza de que gostaria de parar a gravação?",
"stopStreamingWarning": "Tem a certeza de que gostaria de parar a transmissão em direto?",
"streamKey": "Chave de transmissão em direto",
@@ -368,7 +379,7 @@
"focusRemote": "Focar no vídeo de outro participante",
"fullScreen": "Entrar ou sair da tela cheia",
"keyboardShortcuts": "Atalhos de teclado",
"localRecording": "Mostrar ou ocultar controles de gravação local",
"localRecording": "Mostrar ou ocultar controlos de gravação local",
"mute": "Deixar mudo ou não o microfone",
"pushToTalk": "Pressione para falar",
"raiseHand": "Erga ou baixe sua mão",
@@ -416,7 +427,7 @@
"on": "Ligado",
"unknown": "Desconhecido"
},
"dialogTitle": "Controles da Gravação Local",
"dialogTitle": "Controlos da Gravação Local",
"duration": "Duração",
"durationNA": "N/D",
"encoding": "Codificando",
@@ -436,7 +447,7 @@
"participantStats": "Estatísticas dos Participantes",
"sessionToken": "Token de Sessão",
"start": "Iniciar gravação",
"stop": "Parar a Gravação",
"stop": "Parar gravação",
"yes": "Sim"
},
"lockRoomPassword": "senha",
@@ -481,10 +492,62 @@
"passwordDigitsOnly": "Até {{number}} dígitos",
"poweredby": "distribuído por",
"prejoin": {
"doNotShow": "Não mostre este ecrã novamente",
"joinMeeting": "Participar na reunião",
"joinWithoutAudio": "Participar sem áudio",
"lookGood": "O seu microfone está a funcionar corretamente"
"audioAndVideoError": "Erro no áudio e vídeo:",
"audioDeviceProblem": "Há um problema com o seu dispositivo de áudio",
"audioOnlyError": "Erro no áudio:",
"audioTrackError": "Não foi possível criar a pista de áudio.",
"calling": "A chamar",
"callMe": "Ligue-me",
"callMeAtNumber": "Ligue-me para este número:",
"configuringDevices": "A configurar os dispositivos...",
"connectedWithAudioQ": "Está ligado com áudio?",
"connection": {
"good": "A sua ligação à Internet parece boa!",
"nonOptimal": "A sua ligação à Internet não é óptima",
"poor": "Tem uma má ligação à Internet"
},
"connectionDetails": {
"audioClipping": "Prevemos que o seu áudio tenha cortes.",
"audioHighQuality": "Prevemos que o seu áudio tenha excelente qualidade.",
"audioLowNoVideo": "Prevemos que a qualidade do seu áudio seja baixa e sem vídeo.",
"goodQuality": "Fantástico! A qualidade dos seus meios de comunicação vai ser óptima.",
"noMediaConnectivity": "Não foi possível encontrar uma forma de estabelecer a conectividade dos meios de comunicação para este teste. Isto é tipicamente causado por uma firewall ou NAT.",
"noVideo": "Prevemos que o seu vídeo seja terrível.",
"undetectable": "Se mesmo assim não conseguir fazer chamadas no browser, recomendamos que se certifique de que os seus altifalantes, microfone e câmara estão devidamente configurados, que concedeu ao seu browser direitos de utilização do seu microfone e câmara, e que a versão do seu browser está actualizada. Se mesmo assim tiver problemas em telefonar, deverá contactar o criador da aplicação web.",
"veryPoorConnection": "Prevemos que a qualidade da sua chamada seja realmente terrível.",
"videoFreezing": "Prevemos que o seu vídeo congele, fique preto, e seja pixelizado.",
"videoHighQuality": "Prevemos que o seu vídeo tenha boa qualidade.",
"videoLowQuality": "Prevemos que o seu vídeo tenha baixa qualidade em termos de velocidade de fotogramas e resolução.",
"videoTearing": "Prevemos que o seu vídeo seja pixelizado ou que tenha artefactos visuais."
},
"copyAndShare": "Copiar e partilhar a ligação da reunião.",
"dialInMeeting": "Entrar com chamada telefónica",
"dialInPin": "Entrar com chamada telefónica e introduzir o código PIN:",
"dialing": "A marcar",
"doNotShow": "Não volte a mostrar este ecrã",
"errorDialOut": "Não foi possível marcar",
"errorDialOutDisconnected": "Não foi possível marcar. Desligado",
"errorDialOutFailed": "Não foi possível marcar. Falha na chamada",
"errorDialOutStatus": "Erro ao obter o estado da chamada realizada",
"errorMissingName": "Por favor, digite o seu nome para participar na reunião",
"errorStatusCode": "Erro ao marcar, código do estado: {{status}}",
"errorValidation": "Falhou a validação do número",
"iWantToDialIn": "Quero entrar por telefone",
"joinAudioByPhone": "Entrar com o áudio do telefone",
"joinMeeting": "Entrar na reunião",
"joinWithoutAudio": "Entrar sem áudio",
"initiated": "Chamada iniciada",
"linkCopied": "Ligação copiada para a área de transferência",
"lookGood": "Parece que o seu microfone está a funcionar corretamente",
"or": "ou",
"premeeting": "Pré-reunião",
"showScreen": "Ativar o ecrã de pré-reunião",
"keyboardShortcuts" : "Ativar os atalhos de teclado",
"startWithPhone": "Iniciar com o áudio do telefone",
"screenSharingError": "Erro de partilha de ecrã:",
"videoOnlyError": "Erro de vídeo:",
"videoTrackError": "Não foi possível criar a pista de vídeo.",
"viewAllNumbers": "ver todos os números"
},
"presenceStatus": {
"busy": "Ocupado",
@@ -539,26 +602,39 @@
},
"settings": {
"calendar": {
"about": "A integração do calendário {{appName}} é usada para acessar com segurança o seu calendário para que ele possa ler os próximos eventos.",
"disconnect": "Desconectar",
"microsoftSignIn": "Entrar com Microsoft",
"signedIn": "Atualmente acessando eventos do calendário para {{email}}. Clique no botão Desconectar abaixo para parar de acessar os eventos da agenda.",
"about": "A integração do calendário {{appName}} é utilizada para aceder com segurança ao seu calendário para que este possa ler os próximos eventos.",
"disconnect": "Desligar",
"microsoftSignIn": "Iniciar sessão com a Microsoft",
"signedIn": "Atualmente a aceder a eventos de calendário por {{email}}. Clique no botão Desconectar abaixo para parar de aceder a eventos de calendário.",
"title": "Calendário"
},
"desktopShareFramerate": "Taxa de fotogramas para partilha do ambiente de trabalho",
"desktopShareWarning": "É necessário reiniciar a partilha do ecrã para que as novas definições entrem em vigor.",
"desktopShareHighFpsWarning": "Uma taxa de fotogramas mais elevada para a partilha do ambiente de trabalho pode afectar a sua largura de banda. É necessário reiniciar a partilha de ecrã para que as novas definições entrem em vigor.",
"devices": "Dispositivos",
"followMe": "Todos me seguem",
"framesPerSecond": "fotogramas-por-segundo",
"incomingMessage": "Receber mensagem",
"language": "Idioma",
"loggedIn": "Conectado como {{name}}",
"loggedIn": "Sessão iniciada como {{name}}",
"microphones": "Microfones",
"moderator": "Moderador",
"more": "Mais",
"name": "Nome",
"noDevice": "Nenhum",
"participantJoined": "Entrar participante",
"participantLeft": "Sair participante",
"playSounds": "Reproduzir som quando",
"sameAsSystem": "O mesmo que o sistema ({{label}})",
"selectAudioOutput": "Saída de áudio",
"selectCamera": "Câmera",
"selectCamera": "Câmara",
"selectMic": "Microfone",
"startAudioMuted": "Todos iniciam mudos",
"startVideoMuted": "Todos iniciam ocultos",
"title": "Configurações"
"sounds": "Sons",
"speakers": "Participantes",
"startAudioMuted": "Todos começam com microfone desligado",
"startVideoMuted": "Todos começam com câmara desligada",
"talkWhileMuted": "se fala e está com microfone desligado",
"title": "Definições"
},
"settingsView": {
"advanced": "",
@@ -643,15 +719,15 @@
"sharedvideo": "Mudar a partilha de vídeos do YouTube",
"shareRoom": "Convidar alguém",
"shareYourScreen": "Iniciar / Parar de partilhar o seu ecrã",
"shortcuts": "Mudar atalhos",
"shortcuts": "Mostrar / Esconder atalhos",
"show": "Mostrar no palco",
"speakerStats": "Mudar estatísticas do apresentador",
"speakerStats": "Mostrar / Esconder estatísticas dos participantes",
"tileView": "Mudar para vista em quadrícula",
"toggleCamera": "Mudar a câmara",
"toggleFilmstrip": "Mudar para película de filme",
"videomute": "Iniciar / Parar câmara",
"videoblur": "Mudar o desfoque de vídeo",
"selectBackground": "Selecionar o fundo",
"selectBackground": "Selecionar plano de fundo",
"expand": "Expandir",
"collapse": "Colapsar"
},
@@ -708,7 +784,7 @@
"sharedvideo": "Partilhar vídeo",
"shareRoom": "Convidar alguém",
"shortcuts": "Ver atalhos",
"speakerStats": "Estatísticas do Apresentador",
"speakerStats": "Estatísticas dos participantes",
"startScreenSharing": "Iniciar partilha de ecrã",
"startSubtitles": "Iniciar legendas",
"stopScreenSharing": "Parar partilha de ecrã",
@@ -718,7 +794,7 @@
"tileViewToggle": "Mudar para vista em quadrícula",
"toggleCamera": "Mudar a câmara",
"videomute": "Iniciar / Parar câmara",
"selectBackground": "Selecionar o fundo"
"selectBackground": "Selecionar plano de fundo"
},
"transcribing": {
"ccButtonTooltip": "Iniciar/parar legendas",

View File

@@ -70,12 +70,17 @@
},
"privateNotice": "私人訊息傳送至 {{recipient}}",
"title": "對話",
"you": "您"
"you": "您",
"message": "訊息",
"messageAccessibleTitle": "{{user}} 說:",
"messageAccessibleTitleMe": "您說:",
"smileysPanel": "表情符號面板"
},
"chromeExtensionBanner": {
"installExtensionText": "安裝適用於 Google 行事曆及 Office 365 整合的擴充功能",
"buttonText": "安裝 Chrome 擴充功能",
"dontShowAgain": "不再顯示"
"dontShowAgain": "不再顯示",
"close": "關閉"
},
"connectingOverlay": {
"joiningRoom": "正在將您連接至您的會議..."
@@ -204,10 +209,13 @@
"e2eeLabel": "啟用端對端加密",
"e2eeWarning": "警告:看來不是每位此會議的參與者都有啟用端對端加密,如果您啟用了,他們可能無法看/聽到您。",
"enterDisplayName": "請在此輸入您自己的名字",
"enterDisplayNameToJoin": "請輸入您的名字以加入",
"embedMeeting": "嵌入會議",
"error": "錯誤",
"gracefulShutdown": "我們的服務目前關閉維護中,請稍後再試。",
"grantModeratorDialog": "您確定要使此參與者成為管理員嗎?",
"grantModeratorTitle": "授予管理員",
"hideShareAudioHelper": "不再顯示",
"IamHost": "我是主辦人",
"incorrectRoomLockPassword": "密碼不符",
"incorrectPassword": "錯誤的用戶名稱或密碼",
@@ -260,11 +268,14 @@
"passwordNotSupported": "尚未支援設定會議密碼 $t(lockRoomPassword)。",
"passwordNotSupportedTitle": "尚未支援 $t(lockRoomPasswordUppercase)",
"passwordRequired": "必須要有 $t(lockRoomPasswordUppercase)",
"permissionErrorTitle": "需要權限",
"permissionCameraRequiredError": "參與視訊會議需要存取攝影機的權限。請在設定中授權",
"permissionMicRequiredError": "參與音訊會議需要存取麥克風的權限。請在設定中授權",
"popupError": "您的瀏覽器在此網站上阻擋彈出視窗。請在瀏覽器的安全設定中啟用並再試一次。",
"popupErrorTitle": "彈出視窗遭到阻擋",
"readMore": "閱讀更多",
"readMore": "更多",
"recording": "錄影中",
"recordingDisabledForGuestTooltip": "訪客無法啟動錄影。",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "正在直播時無法使用",
"recordingDisabledTooltip": "啟動錄影已停用。",
"rejoinNow": "立即重新加入",
"remoteControlAllowedMessage": "{{user}} 接受您進行遠端控制的請求!",
@@ -293,6 +304,13 @@
"sessTerminated": "通話已經終止",
"sessionRestarted": "通話被橋接器重新啟動",
"Share": "分享",
"shareAudio": "繼續",
"shareAudioTitle" : "如何分享音訊",
"shareAudioWarningTitle": "您必須先停止分享畫面才能分享音訊",
"shareAudioWarningH1": "如果您只要分享音訊:",
"shareAudioWarningD1": "您必須先停止分享畫面才能分享音訊。",
"shareAudioWarningD2": "您必須重新啟動畫面分享並勾選 \"分享音訊\" 選項。",
"shareMediaWarningGenericH2": "如果您想要同時分享畫面跟音訊",
"shareVideoLinkError": "請提供正確的 YouTube 連結。",
"shareVideoTitle": "分享影像",
"shareYourScreen": "分享自己的螢幕",
@@ -300,6 +318,10 @@
"startLiveStreaming": "啟動直播串流",
"startRecording": "啟動錄影作業",
"startRemoteControlErrorMessage": "嘗試啟動遠端控制階段時發生錯誤!",
"shareScreenWarningTitle": "您必須先停止分享音訊才能分享畫面",
"shareScreenWarningH1": "如果您只要分享畫面:",
"shareScreenWarningD1": "您必須先停止分享音訊才能分享畫面。",
"shareScreenWarningD2": "您必須先停止分享音訊,啟動畫面分享,然後勾選 \"分享音訊\" 選項。",
"stopLiveStreaming": "停止直播串流",
"stopRecording": "停止錄影",
"stopRecordingWarning": "確定要停止錄影嗎?",
@@ -316,11 +338,16 @@
"userIdentifier": "使用者標識符",
"userPassword": "用戶密碼",
"videoLink": "影片連結",
"viewUpgradeOptions": "查看升級方案",
"viewUpgradeOptionsContent": "若要無限制地使用進階版功能像是錄影、逐字稿、RTMP 串流...等,您必須升級您的方案。",
"viewUpgradeOptionsTitle": "您找到了進階版功能!",
"WaitForHostMsg": "此會議 <b>{{room}}</b> 尚未啟動。如果您是會議主人,請進行認證;否則,請等待會議主人到達。",
"WaitForHostMsgWOk": "此會議 <b>{{room}}</b> 尚未啟動。如果您是會議主人,請按 [確定] 進行認證;否則,請等待會議主人到達。",
"WaitingForHost": "等侯主辦人...",
"Yes": "是的",
"yourEntireScreen": "您的畫面"
"yourEntireScreen": "您的畫面",
"remoteUserControls": "{{username}} 的遠端使用者控制",
"localUserControls": "本機使用者控制"
},
"dialOut": {
"statusMessage": "現在狀態為 {{status}}"
@@ -341,8 +368,20 @@
"slightBlur": "稍微模糊",
"removeBackground": "移除背景",
"addBackground": "新增背景",
"pleaseWait": "請稍候...",
"none": "無",
"desktopShareError": "無法建立桌面分享"
"uploadedImage": "上傳圖片 {{index}}",
"deleteImage": "刪除圖片",
"image1" : "海灘",
"image2" : "牆壁 (白)",
"image3" : "空房間 (白)",
"image4" : "落地燈 (黑)",
"image5" : "山岳",
"image6" : "森林",
"image7" : "日出",
"desktopShareError": "無法建立桌面分享",
"desktopShare":"桌面分享",
"webAssemblyWarning": "不支援 WebAssembly"
},
"feedback": {
"average": "普通",
@@ -351,7 +390,8 @@
"good": "很好",
"rateExperience": "對本次會議的體驗評分",
"veryBad": "極差",
"veryGood": "極好"
"veryGood": "極好",
"star": "星級"
},
"incomingCall": {
"answer": "接通",
@@ -366,8 +406,9 @@
"cancelPassword": "取消 $t(lockRoomPassword)",
"conferenceURL": "連結:",
"country": "國家",
"dialANumber": "要參加您的會議,撥打以下號碼其中之一,然後輸入 PIN 碼。",
"dialANumber": "要參加您的會議,撥打以下其中一支號碼,然後輸入 PIN 碼。",
"dialInConferenceID": "PIN 號碼:",
"copyNumber":"複製號碼",
"dialInNotSupported": "抱歉,目前不支援電話撥入。",
"dialInNumber": "撥入:",
"dialInSummaryError": "目前解析撥入資訊錯誤。請稍後再試一次。",
@@ -387,6 +428,7 @@
"noRoom": "沒有會議室指定要撥入。",
"numbers": "撥入號碼",
"password": "$t(lockRoomPasswordUppercase)",
"sip": "SIP 位址",
"title": "分享",
"tooltip": "顯示此會議的連結及電話撥入號碼",
"label": "撥入資訊"
@@ -405,6 +447,7 @@
"support": "支援",
"supportMsg": "如果這種狀況持續發生,請聯絡"
},
"jitsiHome": "{{logo}} 商標,首頁連結",
"keyboardShortcuts": {
"focusLocal": "聚焦於自己的影像",
"focusRemote": "聚焦於另一人的影像",
@@ -495,7 +538,8 @@
"disconnected": "已經中斷連接",
"focus": "會議焦點",
"focusFail": "{{component}} 無法使用 - 請在 {{ms}} 秒後重試",
"grantedTo": "主持人權限已授予 {{to}} ",
"grantedTo": "主持人權限已授予 {{to}}",
"hostAskedUnmute": "主持人希望您能解除靜音",
"invitedOneMember": "{{name}} 已受邀請",
"invitedThreePlusMembers": "{{name}} 及 {{count}} 位人員已受邀請",
"invitedTwoMembers": "{{first}} 及 {{second}} 已受邀請",
@@ -516,7 +560,7 @@
"somebody": "某人",
"startSilentTitle": "您加入了會議而無聲音輸出!",
"startSilentDescription": "重新加入會議以啟用語音",
"suboptimalBrowserWarning": "恐怕您本次會議體驗並不太好,我們會想辦法改進的。但在此之前,敬請使用 <a href='{{recommendedBrowserPageLink}}' target='_blank'>完全支援的瀏覽器</a> 。",
"suboptimalBrowserWarning": "恐怕您本次會議體驗並不太好,我們會想辦法改進的。但在此之前,敬請使用<a href='{{recommendedBrowserPageLink}}' target='_blank'>完全支援的瀏覽器</a> 。",
"suboptimalExperienceTitle": "瀏覽器警告",
"unmute": "取消靜音",
"newDeviceCameraTitle": "偵測到新的攝影裝置",
@@ -525,16 +569,34 @@
"OldElectronAPPTitle": "安全漏洞!",
"oldElectronClientDescription1": "您似乎正在使用 Jitsi Meet 客戶端的舊版本,其有已知的安全漏洞。請更新到",
"oldElectronClientDescription2": "最新版本",
"oldElectronClientDescription3": ""
"oldElectronClientDescription3": "",
"moderationInEffectDescription": "若要說話,請舉手",
"moderationInEffectCSDescription": "若要分享視訊,請舉手",
"moderationInEffectVideoDescription": "若要使視訊可見,請舉手",
"moderationInEffectTitle": "麥克風已被管理員靜音",
"moderationInEffectCSTitle": "內容分享已被管理員停用",
"moderationInEffectVideoTitle": "視訊已被管理員靜音",
"moderationRequestFromModerator": "主持人希望您能解除靜音",
"moderationRequestFromParticipant": "想要講話",
"moderationStartedTitle": "開始管理",
"moderationStoppedTitle": "停止管理",
"moderationToggleDescription": "由 {{participantDisplayName}}",
"raiseHandAction": "舉手",
"groupTitle": "通知"
},
"participantsPane": {
"close": "關閉",
"headings": {
"lobby": "大廳 ({{count}})",
"participantsList": "會議參與者 ({{count}})"
},
"actions": {
"allow": "允許參與者能夠:",
"invite": "邀請他人",
"askUnmute": "要求解除靜音",
"muteAll": "靜音所有人",
"startModeration": "將他們解除靜音或開始視訊",
"stopEveryonesVideo": "停止所有人的視訊",
"stopVideo": "停止影片"
}
},
@@ -592,6 +654,7 @@
"or": "或",
"premeeting": "會前",
"showScreen": "啟用會前畫面",
"keyboardShortcuts" : "啟用鍵盤快捷鍵",
"startWithPhone": "使用手機音訊裝置開始",
"screenSharingError": "畫面分享錯誤:",
"videoOnlyError": "視訊錯誤:",
@@ -613,6 +676,7 @@
"ringing": "鈴鈴鈴..."
},
"profile": {
"avatar": "頭像",
"setDisplayNameLabel": "設定您的顯示名稱",
"setEmailInput": "輸入您的電子信箱",
"setEmailLabel": "設定您的 Gravatar 電子信箱",
@@ -627,12 +691,15 @@
"beta": "BETA",
"busy": "我們正在釋放錄影資源。請過幾分鐘後再試。",
"busyTitle": "全部錄影目前忙碌",
"copyLink": "複製連結",
"error": "錄影失敗。請重試。",
"errorFetchingLink": "取得錄影檔連結時發生錯誤。",
"expandedOff": "錄影已經停止",
"expandedOn": "此會議目前正在錄影。",
"expandedPending": "錄影正在啟動...",
"failedToStart": "錄影啟動失敗",
"fileSharingdescription": "分享錄影給會議參與者",
"linkGenerated": "我們建立了您的錄影檔的連結。",
"live": "直播",
"loggedIn": "以 {{userName}} 登入",
"off": "錄影已經停止",
@@ -647,7 +714,8 @@
"signIn": "登入",
"signOut": "登出",
"unavailable": "喔哦!{{serviceName}} 目前無法使用。我們正在解決此問題,請稍後再試。",
"unavailableTitle": "錄影無法使用"
"unavailableTitle": "錄影無法使用",
"uploadToCloud": "上傳至雲端"
},
"sectionList": {
"pullToRefresh": "拉下以重新整理"
@@ -666,8 +734,12 @@
"signedIn": "目前能夠存取 {{email}} 的行事曆事件。點按下方取消連接鈕可以停止存取行事曆事件。",
"title": "行事曆"
},
"desktopShareFramerate": "桌面分享幀數",
"desktopShareWarning": "您必須重新啟動桌面分享以套用新的設定。",
"desktopShareHighFpsWarning": "較高的桌面分享幀數可能會影響您的頻寬。您必須重新啟動桌面分享以套用新的設定。",
"devices": "裝置",
"followMe": "全部人跟隨我",
"framesPerSecond": "幀數",
"language": "語言",
"loggedIn": "以 {{name}} 登入",
"microphones": "麥克風",
@@ -736,6 +808,7 @@
"callQuality": "管理影像品質",
"cc": "切換字幕",
"chat": "切換聊天視窗",
"clap": "鼓掌",
"document": "切換分享文件",
"download": "下載我們的應用程式",
"embedMeeting": "嵌入會議",
@@ -745,7 +818,9 @@
"hangup": "離開通話",
"help": "說明",
"invite": "邀請人員",
"joy": "笑到流淚",
"kick": "踢出參與者",
"like": "比讚",
"lobbyButton": "啟用/停用大廳模式",
"localRecording": "切換本地端錄影控制",
"lockRoom": "切換會議密碼",
@@ -758,10 +833,12 @@
"muteEveryonesVideo": "停用所有人的攝影機",
"muteEveryoneElsesVideo": "停用其他人的攝影機",
"participants": "參與者",
"party": "拉炮",
"pip": "切換子母畫面模式",
"privateMessage": "發送私人訊息",
"profile": "編輯您的簡介",
"raiseHand": "切換舉手",
"reactionsMenu": "切換反應選單",
"recording": "切換錄影",
"remoteMute": "靜音參與者",
"remoteVideoMute": "停用參與者的攝影機",
@@ -773,22 +850,30 @@
"shareYourScreen": "切換螢幕分享",
"shortcuts": "切換快捷鍵",
"show": "顯示在台上",
"smile": "微笑",
"speakerStats": "切換聲音輸出統計",
"surprised": "驚訝",
"tileView": "切換格狀檢視",
"toggleCamera": "切換攝影機",
"toggleFilmstrip": "切換幻燈片",
"videomute": "切換影片靜音",
"selectBackground": "選擇背景"
"videomute": "啟用/停用攝影機",
"videoblur": "切換畫面模糊",
"selectBackground": "選擇背景",
"expand": "展開",
"collapse": "收回"
},
"addPeople": "新增人員到您的通話中",
"audioSettings": "音訊設定",
"videoSettings": "視訊設定",
"audioOnlyOff": "停用低頻寬模式",
"audioOnlyOn": "啟用低頻寬模式",
"audioRoute": "選擇音訊裝置",
"authenticate": "認證",
"callQuality": "管理影像品質",
"chat": "開啟/關閉聊天欄",
"clap": "鼓掌",
"closeChat": "關閉聊天欄",
"closeReactionsMenu": "關閉反應選單",
"documentClose": "關閉分享檔案欄",
"documentOpen": "開啟分享檔案欄",
"download": "下載我們的應用程式",
@@ -802,6 +887,8 @@
"hangup": "離開",
"help": "說明",
"invite": "邀請人員",
"joy": "高興",
"like": "比讚",
"lobbyButtonDisable": "停用大廳模式",
"lobbyButtonEnable": "啟用大廳模式",
"login": "登入",
@@ -819,30 +906,40 @@
"noAudioSignalDialInLinkDesc": "撥入號碼",
"noisyAudioInputTitle": "您的麥克風疑似有雜音!",
"noisyAudioInputDesc": "噪音聽起來是從您的麥克風傳來的,請考慮靜音或更換裝置。",
"openChat": "開啟交談",
"openChat": "開啟聊天欄",
"openReactionsMenu": "開啟反應選單",
"participants": "參與者",
"party": "慶祝",
"pip": "進入子母畫面模式",
"privateMessage": "發送私人訊息",
"profile": "編輯您的簡介",
"raiseHand": "舉手/取消請求發言",
"raiseYourHand": "舉手發言",
"reactionClap": "傳送鼓掌反應",
"reactionJoy": "傳送高興反應",
"reactionLike": "傳送比讚反應",
"reactionParty": "傳送拉炮反應",
"reactionSmile": "傳送微笑反應",
"reactionSurprised": "傳送驚訝反應",
"security": "安全性選項",
"Settings": "設定",
"shareaudio": "分享音訊",
"sharedvideo": "分享影片",
"shareRoom": "邀請他人",
"shortcuts": "查看快捷鍵",
"smile": "微笑",
"speakerStats": "聲音輸出數據",
"startScreenSharing": "啟動螢幕分享",
"startScreenSharing": "啟動畫面分享",
"startSubtitles": "啟動字幕",
"stopScreenSharing": "停止螢幕分享",
"stopAudioSharing": "停止分享音訊",
"stopScreenSharing": "停止分享畫面",
"stopSubtitles": "停止字幕",
"stopSharedVideo": "停止影片",
"surprised": "驚訝",
"talkWhileMutedPopup": "您要發言嗎?目前您處於靜音。",
"tileViewToggle": "切換格狀檢視",
"toggleCamera": "切換攝影機",
"videomute": "啟動/停止攝影機",
"videoSettings": "影像設定",
"selectBackground": "選擇背景"
},
"transcribing": {
@@ -869,6 +966,7 @@
"react-nativeGrantPermissions": "當瀏覽器要求權限時,請選擇<b><i>允許</i></b>。",
"safariGrantPermissions": "當瀏覽器要求權限時,請選擇<b><i>確定</i></b>。"
},
"volumeSlider": "音量滑條",
"videoSIPGW": {
"busy": "我們正在釋放資源。請過幾分鐘後再試。",
"busyTitle": "會議室服務目前忙碌中",
@@ -913,6 +1011,7 @@
"videomute": "參與者已經停止攝影裝置"
},
"welcomepage": {
"addMeetingName": "新增會議名稱",
"accessibilityLabel": {
"join": "點擊即可加入",
"roomname": "輸入會議室名稱"
@@ -934,6 +1033,9 @@
"info": "資訊",
"join": "建立 / 加入",
"jitsiOnMobile": "Jitsi 手機應用程式 下載應用程式,不論何時何地都能發起會議",
"mobileDownLoadLinkIos": "下載 iOS 版本的手機應用程式",
"mobileDownLoadLinkAndroid": "下載 Android 版本的手機應用程式",
"mobileDownLoadLinkFDroid": "前往 F-Droid 下載 Android 版本的手機應用程式",
"moderatedMessage": "或 <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">預先建立會議</a>,您將成為唯一一個管理員。",
"privacy": "隱私",
"recentList": "最近使用",
@@ -946,7 +1048,15 @@
"sendFeedback": "傳送回饋",
"startMeeting": "開始會議",
"terms": "條款",
"title": "安全、功能齊全、完全免費的視訊會議"
"title": "安全、功能齊全、完全免費的視訊會議",
"logo":{
"calendar":"行事曆圖示",
"microsoftLogo":"Microsoft 商標",
"logoDeepLinking":"Jitsi meet 商標",
"desktopPreviewThumbnail":"桌面分享縮圖",
"googleLogo":"Google 商標",
"policyLogo":"政策圖示"
}
},
"lonelyMeetingExperience": {
"button": "邀請其他人",

View File

@@ -258,10 +258,12 @@
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantButton": "Mute",
"muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantsVideoDialog": "Are you sure you want to turn off this participant's camera? You won't be able to turn the camera back on, but they can turn it back on at any time.",
"muteParticipantTitle": "Mute this participant?",
"muteParticipantsVideoButton": "Disable camera",
"muteParticipantsVideoTitle": "Disable camera of this participant?",
"muteParticipantsVideoBody": "You won't be able to turn the camera back on, but they can turn it back on at any time.",
"noDropboxToken": "No valid Dropbox token",
"Ok": "OK",
"password": "Password",
"passwordLabel": "The meeting has been locked by a participant. Please enter the $t(lockRoomPassword) to join.",
@@ -311,7 +313,7 @@
"shareAudioWarningD1": "you need to stop screen sharing before sharing your audio.",
"shareAudioWarningD2": "you need to restart your screen sharing and check the \"share audio\" option.",
"shareMediaWarningGenericH2": "If you want to share your screen and audio",
"shareVideoLinkError": "Please provide a correct youtube link.",
"shareVideoLinkError": "Please provide a correct video link.",
"shareVideoTitle": "Share video",
"shareYourScreen": "Share your screen",
"shareYourScreenDisabled": "Screen sharing disabled.",
@@ -418,6 +420,10 @@
"invitePhone": "To join by phone instead, tap this: {{number}},,{{conferenceID}}#\n",
"invitePhoneAlternatives": "Looking for a different dial-in number?\nSee meeting dial-in numbers: {{url}}\n\n\nIf also dialing-in through a room phone, join without connecting to audio: {{silentUrl}}",
"inviteSipEndpoint": "To join using the SIP address, enter this: {{sipUri}}",
"inviteTextiOSPersonal": "{{name}} is inviting you to a meeting.",
"inviteTextiOSJoinSilent": "If you are dialing-in through a room phone, use this link to join without connecting to audio: {{silentUrl}}.",
"inviteTextiOSInviteUrl": "Click the following link to join: {{inviteUrl}}.",
"inviteTextiOSPhone": "To join via phone, use this number: {{number}},,{{conferenceID}}#. If you are looking for a different number, this is the full list: {{didUrl}}.",
"inviteURLFirstPartGeneral": "You are invited to join a meeting.",
"inviteURLFirstPartPersonal": "{{name}} is inviting you to a meeting.\n",
"inviteURLSecondPart": "\nJoin the meeting:\n{{url}}\n",
@@ -586,18 +592,24 @@
},
"participantsPane": {
"close": "Close",
"header": "Participants",
"headings": {
"lobby": "Lobby ({{count}})",
"participantsList": "Meeting participants ({{count}})"
"participantsList": "Meeting participants ({{count}})",
"waitingLobby": "Waiting in lobby ({{count}})"
},
"actions": {
"allow": "Allow attendees to:",
"blockEveryoneMicCamera": "Block everyone's mic and camera",
"invite": "Invite Someone",
"askUnmute": "Ask to unmute",
"mute": "Mute",
"muteAll": "Mute all",
"muteEveryoneElse": "Mute everyone else",
"startModeration": "Unmute themselves or start video",
"stopEveryonesVideo": "Stop everyone's video",
"stopVideo": "Stop video"
"stopVideo": "Stop video",
"unblockEveryoneMicCamera": "Unblock everyone's mic and camera"
}
},
"passwordSetRemotely": "Set by another participant",
@@ -740,6 +752,7 @@
"devices": "Devices",
"followMe": "Everyone follows me",
"framesPerSecond": "frames-per-second",
"incomingMessage": "Incoming message",
"language": "Language",
"loggedIn": "Logged in as {{name}}",
"microphones": "Microphones",
@@ -747,13 +760,18 @@
"more": "More",
"name": "Name",
"noDevice": "None",
"participantJoined": "Participant Joined",
"participantLeft": "Participant Left",
"playSounds": "Play sound on",
"sameAsSystem": "Same as system ({{label}})",
"selectAudioOutput": "Audio output",
"selectCamera": "Camera",
"selectMic": "Microphone",
"sounds": "Sounds",
"speakers": "Speakers",
"startAudioMuted": "Everyone starts muted",
"startVideoMuted": "Everyone starts hidden",
"talkWhileMuted": "Talk while muted",
"title": "Settings"
},
"settingsView": {
@@ -805,9 +823,11 @@
"accessibilityLabel": {
"audioOnly": "Toggle audio only",
"audioRoute": "Select the sound device",
"boo": "Boo",
"callQuality": "Manage video quality",
"cc": "Toggle subtitles",
"chat": "Open / Close chat",
"clap": "Clap",
"document": "Toggle shared document",
"download": "Download our apps",
"embedMeeting": "Embed meeting",
@@ -818,6 +838,8 @@
"help": "Help",
"invite": "Invite people",
"kick": "Kick participant",
"laugh": "Laugh",
"like": "Thumbs Up",
"lobbyButton": "Enable/disable lobby mode",
"localRecording": "Toggle local recording controls",
"lockRoom": "Toggle meeting password",
@@ -830,10 +852,12 @@
"muteEveryonesVideo": "Disable everyone's camera",
"muteEveryoneElsesVideo": "Disable everyone else's camera",
"participants": "Participants",
"party": "Party Popper",
"pip": "Toggle Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
"raiseHand": "Raise / Lower your hand",
"reactionsMenu": "Open / Close reactions menu",
"recording": "Toggle recording",
"remoteMute": "Mute participant",
"remoteVideoMute": "Disable camera of participant",
@@ -846,6 +870,7 @@
"shortcuts": "Toggle shortcuts",
"show": "Show on stage",
"speakerStats": "Toggle speaker statistics",
"surprised": "Surprised",
"tileView": "Toggle tile view",
"toggleCamera": "Toggle camera",
"toggleFilmstrip": "Toggle filmstrip",
@@ -862,9 +887,12 @@
"audioOnlyOn": "Enable low bandwidth mode",
"audioRoute": "Select the sound device",
"authenticate": "Authenticate",
"boo": "Boo",
"callQuality": "Manage video quality",
"chat": "Open / Close chat",
"clap": "Clap",
"closeChat": "Close chat",
"closeReactionsMenu": "Close reactions menu",
"documentClose": "Close shared document",
"documentOpen": "Open shared document",
"download": "Download our apps",
@@ -878,6 +906,8 @@
"hangup": "Leave the meeting",
"help": "Help",
"invite": "Invite people",
"laugh": "Laugh",
"like": "Thumbs Up",
"lobbyButtonDisable": "Disable lobby mode",
"lobbyButtonEnable": "Enable lobby mode",
"login": "Login",
@@ -896,12 +926,20 @@
"noisyAudioInputTitle": "Your microphone appears to be noisy!",
"noisyAudioInputDesc": "It sounds like your microphone is making noise, please consider muting or changing the device.",
"openChat": "Open chat",
"openReactionsMenu": "Open reactions menu",
"participants": "Participants",
"party": "Celebration",
"pip": "Enter Picture-in-Picture mode",
"privateMessage": "Send private message",
"profile": "Edit your profile",
"raiseHand": "Raise / Lower your hand",
"raiseYourHand": "Raise your hand",
"reactionBoo": "Send boo reaction",
"reactionClap": "Send clap reaction",
"reactionLaugh": "Send laugh reaction",
"reactionLike": "Send thumbs up reaction",
"reactionParty": "Send party popper reaction",
"reactionSurprised": "Send surprised reaction",
"security": "Security options",
"Settings": "Settings",
"shareaudio": "Share audio",
@@ -915,6 +953,7 @@
"stopScreenSharing": "Stop screen sharing",
"stopSubtitles": "Stop subtitles",
"stopSharedVideo": "Stop video",
"surprised": "Surprised",
"talkWhileMutedPopup": "Trying to speak? You are muted.",
"tileViewToggle": "Toggle tile view",
"toggleCamera": "Toggle camera",
@@ -1077,6 +1116,7 @@
"passwordField": "Enter meeting password",
"passwordJoinButton": "Join",
"reject": "Reject",
"rejectAll": "Reject all",
"toggleLabel": "Enable lobby"
}
}

View File

@@ -12,6 +12,9 @@ var loggingConfig = {
// {@link #defaultLogLevel}:
'modules/RTC/TraceablePeerConnection.js': 'info',
'modules/statistics/CallStats.js': 'info',
'modules/sdp/SDPUtil.js': 'info',
'modules/xmpp/JingleSessionPC.js': 'info',
'modules/xmpp/strophe.jingle.js': 'info',
'modules/xmpp/strophe.util.js': 'log'
};

View File

@@ -23,7 +23,8 @@ import {
getParticipantById,
pinParticipant,
kickParticipant,
raiseHand
raiseHand,
isParticipantModerator
} from '../../react/features/base/participants';
import { updateSettings } from '../../react/features/base/settings';
import { isToggleCameraEnabled, toggleCamera } from '../../react/features/base/tracks';
@@ -105,13 +106,14 @@ function initCommands() {
const muteMediaType = mediaType ? mediaType : MEDIA_TYPE.AUDIO;
sendAnalytics(createApiEvent('muted-everyone'));
const participants = APP.store.getState()['features/base/participants'];
const localIds = participants
.filter(participant => participant.local)
.filter(participant => participant.role === 'moderator')
.map(participant => participant.id);
const localParticipant = getLocalParticipant(APP.store.getState());
const exclude = [];
APP.store.dispatch(muteAllParticipants(localIds, muteMediaType));
if (localParticipant && isParticipantModerator(localParticipant)) {
exclude.push(localParticipant.id);
}
APP.store.dispatch(muteAllParticipants(exclude, muteMediaType));
},
'toggle-lobby': isLobbyEnabled => {
APP.store.dispatch(toggleLobbyMode(isLobbyEnabled));
@@ -572,6 +574,12 @@ function initCommands() {
});
break;
}
case 'get-custom-avatar-backgrounds' : {
callback({
avatarBackgrounds: APP.store.getState()['features/dynamic-branding'].avatarBackgrounds
});
break;
}
default:
return false;
}

View File

@@ -15,3 +15,8 @@ export const API_ID = parseURLParams(window.location).jitsi_meet_external_api_id
* The payload name for the datachannel/endpoint text message event
*/
export const ENDPOINT_TEXT_MESSAGE_NAME = 'endpoint-text-message';
/**
* The payload name for the datachannel/endpoint reaction event
*/
export const ENDPOINT_REACTION_NAME = 'endpoint-reaction';

View File

@@ -170,7 +170,7 @@ function parseArguments(args) {
switch (typeof firstArg) {
case 'string': // old arguments format
case undefined: {
case 'undefined': {
// Not sure which format but we are trying to parse the old
// format because if the new format is used everything will be undefined
// anyway.
@@ -769,6 +769,17 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return getCurrentDevices(this._transport);
}
/**
* Returns any custom avatars backgrounds.
*
* @returns {Promise} - Resolves with the list of custom avatar backgrounds.
*/
getCustomAvatarBackgrounds() {
return this._transport.sendRequest({
name: 'get-custom-avatar-backgrounds'
});
}
/**
* Returns the current livestream url.
*

View File

@@ -142,20 +142,23 @@ const KeyboardShortcut = {
* @param exec the function to be executed when the shortcut is pressed
* @param helpDescription the description of the shortcut that would appear
* in the help menu
* @param altKey whether or not the alt key must be pressed.
*/
registerShortcut(// eslint-disable-line max-params
shortcutChar,
shortcutAttr,
exec,
helpDescription) {
_shortcuts.set(shortcutChar, {
helpDescription,
altKey = false) {
_shortcuts.set(altKey ? `:${shortcutChar}` : shortcutChar, {
character: shortcutChar,
function: exec,
shortcutAttr
shortcutAttr,
altKey
});
if (helpDescription) {
this._addShortcutToHelp(shortcutChar, helpDescription);
this._addShortcutToHelp(altKey ? `:${shortcutChar}` : shortcutChar, helpDescription);
}
},
@@ -164,9 +167,10 @@ const KeyboardShortcut = {
*
* @param shortcutChar unregisters the given shortcut, which means it will
* no longer be usable
* @param altKey whether or not shortcut is combo with alt key
*/
unregisterShortcut(shortcutChar) {
_shortcuts.delete(shortcutChar);
unregisterShortcut(shortcutChar, altKey = false) {
_shortcuts.delete(altKey ? `:${shortcutChar}` : shortcutChar);
_shortcutsHelp.delete(shortcutChar);
},
@@ -175,6 +179,15 @@ const KeyboardShortcut = {
* @returns {string} e.key or something close if not supported
*/
_getKeyboardKey(e) {
// If alt is pressed a different char can be returned so this takes
// the char from the code. It also prefixes with a colon to differentiate
// alt combo from simple keypress.
if (e.altKey) {
const key = e.code.replace('Key', '');
return `:${key}`;
}
// If e.key is a string, then it is assumed it already plainly states
// the key pressed. This may not be true in all cases, such as with Edge
// and "?", when the browser cannot properly map a key press event to a

66
package-lock.json generated
View File

@@ -2913,9 +2913,9 @@
"integrity": "sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw=="
},
"@react-native-async-storage/async-storage": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.13.2.tgz",
"integrity": "sha512-isTDvUApRJPVWFxV15yrQSOGqarX7cIedq/y4N5yWSnotf68D9qvDEv1I7rCXhkBDi0u4OJt6GA9dksUT0D3wg==",
"version": "1.15.5",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.15.5.tgz",
"integrity": "sha512-4AYehLH39B9a8UXCMf3ieOK+G61wGMP72ikx6/XSMA0DUnvx0PgaeaT2Wyt06kTrDTy8edewKnbrbeqwaM50TQ==",
"requires": {
"deep-assign": "^3.0.0"
}
@@ -3102,6 +3102,11 @@
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.1.5.tgz",
"integrity": "sha512-lagdZr9UiVAccNXYfTEj+aUcPCx9ykbMe9puffeIyF3JsRuMmlu3BjHYx1klUHX7wNRmFNC8qVP0puxUt1sZ0A=="
},
"@react-native-community/slider": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@react-native-community/slider/-/slider-3.0.3.tgz",
"integrity": "sha512-8IeHfDwJ9/CTUwFs6x90VlobV3BfuPgNLjTgC6dRZovfCWigaZwVNIFFJnHBakK3pW2xErAPwhdvNR4JeNoYbw=="
},
"@svgr/babel-plugin-add-jsx-attribute": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-4.2.0.tgz",
@@ -7534,6 +7539,11 @@
}
}
},
"eme-encryption-scheme-polyfill": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.0.3.tgz",
"integrity": "sha512-44CNFMsqzHdKHrzWxlS7xZ8KUHn5XutBqpmCuWzNIynmAyFInHrrD3ozv/RvK9ZhgV6QY6Easx8EWAmxteNodg=="
},
"emoji-regex": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz",
@@ -8239,7 +8249,8 @@
"events": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
"integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg=="
"integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==",
"dev": true
},
"eventsource": {
"version": "1.0.7",
@@ -11011,6 +11022,11 @@
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-2.2.0.tgz",
"integrity": "sha1-fYa9VmefWM5qhHBKZX3TkruoGnk="
},
"keymirror": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/keymirror/-/keymirror-0.1.1.tgz",
"integrity": "sha1-kYiJ6hP40KQufFVyUO7nE63JXDU="
},
"killable": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
@@ -11071,8 +11087,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#b43a9fa0eea73ba48d805fba97d0cd3fb9d8d07a",
"from": "github:jitsi/lib-jitsi-meet#b43a9fa0eea73ba48d805fba97d0cd3fb9d8d07a",
"version": "github:jitsi/lib-jitsi-meet#6a3df11ffa7a2204b579326e23cdaa85a79521d1",
"from": "github:jitsi/lib-jitsi-meet#6a3df11ffa7a2204b579326e23cdaa85a79521d1",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "github:jitsi/sdp-interop#5fc4af6dcf8a6e6af9fedbcd654412fd47b1b4ae",
@@ -15138,14 +15154,25 @@
"whatwg-url-without-unicode": "8.0.0-3"
}
},
"react-native-video": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-5.1.1.tgz",
"integrity": "sha512-zee8gRUrjPWRoZSEBiMebClqu1iAuCQNLjzqpmXFrRWEoJj7azM3BPqLQWJgsnfLiYUYGySeApC/G60THM5+tw==",
"requires": {
"keymirror": "^0.1.1",
"prop-types": "^15.7.2",
"shaka-player": "^2.5.9"
}
},
"react-native-watch-connectivity": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/react-native-watch-connectivity/-/react-native-watch-connectivity-0.4.3.tgz",
"integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
},
"react-native-webrtc": {
"version": "github:react-native-webrtc/react-native-webrtc#510d20dd62c1768885a98f36fde83f9e48a723fa",
"from": "github:react-native-webrtc/react-native-webrtc#510d20dd62c1768885a98f36fde83f9e48a723fa",
"version": "1.92.0",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.92.0.tgz",
"integrity": "sha512-nztKQ/SmO1DgA3QWCDHHK8ZVDf+5rLbmH42Ukoqnld7ut8/ehmFZXc17aSV/BN0H60jPigGqAMYopt/LZak7Sg==",
"requires": {
"base64-js": "^1.1.2",
"cross-os": "^1.3.0",
@@ -15186,11 +15213,18 @@
}
},
"react-native-youtube-iframe": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-1.2.3.tgz",
"integrity": "sha512-3O8OFJyohGNlYX4D97aWfLLlhEHhlLHDCLgXM+SsQBwP9r1oLnKgXWoy1gce+Vr8qgrqeQgmx1ki+10AAd4KWQ==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-native-youtube-iframe/-/react-native-youtube-iframe-2.1.1.tgz",
"integrity": "sha512-vnLzA5zcnMwa1gMqGfvkjaE82NW1Nd2Up4Q1OUz6IKm69xSG/9/m4APZ5fCN8UMhy6lH95iagd497J7jwEwz3w==",
"requires": {
"events": "^3.0.0"
"events": "^3.2.0"
},
"dependencies": {
"events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
}
}
},
"react-node-resolver": {
@@ -16064,6 +16098,14 @@
"safe-buffer": "^5.0.1"
}
},
"shaka-player": {
"version": "2.5.22",
"resolved": "https://registry.npmjs.org/shaka-player/-/shaka-player-2.5.22.tgz",
"integrity": "sha512-PAoeNLUQ/hT/9dY7QvNFgIiDtXSqbYVFuXXtLHh7ytVVqTvI/p4HLwfYShiR+sE/sbsDOr9D5l9D/ztLPhxgtw==",
"requires": {
"eme-encryption-scheme-polyfill": "^2.0.1"
}
},
"shallow-clone": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz",

View File

@@ -36,9 +36,10 @@
"@jitsi/js-utils": "1.0.6",
"@material-ui/core": "4.11.3",
"@microsoft/microsoft-graph-client": "1.1.0",
"@react-native-async-storage/async-storage": "1.13.2",
"@react-native-async-storage/async-storage": "1.15.5",
"@react-native-community/google-signin": "3.0.1",
"@react-native-community/netinfo": "4.1.5",
"@react-native-community/slider": "3.0.3",
"@svgr/webpack": "4.3.2",
"amplitude-js": "8.2.1",
"base64-js": "1.3.1",
@@ -55,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#b43a9fa0eea73ba48d805fba97d0cd3fb9d8d07a",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#6a3df11ffa7a2204b579326e23cdaa85a79521d1",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",
@@ -86,10 +87,11 @@
"react-native-svg": "12.1.0",
"react-native-svg-transformer": "0.14.3",
"react-native-url-polyfill": "1.2.0",
"react-native-video": "5.1.1",
"react-native-watch-connectivity": "0.4.3",
"react-native-webrtc": "github:react-native-webrtc/react-native-webrtc#510d20dd62c1768885a98f36fde83f9e48a723fa",
"react-native-webrtc": "1.92.0",
"react-native-webview": "11.0.2",
"react-native-youtube-iframe": "1.2.3",
"react-native-youtube-iframe": "2.1.1",
"react-redux": "7.1.0",
"react-textarea-autosize": "8.3.0",
"react-transition-group": "2.4.0",

View File

@@ -21,6 +21,7 @@ const TOOLBAR_TIMEOUT = 4000;
*/
type State = {
avatarURL: string,
customAvatarBackgrounds: Array<string>,
displayName: string,
formattedDisplayName: string,
isVideoDisplayed: boolean,
@@ -48,6 +49,7 @@ export default class AlwaysOnTop extends Component<*, State> {
this.state = {
avatarURL: '',
customAvatarBackgrounds: [],
displayName: '',
formattedDisplayName: '',
isVideoDisplayed: true,
@@ -178,7 +180,14 @@ export default class AlwaysOnTop extends Component<*, State> {
* @returns {ReactElement}
*/
_renderVideoNotAvailableScreen() {
const { avatarURL, displayName, formattedDisplayName, isVideoDisplayed, userID } = this.state;
const {
avatarURL,
customAvatarBackgrounds,
displayName,
formattedDisplayName,
isVideoDisplayed,
userID
} = this.state;
if (isVideoDisplayed) {
return null;
@@ -188,7 +197,7 @@ export default class AlwaysOnTop extends Component<*, State> {
<div id = 'videoNotAvailableScreen'>
<div id = 'avatarContainer'>
<StatelessAvatar
color = { getAvatarColor(userID) }
color = { getAvatarColor(userID, customAvatarBackgrounds) }
id = 'avatar'
initials = { getInitials(displayName) }
url = { avatarURL } />)
@@ -218,6 +227,12 @@ export default class AlwaysOnTop extends Component<*, State> {
window.addEventListener('mousemove', this._mouseMove);
this._hideToolbarAfterTimeout();
api.getCustomAvatarBackgrounds()
.then(res =>
this.setState({
customAvatarBackgrounds: res.avatarBackgrounds || []
}))
.catch(console.error);
}
/**

View File

@@ -795,6 +795,23 @@ export function createToolbarEvent(buttonName, attributes = {}) {
};
}
/**
* Creates an event associated with a reaction button being clicked/pressed.
*
* @param {string} buttonName - The identifier of the reaction button which was
* clicked/pressed.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createReactionMenuEvent(buttonName) {
return {
action: 'clicked',
actionSubject: buttonName,
source: 'reaction.button',
type: TYPE_UI
};
}
/**
* Creates an event which indicates that a local track was muted.
*

View File

@@ -24,7 +24,7 @@ import {
parseURIString,
toURLString
} from '../base/util';
import { isVpaasMeeting } from '../billing-counter/functions';
import { isVpaasMeeting } from '../jaas/functions';
import { clearNotifications, showNotification } from '../notifications';
import { setFatalError } from '../overlay';

View File

@@ -11,7 +11,6 @@ import { getFeatureFlag } from '../../base/flags/functions';
import { Platform } from '../../base/react';
import { DimensionsDetector, clientResized } from '../../base/responsive-ui';
import { updateSettings } from '../../base/settings';
import JitsiThemePaperProvider from '../../base/ui/components/JitsiThemeProvider.native';
import logger from '../logger';
import { AbstractApp } from './AbstractApp';
@@ -128,12 +127,10 @@ export class App extends AbstractApp {
*/
_createMainElement(component, props) {
return (
<JitsiThemePaperProvider>
<DimensionsDetector
onDimensionsChanged = { this._onDimensionsChanged }>
{ super._createMainElement(component, props) }
</DimensionsDetector>
</JitsiThemePaperProvider>
<DimensionsDetector
onDimensionsChanged = { this._onDimensionsChanged }>
{ super._createMainElement(component, props) }
</DimensionsDetector>
);
}

View File

@@ -18,7 +18,6 @@ import '../base/sounds/middleware';
import '../base/testing/middleware';
import '../base/tracks/middleware';
import '../base/user-interaction/middleware';
import '../billing-counter/middleware';
import '../calendar-sync/middleware';
import '../chat/middleware';
import '../conference/middleware';
@@ -35,6 +34,7 @@ import '../large-video/middleware';
import '../lobby/middleware';
import '../notifications/middleware';
import '../overlay/middleware';
import '../reactions/middleware';
import '../recent-list/middleware';
import '../recording/middleware';
import '../rejoin/middleware';

View File

@@ -25,7 +25,6 @@ import '../base/sounds/reducer';
import '../base/testing/reducer';
import '../base/tracks/reducer';
import '../base/user-interaction/reducer';
import '../billing-counter/reducer';
import '../calendar-sync/reducer';
import '../chat/reducer';
import '../deep-linking/reducer';
@@ -41,6 +40,8 @@ import '../large-video/reducer';
import '../lobby/reducer';
import '../notifications/reducer';
import '../overlay/reducer';
import '../participants-pane/reducer';
import '../reactions/reducer';
import '../recent-list/reducer';
import '../recording/reducer';
import '../settings/reducer';

View File

@@ -49,24 +49,24 @@ export const disableModeration = (mediaType: MediaType, actor: Object) => {
/**
* Hides the notification with the participant that asked to unmute audio.
*
* @param {string} id - The participant id.
* @param {Object} participant - The participant for which the notification to be hidden.
* @returns {Object}
*/
export function dismissPendingAudioParticipant(id: string) {
return dismissPendingParticipant(id, MEDIA_TYPE.AUDIO);
export function dismissPendingAudioParticipant(participant: Object) {
return dismissPendingParticipant(participant, MEDIA_TYPE.AUDIO);
}
/**
* Hides the notification with the participant that asked to unmute.
*
* @param {string} id - The participant id.
* @param {Object} participant - The participant for which the notification to be hidden.
* @param {MediaType} mediaType - The media type.
* @returns {Object}
*/
export function dismissPendingParticipant(id: string, mediaType: MediaType) {
export function dismissPendingParticipant(participant: Object, mediaType: MediaType) {
return {
type: DISMISS_PENDING_PARTICIPANT,
id,
participant,
mediaType
};
}
@@ -145,13 +145,13 @@ export function showModeratedNotification(mediaType: MediaType) {
/**
* Shows a notification with the participant that asked to audio unmute.
*
* @param {string} id - The participant id.
* @param {Object} participant - The participant for which is the notification.
* @returns {Object}
*/
export function participantPendingAudio(id: string) {
export function participantPendingAudio(participant: Object) {
return {
type: PARTICIPANT_PENDING_AUDIO,
id
participant
};
}

View File

@@ -3,7 +3,10 @@ import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import NotificationWithParticipants from '../../notifications/components/web/NotificationWithParticipants';
import { approveAudio, dismissPendingAudioParticipant } from '../actions';
import {
approveParticipant,
dismissPendingAudioParticipant
} from '../actions';
import { getParticipantsAskingToAudioUnmute } from '../functions';
@@ -25,7 +28,7 @@ export default function() {
</div>
<NotificationWithParticipants
approveButtonText = { t('notify.unmute') }
onApprove = { approveAudio }
onApprove = { approveParticipant }
onReject = { dismissPendingAudioParticipant }
participants = { participants }
rejectButtonText = { t('dialog.dismiss') }

View File

@@ -1,7 +1,7 @@
// @flow
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
import { getParticipantById, isLocalParticipantModerator } from '../base/participants/functions';
import { isLocalParticipantModerator } from '../base/participants/functions';
import { MEDIA_TYPE_TO_WHITELIST_STORE_KEY, MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
@@ -13,6 +13,14 @@ import { MEDIA_TYPE_TO_WHITELIST_STORE_KEY, MEDIA_TYPE_TO_PENDING_STORE_KEY } fr
*/
const getState = state => state['features/av-moderation'];
/**
* We use to construct once the empty array so we can keep the same instance between calls
* of getParticipantsAskingToAudioUnmute.
*
* @type {*[]}
*/
const EMPTY_ARRAY = [];
/**
* Returns whether moderation is enabled per media type.
*
@@ -33,6 +41,17 @@ export const isEnabledFromState = (mediaType: MediaType, state: Object) =>
*/
export const isEnabled = (mediaType: MediaType) => (state: Object) => isEnabledFromState(mediaType, state);
/**
* Returns whether moderation is supported by the backend.
*
* @returns {null|boolean}
*/
export const isSupported = () => (state: Object) => {
const { conference } = state['features/base/conference'];
return conference ? conference.isAVModerationSupported() : false;
};
/**
* Returns whether local participant is approved to unmute a media type.
*
@@ -74,15 +93,15 @@ export const isParticipantApproved = (id: string, mediaType: MediaType) => (stat
/**
* Returns a selector creator which determines if the participant is pending or not for a media type.
*
* @param {string} id - The participant id.
* @param {Participant} participant - The participant.
* @param {MEDIA_TYPE} mediaType - The media type to check.
* @returns {boolean}
*/
export const isParticipantPending = (id: string, mediaType: MediaType) => (state: Object) => {
export const isParticipantPending = (participant: Object, mediaType: MediaType) => (state: Object) => {
const storeKey = MEDIA_TYPE_TO_PENDING_STORE_KEY[mediaType];
const arr = getState(state)[storeKey];
return Boolean(arr.find(pending => pending === id));
return Boolean(arr.find(pending => pending.id === participant.id));
};
/**
@@ -94,12 +113,10 @@ export const isParticipantPending = (id: string, mediaType: MediaType) => (state
*/
export const getParticipantsAskingToAudioUnmute = (state: Object) => {
if (isLocalParticipantModerator(state)) {
const ids = getState(state).pendingAudio;
return ids.map(id => getParticipantById(state, id)).filter(Boolean);
return getState(state).pendingAudio;
}
return [];
return EMPTY_ARRAY;
};
/**

View File

@@ -127,14 +127,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
// this is handled only by moderators
if (audioModerationEnabled && isLocalParticipantModerator(state)) {
const { participant: { id, raisedHand } } = action;
const participant = action.participant;
if (raisedHand) {
if (participant.raisedHand) {
// if participant raises hand show notification
!isParticipantApproved(id, MEDIA_TYPE.AUDIO)(state) && dispatch(participantPendingAudio(id));
!isParticipantApproved(participant.id, MEDIA_TYPE.AUDIO)(state)
&& dispatch(participantPendingAudio(participant));
} else {
// if participant lowers hand hide notification
isParticipantPending(id, MEDIA_TYPE.AUDIO)(state) && dispatch(dismissPendingAudioParticipant(id));
isParticipantPending(participant, MEDIA_TYPE.AUDIO)(state)
&& dispatch(dismissPendingAudioParticipant(participant));
}
}

View File

@@ -1,6 +1,11 @@
/* @flow */
import { MEDIA_TYPE } from '../base/media/constants';
import type { MediaType } from '../base/media/constants';
import {
PARTICIPANT_LEFT,
PARTICIPANT_UPDATED
} from '../base/participants';
import { ReducerRegistry } from '../base/redux';
import {
@@ -11,6 +16,7 @@ import {
PARTICIPANT_APPROVED,
PARTICIPANT_PENDING_AUDIO
} from './actionTypes';
import { MEDIA_TYPE_TO_PENDING_STORE_KEY } from './constants';
const initialState = {
audioModerationEnabled: false,
@@ -21,6 +27,41 @@ const initialState = {
pendingVideo: []
};
/**
Updates a participant in the state for the specified media type.
*
* @param {MediaType} mediaType - The media type.
* @param {Object} participant - Information about participant to be modified.
* @param {Object} state - The current state.
* @private
* @returns {boolean} - Whether state instance was modified.
*/
function _updatePendingParticipant(mediaType: MediaType, participant, state: Object = {}) {
let arrayItemChanged = false;
const storeKey = MEDIA_TYPE_TO_PENDING_STORE_KEY[mediaType];
const arr = state[storeKey];
const newArr = arr.map(pending => {
if (pending.id === participant.id) {
arrayItemChanged = true;
return {
...pending,
...participant
};
}
return pending;
});
if (arrayItemChanged) {
state[storeKey] = newArr;
return true;
}
return false;
}
ReducerRegistry.register('features/av-moderation', (state = initialState, action) => {
switch (action.type) {
@@ -65,13 +106,13 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
}
case PARTICIPANT_PENDING_AUDIO: {
const { id } = action;
const { participant } = action;
// Add participant to pendigAudio array only if it's not already added
if (!state.pendingAudio.find(pending => pending === id)) {
// Add participant to pendingAudio array only if it's not already added
if (!state.pendingAudio.find(pending => pending.id === participant.id)) {
const updated = [ ...state.pendingAudio ];
updated.push(id);
updated.push(participant);
return {
...state,
@@ -82,20 +123,79 @@ ReducerRegistry.register('features/av-moderation', (state = initialState, action
return state;
}
case PARTICIPANT_UPDATED: {
const participant = action.participant;
const { audioModerationEnabled, videoModerationEnabled } = state;
let hasStateChanged = false;
// skips changing the reference of pendingAudio or pendingVideo,
// if there is no change in the elements
if (audioModerationEnabled) {
hasStateChanged = _updatePendingParticipant(MEDIA_TYPE.AUDIO, participant, state);
}
if (videoModerationEnabled) {
hasStateChanged = _updatePendingParticipant(MEDIA_TYPE.VIDEO, participant, state);
}
// If the state has changed we need to return a new object reference in order to trigger subscriber updates.
if (hasStateChanged) {
return {
...state
};
}
return state;
}
case PARTICIPANT_LEFT: {
const participant = action.participant;
const { audioModerationEnabled, videoModerationEnabled } = state;
let hasStateChanged = false;
// skips changing the reference of pendingAudio or pendingVideo,
// if there is no change in the elements
if (audioModerationEnabled) {
const newPendingAudio = state.pendingAudio.filter(pending => pending.id !== participant.id);
if (state.pendingAudio.length !== newPendingAudio.length) {
state.pendingAudio = newPendingAudio;
hasStateChanged = true;
}
}
if (videoModerationEnabled) {
const newPendingVideo = state.pendingVideo.filter(pending => pending.id !== participant.id);
if (state.pendingVideo.length !== newPendingVideo.length) {
state.pendingVideo = newPendingVideo;
hasStateChanged = true;
}
}
// If the state has changed we need to return a new object reference in order to trigger subscriber updates.
if (hasStateChanged) {
return {
...state
};
}
return state;
}
case DISMISS_PENDING_PARTICIPANT: {
const { id, mediaType } = action;
const { participant, mediaType } = action;
if (mediaType === MEDIA_TYPE.AUDIO) {
return {
...state,
pendingAudio: state.pendingAudio.filter(pending => pending !== id)
pendingAudio: state.pendingAudio.filter(pending => pending.id !== participant.id)
};
}
if (mediaType === MEDIA_TYPE.VIDEO) {
return {
...state,
pendingAudio: state.pendingVideo.filter(pending => pending !== id)
pendingVideo: state.pendingVideo.filter(pending => pending.id !== participant.id)
};
}

View File

@@ -10,6 +10,11 @@ import { StatelessAvatar } from '.';
export type Props = {
/**
* Custom avatar backgrounds from branding.
*/
_customAvatarBackgrounds: Array<string>,
/**
* The string we base the initials on (this is generated from a list of precedences).
*/
@@ -133,6 +138,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
*/
render() {
const {
_customAvatarBackgrounds,
_initialsBase,
_loadableAvatarUrl,
className,
@@ -172,7 +178,7 @@ class Avatar<P: Props> extends PureComponent<P, State> {
if (initials) {
if (dynamicColor) {
avatarProps.color = getAvatarColor(colorBase || _initialsBase);
avatarProps.color = getAvatarColor(colorBase || _initialsBase, _customAvatarBackgrounds);
}
avatarProps.initials = initials;
@@ -211,6 +217,7 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
const _initialsBase = _participant?.name ?? displayName;
return {
_customAvatarBackgrounds: state['features/dynamic-branding'].avatarBackgrounds,
_initialsBase,
_loadableAvatarUrl: _participant?.loadableAvatarUrl,
colorBase: !colorBase && _participant ? _participant.id : colorBase

View File

@@ -2,7 +2,6 @@
import React from 'react';
import { translate } from '../../../../base/i18n';
import { Icon } from '../../../icons';
import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
@@ -31,19 +30,14 @@ type Props = AbstractProps & {
/**
* TestId of the element, if any.
*/
testId?: string,
/**
* Invoked to obtain translated strings.
*/
t: Function
testId?: string
};
/**
* Implements a stateless avatar component that renders an avatar purely from what gets passed through
* props.
*/
class StatelessAvatar extends AbstractStatelessAvatar<Props> {
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
/**
* Implements {@code Component#render}.
*
@@ -70,7 +64,7 @@ class StatelessAvatar extends AbstractStatelessAvatar<Props> {
return (
<div className = { this._getBadgeClassName() }>
<img
alt = { this.props.t('profile.avatar') }
alt = 'avatar'
className = { this._getAvatarClassName() }
data-testid = { this.props.testId }
id = { this.props.id }
@@ -111,7 +105,7 @@ class StatelessAvatar extends AbstractStatelessAvatar<Props> {
return (
<div className = { this._getBadgeClassName() }>
<img
alt = { this.props.t('profile.avatar') }
alt = 'avatar'
className = { this._getAvatarClassName('defaultAvatar') }
data-testid = { this.props.testId }
id = { this.props.id }
@@ -131,7 +125,7 @@ class StatelessAvatar extends AbstractStatelessAvatar<Props> {
const { size } = this.props;
return {
backgroundColor: color || undefined,
background: color || undefined,
fontSize: size ? size * 0.5 : '180%',
height: size || '100%',
width: size || '100%'
@@ -165,5 +159,3 @@ class StatelessAvatar extends AbstractStatelessAvatar<Props> {
_isIcon: (?string | ?Object) => boolean
}
export default translate(StatelessAvatar);

View File

@@ -16,9 +16,13 @@ const AVATAR_OPACITY = 0.4;
* Generates the background color of an initials based avatar.
*
* @param {string?} initials - The initials of the avatar.
* @param {Array<strig>} customAvatarBackgrounds - Custom avatar background values.
* @returns {string}
*/
export function getAvatarColor(initials: ?string) {
export function getAvatarColor(initials: ?string, customAvatarBackgrounds: Array<string>) {
const hasCustomAvatarBackgronds = customAvatarBackgrounds && customAvatarBackgrounds.length;
const colorsBase = hasCustomAvatarBackgronds ? customAvatarBackgrounds : AVATAR_COLORS;
let colorIndex = 0;
if (initials) {
@@ -28,10 +32,10 @@ export function getAvatarColor(initials: ?string) {
nameHash += s.codePointAt(0);
}
colorIndex = nameHash % AVATAR_COLORS.length;
colorIndex = nameHash % colorsBase.length;
}
return `rgba(${AVATAR_COLORS[colorIndex]}, ${AVATAR_OPACITY})`;
return hasCustomAvatarBackgronds ? colorsBase[colorIndex] : `rgba(${colorsBase[colorIndex]}, ${AVATAR_OPACITY})`;
}
/**

View File

@@ -6,7 +6,6 @@ import {
createStartMutedConfigurationEvent,
sendAnalytics
} from '../../analytics';
import { getName } from '../../app/functions';
import { endpointMessageReceived } from '../../subtitles';
import { getReplaceParticipant } from '../config/functions';
import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection';
@@ -14,7 +13,6 @@ import { JitsiConferenceEvents } from '../lib-jitsi-meet';
import { MEDIA_TYPE, setAudioMuted, setVideoMuted } from '../media';
import {
dominantSpeakerChanged,
getLocalParticipant,
getNormalizedDisplayName,
participantConnectionStatusChanged,
participantKicked,
@@ -24,11 +22,7 @@ import {
participantUpdated
} from '../participants';
import { getLocalTracks, replaceLocalTrack, trackAdded, trackRemoved } from '../tracks';
import {
getBackendSafePath,
getBackendSafeRoomName,
getJitsiMeetGlobalNS
} from '../util';
import { getBackendSafeRoomName } from '../util';
import {
AUTH_STATUS_CHANGED,
@@ -61,6 +55,7 @@ import {
_addLocalTracksToConference,
commonUserJoinedHandling,
commonUserLeftHandling,
getConferenceOptions,
getCurrentConference,
sendLocalParticipant
} from './functions';
@@ -434,22 +429,7 @@ export function createConference() {
throw new Error('Cannot join a conference without a room name!');
}
const config = state['features/base/config'];
const { tenant } = state['features/base/jwt'];
const { email, name: nick } = getLocalParticipant(state);
const conference
= connection.initJitsiConference(
getBackendSafeRoomName(room), {
...config,
applicationName: getName(),
getWiFiStatsMethod: getJitsiMeetGlobalNS().getWiFiStats,
confID: `${locationURL.host}${getBackendSafePath(locationURL.pathname)}`,
siteID: tenant,
statisticsDisplayName: config.enableDisplayNameInStats ? nick : undefined,
statisticsId: config.enableEmailInStats ? email : undefined
});
const conference = connection.initJitsiConference(getBackendSafeRoomName(room), getConferenceOptions(state));
connection[JITSI_CONNECTION_CONFERENCE_KEY] = conference;

View File

@@ -2,6 +2,7 @@
import _ from 'lodash';
import { getName } from '../../app/functions';
import { JitsiTrackErrors } from '../lib-jitsi-meet';
import {
getLocalParticipant,
@@ -11,7 +12,7 @@ import {
participantLeft
} from '../participants';
import { toState } from '../redux';
import { safeDecodeURIComponent } from '../util';
import { getBackendSafePath, getJitsiMeetGlobalNS, safeDecodeURIComponent } from '../util';
import {
AVATAR_URL_COMMAND,
@@ -198,6 +199,53 @@ export function getConferenceNameForTitle(stateful: Function | Object) {
return safeStartCase(safeDecodeURIComponent(getConferenceState(toState(stateful)).room));
}
/**
* Returns an object aggregating the conference options.
*
* @param {Object|Function} stateful - The redux store state.
* @returns {Object} - Options object.
*/
export function getConferenceOptions(stateful: Function | Object) {
const state = toState(stateful);
const config = state['features/base/config'];
const { locationURL } = state['features/base/connection'];
const { tenant } = state['features/base/jwt'];
const { email, name: nick } = getLocalParticipant(state);
const options = { ...config };
if (tenant) {
options.siteID = tenant;
}
if (options.enableDisplayNameInStats && nick) {
options.statisticsDisplayName = nick;
}
if (options.enableEmailInStats && email) {
options.statisticsId = email;
}
if (locationURL) {
options.confID = `${locationURL.host}${getBackendSafePath(locationURL.pathname)}`;
}
options.applicationName = getName();
// Disable analytics, if requessted.
if (options.disableThirdPartyRequests) {
delete config.analytics.scriptURLs;
delete config.analytics.amplitudeAPPKey;
delete config.analytics.googleAnalyticsTrackingId;
delete options.callStatsID;
delete options.callStatsSecret;
} else {
options.getWiFiStatsMethod = getWiFiStatsMethod;
}
return options;
}
/**
* Returns the UTC timestamp when the first participant joined the conference.
*
@@ -244,6 +292,21 @@ export function getRoomName(state: Object): string {
return getConferenceState(state).room;
}
/**
* Returns the result of getWiFiStats from the global NS or does nothing
* (returns empty result).
* Fixes a concurrency problem where we need to pass a function when creating
* a JitsiConference, but that method is added to the context later.
*
* @returns {Promise}
* @private
*/
function getWiFiStatsMethod() {
const gloabalNS = getJitsiMeetGlobalNS();
return gloabalNS.getWiFiStats ? gloabalNS.getWiFiStats() : Promise.resolve('{}');
}
/**
* Handle an error thrown by the backend (i.e. {@code lib-jitsi-meet}) while
* manipulating a conference participant (e.g. Pin or select participant).

View File

@@ -398,10 +398,9 @@ function _pinParticipant({ getState }, next, action) {
return next(action);
}
const participants = state['features/base/participants'];
const id = action.participant.id;
const participantById = getParticipantById(participants, id);
const pinnedParticipant = getPinnedParticipant(participants);
const participantById = getParticipantById(state, id);
const pinnedParticipant = getPinnedParticipant(state);
const actionName = id ? ACTION_PINNED : ACTION_UNPINNED;
const local
= (participantById && participantById.local)

View File

@@ -87,6 +87,7 @@ export default [
'disableH264',
'disableHPF',
'disableInviteFunctions',
'disableIncomingMessageSound',
'disableJoinLeaveSounds',
'disableLocalVideoFlip',
'disableNS',
@@ -130,6 +131,7 @@ export default [
'gatherStats',
'googleApiApplicationClientID',
'hideConferenceSubject',
'hideRecordingLabel',
'hideParticipantsStats',
'hideConferenceTimer',
'hiddenDomain',

View File

@@ -14,10 +14,35 @@ export const _CONFIG_STORE_PREFIX = 'config.js';
* @type Array<string>
*/
export const TOOLBAR_BUTTONS = [
'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'shareaudio', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'participants-pane', 'feedback', 'stats', 'shortcuts',
'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone',
'security', 'toggle-camera'
'camera',
'chat',
'closedcaptions',
'desktop',
'download',
'embedmeeting',
'etherpad',
'feedback',
'filmstrip',
'fullscreen',
'hangup',
'help',
'invite',
'livestreaming',
'microphone',
'mute-everyone',
'mute-video-everyone',
'participants-pane',
'profile',
'raisehand',
'recording',
'security',
'select-background',
'settings',
'shareaudio',
'sharedvideo',
'shortcuts',
'stats',
'tileview',
'toggle-camera',
'videoquality'
];

View File

@@ -60,8 +60,6 @@ export function getRecordingSharingUrl(state: Object) {
return state['features/base/config'].recordingSharingUrl;
}
/* eslint-disable max-params, no-shadow */
/**
* Overrides JSON properties in {@code config} and
* {@code interfaceConfig} Objects with the values from {@code newConfig}.

View File

@@ -56,12 +56,15 @@ export function getToolbarButtons(state: Object): Array<string> {
}
/**
* Curried selector to check if the specified button is enabled.
* Checks if the specified button is enabled.
*
* @param {string} buttonName - The name of the button.
* {@link interfaceConfig}.
* @returns {Function} - Selector that returns a boolean.
* @param {Object|Array<string>} state - The redux state or the array with the enabled buttons.
* @returns {boolean} - True if the button is enabled and false otherwise.
*/
export const isToolbarButtonEnabled = (buttonName: string) =>
(state: Object): boolean =>
getToolbarButtons(state).includes(buttonName);
export function isToolbarButtonEnabled(buttonName: string, state: Object | Array<string>) {
const buttons = Array.isArray(state) ? state : getToolbarButtons(state);
return buttons.includes(buttonName);
}

View File

@@ -2,6 +2,8 @@
import React, { Component } from 'react';
import { type ReactionEmojiProps } from '../../../reactions/constants';
/**
* The type of the React {@code Component} props of {@link DialogContainer}.
*/
@@ -25,7 +27,12 @@ type Props = {
/**
* True if the UI is in a compact state where we don't show dialogs.
*/
_reducedUI: boolean
_reducedUI: boolean,
/**
* Array of reactions to be displayed.
*/
_reactionsQueue: Array<ReactionEmojiProps>
};
/**

View File

@@ -25,11 +25,21 @@ const GESTURE_SPEED_THRESHOLD = 0.2;
*/
type Props = {
/**
* The height of the screen.
*/
_height: number,
/**
* The color-schemed stylesheet of the feature.
*/
_styles: StyleType,
/**
* Whether to add padding to scroll view.
*/
addScrollViewPadding?: boolean,
/**
* The children to be displayed within this component.
*/
@@ -49,7 +59,22 @@ type Props = {
/**
* Function to render a bottom sheet header element, if necessary.
*/
renderHeader: ?Function
renderHeader: ?Function,
/**
* Function to render a bottom sheet footer element, if necessary.
*/
renderFooter: ?Function,
/**
* Whether to show sliding view or not.
*/
showSlidingView?: boolean,
/**
* The component's external style
*/
style: Object
};
/**
@@ -58,6 +83,16 @@ type Props = {
class BottomSheet extends PureComponent<Props> {
panResponder: Object;
/**
* Default values for {@code BottomSheet} component's properties.
*
* @static
*/
static defaultProps = {
addScrollViewPadding: true,
showSlidingView: true
};
/**
* Instantiates a new component.
*
@@ -80,7 +115,15 @@ class BottomSheet extends PureComponent<Props> {
* @returns {ReactElement}
*/
render() {
const { _styles, renderHeader } = this.props;
const {
_height,
_styles,
addScrollViewPadding,
renderHeader,
renderFooter,
showSlidingView,
style
} = this.props;
return (
<SlidingView
@@ -88,7 +131,7 @@ class BottomSheet extends PureComponent<Props> {
accessibilityViewIsModal = { true }
onHide = { this.props.onCancel }
position = 'bottom'
show = { true }>
show = { showSlidingView }>
<View
pointerEvents = 'box-none'
style = { styles.sheetContainer }>
@@ -99,15 +142,22 @@ class BottomSheet extends PureComponent<Props> {
<SafeAreaView
style = { [
styles.sheetItemContainer,
_styles.sheet
renderHeader
? _styles.sheetHeader
: _styles.sheet,
style,
{
maxHeight: _height - 100
}
] }
{ ...this.panResponder.panHandlers }>
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false }
style = { styles.scrollView } >
style = { addScrollViewPadding && styles.scrollView } >
{ this.props.children }
</ScrollView>
{ renderFooter && renderFooter() }
</SafeAreaView>
</View>
</SlidingView>
@@ -167,7 +217,8 @@ class BottomSheet extends PureComponent<Props> {
*/
function _mapStateToProps(state) {
return {
_styles: ColorSchemeRegistry.get(state, 'BottomSheet')
_styles: ColorSchemeRegistry.get(state, 'BottomSheet'),
_height: state['features/base/responsive-ui'].clientHeight
};
}

View File

@@ -1,3 +1,7 @@
import React from 'react';
import { ReactionEmoji } from '../../../../reactions/components';
import { getReactionsQueue } from '../../../../reactions/functions.any';
import { connect } from '../../../redux';
import AbstractDialogContainer, {
abstractMapStateToProps
@@ -11,6 +15,22 @@ import AbstractDialogContainer, {
* @extends AbstractDialogContainer
*/
class DialogContainer extends AbstractDialogContainer {
/**
* Returns the reactions to be displayed.
*
* @returns {Array<React$Element>}
*/
_renderReactions() {
const { _reactionsQueue } = this.props;
return _reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }
reaction = { reaction }
uid = { uid } />));
}
/**
* Implements React's {@link Component#render()}.
*
@@ -18,8 +38,18 @@ class DialogContainer extends AbstractDialogContainer {
* @returns {ReactElement}
*/
render() {
return this._renderDialogContent();
return (<React.Fragment>
{this._renderReactions()}
{this._renderDialogContent()}
</React.Fragment>);
}
}
export default connect(abstractMapStateToProps)(DialogContainer);
const mapStateToProps = state => {
return {
...abstractMapStateToProps(state),
_reactionsQueue: getReactionsQueue(state)
};
};
export default connect(mapStateToProps)(DialogContainer);

View File

@@ -2,6 +2,7 @@
import { StyleSheet } from 'react-native';
import BaseTheme from '../../../../base/ui/components/BaseTheme.native';
import { ColorSchemeRegistry, schemeColor } from '../../../color-scheme';
import { BoxModel, ColorPalette } from '../../../styles';
import { PREFERRED_DIALOG_SIZE } from '../../constants';
@@ -33,7 +34,7 @@ export const bottomSheetStyles = {
},
scrollView: {
paddingHorizontal: MD_ITEM_MARGIN_PADDING
paddingHorizontal: 0
},
/**
@@ -117,7 +118,7 @@ const brandedDialogText = {
};
const brandedDialogLabelStyle = {
color: schemeColor('text'),
color: ColorPalette.white,
flexShrink: 1,
fontSize: MD_FONT_SIZE,
opacity: 0.90
@@ -130,7 +131,7 @@ const brandedDialogItemContainerStyle = {
};
const brandedDialogIconStyle = {
color: schemeColor('icon'),
color: ColorPalette.white,
fontSize: 24
};
@@ -171,27 +172,37 @@ ColorSchemeRegistry.register('BottomSheet', {
*/
labelStyle: {
...brandedDialogLabelStyle,
marginLeft: 32
marginLeft: 16
},
/**
* Container style for a generic item rendered in the menu.
*/
style: {
...brandedDialogItemContainerStyle
...brandedDialogItemContainerStyle,
paddingHorizontal: MD_ITEM_MARGIN_PADDING
},
/**
* Additional style that is not directly used as a style object.
*/
underlayColor: ColorPalette.overflowMenuItemUnderlay
underlayColor: ColorPalette.toggled
},
/**
* Bottom sheet's base style.
*/
sheet: {
backgroundColor: schemeColor('background')
backgroundColor: BaseTheme.palette.ui02,
borderTopLeftRadius: 16,
borderTopRightRadius: 16
},
/**
* Bottom sheet's base style with header.
*/
sheetHeader: {
backgroundColor: BaseTheme.palette.ui02
}
});

View File

@@ -214,3 +214,9 @@ export const VIDEO_SHARE_BUTTON_ENABLED = 'video-share.enabled';
* Default: disabled (false).
*/
export const WELCOME_PAGE_ENABLED = 'welcomepage.enabled';
/**
* Flag indicating if the reactions feature should be enabled.
* Default: disabled (false).
*/
export const REACTIONS_ENABLED = 'reactions.enabled';

View File

@@ -50,6 +50,11 @@ type Props = {
*/
headerProps: Object,
/**
* True if the header with navigation should be hidden, false otherwise.
*/
hideHeaderWithNavigation?: boolean,
/**
* The ID of the modal that is being rendered. This is used to show/hide the modal.
*/
@@ -78,7 +83,8 @@ type Props = {
*/
class JitsiModal extends PureComponent<Props> {
static defaultProps = {
position: 'bottom'
position: 'bottom',
hideHeaderWithNavigation: false
};
/**
@@ -98,7 +104,17 @@ class JitsiModal extends PureComponent<Props> {
* @inheritdoc
*/
render() {
const { _headerStyles, _show, _styles, children, footerComponent, headerProps, position, style } = this.props;
const {
_headerStyles,
_show,
_styles,
children,
footerComponent,
headerProps,
position,
hideHeaderWithNavigation,
style
} = this.props;
return (
<SlidingView
@@ -119,6 +135,7 @@ class JitsiModal extends PureComponent<Props> {
] }>
<HeaderWithNavigation
{ ...headerProps }
hideHeaderWithNavigation = { hideHeaderWithNavigation }
onPressBack = { this._onRequestClose } />
<SafeAreaView style = { styles.safeArea }>
{ children }

View File

@@ -3,7 +3,7 @@
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { YoutubeLargeVideo } from '../../../shared-video/components';
import { SharedVideo } from '../../../shared-video/components/native';
import { Avatar } from '../../avatar';
import { translate } from '../../i18n';
import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
@@ -208,11 +208,11 @@ class ParticipantView extends Component<Props> {
? this.props.testHintId
: `org.jitsi.meet.Participant#${this.props.participantId}`;
const renderYoutubeLargeVideo = _isFakeParticipant && !disableVideo;
const renderSharedVideo = _isFakeParticipant && !disableVideo;
return (
<Container
onClick = { renderVideo || renderYoutubeLargeVideo ? undefined : onPress }
onClick = { renderVideo || renderSharedVideo ? undefined : onPress }
style = {{
...styles.participantView,
...this.props.style
@@ -221,10 +221,10 @@ class ParticipantView extends Component<Props> {
<TestHint
id = { testHintId }
onPress = { renderYoutubeLargeVideo ? undefined : onPress }
onPress = { renderSharedVideo ? undefined : onPress }
value = '' />
{ renderYoutubeLargeVideo && <YoutubeLargeVideo youtubeId = { this.props.participantId } /> }
{ renderSharedVideo && <SharedVideo /> }
{ !_isFakeParticipant && renderVideo
&& <VideoTrack
@@ -234,7 +234,7 @@ class ParticipantView extends Component<Props> {
zOrder = { this.props.zOrder }
zoomEnabled = { this.props.zoomEnabled } /> }
{ !renderYoutubeLargeVideo && !renderVideo
{ !renderSharedVideo && !renderVideo
&& <View style = { styles.avatarContainer }>
<Avatar
participantId = { this.props.participantId }

View File

@@ -79,16 +79,15 @@ export function getFirstLoadableAvatarUrl(participant: Object, store: Store<any,
/**
* Returns local participant from Redux state.
*
* @param {(Function|Object|Participant[])} stateful - The redux state
* features/base/participants, the (whole) redux state, or redux's
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @returns {(Participant|undefined)}
*/
export function getLocalParticipant(stateful: Object | Function) {
const participants = _getAllParticipants(stateful);
const state = toState(stateful)['features/base/participants'];
return participants.find(p => p.local);
return state.local;
}
/**
@@ -109,8 +108,7 @@ export function getNormalizedDisplayName(name: string) {
/**
* Returns participant by ID from Redux state.
*
* @param {(Function|Object|Participant[])} stateful - The redux state
* features/base/participants, the (whole) redux state, or redux's
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @param {string} id - The ID of the participant to retrieve.
@@ -119,37 +117,82 @@ export function getNormalizedDisplayName(name: string) {
*/
export function getParticipantById(
stateful: Object | Function, id: string): ?Object {
const participants = _getAllParticipants(stateful);
const state = toState(stateful)['features/base/participants'];
const { local, remote } = state;
return participants.find(p => p.id === id);
return remote.get(id) || (local?.id === id ? local : undefined);
}
/**
* Returns the participant with the ID matching the passed ID or the local participant if the ID is
* undefined.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @param {string|undefined} [participantID] - An optional partipantID argument.
* @returns {Participant|undefined}
*/
export function getParticipantByIdOrUndefined(stateful: Object | Function, participantID: ?string) {
return participantID ? getParticipantById(stateful, participantID) : getLocalParticipant(stateful);
}
/**
* Returns a count of the known participants in the passed in redux state,
* excluding any fake participants.
*
* @param {(Function|Object|Participant[])} stateful - The redux state
* features/base/participants, the (whole) redux state, or redux's
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @returns {number}
*/
export function getParticipantCount(stateful: Object | Function) {
return getParticipants(stateful).length;
const state = toState(stateful)['features/base/participants'];
const { local, remote, fakeParticipants } = state;
return remote.size - fakeParticipants.size + (local ? 1 : 0);
}
/**
* Returns the Map with fake participants.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @returns {Map<string, Participant>} - The Map with fake participants.
*/
export function getFakeParticipants(stateful: Object | Function) {
return toState(stateful)['features/base/participants'].fakeParticipants;
}
/**
* Returns a count of the known remote participants in the passed in redux state.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @returns {number}
*/
export function getRemoteParticipantCount(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants'];
return state.remote.size;
}
/**
* Returns a count of the known participants in the passed in redux state,
* including fake participants.
*
* @param {(Function|Object|Participant[])} stateful - The redux state
* features/base/participants, the (whole) redux state, or redux's
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @returns {number}
*/
export function getParticipantCountWithFake(stateful: Object | Function) {
return _getAllParticipants(stateful).length;
const state = toState(stateful)['features/base/participants'];
const { local, remote } = state;
return remote.size + (local ? 1 : 0);
}
/**
@@ -185,17 +228,6 @@ export function getParticipantDisplayName(
: 'Fellow Jitster';
}
/**
* Curried version of getParticipantDisplayName.
*
* @see {@link getParticipantDisplayName}
* @param {string} id - The ID of the participant's display name to retrieve.
* @returns {Function}
*/
export const getParticipantDisplayNameWithId = (id: string) =>
(state: Object | Function) =>
getParticipantDisplayName(state, id);
/**
* Returns the presence status of a participant associated with the passed id.
*
@@ -219,64 +251,45 @@ export function getParticipantPresenceStatus(
}
/**
* Selectors for getting all known participants with fake participants filtered
* out.
* Returns true if there is at least 1 participant with screen sharing feature and false otherwise.
*
* @param {(Function|Object|Participant[])} stateful - The redux state
* features/base/participants, the (whole) redux state, or redux's
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state.
* @returns {boolean}
*/
export function haveParticipantWithScreenSharingFeature(stateful: Object | Function) {
return toState(stateful)['features/base/participants'].haveParticipantWithScreenSharingFeature;
}
/**
* Selectors for getting all remote participants.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @returns {Participant[]}
* @returns {Map<string, Object>}
*/
export function getParticipants(stateful: Object | Function) {
return _getAllParticipants(stateful).filter(p => !p.isFakeParticipant);
export function getRemoteParticipants(stateful: Object | Function) {
return toState(stateful)['features/base/participants'].remote;
}
/**
* Returns the participant which has its pinned state set to truthy.
*
* @param {(Function|Object|Participant[])} stateful - The redux state
* features/base/participants, the (whole) redux state, or redux's
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @returns {(Participant|undefined)}
*/
export function getPinnedParticipant(stateful: Object | Function) {
return _getAllParticipants(stateful).find(p => p.pinned);
}
const state = toState(stateful)['features/base/participants'];
const { pinnedParticipant } = state;
/**
* Returns array of participants from Redux state.
*
* @param {(Function|Object|Participant[])} stateful - The redux state
* features/base/participants, the (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @private
* @returns {Participant[]}
*/
function _getAllParticipants(stateful) {
return (
Array.isArray(stateful)
? stateful
: toState(stateful)['features/base/participants'] || []);
}
if (!pinnedParticipant) {
return undefined;
}
/**
* Returns the youtube fake participant.
* At the moment it is considered the youtube participant the only fake participant in the list.
*
* @param {(Function|Object|Participant[])} stateful - The redux state
* features/base/participants, the (whole) redux state, or redux's
* {@code getState} function to be used to retrieve the state
* features/base/participants.
* @private
* @returns {Participant}
*/
export function getYoutubeParticipant(stateful: Object | Function) {
const participants = _getAllParticipants(stateful);
return participants.filter(p => p.isFakeParticipant)[0];
return getParticipantById(stateful, pinnedParticipant);
}
/**
@@ -289,6 +302,24 @@ export function isParticipantModerator(participant: Object) {
return participant?.role === PARTICIPANT_ROLE.MODERATOR;
}
/**
* Returns the dominant speaker participant.
*
* @param {(Function|Object)} stateful - The (whole) redux state or redux's
* {@code getState} function to be used to retrieve the state features/base/participants.
* @returns {Participant} - The participant from the redux store.
*/
export function getDominantSpeakerParticipant(stateful: Object | Function) {
const state = toState(stateful)['features/base/participants'];
const { dominantSpeaker } = state;
if (!dominantSpeaker) {
return undefined;
}
return getParticipantById(stateful, dominantSpeaker);
}
/**
* Returns true if all of the meeting participants are moderators.
*
@@ -297,9 +328,9 @@ export function isParticipantModerator(participant: Object) {
* @returns {boolean}
*/
export function isEveryoneModerator(stateful: Object | Function) {
const participants = _getAllParticipants(stateful);
const state = toState(stateful)['features/base/participants'];
return participants.every(isParticipantModerator);
return state.everyoneIsModerator === true;
}
/**
@@ -321,14 +352,15 @@ export function isIconUrl(icon: ?string | ?Object) {
* @returns {boolean}
*/
export function isLocalParticipantModerator(stateful: Object | Function) {
const state = toState(stateful);
const localParticipant = getLocalParticipant(state);
const state = toState(stateful)['features/base/participants'];
if (!localParticipant) {
const { local } = state;
if (!local) {
return false;
}
return isParticipantModerator(localParticipant);
return isParticipantModerator(local);
}
/**
@@ -390,7 +422,7 @@ async function _getFirstLoadableAvatarUrl(participant, store) {
for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
const url = AVATAR_CHECKER_FUNCTIONS[i](participant, store);
if (url) {
if (url !== null) {
if (AVATAR_CHECKED_URLS.has(url)) {
if (AVATAR_CHECKED_URLS.get(url)) {
return url;

View File

@@ -1,5 +1,7 @@
// @flow
import { batch } from 'react-redux';
import UIEvents from '../../../../service/UI/UIEvents';
import { toggleE2EE } from '../../e2ee/actions';
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
@@ -43,7 +45,8 @@ import {
getLocalParticipant,
getParticipantById,
getParticipantCount,
getParticipantDisplayName
getParticipantDisplayName,
getRemoteParticipants
} from './functions';
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
@@ -182,11 +185,12 @@ MiddlewareRegistry.register(store => next => action => {
StateListenerRegistry.register(
/* selector */ state => getCurrentConference(state),
/* listener */ (conference, { dispatch, getState }) => {
for (const p of getState()['features/base/participants']) {
!p.local
&& (!conference || p.conference !== conference)
&& dispatch(participantLeft(p.id, p.conference, p.isReplaced));
}
batch(() => {
for (const [ id, p ] of getRemoteParticipants(getState())) {
(!conference || p.conference !== conference)
&& dispatch(participantLeft(id, p.conference, p.isReplaced));
}
});
});
/**
@@ -367,6 +371,7 @@ function _localParticipantLeft({ dispatch }, next, action) {
function _maybePlaySounds({ getState, dispatch }, action) {
const state = getState();
const { startAudioMuted, disableJoinLeaveSounds } = state['features/base/config'];
const { soundsParticipantJoined: joinSound, soundsParticipantLeft: leftSound } = state['features/base/settings'];
// If we have join/leave sounds disabled, don't play anything.
if (disableJoinLeaveSounds) {
@@ -383,13 +388,16 @@ function _maybePlaySounds({ getState, dispatch }, action) {
const { isReplacing, isReplaced } = action.participant;
if (action.type === PARTICIPANT_JOINED) {
if (!joinSound) {
return;
}
const { presence } = action.participant;
// The sounds for the poltergeist are handled by features/invite.
if (presence !== INVITED && presence !== CALLING && !isReplacing) {
dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID));
}
} else if (action.type === PARTICIPANT_LEFT && !isReplaced) {
} else if (action.type === PARTICIPANT_LEFT && !isReplaced && leftSound) {
dispatch(playSound(PARTICIPANT_LEFT_SOUND_ID));
}
}

View File

@@ -12,6 +12,7 @@ import {
SET_LOADABLE_AVATAR_URL
} from './actionTypes';
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
import { isParticipantModerator } from './functions';
/**
* Participant object.
@@ -51,6 +52,16 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
'pinned'
];
const DEFAULT_STATE = {
haveParticipantWithScreenSharingFeature: false,
dominantSpeaker: undefined,
everyoneIsModerator: false,
pinnedParticipant: undefined,
local: undefined,
remote: new Map(),
fakeParticipants: new Map()
};
/**
* Listen for actions which add, remove, or update the set of participants in
* the conference.
@@ -62,18 +73,157 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
* added/removed/modified.
* @returns {Participant[]}
*/
ReducerRegistry.register('features/base/participants', (state = [], action) => {
ReducerRegistry.register('features/base/participants', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case PARTICIPANT_ID_CHANGED: {
const { local } = state;
if (local) {
state.local = {
...local,
id: action.newValue
};
return {
...state
};
}
return state;
}
case DOMINANT_SPEAKER_CHANGED: {
const { participant } = action;
const { id } = participant;
const { dominantSpeaker } = state;
// Only one dominant speaker is allowed.
if (dominantSpeaker) {
_updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
}
if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
return {
...state,
dominantSpeaker: id
};
}
delete state.dominantSpeaker;
return {
...state
};
}
case PIN_PARTICIPANT: {
const { participant } = action;
const { id } = participant;
const { pinnedParticipant } = state;
// Only one pinned participant is allowed.
if (pinnedParticipant) {
_updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
}
if (_updateParticipantProperty(state, id, 'pinned', true)) {
return {
...state,
pinnedParticipant: id
};
}
delete state.pinnedParticipant;
return {
...state
};
}
case SET_LOADABLE_AVATAR_URL:
case DOMINANT_SPEAKER_CHANGED:
case PARTICIPANT_ID_CHANGED:
case PARTICIPANT_UPDATED:
case PIN_PARTICIPANT:
return state.map(p => _participant(p, action));
case PARTICIPANT_UPDATED: {
const { participant } = action;
let { id } = participant;
const { local } = participant;
case PARTICIPANT_JOINED:
return [ ...state, _participantJoined(action) ];
if (!id && local) {
id = LOCAL_PARTICIPANT_DEFAULT_ID;
}
let newParticipant;
if (state.remote.has(id)) {
newParticipant = _participant(state.remote.get(id), action);
state.remote.set(id, newParticipant);
} else if (id === state.local?.id) {
newParticipant = state.local = _participant(state.local, action);
}
if (newParticipant) {
// everyoneIsModerator calculation:
const isModerator = isParticipantModerator(newParticipant);
if (state.everyoneIsModerator && !isModerator) {
state.everyoneIsModerator = false;
} else if (!state.everyoneIsModerator && isModerator) {
state.everyoneIsModerator = _isEveryoneModerator(state);
}
// haveParticipantWithScreenSharingFeature calculation:
const { features = {} } = participant;
// Currently we use only PARTICIPANT_UPDATED to set a feature to enabled and we never disable it.
if (String(features['screen-sharing']) === 'true') {
state.haveParticipantWithScreenSharingFeature = true;
}
}
return {
...state
};
}
case PARTICIPANT_JOINED: {
const participant = _participantJoined(action);
const { pinnedParticipant, dominantSpeaker } = state;
if (participant.pinned) {
if (pinnedParticipant) {
_updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
}
state.pinnedParticipant = participant.id;
}
if (participant.dominantSpeaker) {
if (dominantSpeaker) {
_updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
}
state.dominantSpeaker = participant.id;
}
const isModerator = isParticipantModerator(participant);
const { local, remote } = state;
if (state.everyoneIsModerator && !isModerator) {
state.everyoneIsModerator = false;
} else if (!local && remote.size === 0 && isModerator) {
state.everyoneIsModerator = true;
}
if (participant.local) {
return {
...state,
local: participant
};
}
state.remote.set(participant.id, participant);
if (participant.isFakeParticipant) {
state.fakeParticipants.set(participant.id, participant);
}
return { ...state };
}
case PARTICIPANT_LEFT: {
// XXX A remote participant is uniquely identified by their id in a
// specific JitsiConference instance. The local participant is uniquely
@@ -81,23 +231,111 @@ ReducerRegistry.register('features/base/participants', (state = [], action) => {
// (and the fact that the local participant "joins" at the beginning of
// the app and "leaves" at the end of the app).
const { conference, id } = action.participant;
const { fakeParticipants, remote, local, dominantSpeaker, pinnedParticipant } = state;
let oldParticipant = remote.get(id);
return state.filter(p =>
!(
p.id === id
if (oldParticipant && oldParticipant.conference === conference) {
remote.delete(id);
} else if (local?.id === id) {
oldParticipant = state.local;
delete state.local;
} else {
// no participant found
return state;
}
// XXX Do not allow collisions in the IDs of the local
// participant and a remote participant cause the removal of
// the local participant when the remote participant's
// removal is requested.
&& p.conference === conference
&& (conference || p.local)));
if (!state.everyoneIsModerator && !isParticipantModerator(oldParticipant)) {
state.everyoneIsModerator = _isEveryoneModerator(state);
}
const { features = {} } = oldParticipant || {};
if (state.haveParticipantWithScreenSharingFeature && String(features['screen-sharing']) === 'true') {
const { features: localFeatures = {} } = state.local || {};
if (String(localFeatures['screen-sharing']) !== 'true') {
state.haveParticipantWithScreenSharingFeature = false;
// eslint-disable-next-line no-unused-vars
for (const [ key, participant ] of state.remote) {
const { features: f = {} } = participant;
if (String(f['screen-sharing']) === 'true') {
state.haveParticipantWithScreenSharingFeature = true;
break;
}
}
}
}
if (dominantSpeaker === id) {
state.dominantSpeaker = undefined;
}
if (pinnedParticipant === id) {
state.pinnedParticipant = undefined;
}
if (fakeParticipants.has(id)) {
fakeParticipants.delete(id);
}
return { ...state };
}
}
return state;
});
/**
* Loops trough the participants in the state in order to check if all participants are moderators.
*
* @param {Object} state - The local participant redux state.
* @returns {boolean}
*/
function _isEveryoneModerator(state) {
if (isParticipantModerator(state.local)) {
// eslint-disable-next-line no-unused-vars
for (const [ k, p ] of state.remote) {
if (!isParticipantModerator(p)) {
return false;
}
}
return true;
}
return false;
}
/**
* Updates a specific property for a participant.
*
* @param {State} state - The redux state.
* @param {string} id - The ID of the participant.
* @param {string} property - The property to update.
* @param {*} value - The new value.
* @returns {boolean} - True if a participant was updated and false otherwise.
*/
function _updateParticipantProperty(state, id, property, value) {
const { remote, local } = state;
if (remote.has(id)) {
remote.set(id, set(remote.get(id), property, value));
return true;
} else if (local?.id === id) {
state.local = set(local, property, value);
return true;
}
return false;
}
/**
* Reducer function for a single participant.
*
@@ -112,56 +350,22 @@ ReducerRegistry.register('features/base/participants', (state = [], action) => {
*/
function _participant(state: Object = {}, action) {
switch (action.type) {
case DOMINANT_SPEAKER_CHANGED:
// Only one dominant speaker is allowed.
return (
set(state, 'dominantSpeaker', state.id === action.participant.id));
case PARTICIPANT_ID_CHANGED: {
// A participant is identified by an id-conference pair. Only the local
// participant is with an undefined conference.
const { conference } = action;
if (state.id === action.oldValue
&& state.conference === conference
&& (conference || state.local)) {
return {
...state,
id: action.newValue
};
}
break;
}
case SET_LOADABLE_AVATAR_URL:
case PARTICIPANT_UPDATED: {
const { participant } = action; // eslint-disable-line no-shadow
let { id } = participant;
const { local } = participant;
if (!id && local) {
id = LOCAL_PARTICIPANT_DEFAULT_ID;
}
const newState = { ...state };
if (state.id === id) {
const newState = { ...state };
for (const key in participant) {
if (participant.hasOwnProperty(key)
&& PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
=== -1) {
newState[key] = participant[key];
}
for (const key in participant) {
if (participant.hasOwnProperty(key)
&& PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
=== -1) {
newState[key] = participant[key];
}
return newState;
}
break;
}
case PIN_PARTICIPANT:
// Currently, only one pinned participant is allowed.
return set(state, 'pinned', state.id === action.participant.id);
return newState;
}
}
return state;

View File

@@ -27,6 +27,11 @@ type Props = {
*/
headerLabelKey: ?string,
/**
* True if the header with navigation should be hidden, false otherwise.
*/
hideHeaderWithNavigation?: boolean,
/**
* Callback to be invoked on pressing the back button.
*/
@@ -48,17 +53,18 @@ class HeaderWithNavigation extends Component<Props> {
* @inheritdoc
*/
render() {
const { onPressBack, onPressForward } = this.props;
const { hideHeaderWithNavigation, onPressBack, onPressForward } = this.props;
return (
<Header>
{ onPressBack && <BackButton onPress = { onPressBack } /> }
<HeaderLabel labelKey = { this.props.headerLabelKey } />
{ onPressForward && <ForwardButton
disabled = { this.props.forwardDisabled }
labelKey = { this.props.forwardLabelKey }
onPress = { onPressForward } /> }
</Header>
!hideHeaderWithNavigation
&& <Header>
{ onPressBack && <BackButton onPress = { onPressBack } /> }
<HeaderLabel labelKey = { this.props.headerLabelKey } />
{ onPressForward && <ForwardButton
disabled = { this.props.forwardDisabled }
labelKey = { this.props.forwardLabelKey }
onPress = { onPressForward } /> }
</Header>
);
}
}

View File

@@ -2,7 +2,7 @@
import React, { Component } from 'react';
import { isVpaasMeeting } from '../../../../billing-counter/functions';
import { isVpaasMeeting } from '../../../../jaas/functions';
import { translate } from '../../../i18n';
import { connect } from '../../../redux';

View File

@@ -27,6 +27,10 @@ const DEFAULT_STATE = {
micDeviceId: undefined,
serverURL: undefined,
hideShareAudioHelper: false,
soundsIncomingMessage: true,
soundsParticipantJoined: true,
soundsParticipantLeft: true,
soundsTalkWhileMuted: true,
startAudioOnly: false,
startWithAudioMuted: false,
startWithVideoMuted: false,

View File

@@ -227,7 +227,7 @@ export function noDataFromSource(track) {
* @returns {Function}
*/
export function showNoDataFromSourceVideoError(jitsiTrack) {
return (dispatch, getState) => {
return async (dispatch, getState) => {
let notificationInfo;
const track = getTrackByJitsiTrack(getState()['features/base/tracks'], jitsiTrack);
@@ -239,12 +239,11 @@ export function showNoDataFromSourceVideoError(jitsiTrack) {
if (track.isReceivingData) {
notificationInfo = undefined;
} else {
const notificationAction = showErrorNotification({
const notificationAction = await dispatch(showErrorNotification({
descriptionKey: 'dialog.cameraNotSendingData',
titleKey: 'dialog.cameraNotSendingDataTitle'
});
}));
dispatch(notificationAction);
notificationInfo = {
uid: notificationAction.uid
};
@@ -362,7 +361,7 @@ function replaceStoredTracks(oldTrack, newTrack) {
* @returns {{ type: TRACK_ADDED, track: Track }}
*/
export function trackAdded(track) {
return (dispatch, getState) => {
return async (dispatch, getState) => {
track.on(
JitsiTrackEvents.TRACK_MUTE_CHANGED,
() => dispatch(trackMutedChanged(track)));
@@ -389,12 +388,10 @@ export function trackAdded(track) {
track.on(JitsiTrackEvents.NO_DATA_FROM_SOURCE, () => dispatch(noDataFromSource({ jitsiTrack: track })));
if (!isReceivingData) {
if (mediaType === MEDIA_TYPE.AUDIO) {
const notificationAction = showNotification({
const notificationAction = await dispatch(showNotification({
descriptionKey: 'dialog.micNotSendingData',
titleKey: 'dialog.micNotSendingDataTitle'
});
dispatch(notificationAction);
}));
// Set the notification ID so that other parts of the application know that this was
// displayed in the context of the current device.

View File

@@ -21,68 +21,50 @@ import logger from './logger';
export const getTrackState = state => state['features/base/tracks'];
/**
* Higher-order function that returns a selector for a specific participant
* and media type.
* Checks if the passed media type is muted for the participant.
*
* @param {Object} participant - Participant reference.
* @param {MEDIA_TYPE} mediaType - Media type.
* @returns {Function} Selector.
* @param {Object} state - Global state.
* @returns {boolean} - Is the media type muted for the participant.
*/
export const getIsParticipantMediaMuted = (participant, mediaType) =>
export function isParticipantMediaMuted(participant, mediaType, state) {
if (!participant) {
return false;
}
/**
* Bound selector.
*
* @param {Object} state - Global state.
* @returns {boolean} Is the media type muted for the participant.
*/
state => {
if (!participant) {
return;
}
const tracks = getTrackState(state);
const tracks = getTrackState(state);
if (participant?.local) {
return isLocalTrackMuted(tracks, mediaType);
} else if (!participant?.isFakeParticipant) {
return isRemoteTrackMuted(tracks, mediaType, participant.id);
}
if (participant?.local) {
return isLocalTrackMuted(tracks, mediaType);
} else if (!participant?.isFakeParticipant) {
return isRemoteTrackMuted(tracks, mediaType, participant.id);
}
return true;
};
return true;
}
/**
* Higher-order function that returns a selector for a specific participant.
* Checks if the participant is audio muted.
*
* @param {Object} participant - Participant reference.
* @returns {Function} Selector.
* @param {Object} state - Global state.
* @returns {boolean} - Is audio muted for the participant.
*/
export const getIsParticipantAudioMuted = participant =>
/**
* Bound selector.
*
* @param {Object} state - Global state.
* @returns {boolean} Is audio muted for the participant.
*/
state => getIsParticipantMediaMuted(participant, MEDIA_TYPE.AUDIO)(state);
export function isParticipantAudioMuted(participant, state) {
return isParticipantMediaMuted(participant, MEDIA_TYPE.AUDIO, state);
}
/**
* Higher-order function that returns a selector for a specific participant.
* Checks if the participant is video muted.
*
* @param {Object} participant - Participant reference.
* @returns {Function} Selector.
* @param {Object} state - Global state.
* @returns {boolean} - Is video muted for the participant.
*/
export const getIsParticipantVideoMuted = participant =>
/**
* Bound selector.
*
* @param {Object} state - Global state.
* @returns {boolean} Is video muted for the participant.
*/
state => getIsParticipantMediaMuted(participant, MEDIA_TYPE.VIDEO)(state);
export function isParticipantVideoMuted(participant, state) {
return isParticipantMediaMuted(participant, MEDIA_TYPE.VIDEO, state);
}
/**
* Creates a local video track for presenter. The constraints are computed based

View File

@@ -18,6 +18,7 @@ export const colors = {
primary08: '#99BBF3',
primary09: '#CCDDF9',
surface00: '#111111',
surface01: '#040404',
surface02: '#141414',
surface03: '#292929',
@@ -29,11 +30,13 @@ export const colors = {
surface09: '#C2C2C2',
surface10: '#E0E0E0',
surface11: '#FFF',
surface12: '#AAAAAA',
success04: '#189B55',
success05: '#1EC26A',
warning05: '#F8AE1A'
warning05: '#F8AE1A',
warning06: '#ED9E1B'
};
// Mapping between the token used and the color
@@ -108,6 +111,9 @@ export const colorMap = {
// Disabled state for danger buttons
actionDangerDisabled: 'error03',
// Bottom sheet background
bottomSheet: 'surface00',
// Primary text default color for body copy & headers
text01: 'surface11',
@@ -117,6 +123,9 @@ export const colorMap = {
// Tertiary text with low contrast placeholders, disabled actions, label for disabled buttons
text03: 'surface07',
// Text for bottom sheet items
text04: 'surface12',
// error messages
textError: 'error06',
@@ -148,6 +157,9 @@ export const colorMap = {
// Background for high-contrast input fields
field02: 'surface11',
// Color for the section divider
dividerColor: 'surface12',
// Background for high-contrast input fields on hover
field02Hover: 'primary09',
@@ -197,20 +209,24 @@ export const colorMap = {
success02: 'success05',
// Color for warning messages applied to icons, borders & backgrounds
warning01: 'warning05'
warning01: 'warning05',
// Color for indicating a raised hand
warning02: 'warning06'
};
export const font = {
weightRegular: 400,
weightSemiBold: 600
weightRegular: '400',
weightSemiBold: '600'
};
export const shape = {
borderRadius: 6
borderRadius: 6,
boxShadow: 'inset 0px -1px 0px rgba(255, 255, 255, 0.15)'
};
export const spacing = [ 0, 4, 8, 16, 24, 32, 40, 48, 56 ];
export const spacing = [ 0, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80 ];
export const typography = {
labelRegular: {

View File

@@ -45,17 +45,18 @@ const _URI_PATH_PATTERN = '([^?#]*)';
export const URI_PROTOCOL_PATTERN = '^([a-z][a-z0-9\\.\\+-]*:)';
/**
* Excludes/removes certain characters from a specific room (name) which are
* incompatible with Jitsi Meet on the client and/or server sides.
* Excludes/removes certain characters from a specific path part which are
* incompatible with Jitsi Meet on the client and/or server sides. The main
* use case for this method is to clean up the room name and the tenant.
*
* @param {?string} room - The room (name) to fix.
* @param {?string} pathPart - The path part to fix.
* @private
* @returns {?string}
*/
function _fixRoom(room: ?string) {
return room
? room.replace(new RegExp(_ROOM_EXCLUDE_PATTERN, 'g'), '')
: room;
function _fixPathPart(pathPart: ?string) {
return pathPart
? pathPart.replace(new RegExp(_ROOM_EXCLUDE_PATTERN, 'g'), '')
: pathPart;
}
/**
@@ -335,6 +336,11 @@ export function parseURIString(uri: ?string) {
const obj = parseStandardURIString(_fixURIStringScheme(uri));
// XXX While the components/segments of pathname are URI encoded, Jitsi Meet
// on the client and/or server sides still don't support certain characters.
obj.pathname = obj.pathname.split('/').map(pathPart => _fixPathPart(pathPart))
.join('/');
// Add the properties that are specific to a Jitsi Meet resource (location)
// such as contextRoot, room:
@@ -344,24 +350,9 @@ export function parseURIString(uri: ?string) {
// The room (name) is the last component/segment of pathname.
const { pathname } = obj;
// XXX While the components/segments of pathname are URI encoded, Jitsi Meet
// on the client and/or server sides still don't support certain characters.
const contextRootEndIndex = pathname.lastIndexOf('/');
let room = pathname.substring(contextRootEndIndex + 1) || undefined;
if (room) {
const fixedRoom = _fixRoom(room);
if (fixedRoom !== room) {
room = fixedRoom;
// XXX Drive fixedRoom into pathname (because room is derived from
// pathname).
obj.pathname
= pathname.substring(0, contextRootEndIndex + 1) + (room || '');
}
}
obj.room = room;
obj.room = pathname.substring(contextRootEndIndex + 1) || undefined;
if (contextRootEndIndex > 1) {
// The part of the pathname from the beginning to the room name is the tenant.

View File

@@ -1,4 +0,0 @@
/**
* Action used to store the flag signaling the endpoint has been counted.
*/
export const SET_ENDPOINT_COUNTED = 'SET_ENDPOINT_COUNTED';

View File

@@ -1,42 +0,0 @@
// @flow
import { SET_ENDPOINT_COUNTED } from './actionTypes';
import { extractVpaasTenantFromPath, getBillingId, sendCountRequest } from './functions';
/**
* Sends a billing count request when needed.
*
* @returns {Function}
*/
export function countEndpoint() {
return function(dispatch: Function, getState: Function) {
const state = getState();
const baseUrl = state['features/base/config'].billingCounterUrl;
const jwt = state['features/base/jwt'].jwt;
const tenant = extractVpaasTenantFromPath(state['features/base/connection'].locationURL.pathname);
const shouldSendRequest = Boolean(baseUrl && jwt && tenant);
if (shouldSendRequest) {
const billingId = getBillingId();
sendCountRequest({
baseUrl,
billingId,
jwt,
tenant
});
dispatch(setEndpointCounted());
}
};
}
/**
* Action used to mark the endpoint as counted.
*
* @returns {Object}
*/
function setEndpointCounted() {
return {
type: SET_ENDPOINT_COUNTED
};
}

View File

@@ -1,9 +0,0 @@
/**
* The key for the billing id stored in localStorage.
*/
export const BILLING_ID = 'jitsiMeetId';
/**
* The prefix for the vpaas tenant.
*/
export const VPAAS_TENANT_PREFIX = 'vpaas-magic-cookie';

View File

@@ -1,116 +0,0 @@
// @flow
import { jitsiLocalStorage } from '@jitsi/js-utils';
import uuid from 'uuid';
import { BILLING_ID, VPAAS_TENANT_PREFIX } from './constants';
import logger from './logger';
/**
* Returns the full vpaas tenant if available, given a path.
*
* @param {string} path - The meeting url path.
* @returns {string}
*/
export function extractVpaasTenantFromPath(path: string) {
const [ , tenant ] = path.split('/');
if (tenant.startsWith(VPAAS_TENANT_PREFIX)) {
return tenant;
}
return '';
}
/**
* Returns the vpaas tenant.
*
* @param {Object} state - The global state.
* @returns {string}
*/
export function getVpaasTenant(state: Object) {
return extractVpaasTenantFromPath(state['features/base/connection'].locationURL.pathname);
}
/**
* Returns true if the current meeting is a vpaas one.
*
* @param {Object} state - The state of the app.
* @returns {boolean}
*/
export function isVpaasMeeting(state: Object) {
const { billingCounterUrl, iAmRecorder, iAmSipGateway } = state['features/base/config'];
const { jwt } = state['features/base/jwt'];
const isAllowed = iAmRecorder || iAmSipGateway || Boolean(jwt);
return Boolean(
billingCounterUrl
&& extractVpaasTenantFromPath(
state['features/base/connection'].locationURL.pathname)
&& isAllowed
);
}
/**
* Sends a billing counter request.
*
* @param {Object} reqData - The request info.
* @param {string} reqData.baseUrl - The base url for the request.
* @param {string} billingId - The unique id of the client.
* @param {string} jwt - The JWT token.
* @param {string} tenat - The client tenant.
* @returns {void}
*/
export async function sendCountRequest({ baseUrl, billingId, jwt, tenant }: {
baseUrl: string,
billingId: string,
jwt: string,
tenant: string
}) {
const fullUrl = `${baseUrl}/${encodeURIComponent(tenant)}/${billingId}`;
const headers = {
'Authorization': `Bearer ${jwt}`
};
try {
const res = await fetch(fullUrl, {
method: 'GET',
headers
});
if (!res.ok) {
logger.error('Status error:', res.status);
}
} catch (err) {
logger.error('Could not send request', err);
}
}
/**
* Returns the stored billing id (or generates a new one if none is present).
*
* @returns {string}
*/
export function getBillingId() {
let billingId = jitsiLocalStorage.getItem(BILLING_ID);
if (!billingId) {
billingId = uuid.v4();
jitsiLocalStorage.setItem(BILLING_ID, billingId);
}
return billingId;
}
/**
* Returns the billing id for vpaas meetings.
*
* @param {Object} state - The state of the app.
* @returns {string | undefined}
*/
export function getVpaasBillingId(state: Object) {
if (isVpaasMeeting(state)) {
return getBillingId();
}
}

View File

@@ -1,29 +0,0 @@
import { ReducerRegistry } from '../base/redux';
import {
SET_ENDPOINT_COUNTED
} from './actionTypes';
const DEFAULT_STATE = {
endpointCounted: false
};
/**
* Listen for actions that mutate the billing-counter state
*/
ReducerRegistry.register(
'features/billing-counter', (state = DEFAULT_STATE, action) => {
switch (action.type) {
case SET_ENDPOINT_COUNTED: {
return {
...state,
endpointCounted: true
};
}
default:
return state;
}
},
);

View File

@@ -108,6 +108,6 @@ export function _mapStateToProps(state: Object) {
_isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
_isOpen: isOpen,
_messages: messages,
_showNamePrompt: !_localParticipant.name
_showNamePrompt: !_localParticipant?.name
};
}

View File

@@ -0,0 +1,109 @@
// @flow
import React from 'react';
import { translate } from '../../../base/i18n';
import { IconChat } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import ChatCounter from './ChatCounter';
/**
* The type of the React {@code Component} props of {@link ChatButton}.
*/
type Props = AbstractButtonProps & {
/**
* Whether or not the chat feature is currently displayed.
*/
_chatOpen: boolean,
/**
* External handler for click action.
*/
handleClick: Function
};
/**
* Implementation of a button for accessing chat pane.
*/
class ChatButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.chat';
icon = IconChat;
label = 'toolbar.openChat';
toggledLabel = 'toolbar.closeChat';
/**
* Retrieves tooltip dynamically.
*/
get tooltip() {
if (this._isToggled()) {
return 'toolbar.closeChat';
}
return 'toolbar.openChat';
}
/**
* Required by linter due to AbstractButton overwritten prop being writable.
*
* @param {string} value - The value.
*/
set tooltip(value) {
return value;
}
/**
* Handles clicking / pressing the button, and opens the appropriate dialog.
*
* @protected
* @returns {void}
*/
_handleClick() {
this.props.handleClick();
}
/**
* Indicates whether this button is in toggled state or not.
*
* @override
* @protected
* @returns {boolean}
*/
_isToggled() {
return this.props._chatOpen;
}
/**
* Overrides AbstractButton's {@link Component#render()}.
*
* @override
* @protected
* @returns {boReact$Nodeolean}
*/
render(): React$Node {
return (
<div
className = 'toolbar-button-with-badge'
key = 'chatcontainer'>
{super.render()}
<ChatCounter />
</div>
);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = state => {
return {
_chatOpen: state['features/chat'].isOpen
};
};
export default translate(connect(mapStateToProps)(ChatButton));

View File

@@ -1,5 +1,6 @@
// @flow
export { default as Chat } from './Chat';
export { default as ChatButton } from './ChatButton';
export { default as ChatCounter } from './ChatCounter';
export { default as ChatPrivacyDialog } from './ChatPrivacyDialog';

View File

@@ -1,5 +1,8 @@
// @flow
import { batch } from 'react-redux';
import { ENDPOINT_REACTION_NAME } from '../../../modules/API/constants';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import {
CONFERENCE_JOINED,
@@ -19,7 +22,16 @@ import {
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
import { openDisplayNamePrompt } from '../display-name';
import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
import { pushReactions } from '../reactions/actions.any';
import { getReactionMessageFromBuffer } from '../reactions/functions.any';
import { endpointMessageReceived } from '../subtitles';
import { showToolbox } from '../toolbox/actions';
import {
hideToolbox,
setToolboxTimeout,
setToolboxVisible
} from '../toolbox/actions.web';
import { ADD_MESSAGE, SEND_MESSAGE, OPEN_CHAT, CLOSE_CHAT } from './actionTypes';
import { addMessage, clearMessages } from './actions';
@@ -143,6 +155,15 @@ MiddlewareRegistry.register(store => next => action => {
}
break;
}
case ADD_REACTION_MESSAGE: {
_handleReceivedMessage(store, {
id: localParticipant.id,
message: action.message,
privateMessage: false,
timestamp: Date.now()
}, false);
}
}
return next(action);
@@ -189,7 +210,6 @@ StateListenerRegistry.register(
* @returns {void}
*/
function _addChatMsgListener(conference, store) {
if (store.getState()['features/base/config'].iAmRecorder) {
// We don't register anything on web if we are in iAmRecorder mode
return;
@@ -219,6 +239,34 @@ function _addChatMsgListener(conference, store) {
}
);
conference.on(
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
(...args) => {
store.dispatch(endpointMessageReceived(...args));
if (args && args.length >= 2) {
const [ { _id }, eventData ] = args;
if (eventData.name === ENDPOINT_REACTION_NAME) {
batch(() => {
store.dispatch(setToolboxVisible(true));
store.dispatch(setToolboxTimeout(
() => store.dispatch(hideToolbox()),
5000)
);
store.dispatch(pushReactions(eventData.reactions));
});
_handleReceivedMessage(store, {
id: _id,
message: getReactionMessageFromBuffer(eventData.reactions),
privateMessage: false,
timestamp: eventData.timestamp
}, false);
}
}
});
conference.on(
JitsiConferenceEvents.CONFERENCE_ERROR, (errorType, error) => {
errorType === JitsiConferenceErrors.CHAT_ERROR && _handleChatError(store, error);
@@ -247,14 +295,20 @@ function _handleChatError({ dispatch }, error) {
*
* @param {Store} store - The Redux store.
* @param {Object} message - The message object.
* @param {boolean} shouldPlaySound - Whether or not to play the incoming message sound.
* @returns {void}
*/
function _handleReceivedMessage({ dispatch, getState }, { id, message, privateMessage, timestamp }) {
function _handleReceivedMessage({ dispatch, getState },
{ id, message, privateMessage, timestamp },
shouldPlaySound = true
) {
// Logic for all platforms:
const state = getState();
const { isOpen: isChatOpen } = state['features/chat'];
const { disableIncomingMessageSound } = state['features/base/config'];
const { soundsIncomingMessage: soundEnabled } = state['features/base/settings'];
if (!isChatOpen) {
if (!disableIncomingMessageSound && soundEnabled && shouldPlaySound && !isChatOpen) {
dispatch(playSound(INCOMING_MSG_SOUND_ID));
}

View File

@@ -16,7 +16,7 @@ import { translate } from '../../base/i18n';
import { Icon, IconClose } from '../../base/icons';
import { browser } from '../../base/lib-jitsi-meet';
import { connect } from '../../base/redux';
import { isVpaasMeeting } from '../../billing-counter/functions';
import { isVpaasMeeting } from '../../jaas/functions';
import logger from '../logger';

View File

@@ -23,6 +23,7 @@ import { AddPeopleDialog, CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList } from '../../../lobby';
import { BackButtonRegistry } from '../../../mobile/back-button';
import { ParticipantsPane } from '../../../participants-pane/components/native';
import { Captions } from '../../../subtitles';
import { setToolboxVisible } from '../../../toolbox/actions';
import { Toolbox } from '../../../toolbox/components/native';
@@ -71,6 +72,11 @@ type Props = AbstractProps & {
*/
_fullscreenEnabled: boolean,
/**
* The indicator which determines if the participants pane is open.
*/
_isParticipantsPaneOpen: boolean,
/**
* The ID of the participant currently on stage (if any)
*/
@@ -237,6 +243,7 @@ class Conference extends AbstractConference<Props, *> {
_renderContent() {
const {
_connecting,
_isParticipantsPaneOpen,
_largeVideoParticipantId,
_reducedUI,
_shouldDisplayTileView
@@ -296,11 +303,14 @@ class Conference extends AbstractConference<Props, *> {
</SafeAreaView>
<TestConnectionInfo />
{ this._renderConferenceNotification() }
{ this._renderConferenceModals() }
{_shouldDisplayTileView && <Toolbox />}
{ _isParticipantsPaneOpen && <ParticipantsPane /> }
</>
);
}
@@ -391,6 +401,7 @@ function _mapStateToProps(state) {
membersOnly,
leaving
} = state['features/base/conference'];
const { isOpen } = state['features/participants-pane'];
const { aspectRatio, reducedUI } = state['features/base/responsive-ui'];
// XXX There is a window of time between the successful establishment of the
@@ -412,6 +423,7 @@ function _mapStateToProps(state) {
_connecting: Boolean(connecting_),
_filmstripVisible: isFilmstripVisible(state),
_fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true),
_isParticipantsPaneOpen: isOpen,
_largeVideoParticipantId: state['features/large-video'].participantId,
_pictureInPictureEnabled: getFeatureFlag(state, PIP_ENABLED),
_reducedUI: reducedUI,

View File

@@ -15,7 +15,7 @@ import { Filmstrip } from '../../../filmstrip';
import { CalleeInfoContainer } from '../../../invite';
import { LargeVideo } from '../../../large-video';
import { KnockingParticipantList, LobbyScreen } from '../../../lobby';
import { ParticipantsPane } from '../../../participants-pane/components';
import { ParticipantsPane } from '../../../participants-pane/components/web';
import { getParticipantsPaneOpen } from '../../../participants-pane/functions';
import { Prejoin, isPrejoinPageVisible } from '../../../prejoin';
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';

View File

@@ -8,7 +8,7 @@ import { getParticipantCount } from '../../../base/participants/functions';
import { connect } from '../../../base/redux';
import { E2EELabel } from '../../../e2ee';
import { LocalRecordingLabel } from '../../../local-recording';
import { RecordingLabel } from '../../../recording';
import { getSessionStatusToShow, RecordingLabel } from '../../../recording';
import { isToolboxVisible } from '../../../toolbox/functions.web';
import { TranscribingLabel } from '../../../transcribing';
import { VideoQualityLabel } from '../../../video-quality';
@@ -38,6 +38,11 @@ type Props = {
*/
_hideConferenceTimer: boolean,
/**
* Whether the recording label should be shown or not.
*/
_hideRecordingLabel: boolean,
/**
* Whether the participant count should be shown or not.
*/
@@ -52,7 +57,20 @@ type Props = {
/**
* Indicates whether the component should be visible or not.
*/
_visible: boolean
_visible: boolean,
/**
* Whether or not the recording label is visible.
*/
_recordingLabel: boolean
};
const getLeftMargin = () => {
const subjectContainerWidth = document.getElementById('subject-container')?.clientWidth ?? 0;
const recContainerWidth = document.getElementById('rec-container')?.clientWidth ?? 0;
const subjectDetailsContainer = document.getElementById('subject-details-container')?.clientWidth ?? 0;
return (subjectContainerWidth - recContainerWidth - subjectDetailsContainer) / 2;
};
/**
@@ -66,29 +84,53 @@ function ConferenceInfo(props: Props) {
_hideConferenceNameAndTimer,
_hideConferenceTimer,
_showParticipantCount,
_hideRecordingLabel,
_subject,
_fullWidth,
_visible
_visible,
_recordingLabel
} = props;
return (
<div className = { `subject ${_visible ? 'visible' : ''}` }>
<div className = { `subject-info-container${_fullWidth ? ' subject-info-container--full-width' : ''}` }>
{
!_hideConferenceNameAndTimer
&& <div className = 'subject-info'>
{ _subject && <span className = 'subject-text'>{ _subject }</span>}
{ !_hideConferenceTimer && <ConferenceTimer /> }
</div>
<div className = { `subject ${_recordingLabel ? 'recording' : ''} ${_visible ? 'visible' : ''}` }>
<div
className = { `subject-info-container${_fullWidth ? ' subject-info-container--full-width' : ''}` }
id = 'subject-container'>
{!_hideRecordingLabel && <div
className = 'show-always'
id = 'rec-container'
// eslint-disable-next-line react-native/no-inline-styles
style = {{
marginLeft: !_recordingLabel || _visible ? 0 : getLeftMargin()
}}>
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
<LocalRecordingLabel />
</div>
}
{ _showParticipantCount && <ParticipantsCount /> }
<E2EELabel />
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
<LocalRecordingLabel />
<TranscribingLabel />
<VideoQualityLabel />
<InsecureRoomNameLabel />
<div
className = 'subject-details-container'
id = 'subject-details-container'>
{
!_hideConferenceNameAndTimer
&& <div className = 'subject-info'>
{ _subject && <span className = 'subject-text'>{ _subject }</span>}
{ !_hideConferenceTimer && <ConferenceTimer /> }
</div>
}
{ _showParticipantCount && <ParticipantsCount /> }
<E2EELabel />
{_hideRecordingLabel && (
<>
<RecordingLabel mode = { JitsiRecordingConstants.mode.FILE } />
<RecordingLabel mode = { JitsiRecordingConstants.mode.STREAM } />
<LocalRecordingLabel />
</>
)}
<TranscribingLabel />
<VideoQualityLabel />
<InsecureRoomNameLabel />
</div>
</div>
</div>
);
@@ -109,16 +151,32 @@ function ConferenceInfo(props: Props) {
*/
function _mapStateToProps(state) {
const participantCount = getParticipantCount(state);
const { hideConferenceTimer, hideConferenceSubject, hideParticipantsStats } = state['features/base/config'];
const {
hideConferenceTimer,
hideConferenceSubject,
hideParticipantsStats,
hideRecordingLabel,
iAmRecorder
} = state['features/base/config'];
const { clientWidth } = state['features/base/responsive-ui'];
const shouldHideRecordingLabel = hideRecordingLabel || iAmRecorder;
const fileRecordingStatus = getSessionStatusToShow(state, JitsiRecordingConstants.mode.FILE);
const streamRecordingStatus = getSessionStatusToShow(state, JitsiRecordingConstants.mode.STREAM);
const isFileRecording = fileRecordingStatus ? fileRecordingStatus !== JitsiRecordingConstants.status.OFF : false;
const isStreamRecording = streamRecordingStatus
? streamRecordingStatus !== JitsiRecordingConstants.status.OFF : false;
const { isEngaged } = state['features/local-recording'];
return {
_hideConferenceNameAndTimer: clientWidth < 300,
_hideConferenceTimer: Boolean(hideConferenceTimer),
_hideRecordingLabel: shouldHideRecordingLabel,
_fullWidth: state['features/video-layout'].tileViewEnabled,
_showParticipantCount: participantCount > 2 && !hideParticipantsStats,
_subject: hideConferenceSubject ? '' : getConferenceName(state),
_visible: isToolboxVisible(state)
_visible: isToolboxVisible(state),
_recordingLabel: (isFileRecording || isStreamRecording || isEngaged) && !shouldHideRecordingLabel
};
}

View File

@@ -1,30 +1,9 @@
import { getName } from '../app/functions.web';
import { isSuboptimalBrowser } from '../base/environment';
import { translateToHTML } from '../base/i18n';
import { getLocalParticipant } from '../base/participants';
import { toState } from '../base/redux';
import { getBackendSafePath, getJitsiMeetGlobalNS } from '../base/util';
import { getVpaasBillingId } from '../billing-counter/functions';
import { showWarningNotification } from '../notifications';
import { createRnnoiseProcessor } from '../stream-effects/rnnoise';
export * from './functions.any';
/**
* Returns the result of getWiFiStats from the global NS or does nothing
(returns empty result).
* Fixes a concurrency problem where we need to pass a function when creating
* a JitsiConference, but that method is added to the context later.
*
* @returns {Promise}
* @private
*/
const getWiFiStatsMethod = () => {
const gloabalNS = getJitsiMeetGlobalNS();
return gloabalNS.getWiFiStats ? gloabalNS.getWiFiStats() : Promise.resolve('{}');
};
/**
* Shows the suboptimal experience notification if needed.
*
@@ -50,49 +29,3 @@ export function maybeShowSuboptimalExperienceNotification(dispatch, t) {
);
}
}
/**
* Returns an object aggregating the conference options.
*
* @param {Object|Function} stateful - The redux store state.
* @returns {Object} - Options object.
*/
export function getConferenceOptions(stateful) {
const state = toState(stateful);
const options = state['features/base/config'];
const { locationURL } = state['features/base/connection'];
const { tenant } = state['features/base/jwt'];
const { email, name: nick } = getLocalParticipant(state);
if (tenant) {
options.siteID = tenant;
}
if (options.enableDisplayNameInStats && nick) {
options.statisticsDisplayName = nick;
}
if (options.enableEmailInStats && email) {
options.statisticsId = email;
}
if (locationURL) {
options.confID = `${locationURL.host}${getBackendSafePath(locationURL.pathname)}`;
}
options.applicationName = getName();
options.getWiFiStatsMethod = getWiFiStatsMethod;
options.createVADProcessor = createRnnoiseProcessor;
options.billingId = getVpaasBillingId(state);
// Disable CallStats, if requessted.
if (options.disableThirdPartyRequests) {
delete options.callStatsID;
delete options.callStatsSecret;
delete options.getWiFiStatsMethod;
}
return options;
}

View File

@@ -92,11 +92,6 @@ type Props = AbstractProps & {
*/
iconSize: number,
/**
* Whether or not the displays stats are for local video.
*/
isLocalVideo: boolean,
/**
* Relative to the icon from where the popover for more connection details
* should display.
@@ -149,7 +144,9 @@ class ConnectionIndicator extends AbstractConnectionIndicator<Props, AbstractSta
return (
<Popover
className = { rootClassNames }
content = { <ConnectionIndicatorContent participantId = { this.props.participantId } /> }
content = { <ConnectionIndicatorContent
inheritedStats = { this.state.stats }
participantId = { this.props.participantId } /> }
disablePopover = { !this.props.enableStatsDisplay }
position = { this.props.statsPopoverPosition }>
<div className = 'popover-trigger'>

View File

@@ -58,6 +58,11 @@ const QUALITY_TO_WIDTH: Array<Object> = [
*/
type Props = AbstractProps & {
/**
* The audio SSRC of this client.
*/
_audioSsrc: number,
/**
* The current condition of the user's connection, matching one of the
* enumerated values in the library.
@@ -65,9 +70,31 @@ type Props = AbstractProps & {
_connectionStatus: string,
/**
* The audio SSRC of this client.
* Whether or not should display the "Show More" link in the local video
* stats table.
*/
audioSsrc: number,
_disableShowMoreStats: boolean,
/**
* Whether or not should display the "Save Logs" link in the local video
* stats table.
*/
_enableSaveLogs: boolean,
/**
* Whether or not the displays stats are for local video.
*/
_isLocalVideo: boolean,
/**
* Invoked to save the conference logs.
*/
_onSaveLogs: Function,
/**
* The video SSRC of this client.
*/
_videoSsrc: number,
/**
* Css class to apply on container
@@ -80,36 +107,14 @@ type Props = AbstractProps & {
dispatch: Dispatch<any>,
/**
* Whether or not should display the "Show More" link in the local video
* stats table.
* Optional param for passing existing connection stats on component instantiation
*/
disableShowMoreStats: boolean,
/**
* Whether or not should display the "Save Logs" link in the local video
* stats table.
*/
enableSaveLogs: boolean,
/**
* Whether or not the displays stats are for local video.
*/
isLocalVideo: boolean,
inheritedStats: Object,
/**
* Invoked to obtain translated strings.
*/
t: Function,
/**
* The video SSRC of this client.
*/
videoSsrc: number,
/**
* Invoked to save the conference logs.
*/
_onSaveLogs: Function
t: Function
};
/**
@@ -143,7 +148,7 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
autoHideTimeout: undefined,
showIndicator: false,
showMoreStats: false,
stats: {}
stats: props.inheritedStats || {}
};
// Bind event handlers so they are only bound once for every instance.
@@ -174,17 +179,17 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
return (
<ConnectionStatsTable
audioSsrc = { this.props.audioSsrc }
audioSsrc = { this.props._audioSsrc }
bandwidth = { bandwidth }
bitrate = { bitrate }
bridgeCount = { bridgeCount }
codec = { codec }
connectionSummary = { this._getConnectionStatusTip() }
disableShowMoreStats = { this.props.disableShowMoreStats }
disableShowMoreStats = { this.props._disableShowMoreStats }
e2eRtt = { e2eRtt }
enableSaveLogs = { this.props.enableSaveLogs }
enableSaveLogs = { this.props._enableSaveLogs }
framerate = { framerate }
isLocalVideo = { this.props.isLocalVideo }
isLocalVideo = { this.props._isLocalVideo }
maxEnabledResolution = { maxEnabledResolution }
onSaveLogs = { this.props._onSaveLogs }
onShowMore = { this._onToggleShowMore }
@@ -195,7 +200,7 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
serverRegion = { serverRegion }
shouldShowMore = { this.state.showMoreStats }
transport = { transport }
videoSsrc = { this.props.videoSsrc } />
videoSsrc = { this.props._videoSsrc } />
);
}
@@ -303,8 +308,9 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
= participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
const props = {
_connectionStatus: participant?.connectionStatus,
enableSaveLogs: state['features/base/config'].enableSaveLogs,
disableShowMoreStats: state['features/base/config'].disableShowMoreStats
_enableSaveLogs: state['features/base/config'].enableSaveLogs,
_disableShowMoreStats: state['features/base/config'].disableShowMoreStats,
_isLocalVideo: participant?.local
};
if (conference) {
@@ -315,8 +321,8 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
return {
...props,
audioSsrc: firstAudioTrack ? conference.getSsrcByTrack(firstAudioTrack.jitsiTrack) : undefined,
videoSsrc: firstVideoTrack ? conference.getSsrcByTrack(firstVideoTrack.jitsiTrack) : undefined
_audioSsrc: firstAudioTrack ? conference.getSsrcByTrack(firstAudioTrack.jitsiTrack) : undefined,
_videoSsrc: firstVideoTrack ? conference.getSsrcByTrack(firstVideoTrack.jitsiTrack) : undefined
};
}

View File

@@ -3,7 +3,7 @@
import { isMobileBrowser } from '../base/environment/utils';
import { Platform } from '../base/react';
import { URI_PROTOCOL_PATTERN } from '../base/util';
import { isVpaasMeeting } from '../billing-counter/functions';
import { isVpaasMeeting } from '../jaas/functions';
import {
DeepLinkingDesktopPage,
@@ -58,7 +58,7 @@ export function getDeepLinkingPage(state) {
if (launchInWeb
|| !room
|| state['features/base/config'].disableDeepLinking
|| (isVpaasMeeting(state) && !appScheme)) {
|| (isVpaasMeeting(state) && (!appScheme || appScheme === 'com.8x8.meet'))) {
return Promise.resolve();
}

View File

@@ -10,7 +10,7 @@ import {
getDeviceIdByLabel,
groupDevicesByKind,
setAudioInputDeviceAndUpdateSettings,
setAudioOutputDeviceId,
setAudioOutputDevice,
setVideoInputDeviceAndUpdateSettings
} from '../base/devices';
import { isIosMobileBrowser } from '../base/environment/utils';
@@ -189,12 +189,11 @@ export function processExternalDeviceRequest( // eslint-disable-line max-params
if (deviceId) {
switch (device.kind) {
case 'audioinput': {
case 'audioinput':
dispatch(setAudioInputDeviceAndUpdateSettings(deviceId));
break;
}
case 'audiooutput':
setAudioOutputDeviceId(deviceId, dispatch);
dispatch(setAudioOutputDevice(deviceId));
break;
case 'videoinput':
dispatch(setVideoInputDeviceAndUpdateSettings(deviceId));

View File

@@ -288,8 +288,7 @@ function _mapStateToProps(state, ownProps) {
return {
_configuredDisplayName: participant && participant.name,
_nameToDisplay: getParticipantDisplayName(
state, participantID)
_nameToDisplay: getParticipantDisplayName(state, participantID)
};
}

View File

@@ -0,0 +1,45 @@
// @flow
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
type Props = {
/**
* The name to be displayed within the badge.
*/
name: string
}
const useStyles = makeStyles(theme => {
return {
badge: {
background: 'rgba(0, 0, 0, 0.6)',
borderRadius: '3px',
color: theme.palette.text01,
maxWidth: '50%',
overflow: 'hidden',
padding: '2px 16px',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
};
});
/**
* Component that displays a name badge.
*
* @param {Props} props - The props of the component.
* @returns {ReactElement}
*/
const DisplayNameBadge = ({ name }: Props) => {
const classes = useStyles();
return (
<div className = { classes.badge }>
{name}
</div>
);
};
export default DisplayNameBadge;

View File

@@ -0,0 +1,60 @@
// @flow
import { makeStyles } from '@material-ui/core/styles';
import React from 'react';
import { useSelector } from 'react-redux';
import { getLocalParticipant } from '../../../base/participants';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import { getLargeVideoParticipant } from '../../../large-video/functions';
import { isToolboxVisible } from '../../../toolbox/functions.web';
import { isLayoutTileView } from '../../../video-layout';
import DisplayNameBadge from './DisplayNameBadge';
const useStyles = makeStyles(theme => {
return {
badgeContainer: {
...withPixelLineHeight(theme.typography.bodyShortRegularLarge),
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
marginBottom: theme.spacing(2),
transition: 'margin-bottom 0.3s'
},
containerElevated: {
marginBottom: theme.spacing(7)
}
};
});
/**
* Component that renders the dominant speaker's name as a badge above the toolbar in stage view.
*
* @returns {ReactElement|null}
*/
const DominantSpeakerName = () => {
const classes = useStyles();
const largeVideoParticipant = useSelector(getLargeVideoParticipant);
const nameToDisplay = largeVideoParticipant?.name;
const selectedId = largeVideoParticipant?.id;
const localParticipant = useSelector(getLocalParticipant);
const localId = localParticipant?.id;
const isTileView = useSelector(isLayoutTileView);
const toolboxVisible = useSelector(isToolboxVisible);
if (nameToDisplay && selectedId !== localId && !isTileView) {
return (
<div
className = { `${classes.badgeContainer}${toolboxVisible ? '' : ` ${classes.containerElevated}`}` }>
<DisplayNameBadge name = { nameToDisplay } />
</div>
);
}
return null;
};
export default DominantSpeakerName;

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