mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-09-10 07:48:39 +00:00
Compare commits
18 Commits
6933
...
hmuresan/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2eabf537d7 | ||
|
|
0d5dae7ab9 | ||
|
|
992bf47850 | ||
|
|
4f34a576d0 | ||
|
|
921f3ee8cd | ||
|
|
42838e756c | ||
|
|
e6eba3536a | ||
|
|
74bdb7bc3f | ||
|
|
192187db32 | ||
|
|
5dce74e21d | ||
|
|
fd7b6f457e | ||
|
|
2bf3089dbf | ||
|
|
d396727e39 | ||
|
|
3e58cd8af3 | ||
|
|
cf7e692186 | ||
|
|
5e90e72562 | ||
|
|
cbae997eda | ||
|
|
6bd9a6bd15 |
3
.github/workflows/ci-lua.yml
vendored
3
.github/workflows/ci-lua.yml
vendored
@@ -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
8
.luacheckrc
Normal file
@@ -0,0 +1,8 @@
|
||||
global = false
|
||||
unused = false
|
||||
redefined = false
|
||||
ignore = { "581" }
|
||||
max_line_length = false
|
||||
color = false
|
||||
formatter = "plain"
|
||||
quiet = 1
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
350
css/_polls.scss
350
css/_polls.scss
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
2
modules/API/external/external_api.js
vendored
2
modules/API/external/external_api.js
vendored
@@ -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;
|
||||
|
||||
|
||||
@@ -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
60
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -105,7 +105,8 @@ export function commonUserJoinedHandling(
|
||||
name: displayName,
|
||||
presence: user.getStatus(),
|
||||
role: user.getRole(),
|
||||
isReplacing
|
||||
isReplacing,
|
||||
sources: user.getSources()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ export interface IJitsiConference {
|
||||
sendFaceLandmarks: (faceLandmarks: FaceLandmarks) => void;
|
||||
sendFeedback: Function;
|
||||
sendLobbyMessage: Function;
|
||||
sendMessage: Function;
|
||||
sessionId: string;
|
||||
setDesktopSharingFrameRate: Function;
|
||||
setDisplayName: Function;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import './native';
|
||||
|
||||
// Re-export JitsiMeetJS from the library lib-jitsi-meet to (the other features
|
||||
// of) the project jitsi-meet.
|
||||
//
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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)) ?? ''
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 } />
|
||||
)}
|
||||
|
||||
@@ -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)'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -26,7 +26,8 @@ const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
messageGroup: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
flexDirection: 'column',
|
||||
maxWidth: '100%'
|
||||
},
|
||||
|
||||
groupContainer: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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();
|
||||
}, []);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) } />}
|
||||
|
||||
@@ -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 }
|
||||
@@ -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 });
|
||||
@@ -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);
|
||||
|
||||
@@ -35,7 +35,7 @@ export const dialogStyles = createStyleSheet({
|
||||
},
|
||||
|
||||
optionRemoveButtonText: {
|
||||
color: BaseTheme.palette.actionDangerActive
|
||||
color: BaseTheme.palette.link01
|
||||
},
|
||||
|
||||
field: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>);
|
||||
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -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 (
|
||||
@@ -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);
|
||||
177
react/features/polls/components/web/PollResults.tsx
Normal file
177
react/features/polls/components/web/PollResults.tsx
Normal 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);
|
||||
@@ -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;
|
||||
89
react/features/polls/components/web/PollsList.tsx
Normal file
89
react/features/polls/components/web/PollsList.tsx
Normal 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;
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 {
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user