Compare commits

...

62 Commits

Author SHA1 Message Date
damencho
17e1774f6e Revert "drop: debug."
This reverts commit 7d48ea1bca.
2025-04-29 17:17:53 -05:00
damencho
249bd3a660 Revert "drop: fix build."
This reverts commit 23bba927f3.
2025-04-29 17:17:49 -05:00
damencho
23bba927f3 drop: fix build. 2025-04-29 16:30:31 -05:00
damencho
7d48ea1bca drop: debug. 2025-04-29 14:31:04 -05:00
damencho
a07a1cfe93 fix(prosody): Adds a nil check for ends_with utility. 2025-04-28 15:43:42 -05:00
damencho
16c45c15c8 fix(prosody): Fixes filter rayo message when int id is used.
Make sure we add string values to the stanza.
2025-04-28 13:50:10 -05:00
Jaya Allamsetty
5d5d6c3068 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1979.0.0+9da20d5f...v1980.0.0+34a32e86
2025-04-25 14:26:20 -04:00
Jaya Allamsetty
19399ec123 fix(test): Fix codec selection test 2025-04-24 11:05:53 -04:00
Calinteodor
3c27f15490 fix(invite/add-people-dialog): isCorsAvatarURL update (#15959)
For the case of AddPeopleDialog, isCORSAvatarURL takes url param as a function aka an Icon component, thus we need it to return false.
2025-04-24 09:41:47 +03:00
Saúl Ibarra Corretgé
607073c669 feat(local-recordings) remove recording time limit
Use the `showSaveFilePicker` File System Access API to pre-select the
file for download and stream the contents there. The browser uses a
temporary file as the buffer, thus not requiring us to buffer the
contents in memory.

Also change the container to MP4, since we have no way to fix the
seeking problem since we don't have the file in memory. Good news is
that it's supported since Chrome 126 and we can feature detect it!

Finally, add a helper `isSupprted` method which feature-detects
everything we need to make this work.

Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/showSaveFilePicker
Ref: https://groups.google.com/a/chromium.org/g/blink-dev/c/p1OMVj1FrMI/m/6FdLk7rZAQAJ
2025-04-23 13:52:02 -04:00
Hristo Terezov
f92ee57f9c chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1973.0.0+64dcc15c...v1979.0.0+9da20d5f
2025-04-23 12:29:31 -05:00
Hristo Terezov
32331d7465 doc(config): testing.enableAV1ForFF 2025-04-23 12:08:19 -05:00
Hristo Terezov
88685c43fb fix(codecSelection): test 2025-04-23 12:08:19 -05:00
Hristo Terezov
348573b254 feat: Implement a closed caption history panel. 2025-04-21 08:56:31 -05:00
Christoph Settgast
1a05991b8c lang: update German translation (#15949) 2025-04-18 12:19:37 +02:00
Joshua Irmer
c3f2390642 fix(participants): fix dynamic participants count in german translation (#15902)
* fix(participants): fix dynamic participants count in german translation

Signed-off-by: Joshua Irmer <irmer@gonicus.de>

* Update lang/main-de.json

---------

Signed-off-by: Joshua Irmer <irmer@gonicus.de>
Co-authored-by: Christoph Settgast <csett86_git@quicksands.de>
2025-04-18 10:38:49 +02:00
Saúl Ibarra Corretgé
7cf8902fdd fix(recording) skip consent dialog on Spot TV 2025-04-17 21:33:05 +02:00
Saúl Ibarra Corretgé
3e4fb82d58 fix(polls) halt processing of malformed polls
We need to return something other than nil in order to halt the
processing of the event.

https://prosody.im/doc/developers/moduleapi#modulehook_event_name_handler_priority
2025-04-17 12:00:38 +02:00
Calinteodor
057dc0e4d2 feat(base/ui/native): Convert rem to px (#15934)
* feat(base/ui): create and use conversion helpers for tokens on mobile
2025-04-15 16:04:58 +03:00
Saúl Ibarra Corretgé
ce4cbacceb feat(web,input) add hint for 1Password to ignore input fields
When autoComplete is set to off, at least.
2025-04-15 14:35:15 +02:00
Saúl Ibarra Corretgé
af4f122602 fix(ui) default to "off" for autoComplete in Input 2025-04-15 14:35:15 +02:00
Hristo Terezov
b7f5b8ecd2 feat(toolbar): Enable 9th and 10th button 2025-04-15 07:20:20 -05:00
Saúl Ibarra Corretgé
d15e51adbd chore(deps) npm audit fix 2025-04-14 13:46:24 +02:00
raduanastase8x8
affef1ac66 fix(web,input) Added description to inputs that filter participants 2025-04-14 13:38:27 +02:00
Saúl Ibarra Corretgé
7f95dbb6d6 fix(rtcstats) drop unnecessary dependency
We either expose those events in LJM or live with strings since they
match standard WebRTC states, but depending on the package just for 3
events is just not right.
2025-04-14 10:34:16 +02:00
Saúl Ibarra Corretgé
8065da61c7 feat(lang,settings) remove experimental label from multi-pinning 2025-04-14 10:26:37 +02:00
emrah
b6df08f072 fix(prosody): add consider_websocket_secure into Prosody config 2025-04-13 15:32:11 +02:00
Дамян Минков
73752618a3 fix(prosody): Fixes extracting domain when more cases. (#15930)
This was broken and was passing as room_node as room_name even for tenant meetings.
This fixes 0b0bbcf.
2025-04-11 10:56:30 -05:00
Hristo Terezov
3c7829a457 fix(DesktopPicker): Stops displaying if closed too fast.
If the desktop picker window is closed before we load the sources, a JS error is thrown. From there the app goes into a broken state where when the screen sharing button is pressed nothing happens.  Explanation:
When the error from the _onCloseModal handler is thrown we don't reach the line to call the onSourceChoose callback. The result is that we never call the callback received by setDisplayMediaRequestHandler. It seems that when this happens on subsequent gDM calls electron won't call the setDisplayMediaRequestHandler and therefore we don't display the desktop picker.
2025-04-11 09:32:22 -05:00
damencho
0b0bbcf194 fix(prosody): Fixes extracting domain from rooms without a domain. 2025-04-11 09:23:04 -05:00
Дамян Минков
3cd29fd63e * feat(tests): Simplifies display names and participant create.
* feat(tests): Simplifies display names and participant create.

Moves token creation only when needed.

* squash: Skip webhook check of user id for guest participants.

* squash: Waits for kick reason dialog.

* squash: Simplifies by matching participant name and display name.

* squash: Drop displayname field.
2025-04-11 09:22:55 -05:00
raduanastase8x8
c486dc07db fix(overlay|prejoin) Added level to items with heading role 2025-04-11 11:06:53 +02:00
damencho
b48772f898 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1970.0.0+22f015dd...v1973.0.0+64dcc15c
2025-04-10 12:21:38 -05:00
damencho
21533f0261 fix(config): Drop a default websocket config.
Fixes #15916.
2025-04-10 12:21:38 -05:00
damencho
1ecfa3a78e fix(prosody): Adds some nil checks.
Found some errors while checking test prosody logs.
2025-04-10 12:21:38 -05:00
damencho
921c27165d feat(metadata): Converts presence startMuted to metadata. 2025-04-10 12:21:38 -05:00
raduanastase8x8
6fa94b0bb4 style(general) Replaced fixed values for font-size and line-height with rem (#15917)
* style(general) Replaced font-size fixed units with rem

* style(general) Replaced font-size fixed units with rem in the tokens

* style(general) Replaced line-height fixed units with rem
2025-04-10 16:06:52 +03:00
damencho
3f3a074e02 fix(prosody): Enables websocket in default config. 2025-04-09 15:07:12 -05:00
damencho
ecb57774ed feat(tests): Checks on participant left for user id and customer. 2025-04-09 14:56:01 -05:00
damencho
1e82f1fb1a fix(tests): Adds a data field in context to be reused in tests. 2025-04-09 14:56:01 -05:00
Jaya Allamsetty
b4b54108c5 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1968.0.0+5674bde4...v1970.0.0+22f015dd
2025-04-09 14:55:13 -04:00
Дамян Минков
6903ccf009 fix(features): Allow checking features when jwt is not available. (#15906)
* fix(features): Allow checking features when jwt is not available.

Fixes #15904.

* squash: Drop unused import.
2025-04-09 11:48:24 -05:00
damencho
6e45643cce feat(prosody): Adds an occupant check on disco-info. 2025-04-09 11:48:09 -05:00
Jaya Allamsetty
379b26a753 fix(config) Update the codec preference order 2025-04-09 10:30:26 -04:00
Jaya Allamsetty
e8faa99ff0 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1966.0.0+46b866d8...v1968.0.0+5674bde4
2025-04-08 18:57:57 -04:00
Joshua Irmer
62d2c9f633 fix(UploadImageButton): add accessibility role
Signed-off-by: Joshua Irmer <irmer@gonicus.de>
2025-04-08 15:20:42 +02:00
damencho
311c404a1a feat(tests): Adds transcription tests. 2025-04-07 14:00:05 -05:00
damencho
b17a3a9d86 fix(tests): Adds default permissions. 2025-04-07 14:00:05 -05:00
damencho
5477e46489 fix(tests): Adds option to clear iframeAPI events. 2025-04-07 14:00:05 -05:00
damencho
7e170fe0d0 fix(tests): Replies to SETTINGS_PROVISIONING wh requests. 2025-04-07 14:00:05 -05:00
damencho
bbbc9d1f46 fix(visitors): Updates promotion path for non jaas meetings. 2025-04-07 12:22:07 -05:00
damencho
084905545f chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1965.0.0+492a3e92...v1966.0.0+46b866d8

Fixes #15862.
2025-04-04 12:44:46 -05:00
damencho
d542b05bc4 feat(tests): Checks for mute icon in filmstrip and participants pane. 2025-04-04 12:44:46 -05:00
damencho
58b73d8dbd feat: Save state debug function. 2025-04-04 12:44:39 -05:00
damencho
471853ef15 fix(tests): Some times the leave can take more time.
Like close page and timeouts for navigating.
2025-04-03 13:39:38 -05:00
damencho
a0070c34f7 fix(tests): Adds more prints from the iframeAPI page. 2025-04-03 13:39:38 -05:00
damencho
5bc8bc8b64 fix(invite): Improves the logged error.
Print more information about the error, instead of just "Object(6)".
2025-04-03 13:39:38 -05:00
damencho
ea04c5e96e fix(api): Drops firing local user role changed twice.
It is already fired by dispatching localParticipantRoleChanged.
2025-04-03 13:39:38 -05:00
damencho
d4e2f17acc chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1964.0.0+d413f971...v1965.0.0+492a3e92
2025-04-03 13:31:32 -05:00
JPL
4dcc4b3711 fix(rn) adapt to new BackHandler eventing API 2025-04-03 15:45:47 +02:00
Дамян Минков
80081ce31c fix(logging): Logger names changed, adjusting. (#15879)
* fix(logging): Logger names changed, adjusting.

* chore(deps) lib-jitsi-meet@latest

https://github.com/jitsi/lib-jitsi-meet/compare/v1963.0.0+403ceed2...v1964.0.0+d413f971
2025-04-02 14:18:43 -05:00
Andrei Gavrilescu
5314d779ed feat(rtcstats): attach LogCollector storage to rtcstats lifecycle
* attach LogCollector to rtcstats lifecycle

* remove legacy param
2025-04-02 09:07:39 -05:00
162 changed files with 2957 additions and 1225 deletions

View File

@@ -1060,6 +1060,14 @@ export default {
downloadJSON(logs, filename);
},
/**
* Download app state, a function that can be called from console while debugging.
* @param filename (optional) specify target filename
*/
saveState(filename = 'meet-state.json') {
downloadJSON(APP.store.getState(), filename);
},
/**
* Exposes a Command(s) API on this instance. It is necessitated by (1) the
* desire to keep room private to this instance and (2) the need of other
@@ -1548,7 +1556,6 @@ export default {
}
APP.store.dispatch(localParticipantRoleChanged(role));
APP.API.notifyUserRoleChanged(id, role);
} else {
APP.store.dispatch(participantRoleChanged(id, role));
}

View File

@@ -50,7 +50,8 @@ var config = {
// Websocket URL (XMPP)
websocket: 'wss://jitsi-meet.example.com/' + subdir + 'xmpp-websocket',
websocketKeepAliveUrl: 'https://jitsi-meet.example.com/' + subdir + '_unlock',
// websocketKeepAliveUrl: 'https://jitsi-meet.example.com/' + subdir + '_unlock',
// Whether BOSH should be preferred over WebSocket if both are configured.
// preferBosh: false,
@@ -88,6 +89,9 @@ var config = {
// Enables use of getDisplayMedia in electron
// electronUseGetDisplayMedia: false,
// Enables AV1 codec for FF. Note: By default it is disabled.
// enableAV1ForFF: false,
// Enables the use of the codec selection API supported by the browsers .
// enableCodecSelectionAPI: false,
@@ -519,7 +523,7 @@ var config = {
// videoQuality: {
//
// // Provides a way to set the codec preference on desktop based endpoints.
// codecPreferenceOrder: [ 'VP9', 'VP8', 'H264', 'AV1' ],
// codecPreferenceOrder: [ 'AV1', 'VP9', 'VP8', 'H264' ],
//
// // Provides a way to set the codec for screenshare.
// screenshareCodec: 'AV1',
@@ -596,7 +600,7 @@ var config = {
// },
//
// // Provides a way to set the codec preference on mobile devices, both on RN and mobile browser based endpoint
// mobileCodecPreferenceOrder: [ 'VP8', 'VP9', 'H264' ],
// mobileCodecPreferenceOrder: [ 'VP8', 'VP9', 'H264', 'AV1' ],
// },
// Notification timeouts
@@ -1074,10 +1078,10 @@ var config = {
// Provides a way to set the codec preference on mobile devices, both on RN and mobile browser based
// endpoints.
// mobileCodecPreferenceOrder: [ 'H264', 'VP8', 'VP9' ],
// mobileCodecPreferenceOrder: [ 'H264', 'VP8', 'VP9', 'AV1' ],
//
// Provides a way to set the codec preference on desktop based endpoints.
// codecPreferenceOrder: [ 'VP9', 'VP8', 'H264 ],
// codecPreferenceOrder: [ 'AV1', 'VP9', 'VP8', 'H264 ],
// Provides a way to set the codec for screenshare.
// screenshareCodec: 'AV1',

View File

@@ -4,12 +4,12 @@
text-align: center;
h2 {
font-size: 48px;
font-size: 3rem;
color : #f2f2f2;
}
&__message {
font-size: 24px;
font-size: 1.5rem;
margin-top: 20px;
}
}

View File

@@ -28,7 +28,7 @@ body {
margin: 0px;
width: 100%;
height: 100%;
font-size: 12px;
font-size: 0.75rem;
font-weight: 400;
overflow: hidden;
color: #F1F1F1;
@@ -139,7 +139,7 @@ form {
position: absolute;
left: 25;
bottom: 7;
font-size: 11pt;
font-size: 0.875rem;
color: rgba(255,255,255,.50);
text-decoration: none;
z-index: $watermarkZ;

View File

@@ -11,9 +11,9 @@
#chatconversation {
box-sizing: border-box;
flex: 1;
font-size: 10pt;
font-size: 0.75rem;
height: calc(100% - 10px);
line-height: 20px;
line-height: 1.25rem;
overflow: auto;
padding: 16px;
text-align: left;
@@ -72,7 +72,7 @@
#nickname {
text-align: center;
color: #9d9d9d;
font-size: 16px;
font-size: 1rem;
margin: auto 0;
padding: 0 16px;
@@ -86,7 +86,7 @@
}
label {
line-height: 24px;
line-height: 1.5rem;
}
}
@@ -98,7 +98,7 @@
}
.chatmessage .usermessage {
font-size: 16px;
font-size: 1rem;
}
}
@@ -124,7 +124,7 @@
}
#smileys {
font-size: 20pt;
font-size: 1.625rem;
margin: auto;
cursor: pointer;
}
@@ -168,7 +168,7 @@
}
#smileysContainer .smiley {
font-size: 20pt;
font-size: 1.625rem;
}
.smileyContainer {
@@ -219,8 +219,8 @@
box-sizing: border-box;
color: #fff;
font-weight: 600;
font-size: 24px;
line-height: 32px;
font-size: 1.5rem;
line-height: 2rem;
.jitsi-icon {
cursor: pointer;

View File

@@ -34,8 +34,8 @@
}
&__checkbox-label {
font-size: 14px;
line-height: 18px;
font-size: 0.875rem;
line-height: 1.125rem;
display: flex;
align-items: center;
letter-spacing: -0.006em;
@@ -51,8 +51,8 @@
}
&__text-container {
font-size: 14px;
line-height: 18px;
font-size: 0.875rem;
line-height: 1.125rem;
display: flex;
align-items: center;
letter-spacing: -0.006em;
@@ -84,8 +84,8 @@
&__button-text {
font-weight: 600;
font-size: 14px;
line-height: 40px;
font-size: 0.875rem;
line-height: 2.5rem;
text-align: center;
letter-spacing: -0.006em;
color: #FFFFFF;

View File

@@ -10,7 +10,7 @@
margin: 17px 0;
padding-bottom: 17px;
color: #ffffff;
font-size: 21px;
font-size: 1.25rem;
letter-spacing: 0.3px;
border-bottom: 1px solid lighten(#FFFFFF, 10%);
}
@@ -19,12 +19,12 @@
color: #ffffff;
display: block;
margin-top: 22px;
font-size: 16px;
font-size: 1rem;
}
&__icon {
margin: 0 10px;
font-size: 50px;
font-size: 3.125rem;
}
}

View File

@@ -1,7 +1,7 @@
.meetings-list {
font-size: 14px;
font-size: 0.875rem;
color: #253858;
line-height: 20px;
line-height: 1.25rem;
text-align: left;
text-overflow: ellipsis;
display: flex;
@@ -20,8 +20,8 @@
.description {
color: #2f3237;
font-size: 14px;
line-height: 18px;
font-size: 0.875rem;
line-height: 1.125rem;
margin-bottom: 16px;
max-width: 436px;
}
@@ -37,8 +37,8 @@
color: #0163FF;
cursor: pointer;
display: flex;
font-size: 14px;
line-height: 18px;
font-size: 0.875rem;
line-height: 1.125rem;
margin: 24px 0 32px 0;
}
@@ -101,17 +101,17 @@
}
.title {
font-size: 12px;
font-size: 0.75rem;
font-weight: 600;
line-height: 16px;
line-height: 1rem;
margin-bottom: 4px;
}
.subtitle {
color: #5E6D7A;
font-weight: normal;
font-size: 12px;
line-height: 16px;
font-size: 0.75rem;
line-height: 1rem;
}

View File

@@ -1,7 +1,7 @@
%navigate-section-list-text {
width: 100%;
font-size: 14px;
line-height: 20px;
font-size: 0.875rem;
line-height: 1.25rem;
color: $welcomePageTitleColor;
text-align: left;
font-family: 'open_sanslight', Helvetica, sans-serif;
@@ -52,7 +52,7 @@
.navigate-section-tile-body {
@extend %navigate-section-list-tile-text;
font-weight: normal;
line-height: 24px;
line-height: 1.5rem;
}
.navigate-section-list-tile-info {
flex: 1;
@@ -61,7 +61,7 @@
.navigate-section-tile-title {
@extend %navigate-section-list-tile-text;
font-weight: bold;
line-height: 24px;
line-height: 1.5rem;
}
.navigate-section-section-header {
@extend %navigate-section-list-text;

View File

@@ -8,8 +8,8 @@
&__text {
text-align: center;
font-size: 14px;
line-height: 21px;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 300;
}
}

View File

@@ -55,7 +55,7 @@
span.emoji {
width: 40px;
height: 40px;
font-size: 22px;
font-size: 1.375rem;
display: flex;
align-items: center;
justify-content: center;
@@ -63,7 +63,7 @@
@for $i from 1 through 12 {
&.increase-#{$i}{
font-size: calc(20px + #{$i}px);
font-size: calc(1.25rem + #{$i}px);
}
}
}
@@ -96,8 +96,8 @@
span.text {
font-style: normal;
font-weight: 600;
font-size: 14px;
line-height: 24px;
font-size: 0.875rem;
line-height: 1.5rem;
margin-left: 8px;
}
}
@@ -132,8 +132,8 @@ $reactionCount: 20;
.reaction-emoji {
position: absolute;
font-size: 24px;
line-height: 32px;
font-size: 1.5rem;
line-height: 2rem;
width: 32px;
height: 32px;
top: 0;

View File

@@ -12,7 +12,7 @@
.recording-title {
display: inline-flex;
align-items: center;
font-size: 14px;
font-size: 0.875rem;
margin-left: 16px;
max-width: 70%;
@@ -35,8 +35,8 @@
.local-recording-warning {
margin-top: 8px;
display: block;
font-size: 14px;
line-height: 20px;
font-size: 0.875rem;
line-height: 1.25rem;
padding: 8px 16px;
&.text {
@@ -126,7 +126,7 @@
.recording-info-title {
display: inline-flex;
font-size: 14px;
font-size: 0.875rem;
width: 290px
}
@@ -150,7 +150,7 @@
/**
* Set font-size to be consistent with Atlaskit FieldText.
*/
font-size: 14px;
font-size: 0.875rem;
.broadcast-dropdown {
text-align: left;
@@ -194,6 +194,6 @@
.warning-text {
color:#FFD740;
font-size: 12px;
font-size: 0.75rem;
}
}

View File

@@ -2,7 +2,7 @@
width: 30%;
margin: 20% auto;
text-align: center;
font-size: 24px;
font-size: 1.5rem;
.thanks-msg {
border-bottom: 1px solid #FFFFFF;
@@ -10,16 +10,16 @@
padding-right: 30px;
p {
margin: 30px auto;
font-size: 24px;
line-height: 24px;
font-size: 1.5rem;
line-height: 1.5rem;
}
}
.hint-msg {
p {
margin: 26px auto;
font-weight: 600;
font-size: 16px;
line-height: 18px;
font-size: 1rem;
line-height: 1.125rem;
.hint-msg__holder{
font-weight: 200;
}
@@ -33,7 +33,7 @@
}
.forbidden-msg {
p {
font-size: 16px;
font-size: 1rem;
margin-top: 15px;
}
}

View File

@@ -70,7 +70,7 @@ input[type="reset"] {
body {
color: #333;
font-family: Arial, sans-serif;
font-size: 14px;
font-size: 0.875rem;
line-height: 1.42857142857143;
}
/* International Font Stacks*/
@@ -113,7 +113,7 @@ pre:first-child {
/* Headings: desired line height in px / font size = unitless line height */
h1 {
color: #333;
font-size: 32px;
font-size: 2rem;
font-weight: normal;
line-height: 1.25;
text-transform: none;
@@ -121,7 +121,7 @@ h1 {
}
h2 {
color: #333;
font-size: 24px;
font-size: 1.5rem;
font-weight: normal;
line-height: 1.25;
text-transform: none;
@@ -129,14 +129,14 @@ h2 {
}
h3 {
color: #333;
font-size: 20px;
font-size: 1.25rem;
font-weight: normal;
line-height: 1.5;
text-transform: none;
margin: 30px 0 0 0;
}
h4 {
font-size: 16px;
font-size: 1rem;
font-weight: bold;
line-height: 1.25;
text-transform: none;
@@ -144,7 +144,7 @@ h4 {
}
h5 {
color: #333;
font-size: 14px;
font-size: 0.875rem;
font-weight: bold;
line-height: 1.42857143;
text-transform: none;
@@ -152,7 +152,7 @@ h5 {
}
h6 {
color: #707070;
font-size: 12px;
font-size: 0.75rem;
font-weight: bold;
line-height: 1.66666667;
text-transform: uppercase;
@@ -179,7 +179,7 @@ h5 + h6 {
/* Other typographical elements */
small {
color: #707070;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.33333333333333;
}
code,

View File

@@ -4,7 +4,7 @@
#enter_room {
.welcome-page-button {
font-size: 16px;
font-size: 1rem;
left: 0;
text-align: center;
width: 100%;

View File

@@ -9,9 +9,9 @@
// Do not inherit the font-family from the toolbar button, because it's an
// icon style.
font-family: $baseFontFamily;
font-size: 9px;
font-size: 0.5rem;
font-weight: 700;
line-height: 13px;
line-height: 0.75rem;
min-width: 13px;
overflow: hidden;
text-align: center;
@@ -69,8 +69,8 @@
.badge-round {
bottom: -5px;
font-size: 12px;
line-height: 20px;
font-size: 0.75rem;
line-height: 1.25rem;
min-width: 20px;
pointer-events: none;
position: absolute;

View File

@@ -50,11 +50,11 @@ $filmstripVideosZ: $toolbarZ + 1;
$primaryUnsupportedBrowserButtonBgColor: #0052CC;
$unsupportedBrowserButtonBgColor: rgba(9, 30, 66, 0.04);
$unsupportedBrowserTextColor: #4a4a4a;
$unsupportedBrowserTextSmallFontSize: 17px;
$unsupportedBrowserTextSmallFontSize: 1rem;
$unsupportedBrowserTitleColor: #fff;
$unsupportedBrowserTitleFontSize: 24px;
$unsupportedBrowserTitleFontSize: 1.5rem;
$unsupportedDesktopBrowserTextColor: rgba(255, 255, 255, 0.7);
$unsupportedDesktopBrowserTextFontSize: 21px;
$unsupportedDesktopBrowserTextFontSize: 1.25rem;
/**
* The size of the default watermark.
@@ -89,7 +89,7 @@ $welcomePageHeaderContainerDisplay: flex;
$welcomePageHeaderContainerMargin: $welcomePageHeaderContainerMarginTop auto 0;
$welcomePageHeaderTextTitleMarginBottom: 0;
$welcomePageHeaderTextTitleFontSize: 42px;
$welcomePageHeaderTextTitleFontSize: 2.625rem;
$welcomePageHeaderTextTitleFontWeight: normal;
$welcomePageHeaderTextTitleLineHeight: 50px;
$welcomePageHeaderTextTitleOpacity: 1;

View File

@@ -160,7 +160,7 @@
}
#alwaysOnTop .displayname {
font-size: 15px;
font-size: 0.875rem;
position: inherit;
width: 100%;
left: 0px;
@@ -294,7 +294,7 @@
width: auto;
z-index: $zindex2;
font-weight: 600;
font-size: 14px;
font-size: 0.875rem;
text-align: center;
color: #FFF;
left: 50%;
@@ -340,7 +340,7 @@
.presence-label {
color: #fff;
font-size: 12px;
font-size: 0.75rem;
font-weight: 100;
left: 0;
margin: 0 auto;

View File

@@ -53,9 +53,9 @@ body.welcome-page {
.header-text-subtitle {
color: #fff;
font-size: 20px;
font-size: 1.25rem;
font-weight: 600;
line-height: 26px;
line-height: 1.625rem;
margin: 16px 0 32px 0;
text-align: $welcomePageHeaderTextAlign;
@@ -64,7 +64,7 @@ body.welcome-page {
.not-allow-title-character-div {
color: #f03e3e;
background-color: #fff;
font-size: 12px;
font-size: 0.75rem;
font-weight: 600;
margin: 10px 0px 5px 0px;
text-align: $welcomePageHeaderTextAlign;
@@ -147,7 +147,7 @@ body.welcome-page {
display: inline-block;
height: 50px;
width: 100%;
font-size: 14px;
font-size: 0.875rem;
padding-left: 10px;
&.focus-visible {
@@ -172,7 +172,7 @@ body.welcome-page {
}
.tab-container {
font-size: 16px;
font-size: 1rem;
position: relative;
text-align: left;
display: $welcomePageTabContainerDisplay;
@@ -191,8 +191,8 @@ body.welcome-page {
background-color: #c7ddff;
border-radius: 6px;
color: #0163FF;
font-size: 14px;
line-height: 18px;
font-size: 0.875rem;
line-height: 1.125rem;
margin: 4px;
display: $welcomePageTabButtonsDisplay;
@@ -218,7 +218,7 @@ body.welcome-page {
.welcome-page-button {
border: 0;
font-size: 14px;
font-size: 0.875rem;
background: $welcomePageButtonBg;
border-radius: 3px;
color: #FFFFFF;
@@ -246,7 +246,7 @@ body.welcome-page {
* {
cursor: pointer;
font-size: 32px;
font-size: 2rem;
}
.toolbox-icon {

View File

@@ -20,7 +20,7 @@
width: 28px;
i {
line-height: 28px;
line-height: 1.75rem;
margin: auto;
}
}
@@ -35,7 +35,7 @@
padding-left: 10px;
i {
line-height: 20px;
line-height: 1.25rem;
margin: auto;
}
}

View File

@@ -1,7 +1,7 @@
.info-dialog {
cursor: default;
display: flex;
font-size: 14px;
font-size: 0.875rem;
.info-dialog-column {
margin-right: 10px;
@@ -53,8 +53,8 @@
max-width: 334px;
width: 100%;
margin-top: 20px;
font-size: 12px;
line-height: 24px;
font-size: 0.75rem;
line-height: 1.5rem;
border-collapse: collapse;
* {
@@ -107,7 +107,7 @@
box-sizing: border-box;
display: flex;
flex-direction: column;
font-size: 12px;
font-size: 0.75rem;
max-height: 100%;
overflow: auto;
padding: 15pt;

View File

@@ -1,8 +1,8 @@
.invite-more {
&-dialog {
color: #fff;
font-size: 15px;
line-height: 24px;
font-size: 0.875rem;
line-height: 1.5rem;
&.separator {
margin: 24px 0 24px -20px;

View File

@@ -1,5 +1,5 @@
.share-screen-warn-dialog {
font-size: 14px;
font-size: 0.875rem;
.separator-line {
margin: 24px 0 24px -20px;

View File

@@ -1,15 +1,15 @@
.security {
&-dialog {
color: #fff;
font-size: 15px;
line-height: 24px;
font-size: 0.875rem;
line-height: 1.5rem;
&.password-section {
display: flex;
flex-direction: column;
.description {
font-size: 13px;
font-size: 0.75rem;
}
.password {
@@ -24,7 +24,7 @@
button {
cursor: pointer;
text-decoration: none;
font-size: 14px;
font-size: 0.875rem;
color: #6FB1EA;
}

View File

@@ -1,7 +1,7 @@
.lobby-screen {
font-size: 16px;
font-size: 1rem;
font-weight: 400;
line-height: 26px;
line-height: 1.625rem;
&-content {
align-items: center;
@@ -43,7 +43,7 @@
flex-direction: column;
.description {
font-size: 13px;
font-size: 0.75rem;
}
.control-row {
@@ -53,7 +53,7 @@
margin-top: 15px;
label {
font-size: 14px;
font-size: 0.875rem;
font-weight: bold;
}
}
@@ -191,9 +191,9 @@
.title {
flex: 1;
color: #fff;
font-size: 20px;
font-size: 1.25rem;
font-weight: 600;
line-height: 28px;
line-height: 1.75rem;
letter-spacing: -1.2%;
}
}
@@ -214,8 +214,8 @@
border-radius: 6px;
box-sizing: border-box;
color: white;
font-size: 12px;
line-height: 16px;
font-size: 0.75rem;
line-height: 1rem;
margin-bottom: 16px;
margin-top: -8px;
padding: 4px;

View File

@@ -5,9 +5,9 @@
color: #fff;
cursor: pointer;
display: inline-block;
font-size: 14px;
font-size: 0.875rem;
font-weight: 600;
line-height: 24px;
line-height: 1.5rem;
margin-bottom: 16px;
padding: 7px 16px;
position: relative;
@@ -26,7 +26,7 @@
&.text {
width: auto;
font-size: 13px;
font-size: 0.75rem;
margin: 0;
padding: 0;
}
@@ -98,7 +98,7 @@
}
.action-btn {
font-size: 16px;
font-size: 1rem;
margin-bottom: 8px;
padding: 11px 16px;
}

View File

@@ -1,13 +1,13 @@
.reload_overlay_title {
display: block;
font-size: 16px;
line-height: 20px;
font-size: 1rem;
line-height: 1.25rem;
}
.reload_overlay_text {
display: block;
font-size: 12px;
line-height: 30px;
font-size: 0.75rem;
line-height: 1.875rem;
}
#reloadProgressBar {

View File

@@ -34,12 +34,12 @@
&__status{
margin-top: 15px;
font-size: 14px;
line-height: 20px;
font-size: 0.875rem;
line-height: 1.25rem;
}
&__name {
font-size: 24px;
line-height: 32px;
font-size: 1.5rem;
line-height: 2rem;
}
}

View File

@@ -8,7 +8,7 @@
cursor: pointer;
display: inline-flex;
font-family: Roboto, arial, sans-serif;
font-size: 14px;
font-size: 0.875rem;
padding: 1px;
.google-cta {
@@ -17,7 +17,7 @@
/**
* Hack the line height for vertical centering of text.
*/
line-height: 32px;
line-height: 2rem;
margin: 0 15px;
}

View File

@@ -17,8 +17,8 @@
.microsoft-cta {
display: inline-block;
color: #5E5E5E;
font-size: 15px;
line-height: 41px;
font-size: 0.875rem;
line-height: 2.5rem;
}
.microsoft-logo {

View File

@@ -15,6 +15,7 @@ external_services = {
cross_domain_bosh = false;
consider_bosh_secure = true;
consider_websocket_secure = true;
-- https_ports = { }; -- Remove this line to prevent listening on port 5284
-- by default prosody 0.12 sends cors headers, if you want to disable it uncomment the following (the config is available on 0.12.1)
@@ -63,6 +64,7 @@ VirtualHost "jitmeet.example.com"
-- we need bosh
modules_enabled = {
"bosh";
"websocket";
"smacks";
"ping"; -- Enable mod_ping
"speakerstats";

View File

@@ -1111,7 +1111,7 @@
"incomingMessage": "Příchozí zpráva",
"language": "Jazyk",
"loggedIn": "Přihlášen/a jako {{name}}",
"maxStageParticipants": "Maximální počet účastníků, které lze připnout na hlavní pódium (EXPERIMENTÁLNÍ)",
"maxStageParticipants": "Maximální počet účastníků, které lze připnout na hlavní pódium",
"microphones": "Mikrofony",
"moderator": "Moderátor",
"moderatorOptions": "Možnosti moderátora",

View File

@@ -263,6 +263,7 @@
"Remove": "Entfernen",
"Share": "Teilen",
"Submit": "OK",
"Understand": "Verstanden",
"WaitForHostMsg": "Die Konferenz wurde noch nicht gestartet. Falls Sie die Konferenz leiten, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
"WaitForHostNoAuthMsg": "Die Konferenz wurde noch nicht gestartet. Bitte warten Sie, bis die Konferenz gestartet wird.",
"WaitingForHostButton": "Auf Moderation warten",
@@ -393,6 +394,8 @@
"recentlyUsedObjects": "Ihre zuletzt verwendeten Objekte",
"recording": "Aufnahme",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Während eines Livestreams nicht möglich",
"recordingInProgressDescription": "Diese Konferenz wird aufgezeichnet. Ihr Ton und Video ist deaktiviert, wenn Sie es aktivieren, stimmen Sie der Aufzeichnung zu.",
"recordingInProgressTitle": "Aufnahme läuft",
"rejoinNow": "Jetzt erneut beitreten",
"remoteControlAllowedMessage": "{{user}} hat die Anfrage zur Fernsteuerung angenommen!",
"remoteControlDeniedMessage": "{{user}} hat die Anfrage zur Fernsteuerung verweigert!",
@@ -749,7 +752,8 @@
"dataChannelClosedDescriptionWithAudio": "Die Steuerungsverbindung (Bridge Channel) wurde unterbrochen, daher können Video- und Tonprobleme auftreten.",
"dataChannelClosedWithAudio": "Ton- und Videoqualität können beeinträchtigt sein",
"disabledIframe": "Die Einbettung ist nur für Demo-Zwecke vorgesehen. Diese Konferenz wird in {{timeout}} Minuten beendet.",
"disabledIframeSecondary": "Die Einbettung von {{domain}} ist nur für Demo-Zwecke vorgesehen. Diese Konferenz wird in {{timeout}} Minuten beendet. Bitte nutzen Sie <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> für produktive Zwecke!",
"disabledIframeSecondaryNative": "Die Einbettung von {{domain}} ist nur für Demo-Zwecke vorgesehen. Diese Konferenz wird in {{timeout}} Minuten beendet.",
"disabledIframeSecondaryWeb": "Die Einbettung von {{domain}} ist nur für Demo-Zwecke vorgesehen. Diese Konferenz wird in {{timeout}} Minuten beendet. Bitte nutzen Sie <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a> für produktive Zwecke!",
"disconnected": "getrennt",
"displayNotifications": "Benachrichtigungen anzeigen für",
"dontRemindMe": "Nicht erinnern",
@@ -877,6 +881,7 @@
"waitingLobby": "In der Lobby ({{count}})"
},
"search": "Suche Anwesende",
"searchDescription": "Tippen Sie um die Anwesendenliste zu filtern",
"title": "Anwesende"
},
"passwordDigitsOnly": "Bis zu {{number}} Ziffern",
@@ -1104,6 +1109,7 @@
"signedIn": "Momentan wird auf Kalendertermine von {{email}} zugegriffen. Klicken Sie auf die folgende Schaltfläche „Trennen“, um den Zugriff auf die Kalendertermine zu stoppen.",
"title": "Kalender"
},
"chatWithPermissions": "Chat mit Freigaben",
"desktopShareFramerate": "Framerate für Bildschirmfreigabe",
"desktopShareHighFpsWarning": "Eine höhere Framerate könnte sich auf Ihre Datenrate auswirken. Sie müssen die Bildschirmfreigabe neustarten, damit die Einstellung übernommen wird.",
"desktopShareWarning": "Sie müssen die Bildschirmfreigabe neustarten, damit die Einstellung übernommen wird.",
@@ -1192,6 +1198,7 @@
"neutral": "Neutral",
"sad": "Traurig",
"search": "Suche",
"searchDescription": "Tippen Sie um die Anwesendenliste zu filtern",
"searchHint": "Suche Anwesende",
"seconds": "{{count}} Sek.",
"speakerStats": "Sprechstatistik",
@@ -1270,7 +1277,7 @@
"muteGUMPending": "Verbinde Ihr Mikrofon",
"noiseSuppression": "Rauschunterdrückung",
"openChat": "Chat öffnen",
"participants": "Anwesende",
"participants": "Anwesenheitsliste öffnen. {{participantsCount}} anwesend",
"pip": "Bild-in-Bild-Modus ein-/ausschalten",
"privateMessage": "Private Nachricht senden",
"profile": "Profil bearbeiten",
@@ -1408,7 +1415,8 @@
"ccButtonTooltip": "Untertitel ein-/ausschalten",
"expandedLabel": "Transkribieren ist derzeit eingeschaltet",
"failed": "Transkribieren fehlgeschlagen",
"labelToolTip": "Die Konferenz wird transkribiert",
"labelTooltip": "Die Konferenz wird transkribiert",
"labelTooltipExtra": "Zusätzlich wird das Transkript später verfügbar sein.",
"sourceLanguageDesc": "Aktuell ist die Sprache der Konferenz auf <b>{{sourceLanguage}}</b> eingestellt. <br/> Sie könne dies hier ",
"sourceLanguageHere": "ändern",
"start": "Anzeige der Untertitel starten",

View File

@@ -984,7 +984,7 @@
"incomingMessage": "Εισερχόμενο μήνυμα",
"language": "Γλώσσα",
"loggedIn": "Συνδέθηκε ως {{name}}",
"maxStageParticipants": "Μέγιστος αριθμός συμμετεχόντων που μπορούν να διατηρηθούν στην κύρια σκηνή (ΠΕΙΡΑΜΑΤΙΚΟ)",
"maxStageParticipants": "Μέγιστος αριθμός συμμετεχόντων που μπορούν να διατηρηθούν στην κύρια σκηνή",
"microphones": "Μικρόφωνα",
"moderator": "Συντονιστής",
"moderatorOptions": "Επιλογές συντονιστή",

View File

@@ -1070,7 +1070,7 @@
"incomingMessage": "Envena mesaĝo",
"language": "Lingvo",
"loggedIn": "Ensalutinta kiels {{name}}",
"maxStageParticipants": "Maksimuma nombro da partoprenantoj, kiuj povas esti alpinglitaj al la ĉefa scenejo (EXPERIMENTA)",
"maxStageParticipants": "Maksimuma nombro da partoprenantoj, kiuj povas esti alpinglitaj al la ĉefa scenejo",
"microphones": "Mikrofonoj",
"moderator": "Kunvenestro",
"moderatorOptions": "Kunvenestaj agordoj",

View File

@@ -1026,7 +1026,7 @@
"incomingMessage": "پیام ورودی",
"language": "زبان",
"loggedIn": "واردشده به عنوان {{name}}",
"maxStageParticipants": "بیشینه تعداد شرکت‌کنندگانی که می‌توانند به صحنه اصلی سنجاق شوند (<b>آزمایشی</b>)",
"maxStageParticipants": "بیشینه تعداد شرکت‌کنندگانی که می‌توانند به صحنه اصلی سنجاق شوند",
"microphones": "میکروفون‌ها",
"moderator": "مدیر",
"moderatorOptions": "گزینه‌های مدیر",

View File

@@ -1111,7 +1111,7 @@
"incomingMessage": "un message arrive",
"language": "Langue",
"loggedIn": "Connecté en tant que {{name}}",
"maxStageParticipants": "Nombre maximum de participants pouvant être épinglé sur laffichage principal (EXPÉRIMENTAL)",
"maxStageParticipants": "Nombre maximum de participants pouvant être épinglé sur laffichage principal",
"microphones": "Microphones",
"moderator": "Modérateur",
"moderatorOptions": "Options de modérateur",

View File

@@ -1077,7 +1077,7 @@
"incomingMessage": "un message arrive",
"language": "Langue",
"loggedIn": "Connecté en tant que {{name}}",
"maxStageParticipants": "Nombre maximum de participants pouvant être épinglé sur laffichage principal (EXPÉRIMENTAL)",
"maxStageParticipants": "Nombre maximum de participants pouvant être épinglé sur laffichage principal",
"microphones": "Microphones",
"moderator": "Modérateur",
"moderatorOptions": "Options de modérateur",

View File

@@ -1088,7 +1088,7 @@
"incomingMessage": "Pesan masuk",
"language": "Bahasa",
"loggedIn": "Masuk sebagai {{name}}",
"maxStageParticipants": "Jumlah maksimum peserta yang dapat ditampilkan di panggung utama (PERCOBAAN)",
"maxStageParticipants": "Jumlah maksimum peserta yang dapat ditampilkan di panggung utama",
"microphones": "Mikrofon",
"moderator": "Moderator",
"moderatorOptions": "Opsi moderator",

View File

@@ -1069,7 +1069,7 @@
"incomingMessage": "Móttekin skilaboð",
"language": "Tungumál",
"loggedIn": "Skráð inn sem {{name}}",
"maxStageParticipants": "Hámarksfjöldi þátttakenda sem hægt er að festa á aðalgluggann (Á TILRAUNASTIGI)",
"maxStageParticipants": "Hámarksfjöldi þátttakenda sem hægt er að festa á aðalgluggann",
"microphones": "Hljóðnemar",
"moderator": "Stjórnandi",
"moderatorOptions": "Valkostir umsjónarmanns",

View File

@@ -1110,7 +1110,7 @@
"incomingMessage": "수신 메시지",
"language": "언어",
"loggedIn": "{{name}}으로 로그인",
"maxStageParticipants": "메인 스테이지에 고정할 수 있는 최대 참가자 수 (실험적 기능)",
"maxStageParticipants": "메인 스테이지에 고정할 수 있는 최대 참가자 수",
"microphones": "마이크",
"moderator": "진행자",
"moderatorOptions": "진행자 옵션",

View File

@@ -1117,7 +1117,7 @@
"incomingMessage": "Ienākošā ziņa",
"language": "Valoda",
"loggedIn": "Ierakstījies kā {{name}}",
"maxStageParticipants": "Maksimālais dalībnieku skaits, kurus var piespraust galvenajai skatuvei (EKSPERIMENTĀLS)",
"maxStageParticipants": "Maksimālais dalībnieku skaits, kurus var piespraust galvenajai skatuvei",
"microphones": "Mikrofoni",
"moderator": "Moderators",
"moderatorOptions": "Moderatora opcijas",

View File

@@ -997,7 +997,7 @@
"incomingMessage": "Ирсэн мессэж",
"language": "Хэл",
"loggedIn": "{{name}} нэвтэрсэн",
"maxStageParticipants": "Үндсэн тайз руу гарах оролцогчийн хамгийн их тоо(Туршилтынх)",
"maxStageParticipants": "Үндсэн тайз руу гарах оролцогчийн хамгийн их тоо",
"microphones": "Микрофон",
"moderator": "Зохицуулагч",
"moderatorOptions": "Зохицуулагчийн сонголт",

View File

@@ -1111,7 +1111,7 @@
"incomingMessage": "Innkommende melding",
"language": "Språk",
"loggedIn": "Logget inn som {{name}}",
"maxStageParticipants": "Maksimalt antall deltakere som kan festes til hovedscenen (EKSPERIMENTELL)",
"maxStageParticipants": "Maksimalt antall deltakere som kan festes til hovedscenen",
"microphones": "Mikrofoner",
"moderator": "Moderator",
"moderatorOptions": "Moderatoralternativer",

View File

@@ -1111,7 +1111,7 @@
"incomingMessage": "Innkommende melding",
"language": "Språk",
"loggedIn": "Logget inn som {{name}}",
"maxStageParticipants": "Maksimalt antall deltakere som kan festes til hovedscenen (EKSPERIMENTELL)",
"maxStageParticipants": "Maksimalt antall deltakere som kan festes til hovedscenen",
"microphones": "Mikrofoner",
"moderator": "Moderator",
"moderatorOptions": "Moderatoralternativer",

View File

@@ -1111,7 +1111,7 @@
"incomingMessage": "Messatge dintrant",
"language": "Lenga",
"loggedIn": "Session a {{name}}",
"maxStageParticipants": "Nombre maximal de participants que se pòt penjar a la scèna principala (EXPERIMENTAL)",
"maxStageParticipants": "Nombre maximal de participants que se pòt penjar a la scèna principala",
"microphones": "Microfòns",
"moderator": "Moderator",
"moderatorOptions": "Opcions de moderacion",

View File

@@ -1097,7 +1097,7 @@
"incomingMessage": "Receber uma mensagem",
"language": "Idioma",
"loggedIn": "Sessão iniciada como {{name}}",
"maxStageParticipants": "Número máximo de participantes que podem ser afixados (EXPERIMENTAL)",
"maxStageParticipants": "Número máximo de participantes que podem ser afixados",
"microphones": "Microfones",
"moderator": "Moderador",
"moderatorOptions": "Opções de moderador",

View File

@@ -1067,7 +1067,7 @@
"incomingMessage": "Mensagem recebida",
"language": "Idioma",
"loggedIn": "Conectado como {{name}}",
"maxStageParticipants": "Número máximo de participantes que podem ser fixados no palco principal (EXPERIMENTAL)",
"maxStageParticipants": "Número máximo de participantes que podem ser fixados no palco principal",
"microphones": "Microfones",
"moderator": "Moderador",
"moderatorOptions": "Opções de moderador",

View File

@@ -1083,7 +1083,7 @@
"incomingMessage": "Входящее сообщение",
"language": "Язык",
"loggedIn": "Вошел как {{name}}",
"maxStageParticipants": "Максимальное количество участников, которых можно закрепить на главной сцене (ЭКСПЕРИМЕНТАЛЬНО)",
"maxStageParticipants": "Максимальное количество участников, которых можно закрепить на главной сцене",
"microphones": "Микрофоны",
"moderator": "Модератор",
"moderatorOptions": "Настройки модератора",

View File

@@ -968,7 +968,7 @@
"incomingMessage": "Messàgiu in intrada",
"language": "Limba",
"loggedIn": "Autenticatzione: {{name}}",
"maxStageParticipants": "Nùmeru màssimu de partetzipantes chi podent èssere apicados a s'iscena printzipale (ISPERIMENTALE)",
"maxStageParticipants": "Nùmeru màssimu de partetzipantes chi podent èssere apicados a s'iscena printzipale",
"microphones": "Micròfonos",
"moderator": "Moderadore",
"more": "Àteru",

View File

@@ -1110,7 +1110,7 @@
"incomingMessage": "Mesazh ardhës",
"language": "Gjuhë",
"loggedIn": "I futur si {{name}}",
"maxStageParticipants": "Numër maksimum pjesëmarrësish që mund të fiksohen te skena kryesore (EKSPERIMENTALe)",
"maxStageParticipants": "Numër maksimum pjesëmarrësish që mund të fiksohen te skena kryesore",
"microphones": "Mikrofona",
"moderator": "Moderator",
"moderatorOptions": "Mundësi moderatori",

View File

@@ -995,7 +995,7 @@
"incomingMessage": "Вхідне повідомлення",
"language": "Мова",
"loggedIn": "Увійшли як {{name}}",
"maxStageParticipants": "Максимальна кількість учасників, яку можна закріпити на головній сцені (ТЕСТУВАННЯ)",
"maxStageParticipants": "Максимальна кількість учасників, яку можна закріпити на головній сцені",
"microphones": "Мікрофони",
"moderator": "Модератор",
"moderatorOptions": "Параметри модерації",

View File

@@ -1081,7 +1081,7 @@
"incomingMessage": "Tin nhắn đang gửi",
"language": "Ngôn ngữ",
"loggedIn": "Đã đăng nhập dưới tên {{name}}",
"maxStageParticipants": "Số lượng người tham gia tối đa có thể được ghim vào sân khấu chính (THỬ NGHIỆM)",
"maxStageParticipants": "Số lượng người tham gia tối đa có thể được ghim vào sân khấu chính",
"microphones": "Micro",
"moderator": "Quản trị viên",
"moderatorOptions": "Tùy chọn quản trị viên",

View File

@@ -1049,7 +1049,7 @@
"incomingMessage": "新消息",
"language": "语言",
"loggedIn": "以{{name}}登录",
"maxStageParticipants": "可以固定的最大参会者人数(实验性功能)",
"maxStageParticipants": "可以固定的最大参会者人数",
"microphones": "麦克风",
"moderator": "主持人",
"moderatorOptions": "主持人选项",

View File

@@ -1066,7 +1066,7 @@
"incomingMessage": "新訊息",
"language": "語言",
"loggedIn": "以{{name}}登入",
"maxStageParticipants": "可被釘選的最大與會者人數(實驗性功能)",
"maxStageParticipants": "可被釘選的最大與會者人數",
"microphones": "麥克風",
"moderator": "主持人",
"moderatorOptions": "主持人選項",

View File

@@ -122,7 +122,9 @@
"nickname": {
"popover": "Choose a nickname",
"title": "Enter a nickname to use chat",
"titleWithPolls": "Enter a nickname to use chat and polls"
"titleWithCC": "Enter a nickname to use chat and closed captions",
"titleWithPolls": "Enter a nickname to use chat and polls",
"titleWithPollsAndCC": "Enter a nickname to use chat, polls and closed captions"
},
"noMessagesMessage": "There are no messages in the meeting yet. Start a conversation here!",
"privateNotice": "Private message to {{recipient}}",
@@ -131,10 +133,13 @@
"systemDisplayName": "System",
"tabs": {
"chat": "Chat",
"closedCaptions": "CC",
"polls": "Polls"
},
"title": "Chat",
"titleWithCC": "Chat and CC",
"titleWithPolls": "Chat and Polls",
"titleWithPollsAndCC": "Chat, Polls and CC",
"you": "you"
},
"chromeExtensionBanner": {
@@ -144,6 +149,10 @@
"dontShowAgain": "Dont show me this again",
"installExtensionText": "Install the extension for Google Calendar and Office 365 integration"
},
"closedCaptionsTab": {
"emptyState": "The closed captions content will be available once a moderator starts it",
"startClosedCaptionsButton": "Start closed captions"
},
"connectingOverlay": {
"joiningRoom": "Connecting you to your meeting…"
},
@@ -881,6 +890,7 @@
"waitingLobby": "Waiting in lobby ({{count}})"
},
"search": "Search participants",
"searchDescription": "Start typing to filter participants",
"title": "Participants"
},
"passwordDigitsOnly": "Up to {{number}} digits",
@@ -1119,7 +1129,7 @@
"incomingMessage": "Incoming message",
"language": "Language",
"loggedIn": "Logged in as {{name}}",
"maxStageParticipants": "Maximum number of participants who can be pinned to the main stage (EXPERIMENTAL)",
"maxStageParticipants": "Maximum number of participants who can be pinned to the main stage",
"microphones": "Microphones",
"moderator": "Moderator",
"moderatorOptions": "Moderator options",
@@ -1138,6 +1148,7 @@
"selectMic": "Microphone",
"selfView": "Self view",
"shortcuts": "Shortcuts",
"showSubtitlesOnStage": "Show subtitles on stage",
"speakers": "Speakers",
"startAudioMuted": "Everyone starts muted",
"startReactionsMuted": "Mute reaction sounds for everyone",
@@ -1197,6 +1208,7 @@
"neutral": "Neutral",
"sad": "Sad",
"search": "Search",
"searchDescription": "Start typing to filter participants",
"searchHint": "Search participants",
"seconds": "{{count}}s",
"speakerStats": "Participants Stats",
@@ -1233,6 +1245,7 @@
"closeChat": "Close chat",
"closeMoreActions": "Close more actions menu",
"closeParticipantsPane": "Close participants pane",
"closedCaptions": "Closed captions",
"collapse": "Collapse",
"document": "Toggle shared document",
"documentClose": "Close shared document",
@@ -1323,6 +1336,7 @@
"closeChat": "Close chat",
"closeParticipantsPane": "Close participants pane",
"closeReactionsMenu": "Close reactions menu",
"closedCaptions": "Closed captions",
"disableNoiseSuppression": "Disable extra noise suppression (BETA)",
"disableReactionSounds": "You can disable reaction sounds for this meeting",
"documentClose": "Close shared document",
@@ -1415,13 +1429,16 @@
"failed": "Transcribing failed",
"labelTooltip": "This meeting is being transcribed.",
"labelTooltipExtra": "In addition, a transcript will be available later.",
"openClosedCaptions": "Open closed captions",
"original": "Original",
"sourceLanguageDesc": "Currently the meeting language is set to <b>{{sourceLanguage}}</b>. <br/> You can change it from ",
"sourceLanguageHere": "here",
"start": "Start showing subtitles",
"stop": "Stop showing subtitles",
"subtitles": "Subtitles",
"subtitlesOff": "Off",
"tr": "TR"
"tr": "TR",
"translateTo": "Translate to"
},
"unpinParticipant": "{{participantName}} - Unpin",
"userMedia": {

991
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,6 @@
"@jitsi/js-utils": "2.2.1",
"@jitsi/logger": "2.0.2",
"@jitsi/rnnoise-wasm": "0.2.1",
"@jitsi/rtcstats": "9.5.1",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@microsoft/microsoft-graph-client": "3.0.1",
"@mui/material": "5.12.1",
@@ -68,7 +67,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1963.0.0+403ceed2/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1980.0.0+34a32e86/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -123,7 +122,6 @@
"util": "0.12.1",
"uuid": "8.3.2",
"wasm-check": "2.0.1",
"webm-duration-fix": "1.0.4",
"windows-iana": "3.1.0",
"zxcvbn": "4.4.2"
},

View File

@@ -72,11 +72,15 @@ export function getInitials(s?: string) {
/**
* Checks if the passed URL should be loaded with CORS.
*
* @param {string} url - The URL.
* @param {string | Function} url - The URL (on mobile we use a specific Icon component for avatars).
* @param {Array<string>} corsURLs - The URL pattern that matches a URL that needs to be handled with CORS.
* @returns {void}
* @returns {boolean}
*/
export function isCORSAvatarURL(url: string, corsURLs: Array<string> = []): boolean {
export function isCORSAvatarURL(url: string | Function, corsURLs: Array<string> = []): boolean {
if (typeof url === 'function') {
return false;
}
return corsURLs.some(pattern => url.startsWith(pattern));
}

View File

@@ -190,10 +190,10 @@ export interface IConfig {
obfuscateRoomName?: boolean;
rtcstatsEnabled?: boolean;
rtcstatsEndpoint?: string;
rtcstatsLogFlushSizeBytes?: number;
rtcstatsPollInterval?: number;
rtcstatsSendSdp?: boolean;
rtcstatsStoreLogs?: boolean;
rtcstatsUseLegacy?: boolean;
scriptURLs?: Array<string>;
watchRTCEnabled?: boolean;
whiteListedEvents?: string[];
@@ -617,6 +617,7 @@ export interface IConfig {
transcription?: {
autoCaptionOnTranscribe?: boolean;
autoTranscribeOnRecord?: boolean;
disableClosedCaptions?: boolean;
enabled?: boolean;
preferredLanguage?: string;
translationLanguages?: Array<string>;

View File

@@ -24,7 +24,6 @@ export function _cleanupConfig(config: IConfig) {
delete config.analytics?.rtcstatsEndpoint;
delete config.analytics?.rtcstatsPollInterval;
delete config.analytics?.rtcstatsSendSdp;
delete config.analytics?.rtcstatsUseLegacy;
delete config.analytics?.obfuscateRoomName;
delete config.analytics?.watchRTCEnabled;
delete config.watchRTCConfigParams;

View File

@@ -53,7 +53,6 @@ export function isJwtFeatureEnabled(
feature: ParticipantFeaturesKey,
ifNotInFeatures: boolean
) {
const { jwt } = state['features/base/jwt'];
let { features } = getLocalParticipant(state) || {};
if (typeof features === 'undefined' && isVpaasMeeting(state)) {
@@ -62,7 +61,6 @@ export function isJwtFeatureEnabled(
}
return isJwtFeatureEnabledStateless({
jwt,
localParticipantFeatures: features,
feature,
ifNotInFeatures
@@ -79,7 +77,6 @@ interface IIsJwtFeatureEnabledStatelessParams {
/**
* Check if the given JWT feature is enabled.
*
* @param {string | undefined} jwt - The jwt token.
* @param {ILocalParticipant} localParticipantFeatures - The features of the local participant.
* @param {string} feature - The feature we want to check.
* @param {boolean} ifNotInFeatures - Default value if features is missing
@@ -87,12 +84,11 @@ interface IIsJwtFeatureEnabledStatelessParams {
* @returns {boolean}
*/
export function isJwtFeatureEnabledStateless({
jwt,
localParticipantFeatures: features,
feature,
ifNotInFeatures
}: IIsJwtFeatureEnabledStatelessParams) {
if (!jwt || typeof features?.[feature] === 'undefined') {
if (typeof features?.[feature] === 'undefined') {
return ifNotInFeatures;
}

View File

@@ -1,12 +1,13 @@
import { IStore } from '../../app/types';
import JitsiMeetJS from '../../base/lib-jitsi-meet';
import RTCStats from '../../rtcstats/RTCStats';
import { isRTCStatsEnabled } from '../../rtcstats/functions';
/**
* Implements log storage interface from the @jitsi/logger lib.
* Implements log storage interface from the @jitsi/logger lib, as it stands
* now it only sends logs to the rtcstats server in case it is enabled.
*/
export default class JitsiMeetLogStorage {
counter: number;
getState: IStore['getState'];
/**
@@ -15,12 +16,6 @@ export default class JitsiMeetLogStorage {
* @param {Function} getState - The Redux store's {@code getState} method.
*/
constructor(getState: IStore['getState']) {
/**
* Counts each log entry, increases on every batch log entry stored.
*
* @type {number}
*/
this.counter = 1;
/**
* The Redux store's {@code getState} method.
@@ -31,18 +26,14 @@ export default class JitsiMeetLogStorage {
}
/**
* The JitsiMeetLogStorage is ready when the conference has been joined.
* A conference is considered joined when the 'conference' field is defined
* in the base/conference state.
* The JitsiMeetLogStorage is ready we can use the rtcstats trace to send logs
* to the rtcstats server.
*
* @returns {boolean} <tt>true</tt> when this storage is ready or
* <tt>false</tt> otherwise.
*/
isReady() {
const { conference, error: conferenceError } = this.getState()['features/base/conference'];
const { error: connectionError } = this.getState()['features/base/connection'];
return Boolean(conference || conferenceError || connectionError);
return JitsiMeetJS.rtcstats.isTraceAvailable();
}
/**
@@ -55,9 +46,9 @@ export default class JitsiMeetLogStorage {
const config = this.getState()['features/base/config'];
// Saving the logs in RTCStats is a new feature and so there is no prior behavior that needs to be maintained.
// That said, this is still experimental and needs to be rolled out gradually so we want this to be off by
// default.
// RTCStats can run without sending app logs to the server.
// Be mindful that there exists another LogStorage instance withing lib-jitsi-meet,
// that is used to send logs generated there.
return config?.analytics?.rtcstatsStoreLogs && isRTCStatsEnabled(this.getState());
}

View File

@@ -107,9 +107,6 @@ function _conferenceJoined({ getState }: IStore, next: Function, action: AnyActi
const { logCollector } = getState()['features/base/logging'];
if (logCollector && conference === getCurrentConference(getState())) {
// Start the LogCollector's periodic "store logs" task
logCollector.start();
// Make an attempt to flush in case a lot of logs have been cached,
// before the collector was started.
logCollector.flush();
@@ -150,12 +147,21 @@ function _initLogging({ dispatch, getState }: IStore,
// Create the LogCollector and register it as the global log transport. It
// is done early to capture as much logs as possible. Captured logs will be
// cached, before the JitsiMeetLogStorage gets ready (statistics module is
// initialized).
// cached, before the JitsiMeetLogStorage gets ready (RTCStats trace is
// available).
if (!logCollector && !loggingConfig.disableLogCollector) {
const _logCollector = new Logger.LogCollector(new JitsiMeetLogStorage(getState));
const { apiLogLevels, analytics: { rtcstatsLogFlushSizeBytes } = {} } = getState()['features/base/config'];
const { apiLogLevels } = getState()['features/base/config'];
// The smaller the flush size the smaller the chance of losing logs, but
// the more often the logs will be sent to the server, by default the LogCollector
// will set once the logs reach 10KB or 30 seconds have passed since the last flush,
// this means if something happens between that interval and the logs don't get flushed
// they will be lost, for instance the meeting tab is closed, the browser crashes,
// an uncaught exception happens, etc.
// If undefined is passed the default values will be used,
const _logCollector = new Logger.LogCollector(new JitsiMeetLogStorage(getState), {
maxEntryLength: rtcstatsLogFlushSizeBytes
});
if (apiLogLevels && Array.isArray(apiLogLevels) && typeof APP === 'object') {
const transport = buildExternalApiLogTransport(apiLogLevels);
@@ -165,6 +171,9 @@ function _initLogging({ dispatch, getState }: IStore,
}
Logger.addGlobalTransport(_logCollector);
_logCollector.start();
dispatch(setLogCollector(_logCollector));
// The JitsiMeetInMemoryLogStorage can not be accessed on mobile through

View File

@@ -17,8 +17,8 @@ const DEFAULT_LOGGING_CONFIG: ILoggingConfig = {
loggers: {
// The following are too verbose in their logging with the
// {@link #defaultLogLevel}:
'modules/RTC/TraceablePeerConnection.js': 'info',
'modules/xmpp/strophe.util.js': 'log'
'modules/RTC/TraceablePeerConnection': 'info',
'modules/xmpp/strophe.util': 'log'
}
};
@@ -41,10 +41,10 @@ const DEFAULT_STATE = {
// Reduce default verbosity on mobile, it kills performance.
if (navigator.product === 'ReactNative') {
const RN_LOGGERS: { [key: string]: LogLevel; } = {
'modules/sdp/SDPUtil.js': 'info',
'modules/xmpp/ChatRoom.js': 'warn',
'modules/xmpp/JingleSessionPC.js': 'info',
'modules/xmpp/strophe.jingle.js': 'info'
'modules/sdp/SDPUtil': 'info',
'modules/xmpp/ChatRoom': 'warn',
'modules/xmpp/JingleSessionPC': 'info',
'modules/xmpp/strophe.jingle': 'info'
};
DEFAULT_STATE.config.loggers = {

View File

@@ -70,6 +70,11 @@ interface IState {
* A generic animated slider view to be used for animated menus.
*/
export default class SlidingView extends PureComponent<IProps, IState> {
/**
* Initializes hardwareBackPress subscription.
*/
_hardwareBackPressSubscription: any;
/**
* True if the component is mounted.
*/
@@ -120,7 +125,7 @@ export default class SlidingView extends PureComponent<IProps, IState> {
* @inheritdoc
*/
override componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
this._hardwareBackPressSubscription = BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
this._mounted = true;
this._setShow(this.props.show);
@@ -145,7 +150,7 @@ export default class SlidingView extends PureComponent<IProps, IState> {
* @inheritdoc
*/
override componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
this._hardwareBackPressSubscription?.remove();
this._mounted = false;
}

View File

@@ -12,6 +12,7 @@
* localFlipX: boolean,
* micDeviceId: string,
* serverURL: string,
* showSubtitlesOnStage: boolean,
* startAudioOnly: boolean,
* startWithAudioMuted: boolean,
* startWithVideoMuted: boolean,

View File

@@ -29,6 +29,7 @@ const DEFAULT_STATE: ISettingsState = {
micDeviceId: undefined,
serverURL: undefined,
hideShareAudioHelper: false,
showSubtitlesOnStage: false,
soundsIncomingMessage: true,
soundsParticipantJoined: true,
soundsParticipantKnocking: true,
@@ -67,6 +68,7 @@ export interface ISettingsState {
maxStageParticipants?: number;
micDeviceId?: string | boolean;
serverURL?: string;
showSubtitlesOnStage?: boolean;
soundsIncomingMessage?: boolean;
soundsParticipantJoined?: boolean;
soundsParticipantKnocking?: boolean;

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
import React from 'react';
interface IHiddenDescriptionProps {
children: React.ReactNode;
id: string;
}
export const HiddenDescription: React.FC<IHiddenDescriptionProps> = ({ id, children }) => {
const hiddenStyle: React.CSSProperties = {
border: 0,
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: '1px',
margin: '-1px',
overflow: 'hidden',
padding: 0,
position: 'absolute',
width: '1px',
whiteSpace: 'nowrap'
};
return (
<span
id = { id }
style = { hiddenStyle }>
{children}
</span>
);
};

View File

@@ -7,6 +7,7 @@ import Icon from '../../../icons/components/Icon';
import { IconCloseCircle } from '../../../icons/svg';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { IInputProps } from '../types';
import { HiddenDescription } from './HiddenDescription';
interface IProps extends IInputProps {
accessibilityLabel?: string;
@@ -14,6 +15,7 @@ interface IProps extends IInputProps {
autoFocus?: boolean;
bottomLabel?: string;
className?: string;
hiddenDescription?: string; // Text that will be announced by screen readers but not displayed visually.
iconClick?: () => void;
/**
@@ -152,13 +154,14 @@ const useStyles = makeStyles()(theme => {
const Input = React.forwardRef<any, IProps>(({
accessibilityLabel,
autoComplete,
autoComplete = 'off',
autoFocus,
bottomLabel,
className,
clearable = false,
disabled,
error,
hiddenDescription,
icon,
iconClick,
id,
@@ -185,11 +188,22 @@ const Input = React.forwardRef<any, IProps>(({
const { classes: styles, cx } = useStyles();
const isMobile = isMobileBrowser();
const showClearIcon = clearable && value !== '' && !disabled;
const inputAutoCompleteOff = autoComplete === 'off' ? { 'data-1p-ignore': '' } : {};
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
onChange?.(e.target.value), []);
const clearInput = useCallback(() => onChange?.(''), []);
const hiddenDescriptionId = `${id}-hidden-description`;
let ariaDescribedById: string | undefined;
if (bottomLabel) {
ariaDescribedById = `${id}-description`;
} else if (hiddenDescription) {
ariaDescribedById = hiddenDescriptionId;
} else {
ariaDescribedById = undefined;
}
return (
<div className = { cx(styles.inputContainer, className) }>
@@ -207,6 +221,7 @@ const Input = React.forwardRef<any, IProps>(({
src = { icon } />}
{textarea ? (
<TextareaAutosize
aria-describedby = { ariaDescribedById }
aria-label = { accessibilityLabel }
autoComplete = { autoComplete }
autoFocus = { autoFocus }
@@ -227,7 +242,7 @@ const Input = React.forwardRef<any, IProps>(({
value = { value } />
) : (
<input
aria-describedby = { bottomLabel ? `${id}-description` : undefined }
aria-describedby = { ariaDescribedById }
aria-label = { accessibilityLabel }
autoComplete = { autoComplete }
autoFocus = { autoFocus }
@@ -236,6 +251,7 @@ const Input = React.forwardRef<any, IProps>(({
data-testid = { testId }
disabled = { disabled }
id = { id }
{ ...inputAutoCompleteOff }
{ ...(mode ? { inputmode: mode } : {}) }
{ ...(type === 'number' ? { max: maxValue } : {}) }
maxLength = { maxLength }
@@ -266,6 +282,7 @@ const Input = React.forwardRef<any, IProps>(({
{bottomLabel}
</span>
)}
{!bottomLabel && hiddenDescription && <HiddenDescription id = { hiddenDescriptionId }>{ hiddenDescription }</HiddenDescription>}
</div>
);
});

View File

@@ -2,6 +2,47 @@ import { DefaultTheme } from 'react-native-paper';
import { createColorTokens } from './utils';
// Base font size in pixels (standard is 16px = 1rem)
const BASE_FONT_SIZE = 16;
/**
* Converts rem to pixels.
*
* @param {string} remValue - The value in rem units (e.g. '0.875rem').
* @returns {number}
*/
function remToPixels(remValue: string): number {
const numericValue = parseFloat(remValue.replace('rem', ''));
return Math.round(numericValue * BASE_FONT_SIZE);
}
/**
* Converts all rem to pixels in an object.
*
* @param {Object} obj - The object to convert rem values in.
* @returns {Object}
*/
function convertRemValues(obj: any): any {
const converted: { [key: string]: any; } = {};
if (typeof obj !== 'object' || obj === null) {
return obj;
}
Object.entries(obj).forEach(([ key, value ]) => {
if (typeof value === 'string' && value.includes('rem')) {
converted[key] = remToPixels(value);
} else if (typeof value === 'object' && value !== null) {
converted[key] = convertRemValues(value);
} else {
converted[key] = value;
}
});
return converted;
}
/**
* Creates a React Native Paper theme based on local UI tokens.
*
@@ -13,10 +54,10 @@ export function createNativeTheme({ font, colorMap, shape, spacing, typography }
...DefaultTheme,
palette: createColorTokens(colorMap),
shape,
spacing,
spacing: spacing.map(remToPixels),
typography: {
font,
...typography
...convertRemValues(typography)
}
};
}

View File

@@ -18,7 +18,7 @@ interface ThemeProps {
colorMap: Object;
font: Object;
shape: Object;
spacing: Array<number>;
spacing: Array<number | string>;
typography: Object;
}

View File

@@ -5,8 +5,22 @@
* @param {string} filename - The filename to give to the downloaded file.
* @returns {void}
*/
export function downloadJSON(json: Object, filename: string) {
const data = encodeURIComponent(JSON.stringify(json, null, ' '));
export function downloadJSON(json: Object, filename: string): void {
const replacer = () => {
const seen = new WeakSet();
return (_: any, value: any) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[circular ref]';
}
seen.add(value);
}
return value;
};
};
const data = encodeURIComponent(JSON.stringify(json, replacer(), ' '));
const elem = document.createElement('a');

View File

@@ -0,0 +1,79 @@
/**
* Interface representing a message that can be grouped.
* Used by both chat messages and subtitles.
*/
export interface IGroupableMessage {
/**
* The ID of the participant who sent the message.
*/
participantId: string;
}
/**
* Interface representing a group of messages from the same sender.
*
* @template T - The type of messages in the group, must extend IGroupableMessage.
*/
export interface IMessageGroup<T extends IGroupableMessage> {
/**
* Array of messages in this group.
*/
messages: T[];
/**
* The ID of the participant who sent all messages in this group.
*/
senderId: string;
}
/**
* Groups an array of messages by sender.
*
* @template T - The type of messages to group, must extend IGroupableMessage.
* @param {T[]} messages - The array of messages to group.
* @returns {IMessageGroup<T>[]} - An array of message groups, where each group contains messages from the same sender.
* @example
* const messages = [
* { participantId: "user1", timestamp: 1000 },
* { participantId: "user1", timestamp: 2000 },
* { participantId: "user2", timestamp: 3000 }
* ];
* const groups = groupMessagesBySender(messages);
* // Returns:
* // [
* // {
* // senderId: "user1",
* // messages: [
* // { participantId: "user1", timestamp: 1000 },
* // { participantId: "user1", timestamp: 2000 }
* // ]
* // },
* // { senderId: "user2", messages: [{ participantId: "user2", timestamp: 3000 }] }
* // ]
*/
export function groupMessagesBySender<T extends IGroupableMessage>(
messages: T[]
): IMessageGroup<T>[] {
if (!messages?.length) {
return [];
}
const groups: IMessageGroup<T>[] = [];
let currentGroup: IMessageGroup<T> | null = null;
for (const message of messages) {
if (!currentGroup || currentGroup.senderId !== message.participantId) {
currentGroup = {
messages: [ message ],
senderId: message.participantId
};
groups.push(currentGroup);
} else {
currentGroup.messages.push(message);
}
}
return groups;
}

View File

@@ -0,0 +1,9 @@
/**
* Checks if Jitsi Meet is running on Spot TV.
*
* @returns {boolean} Whether or not Jitsi Meet is running on Spot TV.
*/
export function isSpotTV(): boolean {
return navigator.userAgent.includes('SpotElectron/');
}

View File

@@ -98,14 +98,14 @@ export const SEND_REACTION = 'SEND_REACTION';
export const SET_PRIVATE_MESSAGE_RECIPIENT = 'SET_PRIVATE_MESSAGE_RECIPIENT';
/**
* The type of action which signals the update a _isPollsTabFocused.
* The type of action which signals setting the focused tab.
*
* {
* isPollsTabFocused: boolean,
* type: SET_PRIVATE_MESSAGE_RECIPIENT
* type: SET_FOCUSED_TAB,
* tabId: string
* }
*/
export const SET_IS_POLL_TAB_FOCUSED = 'SET_IS_POLL_TAB_FOCUSED';
export const SET_FOCUSED_TAB = 'SET_FOCUSED_TAB';
/**
* The type of action which sets the current recipient for lobby messages.

View File

@@ -10,14 +10,16 @@ import {
CLEAR_MESSAGES,
CLOSE_CHAT,
EDIT_MESSAGE,
OPEN_CHAT,
REMOVE_LOBBY_CHAT_PARTICIPANT,
SEND_MESSAGE,
SEND_REACTION,
SET_IS_POLL_TAB_FOCUSED,
SET_FOCUSED_TAB,
SET_LOBBY_CHAT_ACTIVE_STATE,
SET_LOBBY_CHAT_RECIPIENT,
SET_PRIVATE_MESSAGE_RECIPIENT
} from './actionTypes';
import { ChatTabs } from './constants';
/**
* Adds a chat message to the collection of messages.
@@ -169,18 +171,36 @@ export function setPrivateMessageRecipient(participant?: Object) {
}
/**
* Set the value of _isPollsTabFocused.
* Set the value of the currently focused tab.
*
* @param {boolean} isPollsTabFocused - The new value for _isPollsTabFocused.
* @returns {Function}
* @param {string} tabId - The id of the currently focused tab.
* @returns {{
* type: SET_FOCUSED_TAB,
* tabId: string
* }}
*/
export function setIsPollsTabFocused(isPollsTabFocused: boolean) {
export function setFocusedTab(tabId: ChatTabs) {
return {
isPollsTabFocused,
type: SET_IS_POLL_TAB_FOCUSED
type: SET_FOCUSED_TAB,
tabId
};
}
/**
* Opens the chat panel with CC tab active.
*
* @returns {Object} The redux action.
*/
export function openCCPanel() {
return async (dispatch: IStore['dispatch']) => {
dispatch(setFocusedTab(ChatTabs.CLOSED_CAPTIONS));
dispatch({
type: OPEN_CHAT
});
};
}
/**
* Initiates the sending of messages between a moderator and a lobby attendee.
*

View File

@@ -1,26 +1,28 @@
import React from 'react';
import React, { Component } from 'react';
import { FlatList, Text, TextStyle, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n/functions';
import { IMessageGroup, groupMessagesBySender } from '../../../base/util/messageGrouping';
import { IMessage } from '../../types';
import AbstractMessageContainer, { IProps as AbstractProps } from '../AbstractMessageContainer';
import ChatMessageGroup from './ChatMessageGroup';
import styles from './styles';
interface IProps extends AbstractProps {
/**
* Function to be used to translate i18n labels.
*/
interface IProps {
messages: IMessage[];
t: Function;
}
/**
* Implements a container to render all the chat messages in a conference.
*/
class MessageContainer extends AbstractMessageContainer<IProps, any> {
class MessageContainer extends Component<IProps, any> {
static defaultProps = {
messages: [] as IMessage[]
};
/**
* Instantiates a new instance of the component.
*
@@ -32,6 +34,7 @@ class MessageContainer extends AbstractMessageContainer<IProps, any> {
this._keyExtractor = this._keyExtractor.bind(this);
this._renderListEmptyComponent = this._renderListEmptyComponent.bind(this);
this._renderMessageGroup = this._renderMessageGroup.bind(this);
this._getMessagesGroupedBySender = this._getMessagesGroupedBySender.bind(this);
}
/**
@@ -94,9 +97,21 @@ class MessageContainer extends AbstractMessageContainer<IProps, any> {
* @param {Array<Object>} messages - The chat message to render.
* @returns {React$Element<*>}
*/
_renderMessageGroup({ item: messages }: { item: IMessage[]; }) {
_renderMessageGroup({ item: group }: { item: IMessageGroup<IMessage>; }) {
const { messages } = group;
return <ChatMessageGroup messages = { messages } />;
}
/**
* Returns an array of message groups, where each group is an array of messages
* grouped by the sender.
*
* @returns {Array<Array<Object>>}
*/
_getMessagesGroupedBySender() {
return groupMessagesBySender(this.props.messages);
}
}
export default translate(connect()(MessageContainer));

View File

@@ -9,12 +9,14 @@ import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Tabs from '../../../base/ui/components/web/Tabs';
import { arePollsDisabled } from '../../../conference/functions.any';
import PollsPane from '../../../polls/components/web/PollsPane';
import { sendMessage, setIsPollsTabFocused, toggleChat } from '../../actions.web';
import { CHAT_SIZE, CHAT_TABS, SMALL_WIDTH_THRESHOLD } from '../../constants';
import { isCCTabEnabled } from '../../../subtitles/functions.any';
import { sendMessage, setFocusedTab, toggleChat } from '../../actions.web';
import { CHAT_SIZE, ChatTabs, SMALL_WIDTH_THRESHOLD } from '../../constants';
import { IChatProps as AbstractProps } from '../../types';
import ChatHeader from './ChatHeader';
import ChatInput from './ChatInput';
import ClosedCaptionsTab from './ClosedCaptionsTab';
import DisplayNameForm from './DisplayNameForm';
import KeyboardAvoider from './KeyboardAvoider';
import MessageContainer from './MessageContainer';
@@ -22,6 +24,16 @@ import MessageRecipient from './MessageRecipient';
interface IProps extends AbstractProps {
/**
* The currently focused tab.
*/
_focusedTab: ChatTabs;
/**
* True if the CC tab is enabled and false otherwise.
*/
_isCCTabEnabled: boolean;
/**
* Whether the chat is opened in a modal or not (computed based on window width).
*/
@@ -37,11 +49,6 @@ interface IProps extends AbstractProps {
*/
_isPollsEnabled: boolean;
/**
* Whether the poll tab is focused or not.
*/
_isPollsTabFocused: boolean;
/**
* Number of unread poll messages.
*/
@@ -147,7 +154,8 @@ const Chat = ({
_isModal,
_isOpen,
_isPollsEnabled,
_isPollsTabFocused,
_isCCTabEnabled,
_focusedTab,
_messages,
_nbUnreadMessages,
_nbUnreadPolls,
@@ -203,8 +211,8 @@ const Chat = ({
* @returns {void}
*/
const onChangeTab = useCallback((id: string) => {
dispatch(setIsPollsTabFocused(id !== CHAT_TABS.CHAT));
}, []);
dispatch(setFocusedTab(id as ChatTabs));
}, [ dispatch ]);
/**
* Returns a React Element for showing chat messages and a form to send new
@@ -216,15 +224,15 @@ const Chat = ({
function renderChat() {
return (
<>
{_isPollsEnabled && renderTabs()}
{renderTabs()}
<div
aria-labelledby = { CHAT_TABS.CHAT }
aria-labelledby = { ChatTabs.CHAT }
className = { cx(
classes.chatPanel,
!_isPollsEnabled && classes.chatPanelNoTabs,
_isPollsTabFocused && 'hide'
!_isPollsEnabled && !_isCCTabEnabled && classes.chatPanelNoTabs,
_focusedTab !== ChatTabs.CHAT && 'hide'
) }
id = { `${CHAT_TABS.CHAT}-panel` }
id = { `${ChatTabs.CHAT}-panel` }
role = 'tabpanel'
tabIndex = { 0 }>
<MessageContainer
@@ -233,49 +241,76 @@ const Chat = ({
<ChatInput
onSend = { onSendMessage } />
</div>
{_isPollsEnabled && (
{ _isPollsEnabled && (
<>
<div
aria-labelledby = { CHAT_TABS.POLLS }
className = { cx(classes.pollsPanel, !_isPollsTabFocused && 'hide') }
id = { `${CHAT_TABS.POLLS}-panel` }
aria-labelledby = { ChatTabs.POLLS }
className = { cx(classes.pollsPanel, _focusedTab !== ChatTabs.POLLS && 'hide') }
id = { `${ChatTabs.POLLS}-panel` }
role = 'tabpanel'
tabIndex = { 0 }>
tabIndex = { 1 }>
<PollsPane />
</div>
<KeyboardAvoider />
</>
)}
{ _isCCTabEnabled && <div
aria-labelledby = { ChatTabs.CLOSED_CAPTIONS }
className = { cx(classes.chatPanel, _focusedTab !== ChatTabs.CLOSED_CAPTIONS && 'hide') }
id = { `${ChatTabs.CLOSED_CAPTIONS}-panel` }
role = 'tabpanel'
tabIndex = { 2 }>
<ClosedCaptionsTab />
</div> }
</>
);
}
/**
* Returns a React Element showing the Chat and Polls tab.
* Returns a React Element showing the Chat, Polls and Subtitles tabs.
*
* @private
* @returns {ReactElement}
*/
function renderTabs() {
const tabs = [
{
accessibilityLabel: t('chat.tabs.chat'),
countBadge:
_focusedTab !== ChatTabs.CHAT && _nbUnreadMessages > 0 ? _nbUnreadMessages : undefined,
id: ChatTabs.CHAT,
controlsId: `${ChatTabs.CHAT}-panel`,
label: t('chat.tabs.chat')
}
];
if (_isPollsEnabled) {
tabs.push({
accessibilityLabel: t('chat.tabs.polls'),
countBadge: _focusedTab !== ChatTabs.POLLS && _nbUnreadPolls > 0 ? _nbUnreadPolls : undefined,
id: ChatTabs.POLLS,
controlsId: `${ChatTabs.POLLS}-panel`,
label: t('chat.tabs.polls')
});
}
if (_isCCTabEnabled) {
tabs.push({
accessibilityLabel: t('chat.tabs.closedCaptions'),
countBadge: undefined,
id: ChatTabs.CLOSED_CAPTIONS,
controlsId: `${ChatTabs.CLOSED_CAPTIONS}-panel`,
label: t('chat.tabs.closedCaptions')
});
}
return (
<Tabs
accessibilityLabel = { t(_isPollsEnabled ? 'chat.titleWithPolls' : 'chat.title') }
onChange = { onChangeTab }
selected = { _isPollsTabFocused ? CHAT_TABS.POLLS : CHAT_TABS.CHAT }
tabs = { [ {
accessibilityLabel: t('chat.tabs.chat'),
countBadge: _isPollsTabFocused && _nbUnreadMessages > 0 ? _nbUnreadMessages : undefined,
id: CHAT_TABS.CHAT,
controlsId: `${CHAT_TABS.CHAT}-panel`,
label: t('chat.tabs.chat')
}, {
accessibilityLabel: t('chat.tabs.polls'),
countBadge: !_isPollsTabFocused && _nbUnreadPolls > 0 ? _nbUnreadPolls : undefined,
id: CHAT_TABS.POLLS,
controlsId: `${CHAT_TABS.POLLS}-panel`,
label: t('chat.tabs.polls')
}
] } />
selected = { _focusedTab }
tabs = { tabs } />
);
}
@@ -286,10 +321,13 @@ const Chat = ({
onKeyDown = { onEscClick } >
<ChatHeader
className = { cx('chat-header', classes.chatHeader) }
isCCTabEnabled = { _isCCTabEnabled }
isPollsEnabled = { _isPollsEnabled }
onCancel = { onToggleChat } />
{_showNamePrompt
? <DisplayNameForm isPollsEnabled = { _isPollsEnabled } />
? <DisplayNameForm
isCCTabEnabled = { _isCCTabEnabled }
isPollsEnabled = { _isPollsEnabled } />
: renderChat()}
</div> : null
);
@@ -306,7 +344,8 @@ const Chat = ({
* _isModal: boolean,
* _isOpen: boolean,
* _isPollsEnabled: boolean,
* _isPollsTabFocused: boolean,
* _isCCTabEnabled: boolean,
* _focusedTab: string,
* _messages: Array<Object>,
* _nbUnreadMessages: number,
* _nbUnreadPolls: number,
@@ -314,7 +353,7 @@ const Chat = ({
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { isOpen, isPollsTabFocused, messages, nbUnreadMessages } = state['features/chat'];
const { isOpen, focusedTab, messages, nbUnreadMessages } = state['features/chat'];
const { nbUnreadPolls } = state['features/polls'];
const _localParticipant = getLocalParticipant(state);
@@ -322,7 +361,8 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
_isModal: window.innerWidth <= SMALL_WIDTH_THRESHOLD,
_isOpen: isOpen,
_isPollsEnabled: !arePollsDisabled(state),
_isPollsTabFocused: isPollsTabFocused,
_isCCTabEnabled: isCCTabEnabled(state),
_focusedTab: focusedTab,
_messages: messages,
_nbUnreadMessages: nbUnreadMessages,
_nbUnreadPolls: nbUnreadPolls,

View File

@@ -13,6 +13,11 @@ interface IProps {
*/
className: string;
/**
* Whether CC tab is enabled or not.
*/
isCCTabEnabled: boolean;
/**
* Whether the polls feature is enabled or not.
*/
@@ -29,7 +34,7 @@ interface IProps {
*
* @returns {React$Element<any>}
*/
function ChatHeader({ className, isPollsEnabled }: IProps) {
function ChatHeader({ className, isPollsEnabled, isCCTabEnabled }: IProps) {
const dispatch = useDispatch();
const { t } = useTranslation();
@@ -44,13 +49,23 @@ function ChatHeader({ className, isPollsEnabled }: IProps) {
}
}, []);
let title = 'chat.title';
if (isCCTabEnabled && isPollsEnabled) {
title = 'chat.titleWithPollsAndCC';
} else if (isCCTabEnabled) {
title = 'chat.titleWithCC';
} else if (isPollsEnabled) {
title = 'chat.titleWithPolls';
}
return (
<div
className = { className || 'chat-dialog-header' }>
<span
aria-level = { 1 }
role = 'heading'>
{ t(isPollsEnabled ? 'chat.titleWithPolls' : 'chat.title') }
{ t(title) }
</span>
<Icon
ariaLabel = { t('toolbar.closeChat') }

View File

@@ -0,0 +1,178 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import Icon from '../../../base/icons/components/Icon';
import { IconSubtitles } from '../../../base/icons/svg';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Button from '../../../base/ui/components/web/Button';
import { groupMessagesBySender } from '../../../base/util/messageGrouping';
import { setRequestingSubtitles } from '../../../subtitles/actions.any';
import LanguageSelector from '../../../subtitles/components/web/LanguageSelector';
import { canStartSubtitles } from '../../../subtitles/functions.any';
import { ISubtitle } from '../../../subtitles/types';
import { isTranscribing } from '../../../transcribing/functions';
import { SubtitlesMessagesContainer } from './SubtitlesMessagesContainer';
import { IReduxState } from '../../../app/types';
/**
* The styles for the ClosedCaptionsTab component.
*/
const useStyles = makeStyles()(theme => {
return {
subtitlesList: {
display: 'flex',
flexDirection: 'column',
height: '100%',
overflowY: 'auto',
padding: '16px',
flex: 1,
boxSizing: 'border-box',
color: theme.palette.text01
},
container: {
display: 'flex',
flexDirection: 'column',
height: '100%',
position: 'relative',
overflow: 'hidden'
},
messagesContainer: {
display: 'flex',
flexDirection: 'column',
flex: 1,
overflow: 'hidden'
},
emptyContent: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
padding: '16px',
boxSizing: 'border-box',
flexDirection: 'column',
gap: '16px',
color: theme.palette.text01,
textAlign: 'center'
},
emptyIcon: {
width: '100px',
padding: '16px',
'& svg': {
width: '100%',
height: 'auto'
}
},
emptyState: {
...withPixelLineHeight(theme.typography.bodyLongBold),
color: theme.palette.text02
}
};
});
/**
* Component that displays the subtitles history in a scrollable list.
*
* @returns {JSX.Element} - The ClosedCaptionsTab component.
*/
export default function ClosedCaptionsTab() {
const { classes, theme } = useStyles();
const dispatch = useDispatch();
const { t } = useTranslation();
const subtitles = useSelector((state: IReduxState) => state['features/subtitles'].subtitlesHistory);
const language = useSelector((state: IReduxState) => state['features/subtitles']._language);
const selectedLanguage = language?.replace('translation-languages:', '');
const _isTranscribing = useSelector(isTranscribing);
const _canStartSubtitles = useSelector(canStartSubtitles);
const [ isButtonPressed, setButtonPressed ] = useState(false);
const filteredSubtitles = useMemo(() => {
// First, create a map of transcription messages by message ID
const transcriptionMessages = new Map(
subtitles
.filter(s => s.isTranscription)
.map(s => [ s.id, s ])
);
if (!selectedLanguage) {
// When no language is selected, show all original transcriptions
return Array.from(transcriptionMessages.values());
}
// Then, create a map of translation messages by message ID
const translationMessages = new Map(
subtitles
.filter(s => !s.isTranscription && s.language === selectedLanguage)
.map(s => [ s.id, s ])
);
// When a language is selected, for each transcription message:
// 1. Use its translation if available
// 2. Fall back to the original transcription if no translation exists
return Array.from(transcriptionMessages.values())
.filter((m: ISubtitle) => !m.interim)
.map(m => translationMessages.get(m.id) ?? m);
}, [ subtitles, selectedLanguage ]);
const groupedSubtitles = useMemo(() =>
groupMessagesBySender(filteredSubtitles), [ filteredSubtitles ]);
const startClosedCaptions = useCallback(() => {
if (isButtonPressed) {
return;
}
dispatch(setRequestingSubtitles(true, false, null));
setButtonPressed(true);
}, [ dispatch, isButtonPressed, setButtonPressed ]);
if (!_isTranscribing) {
if (_canStartSubtitles) {
return (
<div className = { classes.emptyContent }>
<Button
accessibilityLabel = 'Start Closed Captions'
appearance = 'primary'
disabled = { isButtonPressed }
labelKey = 'closedCaptionsTab.startClosedCaptionsButton'
onClick = { startClosedCaptions }
size = 'large'
type = 'primary' />
</div>
);
}
if (isButtonPressed) {
setButtonPressed(false);
}
return (
<div className = { classes.emptyContent }>
<Icon
className = { classes.emptyIcon }
color = { theme.palette.icon03 }
src = { IconSubtitles } />
<span className = { classes.emptyState }>
{ t('closedCaptionsTab.emptyState') }
</span>
</div>
);
}
if (isButtonPressed) {
setButtonPressed(false);
}
return (
<div className = { classes.container }>
<LanguageSelector />
<div className = { classes.messagesContainer }>
<SubtitlesMessagesContainer
groups = { groupedSubtitles }
messages = { filteredSubtitles } />
</div>
</div>
);
}

View File

@@ -20,6 +20,11 @@ interface IProps extends WithTranslation {
*/
dispatch: IStore['dispatch'];
/**
* Whether CC tab is enabled or not.
*/
isCCTabEnabled: boolean;
/**
* Whether the polls feature is enabled or not.
*/
@@ -69,16 +74,26 @@ class DisplayNameForm extends Component<IProps, IState> {
* @returns {ReactElement}
*/
override render() {
const { isPollsEnabled, t } = this.props;
const { isCCTabEnabled, isPollsEnabled, t } = this.props;
let title = 'chat.nickname.title';
if (isCCTabEnabled && isPollsEnabled) {
title = 'chat.nickname.titleWithPollsAndCC';
} else if (isCCTabEnabled) {
title = 'chat.nickname.titleWithCC';
} else if (isPollsEnabled) {
title = 'chat.nickname.titleWithPolls';
}
return (
<div id = 'nickname'>
<form onSubmit = { this._onSubmit }>
<Input
accessibilityLabel = { t('chat.nickname.title') }
accessibilityLabel = { t(title) }
autoFocus = { true }
id = 'nickinput'
label = { t(isPollsEnabled ? 'chat.nickname.titleWithPolls' : 'chat.nickname.title') }
label = { t(title) }
name = 'name'
onChange = { this._onDisplayNameChange }
placeholder = { t('chat.nickname.popover') }

View File

@@ -1,13 +1,19 @@
import { throttle } from 'lodash-es';
import React, { RefObject } from 'react';
import React, { Component, RefObject } from 'react';
import { scrollIntoView } from 'seamless-scroll-polyfill';
import { groupMessagesBySender } from '../../../base/util/messageGrouping';
import { MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from '../../constants';
import AbstractMessageContainer, { IProps } from '../AbstractMessageContainer';
import { IMessage } from '../../types';
import ChatMessageGroup from './ChatMessageGroup';
import NewMessagesButton from './NewMessagesButton';
interface IProps {
messages: IMessage[];
}
interface IState {
/**
@@ -29,9 +35,9 @@ interface IState {
/**
* Displays all received chat messages, grouped by sender.
*
* @augments AbstractMessageContainer
* @augments Component
*/
export default class MessageContainer extends AbstractMessageContainer<IProps, IState> {
export default class MessageContainer extends Component<IProps, IState> {
/**
* Component state used to decide when the hasNewMessages button to appear
* and where to scroll when click on hasNewMessages button.
@@ -59,6 +65,10 @@ export default class MessageContainer extends AbstractMessageContainer<IProps, I
*/
_bottomListObserver: IntersectionObserver;
static defaultProps = {
messages: [] as IMessage[]
};
/**
* Initializes a new {@code MessageContainer} instance.
*
@@ -86,14 +96,15 @@ export default class MessageContainer extends AbstractMessageContainer<IProps, I
*/
override render() {
const groupedMessages = this._getMessagesGroupedBySender();
const messages = groupedMessages.map((group, index) => {
const messageType = group[0]?.messageType;
const content = groupedMessages.map((group, index) => {
const { messages } = group;
const messageType = messages[0]?.messageType;
return (
<ChatMessageGroup
className = { messageType || MESSAGE_TYPE_REMOTE }
key = { index }
messages = { group } />
messages = { messages } />
);
});
@@ -106,7 +117,7 @@ export default class MessageContainer extends AbstractMessageContainer<IProps, I
ref = { this._messageListRef }
role = 'log'
tabIndex = { 0 }>
{ messages }
{ content }
{ !this.state.isScrolledToBottom && this.state.hasNewMessages
&& <NewMessagesButton
@@ -313,4 +324,14 @@ export default class MessageContainer extends AbstractMessageContainer<IProps, I
return false;
}
/**
* Returns an array of message groups, where each group is an array of messages
* grouped by the sender.
*
* @returns {Array<Array<Object>>}
*/
_getMessagesGroupedBySender() {
return groupMessagesBySender(this.props.messages);
}
}

View File

@@ -0,0 +1,97 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { getParticipantDisplayName } from '../../../base/participants/functions';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import { ISubtitle } from '../../../subtitles/types';
/**
* Props for the SubtitleMessage component.
*/
interface IProps extends ISubtitle {
/**
* Whether to show the display name of the participant.
*/
showDisplayName: boolean;
}
/**
* The styles for the SubtitleMessage component.
*/
const useStyles = makeStyles()(theme => {
return {
messageContainer: {
backgroundColor: theme.palette.ui02,
borderRadius: '4px 12px 12px 12px',
padding: '12px',
maxWidth: '100%',
marginTop: '4px',
boxSizing: 'border-box',
display: 'inline-flex'
},
messageContent: {
maxWidth: '100%',
overflow: 'hidden',
flex: 1
},
messageHeader: {
...withPixelLineHeight(theme.typography.labelBold),
color: theme.palette.text02,
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
marginBottom: theme.spacing(1),
maxWidth: '130px'
},
messageText: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
},
timestamp: {
...withPixelLineHeight(theme.typography.labelRegular),
color: theme.palette.text03,
marginTop: theme.spacing(1)
},
interim: {
opacity: 0.7
}
};
});
/**
* Component that renders a single subtitle message with the participant's name,
* message content, and timestamp.
*
* @param {IProps} props - The component props.
* @returns {JSX.Element} - The rendered subtitle message.
*/
export default function SubtitleMessage({ participantId, text, timestamp, interim, showDisplayName }: IProps) {
const { classes } = useStyles();
const participantName = useSelector((state: any) =>
getParticipantDisplayName(state, participantId));
return (
<div className = { `${classes.messageContainer} ${interim ? classes.interim : ''}` }>
<div className = { classes.messageContent }>
{showDisplayName && (
<div className = { classes.messageHeader }>
{participantName}
</div>
)}
<div className = { classes.messageText }>{text}</div>
<div className = { classes.timestamp }>
{new Date(timestamp).toLocaleTimeString()}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,76 @@
import React from 'react';
import { makeStyles } from 'tss-react/mui';
import Avatar from '../../../base/avatar/components/Avatar';
import { ISubtitle } from '../../../subtitles/types';
import SubtitleMessage from './SubtitleMessage';
/**
* Props for the SubtitlesGroup component.
*/
interface IProps {
/**
* Array of subtitle messages to be displayed in this group.
*/
messages: ISubtitle[];
/**
* The ID of the participant who sent these subtitles.
*/
senderId: string;
}
const useStyles = makeStyles()(theme => {
return {
groupContainer: {
display: 'flex',
marginBottom: theme.spacing(3)
},
avatar: {
marginRight: theme.spacing(2),
alignSelf: 'flex-start'
},
messagesContainer: {
display: 'flex',
flexDirection: 'column',
flex: 1,
maxWidth: 'calc(100% - 56px)', // 40px avatar + 16px margin
gap: theme.spacing(1)
}
};
});
/**
* Component that renders a group of subtitle messages from the same sender.
*
* @param {IProps} props - The props for the component.
* @returns {JSX.Element} - A React component rendering a group of subtitles.
*/
export function SubtitlesGroup({ messages, senderId }: IProps) {
const { classes } = useStyles();
if (!messages.length) {
return null;
}
return (
<div className = { classes.groupContainer }>
<Avatar
className = { classes.avatar }
participantId = { senderId }
size = { 32 } />
<div className = { classes.messagesContainer }>
{messages.map((message, index) => (
<SubtitleMessage
key = { `${message.timestamp}-${message.id}` }
showDisplayName = { index === 0 }
{ ...message } />
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,154 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { scrollIntoView } from 'seamless-scroll-polyfill';
import { makeStyles } from 'tss-react/mui';
import { ISubtitle } from '../../../subtitles/types';
import NewMessagesButton from './NewMessagesButton';
import { SubtitlesGroup } from './SubtitlesGroup';
interface IProps {
groups: Array<{
messages: ISubtitle[];
senderId: string;
}>;
messages: ISubtitle[];
}
/**
* The padding value used for the message list.
*
* @constant {string}
*/
const MESSAGE_LIST_PADDING = '16px';
const useStyles = makeStyles()(() => {
return {
container: {
flex: 1,
overflow: 'hidden',
position: 'relative',
height: '100%'
},
messagesList: {
height: '100%',
overflowY: 'auto',
padding: MESSAGE_LIST_PADDING,
boxSizing: 'border-box'
}
};
});
/**
* Component that handles the display and scrolling behavior of subtitles messages.
* It provides auto-scrolling for new messages and a button to jump to new messages
* when the user has scrolled up.
*
* @returns {JSX.Element} - A React component displaying subtitles messages with scroll functionality.
*/
export function SubtitlesMessagesContainer({ messages, groups }: IProps) {
const { classes } = useStyles();
const [ hasNewMessages, setHasNewMessages ] = useState(false);
const [ isScrolledToBottom, setIsScrolledToBottom ] = useState(true);
const [ observer, setObserver ] = useState<IntersectionObserver | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToElement = useCallback((withAnimation: boolean, element: Element | null) => {
const scrollTo = element ? element : messagesEndRef.current;
const block = element ? 'end' : 'nearest';
scrollIntoView(scrollTo as Element, {
behavior: withAnimation ? 'smooth' : 'auto',
block
});
}, [ messagesEndRef.current ]);
const handleNewMessagesClick = useCallback(() => {
scrollToElement(true, null);
}, [ scrollToElement ]);
const handleIntersectBottomList = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry: IntersectionObserverEntry) => {
if (entry.isIntersecting) {
setIsScrolledToBottom(true);
setHasNewMessages(false);
}
if (!entry.isIntersecting) {
setIsScrolledToBottom(false);
}
});
};
const createBottomListObserver = () => {
const target = document.querySelector('#subtitles-messages-end');
if (target) {
const newObserver = new IntersectionObserver(
handleIntersectBottomList, {
root: document.querySelector('#subtitles-messages-list'),
rootMargin: MESSAGE_LIST_PADDING,
threshold: 1
});
setObserver(newObserver);
newObserver.observe(target);
}
};
useEffect(() => {
scrollToElement(false, null);
createBottomListObserver();
return () => {
if (observer) {
observer.disconnect();
setObserver(null);
}
};
}, []);
const previousMessages = useRef(messages);
useEffect(() => {
const newMessages = messages.filter(message => !previousMessages.current.includes(message));
if (newMessages.length > 0) {
if (isScrolledToBottom) {
scrollToElement(false, null);
} else {
setHasNewMessages(true);
}
}
previousMessages.current = messages;
},
// isScrolledToBottom is not a dependency because we neither need to show the new messages button neither scroll to the
// bottom when the user has scrolled up.
[ messages, scrollToElement ]);
return (
<div
className = { classes.container }
id = 'subtitles-messages-container'>
<div
className = { classes.messagesList }
id = 'subtitles-messages-list'>
{groups.map(group => (
<SubtitlesGroup
key = { `${group.senderId}-${group.messages[0].timestamp}` }
messages = { group.messages }
senderId = { group.senderId } />
))}
{ !isScrolledToBottom && hasNewMessages && (
<NewMessagesButton
onGoToFirstUnreadMessage = { handleNewMessagesClick } />
)}
<div
id = 'subtitles-messages-end'
ref = { messagesEndRef } />
</div>
</div>
);
}

View File

@@ -39,10 +39,11 @@ export const SMALL_WIDTH_THRESHOLD = 580;
*/
export const LOBBY_CHAT_MESSAGE = 'LOBBY_CHAT_MESSAGE';
export const CHAT_TABS = {
POLLS: 'polls-tab',
CHAT: 'chat-tab'
};
export enum ChatTabs {
CHAT = 'chat-tab',
CLOSED_CAPTIONS = 'cc-tab',
POLLS = 'polls-tab'
}
/**
* Formatter string to display the message timestamp.

View File

@@ -40,11 +40,12 @@ import {
OPEN_CHAT,
SEND_MESSAGE,
SEND_REACTION,
SET_IS_POLL_TAB_FOCUSED
SET_FOCUSED_TAB
} from './actionTypes';
import { addMessage, addMessageReaction, clearMessages, closeChat, setPrivateMessageRecipient } from './actions.any';
import { ChatPrivacyDialog } from './components';
import {
ChatTabs,
INCOMING_MSG_SOUND_ID,
LOBBY_CHAT_MESSAGE,
MESSAGE_TYPE_ERROR,
@@ -103,15 +104,15 @@ MiddlewareRegistry.register(store => next => action => {
break;
case CLOSE_CHAT: {
const isPollTabOpen = getState()['features/chat'].isPollsTabFocused;
const { focusedTab } = getState()['features/chat'];
unreadCount = 0;
if (focusedTab === ChatTabs.CHAT) {
unreadCount = 0;
if (typeof APP !== 'undefined') {
APP.API.notifyChatUpdated(unreadCount, false);
}
if (isPollTabOpen) {
if (typeof APP !== 'undefined') {
APP.API.notifyChatUpdated(unreadCount, false);
}
} else if (focusedTab === ChatTabs.POLLS) {
dispatch(resetNbUnreadPollsMessages());
}
break;
@@ -161,35 +162,37 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case SET_FOCUSED_TAB:
case OPEN_CHAT: {
unreadCount = 0;
const focusedTab = action.tabId || getState()['features/chat'].focusedTab;
if (typeof APP !== 'undefined') {
APP.API.notifyChatUpdated(unreadCount, true);
}
if (focusedTab === ChatTabs.CHAT) {
unreadCount = 0;
const { privateMessageRecipient } = store.getState()['features/chat'];
if (
isSendGroupChatDisabled(store.getState())
&& privateMessageRecipient
&& !action.participant
) {
const participant = getParticipantById(store.getState(), privateMessageRecipient.id);
if (participant) {
action.participant = participant;
if (typeof APP !== 'undefined') {
APP.API.notifyChatUpdated(unreadCount, true);
}
const { privateMessageRecipient } = store.getState()['features/chat'];
if (
isSendGroupChatDisabled(store.getState())
&& privateMessageRecipient
&& !action.participant
) {
const participant = getParticipantById(store.getState(), privateMessageRecipient.id);
if (participant) {
action.participant = participant;
}
}
} else if (focusedTab === ChatTabs.POLLS) {
dispatch(resetNbUnreadPollsMessages());
}
break;
}
case SET_IS_POLL_TAB_FOCUSED: {
dispatch(resetNbUnreadPollsMessages());
break;
}
case SEND_MESSAGE: {
const state = store.getState();
const conference = getCurrentConference(state);
@@ -256,7 +259,6 @@ MiddlewareRegistry.register(store => next => action => {
lobbyChat: false
}, false, true);
}
break;
}
}
@@ -530,8 +532,7 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
// skip message notifications on join (the messages having timestamp - coming from the history)
const shouldShowNotification = userSelectedNotifications?.['notify.chatMessages']
&& !hasRead && !isReaction
&& (!timestamp || lobbyChat);
&& !hasRead && !isReaction && (!timestamp || lobbyChat);
if (isGuest) {
displayNameToShow = `${displayNameToShow} ${i18next.t('visitors.chatIndicator')}`;

View File

@@ -1,5 +1,6 @@
import { ILocalParticipant, IParticipant } from '../base/participants/types';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { ChatTabs } from './constants';
import {
ADD_MESSAGE,
@@ -9,10 +10,10 @@ import {
EDIT_MESSAGE,
OPEN_CHAT,
REMOVE_LOBBY_CHAT_PARTICIPANT,
SET_IS_POLL_TAB_FOCUSED,
SET_LOBBY_CHAT_ACTIVE_STATE,
SET_LOBBY_CHAT_RECIPIENT,
SET_PRIVATE_MESSAGE_RECIPIENT
SET_PRIVATE_MESSAGE_RECIPIENT,
SET_FOCUSED_TAB
} from './actionTypes';
import { IMessage } from './types';
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
@@ -20,21 +21,20 @@ import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
const DEFAULT_STATE = {
groupChatWithPermissions: false,
isOpen: false,
isPollsTabFocused: false,
lastReadMessage: undefined,
messages: [],
reactions: {},
nbUnreadMessages: 0,
privateMessageRecipient: undefined,
lobbyMessageRecipient: undefined,
isLobbyChatActive: false
isLobbyChatActive: false,
focusedTab: ChatTabs.CHAT
};
export interface IChatState {
focusedTab: ChatTabs;
groupChatWithPermissions: boolean;
isLobbyChatActive: boolean;
isOpen: boolean;
isPollsTabFocused: boolean;
lastReadMessage?: IMessage;
lobbyMessageRecipient?: {
id: string;
@@ -78,7 +78,7 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
...state,
lastReadMessage:
action.hasRead ? newMessage : state.lastReadMessage,
nbUnreadMessages: state.isPollsTabFocused ? state.nbUnreadMessages + 1 : state.nbUnreadMessages,
nbUnreadMessages: state.focusedTab !== ChatTabs.CHAT ? state.nbUnreadMessages + 1 : state.nbUnreadMessages,
messages
};
}
@@ -170,13 +170,6 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
isLobbyChatActive: false
};
case SET_IS_POLL_TAB_FOCUSED: {
return {
...state,
isPollsTabFocused: action.isPollsTabFocused,
nbUnreadMessages: 0
}; }
case SET_LOBBY_CHAT_RECIPIENT:
return {
...state,
@@ -215,7 +208,15 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
groupChatWithPermissions: Boolean(metadata.permissions.groupChatRestricted)
};
}
break;
}
case SET_FOCUSED_TAB:
return {
...state,
focusedTab: action.tabId,
nbUnreadMessages: action.tabId === ChatTabs.CHAT ? 0 : state.nbUnreadMessages
};
}
return state;

View File

@@ -180,6 +180,11 @@ class Conference extends AbstractConference<IProps, State> {
*/
_expandedLabelTimeout: any;
/**
* Initializes hardwareBackPress subscription.
*/
_hardwareBackPressSubscription: any;
/**
* Initializes a new Conference instance.
*
@@ -216,7 +221,7 @@ class Conference extends AbstractConference<IProps, State> {
navigation
} = this.props;
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
this._hardwareBackPressSubscription = BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
if (_audioOnlyEnabled && _startCarMode) {
navigation.navigate(screen.conference.carmode);
@@ -258,7 +263,7 @@ class Conference extends AbstractConference<IProps, State> {
*/
override componentWillUnmount() {
// Tear handling any hardware button presses for back navigation down.
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
this._hardwareBackPressSubscription?.remove();
clearTimeout(this._expandedLabelTimeout.current ?? 0);
}

View File

@@ -275,7 +275,7 @@ class DesktopPicker extends PureComponent<IProps, IState> {
const { sources } = this.state;
// @ts-ignore
const source = sources.screen.concat(sources.window).find(s => s.id === id);
const source = (sources?.screen ?? []).concat(sources?.window ?? []).find(s => s.id === id);
this.props.onSourceChoose(id, type, screenShareAudio, source);
this.props.dispatch(hideDialog());

View File

@@ -249,6 +249,8 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
const { item } = flatListItem;
switch (item.type) {
// isCORSAvatarURL in this case is false
case INVITE_TYPES.PHONE:
return {
avatar: IconPhoneRinging,

View File

@@ -144,9 +144,8 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case UPDATE_DIAL_IN_NUMBERS_FAILED:
logger.error(
'Error encountered while fetching dial-in numbers:',
action.error);
logger.error('Error encountered while fetching dial-in numbers:', action.error,
action.error?.message, action.error?.name, action.error?.stack);
break;
}

View File

@@ -11,11 +11,13 @@ import Watermarks from '../../base/react/components/web/Watermarks';
import { getHideSelfView } from '../../base/settings/functions.any';
import { getVideoTrackByParticipant } from '../../base/tracks/functions.web';
import { setColorAlpha } from '../../base/util/helpers';
import { isSpotTV } from '../../base/util/spot';
import StageParticipantNameLabel from '../../display-name/components/web/StageParticipantNameLabel';
import { FILMSTRIP_BREAKPOINT } from '../../filmstrip/constants';
import { getVerticalViewMaxWidth, isFilmstripResizable } from '../../filmstrip/functions.web';
import SharedVideo from '../../shared-video/components/web/SharedVideo';
import Captions from '../../subtitles/components/web/Captions';
import { areClosedCaptionsEnabled } from '../../subtitles/functions.any';
import { setTileView } from '../../video-layout/actions.web';
import Whiteboard from '../../whiteboard/components/web/Whiteboard';
import { isWhiteboardEnabled } from '../../whiteboard/functions';
@@ -24,8 +26,6 @@ import { getLargeVideoParticipant } from '../functions';
import ScreenSharePlaceholder from './ScreenSharePlaceholder.web';
// Hack to detect Spot.
const SPOT_DISPLAY_NAME = 'Meeting Room';
interface IProps {
@@ -100,6 +100,11 @@ interface IProps {
*/
_showDominantSpeakerBadge: boolean;
/**
* Whether or not to show subtitles button.
*/
_showSubtitles?: boolean;
/**
* The width of the vertical filmstrip (user resized).
*/
@@ -200,7 +205,8 @@ class LargeVideo extends Component<IProps> {
_isDisplayNameVisible,
_noAutoPlayVideo,
_showDominantSpeakerBadge,
_whiteboardEnabled
_whiteboardEnabled,
_showSubtitles
} = this.props;
const style = this._getCustomStyles();
const className = `videocontainer${_isChatOpen ? ' shift-right' : ''}`;
@@ -248,8 +254,8 @@ class LargeVideo extends Component<IProps> {
playsInline = { true } /* for Safari on iOS to work */ />
</div>
</div>
{ interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES
|| <Captions /> }
{ (!interfaceConfig.DISABLE_TRANSCRIPTION_SUBTITLES && _showSubtitles)
&& <Captions /> }
{
_isDisplayNameVisible
&& (
@@ -364,20 +370,20 @@ function _mapStateToProps(state: IReduxState) {
const { backgroundColor, backgroundImageUrl } = state['features/dynamic-branding'];
const { isOpen: isChatOpen } = state['features/chat'];
const { width: verticalFilmstripWidth, visible } = state['features/filmstrip'];
const { defaultLocalDisplayName, hideDominantSpeakerBadge } = state['features/base/config'];
const { hideDominantSpeakerBadge } = state['features/base/config'];
const { seeWhatIsBeingShared } = state['features/large-video'];
const localParticipantId = getLocalParticipant(state)?.id;
const largeVideoParticipant = getLargeVideoParticipant(state);
const videoTrack = getVideoTrackByParticipant(state, largeVideoParticipant);
const isLocalScreenshareOnLargeVideo = largeVideoParticipant?.id?.includes(localParticipantId ?? '')
&& videoTrack?.videoType === VIDEO_TYPE.DESKTOP;
const isOnSpot = defaultLocalDisplayName === SPOT_DISPLAY_NAME;
return {
_backgroundAlpha: state['features/base/config'].backgroundAlpha,
_customBackgroundColor: backgroundColor,
_customBackgroundImageUrl: backgroundImageUrl,
_displayScreenSharingPlaceholder: Boolean(isLocalScreenshareOnLargeVideo && !seeWhatIsBeingShared && !isOnSpot),
_displayScreenSharingPlaceholder:
Boolean(isLocalScreenshareOnLargeVideo && !seeWhatIsBeingShared && !isSpotTV()),
_hideSelfView: getHideSelfView(state),
_isChatOpen: isChatOpen,
_isDisplayNameVisible: isDisplayNameVisible(state),
@@ -388,6 +394,8 @@ function _mapStateToProps(state: IReduxState) {
_resizableFilmstrip: isFilmstripResizable(state),
_seeWhatIsBeingShared: Boolean(seeWhatIsBeingShared),
_showDominantSpeakerBadge: !hideDominantSpeakerBadge,
_showSubtitles: areClosedCaptionsEnabled(state)
&& Boolean(state['features/base/settings'].showSubtitlesOnStage),
_verticalFilmstripWidth: verticalFilmstripWidth.current,
_verticalViewMaxWidth: getVerticalViewMaxWidth(state),
_visibleFilmstrip: visible,

View File

@@ -9,9 +9,9 @@ import {
getClientHeight,
getClientWidth
} from '../../../../../base/modal/components/functions';
import { setIsPollsTabFocused } from '../../../../../chat/actions.native';
// @ts-ignore
import { setFocusedTab } from '../../../../../chat/actions.any';
import Chat from '../../../../../chat/components/native/Chat';
import { ChatTabs } from '../../../../../chat/constants';
import { resetNbUnreadPollsMessages } from '../../../../../polls/actions';
import PollsPane from '../../../../../polls/components/native/PollsPane';
import { screen } from '../../../routes';
@@ -23,8 +23,8 @@ const ChatAndPolls = () => {
const clientHeight = useSelector(getClientHeight);
const clientWidth = useSelector(getClientWidth);
const dispatch = useDispatch();
const { isPollsTabFocused } = useSelector((state: IReduxState) => state['features/chat']);
const initialRouteName = isPollsTabFocused
const { focusedTab } = useSelector((state: IReduxState) => state['features/chat']);
const initialRouteName = focusedTab === ChatTabs.POLLS
? screen.conference.chatandpolls.tab.polls
: screen.conference.chatandpolls.tab.chat;
@@ -42,7 +42,7 @@ const ChatAndPolls = () => {
component = { Chat }
listeners = {{
tabPress: () => {
dispatch(setIsPollsTabFocused(false));
dispatch(setFocusedTab(ChatTabs.CHAT));
}
}}
name = { screen.conference.chatandpolls.tab.chat } />
@@ -50,7 +50,7 @@ const ChatAndPolls = () => {
component = { PollsPane }
listeners = {{
tabPress: () => {
dispatch(setIsPollsTabFocused(true));
dispatch(setFocusedTab(ChatTabs.POLLS));
dispatch(resetNbUnreadPollsMessages);
}
}}

View File

@@ -33,6 +33,7 @@ class PageReloadOverlay extends AbstractPageReloadOverlay<IProps> {
className = 'inlay'
role = 'dialog'>
<span
aria-level = { 1 }
className = 'reload_overlay_title'
id = 'reload_overlay_title'
role = 'heading'>

View File

@@ -130,6 +130,7 @@ function MeetingParticipants({
accessibilityLabel = { t('participantsPane.search') }
className = { styles.search }
clearable = { true }
hiddenDescription = { t('participantsPane.searchDescription') }
id = 'participants-search-input'
onChange = { setSearchString }
placeholder = { t('participantsPane.search') }

View File

@@ -68,11 +68,13 @@ export const AudioStateIcons = {
[MEDIA_STATE.FORCE_MUTED]: (
<Icon
color = '#E04757'
id = 'audioMuted'
size = { 16 }
src = { IconMicSlash } />
),
[MEDIA_STATE.MUTED]: (
<Icon
id = 'audioMuted'
size = { 16 }
src = { IconMicSlash } />
),

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