Compare commits

...

100 Commits

Author SHA1 Message Date
Hristo Terezov
a21a515c40 tmp 2021-08-09 16:57:47 -05: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
178 changed files with 4493 additions and 1570 deletions

View File

@@ -70,11 +70,12 @@ 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-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,6 +187,7 @@ 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(),

View File

@@ -19,6 +19,8 @@ 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'
@@ -28,4 +30,4 @@ project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../
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

@@ -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
@@ -603,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
@@ -732,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

@@ -17,8 +17,7 @@ import {
JitsiConnectionErrors,
JitsiConnectionEvents
} from './react/features/base/lib-jitsi-meet';
import { isVpaasMeeting } from './react/features/billing-counter/functions';
import { getJaasJWT } from './react/features/jaas/functions';
import { isVpaasMeeting, getJaasJWT } from './react/features/jaas/functions';
import { setPrejoinDisplayNameRequired } from './react/features/prejoin/actions';
const logger = Logger.getLogger(__filename);
@@ -89,8 +88,9 @@ export async function connect(id, password, roomName) {
const connectionConfig = Object.assign({}, config);
const state = APP.store.getState();
let { jwt } = state['features/base/jwt'];
const { iAmRecorder, iAmSipGateway } = state['features/base/config'];
if (!jwt && isVpaasMeeting(state, false)) {
if (!iAmRecorder && !iAmSipGateway && !jwt && isVpaasMeeting(state)) {
jwt = await getJaasJWT(state);
APP.store.dispatch(setJWT(jwt));
}

View File

@@ -4,16 +4,28 @@
right: 0;
bottom: 0;
z-index: $drawerZ;
background-color: #141414;
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: calc(80vh - 64px);
background: #242528;
border-radius: 16px 16px 0 0;
overflow-y: hidden;
margin-bottom: env(safe-area-inset-bottom, 0);
width: 100%;
.drawer-toggle {
display: flex;

View File

@@ -90,7 +90,7 @@
width: 20%;
bottom: 0;
left: 40%;
height: 48px;
height: 0;
}
.reactions-menu-popup-container,
@@ -111,8 +111,8 @@ $reactionCount: 20;
line-height: 32px;
width: 32px;
height: 32px;
top: 32px;
left: 10px;
top: 0;
left: 20px;
opacity: 0;
z-index: 1;
@@ -123,8 +123,8 @@ $reactionCount: 20;
@for $i from 1 through $reactionCount {
&.reaction-#{$i} {
animation: animation-#{$i} 5s forwards ease-in-out;
top: #{random(50, 0)}px;
left: #{random(-10, 10)}px;
top: #{random(-40, 10)}px;
left: #{random(0, 30)}px;
}
}
}

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,12 +105,15 @@
margin: 0 auto;
max-width: 100%;
pointer-events: all;
background-color: #131519;
padding-bottom: env(safe-area-inset-bottom, 0);
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;
}
.toolbox-content-wrapper::after {
content: '';
background: $newToolbarBackgroundColor;
padding-bottom: env(safe-area-inset-bottom, 0);
}
.toolbox-content-items {
background: $newToolbarBackgroundColor;
border-radius: 6px;
@@ -118,6 +121,7 @@
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;

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

@@ -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;
}
}
}
/**

View File

@@ -14,11 +14,7 @@
.virtual-background-none:hover {
opacity: 0.5;
border: 2px solid #99bbf3;
@media (min-width: 432px) and (min-width: 432px) and (max-width: 632px) {
height: 60px;
width: 60px;
}
@media (max-width: 432px) {
@media (max-width: 632px) {
height: 60px;
width: 60px;
}
@@ -87,7 +83,7 @@
padding: 0 10px;
}
@media (min-width: 432px) and (max-width: 632px) {
@media (max-width: 632px) {
font-size: 1.5vw;
.desktop-share,
.virtual-background-none,
@@ -106,29 +102,8 @@
width: 60px;
}
}
@media (max-width: 432px) {
@media (max-width: 360px) {
grid-template-columns: auto auto auto;
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: 320px) {
grid-template-columns: auto auto auto;
font-size: 1.5vw;
}
}
@@ -163,7 +138,7 @@
display: none;
left: 96;
bottom: 51;
@media (min-width: 432px) and (max-width: 632px) {
@media (max-width: 632px) {
left: 51px;
}
}
@@ -196,10 +171,7 @@
width: 570px;
margin-bottom: 8px;
z-index: 2;
@media (min-width: 432px) and (max-width: 632px) {
max-width: 336;
}
@media (max-width: 432px) {
@media (max-width: 632px) {
max-width: 336;
}
}

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,6 +58,7 @@ 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-webview', :path => '../node_modules/react-native-webview'
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'

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,11 @@ 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-webrtc (1.89.3):
- React-Core
- react-native-webview (11.0.2):
- React-Core
@@ -348,8 +346,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,6 +392,7 @@ 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-webrtc (from `../node_modules/react-native-webrtc`)
- react-native-webview (from `../node_modules/react-native-webview`)
@@ -475,6 +474,8 @@ 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-webrtc:
@@ -519,7 +520,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 +530,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 +557,9 @@ 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-webrtc: 2e8095a43dd3d95ce796d9750280c0f48aadad3d
react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6
@@ -569,7 +571,7 @@ SPEC CHECKSUMS:
React-RCTText: 4f1b99f228278d2a5e9008eced8dc9c974c4a270
React-RCTVibration: c1041024893fdfdb8371e7c720c437751b711676
ReactCommon: 18014e1d98dbeb9141e935cfe35fc93bd511ffb6
RNCAsyncStorage: bc2f81cc1df90c267ce9ed30bb2dbc93b945a8ee
RNCAsyncStorage: 8324611026e8dc3706f829953aa6e3899f581589
RNDefaultPreference: 56a405ce61033ac77b95004dccd7ac54c2eb50d1
RNDeviceInfo: 72ded653ce636b3f03571e90bed99309a714944e
RNGoogleSignin: 39336070b35fc4cea6a98cf111e00480317be0ae
@@ -578,6 +580,6 @@ SPEC CHECKSUMS:
RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c
PODFILE CHECKSUM: d059cebf82da14a53940a16c24c3330752d4b0c8
PODFILE CHECKSUM: 1fa5a1e259f145d32c1ca968b26dac65cff34b49
COCOAPODS: 1.10.1

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",
@@ -591,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": "",
@@ -703,7 +727,7 @@
"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"
},
@@ -770,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

@@ -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.",
@@ -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",
@@ -588,16 +594,21 @@
"close": "Close",
"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 +751,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 +759,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,6 +822,7 @@
"accessibilityLabel": {
"audioOnly": "Toggle audio only",
"audioRoute": "Select the sound device",
"boo": "Boo",
"callQuality": "Manage video quality",
"cc": "Toggle subtitles",
"chat": "Open / Close chat",
@@ -818,8 +836,8 @@
"hangup": "Leave the meeting",
"help": "Help",
"invite": "Invite people",
"joy": "Laughing Crying",
"kick": "Kick participant",
"laugh": "Laugh",
"like": "Thumbs Up",
"lobbyButton": "Enable/disable lobby mode",
"localRecording": "Toggle local recording controls",
@@ -850,7 +868,6 @@
"shareYourScreen": "Start / Stop sharing your screen",
"shortcuts": "Toggle shortcuts",
"show": "Show on stage",
"smile": "Smile",
"speakerStats": "Toggle speaker statistics",
"surprised": "Surprised",
"tileView": "Toggle tile view",
@@ -869,6 +886,7 @@
"audioOnlyOn": "Enable low bandwidth mode",
"audioRoute": "Select the sound device",
"authenticate": "Authenticate",
"boo": "Boo",
"callQuality": "Manage video quality",
"chat": "Open / Close chat",
"clap": "Clap",
@@ -887,7 +905,7 @@
"hangup": "Leave the meeting",
"help": "Help",
"invite": "Invite people",
"joy": "Joy",
"laugh": "Laugh",
"like": "Thumbs Up",
"lobbyButtonDisable": "Disable lobby mode",
"lobbyButtonEnable": "Enable lobby mode",
@@ -915,11 +933,11 @@
"profile": "Edit your profile",
"raiseHand": "Raise / Lower your hand",
"raiseYourHand": "Raise your hand",
"reactionBoo": "Send boo reaction",
"reactionClap": "Send clap reaction",
"reactionJoy": "Send joy reaction",
"reactionLaugh": "Send laugh reaction",
"reactionLike": "Send thumbs up reaction",
"reactionParty": "Send party popper reaction",
"reactionSmile": "Send smile reaction",
"reactionSurprised": "Send surprised reaction",
"security": "Security options",
"Settings": "Settings",
@@ -927,7 +945,6 @@
"sharedvideo": "Share video",
"shareRoom": "Invite someone",
"shortcuts": "View shortcuts",
"smile": "Smile",
"speakerStats": "Speaker stats",
"startScreenSharing": "Start screen sharing",
"startSubtitles": "Start subtitles",
@@ -1098,6 +1115,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

@@ -574,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

@@ -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.
*

20
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",
@@ -11071,8 +11076,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#89a7e2d9cdd24f48f95f6668ae4d8db1b635cf36",
"from": "github:jitsi/lib-jitsi-meet#89a7e2d9cdd24f48f95f6668ae4d8db1b635cf36",
"version": "github:jitsi/lib-jitsi-meet#fa834c2923e78fe00cc206b1bc99248874d544c6",
"from": "github:jitsi/lib-jitsi-meet#fa834c2923e78fe00cc206b1bc99248874d544c6",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "github:jitsi/sdp-interop#5fc4af6dcf8a6e6af9fedbcd654412fd47b1b4ae",
@@ -15144,8 +15149,9 @@
"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.89.3",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.89.3.tgz",
"integrity": "sha512-K6GbLfOeOhJuxgWx8NKoSMnvl6hkqqOCUHZISABOspj00CXNtJgKvFBuCi+fBeNT/RribOODBFxK2sfHqRz7Ww==",
"requires": {
"base64-js": "^1.1.2",
"cross-os": "^1.3.0",

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#89a7e2d9cdd24f48f95f6668ae4d8db1b635cf36",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#fa834c2923e78fe00cc206b1bc99248874d544c6",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.21",
"moment": "2.29.1",
@@ -87,7 +88,7 @@
"react-native-svg-transformer": "0.14.3",
"react-native-url-polyfill": "1.2.0",
"react-native-watch-connectivity": "0.4.3",
"react-native-webrtc": "github:react-native-webrtc/react-native-webrtc#510d20dd62c1768885a98f36fde83f9e48a723fa",
"react-native-webrtc": "1.89.3",
"react-native-webview": "11.0.2",
"react-native-youtube-iframe": "1.2.3",
"react-redux": "7.1.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';

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,7 @@ 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';

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

@@ -551,7 +551,7 @@ function _updateLocalParticipantInConference({ dispatch, getState }, next, actio
const localParticipant = getLocalParticipant(getState);
if (conference && participant.id === localParticipant.id) {
if (conference && participant.id === localParticipant?.id) {
if ('name' in participant) {
conference.setDisplayName(participant.name);
}

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'
'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

@@ -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.
*/
@@ -57,9 +67,14 @@ type Props = {
renderFooter: ?Function,
/**
* The height of the screen.
*/
_height: number
* Whether to show sliding view or not.
*/
showSlidingView?: boolean,
/**
* The component's external style
*/
style: Object
};
/**
@@ -68,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.
*
@@ -90,7 +115,15 @@ class BottomSheet extends PureComponent<Props> {
* @returns {ReactElement}
*/
render() {
const { _styles, renderHeader, renderFooter, _height } = this.props;
const {
_height,
_styles,
addScrollViewPadding,
renderHeader,
renderFooter,
showSlidingView,
style
} = this.props;
return (
<SlidingView
@@ -98,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 }>
@@ -110,6 +143,7 @@ class BottomSheet extends PureComponent<Props> {
style = { [
styles.sheetItemContainer,
_styles.sheet,
style,
{
maxHeight: _height - 100
}
@@ -118,7 +152,7 @@ class BottomSheet extends PureComponent<Props> {
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false }
style = { styles.scrollView } >
style = { addScrollViewPadding && styles.scrollView } >
{ this.props.children }
</ScrollView>
{ renderFooter && renderFooter() }

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

@@ -21,5 +21,5 @@ import './native';
// used it from source. As an intermediate step, start using the library
// lib-jitsi-meet as a binary on mobile at the time of this writing. In the
// future, implement not packaging it in the application bundle.
import JitsiMeetJS from 'lib-jitsi-meet/lib-jitsi-meet.min';
import JitsiMeetJS from 'lib-jitsi-meet/lib-jitsi-meet.js';
export { JitsiMeetJS as default };

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

@@ -371,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) {
@@ -387,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

@@ -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

@@ -3,3 +3,24 @@ export { default as MiddlewareRegistry } from './MiddlewareRegistry';
export { default as PersistenceRegistry } from './PersistenceRegistry';
export { default as ReducerRegistry } from './ReducerRegistry';
export { default as StateListenerRegistry } from './StateListenerRegistry';
// /**
// *
// */
// MiddlewareRegistry.register(() => next => action => {
// if (performance.mark === undefined) {
// return next(action);
// }
// performance.mark(`${action.type}_start`);
// const result = next(action);
// performance.mark(`${action.type}_end`);
// performance.measure(
// `${action.type}`,
// `${action.type}_start`,
// `${action.type}_end`,
// );
// return result;
// });

View File

@@ -29,12 +29,13 @@ const REDUCED_UI_THRESHOLD = 300;
*/
export function clientResized(clientWidth: number, clientHeight: number) {
return (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { isOpen: isChatOpen } = state['features/chat'];
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
let availableWidth = clientWidth;
if (navigator.product !== 'ReactNative') {
const state = getState();
const { isOpen: isChatOpen } = state['features/chat'];
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
if (isChatOpen) {
availableWidth -= CHAT_SIZE;
}

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

@@ -33,7 +33,8 @@ export const colors = {
success04: '#189B55',
success05: '#1EC26A',
warning05: '#F8AE1A'
warning05: '#F8AE1A',
warning06: '#ED9E1B'
};
// Mapping between the token used and the color
@@ -197,20 +198,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,119 +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.
* @param {boolean} requiredJwt - Whether jwt is required or not.
* @returns {boolean}
*/
export function isVpaasMeeting(state: Object, requiredJwt: boolean = true) {
const { billingCounterUrl, iAmRecorder, iAmSipGateway } = state['features/base/config'];
const { jwt } = state['features/base/jwt'];
const jwtBoolean = requiredJwt ? Boolean(jwt) : true;
const isAllowed = iAmRecorder || iAmSipGateway || jwtBoolean;
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

@@ -22,11 +22,9 @@ import {
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
import { openDisplayNamePrompt } from '../display-name';
import { ADD_REACTIONS_MESSAGE } from '../reactions/actionTypes';
import {
pushReaction
} from '../reactions/actions.any';
import { REACTIONS } from '../reactions/constants';
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 {
@@ -158,13 +156,13 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case ADD_REACTIONS_MESSAGE: {
case ADD_REACTION_MESSAGE: {
_handleReceivedMessage(store, {
id: localParticipant.id,
message: action.message,
privateMessage: false,
timestamp: Date.now()
});
}, false);
}
}
@@ -212,8 +210,6 @@ StateListenerRegistry.register(
* @returns {void}
*/
function _addChatMsgListener(conference, store) {
const reactions = {};
if (store.getState()['features/base/config'].iAmRecorder) {
// We don't register anything on web if we are in iAmRecorder mode
return;
@@ -252,30 +248,21 @@ function _addChatMsgListener(conference, store) {
const [ { _id }, eventData ] = args;
if (eventData.name === ENDPOINT_REACTION_NAME) {
reactions[_id] = reactions[_id] ?? {
timeout: null,
message: ''
};
batch(() => {
store.dispatch(pushReaction(eventData.reaction));
store.dispatch(setToolboxVisible(true));
store.dispatch(setToolboxTimeout(
() => store.dispatch(hideToolbox()),
5000)
);
store.dispatch(pushReactions(eventData.reactions));
});
clearTimeout(reactions[_id].timeout);
reactions[_id].message = `${reactions[_id].message}${REACTIONS[eventData.reaction].message}`;
reactions[_id].timeout = setTimeout(() => {
_handleReceivedMessage(store, {
id: _id,
message: reactions[_id].message,
privateMessage: false,
timestamp: eventData.timestamp
});
delete reactions[_id];
}, 500);
_handleReceivedMessage(store, {
id: _id,
message: getReactionMessageFromBuffer(eventData.reactions),
privateMessage: false,
timestamp: eventData.timestamp
}, false);
}
}
});
@@ -308,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,46 @@ 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 />
<TranscribingLabel />
<VideoQualityLabel />
<InsecureRoomNameLabel />
</div>
</div>
</div>
);
@@ -109,16 +144,30 @@ function ConferenceInfo(props: Props) {
*/
function _mapStateToProps(state) {
const participantCount = getParticipantCount(state);
const { hideConferenceTimer, hideConferenceSubject, hideParticipantsStats } = state['features/base/config'];
const {
hideConferenceTimer,
hideConferenceSubject,
hideParticipantsStats,
hideRecordingLabel
} = state['features/base/config'];
const { clientWidth } = state['features/base/responsive-ui'];
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: hideRecordingLabel,
_fullWidth: state['features/video-layout'].tileViewEnabled,
_showParticipantCount: participantCount > 2 && !hideParticipantsStats,
_subject: hideConferenceSubject ? '' : getConferenceName(state),
_visible: isToolboxVisible(state)
_visible: isToolboxVisible(state),
_recordingLabel: (isFileRecording || isStreamRecording || isEngaged) && !hideRecordingLabel
};
}

View File

@@ -4,7 +4,6 @@ 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';
@@ -85,7 +84,6 @@ export function getConferenceOptions(stateful) {
options.applicationName = getName();
options.getWiFiStatsMethod = getWiFiStatsMethod;
options.createVADProcessor = createRnnoiseProcessor;
options.billingId = getVpaasBillingId(state);
// Disable CallStats, if requessted.
if (options.disableThirdPartyRequests) {

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

@@ -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;

View File

@@ -3,3 +3,4 @@
export { default as DisplayName } from './DisplayName';
export { default as DisplayNameLabel } from './DisplayNameLabel';
export { default as DisplayNamePrompt } from './DisplayNamePrompt';
export { default as DominantSpeakerName } from './DominantSpeakerName';

View File

@@ -15,6 +15,15 @@ import {
const STORE_NAME = 'features/dynamic-branding';
const DEFAULT_STATE = {
/**
* The pool of avatar backgrounds.
*
* @public
* @type {Array<string>}
*/
avatarBackgrounds: [],
/**
* The custom background color for the LargeVideo.
*
@@ -112,10 +121,12 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
didPageUrl,
inviteDomain,
logoClickUrl,
logoImageUrl
logoImageUrl,
avatarBackgrounds
} = action.value;
return {
avatarBackgrounds,
backgroundColor,
backgroundImageUrl,
defaultBranding,

View File

@@ -1,9 +1,7 @@
// @flow
import { MEDIA_TYPE, VIDEO_TYPE } from '../base/media';
import { getLocalParticipant } from '../base/participants';
import { StateListenerRegistry } from '../base/redux';
import { getTrackByMediaTypeAndParticipant } from '../base/tracks';
import { appendSuffix } from '../display-name';
import { shouldDisplayTileView } from '../video-layout';
@@ -45,12 +43,7 @@ StateListenerRegistry.register(
*/
StateListenerRegistry.register(
/* selector */ state => state['features/large-video'].participantId,
/* listener */ (participantId, store) => {
const videoTrack = getTrackByMediaTypeAndParticipant(
store.getState()['features/base/tracks'], MEDIA_TYPE.VIDEO, participantId);
if (videoTrack && videoTrack.videoType === VIDEO_TYPE.CAMERA) {
APP.API.notifyOnStageParticipantChanged(participantId);
}
/* listener */ participantId => {
APP.API.notifyOnStageParticipantChanged(participantId);
}
);

View File

@@ -4,8 +4,8 @@ import type { Dispatch } from 'redux';
import { FEEDBACK_REQUEST_IN_PROGRESS } from '../../../modules/UI/UIErrors';
import { openDialog } from '../base/dialog';
import { isVpaasMeeting } from '../billing-counter/functions';
import { extractFqnFromPath } from '../dynamic-branding/functions';
import { isVpaasMeeting } from '../jaas/functions';
import {
CANCEL_FEEDBACK,

View File

@@ -0,0 +1,59 @@
// @flow
import {
SET_FILMSTRIP_ENABLED,
SET_FILMSTRIP_VISIBLE,
SET_VISIBLE_REMOTE_PARTICIPANTS
} from './actionTypes';
/**
* Sets whether the filmstrip is enabled.
*
* @param {boolean} enabled - Whether the filmstrip is enabled.
* @returns {{
* type: SET_FILMSTRIP_ENABLED,
* enabled: boolean
* }}
*/
export function setFilmstripEnabled(enabled: boolean) {
return {
type: SET_FILMSTRIP_ENABLED,
enabled
};
}
/**
* Sets whether the filmstrip is visible.
*
* @param {boolean} visible - Whether the filmstrip is visible.
* @returns {{
* type: SET_FILMSTRIP_VISIBLE,
* visible: boolean
* }}
*/
export function setFilmstripVisible(visible: boolean) {
return {
type: SET_FILMSTRIP_VISIBLE,
visible
};
}
/**
* Sets the list of the visible participants in the filmstrip by storing the start and end index from the remote
* participants array.
*
* @param {number} startIndex - The start index from the remote participants array.
* @param {number} endIndex - The end index from the remote participants array.
* @returns {{
* type: SET_VISIBLE_REMOTE_PARTICIPANTS,
* startIndex: number,
* endIndex: number
* }}
*/
export function setVisibleRemoteParticipants(startIndex: number, endIndex: number) {
return {
type: SET_VISIBLE_REMOTE_PARTICIPANTS,
startIndex,
endIndex
};
}

View File

@@ -1,42 +1,10 @@
// @flow
import {
SET_FILMSTRIP_ENABLED,
SET_FILMSTRIP_VISIBLE,
SET_TILE_VIEW_DIMENSIONS
} from './actionTypes';
import { getParticipantCountWithFake } from '../base/participants';
/**
* Sets whether the filmstrip is enabled.
*
* @param {boolean} enabled - Whether the filmstrip is enabled.
* @returns {{
* type: SET_FILMSTRIP_ENABLED,
* enabled: boolean
* }}
*/
export function setFilmstripEnabled(enabled: boolean) {
return {
type: SET_FILMSTRIP_ENABLED,
enabled
};
}
/**
* Sets whether the filmstrip is visible.
*
* @param {boolean} visible - Whether the filmstrip is visible.
* @returns {{
* type: SET_FILMSTRIP_VISIBLE,
* visible: boolean
* }}
*/
export function setFilmstripVisible(visible: boolean) {
return {
type: SET_FILMSTRIP_VISIBLE,
visible
};
}
import { SET_TILE_VIEW_DIMENSIONS } from './actionTypes';
import { SQUARE_TILE_ASPECT_RATIO, TILE_MARGIN } from './constants';
import { getColumnCount } from './functions.native';
/**
* Sets the dimensions of the tile view grid. The action is only partially implemented on native as not all
@@ -52,11 +20,40 @@ export function setFilmstripVisible(visible: boolean) {
* dimensions: Object
* }}
*/
export function setTileViewDimensions({ thumbnailSize }: Object) {
return {
type: SET_TILE_VIEW_DIMENSIONS,
dimensions: {
thumbnailSize
export function setTileViewDimensions() {
return (dispatch: Function, getState: Function) => {
const state = getState();
const participantCount = getParticipantCountWithFake(state);
const { clientHeight: height, clientWidth: width } = state['features/base/responsive-ui'];
const columns = getColumnCount(state);
const heightToUse = height - (TILE_MARGIN * 2);
const widthToUse = width - (TILE_MARGIN * 2);
let tileWidth;
// If there is going to be at least two rows, ensure that at least two
// rows display fully on screen.
if (participantCount / columns > 1) {
tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
} else {
tileWidth = Math.min(widthToUse / columns, heightToUse);
}
const tileHeight = Math.floor(tileWidth / SQUARE_TILE_ASPECT_RATIO);
tileWidth = Math.floor(tileWidth);
dispatch({
type: SET_TILE_VIEW_DIMENSIONS,
dimensions: {
columns,
thumbnailSize: {
height: tileHeight,
width: tileWidth
}
}
});
};
}
export * from './actions.any';

View File

@@ -7,7 +7,6 @@ import {
SET_HORIZONTAL_VIEW_DIMENSIONS,
SET_TILE_VIEW_DIMENSIONS,
SET_VERTICAL_VIEW_DIMENSIONS,
SET_VISIBLE_REMOTE_PARTICIPANTS,
SET_VOLUME
} from './actionTypes';
import {
@@ -137,7 +136,7 @@ export function clickOnVideo(n: number) {
}
/**
* Sets the volume for a thumnail's audio.
* Sets the volume for a thumbnail's audio.
*
* @param {string} participantId - The participant ID asociated with the audio.
* @param {string} volume - The volume level.
@@ -155,24 +154,4 @@ export function setVolume(participantId: string, volume: number) {
};
}
/**
* Sets the list of the visible participants in the filmstrip by storing the start and end index from the remote
* participants array.
*
* @param {number} startIndex - The start index from the remote participants array.
* @param {number} endIndex - The end index from the remote participants array.
* @returns {{
* type: SET_VISIBLE_REMOTE_PARTICIPANTS,
* startIndex: number,
* endIndex: number
* }}
*/
export function setVisibleRemoteParticipants(startIndex: number, endIndex: number) {
return {
type: SET_VISIBLE_REMOTE_PARTICIPANTS,
startIndex,
endIndex
};
}
export * from './actions.native';
export * from './actions.any';

View File

@@ -1,17 +1,20 @@
// @flow
import React, { Component } from 'react';
import { SafeAreaView, ScrollView } from 'react-native';
import { FlatList, SafeAreaView, ScrollView } from 'react-native';
import { Platform } from '../../../base/react';
import { connect } from '../../../base/redux';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import { setVisibleRemoteParticipants } from '../../actions';
import { isFilmstripVisible } from '../../functions';
import LocalThumbnail from './LocalThumbnail';
import Thumbnail from './Thumbnail';
import styles from './styles';
/**
* Filmstrip component's property types.
*/
@@ -30,7 +33,12 @@ type Props = {
/**
* The indicator which determines whether the filmstrip is visible.
*/
_visible: boolean
_visible: boolean,
/**
* Invoked to trigger state changes in Redux.
*/
dispatch: Dispatch<any>,
};
/**
@@ -74,6 +82,65 @@ class Filmstrip extends Component<Props> {
this._separateLocalThumbnail = Platform.OS !== 'android';
}
/**
*
* @param {*} item
* @returns
*/
_keyExtractor(item) {
return item;
}
/**
*
* @param {*} data
* @param {*} index
* @returns
*/
_getItemLayout(data, index) {
const { _aspectRatio } = this.props;
const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
const length = isNarrowAspectRatio ? styles.thumbnail.width : styles.thumbnail.height;
return {
length,
offset: length * index,
index
};
}
/**
* A handler for visible items changes.
*
* @param {Object} data - The visible items data.
* @param {Array<Object>} data.viewableItems - The visible items array.
* @returns {void}
*/
_onViewableItemsChanged({ viewableItems = 0 }) {
console.log(`visisble indexes ${JSON.stringify((viewableItems || []).map(i => i.index))}`);
const indexArray = viewableItems.map(i => i.index);
this.props.dispatch(setVisibleRemoteParticipants(Math.min(indexArray), Math.max(indexArray)));
}
/**
* Creates React Elements to display each participant in a thumbnail. Each
* tile will be.
*
* @private
* @returns {ReactElement[]}
*/
_renderThumbnail({ item, index /*, separators */ }) {
console.log(`1rendering thumbnail with index ${index}`);
return (
<Thumbnail
index = { index }
key = { item }
participantID = { item } />)
;
}
/**
* Implements React's {@link Component#render()}.
*
@@ -89,6 +156,8 @@ class Filmstrip extends Component<Props> {
const isNarrowAspectRatio = _aspectRatio === ASPECT_RATIO_NARROW;
const filmstripStyle = isNarrowAspectRatio ? styles.filmstripNarrow : styles.filmstripWide;
const participants = this._sort(_participants, isNarrowAspectRatio);
const initialRowsToRender = Math.ceil(_height / _thumbnailHeight);
return (
<SafeAreaView style = { filmstripStyle }>
@@ -97,6 +166,18 @@ class Filmstrip extends Component<Props> {
&& !isNarrowAspectRatio
&& <LocalThumbnail />
}
<FlatList
data = { participants }
getItemLayout = { this._getItemLayout }
horizontal = { isNarrowAspectRatio }
initialNumToRender = { initialRowsToRender }
key = { isNarrowAspectRatio ? 'narrow' : 'wide' }
keyExtractor = { this._keyExtractor }
onViewableItemsChanged = { this._onViewableItemsChanged }
renderItem = { this._renderThumbnail }
style = { styles.scrollView }
viewabilityConfig = { this.viewabilityConfig }
windowSize = { 2 } />
<ScrollView
horizontal = { isNarrowAspectRatio }
showsHorizontalScrollIndicator = { false }
@@ -128,6 +209,40 @@ class Filmstrip extends Component<Props> {
);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _columns, _height, _thumbnailHeight, _width, onClick } = this.props;
const participants = this._getSortedParticipants();
const initialRowsToRender = Math.ceil(_height / _thumbnailHeight);
return (
<TouchableWithoutFeedback onPress = { onClick }>
<FlatList
contentContainerStyle = { styles.tileView }
data = { participants }
horizontal = { false }
initialNumToRender = { initialRowsToRender }
key = { _columns }
keyExtractor = { this._keyExtractor }
numColumns = { _columns }
onViewableItemsChanged = { this._onViewableItemsChanged }
renderItem = { this._renderThumbnail }
style = {{
height: _height,
width: _width
}}
viewabilityConfig = { this.viewabilityConfig }
windowSize = { 2 } />
</TouchableWithoutFeedback>
);
}
/**
* Sorts a specific array of {@code Participant}s in display order.
*

View File

@@ -32,6 +32,8 @@ import RaisedHandIndicator from './RaisedHandIndicator';
import ScreenShareIndicator from './ScreenShareIndicator';
import VideoMutedIndicator from './VideoMutedIndicator';
import styles, { AVATAR_SIZE } from './styles';
import { PureComponent } from 'react';
import { SQUARE_TILE_ASPECT_RATIO } from '../../constants';
/**
* Thumbnail component's property types.
@@ -112,103 +114,149 @@ type Props = {
* @param {Props} props - Properties passed to this functional component.
* @returns {Component} - A React component.
*/
function Thumbnail(props: Props) {
const {
_audioMuted: audioMuted,
_largeVideo: largeVideo,
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
_renderModeratorIndicator: renderModeratorIndicator,
_participant: participant,
_styles,
_videoTrack: videoTrack,
dispatch,
disableTint,
renderDisplayName,
tileView
} = props;
class Thumbnail extends PureComponent<Props> {
/**
*
* @param {*} props
* @returns
*/
constructor(props: Props) {
super(props);
this._onClick = this._onClick.bind(this);
this._onThumbnailLongPress = this._onThumbnailLongPress.bind(this);
}
/**
* Thumbnail click handler.
*
* @returns {void}
*/
_onClick() {
const { _participantId, _pinned, dispatch, tileView } = this.props;
const participantId = participant.id;
const participantInLargeVideo
= participantId === largeVideo.participantId;
const videoMuted = !videoTrack || videoTrack.muted;
const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
const onClick = useCallback(() => {
if (tileView) {
dispatch(toggleToolboxVisible());
} else {
dispatch(pinParticipant(participant.pinned ? null : participant.id));
dispatch(pinParticipant(_pinned ? null : _participantId));
}
}, [ participant, tileView, dispatch ]);
const onThumbnailLongPress = useCallback(() => {
if (participant.local) {
}
/**
* Thumbnail long press handler.
*
* @returns {void}
*/
_onThumbnailLongPress() {
const { _participantId, _local, dispatch } = this.props;
if (_local) {
dispatch(openDialog(ConnectionStatusComponent, {
participantID: participant.id
participantID: _participantId
}));
} else {
dispatch(openDialog(RemoteVideoMenu, {
participant
participantId: _participantId
}));
}
}, [ participant, dispatch ]);
}
return (
<Container
onClick = { onClick }
onLongPress = { onThumbnailLongPress }
style = { [
styles.thumbnail,
participant.pinned && !tileView
? _styles.thumbnailPinned : null,
props.styleOverrides || null
] }
touchFeedback = { false }>
/**
*
* @returns
*/
render() {
const {
_audioMuted: audioMuted,
_isScreenShare: isScreenShare,
_isFakeParticipant,
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
_renderModeratorIndicator: renderModeratorIndicator,
_participantId: participantId,
_participantInLargeVideo: participantInLargeVideo,
_pinned,
_styles,
_videoMuted: videoMuted,
disableTint,
height,
index,
renderDisplayName,
tileView
} = this.props;
<ParticipantView
avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
disableVideo = { isScreenShare || participant.isFakeParticipant }
participantId = { participantId }
style = { _styles.participantViewStyle }
tintEnabled = { participantInLargeVideo && !disableTint }
tintStyle = { _styles.activeThumbnailTint }
zOrder = { 1 } />
const styleOverrides = tileView ? {
aspectRatio: SQUARE_TILE_ASPECT_RATIO,
flex: 0,
height,
maxHeight: null,
maxWidth: null,
width: null
} : null;
{ renderDisplayName && <Container style = { styles.displayNameContainer }>
<DisplayNameLabel participantId = { participantId } />
</Container> }
console.log(`rendering thumbnail with index ${index}`);
{ renderModeratorIndicator
&& <View style = { styles.moderatorIndicatorContainer }>
<ModeratorIndicator />
</View>}
{ !participant.isFakeParticipant && <View
return (
<Container
onClick = { this._onClick }
onLongPress = { this._onThumbnailLongPress }
style = { [
styles.thumbnailTopIndicatorContainer,
styles.thumbnailTopLeftIndicatorContainer
] }>
<RaisedHandIndicator participantId = { participant.id } />
{ renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
</View> }
{ !participant.isFakeParticipant && <View
style = { [
styles.thumbnailTopIndicatorContainer,
styles.thumbnailTopRightIndicatorContainer
] }>
<ConnectionIndicator participantId = { participant.id } />
</View> }
{ !participant.isFakeParticipant && <Container style = { styles.thumbnailIndicatorContainer }>
{ audioMuted
&& <AudioMutedIndicator /> }
{ videoMuted
&& <VideoMutedIndicator /> }
{ isScreenShare
&& <ScreenShareIndicator /> }
</Container> }
</Container>
);
styles.thumbnail,
_pinned && !tileView ? _styles.thumbnailPinned : null,
styleOverrides
] }
touchFeedback = { false }>
<ParticipantView
avatarSize = { tileView ? AVATAR_SIZE * 1.5 : AVATAR_SIZE }
disableVideo = { isScreenShare || _isFakeParticipant }
participantId = { participantId }
style = { _styles.participantViewStyle }
tintEnabled = { participantInLargeVideo && !disableTint }
tintStyle = { _styles.activeThumbnailTint }
zOrder = { 1 } />
{
renderDisplayName
&& <Container style = { styles.displayNameContainer }>
<DisplayNameLabel participantId = { participantId } />
</Container>
}
{ renderModeratorIndicator
&& <View style = { styles.moderatorIndicatorContainer }>
<ModeratorIndicator />
</View>
}
{
!_isFakeParticipant
&& <View
style = { [
styles.thumbnailTopIndicatorContainer,
styles.thumbnailTopLeftIndicatorContainer
] }>
<RaisedHandIndicator participantId = { participantId } />
{ renderDominantSpeakerIndicator && <DominantSpeakerIndicator /> }
</View>
}
{
!_isFakeParticipant
&& <View
style = { [
styles.thumbnailTopIndicatorContainer,
styles.thumbnailTopRightIndicatorContainer
] }>
<ConnectionIndicator participantId = { participantId } />
</View>
}
{
!_isFakeParticipant
&& <Container style = { styles.thumbnailIndicatorContainer }>
{ audioMuted && <AudioMutedIndicator /> }
{ videoMuted && <VideoMutedIndicator /> }
{ isScreenShare && <ScreenShareIndicator /> }
</Container>
}
</Container>
);
}
}
/**
@@ -231,20 +279,27 @@ function _mapStateToProps(state, ownProps) {
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, id);
const videoTrack
= getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
const videoMuted = !videoTrack || videoTrack.muted;
const isScreenShare = videoTrack && videoTrack.videoType === VIDEO_TYPE.DESKTOP;
const participantCount = getParticipantCount(state);
const renderDominantSpeakerIndicator = participant && participant.dominantSpeaker && participantCount > 2;
const _isEveryoneModerator = isEveryoneModerator(state);
const renderModeratorIndicator = !_isEveryoneModerator
&& participant && participant.role === PARTICIPANT_ROLE.MODERATOR;
const participantInLargeVideo = id === largeVideo.participantId;
return {
_audioMuted: audioTrack?.muted ?? true,
_largeVideo: largeVideo,
_participant: participant,
_isScreenShare: isScreenShare,
_isFakeParticipant: participant.isFakeParticipant,
_local: participant.local,
_participantInLargeVideo: participantInLargeVideo,
_participantId: id,
_pinned: participant.pinned,
_renderDominantSpeakerIndicator: renderDominantSpeakerIndicator,
_renderModeratorIndicator: renderModeratorIndicator,
_styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
_videoTrack: videoTrack
_videoMuted: videoMuted
};
}

View File

@@ -2,7 +2,7 @@
import React, { Component } from 'react';
import {
ScrollView,
FlatList,
TouchableWithoutFeedback,
View
} from 'react-native';
@@ -10,12 +10,11 @@ import type { Dispatch } from 'redux';
import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import { setTileViewDimensions } from '../../actions.native';
import { SQUARE_TILE_ASPECT_RATIO } from '../../constants';
import Thumbnail from './Thumbnail';
import styles from './styles';
import { setVisibleRemoteParticipants } from '../../actions.web';
/**
* The type of the React {@link Component} props of {@link TileView}.
@@ -27,6 +26,8 @@ type Props = {
*/
_aspectRatio: Symbol,
_columns: number,
/**
* Application's viewport height.
*/
@@ -47,6 +48,8 @@ type Props = {
*/
_remoteParticipants: Array<string>,
_thumbnailHeight: number,
/**
* Application's viewport height.
*/
@@ -63,23 +66,6 @@ type Props = {
onClick: Function
};
/**
* The margin for each side of the tile view. Taken away from the available
* height and width for the tile container to display in.
*
* @private
* @type {number}
*/
const MARGIN = 10;
/**
* The aspect ratio the tiles should display in.
*
* @private
* @type {number}
*/
const TILE_ASPECT_RATIO = 1;
/**
* Implements a React {@link Component} which displays thumbnails in a two
* dimensional grid.
@@ -87,22 +73,63 @@ const TILE_ASPECT_RATIO = 1;
* @extends Component
*/
class TileView extends Component<Props> {
/**
* Implements React's {@link Component#componentDidMount}.
*
* @inheritdoc
* @param {*} props
*/
componentDidMount() {
this._updateReceiverQuality();
constructor(props: Props) {
super(props);
this._keyExtractor = this._keyExtractor.bind(this);
this._getItemLayout = this._getItemLayout.bind(this);
this._onViewableItemsChanged = this._onViewableItemsChanged.bind(this);
this._getSortedParticipants = this._getSortedParticipants.bind(this);
this._renderThumbnail = this._renderThumbnail.bind(this);
this.viewabilityConfig = {
itemVisiblePercentThreshold: 30
};
this._flatListStyles = {};
}
/**
* Implements React's {@link Component#componentDidUpdate}.
*
* @inheritdoc
* @param {*} item
* @returns
*/
componentDidUpdate() {
this._updateReceiverQuality();
_keyExtractor(item) {
return item;
}
/**
*
* @param {*} data
* @param {*} index
* @returns
*/
_getItemLayout(data, index) {
const { _columns, _thumbnailHeight } = this.props;
const cellHeight = _thumbnailHeight;
return {
length: cellHeight,
offset: cellHeight * Math.floor(index / _columns),
index
};
}
/**
* A handler for visible items changes.
*
* @param {Object} data - The visible items data.
* @param {Array<Object>} data.viewableItems - The visible items array.
* @returns {void}
*/
_onViewableItemsChanged({ viewableItems = 0 }) {
console.log(`visisble indexes ${JSON.stringify((viewableItems || []).map(i => i.index))}`);
const indexArray = viewableItems.map(i => i.index);
this.props.dispatch(setVisibleRemoteParticipants(Math.min(indexArray), Math.max(indexArray)));
}
/**
@@ -112,54 +139,36 @@ class TileView extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _height, _width, onClick } = this.props;
const rowElements = this._groupIntoRows(this._renderThumbnails(), this._getColumnCount());
const { _columns, _height, _thumbnailHeight, _width, onClick } = this.props;
const participants = this._getSortedParticipants();
const initialRowsToRender = Math.ceil(_height / _thumbnailHeight);
if (this._flatListStyles.height !== _height || this._flatListStyles.width !== _width) {
this._flatListStyles = {
height: _height,
width: _width
};
}
return (
<ScrollView
style = {{
...styles.tileView,
height: _height,
width: _width
}}>
<TouchableWithoutFeedback onPress = { onClick }>
<View
style = {{
...styles.tileViewRows,
minHeight: _height,
minWidth: _width
}}>
{ rowElements }
</View>
</TouchableWithoutFeedback>
</ScrollView>
<TouchableWithoutFeedback onPress = { onClick }>
<FlatList
contentContainerStyle = { styles.tileView }
data = { participants }
horizontal = { false }
initialNumToRender = { initialRowsToRender }
key = { _columns }
keyExtractor = { this._keyExtractor }
numColumns = { _columns }
onViewableItemsChanged = { this._onViewableItemsChanged }
renderItem = { this._renderThumbnail }
style = { this._flatListStyles }
viewabilityConfig = { this.viewabilityConfig }
windowSize = { 2 } />
</TouchableWithoutFeedback>
);
}
/**
* Returns how many columns should be displayed for tile view.
*
* @returns {number}
* @private
*/
_getColumnCount() {
const participantCount = this.props._participantCount;
// For narrow view, tiles should stack on top of each other for a lonely
// call and a 1:1 call. Otherwise tiles should be grouped into rows of
// two.
if (this.props._aspectRatio === ASPECT_RATIO_NARROW) {
return participantCount >= 3 ? 2 : 1;
}
if (participantCount === 4) {
// In wide view, a four person call should display as a 2x2 grid.
return 2;
}
return Math.min(3, participantCount);
}
/**
* Returns all participants with the local participant at the end.
*
@@ -175,63 +184,6 @@ class TileView extends Component<Props> {
return participants;
}
/**
* Calculate the height and width for the tiles.
*
* @private
* @returns {Object}
*/
_getTileDimensions() {
const { _height, _participantCount, _width } = this.props;
const columns = this._getColumnCount();
const heightToUse = _height - (MARGIN * 2);
const widthToUse = _width - (MARGIN * 2);
let tileWidth;
// If there is going to be at least two rows, ensure that at least two
// rows display fully on screen.
if (_participantCount / columns > 1) {
tileWidth = Math.min(widthToUse / columns, heightToUse / 2);
} else {
tileWidth = Math.min(widthToUse / columns, heightToUse);
}
return {
height: tileWidth / TILE_ASPECT_RATIO,
width: tileWidth
};
}
/**
* Splits a list of thumbnails into React Elements with a maximum of
* {@link rowLength} thumbnails in each.
*
* @param {Array} thumbnails - The list of thumbnails that should be split
* into separate row groupings.
* @param {number} rowLength - How many thumbnails should be in each row.
* @private
* @returns {ReactElement[]}
*/
_groupIntoRows(thumbnails, rowLength) {
const rowElements = [];
for (let i = 0; i < thumbnails.length; i++) {
if (i % rowLength === 0) {
const thumbnailsInRow = thumbnails.slice(i, i + rowLength);
rowElements.push(
<View
key = { rowElements.length }
style = { styles.tileViewRow }>
{ thumbnailsInRow }
</View>
);
}
}
return rowElements;
}
/**
* Creates React Elements to display each participant in a thumbnail. Each
* tile will be.
@@ -239,43 +191,29 @@ class TileView extends Component<Props> {
* @private
* @returns {ReactElement[]}
*/
_renderThumbnails() {
_renderThumbnail({ item, index /*, separators */ }) {
const { _thumbnailHeight } = this.props;
const styleOverrides = {
aspectRatio: TILE_ASPECT_RATIO,
aspectRatio: SQUARE_TILE_ASPECT_RATIO,
flex: 0,
height: this._getTileDimensions().height,
height: _thumbnailHeight,
maxHeight: null,
maxWidth: null,
width: null
};
return this._getSortedParticipants()
.map(id => (
<Thumbnail
disableTint = { true }
key = { id }
participantID = { id }
renderDisplayName = { true }
styleOverrides = { styleOverrides }
tileView = { true } />));
}
console.log(`1rendering thumbnail with index ${index}`);
/**
* Sets the receiver video quality based on the dimensions of the thumbnails
* that are displayed.
*
* @private
* @returns {void}
*/
_updateReceiverQuality() {
const { height, width } = this._getTileDimensions();
this.props.dispatch(setTileViewDimensions({
thumbnailSize: {
height,
width
}
}));
return (
<Thumbnail
disableTint = { true }
height = { _thumbnailHeight }
index = { index }
key = { item }
participantID = { item }
renderDisplayName = { true }
tileView = { true } />)
;
}
}
@@ -288,14 +226,18 @@ class TileView extends Component<Props> {
*/
function _mapStateToProps(state) {
const responsiveUi = state['features/base/responsive-ui'];
const { remoteParticipants } = state['features/filmstrip'];
const { remoteParticipants, tileViewDimensions } = state['features/filmstrip'];
const { height } = tileViewDimensions.thumbnailSize;
const { columns } = tileViewDimensions;
return {
_aspectRatio: responsiveUi.aspectRatio,
_columns: columns,
_height: responsiveUi.clientHeight,
_localParticipant: getLocalParticipant(state),
_participantCount: getParticipantCountWithFake(state),
_remoteParticipants: remoteParticipants,
_thumbnailHeight: height,
_width: responsiveUi.clientWidth
};
}

View File

@@ -125,16 +125,10 @@ export default {
},
tileView: {
alignSelf: 'center'
},
tileViewRows: {
justifyContent: 'center'
},
tileViewRow: {
flexDirection: 'row',
alignItems: 'center',
alignSelf: 'center',
justifyContent: 'center'
// flexGrow: 1
}
};

View File

@@ -10,6 +10,7 @@ import {
sendAnalytics
} from '../../../analytics';
import { getToolbarButtons } from '../../../base/config';
import { isMobileBrowser } from '../../../base/environment/utils';
import { translate } from '../../../base/i18n';
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
import { connect } from '../../../base/redux';
@@ -17,7 +18,13 @@ import { showToolbox } from '../../../toolbox/actions.web';
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { setFilmstripVisible, setVisibleRemoteParticipants } from '../../actions';
import { TILE_HORIZONTAL_MARGIN, TILE_VERTICAL_MARGIN, TOOLBAR_HEIGHT } from '../../constants';
import {
ASPECT_RATIO_BREAKPOINT,
TILE_HORIZONTAL_MARGIN,
TILE_VERTICAL_MARGIN,
TOOLBAR_HEIGHT,
TOOLBAR_HEIGHT_MOBILE
} from '../../constants';
import { shouldRemoteVideosBeVisible } from '../../functions';
import AudioTracksContainer from './AudioTracksContainer';
@@ -485,10 +492,6 @@ function _mapStateToProps(state) {
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
reduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''}`.trim();
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
const {
gridDimensions = {},
filmstripHeight,
@@ -496,12 +499,35 @@ function _mapStateToProps(state) {
thumbnailSize: tileViewThumbnailSize
} = state['features/filmstrip'].tileViewDimensions;
const _currentLayout = getCurrentLayout(state);
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
const availableSpace = clientHeight - filmstripHeight;
let filmstripPadding = 0;
if (availableSpace > 0) {
const paddingValue = TOOLBAR_HEIGHT_MOBILE - availableSpace;
if (paddingValue > 0) {
filmstripPadding = paddingValue;
}
} else {
filmstripPadding = TOOLBAR_HEIGHT_MOBILE;
}
const collapseTileView = reduceHeight
&& isMobileBrowser()
&& clientWidth <= ASPECT_RATIO_BREAKPOINT;
const className = `${remoteVideosVisible ? '' : 'hide-videos'} ${
reduceHeight ? 'reduce-height' : ''
} ${shiftRight ? 'shift-right' : ''} ${collapseTileView ? 'collapse' : ''}`.trim();
const videosClassName = `filmstrip__videos${visible ? '' : ' hidden'}`;
let _thumbnailSize, remoteFilmstripHeight, remoteFilmstripWidth;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
_thumbnailSize = tileViewThumbnailSize;
remoteFilmstripHeight = filmstripHeight;
remoteFilmstripHeight = filmstripHeight - (collapseTileView && filmstripPadding > 0 ? filmstripPadding : 0);
remoteFilmstripWidth = filmstripWidth;
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {

View File

@@ -2,10 +2,10 @@
import React, { Component } from 'react';
import { isMobileBrowser } from '../../../../../react/features/base/environment/utils';
import { createScreenSharingIssueEvent, sendAnalytics } from '../../../analytics';
import { AudioLevelIndicator } from '../../../audio-level-indicator';
import { Avatar } from '../../../base/avatar';
import { isMobileBrowser } from '../../../base/environment/utils';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
import {
@@ -139,6 +139,11 @@ export type Props = {|
*/
_isCurrentlyOnLargeVideo: boolean,
/**
* Whether we are currently running in a mobile browser.
*/
_isMobile: boolean,
/**
* Indicates whether the participant is screen sharing.
*/
@@ -612,7 +617,7 @@ class Thumbnail extends Component<Props, State> {
* @returns {ReactElement}
*/
_renderFakeParticipant() {
const { _participant: { avatarURL } } = this.props;
const { _isMobile, _participant: { avatarURL } } = this.props;
const styles = this._getStyles();
const containerClassName = this._getContainerClassName();
@@ -621,8 +626,10 @@ class Thumbnail extends Component<Props, State> {
className = { containerClassName }
id = 'sharedVideoContainer'
onClick = { this._onClick }
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }
{ ...(_isMobile ? {} : {
onMouseEnter: this._onMouseEnter,
onMouseLeave: this._onMouseLeave
}) }
style = { styles.thumbnail }>
{avatarURL ? (
<img
@@ -753,6 +760,7 @@ class Thumbnail extends Component<Props, State> {
const {
_defaultLocalDisplayName,
_disableLocalVideoFlip,
_isMobile,
_isScreenSharing,
_localFlipX,
_disableProfile,
@@ -772,13 +780,17 @@ class Thumbnail extends Component<Props, State> {
className = { containerClassName }
id = 'localVideoContainer'
onClick = { this._onClick }
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }
{ ...(isMobileBrowser() ? {
onTouchEnd: this._onTouchEnd,
onTouchMove: this._onTouchMove,
onTouchStart: this._onTouchStart
} : {}) }
{ ...(_isMobile
? {
onTouchEnd: this._onTouchEnd,
onTouchMove: this._onTouchMove,
onTouchStart: this._onTouchStart
}
: {
onMouseEnter: this._onMouseEnter,
onMouseLeave: this._onMouseLeave
}
) }
style = { styles.thumbnail }>
<div className = 'videocontainer__background' />
<span id = 'localVideoWrapper'>
@@ -875,6 +887,7 @@ class Thumbnail extends Component<Props, State> {
*/
_renderRemoteParticipant() {
const {
_isMobile,
_isTestModeEnabled,
_participant,
_startSilent,
@@ -909,13 +922,17 @@ class Thumbnail extends Component<Props, State> {
className = { containerClassName }
id = { `participant_${id}` }
onClick = { this._onClick }
onMouseEnter = { this._onMouseEnter }
onMouseLeave = { this._onMouseLeave }
{ ...(isMobileBrowser() ? {
onTouchEnd: this._onTouchEnd,
onTouchMove: this._onTouchMove,
onTouchStart: this._onTouchStart
} : {}) }
{ ...(_isMobile
? {
onTouchEnd: this._onTouchEnd,
onTouchMove: this._onTouchMove,
onTouchStart: this._onTouchStart
}
: {
onMouseEnter: this._onMouseEnter,
onMouseLeave: this._onMouseLeave
}
) }
style = { styles.thumbnail }>
{
_videoTrack && <VideoTrack
@@ -1031,6 +1048,7 @@ function _mapStateToProps(state, ownProps): Object {
} = state['features/base/config'];
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
const { localFlipX } = state['features/base/settings'];
const _isMobile = isMobileBrowser();
switch (_currentLayout) {
@@ -1072,7 +1090,7 @@ function _mapStateToProps(state, ownProps): Object {
return {
_audioTrack,
_connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
_connectionIndicatorDisabled: isMobileBrowser() || interfaceConfig.CONNECTION_INDICATOR_DISABLED,
_connectionIndicatorDisabled: _isMobile || interfaceConfig.CONNECTION_INDICATOR_DISABLED,
_currentLayout,
_defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
_disableLocalVideoFlip: Boolean(disableLocalVideoFlip),
@@ -1081,6 +1099,7 @@ function _mapStateToProps(state, ownProps): Object {
_isAudioOnly: Boolean(state['features/base/audio-only'].enabled),
_isCurrentlyOnLargeVideo: state['features/large-video']?.participantId === id,
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
_isMobile,
_isScreenSharing: _videoTrack?.videoType === 'desktop',
_isTestModeEnabled: isTestModeEnabled(state),
_isVideoPlayable: id && isVideoPlayable(state, id),

View File

@@ -163,6 +163,11 @@ export const TILE_HORIZONTAL_MARGIN = 4;
*/
export const TOOLBAR_HEIGHT = 72;
/**
* The height of the whole toolbar.
*/
export const TOOLBAR_HEIGHT_MOBILE = 60;
/**
* The size of the horizontal border of a thumbnail.
*
@@ -215,3 +220,14 @@ export const HORIZONTAL_FILMSTRIP_MARGIN = 39;
* @type {number}
*/
export const SHOW_TOOLBAR_CONTEXT_MENU_AFTER = 600;
/**
* The margin for each side of the tile view. Taken away from the available
* height and width for the tile container to display in.
*
* NOTE: Mobile specific.
*
* @private
* @type {number}
*/
export const TILE_MARGIN = 10;

View File

@@ -3,6 +3,7 @@
import { getFeatureFlag, FILMSTRIP_ENABLED } from '../base/flags';
import { getParticipantCountWithFake } from '../base/participants';
import { toState } from '../base/redux';
import { ASPECT_RATIO_NARROW } from '../base/responsive-ui/constants';
/**
* Returns true if the filmstrip on mobile is visible, false otherwise.
@@ -25,3 +26,31 @@ export function isFilmstripVisible(stateful: Object | Function) {
return getParticipantCountWithFake(state) > 1;
}
/**
* Returns how many columns should be displayed for tile view.
*
* @param {Object | Function} stateful - The Object or Function that can be
* resolved to a Redux state object with the toState function.
* @returns {number} - The number of columns to be rendered in tile view.
* @private
*/
export function getColumnCount(stateful: Object | Function) {
const state = toState(stateful);
const participantCount = getParticipantCountWithFake(state);
const { aspectRatio } = state['features/base/responsive-ui'];
// For narrow view, tiles should stack on top of each other for a lonely
// call and a 1:1 call. Otherwise tiles should be grouped into rows of
// two.
if (aspectRatio === ASPECT_RATIO_NARROW) {
return participantCount >= 3 ? 2 : 1;
}
if (participantCount === 4) {
// In wide view, a four person call should display as a 2x2 grid.
return 2;
}
return Math.min(3, participantCount);
}

View File

@@ -0,0 +1,24 @@
import { MiddlewareRegistry } from '../base/redux';
import { CLIENT_RESIZED, SET_ASPECT_RATIO } from '../base/responsive-ui/actionTypes';
import { setTileViewDimensions } from './actions';
import './subscriber';
/**
* Middleware that handles widnow dimension changes and updates the aspect ratio.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(({ dispatch }) => next => action => {
const result = next(action);
switch (action.type) {
case CLIENT_RESIZED:
case SET_ASPECT_RATIO:
dispatch(setTileViewDimensions());
break;
}
return result;
});

View File

@@ -31,7 +31,7 @@ const DEFAULT_STATE = {
horizontalViewDimensions: {},
/**
* The custom audio volume levels per perticipant.
* The custom audio volume levels per participant.
*
* @type {Object}
*/
@@ -158,7 +158,9 @@ ReducerRegistry.register(
}
}
return state;
return {
...state
};
}
case PARTICIPANT_LEFT: {
const { id, local } = action.participant;
@@ -187,7 +189,9 @@ ReducerRegistry.register(
delete state.participantsVolume[id];
return state;
return {
...state
};
}
}

View File

@@ -0,0 +1,41 @@
// @flow
import { getParticipantCountWithFake } from '../base/participants';
import { StateListenerRegistry } from '../base/redux';
import { getTileViewGridDimensions, shouldDisplayTileView } from '../video-layout';
import { setTileViewDimensions } from './actions';
/**
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
*/
StateListenerRegistry.register(
/* selector */ state => {
const participantCount = getParticipantCountWithFake(state);
if (participantCount < 5) { // the dimensions are updated only when the participant count is lower than 5.
return participantCount;
}
return 4; // make sure we don't update the dimensions.
},
/* listener */ (_, store) => {
const state = store.getState();
if (shouldDisplayTileView(state)) {
store.dispatch(setTileViewDimensions());
}
});
/**
* Listens for changes in the selected layout to calculate the dimensions of the tile view grid and horizontal view.
*/
StateListenerRegistry.register(
/* selector */ state => shouldDisplayTileView(state),
/* listener */ (isTileView, store) => {
const state = store.getState();
if (isTileView) {
store.dispatch(setTileViewDimensions(getTileViewGridDimensions(state)));
}
});

View File

@@ -8,14 +8,15 @@ import { Dialog } from '../../../../base/dialog';
import { translate } from '../../../../base/i18n';
import { JitsiRecordingConstants } from '../../../../base/lib-jitsi-meet';
import { connect } from '../../../../base/redux';
import { isVpaasMeeting } from '../../../../billing-counter/functions';
import { isDynamicBrandingDataLoaded } from '../../../../dynamic-branding/functions';
import EmbedMeetingTrigger from '../../../../embed-meeting/components/EmbedMeetingTrigger';
import { isVpaasMeeting } from '../../../../jaas/functions';
import { getActiveSession } from '../../../../recording';
import { updateDialInNumbers } from '../../../actions';
import {
_getDefaultPhoneNumber,
getInviteText,
getInviteTextiOS,
isAddPeopleEnabled,
isDialOutEnabled,
sharingFeatures,
@@ -62,6 +63,12 @@ type Props = {
*/
_invitationText: string,
/**
* The custom no new-lines meeting invitation text for iOS default email.
* Needed because of this mailto: iOS issue: https://developer.apple.com/forums/thread/681023
*/
_invitationTextiOS: string,
/**
* An alternate app name to be displayed in the email subject.
*/
@@ -110,6 +117,7 @@ function AddPeopleDialog({
_urlSharingVisible,
_emailSharingVisible,
_invitationText,
_invitationTextiOS,
_inviteAppName,
_inviteContactsVisible,
_inviteUrl,
@@ -160,7 +168,8 @@ function AddPeopleDialog({
_emailSharingVisible
? <InviteByEmailSection
inviteSubject = { inviteSubject }
inviteText = { _invitationText } />
inviteText = { _invitationText }
inviteTextiOS = { _invitationTextiOS } />
: null
}
{ _embedMeetingVisible && <EmbedMeetingTrigger /> }
@@ -207,6 +216,9 @@ function mapStateToProps(state, ownProps) {
_invitationText: getInviteText({ state,
phoneNumber,
t: ownProps.t }),
_invitationTextiOS: getInviteTextiOS({ state,
phoneNumber,
t: ownProps.t }),
_inviteAppName: inviteAppName,
_inviteContactsVisible: interfaceConfig.ENABLE_DIAL_OUT && !hideInviteContacts,
_inviteUrl: getInviteURL(state),

View File

@@ -2,6 +2,7 @@
import React, { useState } from 'react';
import { isIosMobileBrowser } from '../../../../base/environment/utils';
import { translate } from '../../../../base/i18n';
import {
Icon,
@@ -27,6 +28,11 @@ type Props = {
*/
inviteText: string,
/**
* The encoded no new-lines iOS invitation text to be sent on default mail.
*/
inviteTextiOS: string,
/**
* Invoked to obtain translated strings.
*/
@@ -38,10 +44,13 @@ type Props = {
*
* @returns {React$Element<any>}
*/
function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
function InviteByEmailSection({ inviteSubject, inviteText, inviteTextiOS, t }: Props) {
const [ isActive, setIsActive ] = useState(false);
const encodedInviteSubject = encodeURIComponent(inviteSubject);
const encodedInviteText = encodeURIComponent(inviteText);
const encodedInviteTextiOS = encodeURIComponent(inviteTextiOS);
const encodedDefaultEmailText = isIosMobileBrowser() ? encodedInviteTextiOS : encodedInviteText;
/**
* Copies the conference invitation to the clipboard.
@@ -100,7 +109,7 @@ function InviteByEmailSection({ inviteSubject, inviteText, t }: Props) {
{
icon: IconEmail,
tooltipKey: 'addPeople.defaultEmail',
url: `mailto:?subject=${encodedInviteSubject}&body=${encodedInviteText}`
url: `mailto:?subject=${encodedInviteSubject}&body=${encodedDefaultEmailText}`
},
{
icon: IconGoogle,

View File

@@ -10,7 +10,7 @@ import { Icon, IconPhone } from '../../../../base/icons';
import { getLocalParticipant } from '../../../../base/participants';
import { MultiSelectAutocomplete } from '../../../../base/react';
import { connect } from '../../../../base/redux';
import { isVpaasMeeting } from '../../../../billing-counter/functions';
import { isVpaasMeeting } from '../../../../jaas/functions';
import { hideAddPeopleDialog } from '../../../actions';
import { INVITE_TYPES } from '../../../constants';
import AbstractAddPeopleDialog, {
@@ -511,11 +511,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
const supportString = t('inlineDialogFailure.supportMsg');
const supportLink = interfaceConfig.SUPPORT_URL;
if (!supportLink) {
return null;
}
const supportLinkContent = (
const supportLinkContent = supportLink ? (
<span>
<span>
{ supportString.padEnd(supportString.length + 1) }
@@ -524,13 +520,14 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
<a
aria-label = { supportLink }
href = { supportLink }
rel = 'noopener noreferrer'>
rel = 'noopener noreferrer'
target = '_blank'>
{ t('inlineDialogFailure.support') }
</a>
</span>
<span>.</span>
</span>
);
) : null;
return (
<div className = 'modal-dialog-form-error'>
@@ -595,7 +592,7 @@ function _mapStateToProps(state) {
return {
..._abstractMapStateToProps(state),
_footerTextEnabled: footerTextEnabled,
_isVpaas: isVpaasMeeting(state, false)
_isVpaas: isVpaasMeeting(state)
};
}

View File

@@ -58,7 +58,7 @@ class DialInSummary extends Component<Props> {
headerLabelKey: 'info.label'
}}
modalId = { DIAL_IN_SUMMARY_VIEW_ID }
style = { styles.backDrop } >
style = { styles.backDrop }>
<WebView
onError = { this._onError }
onShouldStartLoadWithRequest = { this._onNavigate }

View File

@@ -3,12 +3,13 @@
import { getActiveSession } from '../../features/recording/functions';
import { getRoomName } from '../base/conference';
import { getInviteURL } from '../base/connection';
import { isIosMobileBrowser } from '../base/environment/utils';
import { i18next } from '../base/i18n';
import { JitsiRecordingConstants } from '../base/lib-jitsi-meet';
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants';
import { toState } from '../base/redux';
import { doGetJSON, parseURIString } from '../base/util';
import { isVpaasMeeting } from '../billing-counter/functions';
import { isVpaasMeeting } from '../jaas/functions';
import { INVITE_TYPES, SIP_ADDRESS_REGEX } from './constants';
import logger from './logger';
@@ -251,6 +252,49 @@ export function getInviteResultsForQuery(
});
}
/**
* Creates a custom no new lines message for iOS default mail describing how to dial in to the conference.
*
* @returns {string}
*/
export function getInviteTextiOS({
state,
phoneNumber,
t
}: Object) {
if (!isIosMobileBrowser()) {
return '';
}
const dialIn = state['features/invite'];
const inviteUrl = getInviteURL(state);
const localParticipant = getLocalParticipant(state);
const localParticipantName = localParticipant?.name;
const inviteURL = _decodeRoomURI(inviteUrl);
let invite = localParticipantName
? t('info.inviteTextiOSPersonal', { name: localParticipantName })
: t('info.inviteURLFirstPartGeneral');
invite += ' ';
invite += t('info.inviteTextiOSInviteUrl', { inviteUrl });
invite += ' ';
if (shouldDisplayDialIn(dialIn)) {
invite += t('info.inviteTextiOSPhone', {
number: phoneNumber,
conferenceID: dialIn.conferenceID,
didUrl: getDialInfoPageURL(state)
});
}
invite += ' ';
invite += t('info.inviteTextiOSJoinSilent', { silentUrl: `${inviteURL}#config.startSilent=true` });
return invite;
}
/**
* Creates a message describing how to dial in to the conference.
*
@@ -271,7 +315,6 @@ export function getInviteText({
const localParticipantName = localParticipant?.name;
const inviteURL = _decodeRoomURI(inviteUrl);
let invite = localParticipantName
? t('info.inviteURLFirstPartPersonal', { name: localParticipantName })
: t('info.inviteURLFirstPartGeneral');
@@ -396,7 +439,7 @@ export function isDialOutEnabled(state: Object): boolean {
*/
export function isSipInviteEnabled(state: Object): boolean {
const { sipInviteUrl } = state['features/base/config'];
const { features = {} } = getLocalParticipant(state);
const { features = {} } = getLocalParticipant(state) || {};
return state['features/base/jwt'].jwt
&& Boolean(sipInviteUrl)

View File

@@ -1,12 +1,11 @@
// @flow
import { openDialog } from '../base/dialog';
import { VPAAS_TENANT_PREFIX } from '../billing-counter/constants';
import { getVpaasTenant } from '../billing-counter/functions';
import { SET_DETAILS } from './actionTypes';
import { PremiumFeatureDialog } from './components';
import { isFeatureDisabled, sendGetDetailsRequest } from './functions';
import { VPAAS_TENANT_PREFIX } from './constants';
import { getVpaasTenant, isFeatureDisabled, sendGetDetailsRequest } from './functions';
import logger from './logger';
/**

View File

@@ -23,3 +23,8 @@ export const FEATURES = {
* URL for displaying JaaS upgrade options
*/
export const JAAS_UPGRADE_URL = 'https://jaas.8x8.vc/#/plan/upgrade';
/**
* The prefix for the vpaas tenant.
*/
export const VPAAS_TENANT_PREFIX = 'vpaas-magic-cookie-';

View File

@@ -1,9 +1,53 @@
// @flow
import { getVpaasTenant } from '../billing-counter/functions';
import { 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}
*/
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 connection = state['features/base/connection'];
if (connection?.locationURL?.pathname) {
return Boolean(
extractVpaasTenantFromPath(connection?.locationURL?.pathname)
);
}
return false;
}
/**
* Sends a request for retrieving jaas customer details.
*

View File

@@ -1,10 +1,8 @@
import { sendAnalytics, createVpaasConferenceJoinedEvent } from '../analytics';
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { PARTICIPANT_JOINED } from '../base/participants/actionTypes';
import { MiddlewareRegistry } from '../base/redux';
import { countEndpoint } from './actions';
import { isVpaasMeeting, extractVpaasTenantFromPath } from './functions';
import { isVpaasMeeting, getVpaasTenant } from './functions';
/**
* The redux middleware for billing counter.
@@ -20,17 +18,6 @@ MiddlewareRegistry.register(store => next => async action => {
break;
}
case PARTICIPANT_JOINED: {
const shouldCount = !store.getState()['features/billing-counter'].endpointCounted
&& !action.participant.local;
if (shouldCount) {
store.dispatch(countEndpoint());
}
break;
}
}
return next(action);
@@ -45,7 +32,6 @@ MiddlewareRegistry.register(store => next => async action => {
function _maybeTrackVpaasConferenceJoin(state) {
if (isVpaasMeeting(state)) {
sendAnalytics(createVpaasConferenceJoinedEvent(
extractVpaasTenantFromPath(
state['features/base/connection'].locationURL.pathname)));
getVpaasTenant(state)));
}
}

View File

@@ -1,18 +1,21 @@
import { redirectToStaticPage } from '../app/actions';
import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
import { CONNECTION_FAILED } from '../base/connection';
import { JitsiConnectionErrors } from '../base/lib-jitsi-meet';
import { MiddlewareRegistry } from '../base/redux';
import { SET_DETAILS } from './actionTypes';
import { getCustomerDetails } from './actions';
import { STATUSES } from './constants';
import { isVpaasMeeting } from './functions';
/**
* The redux middleware for billing counter.
* The redux middleware for jaas.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => async action => {
switch (action.type) {
case CONFERENCE_JOINED: {
@@ -20,6 +23,22 @@ MiddlewareRegistry.register(store => next => async action => {
break;
}
case CONNECTION_FAILED: {
const { error } = action;
if (!isVpaasMeeting(store.getState()) || !error) {
break;
}
if (error.name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
if (error.message !== 'could not obtain public key') {
break;
}
store.dispatch(redirectToStaticPage('/static/planLimit.html'));
}
break;
}
case SET_DETAILS: {
const { status } = action.payload;

View File

@@ -0,0 +1,15 @@
// @flow
import { getParticipantById } from '../base/participants';
/**
* Selector for the participant currently displaying on the large video.
*
* @param {Object} state - The redux state.
* @returns {Object}
*/
export function getLargeVideoParticipant(state: Object) {
const { participantId } = state['features/large-video'];
return getParticipantById(state, participantId);
}

View File

@@ -1,28 +1,5 @@
// @flow
import { getCurrentConference } from '../base/conference';
/**
* Approves (lets in) or rejects a knocking participant.
*
* @param {Function} getState - Function to get the Redux state.
* @param {string} id - The id of the knocking participant.
* @param {boolean} approved - True if the participant is approved, false otherwise.
* @returns {Function}
*/
export function setKnockingParticipantApproval(getState: Function, id: string, approved: boolean) {
const conference = getCurrentConference(getState());
if (conference) {
if (approved) {
conference.lobbyApproveAccess(id);
} else {
conference.lobbyDenyAccess(id);
}
}
}
/**
* Selector to return lobby state.
*
@@ -32,3 +9,16 @@ export function setKnockingParticipantApproval(getState: Function, id: string, a
export function getLobbyState(state: any) {
return state['features/lobby'];
}
/**
* Selector to return array with knocking participant ids.
*
* @param {any} state - State object.
* @returns {Array}
*/
export function getKnockingParticipantsById(state: any) {
const { knockingParticipants } = state['features/lobby'];
return knockingParticipants.map(participant => participant.id);
}

View File

@@ -1,352 +1,21 @@
// @flow
import { NativeModules } from 'react-native';
import { RTCPeerConnection, RTCSessionDescription } from 'react-native-webrtc';
import { RTCPeerConnection as PC } from 'react-native-webrtc';
/* eslint-disable no-unused-vars */
// Address families.
const AF_INET6 = 30; /* IPv6 */
// Protocols (RFC 1700)
const IPPROTO_TCP = 6; /* tcp */
const IPPROTO_UDP = 17; /* user datagram protocol */
// Protocol families, same as address families for now.
const PF_INET6 = AF_INET6;
const SOCK_DGRAM = 2; /* datagram socket */
const SOCK_STREAM = 1; /* stream socket */
/* eslint-enable no-unused-vars */
// XXX At the time of this writing extending RTCPeerConnection using ES6 'class'
// and 'extends' causes a runtime error related to the attempt to define the
// onaddstream property setter. The error mentions that babelHelpers.set is
// undefined which appears to be a thing inside React Native's packager. As a
// workaround, extend using the pre-ES6 way.
import { synthesizeIPv6Addresses } from './ipv6utils';
/**
* The RTCPeerConnection provided by react-native-webrtc fires onaddstream
* before it remembers remotedescription (and thus makes it available to API
* clients). Because that appears to be a problem for lib-jitsi-meet which has
* been successfully running on Chrome, Firefox and others for a very long
* time, attempt to meet its expectations (by extending RTCPPeerConnection).
*
* @class
* Override PeerConnection to synthesize IPv6 addresses.
*/
export default function _RTCPeerConnection(...args: any[]) {
export default class RTCPeerConnection extends PC {
/* eslint-disable indent, no-invalid-this */
RTCPeerConnection.apply(this, args);
this.onaddstream = (...args) => // eslint-disable-line no-shadow
(this._onaddstreamQueue
? this._queueOnaddstream
: this._invokeOnaddstream)
.apply(this, args);
// Shadow RTCPeerConnection's onaddstream but after _RTCPeerConnection has
// assigned to the property in question. Defining the property on
// _RTCPeerConnection's prototype may (or may not, I don't know) work but I
// don't want to try because the following approach appears to work and I
// understand it.
// $FlowFixMe
Object.defineProperty(this, 'onaddstream', {
configurable: true,
enumerable: true,
get() {
return this._onaddstream;
},
set(value) {
this._onaddstream = value;
}
});
/* eslint-enable indent, no-invalid-this */
}
_RTCPeerConnection.prototype = Object.create(RTCPeerConnection.prototype);
_RTCPeerConnection.prototype.constructor = _RTCPeerConnection;
_RTCPeerConnection.prototype._invokeOnaddstream = function(...args) {
const onaddstream = this._onaddstream;
return onaddstream && onaddstream.apply(this, args);
};
_RTCPeerConnection.prototype._invokeQueuedOnaddstream = function(q) {
q && q.forEach(args => {
try {
this._invokeOnaddstream(...args);
} catch (e) {
// TODO Determine whether the combination of the standard
// setRemoteDescription and onaddstream results in a similar
// swallowing of errors.
console.error(e);
}
});
};
_RTCPeerConnection.prototype._queueOnaddstream = function(...args) {
this._onaddstreamQueue.push(Array.from(args));
};
_RTCPeerConnection.prototype.setRemoteDescription = function(description) {
return (
_synthesizeIPv6Addresses(description)
.catch(reason => {
reason && console.error(reason);
return description;
})
.then(value => _setRemoteDescription.bind(this)(value)));
};
/**
* Adapts react-native-webrtc's {@link RTCPeerConnection#setRemoteDescription}
* implementation which uses the deprecated, callback-based version to the
* {@code Promise}-based version.
*
* @param {RTCSessionDescription} description - The RTCSessionDescription
* which specifies the configuration of the remote end of the connection.
* @private
* @private
* @returns {Promise}
*/
function _setRemoteDescription(description) {
return new Promise((resolve, reject) => {
/* eslint-disable no-invalid-this */
// Ensure I'm not remembering onaddstream invocations from previous
// setRemoteDescription calls. I shouldn't be but... anyway.
this._onaddstreamQueue = [];
RTCPeerConnection.prototype.setRemoteDescription.call(this, description)
.then((...args) => {
let q;
try {
resolve(...args);
} finally {
q = this._onaddstreamQueue;
this._onaddstreamQueue = undefined;
}
this._invokeQueuedOnaddstream(q);
}, (...args) => {
this._onaddstreamQueue = undefined;
reject(...args);
});
/* eslint-enable no-invalid-this */
});
}
// XXX The function _synthesizeIPv6FromIPv4Address is not placed relative to the
// other functions in the file according to alphabetical sorting rule of the
// coding style. But eslint wants constants to be defined before they are used.
/**
* Synthesizes an IPv6 address from a specific IPv4 address.
*
* @param {string} ipv4 - The IPv4 address from which an IPv6 address is to be
* synthesized.
* @returns {Promise<?string>} A {@code Promise} which gets resolved with the
* IPv6 address synthesized from the specified {@code ipv4} or a falsy value to
* be treated as inability to synthesize an IPv6 address from the specified
* {@code ipv4}.
*/
const _synthesizeIPv6FromIPv4Address: string => Promise<?string> = (function() {
// POSIX.getaddrinfo
const { POSIX } = NativeModules;
if (POSIX) {
const { getaddrinfo } = POSIX;
if (typeof getaddrinfo === 'function') {
return ipv4 =>
getaddrinfo(/* hostname */ ipv4, /* servname */ undefined)
.then(([ { ai_addr: ipv6 } ]) => ipv6);
}
/**
* Synthesize IPv6 addresses before calling the underlying setRemoteDescription.
*
* @param {Object} description - SDP.
* @returns {Promise<undefined>} A promise which is resolved once the operation is complete.
*/
async setRemoteDescription(description: Object) {
return super.setRemoteDescription(await synthesizeIPv6Addresses(description));
}
// NAT64AddrInfo.getIPv6Address
const { NAT64AddrInfo } = NativeModules;
if (NAT64AddrInfo) {
const { getIPv6Address } = NAT64AddrInfo;
if (typeof getIPv6Address === 'function') {
return getIPv6Address;
}
}
// There's no POSIX.getaddrinfo or NAT64AddrInfo.getIPv6Address.
return () =>
Promise.reject(
'The impossible just happened! No POSIX.getaddrinfo or'
+ ' NAT64AddrInfo.getIPv6Address!');
})();
/**
* Synthesizes IPv6 addresses on iOS in order to support IPv6 NAT64 networks.
*
* @param {RTCSessionDescription} sdp - The RTCSessionDescription which
* specifies the configuration of the remote end of the connection.
* @private
* @returns {Promise}
*/
function _synthesizeIPv6Addresses(sdp) {
return (
new Promise(resolve => resolve(_synthesizeIPv6Addresses0(sdp)))
.then(({ ips, lines }) =>
Promise.all(Array.from(ips.values()))
.then(() => _synthesizeIPv6Addresses1(sdp, ips, lines))
));
}
/* eslint-disable max-depth */
/**
* Begins the asynchronous synthesis of IPv6 addresses.
*
* @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
* for which IPv6 addresses will be synthesized.
* @private
* @returns {{
* ips: Map,
* lines: Array
* }}
*/
function _synthesizeIPv6Addresses0(sessionDescription) {
const sdp = sessionDescription.sdp;
let start = 0;
const lines = [];
const ips = new Map();
do {
const end = sdp.indexOf('\r\n', start);
let line;
if (end === -1) {
line = sdp.substring(start);
// Break out of the loop at the end of the iteration.
start = undefined;
} else {
line = sdp.substring(start, end);
start = end + 2;
}
if (line.startsWith('a=candidate:')) {
const candidate = line.split(' ');
if (candidate.length >= 10 && candidate[6] === 'typ') {
const ip4s = [ candidate[4] ];
let abort = false;
for (let i = 8; i < candidate.length; ++i) {
if (candidate[i] === 'raddr') {
ip4s.push(candidate[++i]);
break;
}
}
for (const ip of ip4s) {
if (ip.indexOf(':') === -1) {
ips.has(ip)
|| ips.set(ip, new Promise((resolve, reject) => {
const v = ips.get(ip);
if (v && typeof v === 'string') {
resolve(v);
} else {
_synthesizeIPv6FromIPv4Address(ip).then(
value => {
if (!value
|| value.indexOf(':') === -1
|| value === ips.get(ip)) {
ips.delete(ip);
} else {
ips.set(ip, value);
}
resolve(value);
},
reject);
}
}));
} else {
abort = true;
break;
}
}
if (abort) {
ips.clear();
break;
}
line = candidate;
}
}
lines.push(line);
} while (start);
return {
ips,
lines
};
}
/* eslint-enable max-depth */
/**
* Completes the asynchronous synthesis of IPv6 addresses.
*
* @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
* for which IPv6 addresses are being synthesized.
* @param {Map} ips - A Map of IPv4 addresses found in the specified
* sessionDescription to synthesized IPv6 addresses.
* @param {Array} lines - The lines of the specified sessionDescription.
* @private
* @returns {RTCSessionDescription} A RTCSessionDescription that represents the
* result of the synthesis of IPv6 addresses.
*/
function _synthesizeIPv6Addresses1(sessionDescription, ips, lines) {
if (ips.size === 0) {
return sessionDescription;
}
for (let l = 0; l < lines.length; ++l) {
const candidate = lines[l];
if (typeof candidate !== 'string') {
let ip4 = candidate[4];
let ip6 = ips.get(ip4);
ip6 && (candidate[4] = ip6);
for (let i = 8; i < candidate.length; ++i) {
if (candidate[i] === 'raddr') {
ip4 = candidate[++i];
(ip6 = ips.get(ip4)) && (candidate[i] = ip6);
break;
}
}
lines[l] = candidate.join(' ');
}
}
return new RTCSessionDescription({
sdp: lines.join('\r\n'),
type: sessionDescription.type
});
}

View File

@@ -0,0 +1,199 @@
// @flow
import { NativeModules } from 'react-native';
import { RTCSessionDescription } from 'react-native-webrtc';
/**
* Synthesizes IPv6 addresses on iOS in order to support IPv6 NAT64 networks.
*
* @param {RTCSessionDescription} sdp - The RTCSessionDescription which
* specifies the configuration of the remote end of the connection.
* @private
* @returns {Promise}
*/
export function synthesizeIPv6Addresses(sdp: RTCSessionDescription) {
return (
new Promise(resolve => resolve(_synthesizeIPv6Addresses0(sdp)))
.then(({ ips, lines }) =>
Promise.all(Array.from(ips.values()))
.then(() => _synthesizeIPv6Addresses1(sdp, ips, lines))
));
}
/* eslint-disable max-depth */
/**
* Synthesizes an IPv6 address from a specific IPv4 address.
*
* @param {string} ipv4 - The IPv4 address from which an IPv6 address is to be
* synthesized.
* @returns {Promise<?string>} A {@code Promise} which gets resolved with the
* IPv6 address synthesized from the specified {@code ipv4} or a falsy value to
* be treated as inability to synthesize an IPv6 address from the specified
* {@code ipv4}.
*/
const _synthesizeIPv6FromIPv4Address: string => Promise<?string> = (function() {
// POSIX.getaddrinfo
const { POSIX } = NativeModules;
if (POSIX) {
const { getaddrinfo } = POSIX;
if (typeof getaddrinfo === 'function') {
return ipv4 =>
getaddrinfo(/* hostname */ ipv4, /* servname */ undefined)
.then(([ { ai_addr: ipv6 } ]) => ipv6);
}
}
// NAT64AddrInfo.getIPv6Address
const { NAT64AddrInfo } = NativeModules;
if (NAT64AddrInfo) {
const { getIPv6Address } = NAT64AddrInfo;
if (typeof getIPv6Address === 'function') {
return getIPv6Address;
}
}
// There's no POSIX.getaddrinfo or NAT64AddrInfo.getIPv6Address.
return ip => Promise.resolve(ip);
})();
/**
* Begins the asynchronous synthesis of IPv6 addresses.
*
* @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
* for which IPv6 addresses will be synthesized.
* @private
* @returns {{
* ips: Map,
* lines: Array
* }}
*/
function _synthesizeIPv6Addresses0(sessionDescription) {
const sdp = sessionDescription.sdp;
let start = 0;
const lines = [];
const ips = new Map();
do {
const end = sdp.indexOf('\r\n', start);
let line;
if (end === -1) {
line = sdp.substring(start);
// Break out of the loop at the end of the iteration.
start = undefined;
} else {
line = sdp.substring(start, end);
start = end + 2;
}
if (line.startsWith('a=candidate:')) {
const candidate = line.split(' ');
if (candidate.length >= 10 && candidate[6] === 'typ') {
const ip4s = [ candidate[4] ];
let abort = false;
for (let i = 8; i < candidate.length; ++i) {
if (candidate[i] === 'raddr') {
ip4s.push(candidate[++i]);
break;
}
}
for (const ip of ip4s) {
if (ip.indexOf(':') === -1) {
ips.has(ip)
|| ips.set(ip, new Promise((resolve, reject) => {
const v = ips.get(ip);
if (v && typeof v === 'string') {
resolve(v);
} else {
_synthesizeIPv6FromIPv4Address(ip).then(
value => {
if (!value
|| value.indexOf(':') === -1
|| value === ips.get(ip)) {
ips.delete(ip);
} else {
ips.set(ip, value);
}
resolve(value);
},
reject);
}
}));
} else {
abort = true;
break;
}
}
if (abort) {
ips.clear();
break;
}
line = candidate;
}
}
lines.push(line);
} while (start);
return {
ips,
lines
};
}
/* eslint-enable max-depth */
/**
* Completes the asynchronous synthesis of IPv6 addresses.
*
* @param {RTCSessionDescription} sessionDescription - The RTCSessionDescription
* for which IPv6 addresses are being synthesized.
* @param {Map} ips - A Map of IPv4 addresses found in the specified
* sessionDescription to synthesized IPv6 addresses.
* @param {Array} lines - The lines of the specified sessionDescription.
* @private
* @returns {RTCSessionDescription} A RTCSessionDescription that represents the
* result of the synthesis of IPv6 addresses.
*/
function _synthesizeIPv6Addresses1(sessionDescription, ips, lines) {
if (ips.size === 0) {
return sessionDescription;
}
for (let l = 0; l < lines.length; ++l) {
const candidate = lines[l];
if (typeof candidate !== 'string') {
let ip4 = candidate[4];
let ip6 = ips.get(ip4);
ip6 && (candidate[4] = ip6);
for (let i = 8; i < candidate.length; ++i) {
if (candidate[i] === 'raddr') {
ip4 = candidate[++i];
(ip6 = ips.get(ip4)) && (candidate[i] = ip6);
break;
}
}
lines[l] = candidate.join(' ');
}
}
return new RTCSessionDescription({
sdp: lines.join('\r\n'),
type: sessionDescription.type
});
}

View File

@@ -116,7 +116,7 @@ class Notification extends AbstractNotification<Props> {
}
];
if (!hideErrorSupportLink) {
if (!hideErrorSupportLink && interfaceConfig.SUPPORT_URL) {
buttons.push({
content: this.props.t('dialog.contactSupport'),
onClick: this._onOpenSupportLink

View File

@@ -7,3 +7,8 @@ export const PARTICIPANTS_PANE_CLOSE = 'PARTICIPANTS_PANE_CLOSE';
* Action type to signal the opening of the participants pane.
*/
export const PARTICIPANTS_PANE_OPEN = 'PARTICIPANTS_PANE_OPEN';
/**
* Action type to set the volume of the participant.
*/
export const SET_VOLUME = 'SET_VOLUME';

View File

@@ -1,3 +1,5 @@
// @flow
import {
PARTICIPANTS_PANE_CLOSE,
PARTICIPANTS_PANE_OPEN

View File

@@ -0,0 +1,50 @@
// @flow
import { openDialog } from '../base/dialog';
import { SET_VOLUME } from './actionTypes';
import {
ContextMenuMeetingParticipantDetails,
ContextMenuLobbyParticipantReject
} from './components/native';
export * from './actions.any';
/**
* Displays the context menu for the selected lobby participant.
*
* @param {Object} participant - The selected lobby participant.
* @returns {Function}
*/
export function showContextMenuReject(participant: Object) {
return openDialog(ContextMenuLobbyParticipantReject, { participant });
}
/**
* Displays the context menu for the selected meeting participant.
*
* @param {string} participantID - The selected meeting participant id.
* @returns {Function}
*/
export function showContextMenuDetails(participantID: String) {
return openDialog(ContextMenuMeetingParticipantDetails, { participantID });
}
/**
* Sets the volume.
*
* @param {string} participantId - The participant ID associated with the audio.
* @param {string} volume - The volume level.
* @returns {{
* type: SET_VOLUME,
* participantId: string,
* volume: number
* }}
*/
export function setVolume(participantId: string, volume: number) {
return {
type: SET_VOLUME,
participantId,
volume
};
}

View File

@@ -0,0 +1,3 @@
// @flow
export * from './actions.any';

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