Compare commits

...

18 Commits

Author SHA1 Message Date
Horatiu Muresan
2eabf537d7 fix(context-menu) Fit the overflow menu on small heights 2023-01-31 14:10:30 +02:00
Robert Pintilii
0d5dae7ab9 feat(prejoin) Update design (#12844) 2023-01-30 13:34:56 +02:00
Calin-Teodor
992bf47850 feat(polls/native): changed remove option button color to match web 2023-01-30 13:09:52 +02:00
Robert Pintilii
4f34a576d0 feat(polls) Redesign (#12838)
Convert files to TS
Move styles from SCSS to JSS
Implement redesign
2023-01-30 11:35:21 +02:00
Robert Pintilii
921f3ee8cd fix(chat) Break long text in multiple lines (#12842) 2023-01-30 10:48:17 +02:00
Horatiu Muresan
42838e756c fix(self-view) Hide self view when alone in meet as well (#12831) 2023-01-27 16:30:47 +02:00
Werner Fleischer
e6eba3536a feat(ci): using .luacheckrc 2023-01-27 10:59:07 +01:00
Gabriel Borlea
74bdb7bc3f fix(face-landmarks): set session id for webhook using method (#12834) 2023-01-26 23:00:25 +02:00
Mihaela Dumitru
192187db32 feat(config/giphy) add proxyUrl config for giphy requests (#12816) 2023-01-26 16:12:12 +02:00
Saúl Ibarra Corretgé
5dce74e21d fix(external-api) fix sending forms after introduction of sandbox
Ref: https://github.com/jitsi/jitsi-meet-electron/issues/837
2023-01-26 11:36:58 +01:00
Hristo Terezov
fd7b6f457e fix(Thumbnail):limit the size of the avatar to 200 2023-01-26 09:18:25 +02:00
Kirat
2bf3089dbf Fixed overflow of popover (#12814) 2023-01-25 19:09:55 +01:00
Calin-Teodor
d396727e39 feat(base/lib-jitsi-meet): fixed build failing from bad import 2023-01-25 17:30:13 +02:00
Horatiu Muresan
3e58cd8af3 feat(narrow-layout) Use drawer menus on desktop narrow mode (#12799) 2023-01-25 17:02:26 +02:00
Robert Pintilii
cf7e692186 fix(recording-dialog) Fix switch UI (#12826) 2023-01-25 10:45:40 +02:00
Jaya Allamsetty
5e90e72562 Feat ssrc rewriting (#12408)
Use a fixed set of remote tracks for a call based on the ssrc-limit set in jvb config. When this feature is enabled, JVB will signal the audio and video SSRCs and their owner/endpoint info using a bridge channel message. For audio, the mappings are determined based on the energy rankings on the audio sources present in the call and for video, the mappings are calculated based on the video sources the client requests through the video receiver constraints.
Remote tracks are then created/remapped by the client based on these mappings.

* Added track_owner_changed action
* Skip track-based large-video selection in rewriting mode.
* Register OWNER_CHANGED handler at track level.
* feat(participants) Add source info to participants in redux.
With ssrc-rewriting, the receiver constraints need to be generated using the source info received in presence. Currently they are generated from the track info in redux but with ssrc-rewriting, remote sources are not signaled and therefore created until they are being requested through receiver constraints.

Co-authored-by: James A <jqdrqgnq@users.noreply.github.com>
2023-01-24 13:58:58 -05:00
Jaya Allamsetty
cbae997eda chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1577.0.0+5713cb0d...v1578.0.0+5855ca72
2023-01-24 13:51:03 -05:00
Jaya Allamsetty
6bd9a6bd15 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1574.0.0+4d6873c2...v1577.0.0+5713cb0d
2023-01-24 10:18:43 -05:00
81 changed files with 1348 additions and 1139 deletions

View File

@@ -17,8 +17,7 @@ jobs:
- name: Check lua codes
run: |
set -o pipefail
luacheck -q -gur -i 581 --no-max-line-length --no-color --formatter plain $(find -name '*.lua') | awk -F: '
set -o pipefail && luacheck . | awk -F: '
{
print $0
printf "::warning file=%s,line=%s,col=%s::%s\n", $1, $2, $3, $4

8
.luacheckrc Normal file
View File

@@ -0,0 +1,8 @@
global = false
unused = false
redefined = false
ignore = { "581" }
max_line_length = false
color = false
formatter = "plain"
quiet = 1

View File

@@ -103,6 +103,7 @@ import {
participantMutedUs,
participantPresenceChanged,
participantRoleChanged,
participantSourcesUpdated,
participantUpdated,
screenshareParticipantDisplayNameChanged,
updateRemoteParticipantFeatures
@@ -1987,6 +1988,11 @@ export default {
APP.store.dispatch(participantKicked(kicker, kicked));
});
room.on(JitsiConferenceEvents.PARTICIPANT_SOURCE_UPDATED,
jitsiParticipant => {
APP.store.dispatch(participantSourcesUpdated(jitsiParticipant));
});
room.on(JitsiConferenceEvents.SUSPEND_DETECTED, () => {
APP.store.dispatch(suspendDetected());
});

View File

@@ -1515,6 +1515,8 @@ var config = {
// tileTime: 5000,
// // Limit results by rating: g, pg, pg-13, r. Default value: g.
// rating: 'pg',
// // The proxy server url for giphy requests in the web app.
// proxyUrl: 'https://giphy-proxy.example.com',
// },
// Logging

View File

@@ -1,353 +1,3 @@
.poll-dialog {
font-size: 14px;
font-weight: 400;
line-height: 20px;
h1, span, li, strong {
color: #bce;
}
ol {
margin: 0;
}
}
.poll-question-field {
padding: 8px 16px;
padding-bottom: 24px;
border-bottom: 1px solid #525252;
}
.poll-header {
margin-bottom: 8px;
}
.poll-creator {
color: #C2C2C2;
font-weight: 600;
margin: 4px 0 16px 0;
}
.poll-answer-container {
display: flex;
padding: 4px;
background: #3D3D3D;
border-radius: 3px;
margin-bottom: 8px;
@media (max-width: 580px) {
&> span {
padding: 8px 0;
}
svg {
margin-top: 6px;
}
}
}
.poll-answer-field-list, .poll-answer-list, .poll-result-list {
list-style-type: none;
padding: 0;
margin: 0;
}
.poll-answer-field-list {
padding: 0 16px;
}
ol.poll-result-list {
margin-bottom: 1.5em;
}
.poll-result-list > li {
margin-bottom: 16px;
}
.poll-answer-field {
flex-direction: column;
align-items: stretch;
margin-bottom: 16;
}
.poll-answer-field:last-child {
margin-bottom: 0;
}
.poll-create-option-row {
display: flex;
margin-bottom: 4;
}
// Needed to override atlaskit default blue color
.poll-create-container .jsYMHu {
background: #292929;
border-color: #808090;
color: #fff // #808090
}
.poll-add-button {
display: flex;
justify-content: center;
padding: 8px 16px;
}
.poll-remove-option-button {
background: 0 0;
border: none;
color: #E04757;
padding-left: 0;
}
.poll-create-add-option {
border: none;
background-color: #292929;
padding: 3px;
width: 100%;
}
.poll-icon-button, .poll-drag-handle {
.jitsi-icon svg {
fill: #929292;
}
}
.poll-drag-handle {
background-color: transparent;
border: none;
cursor: grab;
padding-left: 8;
padding-top: 8px;
display: flex;
}
.poll-question {
font-size: 16px;
font-weight: 600;
line-height: 26px;
}
.poll-answer-voters {
font-weight: lighter;
list-style-type: none;
border: #616161 solid 1px;
border-radius: 3px;
padding: 2px 6px;
margin: 4px 0px 12px;
background-color: #616161;
}
.poll-answer-header {
display: flex;
justify-content: space-between;
}
.poll-answer-vote-name {
flex-shrink: 1;
overflow-wrap: anywhere
}
.poll-answer-vote-count-container{
display: flex;
}
.poll-answer-vote-count {
margin-left: 10px;
white-space: nowrap;
flex: 1;
text-align: right;
}
.poll-answer-short-results{
display: flex;
min-width: 10em;
justify-content: space-between;
align-items: center;
}
.poll-bar-container, .poll-bar {
border-radius: 3px;
height: 6px;
}
.poll-bar-container {
background-color: #616161;
max-width: 160px;
margin-top: 3px;
flex: 1;
}
.poll-bar {
background-color: #246FE5;
}
.poll-message-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
margin-top: 5px;
}
.poll-notice {
font-weight: 100;
margin-right: 10px;
}
.poll-show-details {
background-color: transparent;
border: none;
&:hover {
text-decoration: underline;
}
}
.poll-result-links {
display: flex;
flex-direction: row;
justify-content: space-between;
a.poll-detail-link, a.poll-change-vote-link {
color: #669AEC;
cursor: pointer;
font-weight: 600;
text-decoration: none;
&:hover {
color: #669AEC;
}
&:visited {
color: #669AEC;
}
}
}
.polls-pane-content {
height: 100%;
position: relative;
}
.pane-content{
display: flex;
flex-direction: column;
height: 100%;
justify-content: center;
align-items: center;
width: 100%;
}
.empty-pane-icon {
width: 50%;
padding: 24px;
}
.empty-pane-icon svg {
fill: #3D3D3D;
width: 100%;
height: auto;
}
.empty-pane-message {
color: #fff;
padding: 0 16px;
text-align: center;
}
.poll-results, .poll-answer {
background: #292929;
border-radius: 8px;
border: 1px solid #666666;
margin: 16px;
padding: 16px;
word-break: break-word;
}
.poll-results {
color: #fff;
}
.poll-answer {
h1, strong ,span {
color: #fff;
}
button > span {
color: inherit;
}
}
.poll-create-label {
color: #C2C2C2;
display: flex;
font-weight: 400;
margin-bottom: 4;
}
.expandable-input{
line-height: 18px;
resize: none;
width: 100%;
height: 40px;
box-sizing: border-box;
overflow: hidden;
border: 1px solid #666666;
background-color: #141414;
color: #FFF;
border-radius: 6px;
padding: 10px 16px;
}
#polls-panel {
height: calc(100% - 119px);
}
.poll-container {
font-size: 14px;
font-weight: 600;
height: calc(100% - 88px);
line-height: 20px;
overflow-y: auto;
position: relative;
& > * + *:not(.ignore-child) {
margin-top: 16px;
}
@media (max-width: 580px) {
height: calc(100% - 102px);
}
}
.poll-create-header {
color: #fff;
font-size: 20px;
line-height: 28px;
margin: 20px 16px;
font-weight: 600;
}
.poll-create-container {
padding: 8px 0;
}
.poll-create-footer {
background-color: #141414;
bottom: 0;
position: absolute;
width: calc(100% - 32px);
}
.poll-footer {
display: flex;
justify-content: space-between;
padding: 0 16px 16px 16px;
}
.poll-answer-footer {
padding: 8px 0 0 0;
}

View File

@@ -18,6 +18,7 @@
align-items: center;
font-size: 14px;
margin-left: 16px;
max-width: 70%;
}
&.space-top {
@@ -82,7 +83,7 @@
background-color: #FFFFFF;
border-radius: 4px;
height: 40px;
width: 56px;
width: 40px;
}
.jitsi-content-recording-icon-container-without-switch {

View File

@@ -41,11 +41,11 @@
&-dropdown-btns {
padding: 8px 0;
}
&-dropdown-container {
position: relative;
width: 100%;
/**
* Override default InlineDialog behaviour, since it does not play nicely with relative widths
*/
@@ -56,5 +56,12 @@
width: 100%;
}
}
}
}
.prejoin-input {
margin-bottom: 16px;
& input {
text-align: center;
}
}

View File

@@ -1,14 +1,4 @@
.premeeting-screen {
background: #292929;
bottom: 0;
display: flex;
font-size: 1.3em;
left: 0;
position: absolute;
right: 0;
top: 0;
z-index: $toolbarZ + 2;
.premeeting-screen {
.action-btn {
border-radius: 6px;
box-sizing: border-box;
@@ -75,129 +65,38 @@
}
}
.content {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
flex-shrink: 0;
height: 100%;
margin: 0 30px;
padding: 24px 0 16px;
#new-toolbox {
bottom: 0;
position: relative;
width: $prejoinDefaultContentWidth;
z-index: $toolbarZ + 2;
transition: none;
&-controls {
align-items: center;
.toolbox-content {
margin-bottom: 4px;
}
.toolbox-content-items {
@include ltr;
background: transparent;
box-shadow: none;
display: flex;
flex-direction: column;
margin: auto;
justify-content: space-between;
padding: 8px 0;
}
.toolbox-content,
.toolbox-content-wrapper,
.toolbox-content-items {
box-sizing: border-box;
width: 100%;
.title {
color: #fff;
font-size: 28px;
font-weight: 600;
letter-spacing: -0.015;
line-height: 36px;
margin-bottom: 16px;
text-align: center;
}
input.field {
background-color: white;
border: none;
outline: none;
border-radius: 6px;
font-size: 14px;
line-height: 20px;
margin-bottom: 16px;
color: #1C2025;
padding: 10px 16px;
text-align: center;
width: 100%;
&.error {
border: 1px solid #E04757;
}
&.focused {
box-shadow: 0px 0px 1px 1.5px black, 0px 0px 1.3px 4px white;
}
}
#new-toolbox {
bottom: 0;
position: relative;
transition: none;
.toolbox-content {
margin-bottom: 4px;
}
.toolbox-content-items {
@include ltr;
background: transparent;
box-shadow: none;
display: flex;
justify-content: space-evenly;
padding: 8px 0;
}
.toolbox-content,
.toolbox-content-wrapper,
.toolbox-content-items {
box-sizing: border-box;
width: 100%;
}
}
}
}
@media (max-width: 720px) {
flex-direction: column-reverse;
.content {
height: auto;
margin: 0 auto;
}
}
// mobile phone landscape
@media (max-height: 420px) {
div.content {
padding: 16px 16px 0 16px;
}
}
@media (max-width: 400px) {
.content {
padding: 16px;
width: 100%;
&-controls {
input.field {
font-size: 16px;
padding: 14px 16px;
}
}
.title {
display: none;
}
}
.device-status-error {
border-radius: 0;
margin: 0 -16px;
}
input.field {
font-size: 16px;
padding: 14px 16px;
}
.action-btn {
font-size: 16px;
margin-bottom: 8px;

View File

@@ -391,7 +391,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
this._frame.name = frameName;
this._frame.id = frameName;
this._setSize(height, width);
this._frame.sandbox = 'allow-scripts allow-same-origin allow-popups';
this._frame.sandbox = 'allow-scripts allow-same-origin allow-popups allow-forms';
this._frame.setAttribute('allowFullScreen', 'true');
this._frame.style.border = 0;

View File

@@ -14,11 +14,13 @@ import { i18next } from '../../../react/features/base/i18n';
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
import { VIDEO_TYPE } from '../../../react/features/base/media';
import {
getLocalParticipant,
getParticipantById,
getParticipantDisplayName,
isLocalScreenshareParticipant,
isScreenShareParticipant
} from '../../../react/features/base/participants';
import { getHideSelfView } from '../../../react/features/base/settings/functions.any';
import {
getVideoTrackByParticipant,
trackStreamingStatusChanged
@@ -232,6 +234,10 @@ export default class LargeVideoManager {
preUpdate.then(() => {
const { id, stream, videoType, resolve } = this.newStreamData;
const state = APP.store.getState();
const shouldHideSelfView = getHideSelfView(state);
const localId = getLocalParticipant(state)?.id;
// FIXME this does not really make sense, because the videoType
// (camera or desktop) is a completely different thing than
@@ -245,13 +251,16 @@ export default class LargeVideoManager {
// eslint-disable-next-line no-shadow
const container = this.getCurrentContainer();
if (shouldHideSelfView && localId === id) {
return container.hide();
}
container.setStream(id, stream, videoType);
// change the avatar url on large
this.updateAvatar();
const isVideoMuted = !stream || stream.isMuted();
const state = APP.store.getState();
const participant = getParticipantById(state, id);
const videoTrack = getVideoTrackByParticipant(state, participant);

60
package-lock.json generated
View File

@@ -23,7 +23,7 @@
"@atlaskit/tooltip": "17.1.2",
"@emotion/react": "11.10.0",
"@emotion/styled": "11.10.0",
"@giphy/js-fetch-api": "4.1.2",
"@giphy/js-fetch-api": "4.7.1",
"@giphy/react-components": "5.6.0",
"@giphy/react-native-sdk": "1.7.0",
"@hapi/bourne": "2.0.0",
@@ -74,7 +74,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/v1574.0.0+4d6873c2/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -3497,26 +3497,26 @@
}
},
"node_modules/@giphy/js-fetch-api": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@giphy/js-fetch-api/-/js-fetch-api-4.1.2.tgz",
"integrity": "sha512-wDfDQu8HiVkLb+YXcZf8QFbznmMHWbg86ZBydYmnp2mfuHyaHKsz9n9PnxdH3RorMS9YM/Ca/zqAM5y89Qj+Hw==",
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/@giphy/js-fetch-api/-/js-fetch-api-4.7.1.tgz",
"integrity": "sha512-LFpi8ZbV/N51nhIDc7ig6HnBUZ0Y/Av0GVCVDOs9zmUK1/zgTA33fkWMCNVxc4wgrugwRXCJA7Mj8Zb8BTea2w==",
"dependencies": {
"@giphy/js-types": "^4.1.0",
"@giphy/js-util": "^4.0.1",
"@giphy/js-types": "^4.4.0",
"@giphy/js-util": "^4.3.0",
"qs": "^6.9.4"
}
},
"node_modules/@giphy/js-types": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@giphy/js-types/-/js-types-4.1.0.tgz",
"integrity": "sha512-+qSN4Mx4TmrjLQm4SC0I/ZBkb5eWM94sljXwfjIlqn0GMSR3geqEqwmE9Uf/ldgzFh+XMMCasQjIdUl2nWc++Q=="
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@giphy/js-types/-/js-types-4.4.0.tgz",
"integrity": "sha512-W9G6crS2oqTn7g0RpvYu1l/sna4LnivRTk25jmxdzujOFb9kvQ+VFM/v9RPYV2GIBnzT/maW/EwjFIba9jkflA=="
},
"node_modules/@giphy/js-util": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@giphy/js-util/-/js-util-4.0.1.tgz",
"integrity": "sha512-46wXgt5Y+MxZjuzjE6JlvMLE+6Vlag+PYxbyTxpsunhmOKNoYK99d51E09iynmXTFuZWYgWJR9LcfnzqsWHy+Q==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@giphy/js-util/-/js-util-4.3.0.tgz",
"integrity": "sha512-ZxYw4DiNVchMxtCjVdLzXJ6+40To1C1B37PJ2a7zUP8Dxa62uGctbyb2PXAB1NmsCZOhy1d0QGEnjxK5LQocHQ==",
"dependencies": {
"@giphy/js-types": "^4.1.0",
"@giphy/js-types": "^4.4.0",
"dompurify": "^2.2.2",
"uuid": "^8.3.0"
}
@@ -13554,8 +13554,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1574.0.0+4d6873c2/lib-jitsi-meet.tgz",
"integrity": "sha512-rqsIoNxmOxE8qXzGu/f6gV7/Cbst7AXjawsG+LFxsVYvfu77wkx1gYyqJryrei0dyZ7uPFadWJy9bpo59gDkfg==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
"integrity": "sha512-AAEClrQNOVHNO1lKr/F1SOyiduZfI6bql3eiIxC3LZ5cBcyoRVmwI6uiAbLail0VkuKnTecEWcfYZ9lvokxrMw==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.0.0",
@@ -23036,26 +23036,26 @@
}
},
"@giphy/js-fetch-api": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@giphy/js-fetch-api/-/js-fetch-api-4.1.2.tgz",
"integrity": "sha512-wDfDQu8HiVkLb+YXcZf8QFbznmMHWbg86ZBydYmnp2mfuHyaHKsz9n9PnxdH3RorMS9YM/Ca/zqAM5y89Qj+Hw==",
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/@giphy/js-fetch-api/-/js-fetch-api-4.7.1.tgz",
"integrity": "sha512-LFpi8ZbV/N51nhIDc7ig6HnBUZ0Y/Av0GVCVDOs9zmUK1/zgTA33fkWMCNVxc4wgrugwRXCJA7Mj8Zb8BTea2w==",
"requires": {
"@giphy/js-types": "^4.1.0",
"@giphy/js-util": "^4.0.1",
"@giphy/js-types": "^4.4.0",
"@giphy/js-util": "^4.3.0",
"qs": "^6.9.4"
}
},
"@giphy/js-types": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@giphy/js-types/-/js-types-4.1.0.tgz",
"integrity": "sha512-+qSN4Mx4TmrjLQm4SC0I/ZBkb5eWM94sljXwfjIlqn0GMSR3geqEqwmE9Uf/ldgzFh+XMMCasQjIdUl2nWc++Q=="
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@giphy/js-types/-/js-types-4.4.0.tgz",
"integrity": "sha512-W9G6crS2oqTn7g0RpvYu1l/sna4LnivRTk25jmxdzujOFb9kvQ+VFM/v9RPYV2GIBnzT/maW/EwjFIba9jkflA=="
},
"@giphy/js-util": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@giphy/js-util/-/js-util-4.0.1.tgz",
"integrity": "sha512-46wXgt5Y+MxZjuzjE6JlvMLE+6Vlag+PYxbyTxpsunhmOKNoYK99d51E09iynmXTFuZWYgWJR9LcfnzqsWHy+Q==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@giphy/js-util/-/js-util-4.3.0.tgz",
"integrity": "sha512-ZxYw4DiNVchMxtCjVdLzXJ6+40To1C1B37PJ2a7zUP8Dxa62uGctbyb2PXAB1NmsCZOhy1d0QGEnjxK5LQocHQ==",
"requires": {
"@giphy/js-types": "^4.1.0",
"@giphy/js-types": "^4.4.0",
"dompurify": "^2.2.2",
"uuid": "^8.3.0"
}
@@ -30617,8 +30617,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1574.0.0+4d6873c2/lib-jitsi-meet.tgz",
"integrity": "sha512-rqsIoNxmOxE8qXzGu/f6gV7/Cbst7AXjawsG+LFxsVYvfu77wkx1gYyqJryrei0dyZ7uPFadWJy9bpo59gDkfg==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
"integrity": "sha512-AAEClrQNOVHNO1lKr/F1SOyiduZfI6bql3eiIxC3LZ5cBcyoRVmwI6uiAbLail0VkuKnTecEWcfYZ9lvokxrMw==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",

View File

@@ -28,7 +28,7 @@
"@atlaskit/tooltip": "17.1.2",
"@emotion/react": "11.10.0",
"@emotion/styled": "11.10.0",
"@giphy/js-fetch-api": "4.1.2",
"@giphy/js-fetch-api": "4.7.1",
"@giphy/react-components": "5.6.0",
"@giphy/react-native-sdk": "1.7.0",
"@hapi/bourne": "2.0.0",
@@ -79,7 +79,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/v1574.0.0+4d6873c2/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1578.0.0+5855ca72/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",

View File

@@ -15,9 +15,11 @@ import {
participantMutedUs,
participantPresenceChanged,
participantRoleChanged,
participantSourcesUpdated,
participantUpdated
} from '../participants/actions';
import { getNormalizedDisplayName } from '../participants/functions';
import { IJitsiParticipant } from '../participants/types';
import { toState } from '../redux/functions';
import {
destroyLocalTracks,
@@ -128,6 +130,10 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
JitsiConferenceEvents.PARTICIPANT_KICKED,
(kicker: any, kicked: any) => dispatch(participantKicked(kicker, kicked)));
conference.on(
JitsiConferenceEvents.PARTICIPANT_SOURCE_UPDATED,
(jitsiParticipant: IJitsiParticipant) => dispatch(participantSourcesUpdated(jitsiParticipant)));
conference.on(
JitsiConferenceEvents.LOCK_STATE_CHANGED, // @ts-ignore
(...args: any[]) => dispatch(lockStateChanged(conference, ...args)));

View File

@@ -105,7 +105,8 @@ export function commonUserJoinedHandling(
name: displayName,
presence: user.getStatus(),
role: user.getRole(),
isReplacing
isReplacing,
sources: user.getSources()
}));
}
}

View File

@@ -85,6 +85,7 @@ export interface IJitsiConference {
sendFaceLandmarks: (faceLandmarks: FaceLandmarks) => void;
sendFeedback: Function;
sendLobbyMessage: Function;
sendMessage: Function;
sessionId: string;
setDesktopSharingFrameRate: Function;
setDisplayName: Function;

View File

@@ -336,12 +336,14 @@ export interface IConfig {
};
firefox_fake_device?: string;
flags?: {
ssrcRewritingEnabled: boolean;
};
focusUserJid?: string;
gatherStats?: boolean;
giphy?: {
displayMode?: 'all' | 'tile' | 'chat';
enabled?: boolean;
proxyUrl?: string;
rating?: 'g' | 'pg' | 'pg-13' | 'r';
sdkKey?: string;
tileTime?: number;

View File

@@ -60,3 +60,13 @@ export const PREMEETING_BUTTONS = [ 'microphone', 'camera', 'select-background',
* The toolbar buttons to show on 3rdParty prejoin screen.
*/
export const THIRD_PARTY_PREJOIN_BUTTONS = [ 'microphone', 'camera', 'select-background' ];
/**
* The set of feature flags.
*
* @enum {string}
*/
export const FEATURE_FLAGS = {
SSRC_REWRITING: 'ssrcRewritingEnabled'
};

View File

@@ -11,7 +11,7 @@ import { parseURLParams } from '../util/parseURLParams';
import { IConfig } from './configType';
import CONFIG_WHITELIST from './configWhitelist';
import { _CONFIG_STORE_PREFIX } from './constants';
import { FEATURE_FLAGS, _CONFIG_STORE_PREFIX } from './constants';
import INTERFACE_CONFIG_WHITELIST from './interfaceConfigWhitelist';
import logger from './logger';
@@ -63,6 +63,16 @@ export function getMultipleVideoSendingSupportFeatureFlag(state: IReduxState) {
return isUnifiedPlanEnabled(state);
}
/**
* Selector used to get the SSRC-rewriting feature flag.
*
* @param {Object} state - The global state.
* @returns {boolean}
*/
export function getSsrcRewritingFeatureFlag(state: IReduxState) {
return getFeatureFlag(state, FEATURE_FLAGS.SSRC_REWRITING);
}
/**
* Selector used to get a feature flag.
*

View File

@@ -1,5 +1,3 @@
import './native';
// Re-export JitsiMeetJS from the library lib-jitsi-meet to (the other features
// of) the project jitsi-meet.
//

View File

@@ -117,6 +117,18 @@ export const PARTICIPANT_KICKED = 'PARTICIPANT_KICKED';
*/
export const PARTICIPANT_LEFT = 'PARTICIPANT_LEFT';
/**
* Action to handle case when the sources attached to a participant are updated.
*
* {
* type: PARTICIPANT_SOURCES_UPDATED,
* participant: {
* id: string
* }
* }
*/
export const PARTICIPANT_SOURCES_UPDATED = 'PARTICIPANT_SOURCES_UPDATED';
/**
* Action to handle case when info about participant changes.
*

View File

@@ -18,6 +18,7 @@ import {
PARTICIPANT_JOINED,
PARTICIPANT_KICKED,
PARTICIPANT_LEFT,
PARTICIPANT_SOURCES_UPDATED,
PARTICIPANT_UPDATED,
PIN_PARTICIPANT,
RAISE_HAND_UPDATED,
@@ -36,7 +37,7 @@ import {
getVirtualScreenshareParticipantOwnerId
} from './functions';
import logger from './logger';
import { FakeParticipant, IParticipant } from './types';
import { FakeParticipant, IJitsiParticipant, IParticipant } from './types';
/**
* Create an action for when dominant speaker changes.
@@ -253,6 +254,39 @@ export function participantJoined(participant: IParticipant) {
};
}
/**
* Updates the sources of a remote participant.
*
* @param {IJitsiParticipant} jitsiParticipant - The IJitsiParticipant instance.
* @returns {{
* type: PARTICIPANT_SOURCES_UPDATED,
* participant: IParticipant
* }}
*/
export function participantSourcesUpdated(jitsiParticipant: IJitsiParticipant) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const id = jitsiParticipant.getId();
const participant = getParticipantById(getState(), id);
if (participant?.local) {
return;
}
const sources = jitsiParticipant.getSources();
if (!sources?.size) {
return;
}
return dispatch({
type: PARTICIPANT_SOURCES_UPDATED,
participant: {
id,
sources
}
});
};
}
/**
* Updates the features of a remote participant.
*

View File

@@ -6,8 +6,9 @@ import { isStageFilmstripAvailable } from '../../filmstrip/functions';
import { IStateful } from '../app/types';
import { GRAVATAR_BASE_URL } from '../avatar/constants';
import { isCORSAvatarURL } from '../avatar/functions';
import { getCurrentConference } from '../conference/functions';
import i18next from '../i18n/i18next';
import { VIDEO_TYPE } from '../media/constants';
import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
import { toState } from '../redux/functions';
import { getScreenShareTrack } from '../tracks/functions';
import { createDeferred } from '../util/helpers';
@@ -21,7 +22,7 @@ import {
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { preloadImage } from './preloadImage';
import { FakeParticipant, IParticipant } from './types';
import { FakeParticipant, IJitsiParticipant, IParticipant, ISourceInfo } from './types';
/**
* Temp structures for avatar urls to be checked/preloaded.
@@ -407,6 +408,35 @@ export function getParticipantDisplayName(stateful: IStateful, id: string): stri
return defaultRemoteDisplayName ?? '';
}
/**
* Returns the source names of the screenshare sources in the conference based on the presence shared by the remote
* endpoints. This should be only used for creating/removing virtual screenshare participant tiles when ssrc-rewriting
* is enabled. Once the tile is created, the source-name gets added to the receiver constraints based on which the
* JVB will add the source to the video sources map and signal it to the local endpoint. Only then, a remote track is
* created/remapped and the tracks in redux will be updated. Once the track is updated in redux, the client will
* will continue to use the other track based getter functions for other operations related to screenshare.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
* retrieve the state.
* @returns {string[]}
*/
export function getRemoteScreensharesBasedOnPresence(stateful: IStateful): string[] {
const conference = getCurrentConference(stateful);
return conference?.getParticipants()?.reduce((screenshares: string[], participant: IJitsiParticipant) => {
const sources: Map<string, Map<string, ISourceInfo>> = participant.getSources();
const videoSources = sources.get(MEDIA_TYPE.VIDEO);
const screenshareSources = Array.from(videoSources ?? new Map())
.filter(source => source[1].videoType === VIDEO_TYPE.DESKTOP && !source[1].muted)
.map(source => source[0]);
// eslint-disable-next-line no-param-reassign
screenshares = [ ...screenshares, ...screenshareSources ];
return screenshares;
}, []);
}
/**
* Returns screenshare participant's display name.
*
@@ -434,6 +464,36 @@ export function getScreenshareParticipantIds(stateful: IStateful): Array<string>
.map(t => t.participantId);
}
/**
* Returns a list source name associated with a given remote participant and for the given media type.
*
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
* retrieve the state.
* @param {string} id - The id of the participant whose source names are to be retrieved.
* @param {string} mediaType - The type of source, audio or video.
* @returns {Array<string>|undefined}
*/
export function getSourceNamesByMediaType(
stateful: IStateful,
id: string,
mediaType: string): Array<string> | undefined {
const participant: IParticipant | undefined = getParticipantById(stateful, id);
if (!participant) {
return;
}
const sources = participant.sources;
if (!sources) {
return;
}
return Array.from(sources.get(mediaType) ?? new Map())
.filter(source => source[1].videoType !== VIDEO_TYPE.DESKTOP || !source[1].muted)
.map(s => s[0]);
}
/**
* Returns the presence status of a participant associated with the passed id.
*

View File

@@ -1,3 +1,4 @@
import { MEDIA_TYPE } from '../media/constants';
import ReducerRegistry from '../redux/ReducerRegistry';
import { set } from '../redux/functions';
@@ -7,6 +8,7 @@ import {
PARTICIPANT_ID_CHANGED,
PARTICIPANT_JOINED,
PARTICIPANT_LEFT,
PARTICIPANT_SOURCES_UPDATED,
PARTICIPANT_UPDATED,
PIN_PARTICIPANT,
RAISE_HAND_UPDATED,
@@ -20,7 +22,7 @@ import {
isRemoteScreenshareParticipant,
isScreenShareParticipant
} from './functions';
import { ILocalParticipant, IParticipant } from './types';
import { FakeParticipant, ILocalParticipant, IParticipant, ISourceInfo } from './types';
/**
* Participant object.
@@ -69,6 +71,7 @@ const DEFAULT_STATE = {
pinnedParticipant: undefined,
raisedHandsQueue: [],
remote: new Map(),
remoteVideoSources: new Set<string>(),
sortedRemoteVirtualScreenshareParticipants: new Map(),
sortedRemoteParticipants: new Map(),
speakersList: new Map()
@@ -84,6 +87,7 @@ export interface IParticipantsState {
pinnedParticipant?: string;
raisedHandsQueue: Array<{ id: string; raisedHandTimestamp: number; }>;
remote: Map<string, IParticipant>;
remoteVideoSources: Set<string>;
sortedRemoteParticipants: Map<string, string>;
sortedRemoteVirtualScreenshareParticipants: Map<string, string>;
speakersList: Map<string, string>;
@@ -243,7 +247,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
fakeParticipant,
id,
name,
pinned
pinned,
sources
} = participant;
const { pinnedParticipant, dominantSpeaker } = state;
@@ -287,6 +292,19 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
state.remote.set(id, participant);
if (sources?.size) {
const videoSources: Map<string, ISourceInfo> | undefined = sources.get(MEDIA_TYPE.VIDEO);
if (videoSources?.size) {
const newRemoteVideoSources = new Set(state.remoteVideoSources);
for (const source of videoSources.keys()) {
newRemoteVideoSources.add(source);
}
state.remoteVideoSources = newRemoteVideoSources;
}
}
// Insert the new participant.
const displayName = _getDisplayName(state, name);
const sortedRemoteParticipants = Array.from(state.sortedRemoteParticipants);
@@ -332,6 +350,23 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
} = state;
let oldParticipant = remote.get(id);
if (oldParticipant?.sources?.size) {
const videoSources: Map<string, ISourceInfo> | undefined = oldParticipant.sources.get(MEDIA_TYPE.VIDEO);
const newRemoteVideoSources = new Set(state.remoteVideoSources);
if (videoSources?.size) {
for (const source of videoSources.keys()) {
newRemoteVideoSources.delete(source);
}
}
state.remoteVideoSources = newRemoteVideoSources;
} else if (oldParticipant?.fakeParticipant === FakeParticipant.RemoteScreenShare) {
const newRemoteVideoSources = new Set(state.remoteVideoSources);
newRemoteVideoSources.delete(id);
state.remoteVideoSources = newRemoteVideoSources;
}
if (oldParticipant && oldParticipant.conference === conference) {
remote.delete(id);
} else if (local?.id === id) {
@@ -374,6 +409,26 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
return { ...state };
}
case PARTICIPANT_SOURCES_UPDATED: {
const { id, sources } = action.participant;
const participant = state.remote.get(id);
if (participant) {
participant.sources = sources;
const videoSources: Map<string, ISourceInfo> = sources.get(MEDIA_TYPE.VIDEO);
if (videoSources?.size) {
const newRemoteVideoSources = new Set(state.remoteVideoSources);
for (const source of videoSources.keys()) {
newRemoteVideoSources.add(source);
}
state.remoteVideoSources = newRemoteVideoSources;
}
}
return { ...state };
}
case RAISE_HAND_UPDATED: {
return {
...state,
@@ -493,7 +548,8 @@ function _participantJoined({ participant }: { participant: IParticipant; }) {
name,
pinned,
presence,
role
role,
sources
} = participant;
let { conference, id } = participant;
@@ -523,7 +579,8 @@ function _participantJoined({ participant }: { participant: IParticipant; }) {
name,
pinned: pinned || false,
presence,
role: role || PARTICIPANT_ROLE.NONE
role: role || PARTICIPANT_ROLE.NONE,
sources
};
}

View File

@@ -3,11 +3,14 @@ import _ from 'lodash';
import { IStore } from '../../app/types';
import { getCurrentConference } from '../conference/functions';
import {
getMultipleVideoSendingSupportFeatureFlag
getMultipleVideoSendingSupportFeatureFlag,
getSsrcRewritingFeatureFlag
} from '../config/functions.any';
import { VIDEO_TYPE } from '../media/constants';
import StateListenerRegistry from '../redux/StateListenerRegistry';
import { createVirtualScreenshareParticipant, participantLeft } from './actions';
import { getRemoteScreensharesBasedOnPresence } from './functions';
import { FakeParticipant } from './types';
StateListenerRegistry.register(
@@ -15,13 +18,50 @@ StateListenerRegistry.register(
/* listener */(tracks, store) => _updateScreenshareParticipants(store)
);
StateListenerRegistry.register(
/* selector */ state => state['features/base/participants'].remoteVideoSources,
/* listener */(remoteVideoSources, store) => getSsrcRewritingFeatureFlag(store.getState())
&& _updateScreenshareParticipantsBasedOnPresence(store)
);
/**
* Compares the old and new screenshare lists provided and creates/removes the virtual screenshare participant
* tiles accodingly.
*
* @param {Array<string>} oldScreenshareSourceNames - List of old screenshare source names.
* @param {Array<string>} newScreenshareSourceNames - Current list of screenshare source names.
* @param {Object} store - The redux store.
* @returns {void}
*/
function _createOrRemoveVirtualParticipants(
oldScreenshareSourceNames: string[],
newScreenshareSourceNames: string[],
store: IStore): void {
const { dispatch, getState } = store;
const conference = getCurrentConference(getState());
const removedScreenshareSourceNames = _.difference(oldScreenshareSourceNames, newScreenshareSourceNames);
const addedScreenshareSourceNames = _.difference(newScreenshareSourceNames, oldScreenshareSourceNames);
if (removedScreenshareSourceNames.length) {
removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, {
fakeParticipant: FakeParticipant.RemoteScreenShare
})));
}
if (addedScreenshareSourceNames.length) {
addedScreenshareSourceNames.forEach(id => dispatch(
createVirtualScreenshareParticipant(id, false, conference)));
}
}
/**
* Handles creating and removing virtual screenshare participants.
*
* @param {*} store - The redux store.
* @returns {void}
*/
function _updateScreenshareParticipants({ getState, dispatch }: IStore) {
function _updateScreenshareParticipants(store: IStore): void {
const { dispatch, getState } = store;
const state = getState();
const conference = getCurrentConference(state);
const tracks = state['features/base/tracks'];
@@ -31,7 +71,7 @@ function _updateScreenshareParticipants({ getState, dispatch }: IStore) {
let newLocalSceenshareSourceName;
const currentScreenshareSourceNames = tracks.reduce((acc: string[], track) => {
if (track.videoType === 'desktop' && !track.jitsiTrack.isMuted()) {
if (track.videoType === VIDEO_TYPE.DESKTOP && !track.jitsiTrack.isMuted()) {
const sourceName: string = track.jitsiTrack.getSourceName();
if (track.local) {
@@ -51,24 +91,30 @@ function _updateScreenshareParticipants({ getState, dispatch }: IStore) {
if (localScreenShare && !newLocalSceenshareSourceName) {
dispatch(participantLeft(localScreenShare.id, conference, {
fakeParticipant: FakeParticipant.LocalScreenShare,
isReplaced: undefined
fakeParticipant: FakeParticipant.LocalScreenShare
}));
}
}
const removedScreenshareSourceNames = _.difference(previousScreenshareSourceNames, currentScreenshareSourceNames);
const addedScreenshareSourceNames = _.difference(currentScreenshareSourceNames, previousScreenshareSourceNames);
if (removedScreenshareSourceNames.length) {
removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, {
fakeParticipant: FakeParticipant.RemoteScreenShare,
isReplaced: undefined
})));
if (getSsrcRewritingFeatureFlag(state)) {
return;
}
if (addedScreenshareSourceNames.length) {
addedScreenshareSourceNames.forEach(id => dispatch(
createVirtualScreenshareParticipant(id, false, conference)));
}
_createOrRemoveVirtualParticipants(previousScreenshareSourceNames, currentScreenshareSourceNames, store);
}
/**
* Handles the creation and removal of remote virtual screenshare participants when ssrc-rewriting is enabled.
*
* @param {Object} store - The redux store.
* @returns {void}
*/
function _updateScreenshareParticipantsBasedOnPresence(store: IStore): void {
const { getState } = store;
const state = getState();
const { sortedRemoteVirtualScreenshareParticipants } = state['features/base/participants'];
const previousScreenshareSourceNames = [ ...sortedRemoteVirtualScreenshareParticipants.keys() ];
const currentScreenshareSourceNames = getRemoteScreensharesBasedOnPresence(state);
_createOrRemoveVirtualParticipants(previousScreenshareSourceNames, currentScreenshareSourceNames, store);
}

View File

@@ -37,6 +37,7 @@ export interface IParticipant {
region?: string;
remoteControlSessionStatus?: boolean;
role?: string;
sources?: Map<string, Map<string, ISourceInfo>>;
supportsRemoteControl?: boolean;
}
@@ -51,10 +52,16 @@ export interface ILocalParticipant extends IParticipant {
userSelectedMicDeviceLabel?: string;
}
export interface ISourceInfo {
muted: boolean;
videoType: string;
}
export interface IJitsiParticipant {
getDisplayName: () => string;
getId: () => string;
getJid: () => string;
getRole: () => string;
getSources: () => Map<string, Map<string, ISourceInfo>>;
isHidden: () => boolean;
}

View File

@@ -7,6 +7,7 @@ import { translate } from '../../../i18n/functions';
import Icon from '../../../icons/components/Icon';
import { IconArrowDown, IconWifi1Bar, IconWifi2Bars, IconWifi3Bars } from '../../../icons/svg';
import { connect } from '../../../redux/functions';
import { withPixelLineHeight } from '../../../styles/functions.web';
import { PREJOIN_DEFAULT_CONTENT_WIDTH } from '../../../ui/components/variables';
import { CONNECTION_TYPE } from '../../constants';
import { getConnectionData } from '../../functions';
@@ -27,11 +28,8 @@ interface IProps extends WithTranslation {
const useStyles = makeStyles()(theme => {
return {
connectionStatus: {
borderRadius: '6px',
color: '#fff',
fontSize: '12px',
letterSpacing: '0.16px',
lineHeight: '16px',
...withPixelLineHeight(theme.typography.bodyShortRegular),
position: 'absolute',
width: '100%',
@@ -56,14 +54,15 @@ const useStyles = makeStyles()(theme => {
backgroundColor: 'rgba(0, 0, 0, 0.7)',
alignItems: 'center',
display: 'flex',
padding: '14px 16px'
padding: '12px 16px',
borderRadius: theme.shape.borderRadius
},
'& .con-status-circle': {
borderRadius: '50%',
display: 'inline-block',
padding: theme.spacing(1),
marginRight: theme.spacing(3)
marginRight: theme.spacing(2)
},
'& .con-status--good': {

View File

@@ -1,5 +1,7 @@
/* eslint-disable lines-around-comment */
import clsx from 'clsx';
import React, { ReactNode } from 'react';
import { connect } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../../app/types';
@@ -9,12 +11,12 @@ import { Toolbox } from '../../../../toolbox/components/web';
import { getConferenceName } from '../../../conference/functions';
import { PREMEETING_BUTTONS, THIRD_PARTY_PREJOIN_BUTTONS } from '../../../config/constants';
import { getToolbarButtons, isToolbarButtonEnabled } from '../../../config/functions.web';
import { connect } from '../../../redux/functions';
import { withPixelLineHeight } from '../../../styles/functions.web';
import ConnectionStatus from './ConnectionStatus';
// @ts-ignore
import Preview from './Preview';
/* eslint-enable lines-around-comment */
interface IProps {
@@ -51,7 +53,7 @@ interface IProps {
/**
* Indicates whether the copy url button should be shown.
*/
showCopyUrlButton: boolean;
showCopyUrlButton?: boolean;
/**
* Indicates whether the device status should be shown.
@@ -86,7 +88,64 @@ interface IProps {
const useStyles = makeStyles()(theme => {
return {
subtitle: {
container: {
height: '100%',
position: 'absolute',
inset: '0 0 0 0',
display: 'flex',
backgroundColor: theme.palette.ui01,
zIndex: 252,
'@media (max-width: 720px)': {
flexDirection: 'column-reverse'
}
},
content: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
flexShrink: 0,
boxSizing: 'border-box',
margin: '0 48px',
padding: '24px 0 16px',
position: 'relative',
width: '300px',
height: '100%',
zIndex: 252,
'@media (max-width: 720px)': {
height: 'auto',
margin: '0 auto'
},
// mobile phone landscape
'@media (max-width: 420px)': {
padding: '16px 16px 0 16px',
width: '100%'
},
'@media (max-width: 400px)': {
padding: '16px'
}
},
contentControls: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
margin: 'auto',
width: '100%'
},
title: {
...withPixelLineHeight(theme.typography.heading4),
color: `${theme.palette.text01}!important`,
marginBottom: theme.spacing(3),
textAlign: 'center',
'@media (max-width: 400px)': {
display: 'none'
}
},
roomName: {
...withPixelLineHeight(theme.typography.heading5),
color: theme.palette.text01,
marginBottom: theme.spacing(4),
@@ -112,7 +171,6 @@ const PreMeetingScreen = ({
videoTrack
}: IProps) => {
const { classes } = useStyles();
const containerClassName = `premeeting-screen ${className ? className : ''}`;
const style = _premeetingBackground ? {
background: _premeetingBackground,
backgroundPosition: 'center',
@@ -120,17 +178,17 @@ const PreMeetingScreen = ({
} : {};
return (
<div className = { containerClassName }>
<div className = { clsx('premeeting-screen', classes.container, className) }>
<div style = { style }>
<div className = 'content'>
<div className = { classes.content }>
<ConnectionStatus />
<div className = 'content-controls'>
<h1 className = 'title'>
<div className = { classes.contentControls }>
<h1 className = { classes.title }>
{title}
</h1>
{_roomName && (
<span className = { classes.subtitle }>
<span className = { classes.roomName }>
{_roomName}
</span>
)}
@@ -175,7 +233,7 @@ function mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
? premeetingButtons
: premeetingButtons.filter(b => isToolbarButtonEnabled(b, toolbarButtons)),
_premeetingBackground: premeetingBackground,
_roomName: hideConferenceSubject ? undefined : getConferenceName(state)
_roomName: (hideConferenceSubject ? undefined : getConferenceName(state)) ?? ''
};
}

View File

@@ -52,3 +52,15 @@ export const SET_REDUCED_UI = 'SET_REDUCED_UI';
*/
export const SET_CONTEXT_MENU_OPEN = 'SET_CONTEXT_MENU_OPEN';
/**
* The type of redux action which signals whether we are in narrow layout.
*
* {
* type: SET_NARROW_LAYOUT,
* isNarrow: boolean
* }
*
* @public
*/
export const SET_NARROW_LAYOUT = 'SET_NARROW_LAYOUT';

View File

@@ -10,6 +10,7 @@ import {
SAFE_AREA_INSETS_CHANGED,
SET_ASPECT_RATIO,
SET_CONTEXT_MENU_OPEN,
SET_NARROW_LAYOUT,
SET_REDUCED_UI
} from './actionTypes';
import { ASPECT_RATIO_NARROW, ASPECT_RATIO_WIDE } from './constants';
@@ -143,3 +144,19 @@ export function setSafeAreaInsets(insets: Object) {
insets
};
}
/**
* Sets narrow layout.
*
* @param {boolean} isNarrow - Whether is narrow layout.
* @returns {{
* type: SET_NARROW_LAYOUT,
* isNarrow: boolean
* }}
*/
export function setNarrowLayout(isNarrow: boolean) {
return {
type: SET_NARROW_LAYOUT,
isNarrow
};
}

View File

@@ -6,6 +6,7 @@ import {
SAFE_AREA_INSETS_CHANGED,
SET_ASPECT_RATIO,
SET_CONTEXT_MENU_OPEN,
SET_NARROW_LAYOUT,
SET_REDUCED_UI
} from './actionTypes';
import { ASPECT_RATIO_NARROW } from './constants';
@@ -22,6 +23,7 @@ const DEFAULT_STATE = {
aspectRatio: ASPECT_RATIO_NARROW,
clientHeight: innerHeight,
clientWidth: innerWidth,
isNarrowLayout: false,
reducedUI: false,
contextMenuOpened: false
};
@@ -31,6 +33,7 @@ export interface IResponsiveUIState {
clientHeight: number;
clientWidth: number;
contextMenuOpened: boolean;
isNarrowLayout: boolean;
reducedUI: boolean;
safeAreaInsets?: {
bottom: number;
@@ -65,6 +68,9 @@ ReducerRegistry.register<IResponsiveUIState>('features/base/responsive-ui',
case SET_CONTEXT_MENU_OPEN:
return set(state, 'contextMenuOpened', action.isOpen);
case SET_NARROW_LAYOUT:
return set(state, 'isNarrowLayout', action.isNarrow);
}
return state;

View File

@@ -3,7 +3,6 @@ import { IStateful } from '../app/types';
import CONFIG_WHITELIST from '../config/configWhitelist';
import { IConfigState } from '../config/reducer';
import { IJwtState } from '../jwt/reducer';
import { getParticipantCount } from '../participants/functions';
import { toState } from '../redux/functions';
import { parseURLParams } from '../util/parseURLParams';
@@ -114,16 +113,6 @@ export function shouldHideShareAudioHelper(state: IReduxState): boolean | undefi
return state['features/base/settings'].hideShareAudioHelper;
}
/**
* Whether we should hide self view.
*
* @param {Object} state - Redux state.
* @returns {boolean}
*/
export function shouldHideSelfView(state: IReduxState) {
return getParticipantCount(state) === 1 ? false : getHideSelfView(state);
}
/**
* Gets the disable self view setting.
*

View File

@@ -66,6 +66,16 @@ export const TRACK_MUTE_UNMUTE_FAILED = 'TRACK_MUTE_UNMUTE_FAILED';
*/
export const TRACK_NO_DATA_FROM_SOURCE = 'TRACK_NO_DATA_FROM_SOURCE';
/**
* The type of redux action dispatched when the owner of a track changes due to ssrc remapping.
*
* {
* type: TRACK_OWNER_CHANGED,
* track: Track
* }
*/
export const TRACK_OWNER_CHANGED = 'TRACK_OWNER_CHANGED';
/**
* The type of redux action dispatched when a track has been (locally or
* remotely) removed from the conference.
@@ -96,7 +106,7 @@ export const TRACK_STOPPED = 'TRACK_STOPPED';
* }
*/
export const TRACK_UPDATED = 'TRACK_UPDATED';
/**
* The type of redux action dispatched when a local track starts being created
* via a WebRTC {@code getUserMedia} call. The action's payload includes an

View File

@@ -27,6 +27,7 @@ import {
TRACK_CREATE_ERROR,
TRACK_MUTE_UNMUTE_FAILED,
TRACK_NO_DATA_FROM_SOURCE,
TRACK_OWNER_CHANGED,
TRACK_REMOVED,
TRACK_STOPPED,
TRACK_UPDATED,
@@ -377,7 +378,9 @@ export function trackAdded(track: any) {
track.on(
JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED,
(type: VideoType) => dispatch(trackVideoTypeChanged(track, type)));
track.on(
JitsiTrackEvents.TRACK_OWNER_CHANGED,
(owner: string) => dispatch(trackOwnerChanged(track, owner)));
const local = track.isLocal();
const isVirtualScreenshareParticipantCreated = !local || getMultipleVideoSendingSupportFeatureFlag(getState());
const mediaType = track.getVideoType() === VIDEO_TYPE.DESKTOP && isVirtualScreenshareParticipantCreated
@@ -582,11 +585,14 @@ export function trackVideoStarted(track: any): {
* }}
*/
export function trackVideoTypeChanged(track: any, videoType: VideoType) {
const mediaType = videoType === VIDEO_TYPE.CAMERA ? MEDIA_TYPE.VIDEO : MEDIA_TYPE.SCREENSHARE;
return {
type: TRACK_UPDATED,
track: {
jitsiTrack: track,
videoType
videoType,
mediaType
}
};
}
@@ -617,6 +623,32 @@ export function trackStreamingStatusChanged(track: any, streamingStatus: string)
};
}
/**
* Create an action for when the owner of the track changes due to ssrc remapping.
*
* @param {(JitsiRemoteTrack)} track - JitsiTrack instance.
* @param {string} participantId - New owner's participant ID.
* @returns {{
* type: TRACK_OWNER_CHANGED,
* track: Track
* }}
*/
export function trackOwnerChanged(track: any, participantId: string): {
track: {
jitsiTrack: any;
participantId: string;
};
type: 'TRACK_OWNER_CHANGED';
} {
return {
type: TRACK_OWNER_CHANGED,
track: {
jitsiTrack: track,
participantId
}
};
}
/**
* Signals passed tracks to be added.
*

View File

@@ -8,6 +8,7 @@ import {
TRACK_CREATE_CANCELED,
TRACK_CREATE_ERROR,
TRACK_NO_DATA_FROM_SOURCE,
TRACK_OWNER_CHANGED,
TRACK_REMOVED,
TRACK_UPDATED,
TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT,
@@ -41,6 +42,18 @@ function track(state: ITrack, action: any) {
}
break;
case TRACK_OWNER_CHANGED: {
const t = action.track;
if (state.jitsiTrack === t.jitsiTrack) {
return {
...state,
participantId: t.participantId
};
}
break;
}
case TRACK_UPDATED: {
const t = action.track;
@@ -103,10 +116,10 @@ ReducerRegistry.register<ITracksState>('features/base/tracks', (state = [], acti
switch (action.type) {
case PARTICIPANT_ID_CHANGED:
case TRACK_NO_DATA_FROM_SOURCE:
case TRACK_OWNER_CHANGED:
case TRACK_UPDATE_LAST_VIDEO_MEDIA_EVENT:
case TRACK_UPDATED:
return state.map((t: ITrack) => track(t, action));
case TRACK_ADDED: {
let withoutTrackStub = state;

View File

@@ -48,7 +48,6 @@ export const colors = {
// after we replace them in the components.
primary10: '#17A0DB',
primary11: '#1081B2',
primary12: '#B8C7E0',
surface00: '#111111',
surface12: '#AAAAAA',
surface13: '#495258',
@@ -199,7 +198,6 @@ export const colorMap = {
border01: 'surface08',
border02: 'surface06',
border03: 'surface04',
border04: 'primary12',
border05: 'surface07',
borderError: 'error06',
warning03: 'warning07',

View File

@@ -10,6 +10,7 @@ import { IInputProps } from '../types';
interface IProps extends IInputProps {
accessibilityLabel?: string;
autoComplete?: string;
autoFocus?: boolean;
bottomLabel?: string;
className?: string;
@@ -21,6 +22,7 @@ interface IProps extends IInputProps {
name?: string;
onKeyPress?: (e: React.KeyboardEvent) => void;
readOnly?: boolean;
required?: boolean;
textarea?: boolean;
type?: 'text' | 'email' | 'number' | 'password';
}
@@ -130,6 +132,7 @@ const useStyles = makeStyles()(theme => {
const Input = React.forwardRef<any, IProps>(({
accessibilityLabel,
autoComplete,
autoFocus,
bottomLabel,
className,
@@ -148,6 +151,7 @@ const Input = React.forwardRef<any, IProps>(({
onKeyPress,
placeholder,
readOnly = false,
required,
textarea = false,
type = 'text',
value
@@ -173,11 +177,13 @@ const Input = React.forwardRef<any, IProps>(({
{textarea ? (
<TextareaAutosize
aria-label = { accessibilityLabel }
autoComplete = { autoComplete }
autoFocus = { autoFocus }
className = { cx(styles.input, isMobile && 'is-mobile',
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
disabled = { disabled }
{ ...(id ? { id } : {}) }
maxLength = { maxLength }
maxRows = { maxRows }
minRows = { minRows }
name = { name }
@@ -186,10 +192,12 @@ const Input = React.forwardRef<any, IProps>(({
placeholder = { placeholder }
readOnly = { readOnly }
ref = { ref }
required = { required }
value = { value } />
) : (
<input
aria-label = { accessibilityLabel }
autoComplete = { autoComplete }
autoFocus = { autoFocus }
className = { cx(styles.input, isMobile && 'is-mobile',
error && 'error', clearable && styles.clearableInput, icon && 'icon-input') }
@@ -202,6 +210,7 @@ const Input = React.forwardRef<any, IProps>(({
placeholder = { placeholder }
readOnly = { readOnly }
ref = { ref }
required = { required }
type = { type }
value = { value } />
)}

View File

@@ -265,15 +265,7 @@ export const commonStyles = (theme: Theme) => {
padding: 6,
textAlign: 'center' as const,
pointerEvents: 'all' as const,
boxShadow: '0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15)',
'& > div': {
marginLeft: 8,
'&:first-child': {
marginLeft: 0
}
}
boxShadow: '0px 2px 8px 4px rgba(0, 0, 0, 0.25), 0px 0px 0px 1px rgba(0, 0, 0, 0.15)'
}
};
};

View File

@@ -93,7 +93,8 @@ const styles = (theme: Theme) => {
userMessage: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01,
whiteSpace: 'pre-wrap'
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
},
privateMessageNotice: {

View File

@@ -26,7 +26,8 @@ const useStyles = makeStyles()(theme => {
return {
messageGroup: {
display: 'flex',
flexDirection: 'column'
flexDirection: 'column',
maxWidth: '100%'
},
groupContainer: {

View File

@@ -81,6 +81,11 @@ type Props = AbstractProps & {
*/
_enableSaveLogs: boolean,
/**
* Whether is narrow layout or not.
*/
_isNarrowLayout: boolean,
/**
* Whether or not the displays stats are for local video.
*/
@@ -193,6 +198,7 @@ class ConnectionIndicatorContent extends AbstractConnectionIndicator<Props, Stat
enableSaveLogs = { this.props._enableSaveLogs }
framerate = { framerate }
isLocalVideo = { this.props._isLocalVideo }
isNarrowLayout = { this.props._isNarrowLayout }
isVirtualScreenshareParticipant = { this.props._isVirtualScreenshareParticipant }
maxEnabledResolution = { maxEnabledResolution }
onSaveLogs = { this.props._onSaveLogs }
@@ -312,6 +318,7 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
const conference = state['features/base/conference'].conference;
const participant
= participantId ? getParticipantById(state, participantId) : getLocalParticipant(state);
const { isNarrowLayout } = state['features/base/responsive-ui'];
const tracks = state['features/base/tracks'];
const audioTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.AUDIO, participantId);
let videoTrack = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
@@ -330,6 +337,7 @@ export function _mapStateToProps(state: Object, ownProps: Props) {
_isConnectionStatusInactive,
_isConnectionStatusInterrupted,
_isE2EEVerified: participant?.e2eeVerified,
_isNarrowLayout: isNarrowLayout,
_isVirtualScreenshareParticipant: isScreenShareParticipant(participant),
_isLocalVideo: participant?.local,
_region: participant?.region,

View File

@@ -98,6 +98,11 @@ interface IProps extends WithTranslation {
*/
isLocalVideo: boolean;
/**
* Whether we are in narrow layout mode or not.
*/
isNarrowLayout: boolean;
/**
* Whether or not the statistics are for screen share.
*/
@@ -261,9 +266,10 @@ class ConnectionStatsTable extends Component<IProps> {
disableShowMoreStats,
enableSaveLogs,
isVirtualScreenshareParticipant,
isLocalVideo
isLocalVideo,
isNarrowLayout
} = this.props;
const className = clsx(classes.connectionStatsTable, { [classes.mobile]: isMobileBrowser() });
const className = clsx(classes.connectionStatsTable, { [classes.mobile]: isMobileBrowser() || isNarrowLayout });
if (isVirtualScreenshareParticipant) {
return this._renderScreenShareStatus();

View File

@@ -88,7 +88,7 @@ export async function sendFaceExpressionsWebhook(state: IReduxState) {
const reqBody = {
meetingFqn: extractFqnFromPath(),
sessionId: conference?.sessionId,
sessionId: conference?.getMeetingUniqueId(),
submitted: Date.now(),
emotions: faceLandmarksBuffer,
participantId: localParticipant?.jwtId,

View File

@@ -5,7 +5,7 @@ import {
getParticipantById,
getRemoteParticipantCountWithFake
} from '../base/participants/functions';
import { shouldHideSelfView } from '../base/settings/functions.web';
import { getHideSelfView } from '../base/settings/functions.any';
import { getMaxColumnCount } from '../video-layout/functions.web';
import {
@@ -146,7 +146,7 @@ export function setVerticalViewDimensions() {
const state = getState();
const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
const { width: filmstripWidth } = state['features/filmstrip'];
const disableSelfView = shouldHideSelfView(state);
const disableSelfView = getHideSelfView(state);
const resizableFilmstrip = isFilmstripResizable(state);
const _verticalViewGrid = showGridInVerticalView(state);
const numberOfRemoteParticipants = getRemoteParticipantCountWithFake(state);
@@ -255,7 +255,7 @@ export function setHorizontalViewDimensions() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
const disableSelfView = shouldHideSelfView(state);
const disableSelfView = getHideSelfView(state);
const thumbnails = calculateThumbnailSizeForHorizontalView(clientHeight);
const remoteVideosContainerWidth
= clientWidth - (disableSelfView ? 0 : thumbnails?.local?.width) - HORIZONTAL_FILMSTRIP_MARGIN;

View File

@@ -8,7 +8,7 @@ import { getLocalParticipant } from '../../../base/participants';
import { Platform } from '../../../base/react';
import { connect } from '../../../base/redux';
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
import { shouldHideSelfView } from '../../../base/settings/functions.any';
import { getHideSelfView } from '../../../base/settings/functions.any';
import { isToolboxVisible } from '../../../toolbox/functions';
import { setVisibleRemoteParticipants } from '../../actions';
import {
@@ -320,7 +320,7 @@ class Filmstrip extends PureComponent<Props> {
*/
function _mapStateToProps(state) {
const { enabled, remoteParticipants } = state['features/filmstrip'];
const disableSelfView = shouldHideSelfView(state);
const disableSelfView = getHideSelfView(state);
const showRemoteVideos = shouldRemoteVideosBeVisible(state);
const responsiveUI = state['features/base/responsive-ui'];

View File

@@ -11,7 +11,7 @@ import type { Dispatch } from 'redux';
import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { shouldHideSelfView } from '../../../base/settings/functions.any';
import { getHideSelfView } from '../../../base/settings/functions.any';
import { setVisibleRemoteParticipants } from '../../actions.web';
import Thumbnail from './Thumbnail';
@@ -283,7 +283,7 @@ class TileView extends PureComponent<Props> {
function _mapStateToProps(state, ownProps) {
const responsiveUi = state['features/base/responsive-ui'];
const { remoteParticipants, tileViewDimensions } = state['features/filmstrip'];
const disableSelfView = shouldHideSelfView(state);
const disableSelfView = getHideSelfView(state);
const { height } = tileViewDimensions.thumbnailSize;
const { columns } = tileViewDimensions;

View File

@@ -16,7 +16,7 @@ import Icon from '../../../base/icons/components/Icon';
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
import { IParticipant } from '../../../base/participants/types';
import { connect } from '../../../base/redux/functions';
import { shouldHideSelfView } from '../../../base/settings/functions.web';
import { getHideSelfView } from '../../../base/settings/functions.any';
import { showToolbox } from '../../../toolbox/actions.web';
import { isButtonEnabled, isToolboxVisible } from '../../../toolbox/functions.web';
import { LAYOUTS } from '../../../video-layout/constants';
@@ -877,7 +877,7 @@ function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
const reduceHeight = state['features/toolbox'].visible && toolbarButtons.length;
const remoteVideosVisible = shouldRemoteVideosBeVisible(state);
const { isOpen: shiftRight } = state['features/chat'];
const disableSelfView = shouldHideSelfView(state);
const disableSelfView = getHideSelfView(state);
const { clientWidth, clientHeight } = state['features/base/responsive-ui'];
const collapseTileView = reduceHeight

View File

@@ -665,7 +665,7 @@ class Thumbnail extends Component<IProps, IState> {
video: {}
};
const avatarSize = Math.min(_height / 2, _width - 30);
const avatarSize = Math.min(_height / 2, _width - 30, 200);
let { left } = style || {};
if (typeof left === 'number' && horizontalOffset) {

View File

@@ -4,7 +4,7 @@ import { shouldComponentUpdate } from 'react-window';
import { getLocalParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { shouldHideSelfView } from '../../../base/settings/functions.any';
import { getHideSelfView } from '../../../base/settings/functions.any';
import { LAYOUTS, getCurrentLayout } from '../../../video-layout';
import { FILMSTRIP_TYPE, TILE_ASPECT_RATIO, TILE_HORIZONTAL_MARGIN } from '../../constants';
import { getActiveParticipantsIds, showGridInVerticalView } from '../../functions';
@@ -152,7 +152,7 @@ function _mapStateToProps(state, ownProps) {
const _currentLayout = getCurrentLayout(state);
const { remoteParticipants: remote } = state['features/filmstrip'];
const activeParticipants = getActiveParticipantsIds(state);
const disableSelfView = shouldHideSelfView(state);
const disableSelfView = getHideSelfView(state);
const _verticalViewGrid = showGridInVerticalView(state);
const filmstripType = ownProps.data?.filmstripType;
const stageFilmstrip = filmstripType === FILMSTRIP_TYPE.STAGE;

View File

@@ -10,7 +10,7 @@ import {
import Platform from '../base/react/Platform.native';
import { toState } from '../base/redux/functions';
import { ASPECT_RATIO_NARROW } from '../base/responsive-ui/constants';
import { shouldHideSelfView } from '../base/settings/functions.native';
import { getHideSelfView } from '../base/settings/functions.any';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import conferenceStyles from '../conference/components/native/styles';
@@ -104,7 +104,7 @@ export function getPinnedActiveParticipants(_state: any) {
*/
export function getTileViewParticipantCount(stateful: IStateful) {
const state = toState(stateful);
const disableSelfView = shouldHideSelfView(state);
const disableSelfView = getHideSelfView(state);
const localParticipant = getLocalParticipant(state);
const participantCount = getParticipantCountWithFake(state) - (disableSelfView && localParticipant ? 1 : 0);
@@ -152,7 +152,7 @@ export function isFilmstripScrollVisible(state: IReduxState) {
const { aspectRatio, clientWidth, clientHeight, safeAreaInsets = {} } = state['features/base/responsive-ui'];
const isNarrowAspectRatio = aspectRatio === ASPECT_RATIO_NARROW;
const disableSelfView = shouldHideSelfView(state);
const disableSelfView = getHideSelfView(state);
const localParticipant = Boolean(getLocalParticipant(state));
const localParticipantVisible = localParticipant && !disableSelfView;
const participantCount

View File

@@ -11,7 +11,7 @@ import {
isScreenShareParticipant
} from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { shouldHideSelfView } from '../base/settings/functions.web';
import { getHideSelfView } from '../base/settings/functions.any';
import {
getVideoTrackByParticipant,
isLocalTrackMuted,
@@ -218,7 +218,7 @@ export function getTileDefaultAspectRatio(disableResponsiveTiles: boolean,
*/
export function getNumberOfPartipantsForTileView(state: IReduxState) {
const { iAmRecorder } = state['features/base/config'];
const disableSelfView = shouldHideSelfView(state);
const disableSelfView = getHideSelfView(state);
const { localScreenShare } = state['features/base/participants'];
const localParticipantsCount = localScreenShare ? 2 : 1;
const numberOfParticipants = getParticipantCountWithFake(state)

View File

@@ -1,9 +1,8 @@
import { isMobileBrowser } from '../base/environment/utils';
import { pinParticipant } from '../base/participants/actions';
import { getParticipantCountWithFake } from '../base/participants/functions';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { clientResized } from '../base/responsive-ui/actions';
import { shouldHideSelfView } from '../base/settings/functions.web';
import { clientResized, setNarrowLayout } from '../base/responsive-ui/actions';
import { getHideSelfView } from '../base/settings/functions.any';
import { selectParticipantInLargeVideo } from '../large-video/actions.any';
import { getParticipantsPaneOpen } from '../participants-pane/functions';
import { setOverflowDrawer } from '../toolbox/actions.web';
@@ -38,7 +37,7 @@ StateListenerRegistry.register(
/* selector */ state => {
return {
numberOfParticipants: getParticipantCountWithFake(state),
disableSelfView: shouldHideSelfView(state),
disableSelfView: getHideSelfView(state),
localScreenShare: state['features/base/participants'].localScreenShare
};
},
@@ -136,9 +135,8 @@ StateListenerRegistry.register(
StateListenerRegistry.register(
/* selector */ state => state['features/base/responsive-ui'].clientWidth < DISPLAY_DRAWER_THRESHOLD,
/* listener */ (widthBelowThreshold, store) => {
if (isMobileBrowser()) {
store.dispatch(setOverflowDrawer(widthBelowThreshold));
}
store.dispatch(setOverflowDrawer(widthBelowThreshold));
store.dispatch(setNarrowLayout(widthBelowThreshold));
});
/**

View File

@@ -8,7 +8,7 @@ import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import Input from '../../../base/ui/components/native/Input';
import { sendMessage } from '../../../chat/actions.any';
import { goBack } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { formatGifUrlMessage, getGifRating, getGifUrl } from '../../functions';
import { formatGifUrlMessage, getGifRating, getGifUrl, getGiphyProxyUrl } from '../../functions';
import GifsMenuFooter from './GifsMenuFooter';
import styles from './styles';
@@ -18,6 +18,7 @@ const GifsMenu = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const rating = useSelector(getGifRating);
const proxyUrl = useSelector(getGiphyProxyUrl);
const options = {
mediaType: GiphyMediaType.Gif,
@@ -33,7 +34,7 @@ const GifsMenu = () => {
});
const sendGif = useCallback(e => {
const url = getGifUrl(e.nativeEvent.media);
const url = getGifUrl(e.nativeEvent.media, proxyUrl);
sendAnalytics(createGifSentEvent());

View File

@@ -1,4 +1,4 @@
import { GiphyFetch, TrendingOptions } from '@giphy/js-fetch-api';
import { GiphyFetch, TrendingOptions, setServerUrl } from '@giphy/js-fetch-api';
import { Grid } from '@giphy/react-components';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -22,7 +22,8 @@ import {
formatGifUrlMessage,
getGifAPIKey,
getGifRating,
getGifUrl
getGifUrl,
getGiphyProxyUrl
} from '../../function.any';
const OVERFLOW_DRAWER_PADDING = 16;
@@ -92,6 +93,7 @@ function GifsMenu() {
const overflowDrawer: boolean = useSelector(showOverflowDrawer);
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const rating = useSelector(getGifRating);
const proxyUrl = useSelector(getGiphyProxyUrl);
const fetchGifs = useCallback(async (offset = 0) => {
const options: TrendingOptions = {
@@ -114,7 +116,7 @@ function GifsMenu() {
const handleGifClick = useCallback((gif, e) => {
e?.stopPropagation();
const url = getGifUrl(gif);
const url = getGifUrl(gif, proxyUrl);
sendAnalytics(createGifSentEvent());
batch(() => {
@@ -177,6 +179,12 @@ function GifsMenu() {
// This fixes that.
useEffect(() => setSearchKey(''), []);
useEffect(() => {
if (proxyUrl) {
setServerUrl(proxyUrl);
}
}, []);
const onInputKeyPress = useCallback((e: React.KeyboardEvent) => {
e.stopPropagation();
}, []);

View File

@@ -3,6 +3,46 @@ import { IReduxState } from '../app/types';
import { GIF_DEFAULT_RATING, GIF_PREFIX } from './constants';
import { IGif } from './reducer';
/**
* Returns the gif config.
*
* @param {IReduxState} state - Redux state.
* @returns {Object}
*/
export function getGifConfig(state: IReduxState) {
return state['features/base/config'].giphy || {};
}
/**
* Get the GIF display mode.
*
* @param {IReduxState} state - Redux state.
* @returns {string}
*/
export function getGifDisplayMode(state: IReduxState) {
return getGifConfig(state).displayMode || 'all';
}
/**
* Get the GIF audience rating.
*
* @param {IReduxState} state - Redux state.
* @returns {string}
*/
export function getGifRating(state: IReduxState) {
return getGifConfig(state).rating || GIF_DEFAULT_RATING;
}
/**
* Get the Giphy proxy url.
*
* @param {IReduxState} state - Redux state.
* @returns {string}
*/
export function getGiphyProxyUrl(state: IReduxState) {
return getGifConfig(state).proxyUrl;
}
/**
* Gets the URL of the GIF for the given participant or null if there's none.
*
@@ -29,13 +69,18 @@ export function isGifMessage(message: string) {
* Returns the url of the gif selected in the gifs menu.
*
* @param {Object} gif - The gif data.
* @param {string} proxyUrl - The proxy server url.
* @returns {boolean}
*/
export function getGifUrl(gif?: { data?: { embed_url: string; }; embed_url?: string; }) {
export function getGifUrl(gif?: { data?: { embed_url: string; }; embed_url?: string; }, proxyUrl?: string) {
const embedUrl = gif?.embed_url || gif?.data?.embed_url || '';
const idx = embedUrl.lastIndexOf('/');
const id = embedUrl.substr(idx + 1);
if (proxyUrl) {
return `${proxyUrl}gifs/id/${id}`;
}
return `https://i.giphy.com/media/${id}/giphy.gif`;
}
@@ -56,7 +101,7 @@ export function formatGifUrlMessage(url: string) {
* @returns {string}
*/
export function getGifAPIKey(state: IReduxState) {
return state['features/base/config']?.giphy?.sdkKey ?? '';
return getGifConfig(state).sdkKey ?? '';
}
/**
@@ -77,26 +122,3 @@ export function isGifEnabled(state: IReduxState) {
return showGiphyIntegration && Boolean(!disableThirdPartyRequests && giphy?.enabled && Boolean(giphy?.sdkKey));
}
/**
* Get the GIF display mode.
*
* @param {IReduxState} state - Redux state.
* @returns {string}
*/
export function getGifDisplayMode(state: IReduxState) {
const { giphy } = state['features/base/config'];
return giphy?.displayMode || 'all';
}
/**
* Get the GIF audience rating.
*
* @param {IReduxState} state - Redux state.
* @returns {string}
*/
export function getGifRating(state: IReduxState) {
const { giphy } = state['features/base/config'];
return giphy?.rating || GIF_DEFAULT_RATING;
}

View File

@@ -2,13 +2,13 @@ import { GiphySDK } from '@giphy/react-native-sdk';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { isGifEnabled } from './function.any';
import { getGifConfig, isGifEnabled } from './function.any';
/**
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/config']?.giphy,
/* selector */ state => getGifConfig(state),
/* listener */ (_, store) => {
const state = store.getState();

View File

@@ -1,4 +1,5 @@
import { IReduxState, IStore } from '../app/types';
import { getSsrcRewritingFeatureFlag } from '../base/config/functions.any';
import { MEDIA_TYPE } from '../base/media/constants';
import {
getDominantSpeakerParticipant,
@@ -170,11 +171,14 @@ function _electParticipantInLargeVideo(state: IReduxState) {
participant = undefined;
// Next, pick the most recent participant with video.
const tracks = state['features/base/tracks'];
const videoTrack = _electLastVisibleRemoteVideo(tracks);
// (Skip this if rewriting, tracks may be detached from any owner.)
if (!getSsrcRewritingFeatureFlag(state)) {
const tracks = state['features/base/tracks'];
const videoTrack = _electLastVisibleRemoteVideo(tracks);
if (videoTrack) {
return videoTrack.participantId;
if (videoTrack) {
return videoTrack.participantId;
}
}
// Last, select the participant that joined last (other than poltergist or other bot type participants).

View File

@@ -7,6 +7,7 @@ import { VIDEO_TYPE } from '../../base/media';
import { getLocalParticipant } from '../../base/participants';
import { Watermarks } from '../../base/react';
import { connect } from '../../base/redux';
import { getHideSelfView } from '../../base/settings/functions.any';
import { getVideoTrackByParticipant } from '../../base/tracks';
import { setColorAlpha } from '../../base/util';
import { StageParticipantNameLabel } from '../../display-name';
@@ -105,6 +106,16 @@ type Props = {
*/
_whiteboardEnabled: boolean;
/**
* Whether or not the hideSelfView is enabled.
*/
_hideSelfView: boolean;
/**
* Local Participant id.
*/
_localParticipantId: string;
/**
* The Redux dispatch function.
*/
@@ -146,7 +157,13 @@ class LargeVideo extends Component<Props> {
* @inheritdoc
*/
componentDidUpdate(prevProps: Props) {
const { _visibleFilmstrip, _isScreenSharing, _seeWhatIsBeingShared, _largeVideoParticipantId } = this.props;
const {
_visibleFilmstrip,
_isScreenSharing,
_seeWhatIsBeingShared,
_largeVideoParticipantId,
_hideSelfView,
_localParticipantId } = this.props;
if (prevProps._visibleFilmstrip !== _visibleFilmstrip) {
this._updateLayout();
@@ -159,6 +176,11 @@ class LargeVideo extends Component<Props> {
if (_isScreenSharing && _seeWhatIsBeingShared) {
VideoLayout.updateLargeVideo(_largeVideoParticipantId, true, true);
}
if (_largeVideoParticipantId === _localParticipantId
&& prevProps._hideSelfView !== _hideSelfView) {
VideoLayout.updateLargeVideo(_largeVideoParticipantId, true, false);
}
}
/**
@@ -351,9 +373,11 @@ function _mapStateToProps(state) {
_customBackgroundColor: backgroundColor,
_customBackgroundImageUrl: backgroundImageUrl,
_displayScreenSharingPlaceholder: isLocalScreenshareOnLargeVideo && !seeWhatIsBeingShared && !isOnSpot,
_hideSelfView: getHideSelfView(state),
_isChatOpen: isChatOpen,
_isScreenSharing: isLocalScreenshareOnLargeVideo,
_largeVideoParticipantId: largeVideoParticipant?.id,
_localParticipantId: localParticipantId,
_noAutoPlayVideo: testingConfig?.noAutoPlayVideo,
_resizableFilmstrip: isFilmstripResizable(state),
_seeWhatIsBeingShared: seeWhatIsBeingShared,

View File

@@ -3,6 +3,7 @@
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { isMobileBrowser } from '../../../../../base/environment/utils';
import { isLocalParticipantModerator } from '../../../../../base/participants';
import { equals } from '../../../../../base/redux';
import useContextMenu from '../../../../../base/ui/hooks/useContextMenu.web';
@@ -40,10 +41,11 @@ export const RoomList = ({ searchString }: Props) => {
const isLocalModerator = useSelector(isLocalParticipantModerator);
const showAutoAssign = useSelector(isAutoAssignParticipantsVisible);
const { hideJoinRoomButton } = useSelector(getBreakoutRoomsConfig);
const _overflowDrawer = useSelector(showOverflowDrawer);
const overflowDrawer = useSelector(showOverflowDrawer);
const [ lowerMenu, raiseMenu, toggleMenu, menuEnter, menuLeave, raiseContext ] = useContextMenu();
const [ lowerParticipantMenu, raiseParticipantMenu, toggleParticipantMenu,
participantMenuEnter, participantMenuLeave, raiseParticipantContext ] = useContextMenu();
const hideMenu = useCallback(() => !overflowDrawer && lowerMenu(), [ overflowDrawer, lowerMenu ]);
const onRaiseMenu = useCallback(room => target => raiseMenu(room, target), [ raiseMenu ]);
return (
@@ -55,14 +57,14 @@ export const RoomList = ({ searchString }: Props) => {
<React.Fragment key = { room.id }>
<CollapsibleRoom
isHighlighted = { raiseContext.entity === room }
onLeave = { lowerMenu }
onLeave = { hideMenu }
onRaiseMenu = { onRaiseMenu(room) }
participantContextEntity = { raiseParticipantContext.entity }
raiseParticipantContextMenu = { raiseParticipantMenu }
room = { room }
searchString = { searchString }
toggleParticipantMenu = { toggleParticipantMenu }>
{!_overflowDrawer && <>
{!isMobileBrowser() && <>
{!hideJoinRoomButton && <JoinActionButton room = { room } />}
{isLocalModerator && !room.isMainRoom
&& <RoomActionEllipsis onClick = { toggleMenu(room) } />}

View File

@@ -1,18 +1,17 @@
// @flow
import React, { useCallback, useState } from 'react';
import type { AbstractComponent } from 'react';
import React, { ComponentType, FormEvent, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { createPollEvent, sendAnalytics } from '../../analytics';
import { createPollEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { COMMAND_NEW_POLL } from '../constants';
/**
* The type of the React {@code Component} props of inheriting component.
*/
type InputProps = {
setCreateMode: boolean => void,
setCreateMode: (mode: boolean) => void;
};
/*
@@ -20,16 +19,15 @@ type InputProps = {
* concrete implementations (web/native).
**/
export type AbstractProps = InputProps & {
answers: Array<string>,
question: string,
setQuestion: string => void,
setAnswer: (number, string) => void,
addAnswer: ?number => void,
moveAnswer: (number, number) => void,
removeAnswer: number => void,
onSubmit: Function,
isSubmitDisabled: boolean,
t: Function,
addAnswer: (index?: number) => void;
answers: Array<string>;
isSubmitDisabled: boolean;
onSubmit: (event?: FormEvent<HTMLFormElement>) => void;
question: string;
removeAnswer: (index: number) => void;
setAnswer: (index: number, value: string) => void;
setQuestion: (question: string) => void;
t: Function;
};
/**
@@ -39,7 +37,7 @@ export type AbstractProps = InputProps & {
* @param {React.AbstractComponent} Component - The concrete component.
* @returns {React.AbstractComponent}
*/
const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (props: InputProps) => {
const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props: InputProps) => {
const { setCreateMode } = props;
@@ -51,25 +49,15 @@ const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (pro
answers[i] = answer;
setAnswers([ ...answers ]);
});
}, [ answers ]);
const addAnswer = useCallback((i: ?number) => {
const addAnswer = useCallback((i?: number) => {
const newAnswers = [ ...answers ];
sendAnalytics(createPollEvent('option.added'));
newAnswers.splice(typeof i === 'number' ? i : answers.length, 0, '');
setAnswers(newAnswers);
});
const moveAnswer = useCallback((i, j) => {
const newAnswers = [ ...answers ];
const answer = answers[i];
sendAnalytics(createPollEvent('option.moved'));
newAnswers.splice(i, 1);
newAnswers.splice(j, 0, answer);
setAnswers(newAnswers);
});
}, [ answers ]);
const removeAnswer = useCallback(i => {
if (answers.length <= 2) {
@@ -80,9 +68,9 @@ const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (pro
sendAnalytics(createPollEvent('option.removed'));
newAnswers.splice(i, 1);
setAnswers(newAnswers);
});
}, [ answers ]);
const conference = useSelector(state => state['features/base/conference'].conference);
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
const onSubmit = useCallback(ev => {
if (ev) {
@@ -95,7 +83,7 @@ const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (pro
return;
}
conference.sendMessage({
conference?.sendMessage({
type: COMMAND_NEW_POLL,
pollId: Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36),
question,
@@ -118,7 +106,6 @@ const AbstractPollCreate = (Component: AbstractComponent<AbstractProps>) => (pro
addAnswer = { addAnswer }
answers = { answers }
isSubmitDisabled = { isSubmitDisabled }
moveAnswer = { moveAnswer }
onSubmit = { onSubmit }
question = { question }
removeAnswer = { removeAnswer }

View File

@@ -1,13 +1,11 @@
// @flow
import React, { useCallback, useMemo, useState } from 'react';
import type { AbstractComponent } from 'react';
import React, { ComponentType, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { createPollEvent, sendAnalytics } from '../../analytics';
import { getParticipantDisplayName } from '../../base/participants';
import { getParticipantById } from '../../base/participants/functions';
import { createPollEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IReduxState } from '../../app/types';
import { getParticipantById, getParticipantDisplayName } from '../../base/participants/functions';
import { useBoundSelector } from '../../base/util/hooks';
import { setVoteChanging } from '../actions';
import { getPoll } from '../functions';
@@ -20,28 +18,28 @@ type InputProps = {
/**
* ID of the poll to display.
*/
pollId: string,
pollId: string;
};
export type AnswerInfo = {
name: string,
percentage: number,
voters?: Array<{ id: number, name: string }>,
voterCount: number
name: string;
percentage: number;
voterCount: number;
voters?: Array<{ id: string; name: string; } | undefined>;
};
/**
* The type of the React {@code Component} props of {@link AbstractPollResults}.
*/
export type AbstractProps = {
answers: Array<AnswerInfo>,
changeVote: Function,
creatorName: string,
showDetails: boolean,
question: string,
t: Function,
toggleIsDetailed: Function,
haveVoted: boolean,
answers: Array<AnswerInfo>;
changeVote: (e: React.MouseEvent) => void;
creatorName: string;
haveVoted: boolean;
question: string;
showDetails: boolean;
t: Function;
toggleIsDetailed: (e: React.MouseEvent) => void;
};
/**
@@ -51,18 +49,18 @@ export type AbstractProps = {
* @param {React.AbstractComponent} Component - The concrete component.
* @returns {React.AbstractComponent}
*/
const AbstractPollResults = (Component: AbstractComponent<AbstractProps>) => (props: InputProps) => {
const AbstractPollResults = (Component: ComponentType<AbstractProps>) => (props: InputProps) => {
const { pollId } = props;
const pollDetails = useSelector(getPoll(pollId));
const participant = useBoundSelector(getParticipantById, pollDetails.senderId);
const reduxState = useSelector(state => state);
const reduxState = useSelector((state: IReduxState) => state);
const [ showDetails, setShowDetails ] = useState(false);
const toggleIsDetailed = useCallback(() => {
sendAnalytics(createPollEvent('vote.detailsViewed'));
setShowDetails(!showDetails);
});
setShowDetails(details => !details);
}, []);
const answers: Array<AnswerInfo> = useMemo(() => {
const allVoters = new Set();
@@ -79,7 +77,7 @@ const AbstractPollResults = (Component: AbstractComponent<AbstractProps>) => (pr
const nrOfVotersPerAnswer = answer.voters ? Object.keys(answer.voters).length : 0;
const percentage = allVoters.size > 0 ? Math.round(nrOfVotersPerAnswer / allVoters.size * 100) : 0;
let voters = null;
let voters;
if (showDetails && answer.voters) {
const answerVoters = answer.voters?.length ? [ ...answer.voters ] : Object.keys({ ...answer.voters });

View File

@@ -1,7 +1,4 @@
// @flow
import React, { useState } from 'react';
import type { AbstractComponent } from 'react';
import React, { ComponentType, useState } from 'react';
import { useTranslation } from 'react-i18next';
/*
@@ -9,10 +6,10 @@ import { useTranslation } from 'react-i18next';
* concrete implementations (web/native).
**/
export type AbstractProps = {
createMode: boolean,
onCreate: void => void,
setCreateMode: boolean => void,
t: Function,
createMode: boolean;
onCreate: () => void;
setCreateMode: (mode: boolean) => void;
t: Function;
};
/**
@@ -22,7 +19,7 @@ export type AbstractProps = {
* @param {React.AbstractComponent} Component - The concrete component.
* @returns {React.AbstractComponent}
*/
const AbstractPollsPane = (Component: AbstractComponent<AbstractProps>) => () => {
const AbstractPollsPane = (Component: ComponentType<AbstractProps>) => () => {
const [ createMode, setCreateMode ] = useState(false);

View File

@@ -35,7 +35,7 @@ export const dialogStyles = createStyleSheet({
},
optionRemoveButtonText: {
color: BaseTheme.palette.actionDangerActive
color: BaseTheme.palette.link01
},
field: {

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Button from '../../../base/ui/components/web/Button';
import Checkbox from '../../../base/ui/components/web/Checkbox';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
@@ -10,8 +10,40 @@ import AbstractPollAnswer, { AbstractProps } from '../AbstractPollAnswer';
const useStyles = makeStyles()(theme => {
return {
container: {
margin: '24px',
padding: '16px',
backgroundColor: theme.palette.ui02,
borderRadius: '8px'
},
header: {
marginBottom: '24px'
},
question: {
...withPixelLineHeight(theme.typography.heading6),
color: theme.palette.text01,
marginBottom: '8px'
},
creator: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text02
},
answerList: {
listStyleType: 'none',
margin: 0,
padding: 0,
marginBottom: '24px'
},
answer: {
display: 'flex',
marginBottom: '16px'
},
footer: {
display: 'flex',
justifyContent: 'flex-end'
},
buttonMargin: {
marginRight: theme.spacing(2)
marginRight: theme.spacing(3)
}
};
});
@@ -27,23 +59,23 @@ const PollAnswer = ({
t
}: AbstractProps) => {
const { changingVote } = poll;
const { classes: styles } = useStyles();
const { classes } = useStyles();
return (
<div className = 'poll-answer'>
<div className = 'poll-header'>
<div className = 'poll-question'>
<span>{ poll.question }</span>
<div className = { classes.container }>
<div className = { classes.header }>
<div className = { classes.question }>
{ poll.question }
</div>
<div className = 'poll-creator'>
<div className = { classes.creator }>
{ t('polls.by', { name: creatorName }) }
</div>
</div>
<ol className = 'poll-answer-list'>
<ul className = { classes.answerList }>
{
poll.answers.map((answer: any, index: number) => (
<li
className = 'poll-answer-container'
className = { classes.answer }
key = { index }>
<Checkbox
checked = { checkBoxStates[index] }
@@ -54,19 +86,17 @@ const PollAnswer = ({
</li>
))
}
</ol>
<div className = 'poll-footer poll-answer-footer' >
</ul>
<div className = { classes.footer } >
<Button
accessibilityLabel = { t('polls.answer.skip') }
className = { styles.buttonMargin }
fullWidth = { true }
className = { classes.buttonMargin }
labelKey = { 'polls.answer.skip' }
onClick = { changingVote ? skipChangeVote : skipAnswer }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.answer.submit') }
disabled = { isSubmitAnswerDisabled(checkBoxStates) }
fullWidth = { true }
labelKey = { 'polls.answer.submit' }
onClick = { submitAnswer } />
</div>

View File

@@ -1,21 +1,62 @@
/* eslint-disable lines-around-comment */
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { makeStyles } from 'tss-react/mui';
// @ts-ignore
import { Tooltip } from '../../../base/tooltip';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
import { ANSWERS_LIMIT, CHAR_LIMIT } from '../../constants';
// @ts-ignore
import AbstractPollCreate from '../AbstractPollCreate';
// @ts-ignore
import type { AbstractProps } from '../AbstractPollCreate';
import AbstractPollCreate, { AbstractProps } from '../AbstractPollCreate';
const useStyles = makeStyles()(theme => {
return {
container: {
height: '100%',
position: 'relative'
},
createContainer: {
padding: '0 24px',
height: 'calc(100% - 88px)',
overflowY: 'auto'
},
header: {
...withPixelLineHeight(theme.typography.heading6),
color: theme.palette.text01,
margin: '24px 0 16px'
},
questionContainer: {
paddingBottom: '24px',
borderBottom: `1px solid ${theme.palette.ui03}`
},
answerList: {
listStyleType: 'none',
margin: 0,
padding: 0
},
answer: {
marginBottom: '24px'
},
removeOption: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.link01,
marginTop: '8px',
border: 0,
background: 'transparent'
},
addButtonContainer: {
display: 'flex'
},
footer: {
position: 'absolute',
bottom: 0,
display: 'flex',
justifyContent: 'flex-end',
padding: '24px',
width: '100%',
boxSizing: 'border-box'
},
buttonMargin: {
marginRight: theme.spacing(2)
marginRight: theme.spacing(3)
}
};
});
@@ -32,7 +73,7 @@ const PollCreate = ({
setQuestion,
t
}: AbstractProps) => {
const { classes: styles } = useStyles();
const { classes } = useStyles();
/*
* This ref stores the Array of answer input fields, allowing us to focus on them.
@@ -139,76 +180,54 @@ const PollCreate = ({
}
}, [ answers, addAnswer, removeAnswer, requestFocus ]);
const autogrow = (ev: React.ChangeEvent<HTMLTextAreaElement>) => {
const el = ev.target;
el.style.height = '1px';
el.style.height = `${el.scrollHeight + 2}px`;
};
/* eslint-disable react/jsx-no-bind */
return (<form
className = 'polls-pane-content'
className = { classes.container }
onSubmit = { onSubmit }>
<div className = 'poll-create-container poll-container'>
<div className = 'poll-create-header'>
<div className = { classes.createContainer }>
<div className = { classes.header }>
{ t('polls.create.create') }
</div>
<div className = 'poll-question-field'>
<span className = 'poll-create-label'>
{ t('polls.create.pollQuestion') }
</span>
<textarea
<div className = { classes.questionContainer }>
<Input
autoFocus = { true }
className = 'expandable-input'
label = { t('polls.create.pollQuestion') }
maxLength = { CHAR_LIMIT }
onChange = { ev => setQuestion(ev.target.value) }
onInput = { autogrow }
onKeyDown = { onQuestionKeyDown }
onChange = { setQuestion }
onKeyPress = { onQuestionKeyDown }
placeholder = { t('polls.create.questionPlaceholder') }
required = { true }
rows = { 1 }
textarea = { true }
value = { question } />
</div>
<ol className = 'poll-answer-field-list'>
<ol className = { classes.answerList }>
{answers.map((answer: any, i: number) =>
(<li
className = 'poll-answer-field'
className = { classes.answer }
key = { i }>
<span className = 'poll-create-label'>
{ t('polls.create.pollOption', { index: i + 1 })}
</span>
<div className = 'poll-create-option-row'>
<textarea
className = 'expandable-input'
maxLength = { CHAR_LIMIT }
onChange = { ev => setAnswer(i, ev.target.value) }
onInput = { autogrow }
onKeyDown = { ev => onAnswerKeyDown(i, ev) }
placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
ref = { r => registerFieldRef(i, r) }
required = { true }
rows = { 1 }
value = { answer } />
</div>
<Input
label = { t('polls.create.pollOption', { index: i + 1 }) }
maxLength = { CHAR_LIMIT }
onChange = { val => setAnswer(i, val) }
onKeyPress = { ev => onAnswerKeyDown(i, ev) }
placeholder = { t('polls.create.answerPlaceholder', { index: i + 1 }) }
ref = { r => registerFieldRef(i, r) }
textarea = { true }
value = { answer } />
{ answers.length > 2
&& <Tooltip content = { t('polls.create.removeOption') }>
<button
className = 'poll-remove-option-button'
onClick = { () => removeAnswer(i) }
type = 'button'>
{ t('polls.create.removeOption') }
</button>
</Tooltip>}
&& <button
className = { classes.removeOption }
onClick = { () => removeAnswer(i) }
type = 'button'>
{ t('polls.create.removeOption') }
</button>}
</li>)
)}
</ol>
<div className = 'poll-add-button'>
<div className = { classes.addButtonContainer }>
<Button
accessibilityLabel = { t('polls.create.addOption') }
disabled = { answers.length >= ANSWERS_LIMIT }
fullWidth = { true }
labelKey = { 'polls.create.addOption' }
onClick = { () => {
addAnswer();
@@ -217,23 +236,20 @@ const PollCreate = ({
type = { BUTTON_TYPES.SECONDARY } />
</div>
</div>
<div className = 'poll-footer poll-create-footer'>
<div className = { classes.footer }>
<Button
accessibilityLabel = { t('polls.create.cancel') }
className = { styles.buttonMargin }
fullWidth = { true }
className = { classes.buttonMargin }
labelKey = { 'polls.create.cancel' }
onClick = { () => setCreateMode(false) }
type = { BUTTON_TYPES.SECONDARY } />
<Button
accessibilityLabel = { t('polls.create.send') }
disabled = { isSubmitDisabled }
fullWidth = { true }
isSubmit = { true }
labelKey = { 'polls.create.send' } />
</div>
</form>);
};
/*

View File

@@ -1,22 +1,22 @@
// @flow
import React from 'react';
import { useSelector } from 'react-redux';
import { PollAnswer, PollResults } from '..';
import { shouldShowResults } from '../../functions';
import PollAnswer from './PollAnswer';
import PollResults from './PollResults';
type Props = {
interface IProps {
/**
* Id of the poll.
*/
pollId: string,
pollId: string;
}
const PollItem = React.forwardRef<Props, HTMLElement>(({ pollId }: Props, ref) => {
const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId }: IProps, ref) => {
const showResults = useSelector(shouldShowResults(pollId));
return (

View File

@@ -1,84 +0,0 @@
// @flow
import React from 'react';
import AbstractPollResults from '../AbstractPollResults';
import type { AbstractProps } from '../AbstractPollResults';
/**
* Component that renders the poll results.
*
* @param {Props} props - The passed props.
* @returns {React.Node}
*/
const PollResults = (props: AbstractProps) => {
const {
answers,
changeVote,
creatorName,
haveVoted,
showDetails,
question,
t,
toggleIsDetailed
} = props;
return (
<div className = 'poll-results'>
<div className = 'poll-header'>
<div className = 'poll-question'>
<strong>{ question }</strong>
</div>
<div className = 'poll-creator'>
{ t('polls.by', { name: creatorName }) }
</div>
</div>
<ol className = 'poll-result-list'>
{answers.map(({ name, percentage, voters, voterCount }, index) =>
(<li key = { index }>
<div className = 'poll-answer-header'>
<span className = 'poll-answer-vote-name' >{name}</span>
</div>
<div className = 'poll-answer-short-results'>
<span className = 'poll-bar-container'>
<div
className = 'poll-bar'
style = {{ width: `${percentage}%` }} />
</span>
<div className = 'poll-answer-vote-count-container'>
<span className = 'poll-answer-vote-count'>({voterCount}) {percentage}%</span>
</div>
</div>
{ showDetails && voters && voterCount > 0
&& <ul className = 'poll-answer-voters'>
{voters.map(voter =>
<li key = { voter.id }>{voter.name}</li>
)}
</ul>}
</li>)
)}
</ol>
<div className = { 'poll-result-links' }>
<a
className = { 'poll-detail-link' }
onClick = { toggleIsDetailed }>
{showDetails ? t('polls.results.hideDetailedResults') : t('polls.results.showDetailedResults')}
</a>
<a
className = { 'poll-change-vote-link' }
onClick = { changeVote }>
{haveVoted ? t('polls.results.changeVote') : t('polls.results.vote')}
</a>
</div>
</div>
);
};
/*
* We apply AbstractPollResults to fill in the AbstractProps common
* to both the web and native implementations.
*/
// eslint-disable-next-line new-cap
export default AbstractPollResults(PollResults);

View File

@@ -0,0 +1,177 @@
import React from 'react';
import { makeStyles } from 'tss-react/mui';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import AbstractPollResults, { AbstractProps } from '../AbstractPollResults';
const useStyles = makeStyles()(theme => {
return {
container: {
margin: '24px',
padding: '16px',
backgroundColor: theme.palette.ui02,
borderRadius: '8px'
},
header: {
marginBottom: '16px'
},
question: {
...withPixelLineHeight(theme.typography.heading6),
color: theme.palette.text01,
marginBottom: '8px'
},
creator: {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text02
},
resultList: {
listStyleType: 'none',
margin: 0,
padding: 0,
'& li': {
marginBottom: '16px'
}
},
answerName: {
display: 'flex',
flexShrink: 1,
overflowWrap: 'anywhere',
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01,
marginBottom: '4px'
},
answerResultContainer: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
minWidth: '10em'
},
barContainer: {
backgroundColor: theme.palette.ui03,
borderRadius: '4px',
height: '6px',
maxWidth: '160px',
width: '158px',
flexGrow: 1,
marginTop: '2px'
},
bar: {
height: '6px',
borderRadius: '4px',
backgroundColor: theme.palette.action01
},
voteCount: {
flex: 1,
textAlign: 'right',
...withPixelLineHeight(theme.typography.bodyShortBold),
color: theme.palette.text01
},
voters: {
margin: 0,
marginTop: '4px',
listStyleType: 'none',
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.ui03,
borderRadius: theme.shape.borderRadius,
padding: '8px 16px',
'& li': {
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.text01,
margin: 0,
marginBottom: '2px',
'&:last-of-type': {
marginBottom: 0
}
}
},
buttonsContainer: {
display: 'flex',
justifyContent: 'space-between',
'& button': {
border: 0,
backgroundColor: 'transparent',
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: theme.palette.link01
}
}
};
});
/**
* Component that renders the poll results.
*
* @param {Props} props - The passed props.
* @returns {React.Node}
*/
const PollResults = ({
answers,
changeVote,
creatorName,
haveVoted,
showDetails,
question,
t,
toggleIsDetailed
}: AbstractProps) => {
const { classes } = useStyles();
return (
<div className = { classes.container }>
<div className = { classes.header }>
<div className = { classes.question }>
{question}
</div>
<div className = { classes.creator }>
{t('polls.by', { name: creatorName })}
</div>
</div>
<ul className = { classes.resultList }>
{answers.map(({ name, percentage, voters, voterCount }, index) =>
(<li key = { index }>
<div className = { classes.answerName }>
{name}
</div>
<div className = { classes.answerResultContainer }>
<span className = { classes.barContainer }>
<div
className = { classes.bar }
style = {{ width: `${percentage}%` }} />
</span>
<div className = { classes.voteCount }>
{voterCount}({percentage}%)
</div>
</div>
{showDetails && voters && voterCount > 0
&& <ul className = { classes.voters }>
{voters.map(voter =>
<li key = { voter?.id }>{voter?.name}</li>
)}
</ul>}
</li>)
)}
</ul>
<div className = { classes.buttonsContainer }>
<button
onClick = { toggleIsDetailed }>
{showDetails ? t('polls.results.hideDetailedResults') : t('polls.results.showDetailedResults')}
</button>
<button
onClick = { changeVote }>
{haveVoted ? t('polls.results.changeVote') : t('polls.results.vote')}
</button>
</div>
</div>
);
};
/*
* We apply AbstractPollResults to fill in the AbstractProps common
* to both the web and native implementations.
*/
// eslint-disable-next-line new-cap
export default AbstractPollResults(PollResults);

View File

@@ -1,58 +0,0 @@
// @flow
import React, { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { Icon, IconMessage } from '../../../base/icons';
import { browser } from '../../../base/lib-jitsi-meet';
import PollItem from './PollItem';
const PollsList = () => {
const { t } = useTranslation();
const polls = useSelector(state => state['features/polls'].polls);
const pollListEndRef = useRef(null);
const scrollToBottom = useCallback(() => {
if (pollListEndRef.current) {
// Safari does not support options
const param = browser.isSafari()
? false : {
behavior: 'smooth',
block: 'end',
inline: 'nearest'
};
pollListEndRef.current.scrollIntoView(param);
}
}, [ pollListEndRef.current ]);
useEffect(() => {
scrollToBottom();
}, [ polls ]);
const listPolls = Object.keys(polls);
return (
<>
{listPolls.length === 0
? <div className = 'pane-content'>
<Icon
className = 'empty-pane-icon'
src = { IconMessage } />
<span className = 'empty-pane-message'>{t('polls.results.empty')}</span>
</div>
: listPolls.map((id, index) => (
<PollItem
key = { id }
pollId = { id }
ref = { listPolls.length - 1 === index ? pollListEndRef : null } />
))}
</>
);
};
export default PollsList;

View File

@@ -0,0 +1,89 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../app/types';
import Icon from '../../../base/icons/components/Icon';
import { IconMessage } from '../../../base/icons/svg';
import { browser } from '../../../base/lib-jitsi-meet';
import { withPixelLineHeight } from '../../../base/styles/functions.web';
import PollItem from './PollItem';
const useStyles = makeStyles()(theme => {
return {
container: {
height: '100%',
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column'
},
emptyIcon: {
width: '100px',
padding: '16px',
'& svg': {
width: '100%',
height: 'auto'
}
},
emptyMessage: {
...withPixelLineHeight(theme.typography.bodyLongBold),
color: theme.palette.text02,
padding: '0 24px',
textAlign: 'center'
}
};
});
const PollsList = () => {
const { t } = useTranslation();
const { classes, theme } = useStyles();
const polls = useSelector((state: IReduxState) => state['features/polls'].polls);
const pollListEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = useCallback(() => {
if (pollListEndRef.current) {
// Safari does not support options
const param = browser.isSafari()
? false : {
behavior: 'smooth' as const,
block: 'end' as const,
inline: 'nearest' as const
};
pollListEndRef.current.scrollIntoView(param);
}
}, [ pollListEndRef.current ]);
useEffect(() => {
scrollToBottom();
}, [ polls ]);
const listPolls = Object.keys(polls);
return (
<>
{listPolls.length === 0
? <div className = { classes.container }>
<Icon
className = { classes.emptyIcon }
color = { theme.palette.icon03 }
src = { IconMessage } />
<span className = { classes.emptyMessage }>{t('polls.results.empty')}</span>
</div>
: listPolls.map((id, index) => (
<PollItem
key = { id }
pollId = { id }
ref = { listPolls.length - 1 === index ? pollListEndRef : null } />
))}
</>
);
};
export default PollsList;

View File

@@ -1,27 +1,42 @@
/* eslint-disable lines-around-comment */
import React from 'react';
import { makeStyles } from 'tss-react/mui';
import Button from '../../../base/ui/components/web/Button';
// @ts-ignore
import AbstractPollsPane from '../AbstractPollsPane';
// @ts-ignore
import type { AbstractProps } from '../AbstractPollsPane';
import AbstractPollsPane, { AbstractProps } from '../AbstractPollsPane';
import PollCreate from './PollCreate';
// @ts-ignore
import PollsList from './PollsList';
const useStyles = makeStyles()(() => {
return {
container: {
height: '100%',
position: 'relative'
},
listContainer: {
height: 'calc(100% - 88px)',
overflowY: 'auto'
},
footer: {
position: 'absolute',
bottom: 0,
padding: '24px',
width: '100%',
boxSizing: 'border-box'
}
};
});
const PollsPane = (props: AbstractProps) => {
const { createMode, onCreate, setCreateMode, t } = props;
const PollsPane = ({ createMode, onCreate, setCreateMode, t }: AbstractProps) => {
const { classes } = useStyles();
return createMode
? <PollCreate setCreateMode = { setCreateMode } />
: <div className = 'polls-pane-content'>
<div className = { 'poll-container' } >
: <div className = { classes.container }>
<div className = { classes.listContainer } >
<PollsList />
</div>
<div className = 'poll-footer poll-create-footer'>
<div className = { classes.footer }>
<Button
accessibilityLabel = { t('polls.create.create') }
autoFocus = { true }

View File

@@ -1,25 +1,30 @@
// @flow
import InlineDialog from '@atlaskit/inline-dialog';
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { IReduxState } from '../../../app/types';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { Avatar } from '../../../base/avatar';
import { isNameReadOnly } from '../../../base/config';
import { translate } from '../../../base/i18n';
import { IconArrowDown, IconArrowUp, IconPhoneRinging, IconVolumeOff } from '../../../base/icons';
import { isVideoMutedByUser } from '../../../base/media';
import { getLocalParticipant } from '../../../base/participants';
import { ActionButton, InputField, PreMeetingScreen } from '../../../base/premeeting';
import { connect } from '../../../base/redux';
import { getDisplayName, updateSettings } from '../../../base/settings';
import { getLocalJitsiVideoTrack } from '../../../base/tracks';
import { isNameReadOnly } from '../../../base/config/functions.web';
import { translate } from '../../../base/i18n/functions';
import { IconArrowDown, IconArrowUp, IconPhoneRinging, IconVolumeOff } from '../../../base/icons/svg';
import { isVideoMutedByUser } from '../../../base/media/functions';
import { getLocalParticipant } from '../../../base/participants/functions';
import ActionButton from '../../../base/premeeting/components/web/ActionButton';
import PreMeetingScreen from '../../../base/premeeting/components/web/PreMeetingScreen';
import { connect } from '../../../base/redux/functions';
import { updateSettings } from '../../../base/settings/actions';
import { getDisplayName } from '../../../base/settings/functions.web';
import { getLocalJitsiVideoTrack } from '../../../base/tracks/functions.web';
import Button from '../../../base/ui/components/web/Button';
import Input from '../../../base/ui/components/web/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import {
joinConference as joinConferenceAction,
joinConferenceWithoutAudio as joinConferenceWithoutAudioAction,
setJoinByPhoneDialogVisiblity as setJoinByPhoneDialogVisiblityAction
} from '../../actions';
} from '../../actions.web';
import {
isDeviceStatusVisible,
isDisplayNameRequired,
@@ -28,114 +33,112 @@ import {
isPrejoinDisplayNameVisible
} from '../../functions';
// @ts-ignore
import JoinByPhoneDialog from './dialogs/JoinByPhoneDialog';
type Props = {
interface IProps extends WithTranslation {
/**
* Indicates whether the display name is editable.
*/
canEditDisplayName: boolean,
canEditDisplayName: boolean;
/**
* Flag signaling if the device status is visible or not.
*/
deviceStatusVisible: boolean,
deviceStatusVisible: boolean;
/**
* If join by phone button should be visible.
*/
hasJoinByPhoneButton: boolean,
hasJoinByPhoneButton: boolean;
/**
* Joins the current meeting.
*/
joinConference: Function,
joinConference: Function;
/**
* Joins the current meeting without audio.
*/
joinConferenceWithoutAudio: Function,
joinConferenceWithoutAudio: Function;
/**
* Whether conference join is in progress.
*/
joiningInProgress: boolean,
joiningInProgress: boolean;
/**
* The name of the user that is about to join.
*/
name: string,
/**
* Updates settings.
*/
updateSettings: Function,
name: string;
/**
* Local participant id.
*/
participantId: string,
participantId: string;
/**
* The prejoin config.
*/
prejoinConfig?: Object,
prejoinConfig?: any;
/**
* Whether the name input should be read only or not.
*/
readOnlyName: boolean,
readOnlyName: boolean;
/**
* Sets visibility of the 'JoinByPhoneDialog'.
*/
setJoinByPhoneDialogVisiblity: Function,
setJoinByPhoneDialogVisiblity: Function;
/**
* Flag signaling the visibility of camera preview.
*/
showCameraPreview: boolean,
/**
* If should show an error when joining without a name.
*/
showErrorOnJoin: boolean,
showCameraPreview: boolean;
/**
* If 'JoinByPhoneDialog' is visible or not.
*/
showDialog: boolean,
showDialog: boolean;
/**
* Used for translation.
* If should show an error when joining without a name.
*/
t: Function,
showErrorOnJoin: boolean;
/**
* Updates settings.
*/
updateSettings: Function;
/**
* The JitsiLocalTrack to display.
*/
videoTrack: ?Object
};
videoTrack?: Object;
}
type State = {
interface IState {
/**
* Flag controlling the visibility of the 'join by phone' buttons.
*/
showJoinByPhoneButtons: boolean
showJoinByPhoneButtons: boolean;
}
/**
* This component is displayed before joining a meeting.
*/
class Prejoin extends Component<Props, State> {
class Prejoin extends Component<IProps, IState> {
showDisplayNameField: boolean;
/**
* Initializes a new {@code Prejoin} instance.
*
* @inheritdoc
*/
constructor(props) {
constructor(props: IProps) {
super(props);
this.state = {
@@ -150,12 +153,11 @@ class Prejoin extends Component<Props, State> {
this._setName = this._setName.bind(this);
this._onJoinConferenceWithoutAudioKeyPress = this._onJoinConferenceWithoutAudioKeyPress.bind(this);
this._showDialogKeyPress = this._showDialogKeyPress.bind(this);
this._onJoinKeyPress = this._onJoinKeyPress.bind(this);
this._getExtraJoinButtons = this._getExtraJoinButtons.bind(this);
this._onInputKeyPress = this._onInputKeyPress.bind(this);
this.showDisplayNameField = props.canEditDisplayName || props.showErrorOnJoin;
}
_onJoinButtonClick: () => void;
/**
* Handler for the join button.
@@ -170,24 +172,6 @@ class Prejoin extends Component<Props, State> {
this.props.joinConference();
}
_onJoinKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
* @param {Object} e - The key event to handle.
*
* @returns {void}
*/
_onJoinKeyPress(e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._onJoinButtonClick();
}
}
_onDropdownClose: () => void;
/**
* Closes the dropdown.
*
@@ -199,38 +183,32 @@ class Prejoin extends Component<Props, State> {
});
}
_onOptionsClick: () => void;
/**
* Displays the join by phone buttons dropdown.
*
* @param {Object} e - The synthetic event.
* @returns {void}
*/
_onOptionsClick(e) {
e.stopPropagation();
_onOptionsClick(e?: React.KeyboardEvent | React.MouseEvent | undefined) {
e?.stopPropagation();
this.setState({
showJoinByPhoneButtons: !this.state.showJoinByPhoneButtons
});
}
_setName: () => void;
/**
* Sets the guest participant name.
*
* @param {string} displayName - Participant name.
* @returns {void}
*/
_setName(displayName) {
_setName(displayName: string) {
this.props.updateSettings({
displayName
});
}
_closeDialog: () => void;
/**
* Closes the join by phone dialog.
*
@@ -240,8 +218,6 @@ class Prejoin extends Component<Props, State> {
this.props.setJoinByPhoneDialogVisiblity(false);
}
_showDialog: () => void;
/**
* Displays the dialog for joining a meeting by phone.
*
@@ -252,8 +228,6 @@ class Prejoin extends Component<Props, State> {
this._onDropdownClose();
}
_showDialogKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
@@ -261,15 +235,13 @@ class Prejoin extends Component<Props, State> {
*
* @returns {void}
*/
_showDialogKeyPress(e) {
_showDialogKeyPress(e: React.KeyboardEvent) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this._showDialog();
}
}
_onJoinConferenceWithoutAudioKeyPress: (Object) => void;
/**
* KeyPress handler for accessibility.
*
@@ -277,7 +249,7 @@ class Prejoin extends Component<Props, State> {
*
* @returns {void}
*/
_onJoinConferenceWithoutAudioKeyPress(e) {
_onJoinConferenceWithoutAudioKeyPress(e: React.KeyboardEvent) {
if (this.props.joinConferenceWithoutAudio
&& (e.key === ' '
|| e.key === 'Enter')) {
@@ -286,8 +258,6 @@ class Prejoin extends Component<Props, State> {
}
}
_getExtraJoinButtons: () => Object;
/**
* Gets the list of extra join buttons.
*
@@ -320,6 +290,20 @@ class Prejoin extends Component<Props, State> {
};
}
/**
* Handle keypress on input.
*
* @param {KeyboardEvent} e - Keyboard event.
* @returns {void}
*/
_onInputKeyPress(e: React.KeyboardEvent) {
const { joinConference } = this.props;
if (e.key === 'Enter') {
joinConference();
}
}
/**
* Implements React's {@link Component#render()}.
*
@@ -330,7 +314,6 @@ class Prejoin extends Component<Props, State> {
const {
deviceStatusVisible,
hasJoinByPhoneButton,
joinConference,
joinConferenceWithoutAudio,
joiningInProgress,
name,
@@ -343,16 +326,16 @@ class Prejoin extends Component<Props, State> {
t,
videoTrack
} = this.props;
const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onJoinKeyPress,
_onOptionsClick, _setName } = this;
const { _closeDialog, _onDropdownClose, _onJoinButtonClick,
_onOptionsClick, _setName, _onInputKeyPress } = this;
const extraJoinButtons = this._getExtraJoinButtons();
let extraButtonsToRender = Object.values(extraJoinButtons).filter((val: Object) =>
let extraButtonsToRender = Object.values(extraJoinButtons).filter((val: any) =>
!(prejoinConfig?.hideExtraJoinButtons || []).includes(val.key)
);
if (!hasJoinByPhoneButton) {
extraButtonsToRender = extraButtonsToRender.filter((btn: Object) => btn.key !== 'by-phone');
extraButtonsToRender = extraButtonsToRender.filter((btn: any) => btn.key !== 'by-phone');
}
const hasExtraJoinButtons = Boolean(extraButtonsToRender.length);
const { showJoinByPhoneButtons } = this.state;
@@ -366,14 +349,14 @@ class Prejoin extends Component<Props, State> {
<div
className = 'prejoin-input-area'
data-testid = 'prejoin.screen'>
{this.showDisplayNameField ? (<InputField
{this.showDisplayNameField ? (<Input
autoComplete = { 'name' }
autoFocus = { true }
className = { showErrorOnJoin ? 'error' : '' }
hasError = { showErrorOnJoin }
className = 'prejoin-input'
error = { showErrorOnJoin }
onChange = { _setName }
onSubmit = { joinConference }
placeHolder = { t('dialog.enterDisplayName') }
onKeyPress = { _onInputKeyPress }
placeholder = { t('dialog.enterDisplayName') }
readOnly = { readOnlyName }
value = { name } />
) : (
@@ -394,7 +377,7 @@ class Prejoin extends Component<Props, State> {
<div className = 'prejoin-preview-dropdown-container'>
<InlineDialog
content = { hasExtraJoinButtons && <div className = 'prejoin-preview-dropdown-btns'>
{extraButtonsToRender.map(({ key, ...rest }: Object) => (
{extraButtonsToRender.map(({ key, ...rest }) => (
<Button
disabled = { joiningInProgress }
fullWidth = { true }
@@ -413,7 +396,6 @@ class Prejoin extends Component<Props, State> {
disabled = { joiningInProgress }
hasOptions = { hasExtraJoinButtons }
onClick = { _onJoinButtonClick }
onKeyPress = { _onJoinKeyPress }
onOptionsClick = { _onOptionsClick }
role = 'button'
tabIndex = { 0 }
@@ -440,10 +422,10 @@ class Prejoin extends Component<Props, State> {
* @param {Object} state - The redux state.
* @returns {Object}
*/
function mapStateToProps(state): Object {
function mapStateToProps(state: IReduxState) {
const name = getDisplayName(state);
const showErrorOnJoin = isDisplayNameRequired(state) && !name;
const { id: participantId } = getLocalParticipant(state);
const { id: participantId } = getLocalParticipant(state) ?? {};
const { joiningInProgress } = state['features/prejoin'];
return {

View File

@@ -4,9 +4,8 @@ import { makeStyles } from 'tss-react/mui';
import { IReduxState } from '../../../../app/types';
import { translate } from '../../../../base/i18n/functions';
import Icon from '../../../../base/icons/components/Icon';
import { IconCheck, IconExclamationTriangle } from '../../../../base/icons/svg';
import { connect } from '../../../../base/redux/functions';
import { withPixelLineHeight } from '../../../../base/styles/functions.web';
import {
getDeviceStatusText,
getDeviceStatusType
@@ -29,13 +28,12 @@ export interface IProps extends WithTranslation {
const useStyles = makeStyles()(theme => {
return {
deviceStatus: {
alignItems: 'center',
color: '#fff',
display: 'flex',
fontSize: '14px',
lineHeight: '20px',
padding: '6px',
textAlign: 'center',
alignItems: 'center',
justifyContent: 'center',
...withPixelLineHeight(theme.typography.bodyShortRegular),
color: '#fff',
marginTop: theme.spacing(4),
'& span': {
marginLeft: theme.spacing(3)
@@ -47,33 +45,23 @@ const useStyles = makeStyles()(theme => {
borderRadius: '6px',
color: theme.palette.uiBackground,
padding: '12px 16px',
textAlign: 'left'
textAlign: 'left',
marginTop: theme.spacing(2)
},
'& .device-icon': {
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
display: 'inline-block',
height: '16px',
width: '16px'
},
'& .device-icon--ok svg path': {
fill: '#189B55'
'@media (max-width: 720px)': {
marginTop: 0
}
},
indicator: {
width: '16px',
height: '16px',
borderRadius: '100%',
backgroundColor: theme.palette.success01
}
};
});
const iconMap = {
warning: {
src: IconExclamationTriangle,
className: 'device-icon--warning'
},
ok: {
src: IconCheck,
className: 'device-icon--ok'
}
};
/**
* Strip showing the current status of the devices.
* User is informed if there are missing or malfunctioning devices.
@@ -82,7 +70,6 @@ const iconMap = {
*/
function DeviceStatus({ deviceStatusType, deviceStatusText, t }: IProps) {
const { classes, cx } = useStyles();
const { src, className } = iconMap[deviceStatusType as keyof typeof iconMap];
const hasError = deviceStatusType === 'warning';
const containerClassName = cx(classes.deviceStatus, { 'device-status-error': hasError });
@@ -91,10 +78,7 @@ function DeviceStatus({ deviceStatusType, deviceStatusText, t }: IProps) {
className = { containerClassName }
role = 'alert'
tabIndex = { -1 }>
<Icon
className = { `device-icon ${className}` }
size = { 16 }
src = { src } />
{!hasError && <div className = { classes.indicator } />}
<span role = 'heading'>
{hasError ? t('prejoin.errorNoPermissions') : t(deviceStatusText ?? '')}
</span>

View File

@@ -236,10 +236,11 @@ class ReactionsMenu extends Component<IProps> {
*/
function mapStateToProps(state: IReduxState) {
const localParticipant = getLocalParticipant(state);
const { isNarrowLayout } = state['features/base/responsive-ui'];
return {
_localParticipantID: localParticipant?.id,
_isMobile: isMobileBrowser(),
_isMobile: isMobileBrowser() || isNarrowLayout,
_isGifEnabled: isGifEnabled(state),
_isGifMenuVisible: isGifsMenuOpen(state),
_raisedHand: hasRaisedHand(localParticipant)

View File

@@ -43,9 +43,9 @@ interface IProps extends WithTranslation {
handleClick: Function;
/**
* Whether or not it's a mobile browser.
* Whether or not it's narrow mode or mobile browser.
*/
isMobile: boolean;
isNarrow: boolean;
/**
* Whether or not the reactions menu is open.
@@ -75,7 +75,7 @@ function ReactionsMenuButton({
dispatch,
handleClick,
isOpen,
isMobile,
isNarrow,
notifyMode,
reactionsQueue,
t
@@ -95,7 +95,7 @@ function ReactionsMenuButton({
return (
<div className = 'reactions-menu-popup-container'>
{!_reactionsEnabled || isMobile ? (
{!_reactionsEnabled || isNarrow ? (
<RaiseHandButton
buttonKey = { buttonKey }
handleClick = { handleClick }
@@ -135,10 +135,12 @@ function ReactionsMenuButton({
* @returns {Object}
*/
function mapStateToProps(state: IReduxState) {
const { isNarrowLayout } = state['features/base/responsive-ui'];
return {
_reactionsEnabled: isReactionsEnabled(state),
isOpen: getReactionsMenuVisibility(state),
isMobile: isMobileBrowser(),
isNarrow: isMobileBrowser() || isNarrowLayout,
reactionsQueue: getReactionsQueue(state)
};
}

View File

@@ -155,12 +155,13 @@ class AudioSettingsButton extends Component<Props> {
*/
function mapStateToProps(state) {
const { permissions = {} } = state['features/base/devices'];
const { isNarrowLayout } = state['features/base/responsive-ui'];
return {
hasPermissions: permissions.audio,
isDisabled: isAudioSettingsButtonDisabled(state),
isOpen: getAudioSettingsVisibility(state),
visible: !isMobileBrowser()
visible: !isMobileBrowser() && !isNarrowLayout
};
}

View File

@@ -5,13 +5,13 @@ import { makeStyles } from 'tss-react/mui';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import Popover from '../../../base/popover/components/Popover.web';
// @ts-ignore
import { ReactionEmoji, ReactionsMenu } from '../../../reactions/components';
import { REACTIONS_MENU_HEIGHT } from '../../../reactions/constants';
import { getReactionsQueue } from '../../../reactions/functions.any';
import { DRAWER_MAX_HEIGHT } from '../../constants';
import { showOverflowDrawer } from '../../functions.web';
// @ts-ignore
import Drawer from './Drawer';
@@ -67,7 +67,7 @@ const OverflowMenuButton = ({
showMobileReactions
}: IProps) => {
const { classes } = useStyles();
const overflowDrawer = useSelector((state: IReduxState) => state['features/toolbox'].overflowDrawer);
const overflowDrawer = useSelector(showOverflowDrawer);
const reactionsQueue = useSelector(getReactionsQueue);
const onCloseDialog = useCallback(() => {

View File

@@ -231,6 +231,11 @@ interface IProps extends WithTranslation {
*/
_isMobile: boolean;
/**
* Whether we are in narrow layout mode.
*/
_isNarrowLayout: boolean;
/**
* Whether or not the profile is disabled.
*/
@@ -348,9 +353,9 @@ const styles = () => {
contextMenu: {
position: 'relative' as const,
right: 'auto',
maxHeight: 'inherit',
margin: 0,
marginBottom: '8px'
marginBottom: '8px',
maxHeight: 'calc(100vh - 100px)'
},
hangupMenu: {
@@ -710,8 +715,10 @@ class Toolbox extends Component<IProps> {
_hasSalesforce,
_isIosMobile,
_isMobile,
_isNarrowLayout,
_isSpeakerStatsDisabled,
_multiStreamModeEnabled,
_reactionsEnabled,
_screenSharing,
_whiteboardEnabled
} = this.props;
@@ -748,7 +755,7 @@ class Toolbox extends Component<IProps> {
group: 2
};
const raisehand = {
const raisehand = (!_reactionsEnabled || (!_isNarrowLayout && !_isMobile)) && {
key: 'raisehand',
Content: ReactionsMenuButton,
handleClick: this._onToolbarToggleRaiseHand,
@@ -1387,6 +1394,7 @@ class Toolbox extends Component<IProps> {
_endConferenceSupported,
_hangupMenuVisible,
_isMobile,
_isNarrowLayout,
_overflowDrawer,
_overflowMenuVisible,
_reactionsEnabled,
@@ -1396,7 +1404,7 @@ class Toolbox extends Component<IProps> {
} = this.props;
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const containerClassName = `toolbox-content${_isMobile ? ' toolbox-content-mobile' : ''}`;
const containerClassName = `toolbox-content${_isMobile || _isNarrowLayout ? ' toolbox-content-mobile' : ''}`;
const { mainMenuButtons, overflowMenuButtons } = this._getVisibleButtons();
@@ -1424,7 +1432,7 @@ class Toolbox extends Component<IProps> {
key = 'overflow-menu'
onVisibilityChange = { this._onSetOverflowVisible }
showMobileReactions = {
_reactionsEnabled && overflowMenuButtons.find(({ key }) => key === 'raisehand')
_reactionsEnabled && (_isMobile || _isNarrowLayout)
}>
<ContextMenu
accessibilityLabel = { t(toolbarAccLabel) }
@@ -1509,6 +1517,7 @@ class Toolbox extends Component<IProps> {
*/
function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
const { conference } = state['features/base/conference'];
const { isNarrowLayout } = state['features/base/responsive-ui'];
const endConferenceSupported = conference?.isEndConferenceSupported();
const {
@@ -1551,6 +1560,7 @@ function _mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
_jwtDisabledButons: getJwtDisabledButtons(state),
_hasSalesforce: isSalesforceEnabled(state),
_hangupMenuVisible: hangupMenuVisible,
_isNarrowLayout: isNarrowLayout,
_localParticipantID: localParticipant?.id,
_localVideo: localVideo,
_multiStreamModeEnabled: getMultipleVideoSendingSupportFeatureFlag(state),

View File

@@ -170,13 +170,14 @@ class VideoSettingsButton extends Component<Props> {
*/
function mapStateToProps(state) {
const { permissions = {} } = state['features/base/devices'];
const { isNarrowLayout } = state['features/base/responsive-ui'];
return {
hasPermissions: permissions.video,
hasVideoTrack: Boolean(getLocalJitsiVideoTrack(state)),
isDisabled: isVideoSettingsButtonDisabled(state),
isOpen: getVideoSettingsVisibility(state),
visible: !isMobileBrowser()
visible: !isMobileBrowser() && !isNarrowLayout
};
}

View File

@@ -221,7 +221,7 @@ class LocalVideoMenuTriggerButton extends Component<IProps> {
overflowDrawer = { _overflowDrawer }
position = { _menuPosition }
visible = { popoverVisible }>
{!_overflowDrawer && buttonVisible && !isMobileBrowser() && (
{buttonVisible && !isMobileBrowser() && (
<Button
accessibilityLabel = { t('dialog.localUserControls') }
className = { classes.triggerButton }

View File

@@ -192,9 +192,10 @@ class RemoteVideoMenuTriggerButton extends Component<IProps> {
id = 'remote-video-menu-trigger'
onPopoverClose = { this._onPopoverClose }
onPopoverOpen = { this._onPopoverOpen }
overflowDrawer = { _overflowDrawer }
position = { this.props._menuPosition }
visible = { popoverVisible }>
{!_overflowDrawer && buttonVisible && !_disabled && (
{ buttonVisible && !_disabled && (
!isMobileBrowser() && <Button
accessibilityLabel = { this.props.t('dialog.remoteUserControls', { username }) }
className = { classes.triggerButton }

View File

@@ -1,9 +1,13 @@
import debounce from 'lodash/debounce';
import { IStore } from '../app/types';
import { IReduxState, IStore } from '../app/types';
import { _handleParticipantError } from '../base/conference/functions';
import { getSsrcRewritingFeatureFlag } from '../base/config/functions.any';
import { MEDIA_TYPE } from '../base/media/constants';
import { getLocalParticipant } from '../base/participants/functions';
import {
getLocalParticipant,
getSourceNamesByMediaType
} from '../base/participants/functions';
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { getTrackSourceNameByMediaTypeAndParticipant } from '../base/tracks/functions';
import { reportError } from '../base/util/helpers';
@@ -94,6 +98,15 @@ StateListenerRegistry.register(
}
);
/**
* Updates the receiver constraints when new video sources are added to the conference.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/participants'].remoteVideoSources,
/* listener */ (remoteVideoSources, store) => {
getSsrcRewritingFeatureFlag(store.getState()) && _updateReceiverVideoConstraints(store);
});
/**
* StateListenerRegistry provides a reliable way of detecting changes to
* maxReceiverVideoQuality* and preferredVideoQuality state and dispatching additional actions.
@@ -294,6 +307,42 @@ StateListenerRegistry.register(
deepEquals: true
});
/**
* Returns the source names asociated with the given participants list.
*
* @param {Array<string>} participantList - The list of participants.
* @param {Object} state - The redux state.
* @returns {Array<string>}
*/
function _getSourceNames(participantList: Array<string>, state: IReduxState): Array<string> {
const { remoteScreenShares } = state['features/video-layout'];
const tracks = state['features/base/tracks'];
const sourceNamesList: string[] = [];
participantList.forEach(participantId => {
if (getSsrcRewritingFeatureFlag(state)) {
const sourceNames: string[] | undefined
= getSourceNamesByMediaType(state, participantId, MEDIA_TYPE.VIDEO);
sourceNames?.length && sourceNamesList.push(...sourceNames);
} else {
let sourceName: string;
if (remoteScreenShares.includes(participantId)) {
sourceName = participantId;
} else {
sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
}
if (sourceName) {
sourceNamesList.push(sourceName);
}
}
});
return sourceNamesList;
}
/**
* Helper function for updating the preferred sender video constraint, based on the user preference.
*
@@ -360,53 +409,29 @@ function _updateReceiverVideoConstraints({ getState }: IStore) {
lastN
};
const activeParticipantsSources: string[] = [];
const visibleRemoteTrackSourceNames: string[] = [];
let activeParticipantsSources: string[] = [];
let visibleRemoteTrackSourceNames: string[] = [];
let largeVideoSourceName: string | undefined;
receiverConstraints.onStageSources = [];
receiverConstraints.selectedSources = [];
if (visibleRemoteParticipants?.size) {
visibleRemoteParticipants.forEach(participantId => {
let sourceName;
if (remoteScreenShares.includes(participantId)) {
sourceName = participantId;
} else {
sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
}
if (sourceName) {
visibleRemoteTrackSourceNames.push(sourceName);
}
});
visibleRemoteTrackSourceNames = _getSourceNames(Array.from(visibleRemoteParticipants), state);
}
if (activeParticipantsIds?.length > 0) {
activeParticipantsIds.forEach((participantId: string) => {
let sourceName;
if (remoteScreenShares.includes(participantId)) {
sourceName = participantId;
} else {
sourceName = getTrackSourceNameByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, participantId);
}
if (sourceName) {
activeParticipantsSources.push(sourceName);
}
});
activeParticipantsSources = _getSourceNames(activeParticipantsIds, state);
}
if (localParticipantId !== largeVideoParticipantId) {
if (remoteScreenShares.includes(largeVideoParticipantId)) {
largeVideoSourceName = largeVideoParticipantId;
} else {
largeVideoSourceName = getTrackSourceNameByMediaTypeAndParticipant(
tracks, MEDIA_TYPE.VIDEO, largeVideoParticipantId
);
largeVideoSourceName = getSsrcRewritingFeatureFlag(state)
? getSourceNamesByMediaType(state, largeVideoParticipantId, MEDIA_TYPE.VIDEO)?.[0]
: getTrackSourceNameByMediaTypeAndParticipant(
tracks, MEDIA_TYPE.VIDEO, largeVideoParticipantId);
}
}