mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-01 04:12:27 +00:00
Compare commits
62 Commits
8525
...
pr-testing
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17e1774f6e | ||
|
|
249bd3a660 | ||
|
|
23bba927f3 | ||
|
|
7d48ea1bca | ||
|
|
a07a1cfe93 | ||
|
|
16c45c15c8 | ||
|
|
5d5d6c3068 | ||
|
|
19399ec123 | ||
|
|
3c27f15490 | ||
|
|
607073c669 | ||
|
|
f92ee57f9c | ||
|
|
32331d7465 | ||
|
|
88685c43fb | ||
|
|
348573b254 | ||
|
|
1a05991b8c | ||
|
|
c3f2390642 | ||
|
|
7cf8902fdd | ||
|
|
3e4fb82d58 | ||
|
|
057dc0e4d2 | ||
|
|
ce4cbacceb | ||
|
|
af4f122602 | ||
|
|
b7f5b8ecd2 | ||
|
|
d15e51adbd | ||
|
|
affef1ac66 | ||
|
|
7f95dbb6d6 | ||
|
|
8065da61c7 | ||
|
|
b6df08f072 | ||
|
|
73752618a3 | ||
|
|
3c7829a457 | ||
|
|
0b0bbcf194 | ||
|
|
3cd29fd63e | ||
|
|
c486dc07db | ||
|
|
b48772f898 | ||
|
|
21533f0261 | ||
|
|
1ecfa3a78e | ||
|
|
921c27165d | ||
|
|
6fa94b0bb4 | ||
|
|
3f3a074e02 | ||
|
|
ecb57774ed | ||
|
|
1e82f1fb1a | ||
|
|
b4b54108c5 | ||
|
|
6903ccf009 | ||
|
|
6e45643cce | ||
|
|
379b26a753 | ||
|
|
e8faa99ff0 | ||
|
|
62d2c9f633 | ||
|
|
311c404a1a | ||
|
|
b17a3a9d86 | ||
|
|
5477e46489 | ||
|
|
7e170fe0d0 | ||
|
|
bbbc9d1f46 | ||
|
|
084905545f | ||
|
|
d542b05bc4 | ||
|
|
58b73d8dbd | ||
|
|
471853ef15 | ||
|
|
a0070c34f7 | ||
|
|
5bc8bc8b64 | ||
|
|
ea04c5e96e | ||
|
|
d4e2f17acc | ||
|
|
4dcc4b3711 | ||
|
|
80081ce31c | ||
|
|
5314d779ed |
@@ -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));
|
||||
}
|
||||
|
||||
14
config.js
14
config.js
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#enter_room {
|
||||
.welcome-page-button {
|
||||
font-size: 16px;
|
||||
font-size: 1rem;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.share-screen-warn-dialog {
|
||||
font-size: 14px;
|
||||
font-size: 0.875rem;
|
||||
|
||||
.separator-line {
|
||||
margin: 24px 0 24px -20px;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -984,7 +984,7 @@
|
||||
"incomingMessage": "Εισερχόμενο μήνυμα",
|
||||
"language": "Γλώσσα",
|
||||
"loggedIn": "Συνδέθηκε ως {{name}}",
|
||||
"maxStageParticipants": "Μέγιστος αριθμός συμμετεχόντων που μπορούν να διατηρηθούν στην κύρια σκηνή (ΠΕΙΡΑΜΑΤΙΚΟ)",
|
||||
"maxStageParticipants": "Μέγιστος αριθμός συμμετεχόντων που μπορούν να διατηρηθούν στην κύρια σκηνή",
|
||||
"microphones": "Μικρόφωνα",
|
||||
"moderator": "Συντονιστής",
|
||||
"moderatorOptions": "Επιλογές συντονιστή",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1026,7 +1026,7 @@
|
||||
"incomingMessage": "پیام ورودی",
|
||||
"language": "زبان",
|
||||
"loggedIn": "واردشده به عنوان {{name}}",
|
||||
"maxStageParticipants": "بیشینه تعداد شرکتکنندگانی که میتوانند به صحنه اصلی سنجاق شوند (<b>آزمایشی</b>)",
|
||||
"maxStageParticipants": "بیشینه تعداد شرکتکنندگانی که میتوانند به صحنه اصلی سنجاق شوند",
|
||||
"microphones": "میکروفونها",
|
||||
"moderator": "مدیر",
|
||||
"moderatorOptions": "گزینههای مدیر",
|
||||
|
||||
@@ -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 l’affichage principal (EXPÉRIMENTAL)",
|
||||
"maxStageParticipants": "Nombre maximum de participants pouvant être épinglé sur l’affichage principal",
|
||||
"microphones": "Microphones",
|
||||
"moderator": "Modérateur",
|
||||
"moderatorOptions": "Options de modérateur",
|
||||
|
||||
@@ -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 l’affichage principal (EXPÉRIMENTAL)",
|
||||
"maxStageParticipants": "Nombre maximum de participants pouvant être épinglé sur l’affichage principal",
|
||||
"microphones": "Microphones",
|
||||
"moderator": "Modérateur",
|
||||
"moderatorOptions": "Options de modérateur",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1110,7 +1110,7 @@
|
||||
"incomingMessage": "수신 메시지",
|
||||
"language": "언어",
|
||||
"loggedIn": "{{name}}으로 로그인",
|
||||
"maxStageParticipants": "메인 스테이지에 고정할 수 있는 최대 참가자 수 (실험적 기능)",
|
||||
"maxStageParticipants": "메인 스테이지에 고정할 수 있는 최대 참가자 수",
|
||||
"microphones": "마이크",
|
||||
"moderator": "진행자",
|
||||
"moderatorOptions": "진행자 옵션",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -997,7 +997,7 @@
|
||||
"incomingMessage": "Ирсэн мессэж",
|
||||
"language": "Хэл",
|
||||
"loggedIn": "{{name}} нэвтэрсэн",
|
||||
"maxStageParticipants": "Үндсэн тайз руу гарах оролцогчийн хамгийн их тоо(Туршилтынх)",
|
||||
"maxStageParticipants": "Үндсэн тайз руу гарах оролцогчийн хамгийн их тоо",
|
||||
"microphones": "Микрофон",
|
||||
"moderator": "Зохицуулагч",
|
||||
"moderatorOptions": "Зохицуулагчийн сонголт",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1083,7 +1083,7 @@
|
||||
"incomingMessage": "Входящее сообщение",
|
||||
"language": "Язык",
|
||||
"loggedIn": "Вошел как {{name}}",
|
||||
"maxStageParticipants": "Максимальное количество участников, которых можно закрепить на главной сцене (ЭКСПЕРИМЕНТАЛЬНО)",
|
||||
"maxStageParticipants": "Максимальное количество участников, которых можно закрепить на главной сцене",
|
||||
"microphones": "Микрофоны",
|
||||
"moderator": "Модератор",
|
||||
"moderatorOptions": "Настройки модератора",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -995,7 +995,7 @@
|
||||
"incomingMessage": "Вхідне повідомлення",
|
||||
"language": "Мова",
|
||||
"loggedIn": "Увійшли як {{name}}",
|
||||
"maxStageParticipants": "Максимальна кількість учасників, яку можна закріпити на головній сцені (ТЕСТУВАННЯ)",
|
||||
"maxStageParticipants": "Максимальна кількість учасників, яку можна закріпити на головній сцені",
|
||||
"microphones": "Мікрофони",
|
||||
"moderator": "Модератор",
|
||||
"moderatorOptions": "Параметри модерації",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1049,7 +1049,7 @@
|
||||
"incomingMessage": "新消息",
|
||||
"language": "语言",
|
||||
"loggedIn": "以{{name}}登录",
|
||||
"maxStageParticipants": "可以固定的最大参会者人数(实验性功能)",
|
||||
"maxStageParticipants": "可以固定的最大参会者人数",
|
||||
"microphones": "麦克风",
|
||||
"moderator": "主持人",
|
||||
"moderatorOptions": "主持人选项",
|
||||
|
||||
@@ -1066,7 +1066,7 @@
|
||||
"incomingMessage": "新訊息",
|
||||
"language": "語言",
|
||||
"loggedIn": "以{{name}}登入",
|
||||
"maxStageParticipants": "可被釘選的最大與會者人數(實驗性功能)",
|
||||
"maxStageParticipants": "可被釘選的最大與會者人數",
|
||||
"microphones": "麥克風",
|
||||
"moderator": "主持人",
|
||||
"moderatorOptions": "主持人選項",
|
||||
|
||||
@@ -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": "Don’t 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
991
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* localFlipX: boolean,
|
||||
* micDeviceId: string,
|
||||
* serverURL: string,
|
||||
* showSubtitlesOnStage: boolean,
|
||||
* startAudioOnly: boolean,
|
||||
* startWithAudioMuted: boolean,
|
||||
* startWithVideoMuted: boolean,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
29
react/features/base/ui/components/web/HiddenDescription.tsx
Normal file
29
react/features/base/ui/components/web/HiddenDescription.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ interface ThemeProps {
|
||||
colorMap: Object;
|
||||
font: Object;
|
||||
shape: Object;
|
||||
spacing: Array<number>;
|
||||
spacing: Array<number | string>;
|
||||
typography: Object;
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
79
react/features/base/util/messageGrouping.ts
Normal file
79
react/features/base/util/messageGrouping.ts
Normal 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;
|
||||
}
|
||||
9
react/features/base/util/spot.ts
Normal file
9
react/features/base/util/spot.ts
Normal 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/');
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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') }
|
||||
|
||||
178
react/features/chat/components/web/ClosedCaptionsTab.tsx
Normal file
178
react/features/chat/components/web/ClosedCaptionsTab.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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') }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
97
react/features/chat/components/web/SubtitleMessage.tsx
Normal file
97
react/features/chat/components/web/SubtitleMessage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
76
react/features/chat/components/web/SubtitlesGroup.tsx
Normal file
76
react/features/chat/components/web/SubtitlesGroup.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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')}`;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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') }
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user