Revert "Group Calls: Redesign (#2859)"
This reverts commit 8fc05e97ff70a23ff7692e399cb305c765b00b60.
This commit is contained in:
parent
7c17c02801
commit
cb8b2bc018
Binary file not shown.
Binary file not shown.
@ -1,296 +0,0 @@
|
|||||||
.root {
|
|
||||||
--group-call-panel-color: #212121;
|
|
||||||
--group-call-panel-header-border-color: #3b3b3b;
|
|
||||||
--group-call-background-color: #000000;
|
|
||||||
--green-button-color: rgba(1, 200, 80, 0.3);
|
|
||||||
--blue-button-color: rgb(60, 135, 247, 0.2);
|
|
||||||
--purple-button-color: rgb(61, 82, 223, 0.2);
|
|
||||||
--gradient-blue: linear-gradient(225deg, #4EABF8 14.73%, #3478F6 85.27%);
|
|
||||||
--gradient-green: linear-gradient(230.46deg, #00A3B4 12.94%, #00CB47 86.29%);
|
|
||||||
--gradient-purple: linear-gradient(230.46deg, #CE4D74 0%, #3D52DF 100%);
|
|
||||||
--gradient-speaking: linear-gradient(135deg, #5CC85C 0%, #48A1B3 101.27%);
|
|
||||||
|
|
||||||
--red-button-color: rgba(255, 89, 90, 0.3);
|
|
||||||
--disabled-button-color: #333333;
|
|
||||||
--color-text-secondary: #AAAAAA;
|
|
||||||
--color-text: #FFFFFF;
|
|
||||||
|
|
||||||
--default-width: 26.25rem;
|
|
||||||
--max-height: 40rem;
|
|
||||||
|
|
||||||
color: var(--color-text);
|
|
||||||
|
|
||||||
:global {
|
|
||||||
.modal-dialog {
|
|
||||||
max-width: var(--default-width);
|
|
||||||
max-height: min(var(--max-height), 100vh);
|
|
||||||
height: 100%;
|
|
||||||
min-height: min(80vh, var(--max-height));
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--group-call-background-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
min-height: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.panelWrapper {
|
|
||||||
max-width: var(--default-width);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
|
||||||
background: var(--group-call-panel-color);
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
overflow: auto;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panelScrollTrigger {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panelHeader {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
|
|
||||||
border-bottom: 0.0625rem solid transparent;
|
|
||||||
|
|
||||||
padding: 0.375rem 0.875rem;
|
|
||||||
user-select: none;
|
|
||||||
z-index: 1;
|
|
||||||
background: var(--group-call-panel-color);
|
|
||||||
|
|
||||||
transition: 0.25s ease-in-out border-bottom-color;
|
|
||||||
|
|
||||||
&.scrolled {
|
|
||||||
border-bottom-color: var(--group-call-panel-header-border-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerButton {
|
|
||||||
color: var(--color-text) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.firstButton {
|
|
||||||
margin-right: 1.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lastButton {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panelHeaderText {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
line-height: 1.375rem;
|
|
||||||
white-space: pre;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
unicode-bidi: plaintext;
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bigger {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.125rem;
|
|
||||||
margin: 0;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participants {
|
|
||||||
position: relative;
|
|
||||||
margin: 0.125rem 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participantVideos {
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addParticipantButton {
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videos {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
width: calc(100% - var(--default-width));
|
|
||||||
}
|
|
||||||
|
|
||||||
.videosHeader {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.375rem 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videosHeaderLastButton {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videosContent {
|
|
||||||
flex-grow: 1;
|
|
||||||
margin: 0.5rem 0.625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
--actions-max-width: 0px;
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(calc(-50% - var(--actions-max-width) / 2));
|
|
||||||
bottom: 1.75rem;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
gap: 1.25rem;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.actionButton {
|
|
||||||
width: 3.375rem !important;
|
|
||||||
height: 3.375rem !important;
|
|
||||||
color: var(--color-text) !important;
|
|
||||||
background-color: var(--green-button-color) !important;
|
|
||||||
transition: 0.15s filter, 0.25s ease-out background-color;
|
|
||||||
backdrop-filter: blur(25px);
|
|
||||||
|
|
||||||
&:global(.disabled) {
|
|
||||||
background: var(--disabled-button-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
filter: brightness(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.destructive {
|
|
||||||
background: var(--red-button-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canRequestToSpeak {
|
|
||||||
background: var(--purple-button-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.muted {
|
|
||||||
background: var(--blue-button-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fullscreen {
|
|
||||||
:global {
|
|
||||||
.modal-dialog {
|
|
||||||
min-width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
max-height: initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.portrait .panelWrapper {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.landscape {
|
|
||||||
.panelWrapper {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.noVideoParticipants) {
|
|
||||||
.actions {
|
|
||||||
--actions-max-width: var(--default-width);
|
|
||||||
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
backdrop-filter: blur(50px);
|
|
||||||
border-radius: 1.25rem;
|
|
||||||
padding: 0.75rem;
|
|
||||||
bottom: 2.5rem;
|
|
||||||
transition: var(--layer-transition) transform, 250ms ease-in-out opacity;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videos:hover ~ .actions, .video:hover ~ .actions, .actions:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.noVideoParticipants {
|
|
||||||
.panelWrapper {
|
|
||||||
max-width: max(50vw, 30rem);
|
|
||||||
width: 100%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
left: 50%;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.modal-content) {
|
|
||||||
background: var(--group-call-panel-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.portrait {
|
|
||||||
.panelWrapper::after {
|
|
||||||
display: block;
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
height: 7.5rem;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
background: linear-gradient(180deg, rgba(33, 33, 33, 0) 0%, rgba(33, 33, 33, 0.65) 48.54%, #212121 100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.noSidebar {
|
|
||||||
.panelWrapper {
|
|
||||||
transform: translate3d(100%, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.videos {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
--actions-max-width: 0px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
345
src/components/calls/group/GroupCall.scss
Normal file
345
src/components/calls/group/GroupCall.scss
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
.GroupCall {
|
||||||
|
.modal-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 37.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
max-height: calc(100% - 4rem);
|
||||||
|
background: #181f27;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Menu {
|
||||||
|
--color-text: white;
|
||||||
|
--color-background-compact-menu: #212121DD;
|
||||||
|
--color-background-compact-menu-hover: #00000066;
|
||||||
|
.bubble {
|
||||||
|
box-shadow: 0 0.25rem 0.5rem 0.125rem rgba(16, 16, 16, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.single-column {
|
||||||
|
opacity: 1 !important;
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
max-width: 100% !important;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: 0;
|
||||||
|
transform: translate3d(0, 100%, 0);
|
||||||
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
.modal-backdrop {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0 auto 0 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.videos {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participants {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
background: #222b34;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
|
||||||
|
.Loading {
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-btn {
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
transition: 0.15s ease-out background-color;
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2f363e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable {
|
||||||
|
overflow: auto;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
max-width: 37.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
max-width: 37.5rem;
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
height: 8.75rem;
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
width: 100%;
|
||||||
|
height: 2rem;
|
||||||
|
background: linear-gradient(0deg, #181f27, rgba(24, 31, 39, 0));
|
||||||
|
z-index: 0;
|
||||||
|
top: -2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper {
|
||||||
|
width: 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.button-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.microphone-wrapper {
|
||||||
|
width: 6rem;
|
||||||
|
|
||||||
|
.button-text {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Loading {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(0, -1.125rem);
|
||||||
|
|
||||||
|
.Spinner {
|
||||||
|
--spinner-size: 6.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-button,
|
||||||
|
.smaller-button {
|
||||||
|
outline: none;
|
||||||
|
border: 0;
|
||||||
|
background: #15415b;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.375rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: 0.25s ease-out background-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #11364b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-button.camera.active {
|
||||||
|
background: #15415b;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #11364b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-button.speaker {
|
||||||
|
background: #2b3a51;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #496092;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-button.leave {
|
||||||
|
background: #5a2824;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #49201d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.smaller-button {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.landscape .scrollable {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-grow: 1;
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.landscape .GroupCallParticipantVideo {
|
||||||
|
max-height: initial;
|
||||||
|
|
||||||
|
video {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.landscape .buttons {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(50% - 15.625rem / 2);
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: auto;
|
||||||
|
gap: 1rem;
|
||||||
|
bottom: 4rem;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 1rem;
|
||||||
|
z-index: 5;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
.button-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-buttons {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.smaller-button {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Loading {
|
||||||
|
transform: none;
|
||||||
|
.Spinner {
|
||||||
|
--spinner-size: 3.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.MicrophoneButton {
|
||||||
|
canvas {
|
||||||
|
width: 2rem !important;
|
||||||
|
height: 2rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.MicrophoneButton,
|
||||||
|
.microphone-wrapper {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
|
||||||
|
.AnimatedSticker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.landscape.no-sidebar .buttons {
|
||||||
|
left: calc(50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.landscape .streams {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.landscape .videos {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
--column-count: 1;
|
||||||
|
grid-template-columns: repeat(var(--column-count), 1fr);
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
|
|
||||||
|
.GroupCallParticipantVideo {
|
||||||
|
max-height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.thumbnail-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.span-last-video .GroupCallParticipantVideo:last-child {
|
||||||
|
grid-column: span var(--column-count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.landscape .participants {
|
||||||
|
width: 15.625rem;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,43 +1,51 @@
|
|||||||
|
import type {
|
||||||
|
GroupCallConnectionState, GroupCallParticipant as TypeGroupCallParticipant,
|
||||||
|
} from '../../../lib/secret-sauce';
|
||||||
|
import {
|
||||||
|
IS_SCREENSHARE_SUPPORTED, switchCameraInput, toggleSpeaker,
|
||||||
|
} from '../../../lib/secret-sauce';
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useEffect, useMemo, useRef, useState,
|
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
import '../../../global/actions/calls';
|
import '../../../global/actions/calls';
|
||||||
|
|
||||||
import type {
|
import type { IAnchorPosition } from '../../../types';
|
||||||
GroupCallConnectionState, GroupCallParticipant as TypeGroupCallParticipant,
|
|
||||||
} from '../../../lib/secret-sauce';
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import type { VideoParticipant } from './hooks/useGroupCallVideoLayout';
|
|
||||||
|
|
||||||
import { IS_SCREENSHARE_SUPPORTED } from '../../../lib/secret-sauce';
|
import {
|
||||||
|
IS_ANDROID,
|
||||||
|
IS_IOS,
|
||||||
|
IS_REQUEST_FULLSCREEN_SUPPORTED,
|
||||||
|
} from '../../../util/windowEnvironment';
|
||||||
|
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import {
|
import {
|
||||||
selectCanInviteToActiveGroupCall,
|
|
||||||
selectGroupCall,
|
selectGroupCall,
|
||||||
selectGroupCallParticipant,
|
selectGroupCallParticipant,
|
||||||
selectIsAdminInActiveGroupCall,
|
selectIsAdminInActiveGroupCall,
|
||||||
} from '../../../global/selectors/calls';
|
} from '../../../global/selectors/calls';
|
||||||
import { selectChat, selectTabState } from '../../../global/selectors';
|
import { selectTabState } from '../../../global/selectors';
|
||||||
import { compact } from '../../../util/iteratees';
|
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useAppLayout from '../../../hooks/useAppLayout';
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
import useGroupCallVideoLayout from './hooks/useGroupCallVideoLayout';
|
|
||||||
import { useIntersectionObserver, useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
|
||||||
|
|
||||||
|
import Loading from '../../ui/Loading';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
|
import DropdownMenu from '../../ui/DropdownMenu';
|
||||||
|
import MenuItem from '../../ui/MenuItem';
|
||||||
import Modal from '../../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
import MicrophoneButton from './MicrophoneButton';
|
import MicrophoneButton from './MicrophoneButton';
|
||||||
|
import AnimatedIcon from '../../common/AnimatedIcon';
|
||||||
import Checkbox from '../../ui/Checkbox';
|
import Checkbox from '../../ui/Checkbox';
|
||||||
|
import GroupCallParticipantMenu from './GroupCallParticipantMenu';
|
||||||
import GroupCallParticipantList from './GroupCallParticipantList';
|
import GroupCallParticipantList from './GroupCallParticipantList';
|
||||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
import GroupCallParticipantStreams from './GroupCallParticipantStreams';
|
||||||
import GroupCallParticipantVideo from './GroupCallParticipantVideo';
|
|
||||||
|
|
||||||
import styles from './GroupCall.module.scss';
|
import './GroupCall.scss';
|
||||||
|
|
||||||
const INTERSECTION_THROTTLE = 200;
|
const CAMERA_FLIP_PLAY_SEGMENT: [number, number] = [0, 10];
|
||||||
|
const PARTICIPANT_HEIGHT = 60;
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
groupCallId: string;
|
groupCallId: string;
|
||||||
@ -49,21 +57,21 @@ type StateProps = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
meParticipant?: TypeGroupCallParticipant;
|
meParticipant?: TypeGroupCallParticipant;
|
||||||
participantsCount?: number;
|
participantsCount?: number;
|
||||||
|
isSpeakerEnabled?: boolean;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
participants: Record<string, TypeGroupCallParticipant>;
|
participants: Record<string, TypeGroupCallParticipant>;
|
||||||
canInvite: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const GroupCall: FC<OwnProps & StateProps> = ({
|
const GroupCall: FC<OwnProps & StateProps> = ({
|
||||||
groupCallId,
|
groupCallId,
|
||||||
isCallPanelVisible,
|
isCallPanelVisible,
|
||||||
connectionState,
|
connectionState,
|
||||||
participantsCount,
|
isSpeakerEnabled,
|
||||||
title,
|
title,
|
||||||
meParticipant,
|
meParticipant,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
participants,
|
participants,
|
||||||
canInvite,
|
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
toggleGroupCallVideo,
|
toggleGroupCallVideo,
|
||||||
@ -72,60 +80,24 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
toggleGroupCallPanel,
|
toggleGroupCallPanel,
|
||||||
connectToActiveGroupCall,
|
connectToActiveGroupCall,
|
||||||
playGroupCallSound,
|
playGroupCallSound,
|
||||||
createGroupCallInviteLink,
|
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const primaryVideoContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const secondaryVideoContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const panelScrollTriggerRef = useRef<HTMLDivElement>(null);
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [isLeaving, setIsLeaving] = useState(false);
|
|
||||||
const isOpen = !isCallPanelVisible && !isLeaving;
|
|
||||||
|
|
||||||
const { observe } = useIntersectionObserver({
|
|
||||||
rootRef: panelRef,
|
|
||||||
throttleMs: INTERSECTION_THROTTLE,
|
|
||||||
isDisabled: !isOpen,
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasScrolled = !useIsIntersecting(panelScrollTriggerRef, isOpen ? observe : undefined);
|
|
||||||
|
|
||||||
const { isMobile, isLandscape } = useAppLayout();
|
const { isMobile, isLandscape } = useAppLayout();
|
||||||
|
|
||||||
|
const [isLeaving, setIsLeaving] = useState(false);
|
||||||
const [isFullscreen, openFullscreen, closeFullscreen] = useFlag();
|
const [isFullscreen, openFullscreen, closeFullscreen] = useFlag();
|
||||||
const [isSidebarOpen, openSidebar, closeSidebar] = useFlag(true);
|
const [isSidebarOpen, openSidebar, closeSidebar] = useFlag(true);
|
||||||
const isLandscapeLayout = Boolean(isFullscreen && isLandscape);
|
const hasVideoParticipants = Object.values(participants).some(({ video, presentation }) => video || presentation);
|
||||||
|
const isLandscapeLayout = isFullscreen && (!isMobile || isLandscape) && hasVideoParticipants;
|
||||||
|
|
||||||
const firstPresentation = useMemo(() => {
|
const [participantMenu, setParticipantMenu] = useState<{
|
||||||
return Object.values(participants).find(({ presentation }) => presentation);
|
participant: TypeGroupCallParticipant;
|
||||||
}, [participants]);
|
anchor: IAnchorPosition;
|
||||||
const videoParticipants = useMemo(() => Object.values(participants)
|
} | undefined>();
|
||||||
.filter(({ video, presentation }) => video || presentation)
|
const [isParticipantMenuOpen, openParticipantMenu, closeParticipantMenu] = useFlag();
|
||||||
.flatMap(({ id, video, presentation }) => compact([
|
|
||||||
video ? {
|
|
||||||
id,
|
|
||||||
type: 'video' as const,
|
|
||||||
} : undefined,
|
|
||||||
presentation ? {
|
|
||||||
id,
|
|
||||||
type: 'screen' as const,
|
|
||||||
} : undefined,
|
|
||||||
])),
|
|
||||||
[participants]);
|
|
||||||
const hasVideoParticipants = videoParticipants.length > 0;
|
|
||||||
|
|
||||||
const groupCallTitle = title || lang('VoipGroupVoiceChat');
|
|
||||||
const membersString = lang('Participants', participantsCount, 'i');
|
|
||||||
|
|
||||||
const [isConfirmLeaveModalOpen, openConfirmLeaveModal, closeConfirmLeaveModal] = useFlag();
|
const [isConfirmLeaveModalOpen, openConfirmLeaveModal, closeConfirmLeaveModal] = useFlag();
|
||||||
const [isEndGroupCallModal, setIsEndGroupCallModal] = useState(false);
|
const [isEndGroupCallModal, setIsEndGroupCallModal] = useState(false);
|
||||||
@ -133,10 +105,20 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const hasVideo = meParticipant?.hasVideoStream;
|
const hasVideo = meParticipant?.hasVideoStream;
|
||||||
const hasPresentation = meParticipant?.hasPresentationStream;
|
const hasPresentation = meParticipant?.hasPresentationStream;
|
||||||
const hasAudioStream = meParticipant?.hasAudioStream;
|
|
||||||
const isConnecting = connectionState !== 'connected';
|
const isConnecting = connectionState !== 'connected';
|
||||||
const canSelfUnmute = meParticipant?.canSelfUnmute;
|
const canSelfUnmute = meParticipant?.canSelfUnmute;
|
||||||
const canRequestToSpeak = !canSelfUnmute && !hasAudioStream;
|
const shouldRaiseHand = !canSelfUnmute && meParticipant?.isMuted;
|
||||||
|
const handleOpenParticipantMenu = useCallback((anchor: HTMLDivElement, participant: TypeGroupCallParticipant) => {
|
||||||
|
const rect = anchor.getBoundingClientRect();
|
||||||
|
const container = containerRef.current!;
|
||||||
|
|
||||||
|
setParticipantMenu({
|
||||||
|
anchor: { x: rect.left, y: rect.top - container.offsetTop + PARTICIPANT_HEIGHT },
|
||||||
|
participant,
|
||||||
|
});
|
||||||
|
|
||||||
|
openParticipantMenu();
|
||||||
|
}, [openParticipantMenu]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (connectionState === 'connected') {
|
if (connectionState === 'connected') {
|
||||||
@ -144,360 +126,263 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
} else if (connectionState === 'reconnecting') {
|
} else if (connectionState === 'reconnecting') {
|
||||||
playGroupCallSound({ sound: 'connecting' });
|
playGroupCallSound({ sound: 'connecting' });
|
||||||
}
|
}
|
||||||
}, [connectionState]);
|
}, [connectionState, playGroupCallSound]);
|
||||||
|
|
||||||
const handleCloseConfirmLeaveModal = useLastCallback(() => {
|
const handleCloseConfirmLeaveModal = useCallback(() => {
|
||||||
closeConfirmLeaveModal();
|
closeConfirmLeaveModal();
|
||||||
setIsEndGroupCallModal(false);
|
setIsEndGroupCallModal(false);
|
||||||
});
|
}, [closeConfirmLeaveModal]);
|
||||||
|
|
||||||
const handleToggleFullscreen = useLastCallback(() => {
|
const MainButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||||
|
return ({ onTrigger, isOpen }) => (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
className={isOpen ? 'active' : undefined}
|
||||||
|
onClick={onTrigger}
|
||||||
|
ariaLabel={lang('AccDescrMoreOptions')}
|
||||||
|
>
|
||||||
|
<i className="icon icon-more" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}, [lang]);
|
||||||
|
|
||||||
|
const handleToggleFullscreen = useCallback(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
closeFullscreen();
|
document.exitFullscreen().then(closeFullscreen);
|
||||||
} else {
|
} else {
|
||||||
openFullscreen();
|
containerRef.current.requestFullscreen().then(openFullscreen);
|
||||||
}
|
}
|
||||||
});
|
}, [closeFullscreen, isFullscreen, openFullscreen]);
|
||||||
|
|
||||||
const handleToggleSidebar = useLastCallback(() => {
|
const handleToggleSidebar = useCallback(() => {
|
||||||
if (isSidebarOpen) {
|
if (isSidebarOpen) {
|
||||||
closeSidebar();
|
closeSidebar();
|
||||||
} else {
|
} else {
|
||||||
openSidebar();
|
openSidebar();
|
||||||
}
|
}
|
||||||
});
|
}, [closeSidebar, isSidebarOpen, openSidebar]);
|
||||||
|
|
||||||
const handleInviteMember = useLastCallback(() => {
|
const handleStreamsDoubleClick = useCallback(() => {
|
||||||
createGroupCallInviteLink();
|
if (!IS_REQUEST_FULLSCREEN_SUPPORTED) return;
|
||||||
});
|
|
||||||
|
|
||||||
const handleClickVideo = useLastCallback(() => {
|
if (!isFullscreen) {
|
||||||
toggleGroupCallVideo();
|
closeSidebar();
|
||||||
});
|
handleToggleFullscreen();
|
||||||
|
} else {
|
||||||
|
handleToggleFullscreen();
|
||||||
|
}
|
||||||
|
}, [closeSidebar, handleToggleFullscreen, isFullscreen]);
|
||||||
|
|
||||||
|
const toggleFullscreen = useCallback(() => {
|
||||||
|
if (isFullscreen) {
|
||||||
|
closeFullscreen();
|
||||||
|
} else {
|
||||||
|
openFullscreen();
|
||||||
|
}
|
||||||
|
}, [closeFullscreen, isFullscreen, openFullscreen]);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
toggleGroupCallPanel();
|
||||||
|
if (isFullscreen) {
|
||||||
|
closeFullscreen();
|
||||||
|
}
|
||||||
|
}, [closeFullscreen, isFullscreen, toggleGroupCallPanel]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!IS_REQUEST_FULLSCREEN_SUPPORTED) return undefined;
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return undefined;
|
||||||
|
|
||||||
|
container.addEventListener('fullscreenchange', toggleFullscreen);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener('fullscreenchange', toggleFullscreen);
|
||||||
|
};
|
||||||
|
}, [toggleFullscreen]);
|
||||||
|
|
||||||
|
const handleClickVideoOrSpeaker = () => {
|
||||||
|
if (shouldRaiseHand) {
|
||||||
|
toggleSpeaker();
|
||||||
|
} else {
|
||||||
|
toggleGroupCallVideo();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connectToActiveGroupCall();
|
connectToActiveGroupCall();
|
||||||
}, [connectToActiveGroupCall, groupCallId]);
|
}, [connectToActiveGroupCall, groupCallId]);
|
||||||
|
|
||||||
const handleLeaveGroupCall = useLastCallback(() => {
|
const endGroupCall = useCallback(() => {
|
||||||
|
setIsEndGroupCallModal(true);
|
||||||
|
setShouldEndGroupCall(true);
|
||||||
|
openConfirmLeaveModal();
|
||||||
|
if (isFullscreen) {
|
||||||
|
handleToggleFullscreen();
|
||||||
|
}
|
||||||
|
}, [handleToggleFullscreen, isFullscreen, openConfirmLeaveModal]);
|
||||||
|
|
||||||
|
const handleLeaveGroupCall = useCallback(() => {
|
||||||
if (isAdmin && !isConfirmLeaveModalOpen) {
|
if (isAdmin && !isConfirmLeaveModalOpen) {
|
||||||
openConfirmLeaveModal();
|
openConfirmLeaveModal();
|
||||||
|
if (isFullscreen) {
|
||||||
|
handleToggleFullscreen();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
playGroupCallSound({ sound: 'leave' });
|
playGroupCallSound({ sound: 'leave' });
|
||||||
setIsLeaving(true);
|
setIsLeaving(true);
|
||||||
closeConfirmLeaveModal();
|
closeConfirmLeaveModal();
|
||||||
});
|
}, [
|
||||||
|
closeConfirmLeaveModal, handleToggleFullscreen, isAdmin, isConfirmLeaveModalOpen, isFullscreen,
|
||||||
|
openConfirmLeaveModal, playGroupCallSound,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleCloseAnimationEnd = useLastCallback(() => {
|
const handleCloseAnimationEnd = useCallback(() => {
|
||||||
if (!isLeaving) return;
|
if (isLeaving) {
|
||||||
|
leaveGroupCall({
|
||||||
leaveGroupCall({
|
shouldDiscard: shouldEndGroupCall,
|
||||||
shouldDiscard: shouldEndGroupCall,
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleToggleGroupCallPresentation = useLastCallback(() => {
|
|
||||||
toggleGroupCallPresentation();
|
|
||||||
});
|
|
||||||
|
|
||||||
const canPinVideo = videoParticipants.length > 1 && isLandscapeLayout;
|
|
||||||
const isLandscapeWithVideos = isLandscapeLayout && hasVideoParticipants;
|
|
||||||
const [pinnedVideo, setPinnedVideo] = useState<VideoParticipant | undefined>(undefined);
|
|
||||||
const {
|
|
||||||
videoLayout,
|
|
||||||
panelOffset,
|
|
||||||
} = useGroupCallVideoLayout({
|
|
||||||
primaryContainerRef: primaryVideoContainerRef,
|
|
||||||
secondaryContainerRef: secondaryVideoContainerRef,
|
|
||||||
videoParticipants,
|
|
||||||
isLandscapeLayout,
|
|
||||||
pinnedVideo,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleOpenFirstPresentation = useLastCallback(() => {
|
|
||||||
if (!firstPresentation) return;
|
|
||||||
|
|
||||||
setPinnedVideo({
|
|
||||||
id: firstPresentation.id,
|
|
||||||
type: 'screen',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(handleOpenFirstPresentation, [handleOpenFirstPresentation, Boolean(firstPresentation)]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!pinnedVideo) return;
|
|
||||||
if (!videoParticipants.some((l) => l.type === pinnedVideo.type && l.id === pinnedVideo.id)) {
|
|
||||||
setPinnedVideo(undefined);
|
|
||||||
}
|
}
|
||||||
}, [pinnedVideo, videoLayout, videoParticipants]);
|
}, [isLeaving, leaveGroupCall, shouldEndGroupCall]);
|
||||||
|
|
||||||
|
const handleToggleGroupCallPresentation = useCallback(() => {
|
||||||
|
toggleGroupCallPresentation();
|
||||||
|
}, [toggleGroupCallPresentation]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={!isCallPanelVisible && !isLeaving}
|
||||||
onClose={toggleGroupCallPanel}
|
onClose={toggleGroupCallPanel}
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
styles.root,
|
'GroupCall',
|
||||||
isFullscreen && styles.fullscreen,
|
(isMobile && !isLandscape) && 'single-column',
|
||||||
isLandscapeLayout && styles.landscape,
|
isLandscapeLayout && 'landscape',
|
||||||
!hasVideoParticipants && styles.noVideoParticipants,
|
!isSidebarOpen && 'no-sidebar',
|
||||||
!isLandscapeLayout && styles.portrait,
|
|
||||||
!isSidebarOpen && isLandscapeWithVideos && styles.noSidebar,
|
|
||||||
)}
|
)}
|
||||||
dialogRef={containerRef}
|
dialogRef={containerRef}
|
||||||
onCloseAnimationEnd={handleCloseAnimationEnd}
|
onCloseAnimationEnd={handleCloseAnimationEnd}
|
||||||
>
|
>
|
||||||
{isLandscapeWithVideos && (
|
<div className="header">
|
||||||
<div className={styles.videos}>
|
<h3>{title || lang('VoipGroupVoiceChat')}</h3>
|
||||||
<div className={styles.videosHeader}>
|
{IS_REQUEST_FULLSCREEN_SUPPORTED && (
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
size="smaller"
|
size="smaller"
|
||||||
color="translucent"
|
color="translucent"
|
||||||
onClick={handleToggleFullscreen}
|
onClick={handleToggleFullscreen}
|
||||||
className={buildClassName(styles.headerButton, styles.firstButton)}
|
ariaLabel={lang(isFullscreen ? 'AccExitFullscreen' : 'AccSwitchToFullscreen')}
|
||||||
ariaLabel={lang(isFullscreen ? 'AccExitFullscreen' : 'AccSwitchToFullscreen')}
|
>
|
||||||
>
|
<i className={buildClassName('icon', isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen')} />
|
||||||
<i
|
</Button>
|
||||||
className={buildClassName('icon', isFullscreen ? 'icon-smallscreen' : 'icon-fullscreen')}
|
)}
|
||||||
aria-hidden
|
{isLandscapeLayout && (
|
||||||
/>
|
<Button
|
||||||
</Button>
|
round
|
||||||
|
size="smaller"
|
||||||
<h3 className={buildClassName(styles.title, styles.bigger)}>
|
color="translucent"
|
||||||
{title || lang('VoipGroupVoiceChat')}
|
onClick={handleToggleSidebar}
|
||||||
</h3>
|
>
|
||||||
|
<i className="icon icon-sidebar" />
|
||||||
{isLandscapeWithVideos && !isSidebarOpen && (
|
</Button>
|
||||||
<Button
|
)}
|
||||||
round
|
{((IS_SCREENSHARE_SUPPORTED && !shouldRaiseHand) || isAdmin) && (
|
||||||
size="smaller"
|
<DropdownMenu
|
||||||
color="translucent"
|
positionX="right"
|
||||||
className={buildClassName(styles.headerButton, styles.videosHeaderLastButton)}
|
trigger={MainButton}
|
||||||
onClick={handleToggleSidebar}
|
>
|
||||||
ariaLabel={lang('AccDescrExpandPanel')}
|
{IS_SCREENSHARE_SUPPORTED && !shouldRaiseHand && (
|
||||||
|
<MenuItem
|
||||||
|
icon="share-screen-outlined"
|
||||||
|
onClick={handleToggleGroupCallPresentation}
|
||||||
>
|
>
|
||||||
<i className="icon icon-sidebar" aria-hidden />
|
{lang(hasPresentation ? 'VoipChatStopScreenCapture' : 'VoipChatStartScreenCapture')}
|
||||||
</Button>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</div>
|
{isAdmin && (
|
||||||
|
<MenuItem
|
||||||
<div
|
icon="phone-discard-outline"
|
||||||
className={styles.videosContent}
|
onClick={endGroupCall}
|
||||||
ref={primaryVideoContainerRef}
|
destructive
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={styles.panelWrapper} ref={panelRef}>
|
|
||||||
<div className={buildClassName(styles.panel, 'custom-scroll')}>
|
|
||||||
<div className={styles.panelScrollTrigger} ref={panelScrollTriggerRef} />
|
|
||||||
|
|
||||||
<div className={buildClassName(styles.panelHeader, hasScrolled && styles.scrolled)}>
|
|
||||||
{!isLandscapeWithVideos && (
|
|
||||||
<Button
|
|
||||||
round
|
|
||||||
size="smaller"
|
|
||||||
color="translucent"
|
|
||||||
ripple={!isMobile}
|
|
||||||
className={buildClassName(
|
|
||||||
styles.firstButton,
|
|
||||||
styles.headerButton,
|
|
||||||
)}
|
|
||||||
onClick={handleToggleFullscreen}
|
|
||||||
ariaLabel={lang('AccSwitchToFullscreen')}
|
|
||||||
>
|
>
|
||||||
<i className="icon icon-fullscreen" aria-hidden />
|
{lang('VoipGroupLeaveAlertEndChat')}
|
||||||
</Button>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
{isLandscapeWithVideos && (
|
)}
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
size="smaller"
|
size="smaller"
|
||||||
ripple={!isMobile}
|
color="translucent"
|
||||||
className={buildClassName(
|
onClick={handleClose}
|
||||||
styles.firstButton,
|
|
||||||
styles.headerButton,
|
|
||||||
)}
|
|
||||||
color="translucent"
|
|
||||||
onClick={handleToggleSidebar}
|
|
||||||
ariaLabel={lang('AccDescrCollapsePanel')}
|
|
||||||
>
|
|
||||||
<i className="icon icon-sidebar" aria-hidden />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={styles.panelHeaderText}>
|
|
||||||
<h3 className={buildClassName(styles.title, isLandscapeWithVideos && styles.bigger)}>
|
|
||||||
{isLandscapeWithVideos ? membersString : groupCallTitle}
|
|
||||||
</h3>
|
|
||||||
{!isLandscapeWithVideos && (
|
|
||||||
<span className={styles.subtitle}>
|
|
||||||
{membersString}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isLandscapeWithVideos && canInvite && (
|
|
||||||
<Button
|
|
||||||
round
|
|
||||||
size="smaller"
|
|
||||||
ripple={!isMobile}
|
|
||||||
className={buildClassName(
|
|
||||||
styles.lastButton,
|
|
||||||
styles.headerButton,
|
|
||||||
)}
|
|
||||||
color="translucent"
|
|
||||||
onClick={handleInviteMember}
|
|
||||||
ariaLabel={lang('VoipGroupInviteMember')}
|
|
||||||
>
|
|
||||||
<i className="icon icon-add-user" aria-hidden />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.participants}>
|
|
||||||
<div
|
|
||||||
className={styles.participantVideos}
|
|
||||||
ref={secondaryVideoContainerRef}
|
|
||||||
style={`height: ${panelOffset}px;`}
|
|
||||||
>
|
|
||||||
{videoLayout.map((layout) => {
|
|
||||||
const participant = participants[layout.participantId];
|
|
||||||
if (!layout.isRemounted || !participant) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
teactOrderKey={layout.orderKey}
|
|
||||||
key={`${layout.participantId}_${layout.type}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GroupCallParticipantVideo
|
|
||||||
teactOrderKey={layout.orderKey}
|
|
||||||
key={`${layout.participantId}_${layout.type}`}
|
|
||||||
layout={layout}
|
|
||||||
canPin={canPinVideo}
|
|
||||||
setPinned={setPinnedVideo}
|
|
||||||
pinnedVideo={pinnedVideo}
|
|
||||||
participant={participant}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<GroupCallParticipantList
|
|
||||||
panelOffset={panelOffset}
|
|
||||||
isLandscape={isLandscapeWithVideos}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FloatingActionButton
|
|
||||||
key="add-participant"
|
|
||||||
isShown={isLandscapeWithVideos && canInvite}
|
|
||||||
onClick={handleInviteMember}
|
|
||||||
className={styles.addParticipantButton}
|
|
||||||
ariaLabel={lang('VoipGroupInviteMember')}
|
|
||||||
>
|
>
|
||||||
<i className="icon icon-add-user-filled" aria-hidden />
|
<i className="icon icon-close" />
|
||||||
</FloatingActionButton>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{videoLayout.map((layout) => {
|
<div className="scrollable custom-scroll">
|
||||||
const participant = participants[layout.participantId];
|
<GroupCallParticipantStreams onDoubleClick={handleStreamsDoubleClick} />
|
||||||
if (layout.isRemounted || !participant) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
teactOrderKey={layout.orderKey}
|
|
||||||
key={`${layout.participantId}_${layout.type}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<GroupCallParticipantVideo
|
|
||||||
teactOrderKey={layout.orderKey}
|
|
||||||
key={`${layout.participantId}_${layout.type}`}
|
|
||||||
layout={layout}
|
|
||||||
canPin={canPinVideo}
|
|
||||||
setPinned={setPinnedVideo}
|
|
||||||
pinnedVideo={pinnedVideo}
|
|
||||||
participant={participant}
|
|
||||||
className={styles.video}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
<div className={styles.actions}>
|
{(!isLandscapeLayout || isSidebarOpen)
|
||||||
<Button
|
&& <GroupCallParticipantList openParticipantMenu={handleOpenParticipantMenu} />}
|
||||||
round
|
</div>
|
||||||
size="default"
|
|
||||||
ripple
|
|
||||||
className={buildClassName(
|
|
||||||
styles.actionButton,
|
|
||||||
!hasAudioStream && styles.muted,
|
|
||||||
canRequestToSpeak && styles.canRequestToSpeak,
|
|
||||||
)}
|
|
||||||
onClick={handleClickVideo}
|
|
||||||
ariaLabel={lang(hasVideo ? 'VoipStopVideo' : 'VoipStartVideo')}
|
|
||||||
disabled={isConnecting}
|
|
||||||
>
|
|
||||||
<i className={buildClassName('icon', !hasVideo ? 'icon-video-stop' : 'icon-video')} aria-hidden />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<GroupCallParticipantMenu
|
||||||
round
|
participant={participantMenu?.participant}
|
||||||
size="default"
|
anchor={participantMenu?.anchor}
|
||||||
ripple
|
isDropdownOpen={isParticipantMenuOpen}
|
||||||
className={buildClassName(
|
closeDropdown={closeParticipantMenu}
|
||||||
styles.actionButton,
|
/>
|
||||||
!hasAudioStream && styles.muted,
|
|
||||||
canRequestToSpeak && styles.canRequestToSpeak,
|
|
||||||
)}
|
|
||||||
onClick={handleToggleGroupCallPresentation}
|
|
||||||
ariaLabel={lang(hasPresentation ? 'lng_group_call_screen_share_stop' : 'lng_group_call_tooltip_screen')}
|
|
||||||
disabled={isConnecting || !IS_SCREENSHARE_SUPPORTED}
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
className={buildClassName('icon', !hasPresentation ? 'icon-share-screen-stop' : 'icon-share-screen')}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<MicrophoneButton className={styles.actionButton} />
|
<div className="buttons">
|
||||||
|
{isConnecting && <Loading />}
|
||||||
|
|
||||||
<Button
|
<div className="button-wrapper">
|
||||||
round
|
<div className="video-buttons">
|
||||||
size="default"
|
{hasVideo && (IS_ANDROID || IS_IOS) && (
|
||||||
ripple
|
<button className="smaller-button" onClick={switchCameraInput}>
|
||||||
className={buildClassName(
|
<AnimatedIcon
|
||||||
styles.actionButton,
|
tgsUrl={LOCAL_TGS_URLS.CameraFlip}
|
||||||
!hasAudioStream && styles.muted,
|
playSegment={CAMERA_FLIP_PLAY_SEGMENT}
|
||||||
canRequestToSpeak && styles.canRequestToSpeak,
|
size={24}
|
||||||
)}
|
/>
|
||||||
ariaLabel={lang('lng_group_call_settings')}
|
</button>
|
||||||
disabled
|
)}
|
||||||
>
|
<button
|
||||||
<i className="icon icon-settings-filled" aria-hidden />
|
className={buildClassName(
|
||||||
</Button>
|
'small-button',
|
||||||
|
shouldRaiseHand ? 'speaker' : 'camera',
|
||||||
|
(hasVideo || (shouldRaiseHand && isSpeakerEnabled)) && 'active',
|
||||||
|
)}
|
||||||
|
onClick={handleClickVideoOrSpeaker}
|
||||||
|
>
|
||||||
|
<i className={buildClassName(
|
||||||
|
'icon',
|
||||||
|
shouldRaiseHand ? 'icon-speaker' : (hasVideo ? 'icon-video-stop' : 'icon-video'),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button
|
<div className="button-text">
|
||||||
round
|
{lang(shouldRaiseHand ? 'VoipSpeaker' : 'VoipCamera')}
|
||||||
size="default"
|
</div>
|
||||||
ripple
|
</div>
|
||||||
className={buildClassName(
|
|
||||||
styles.actionButton,
|
<MicrophoneButton />
|
||||||
styles.destructive,
|
|
||||||
)}
|
<div className="button-wrapper">
|
||||||
onClick={handleLeaveGroupCall}
|
<button className="small-button leave" onClick={handleLeaveGroupCall}>
|
||||||
ariaLabel={lang('lng_group_call_leave')}
|
<i className="icon icon-phone-discard" />
|
||||||
>
|
</button>
|
||||||
<i className="icon icon-close" aria-hidden />
|
|
||||||
</Button>
|
<div className="button-text">
|
||||||
|
{lang('VoipGroupLeave')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
@ -530,20 +415,18 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { groupCallId }): StateProps => {
|
(global, { groupCallId }): StateProps => {
|
||||||
const {
|
const {
|
||||||
connectionState, title, participants, participantsCount, chatId,
|
connectionState, title, isSpeakerDisabled, participants, participantsCount,
|
||||||
} = selectGroupCall(global, groupCallId)! || {};
|
} = selectGroupCall(global, groupCallId)! || {};
|
||||||
|
|
||||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connectionState,
|
connectionState,
|
||||||
title: title || chat?.title,
|
title,
|
||||||
|
isSpeakerEnabled: !isSpeakerDisabled,
|
||||||
participantsCount,
|
participantsCount,
|
||||||
meParticipant: selectGroupCallParticipant(global, groupCallId, global.currentUserId!),
|
meParticipant: selectGroupCallParticipant(global, groupCallId, global.currentUserId!),
|
||||||
isCallPanelVisible: Boolean(selectTabState(global).isCallPanelVisible),
|
isCallPanelVisible: Boolean(selectTabState(global).isCallPanelVisible),
|
||||||
isAdmin: selectIsAdminInActiveGroupCall(global),
|
isAdmin: selectIsAdminInActiveGroupCall(global),
|
||||||
participants,
|
participants,
|
||||||
canInvite: selectCanInviteToActiveGroupCall(global),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(GroupCall));
|
)(GroupCall));
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
.root {
|
|
||||||
:global {
|
|
||||||
.ListItem-button {
|
|
||||||
--color-chat-hover: rgba(255, 255, 255, 0.04);
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.multiline-item {
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex !important;
|
|
||||||
|
|
||||||
.fullName {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
font-size: 1rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitleText {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitleBlue {
|
|
||||||
--color-text-secondary: #4da6e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitleRed {
|
|
||||||
--color-text-secondary: #ff706f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitleGreen {
|
|
||||||
--color-text-secondary: #57bc6c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
78
src/components/calls/group/GroupCallParticipant.scss
Normal file
78
src/components/calls/group/GroupCallParticipant.scss
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
.GroupCallParticipant {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
transition: 0.15s ease-out background-color;
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2f363e;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Avatar {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #848d94;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
|
||||||
|
&.blue {
|
||||||
|
color: #4da6e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.green {
|
||||||
|
color: #57bc6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
color: #ff706f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.microphone {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #ff706f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.can-self-unmute {
|
||||||
|
.microphone {
|
||||||
|
color: #848d94;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.streams {
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,31 +1,24 @@
|
|||||||
import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce';
|
import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce';
|
||||||
import { THRESHOLD } from '../../../lib/secret-sauce';
|
import { THRESHOLD } from '../../../lib/secret-sauce';
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, {
|
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
|
||||||
memo, useCallback, useMemo, useRef,
|
|
||||||
} from '../../../lib/teact/teact';
|
|
||||||
import { withGlobal } from '../../../global';
|
import { withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiChat, ApiUser } from '../../../api/types';
|
import type { ApiChat, ApiUser } from '../../../api/types';
|
||||||
|
|
||||||
import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import renderText from '../../common/helpers/renderText';
|
|
||||||
import { selectChat, selectUser } from '../../../global/selectors';
|
import { selectChat, selectUser } from '../../../global/selectors';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
|
||||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
|
||||||
|
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
import OutlinedMicrophoneIcon from './OutlinedMicrophoneIcon';
|
import OutlinedMicrophoneIcon from './OutlinedMicrophoneIcon';
|
||||||
import ListItem from '../../ui/ListItem';
|
|
||||||
import GroupCallParticipantMenu from './GroupCallParticipantMenu';
|
|
||||||
import FullNameTitle from '../../common/FullNameTitle';
|
|
||||||
|
|
||||||
import styles from './GroupCallParticipant.module.scss';
|
import './GroupCallParticipant.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
participant: TypeGroupCallParticipant;
|
participant: TypeGroupCallParticipant;
|
||||||
|
openParticipantMenu: (anchor: HTMLDivElement, participant: TypeGroupCallParticipant) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -34,132 +27,67 @@ type StateProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const GroupCallParticipant: FC<OwnProps & StateProps> = ({
|
const GroupCallParticipant: FC<OwnProps & StateProps> = ({
|
||||||
|
openParticipantMenu,
|
||||||
participant,
|
participant,
|
||||||
user,
|
user,
|
||||||
chat,
|
chat,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const anchorRef = useRef<HTMLDivElement>(null);
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
const {
|
const { isSelf, isMutedByMe, isMuted } = participant;
|
||||||
isSelf, isMutedByMe, isMuted, hasVideoStream, hasPresentationStream,
|
|
||||||
} = participant;
|
|
||||||
const isSpeaking = (participant.amplitude || 0) > THRESHOLD;
|
const isSpeaking = (participant.amplitude || 0) > THRESHOLD;
|
||||||
const isRaiseHand = Boolean(participant.raiseHandRating);
|
const isRaiseHand = Boolean(participant.raiseHandRating);
|
||||||
|
|
||||||
const {
|
const handleOnClick = () => {
|
||||||
isContextMenuOpen,
|
if (isSelf) return;
|
||||||
contextMenuPosition,
|
openParticipantMenu(anchorRef.current!, participant);
|
||||||
handleContextMenu,
|
};
|
||||||
handleBeforeContextMenu,
|
|
||||||
handleContextMenuClose,
|
|
||||||
handleContextMenuHide,
|
|
||||||
} = useContextMenuHandlers(ref, isSelf);
|
|
||||||
|
|
||||||
const getTriggerElement = useCallback(() => ref.current, []);
|
|
||||||
|
|
||||||
const getRootElement = useCallback(
|
|
||||||
() => ref.current!.closest('.custom-scroll, .no-scrollbar'),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getMenuElement = useCallback(
|
|
||||||
() => menuRef.current!,
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getLayout = useCallback(
|
|
||||||
() => ({ withPortal: true }),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
|
||||||
} = useMenuPosition(
|
|
||||||
contextMenuPosition,
|
|
||||||
getTriggerElement,
|
|
||||||
getRootElement,
|
|
||||||
getMenuElement,
|
|
||||||
getLayout,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasCustomVolume = Boolean(
|
|
||||||
!isMuted && isSpeaking && participant.volume && participant.volume !== GROUP_CALL_DEFAULT_VOLUME,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [aboutText, aboutColor] = useMemo(() => {
|
const [aboutText, aboutColor] = useMemo(() => {
|
||||||
if (isMutedByMe) {
|
|
||||||
return [lang('VoipGroupMutedForMe'), styles.subtitleRed];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRaiseHand) {
|
|
||||||
return [lang('WantsToSpeak'), styles.subtitleBlue];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasCustomVolume) {
|
|
||||||
return [
|
|
||||||
lang('SpeakingWithVolume',
|
|
||||||
(participant.volume! / GROUP_CALL_VOLUME_MULTIPLIER).toString())
|
|
||||||
.replace('%%', '%'),
|
|
||||||
styles.subtitleGreen,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMuted && isSpeaking) {
|
|
||||||
return [
|
|
||||||
lang('Speaking'),
|
|
||||||
styles.subtitleGreen,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSelf) {
|
if (isSelf) {
|
||||||
return [lang('ThisIsYou'), styles.subtitleBlue];
|
return [lang('ThisIsYou'), 'blue'];
|
||||||
}
|
}
|
||||||
|
if (isMutedByMe) {
|
||||||
return participant.about ? [participant.about, ''] : [lang('Listening'), styles.subtitleBlue];
|
return [lang('VoipGroupMutedForMe'), 'red'];
|
||||||
}, [
|
}
|
||||||
isMutedByMe, isRaiseHand, isSelf, hasCustomVolume, isMuted, isSpeaking, participant.about, participant.volume, lang,
|
return isRaiseHand
|
||||||
]);
|
? [lang('WantsToSpeak'), 'blue']
|
||||||
|
: (!isMuted && isSpeaking ? [
|
||||||
|
participant.volume && participant.volume !== GROUP_CALL_DEFAULT_VOLUME
|
||||||
|
? lang('SpeakingWithVolume',
|
||||||
|
(participant.volume / GROUP_CALL_VOLUME_MULTIPLIER).toString())
|
||||||
|
.replace('%%', '%') : lang('Speaking'),
|
||||||
|
'green',
|
||||||
|
]
|
||||||
|
: (participant.about ? [participant.about, ''] : [lang('Listening'), 'blue']));
|
||||||
|
}, [isSpeaking, participant.volume, lang, isSelf, isMutedByMe, isRaiseHand, isMuted, participant.about]);
|
||||||
|
|
||||||
if (!user && !chat) {
|
if (!user && !chat) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const name = user ? `${user.firstName || ''} ${user.lastName || ''}` : chat?.title;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<div
|
||||||
leftElement={<Avatar user={user} chat={chat} className={styles.avatar} />}
|
className={buildClassName(
|
||||||
rightElement={<OutlinedMicrophoneIcon participant={participant} className={styles.icon} />}
|
'GroupCallParticipant',
|
||||||
className={styles.root}
|
participant.canSelfUnmute && 'can-self-unmute',
|
||||||
onClick={handleContextMenu}
|
)}
|
||||||
onMouseDown={handleBeforeContextMenu}
|
onClick={handleOnClick}
|
||||||
onContextMenu={handleContextMenu}
|
ref={anchorRef}
|
||||||
multiline
|
|
||||||
ripple
|
|
||||||
ref={ref}
|
|
||||||
>
|
>
|
||||||
<FullNameTitle peer={user || chat!} withEmojiStatus className={styles.title} />
|
<Avatar user={user} chat={chat} size="medium" />
|
||||||
<span className={buildClassName(styles.subtitle, 'subtitle', aboutColor)}>
|
<div className="info">
|
||||||
{hasPresentationStream && <i className="icon icon-share-screen" aria-hidden />}
|
<span className="name">{name}</span>
|
||||||
{hasVideoStream && <i className="icon icon-video" aria-hidden />}
|
<span className={buildClassName('about', aboutColor)}>{aboutText}</span>
|
||||||
{hasCustomVolume && <i className="icon icon-speaker" aria-hidden />}
|
</div>
|
||||||
<span className={styles.subtitleText}>{renderText(aboutText)}</span>
|
<div className="microphone">
|
||||||
</span>
|
<OutlinedMicrophoneIcon participant={participant} />
|
||||||
<GroupCallParticipantMenu
|
</div>
|
||||||
participant={participant}
|
</div>
|
||||||
isDropdownOpen={isContextMenuOpen}
|
|
||||||
positionX={positionX}
|
|
||||||
positionY={positionY}
|
|
||||||
transformOriginX={transformOriginX}
|
|
||||||
transformOriginY={transformOriginY}
|
|
||||||
style={menuStyle}
|
|
||||||
onClose={handleContextMenuClose}
|
|
||||||
onCloseAnimationEnd={handleContextMenuHide}
|
|
||||||
menuRef={menuRef}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
.root {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
top: 0.25rem;
|
|
||||||
padding-bottom: 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.portrait {
|
|
||||||
padding-bottom: 6rem;
|
|
||||||
}
|
|
||||||
@ -1,22 +1,17 @@
|
|||||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
|
||||||
import { getActions, withGlobal } from '../../../global';
|
|
||||||
|
|
||||||
import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce';
|
import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce';
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import useLang from '../../../hooks/useLang';
|
||||||
import { selectActiveGroupCall } from '../../../global/selectors/calls';
|
import { selectActiveGroupCall } from '../../../global/selectors/calls';
|
||||||
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
|
||||||
|
|
||||||
import GroupCallParticipant from './GroupCallParticipant';
|
import GroupCallParticipant from './GroupCallParticipant';
|
||||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
|
|
||||||
import styles from './GroupCallParticipantList.module.scss';
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
panelOffset: number;
|
openParticipantMenu: (anchor: HTMLDivElement, participant: TypeGroupCallParticipant) => void;
|
||||||
isLandscape: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -26,22 +21,24 @@ type StateProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const GroupCallParticipantList: FC<OwnProps & StateProps> = ({
|
const GroupCallParticipantList: FC<OwnProps & StateProps> = ({
|
||||||
panelOffset,
|
|
||||||
participants,
|
participants,
|
||||||
participantsCount,
|
participantsCount,
|
||||||
isLandscape,
|
openParticipantMenu,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
|
createGroupCallInviteLink,
|
||||||
loadMoreGroupCallParticipants,
|
loadMoreGroupCallParticipants,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
const participantsIds = useMemo(() => {
|
const participantsIds = useMemo(() => {
|
||||||
return Object.keys(participants || {});
|
return Object.keys(participants || {});
|
||||||
}, [participants]);
|
}, [participants]);
|
||||||
|
|
||||||
const handleLoadMoreGroupCallParticipants = useLastCallback(() => {
|
const handleLoadMoreGroupCallParticipants = useCallback(() => {
|
||||||
loadMoreGroupCallParticipants();
|
loadMoreGroupCallParticipants();
|
||||||
});
|
}, [loadMoreGroupCallParticipants]);
|
||||||
|
|
||||||
const [viewportIds, getMore] = useInfiniteScroll(
|
const [viewportIds, getMore] = useInfiniteScroll(
|
||||||
handleLoadMoreGroupCallParticipants,
|
handleLoadMoreGroupCallParticipants,
|
||||||
@ -49,24 +46,37 @@ const GroupCallParticipantList: FC<OwnProps & StateProps> = ({
|
|||||||
participantsIds.length >= participantsCount,
|
participantsIds.length >= participantsCount,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function handleCreateGroupCallInviteLink() {
|
||||||
|
createGroupCallInviteLink();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InfiniteScroll
|
<div className="participants">
|
||||||
items={viewportIds}
|
<div className="invite-btn" onClick={handleCreateGroupCallInviteLink}>
|
||||||
onLoadMore={getMore}
|
<div className="icon-wrapper">
|
||||||
style={`transform: translateY(${panelOffset}px);`}
|
<i className="icon icon-add-user" />
|
||||||
className={buildClassName(styles.root, !isLandscape && styles.portrait)}
|
</div>
|
||||||
>
|
<div className="text">{lang('VoipGroupInviteMember')}</div>
|
||||||
{participants && viewportIds?.map(
|
</div>
|
||||||
(participantId) => (
|
|
||||||
participants[participantId] && (
|
<InfiniteScroll
|
||||||
<GroupCallParticipant
|
items={viewportIds}
|
||||||
key={participantId}
|
onLoadMore={getMore}
|
||||||
participant={participants[participantId]}
|
>
|
||||||
/>
|
{viewportIds?.map(
|
||||||
)
|
(participantId) => (
|
||||||
),
|
participants![participantId] && (
|
||||||
)}
|
<GroupCallParticipant
|
||||||
</InfiniteScroll>
|
key={participantId}
|
||||||
|
openParticipantMenu={openParticipantMenu}
|
||||||
|
participant={participants![participantId]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</InfiniteScroll>
|
||||||
|
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
@import '../../../styles/mixins';
|
@import '../../../styles/mixins';
|
||||||
|
|
||||||
.participant-menu {
|
.participant-menu {
|
||||||
|
position: absolute;
|
||||||
--color-text: white;
|
--color-text: white;
|
||||||
--color-background-compact-menu: #212121DD;
|
--color-background-compact-menu: #212121DD;
|
||||||
--color-background-compact-menu-hover: #00000066;
|
--color-background-compact-menu-hover: #00000066;
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
z-index: var(--z-modal-menu);
|
|
||||||
|
|
||||||
.bubble {
|
.bubble {
|
||||||
backdrop-filter: none !important;
|
|
||||||
background: none !important;
|
background: none !important;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -24,7 +21,6 @@
|
|||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
border-radius: var(--border-radius-default);
|
border-radius: var(--border-radius-default);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +44,7 @@
|
|||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
|
|
||||||
.AnimatedSticker {
|
.AnimatedSticker {
|
||||||
margin-right: 1rem;
|
margin-right: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +86,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: -1.5rem;
|
left: -1.5rem;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: calc(100% + 1.5rem);
|
width: calc(100% + 3rem);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,19 @@
|
|||||||
import type { GroupCallParticipant } from '../../../lib/secret-sauce';
|
import type { GroupCallParticipant } from '../../../lib/secret-sauce';
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useEffect, useState,
|
memo, useCallback, useEffect, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { IAnchorPosition } from '../../../types';
|
||||||
|
|
||||||
import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
|
import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
|
||||||
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import buildStyle from '../../../util/buildStyle';
|
||||||
import useRunThrottled from '../../../hooks/useRunThrottled';
|
import useRunThrottled from '../../../hooks/useRunThrottled';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
|
||||||
import { selectIsAdminInActiveGroupCall } from '../../../global/selectors/calls';
|
import { selectIsAdminInActiveGroupCall } from '../../../global/selectors/calls';
|
||||||
|
|
||||||
import Menu from '../../ui/Menu';
|
import Menu from '../../ui/Menu';
|
||||||
@ -26,15 +28,9 @@ const SPEAKER_ICON_ENABLED_SEGMENT: [number, number] = [17, 34];
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
participant?: GroupCallParticipant;
|
participant?: GroupCallParticipant;
|
||||||
onCloseAnimationEnd: VoidFunction;
|
closeDropdown: VoidFunction;
|
||||||
onClose: VoidFunction;
|
|
||||||
isDropdownOpen: boolean;
|
isDropdownOpen: boolean;
|
||||||
positionX?: 'left' | 'right';
|
anchor?: IAnchorPosition;
|
||||||
positionY?: 'top' | 'bottom';
|
|
||||||
transformOriginX?: number;
|
|
||||||
transformOriginY?: number;
|
|
||||||
style?: string;
|
|
||||||
menuRef?: React.RefObject<HTMLDivElement>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -52,16 +48,10 @@ const SPEAKER_ICON_SIZE = 24;
|
|||||||
|
|
||||||
const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
|
const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
|
||||||
participant,
|
participant,
|
||||||
onCloseAnimationEnd,
|
closeDropdown,
|
||||||
onClose,
|
|
||||||
isDropdownOpen,
|
isDropdownOpen,
|
||||||
|
anchor,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
positionY,
|
|
||||||
menuRef,
|
|
||||||
positionX,
|
|
||||||
style,
|
|
||||||
transformOriginY,
|
|
||||||
transformOriginX,
|
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
toggleGroupCallMute,
|
toggleGroupCallMute,
|
||||||
@ -85,22 +75,6 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
|
|||||||
isMutedByMe ? VOLUME_ZERO : ((participant?.volume || GROUP_CALL_DEFAULT_VOLUME) / GROUP_CALL_VOLUME_MULTIPLIER),
|
isMutedByMe ? VOLUME_ZERO : ((participant?.volume || GROUP_CALL_DEFAULT_VOLUME) / GROUP_CALL_VOLUME_MULTIPLIER),
|
||||||
);
|
);
|
||||||
|
|
||||||
const [shouldPlay, setShouldPlay] = useState(false);
|
|
||||||
|
|
||||||
const isLocalVolumeZero = localVolume === VOLUME_ZERO;
|
|
||||||
const speakerIconPlaySegment = isLocalVolumeZero ? SPEAKER_ICON_DISABLED_SEGMENT : SPEAKER_ICON_ENABLED_SEGMENT;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isDropdownOpen) return;
|
|
||||||
setShouldPlay(false);
|
|
||||||
}, [isDropdownOpen]);
|
|
||||||
|
|
||||||
const handleSetLocalVolume = useLastCallback((volume: number) => {
|
|
||||||
setLocalVolume(volume);
|
|
||||||
const isNewLocalVolumeZero = volume === VOLUME_ZERO;
|
|
||||||
setShouldPlay(isNewLocalVolumeZero !== isLocalVolumeZero);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalVolume(isMutedByMe
|
setLocalVolume(isMutedByMe
|
||||||
? VOLUME_ZERO
|
? VOLUME_ZERO
|
||||||
@ -111,49 +85,49 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const runThrottled = useRunThrottled(VOLUME_CHANGE_THROTTLE);
|
const runThrottled = useRunThrottled(VOLUME_CHANGE_THROTTLE);
|
||||||
|
|
||||||
const handleRemove = useLastCallback((e: React.SyntheticEvent<any>) => {
|
const handleRemove = useCallback((e: React.SyntheticEvent<any>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
openDeleteUserModal();
|
openDeleteUserModal();
|
||||||
onClose();
|
closeDropdown();
|
||||||
});
|
}, [openDeleteUserModal, closeDropdown]);
|
||||||
|
|
||||||
const handleCancelRequestToSpeak = useLastCallback((e: React.SyntheticEvent<any>) => {
|
const handleCancelRequestToSpeak = useCallback((e: React.SyntheticEvent<any>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
requestToSpeak({
|
requestToSpeak({
|
||||||
value: false,
|
value: false,
|
||||||
});
|
});
|
||||||
onClose();
|
closeDropdown();
|
||||||
});
|
}, [requestToSpeak, closeDropdown]);
|
||||||
|
|
||||||
const handleMute = useLastCallback((e: React.SyntheticEvent<any>) => {
|
const handleMute = useCallback((e: React.SyntheticEvent<any>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onClose();
|
closeDropdown();
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
handleSetLocalVolume(isMutedByMe ? GROUP_CALL_DEFAULT_VOLUME / GROUP_CALL_VOLUME_MULTIPLIER : VOLUME_ZERO);
|
setLocalVolume(isMutedByMe ? GROUP_CALL_DEFAULT_VOLUME / GROUP_CALL_VOLUME_MULTIPLIER : VOLUME_ZERO);
|
||||||
} else if (shouldRaiseHand) {
|
|
||||||
handleSetLocalVolume((participant?.volume ?? GROUP_CALL_DEFAULT_VOLUME) / GROUP_CALL_VOLUME_MULTIPLIER);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleGroupCallMute({
|
toggleGroupCallMute({
|
||||||
participantId: id!,
|
participantId: id!,
|
||||||
value: isAdmin ? !shouldRaiseHand : !isMutedByMe,
|
value: isAdmin ? !shouldRaiseHand : !isMutedByMe,
|
||||||
});
|
});
|
||||||
});
|
}, [closeDropdown, toggleGroupCallMute, id, isAdmin, shouldRaiseHand, isMutedByMe]);
|
||||||
|
|
||||||
const handleOpenProfile = useLastCallback((e: React.SyntheticEvent<any>) => {
|
const handleOpenProfile = useCallback((e: React.SyntheticEvent<any>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleGroupCallPanel();
|
toggleGroupCallPanel();
|
||||||
openChat({
|
openChat({
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
onClose();
|
closeDropdown();
|
||||||
});
|
}, [toggleGroupCallPanel, closeDropdown, openChat, id]);
|
||||||
|
|
||||||
|
const isLocalVolumeZero = localVolume === VOLUME_ZERO;
|
||||||
|
const speakerIconPlaySegment = isLocalVolumeZero ? SPEAKER_ICON_DISABLED_SEGMENT : SPEAKER_ICON_ENABLED_SEGMENT;
|
||||||
|
|
||||||
const handleChangeVolume = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChangeVolume = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = Number(e.target.value);
|
const value = Number(e.target.value);
|
||||||
handleSetLocalVolume(value);
|
setLocalVolume(value);
|
||||||
|
|
||||||
runThrottled(() => {
|
runThrottled(() => {
|
||||||
if (value === VOLUME_ZERO) {
|
if (value === VOLUME_ZERO) {
|
||||||
toggleGroupCallMute({
|
toggleGroupCallMute({
|
||||||
@ -173,16 +147,11 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
|
|||||||
<div>
|
<div>
|
||||||
<Menu
|
<Menu
|
||||||
isOpen={isDropdownOpen}
|
isOpen={isDropdownOpen}
|
||||||
positionX={positionX}
|
positionX="right"
|
||||||
positionY={positionY}
|
autoClose
|
||||||
transformOriginX={transformOriginX}
|
style={buildStyle(anchor && `right: 1rem; top: ${anchor.y}px`)}
|
||||||
transformOriginY={transformOriginY}
|
onClose={closeDropdown}
|
||||||
style={style}
|
className="participant-menu"
|
||||||
ref={menuRef}
|
|
||||||
withPortal
|
|
||||||
onClose={onClose}
|
|
||||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
|
||||||
className="participant-menu with-menu-transitions"
|
|
||||||
>
|
>
|
||||||
{!isSelf && !shouldRaiseHand && (
|
{!isSelf && !shouldRaiseHand && (
|
||||||
<div className="group">
|
<div className="group">
|
||||||
@ -204,7 +173,6 @@ const GroupCallParticipantMenu: FC<OwnProps & StateProps> = ({
|
|||||||
<div className="info">
|
<div className="info">
|
||||||
<AnimatedIcon
|
<AnimatedIcon
|
||||||
tgsUrl={LOCAL_TGS_URLS.Speaker}
|
tgsUrl={LOCAL_TGS_URLS.Speaker}
|
||||||
play={shouldPlay ? speakerIconPlaySegment.toString() : false}
|
|
||||||
playSegment={speakerIconPlaySegment}
|
playSegment={speakerIconPlaySegment}
|
||||||
size={SPEAKER_ICON_SIZE}
|
size={SPEAKER_ICON_SIZE}
|
||||||
/>
|
/>
|
||||||
|
|||||||
105
src/components/calls/group/GroupCallParticipantStreams.tsx
Normal file
105
src/components/calls/group/GroupCallParticipantStreams.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import type { GroupCallParticipant } from '../../../lib/secret-sauce';
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React, {
|
||||||
|
memo, useCallback, useMemo, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../../global';
|
||||||
|
import GroupCallParticipantVideo from './GroupCallParticipantVideo';
|
||||||
|
import { selectActiveGroupCall } from '../../../global/selectors/calls';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
onDoubleClick?: VoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
participants?: Record<string, GroupCallParticipant>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelectedVideo = {
|
||||||
|
type: 'video' | 'presentation';
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const GroupCallParticipantStreams: FC<OwnProps & StateProps> = ({
|
||||||
|
participants,
|
||||||
|
onDoubleClick,
|
||||||
|
}) => {
|
||||||
|
const [selectedVideo, setSelectedVideo] = useState<SelectedVideo | undefined>(undefined);
|
||||||
|
const presentationParticipants = useMemo(() => {
|
||||||
|
return Object.values(participants || {}).filter((participant) => participant.hasPresentationStream);
|
||||||
|
}, [participants]);
|
||||||
|
const videoParticipants = useMemo(() => {
|
||||||
|
return Object.values(participants || {}).filter((participant) => participant.hasVideoStream);
|
||||||
|
}, [participants]);
|
||||||
|
|
||||||
|
const totalVideoCount = videoParticipants.length + presentationParticipants.length;
|
||||||
|
// TODO replace with more adequate solution.
|
||||||
|
// There's a max of 30 videos or so right now
|
||||||
|
const columnCount = totalVideoCount <= 2 ? 1 : (
|
||||||
|
totalVideoCount <= 6 ? 2 : (
|
||||||
|
totalVideoCount <= 9 ? 3 : 4
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldSpanLastVideo = totalVideoCount === 3 || (columnCount === 2 && totalVideoCount % 2 !== 0);
|
||||||
|
|
||||||
|
const handleClickVideo = useCallback((id: string, type: 'video' | 'presentation') => {
|
||||||
|
if (!selectedVideo || (id !== selectedVideo.id || type !== selectedVideo.type)) {
|
||||||
|
setSelectedVideo({
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSelectedVideo(undefined);
|
||||||
|
}
|
||||||
|
}, [selectedVideo]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="streams" onDoubleClick={onDoubleClick}>
|
||||||
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
'videos',
|
||||||
|
shouldSpanLastVideo && 'span-last-video',
|
||||||
|
)}
|
||||||
|
style={`--column-count: ${selectedVideo ? 1 : columnCount}`}
|
||||||
|
>
|
||||||
|
{selectedVideo && (
|
||||||
|
<GroupCallParticipantVideo
|
||||||
|
key={selectedVideo.id}
|
||||||
|
isFullscreen
|
||||||
|
onClick={handleClickVideo}
|
||||||
|
participant={participants![selectedVideo.id]}
|
||||||
|
type={selectedVideo.type}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!selectedVideo ? presentationParticipants.map((participant) => (
|
||||||
|
<GroupCallParticipantVideo
|
||||||
|
key={participant.id}
|
||||||
|
onClick={handleClickVideo}
|
||||||
|
participant={participant}
|
||||||
|
type="presentation"
|
||||||
|
/>
|
||||||
|
)) : undefined}
|
||||||
|
{!selectedVideo ? videoParticipants.map((participant) => (
|
||||||
|
<GroupCallParticipantVideo
|
||||||
|
key={participant.id}
|
||||||
|
onClick={handleClickVideo}
|
||||||
|
participant={participant}
|
||||||
|
type="video"
|
||||||
|
/>
|
||||||
|
)) : undefined}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): StateProps => {
|
||||||
|
const { participants } = selectActiveGroupCall(global) || {};
|
||||||
|
return {
|
||||||
|
participants,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(GroupCallParticipantStreams));
|
||||||
@ -1,150 +0,0 @@
|
|||||||
.wrapper {
|
|
||||||
position: absolute;
|
|
||||||
opacity: 1;
|
|
||||||
transform: translate(var(--x), var(--y)) scale(1);
|
|
||||||
|
|
||||||
width: var(--width);
|
|
||||||
height: var(--height);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translate(var(--x), var(--y)) scale(0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.noAnimate {
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
border-radius: 0.625rem;
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
inset: -0.125rem;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
background: var(--gradient-speaking);
|
|
||||||
|
|
||||||
transform: scale(0.96);
|
|
||||||
transition: 0.25s ease-in-out transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 55.62%, rgba(0, 0, 0, 0.5) 86.46%);
|
|
||||||
z-index: 2;
|
|
||||||
border-radius: 0.625rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.speaking::before {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.video {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
object-fit: contain;
|
|
||||||
border-radius: 0.625rem;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoFallback {
|
|
||||||
composes: video;
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnailWrapper {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 0.625rem;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnail {
|
|
||||||
object-fit: cover;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flipped {
|
|
||||||
transform: rotateY(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pinButton {
|
|
||||||
position: absolute;
|
|
||||||
inset-inline-end: 0.25rem;
|
|
||||||
inset-block-start: 0.25rem;
|
|
||||||
z-index: 3;
|
|
||||||
color: #FFFFFF !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottomPanel {
|
|
||||||
position: absolute;
|
|
||||||
inset-block-end: 0;
|
|
||||||
inset-inline: 0;
|
|
||||||
border-end-end-radius: 0.625rem;
|
|
||||||
border-end-start-radius: 0.625rem;
|
|
||||||
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
z-index: 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pinned .bottomPanel, .pinned::after {
|
|
||||||
opacity: 0;
|
|
||||||
transition: 0.25s ease-in-out opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pinned:hover .bottomPanel, .pinned:hover::after {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
color: #FFFFFF;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.125rem;
|
|
||||||
|
|
||||||
:global(.fullName) {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
color: #FFFFFF;
|
|
||||||
opacity: 0.6;
|
|
||||||
line-height: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
126
src/components/calls/group/GroupCallParticipantVideo.scss
Normal file
126
src/components/calls/group/GroupCallParticipantVideo.scss
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
.GroupCallParticipantVideo {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
max-height: 12.875rem;
|
||||||
|
width: calc(50% - 0.25rem);
|
||||||
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
transition: 0.25s ease-out width;
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
|
||||||
|
.thumbnail-avatar {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transform: scale(1.1);
|
||||||
|
|
||||||
|
img {
|
||||||
|
filter: blur(10px);
|
||||||
|
border-radius: 0;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child:nth-child(odd) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
box-shadow: 0 0 0 3px transparent inset;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
content: "";
|
||||||
|
z-index: 5;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
transition: 0.25s ease-out box-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active::before {
|
||||||
|
box-shadow: 0px 0px 0px 3px #78ee7e inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 5;
|
||||||
|
top: 0.75rem;
|
||||||
|
left: 0.75rem;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 0;
|
||||||
|
color: white;
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
transition: 0.25s ease-out opacity, 0.25s ease-out background-color;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
outline: none !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video {
|
||||||
|
object-fit: contain;
|
||||||
|
height: 12.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 0;
|
||||||
|
width: 100%;
|
||||||
|
transform: translate(-50%, -50%) scale(1.5);
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnail {
|
||||||
|
filter: blur(10px) brightness(0.5);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 0.5rem 0.25rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 2rem;
|
||||||
|
background: linear-gradient(0deg, #000, transparent);
|
||||||
|
transition: 0.25s ease-out opacity;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-icon {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.videos:hover .GroupCallParticipantVideo {
|
||||||
|
|
||||||
|
.info {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,314 +1,77 @@
|
|||||||
import React, {
|
import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce';
|
||||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
import { getUserStreams, THRESHOLD } from '../../../lib/secret-sauce';
|
||||||
} from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React, { memo, useCallback } from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../../global';
|
import { withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import type { VideoLayout, VideoParticipant } from './hooks/useGroupCallVideoLayout';
|
|
||||||
import type { GroupCallParticipant as TypeGroupCallParticipant } from '../../../lib/secret-sauce';
|
|
||||||
import type { ApiChat, ApiUser } from '../../../api/types';
|
import type { ApiChat, ApiUser } from '../../../api/types';
|
||||||
|
|
||||||
import { GROUP_CALL_DEFAULT_VOLUME, GROUP_CALL_VOLUME_MULTIPLIER } from '../../../config';
|
import { GROUP_CALL_THUMB_VIDEO_DISABLED } from '../../../config';
|
||||||
import { getUserStreams, THRESHOLD } from '../../../lib/secret-sauce';
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { selectChat, selectUser } from '../../../global/selectors';
|
import { selectChat, selectUser } from '../../../global/selectors';
|
||||||
import { animate } from '../../../util/animation';
|
|
||||||
import { fastRaf } from '../../../util/schedulers';
|
|
||||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
|
||||||
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
|
||||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
|
||||||
import useInterval from '../../../hooks/useInterval';
|
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Avatar from '../../common/Avatar';
|
||||||
import OutlinedMicrophoneIcon from './OutlinedMicrophoneIcon';
|
|
||||||
import FullNameTitle from '../../common/FullNameTitle';
|
|
||||||
import GroupCallParticipantMenu from './GroupCallParticipantMenu';
|
|
||||||
|
|
||||||
import styles from './GroupCallParticipantVideo.module.scss';
|
import './GroupCallParticipantVideo.scss';
|
||||||
|
|
||||||
const VIDEO_FALLBACK_UPDATE_INTERVAL = 1000;
|
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
layout: VideoLayout;
|
|
||||||
setPinned: (participant?: VideoParticipant) => void;
|
|
||||||
pinnedVideo: VideoParticipant | undefined;
|
|
||||||
canPin: boolean;
|
|
||||||
participant: TypeGroupCallParticipant;
|
participant: TypeGroupCallParticipant;
|
||||||
className?: string;
|
type: 'video' | 'presentation';
|
||||||
|
onClick?: (id: string, type: 'video' | 'presentation') => void;
|
||||||
|
isFullscreen?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
user?: ApiUser;
|
user?: ApiUser;
|
||||||
chat?: ApiChat;
|
chat?: ApiChat;
|
||||||
|
currentUserId?: string;
|
||||||
|
isActive?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
|
const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
|
||||||
layout,
|
type,
|
||||||
pinnedVideo,
|
onClick,
|
||||||
setPinned,
|
|
||||||
canPin,
|
|
||||||
className,
|
|
||||||
participant,
|
|
||||||
user,
|
user,
|
||||||
chat,
|
chat,
|
||||||
|
isActive,
|
||||||
|
isFullscreen,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
const handleClick = useCallback(() => {
|
||||||
const thumbnailRef = useRef<HTMLCanvasElement>(null);
|
if (onClick) {
|
||||||
// eslint-disable-next-line no-null/no-null
|
onClick(user?.id || chat!.id, type);
|
||||||
const videoRef = useRef<HTMLVideoElement>(null);
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const videoFallbackRef = useRef<HTMLCanvasElement>(null);
|
|
||||||
|
|
||||||
const {
|
|
||||||
x, y, width, height, noAnimate, isRemoved,
|
|
||||||
type,
|
|
||||||
} = layout;
|
|
||||||
const {
|
|
||||||
isSelf, isMutedByMe, isMuted,
|
|
||||||
} = participant;
|
|
||||||
const isPinned = pinnedVideo?.id === participant.id && pinnedVideo?.type === type;
|
|
||||||
const isSpeaking = (participant.amplitude || 0) > THRESHOLD;
|
|
||||||
const isRaiseHand = Boolean(participant.raiseHandRating);
|
|
||||||
const shouldFlipVideo = type === 'video' && participant.isSelf;
|
|
||||||
|
|
||||||
const status = useMemo(() => {
|
|
||||||
if (isSelf) {
|
|
||||||
return lang('ThisIsYou');
|
|
||||||
}
|
}
|
||||||
|
}, [chat, onClick, type, user?.id]);
|
||||||
|
|
||||||
if (isMutedByMe) {
|
if (!user && !chat) return undefined;
|
||||||
return lang('VoipGroupMutedForMe');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRaiseHand) {
|
|
||||||
return lang('WantsToSpeak');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMuted || !isSpeaking) {
|
|
||||||
return lang('Listening');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (participant.volume && participant.volume !== GROUP_CALL_DEFAULT_VOLUME) {
|
|
||||||
return lang('SpeakingWithVolume',
|
|
||||||
(participant.volume / GROUP_CALL_VOLUME_MULTIPLIER).toString())
|
|
||||||
.replace('%%', '%');
|
|
||||||
}
|
|
||||||
|
|
||||||
return lang('Speaking');
|
|
||||||
}, [isSpeaking, participant.volume, lang, isSelf, isMutedByMe, isRaiseHand, isMuted]);
|
|
||||||
|
|
||||||
const prevLayoutRef = useRef<VideoLayout>();
|
|
||||||
if (!isRemoved) {
|
|
||||||
prevLayoutRef.current = layout;
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
x: prevX, y: prevY, width: prevWidth, height: prevHeight,
|
|
||||||
} = prevLayoutRef.current || {};
|
|
||||||
|
|
||||||
const [currentX, currentY, currentWidth, currentHeight] = isRemoved
|
|
||||||
? [prevX, prevY, prevWidth, prevHeight] : [x, y, width, height];
|
|
||||||
|
|
||||||
const [isHidden, setIsHidden] = useState(!noAnimate);
|
|
||||||
|
|
||||||
const streams = getUserStreams(user?.id || chat!.id);
|
const streams = getUserStreams(user?.id || chat!.id);
|
||||||
const actualStream = type === 'video' ? streams?.video : streams?.presentation;
|
|
||||||
const streamRef = useRef(actualStream);
|
|
||||||
if (actualStream?.active && actualStream?.getVideoTracks()[0].enabled) {
|
|
||||||
streamRef.current = actualStream;
|
|
||||||
}
|
|
||||||
const stream = streamRef.current;
|
|
||||||
|
|
||||||
const handleInactive = useLastCallback(() => {
|
|
||||||
const video = videoRef.current;
|
|
||||||
if (!video) return;
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
video.srcObject = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
stream?.addEventListener('inactive', handleInactive);
|
|
||||||
return () => {
|
|
||||||
stream?.removeEventListener('inactive', handleInactive);
|
|
||||||
};
|
|
||||||
}, [handleInactive, stream]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsHidden(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// When video stream is removed, the video element starts showing empty black screen.
|
|
||||||
// To avoid that, we hide the video element and show the fallback frame instead, which is constantly updated
|
|
||||||
// every VIDEO_FALLBACK_UPDATE_INTERVAL milliseconds.
|
|
||||||
useInterval(() => {
|
|
||||||
if (!stream?.active) return;
|
|
||||||
const video = videoRef.current!;
|
|
||||||
const canvas = videoFallbackRef.current!;
|
|
||||||
|
|
||||||
requestMutation(() => {
|
|
||||||
canvas.width = video.videoWidth;
|
|
||||||
canvas.height = video.videoHeight;
|
|
||||||
canvas.getContext('2d')!.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
||||||
});
|
|
||||||
}, VIDEO_FALLBACK_UPDATE_INTERVAL);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const video = videoRef.current;
|
|
||||||
const thumbnail = thumbnailRef.current;
|
|
||||||
if (!video || !thumbnail || !stream) return undefined;
|
|
||||||
|
|
||||||
const ctx = thumbnail.getContext('2d', { alpha: false });
|
|
||||||
if (!ctx) return undefined;
|
|
||||||
|
|
||||||
let isDrawing = true;
|
|
||||||
requestMutation(() => {
|
|
||||||
if (!isDrawing) return;
|
|
||||||
thumbnail.width = 16;
|
|
||||||
thumbnail.height = 16;
|
|
||||||
ctx.filter = 'blur(2px)';
|
|
||||||
|
|
||||||
const draw = () => {
|
|
||||||
if (!isDrawing) return false;
|
|
||||||
if (!stream.active) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, thumbnail.width, thumbnail.height);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
animate(draw, fastRaf);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isDrawing = false;
|
|
||||||
};
|
|
||||||
}, [stream]);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const {
|
|
||||||
isContextMenuOpen,
|
|
||||||
contextMenuPosition,
|
|
||||||
handleContextMenu,
|
|
||||||
handleContextMenuClose,
|
|
||||||
handleContextMenuHide,
|
|
||||||
} = useContextMenuHandlers(ref, isSelf);
|
|
||||||
|
|
||||||
const getTriggerElement = useCallback(() => ref.current, []);
|
|
||||||
|
|
||||||
const getRootElement = useCallback(
|
|
||||||
() => ref.current!.closest('.custom-scroll, .no-scrollbar'),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getMenuElement = useCallback(
|
|
||||||
() => menuRef.current!,
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getLayout = useCallback(
|
|
||||||
() => ({ withPortal: true }),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
|
||||||
} = useMenuPosition(
|
|
||||||
contextMenuPosition,
|
|
||||||
getTriggerElement,
|
|
||||||
getRootElement,
|
|
||||||
getMenuElement,
|
|
||||||
getLayout,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleClickPin = useCallback(() => {
|
|
||||||
setPinned(!isPinned ? {
|
|
||||||
id: user?.id || chat!.id,
|
|
||||||
type,
|
|
||||||
} : undefined);
|
|
||||||
}, [chat, isPinned, setPinned, type, user?.id]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={buildClassName(
|
className={buildClassName('GroupCallParticipantVideo', isActive && 'active')}
|
||||||
styles.wrapper,
|
onClick={handleClick}
|
||||||
(isHidden || isRemoved) && styles.hidden,
|
|
||||||
noAnimate && styles.noAnimate,
|
|
||||||
className,
|
|
||||||
isPinned && styles.pinned,
|
|
||||||
)}
|
|
||||||
style={`--x: ${currentX}px; --y: ${currentY}px; --width: ${currentWidth}px; --height: ${currentHeight}px;`}
|
|
||||||
ref={ref}
|
|
||||||
onContextMenu={handleContextMenu}
|
|
||||||
onDoubleClick={canPin ? handleClickPin : undefined}
|
|
||||||
>
|
>
|
||||||
<div
|
{isFullscreen && (
|
||||||
className={buildClassName(
|
<button className="back-button">
|
||||||
styles.root,
|
<i className="icon icon-arrow-left" />
|
||||||
isSpeaking && styles.speaking,
|
{lang('Back')}
|
||||||
)}
|
</button>
|
||||||
>
|
)}
|
||||||
{stream && (
|
<Avatar user={user} chat={chat} className="thumbnail-avatar" />
|
||||||
<video
|
{!GROUP_CALL_THUMB_VIDEO_DISABLED && (
|
||||||
className={buildClassName(styles.video, shouldFlipVideo && styles.flipped)}
|
<div className="thumbnail-wrapper">
|
||||||
muted
|
<video className="thumbnail" muted autoPlay playsInline srcObject={streams?.[type]} />
|
||||||
autoPlay
|
|
||||||
playsInline
|
|
||||||
srcObject={stream}
|
|
||||||
ref={videoRef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<canvas
|
|
||||||
className={buildClassName(styles.videoFallback, shouldFlipVideo && styles.flipped)}
|
|
||||||
ref={videoFallbackRef}
|
|
||||||
/>
|
|
||||||
<div className={styles.thumbnailWrapper}>
|
|
||||||
<canvas
|
|
||||||
className={buildClassName(styles.thumbnail, shouldFlipVideo && styles.flipped)}
|
|
||||||
ref={thumbnailRef}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{canPin && (
|
|
||||||
<Button
|
|
||||||
round
|
|
||||||
size="smaller"
|
|
||||||
ripple
|
|
||||||
color="translucent"
|
|
||||||
className={styles.pinButton}
|
|
||||||
ariaLabel={lang(isPinned ? 'lng_group_call_context_unpin_camera' : 'lng_group_call_context_pin_camera')}
|
|
||||||
onClick={handleClickPin}
|
|
||||||
>
|
|
||||||
<i className={buildClassName('icon', isPinned ? 'icon-unpin' : 'icon-pin')} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<div className={styles.bottomPanel}>
|
|
||||||
<div className={styles.info}>
|
|
||||||
<FullNameTitle peer={user || chat!} className={styles.name} />
|
|
||||||
<div className={styles.status}>{status}</div>
|
|
||||||
</div>
|
|
||||||
<OutlinedMicrophoneIcon participant={participant} className={styles.icon} noColor />
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<video className="video" muted autoPlay playsInline srcObject={streams?.[type]} />
|
||||||
|
<div className="info">
|
||||||
|
<i className="icon icon-microphone-alt" />
|
||||||
|
<span className="name">{user?.firstName || chat?.title}</span>
|
||||||
|
{type === 'presentation' && <i className="icon last-icon icon-active-sessions" />}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GroupCallParticipantMenu
|
|
||||||
participant={participant}
|
|
||||||
isDropdownOpen={isContextMenuOpen}
|
|
||||||
positionX={positionX}
|
|
||||||
positionY={positionY}
|
|
||||||
transformOriginX={transformOriginX}
|
|
||||||
transformOriginY={transformOriginY}
|
|
||||||
style={menuStyle}
|
|
||||||
onClose={handleContextMenuClose}
|
|
||||||
onCloseAnimationEnd={handleContextMenuHide}
|
|
||||||
menuRef={menuRef}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -316,8 +79,10 @@ const GroupCallParticipantVideo: FC<OwnProps & StateProps> = ({
|
|||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { participant }): StateProps => {
|
(global, { participant }): StateProps => {
|
||||||
return {
|
return {
|
||||||
|
currentUserId: global.currentUserId,
|
||||||
user: participant.isUser ? selectUser(global, participant.id) : undefined,
|
user: participant.isUser ? selectUser(global, participant.id) : undefined,
|
||||||
chat: !participant.isUser ? selectChat(global, participant.id) : undefined,
|
chat: !participant.isUser ? selectChat(global, participant.id) : undefined,
|
||||||
|
isActive: (participant.amplitude || 0) > THRESHOLD,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(GroupCallParticipantVideo));
|
)(GroupCallParticipantVideo));
|
||||||
|
|||||||
@ -39,7 +39,6 @@
|
|||||||
.info {
|
.info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
line-height: 1.125rem;
|
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
@ -59,16 +58,15 @@
|
|||||||
|
|
||||||
.Avatar {
|
.Avatar {
|
||||||
margin: 0 0 0 -0.75rem;
|
margin: 0 0 0 -0.75rem;
|
||||||
font-size: 0.75rem;
|
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
width: 2rem !important;
|
width: 2rem;
|
||||||
height: 2rem !important;
|
height: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
width: 2.25rem !important;
|
width: 2.25rem;
|
||||||
height: 2.25rem !important;
|
height: 2.25rem;
|
||||||
border: 0.125rem solid var(--color-background);
|
border: 0.125rem solid var(--color-background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
.root {
|
|
||||||
position: relative;
|
|
||||||
background: var(--gradient-green);
|
|
||||||
|
|
||||||
&::before, &::after {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
transition: 0.25s ease-in-out opacity, 0.25s ease-in-out transform;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background: var(--gradient-blue);
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
background: var(--gradient-purple);
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
opacity: 0;
|
|
||||||
transition: 0.25s ease-in-out opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinnerVisible {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.canUnmute::before {
|
|
||||||
opacity: 1;
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mutedByAdmin::after {
|
|
||||||
opacity: 1;
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
58
src/components/calls/group/MicrophoneButton.scss
Normal file
58
src/components/calls/group/MicrophoneButton.scss
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
.MicrophoneButton {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
outline: none !important;
|
||||||
|
position: relative;
|
||||||
|
width: 6rem;
|
||||||
|
height: 6rem;
|
||||||
|
border: 0;
|
||||||
|
background: radial-gradient(100% 100% at 100% 0%, #00a0b9 0%, #33c659 55%, #33c659 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #fff;
|
||||||
|
transition: 0.25s ease-out filter;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
width: 8rem;
|
||||||
|
height: 8rem;
|
||||||
|
background: #64C166;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(10px);
|
||||||
|
opacity: 0.2;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
body.is-ios & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.crossed {
|
||||||
|
background: radial-gradient(100% 100% at 100% 0%, #00AFFE 0%, #00AFFE 55%, #007FFF 100%);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background: #00AFFE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.muted-by-admin {
|
||||||
|
background: radial-gradient(85.5% 103.5% at 87.5% 20.65%, #CE4D74 0%, #3D52DF 100%);
|
||||||
|
&::before {
|
||||||
|
background: #3D52DF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-connecting, &.is-connecting:hover {
|
||||||
|
background: #222B34;
|
||||||
|
&::before {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type { GroupCallConnectionState } from '../../../lib/secret-sauce';
|
import type { GroupCallConnectionState } from '../../../lib/secret-sauce';
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
memo, useEffect, useMemo, useRef, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
@ -13,17 +13,11 @@ import { selectActiveGroupCall, selectGroupCallParticipant } from '../../../glob
|
|||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
import AnimatedIcon from '../../common/AnimatedIcon';
|
import AnimatedIcon from '../../common/AnimatedIcon';
|
||||||
import Button from '../../ui/Button';
|
|
||||||
import Spinner from '../../ui/Spinner';
|
|
||||||
|
|
||||||
import styles from './MicrophoneButton.module.scss';
|
import './MicrophoneButton.scss';
|
||||||
|
|
||||||
const CONNECTION_STATE_DEFAULT = 'discarded';
|
const CONNECTION_STATE_DEFAULT = 'discarded';
|
||||||
|
|
||||||
type OwnProps = {
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
connectionState?: GroupCallConnectionState;
|
connectionState?: GroupCallConnectionState;
|
||||||
hasRequestedToSpeak: boolean;
|
hasRequestedToSpeak: boolean;
|
||||||
@ -34,13 +28,13 @@ type StateProps = {
|
|||||||
|
|
||||||
const REQUEST_TO_SPEAK_THROTTLE = 3000;
|
const REQUEST_TO_SPEAK_THROTTLE = 3000;
|
||||||
const HOLD_TO_SPEAK_TIME = 200;
|
const HOLD_TO_SPEAK_TIME = 200;
|
||||||
const ICON_SIZE = 36;
|
const ICON_SIZE = 48;
|
||||||
|
|
||||||
const MicrophoneButton: FC<OwnProps & StateProps> = ({
|
const MicrophoneButton: FC<StateProps> = ({
|
||||||
className,
|
|
||||||
noAudioStream,
|
noAudioStream,
|
||||||
canSelfUnmute,
|
canSelfUnmute,
|
||||||
isMuted,
|
isMuted,
|
||||||
|
hasRequestedToSpeak,
|
||||||
connectionState,
|
connectionState,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -95,12 +89,12 @@ const MicrophoneButton: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const animatedIconName = isRequestingToSpeak ? 'HandFilled' : 'VoiceMini';
|
const animatedIconName = isRequestingToSpeak ? 'HandFilled' : 'VoiceMini';
|
||||||
|
|
||||||
const toggleMute = useCallback(() => {
|
const toggleMute = () => {
|
||||||
vibrateShort();
|
vibrateShort();
|
||||||
toggleGroupCallMute();
|
toggleGroupCallMute();
|
||||||
}, [toggleGroupCallMute]);
|
};
|
||||||
|
|
||||||
const handleMouseDownMute = useCallback(() => {
|
const handleMouseDownMute = () => {
|
||||||
if (shouldRaiseHand) {
|
if (shouldRaiseHand) {
|
||||||
if (isRequestingToSpeak) return;
|
if (isRequestingToSpeak) return;
|
||||||
vibrateShort();
|
vibrateShort();
|
||||||
@ -120,42 +114,47 @@ const MicrophoneButton: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, HOLD_TO_SPEAK_TIME);
|
}, HOLD_TO_SPEAK_TIME);
|
||||||
}
|
}
|
||||||
}, [isRequestingToSpeak, noAudioStream, requestToSpeak, shouldRaiseHand, toggleMute]);
|
};
|
||||||
|
|
||||||
const handleMouseUpMute = useCallback(() => {
|
const handleMouseUpMute = () => {
|
||||||
if (shouldRaiseHand) {
|
if (shouldRaiseHand) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toggleMute();
|
toggleMute();
|
||||||
muteMouseDownState.current = 'up';
|
muteMouseDownState.current = 'up';
|
||||||
}, [shouldRaiseHand, toggleMute]);
|
};
|
||||||
|
|
||||||
|
const buttonText = lang(
|
||||||
|
hasRequestedToSpeak ? 'VoipMutedTapedForSpeak' : (
|
||||||
|
shouldRaiseHand ? 'VoipMutedByAdmin' : (
|
||||||
|
noAudioStream ? 'VoipUnmute' : 'VoipTapToMute'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<div className="button-wrapper microphone-wrapper">
|
||||||
round
|
<button
|
||||||
size="default"
|
className={buildClassName(
|
||||||
className={buildClassName(
|
'MicrophoneButton',
|
||||||
styles.root,
|
noAudioStream && 'crossed',
|
||||||
!isConnecting && noAudioStream && styles.canUnmute,
|
canSelfUnmute && 'can-self-unmute',
|
||||||
!isConnecting && shouldRaiseHand && styles.mutedByAdmin,
|
isConnecting && 'is-connecting',
|
||||||
className,
|
shouldRaiseHand && 'muted-by-admin',
|
||||||
)}
|
)}
|
||||||
onMouseDown={handleMouseDownMute}
|
onMouseDown={handleMouseDownMute}
|
||||||
onMouseUp={handleMouseUpMute}
|
onMouseUp={handleMouseUpMute}
|
||||||
ariaLabel={lang(isMuted ? 'VoipUnmute' : 'VoipMute')}
|
>
|
||||||
disabled={isConnecting}
|
<AnimatedIcon
|
||||||
>
|
tgsUrl={LOCAL_TGS_URLS[animatedIconName]}
|
||||||
<AnimatedIcon
|
size={ICON_SIZE}
|
||||||
tgsUrl={LOCAL_TGS_URLS[animatedIconName]}
|
playSegment={playSegment}
|
||||||
size={ICON_SIZE}
|
/>
|
||||||
play={playSegment.toString()}
|
</button>
|
||||||
playSegment={playSegment}
|
<div className="button-text">
|
||||||
className={styles.icon}
|
{buttonText}
|
||||||
forceOnHeavyAnimation
|
</div>
|
||||||
forceInBackground
|
</div>
|
||||||
/>
|
|
||||||
<Spinner className={buildClassName(styles.spinner, isConnecting && styles.spinnerVisible)} color="white" />
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -11,13 +11,11 @@ import AnimatedIcon from '../../common/AnimatedIcon';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
participant: GroupCallParticipant;
|
participant: GroupCallParticipant;
|
||||||
noColor?: boolean;
|
noColor?: boolean;
|
||||||
className?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const OutlinedMicrophoneIcon: FC<OwnProps> = ({
|
const OutlinedMicrophoneIcon: FC<OwnProps> = ({
|
||||||
participant,
|
participant,
|
||||||
noColor,
|
noColor,
|
||||||
className,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { isMuted, isMutedByMe } = participant;
|
const { isMuted, isMutedByMe } = participant;
|
||||||
const isSpeaking = (participant.amplitude || 0) > THRESHOLD;
|
const isSpeaking = (participant.amplitude || 0) > THRESHOLD;
|
||||||
@ -53,37 +51,21 @@ const OutlinedMicrophoneIcon: FC<OwnProps> = ({
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [isMuted, shouldRaiseHand, isRaiseHand]);
|
}, [isMuted, shouldRaiseHand, isRaiseHand]);
|
||||||
|
|
||||||
const microphoneColor: string = useMemo(() => {
|
const microphoneColor: string | undefined = useMemo(() => {
|
||||||
if (noColor) {
|
return noColor ? '#ffffff' : (
|
||||||
return '#ffffff';
|
isRaiseHand ? '#4da6e0'
|
||||||
}
|
: (shouldRaiseHand || isMutedByMe ? '#ff706f' : (
|
||||||
|
isSpeaking ? '#57bc6c' : '#848d94'
|
||||||
if (isRaiseHand) {
|
))
|
||||||
return '#4da6e0';
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldRaiseHand || isMutedByMe) {
|
|
||||||
return '#ff706f';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSpeaking) {
|
|
||||||
return '#57bc6c';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '#aaaaaa';
|
|
||||||
}, [noColor, isRaiseHand, shouldRaiseHand, isMutedByMe, isSpeaking]);
|
}, [noColor, isRaiseHand, shouldRaiseHand, isMutedByMe, isSpeaking]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedIcon
|
<AnimatedIcon
|
||||||
tgsUrl={LOCAL_TGS_URLS.VoiceOutlined}
|
tgsUrl={LOCAL_TGS_URLS.VoiceOutlined}
|
||||||
play={playSegment.toString()}
|
|
||||||
playSegment={playSegment}
|
playSegment={playSegment}
|
||||||
size={28}
|
size={28}
|
||||||
color={microphoneColor}
|
color={microphoneColor}
|
||||||
className={className}
|
|
||||||
forceOnHeavyAnimation
|
|
||||||
forceInBackground
|
|
||||||
nonInteractive
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,237 +0,0 @@
|
|||||||
import type { RefObject } from 'react';
|
|
||||||
import {
|
|
||||||
useEffect, useMemo, useState,
|
|
||||||
} from '../../../../lib/teact/teact';
|
|
||||||
import useResizeObserver from '../../../../hooks/useResizeObserver';
|
|
||||||
import usePrevious from '../../../../hooks/usePrevious';
|
|
||||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
|
||||||
|
|
||||||
const PADDING_HORIZONTAL = 10;
|
|
||||||
const PADDING_VERTICAL = 8;
|
|
||||||
const PREFERRED_PANEL_VIDEO_HEIGHT = 240;
|
|
||||||
|
|
||||||
export type VideoLayout = {
|
|
||||||
participantId: string;
|
|
||||||
type: 'video' | 'screen';
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
shouldRemount?: boolean;
|
|
||||||
isRemounted?: boolean;
|
|
||||||
noAnimate?: boolean;
|
|
||||||
isRemoved?: boolean;
|
|
||||||
orderKey: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type VideoParticipant = {
|
|
||||||
id: string;
|
|
||||||
type: 'video' | 'screen';
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function useGroupCallVideoLayout({
|
|
||||||
primaryContainerRef,
|
|
||||||
secondaryContainerRef,
|
|
||||||
videoParticipants,
|
|
||||||
isLandscapeLayout,
|
|
||||||
pinnedVideo,
|
|
||||||
}: {
|
|
||||||
primaryContainerRef: RefObject<HTMLDivElement>;
|
|
||||||
secondaryContainerRef: RefObject<HTMLDivElement>;
|
|
||||||
videoParticipants: VideoParticipant[];
|
|
||||||
isLandscapeLayout: boolean;
|
|
||||||
pinnedVideo: VideoParticipant | undefined;
|
|
||||||
}) {
|
|
||||||
const [videoLayout, setVideoLayout] = useState<VideoLayout[]>([]);
|
|
||||||
const [panelOffset, setPanelOffset] = useState(0);
|
|
||||||
const videosCount = videoParticipants.length;
|
|
||||||
const prevVideosCount = usePrevious(videosCount);
|
|
||||||
const prevVideoParticipants = usePrevious(videoParticipants);
|
|
||||||
const removedVideoParticipants = useMemo(() => {
|
|
||||||
return prevVideoParticipants?.filter(
|
|
||||||
({ id, type }) => !videoParticipants.some((p) => p.id === id && p.type === type),
|
|
||||||
);
|
|
||||||
}, [prevVideoParticipants, videoParticipants]);
|
|
||||||
|
|
||||||
const recalculateLayout = useLastCallback(() => {
|
|
||||||
const primaryContainer = primaryContainerRef.current;
|
|
||||||
const secondaryContainer = secondaryContainerRef.current;
|
|
||||||
if (!secondaryContainer) return;
|
|
||||||
|
|
||||||
const removed = prevVideosCount !== undefined && prevVideosCount > videosCount
|
|
||||||
? prevVideosCount - videosCount : 0;
|
|
||||||
|
|
||||||
const {
|
|
||||||
x: secondaryInitialX,
|
|
||||||
y: secondaryInitialY,
|
|
||||||
width: secondaryContainerWidth,
|
|
||||||
} = secondaryContainer.getBoundingClientRect();
|
|
||||||
|
|
||||||
const layout: VideoLayout[] = [];
|
|
||||||
if (pinnedVideo !== undefined || !primaryContainer || !isLandscapeLayout) {
|
|
||||||
const isRemounted = true;
|
|
||||||
let skip = false;
|
|
||||||
let pinnedSkipIndex = 0;
|
|
||||||
let pinnedPush: VideoLayout | undefined;
|
|
||||||
let participants = videoParticipants;
|
|
||||||
if (pinnedVideo && primaryContainer && isLandscapeLayout) {
|
|
||||||
pinnedSkipIndex = participants
|
|
||||||
.findIndex(({ id, type }) => id === pinnedVideo.id && type === pinnedVideo.type);
|
|
||||||
if (pinnedSkipIndex !== -1) {
|
|
||||||
const {
|
|
||||||
x: initialX,
|
|
||||||
y: initialY,
|
|
||||||
width: containerWidth,
|
|
||||||
height: containerHeight,
|
|
||||||
} = primaryContainer.getBoundingClientRect();
|
|
||||||
|
|
||||||
const { id: participantId, type } = pinnedVideo;
|
|
||||||
|
|
||||||
pinnedPush = {
|
|
||||||
x: initialX,
|
|
||||||
y: initialY,
|
|
||||||
width: containerWidth,
|
|
||||||
height: containerHeight,
|
|
||||||
participantId,
|
|
||||||
type,
|
|
||||||
orderKey: pinnedSkipIndex,
|
|
||||||
};
|
|
||||||
skip = true;
|
|
||||||
participants = participants
|
|
||||||
.filter(({ id, type: videoType }) => id !== participantId || videoType !== pinnedVideo.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const secondaryVideosCounts = skip ? videosCount - 1 : videosCount;
|
|
||||||
|
|
||||||
const isFirstBig = secondaryVideosCounts % 2 === 1;
|
|
||||||
const columns = 2;
|
|
||||||
const rows = Math.ceil(secondaryVideosCounts / columns);
|
|
||||||
const smallWidth = (secondaryContainerWidth - (columns - 1) * PADDING_HORIZONTAL) / columns;
|
|
||||||
const heightTotal = Math.max(0, isFirstBig
|
|
||||||
? (PREFERRED_PANEL_VIDEO_HEIGHT + (rows - 1) * smallWidth + (rows - 1) * PADDING_VERTICAL)
|
|
||||||
: rows * smallWidth + (rows - 1) * PADDING_VERTICAL);
|
|
||||||
|
|
||||||
for (let i = 0; i < secondaryVideosCounts; i++) {
|
|
||||||
const isBig = isFirstBig && i === 0;
|
|
||||||
const width = isBig ? secondaryContainerWidth : smallWidth;
|
|
||||||
const height = isBig ? PREFERRED_PANEL_VIDEO_HEIGHT : smallWidth;
|
|
||||||
|
|
||||||
const realIndex = isFirstBig && i !== 0 ? i + 1 : i;
|
|
||||||
|
|
||||||
const x = (isRemounted ? 0 : secondaryInitialX) + (realIndex % columns) * (width + PADDING_HORIZONTAL);
|
|
||||||
const y = (isRemounted ? 0 : secondaryInitialY) + (isFirstBig && i !== 0 ? (
|
|
||||||
PREFERRED_PANEL_VIDEO_HEIGHT + PADDING_VERTICAL
|
|
||||||
+ (Math.floor(realIndex / columns) - 1) * (height + PADDING_VERTICAL)
|
|
||||||
) : (
|
|
||||||
Math.floor(realIndex / columns) * (height + PADDING_VERTICAL)
|
|
||||||
));
|
|
||||||
layout.push({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
shouldRemount: !isRemounted,
|
|
||||||
isRemounted,
|
|
||||||
noAnimate: true,
|
|
||||||
participantId: participants[i].id,
|
|
||||||
type: participants[i].type,
|
|
||||||
orderKey: i >= pinnedSkipIndex ? i + 1 : i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pinnedPush) {
|
|
||||||
layout.splice(pinnedSkipIndex, 0, pinnedPush);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removedVideoParticipants) {
|
|
||||||
for (let i = 0; i < removed; i++) {
|
|
||||||
layout.push({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
isRemounted,
|
|
||||||
isRemoved: true,
|
|
||||||
participantId: removedVideoParticipants[i].id,
|
|
||||||
type: removedVideoParticipants[i].type,
|
|
||||||
orderKey: i + videosCount,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPanelOffset(heightTotal);
|
|
||||||
setVideoLayout(layout);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
x: initialX,
|
|
||||||
y: initialY,
|
|
||||||
width: containerWidth,
|
|
||||||
height: containerHeight,
|
|
||||||
} = primaryContainer.getBoundingClientRect();
|
|
||||||
|
|
||||||
const columns = calculateColumnsCount(videosCount);
|
|
||||||
const rows = Math.ceil(videosCount / columns);
|
|
||||||
const width = (containerWidth - (columns - 1) * PADDING_HORIZONTAL) / columns;
|
|
||||||
const height = (containerHeight - (rows - 1) * PADDING_VERTICAL) / rows;
|
|
||||||
|
|
||||||
for (let i = 0; i < videosCount; i++) {
|
|
||||||
const x = initialX + (i % columns) * (width + PADDING_HORIZONTAL);
|
|
||||||
const y = initialY + Math.floor(i / columns) * (height + PADDING_VERTICAL);
|
|
||||||
layout.push({
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
participantId: videoParticipants[i].id,
|
|
||||||
type: videoParticipants[i].type,
|
|
||||||
orderKey: i,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (removedVideoParticipants) {
|
|
||||||
for (let i = 0; i < removed; i++) {
|
|
||||||
layout.push({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
isRemoved: true,
|
|
||||||
participantId: removedVideoParticipants[i].id,
|
|
||||||
type: removedVideoParticipants[i].type,
|
|
||||||
orderKey: i + videosCount,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPanelOffset(0);
|
|
||||||
setVideoLayout(layout);
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(recalculateLayout, [
|
|
||||||
recalculateLayout, videoParticipants, isLandscapeLayout, pinnedVideo,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useResizeObserver(primaryContainerRef, recalculateLayout, !primaryContainerRef.current);
|
|
||||||
useResizeObserver(secondaryContainerRef, recalculateLayout, !secondaryContainerRef.current);
|
|
||||||
|
|
||||||
return {
|
|
||||||
videoLayout, panelOffset,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateColumnsCount(videosCount: number) {
|
|
||||||
if (videosCount >= 25) {
|
|
||||||
return 5;
|
|
||||||
} else if (videosCount >= 13) {
|
|
||||||
return 4;
|
|
||||||
} else if (videosCount >= 7) {
|
|
||||||
return 3;
|
|
||||||
} else if (videosCount >= 3) {
|
|
||||||
return 2;
|
|
||||||
} else {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -39,7 +39,6 @@ export type OwnProps = {
|
|||||||
color?: string;
|
color?: string;
|
||||||
isLowPriority?: boolean;
|
isLowPriority?: boolean;
|
||||||
forceOnHeavyAnimation?: boolean;
|
forceOnHeavyAnimation?: boolean;
|
||||||
forceInBackground?: boolean;
|
|
||||||
sharedCanvas?: HTMLCanvasElement;
|
sharedCanvas?: HTMLCanvasElement;
|
||||||
sharedCanvasCoords?: { x: number; y: number };
|
sharedCanvasCoords?: { x: number; y: number };
|
||||||
onClick?: NoneToVoidFunction;
|
onClick?: NoneToVoidFunction;
|
||||||
@ -66,7 +65,6 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
isLowPriority,
|
isLowPriority,
|
||||||
color,
|
color,
|
||||||
forceOnHeavyAnimation,
|
forceOnHeavyAnimation,
|
||||||
forceInBackground,
|
|
||||||
sharedCanvas,
|
sharedCanvas,
|
||||||
sharedCanvasCoords,
|
sharedCanvasCoords,
|
||||||
onClick,
|
onClick,
|
||||||
@ -89,7 +87,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
const shouldUseColorFilter = !sharedCanvas && color;
|
const shouldUseColorFilter = !sharedCanvas && color;
|
||||||
const colorFilter = useColorFilter(shouldUseColorFilter ? color : undefined);
|
const colorFilter = useColorFilter(shouldUseColorFilter ? color : undefined);
|
||||||
|
|
||||||
const playKey = play || (play === false ? false : playSegment);
|
const playKey = play || playSegment;
|
||||||
const playRef = useStateRef(play);
|
const playRef = useStateRef(play);
|
||||||
const playSegmentRef = useStateRef(playSegment);
|
const playSegmentRef = useStateRef(playSegment);
|
||||||
|
|
||||||
@ -179,13 +177,13 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
if (
|
if (
|
||||||
!animation
|
!animation
|
||||||
|| !(playRef.current || playSegmentRef.current)
|
|| !(playRef.current || playSegmentRef.current)
|
||||||
|| isFrozen(forceOnHeavyAnimation, forceInBackground)
|
|| isFrozen(forceOnHeavyAnimation)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playSegmentRef.current) {
|
if (playSegmentRef.current) {
|
||||||
animation.playSegment(playSegmentRef.current, shouldRestart, viewId);
|
animation.playSegment(playSegmentRef.current, viewId);
|
||||||
} else {
|
} else {
|
||||||
animation.play(shouldRestart, viewId);
|
animation.play(shouldRestart, viewId);
|
||||||
}
|
}
|
||||||
@ -219,13 +217,13 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (playKey) {
|
if (playKey) {
|
||||||
if (!isFrozen(forceOnHeavyAnimation, forceInBackground)) {
|
if (!isFrozen(forceOnHeavyAnimation)) {
|
||||||
playAnimation(noLoop);
|
playAnimation(noLoop);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
pauseAnimation();
|
pauseAnimation();
|
||||||
}
|
}
|
||||||
}, [animation, playKey, noLoop, playAnimation, pauseAnimation, forceOnHeavyAnimation, forceInBackground]);
|
}, [animation, playKey, noLoop, playAnimation, pauseAnimation, forceOnHeavyAnimation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (animation) {
|
if (animation) {
|
||||||
@ -243,7 +241,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
// Pausing frame may not happen in background,
|
// Pausing frame may not happen in background,
|
||||||
// so we need to make sure it happens right after focusing,
|
// so we need to make sure it happens right after focusing,
|
||||||
// then we can play again.
|
// then we can play again.
|
||||||
useBackgroundMode(pauseAnimation, playAnimationOnRaf, !playKey || forceInBackground);
|
useBackgroundMode(pauseAnimation, playAnimationOnRaf, !playKey);
|
||||||
|
|
||||||
if (sharedCanvas) {
|
if (sharedCanvas) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -266,8 +264,6 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
|
|
||||||
export default memo(AnimatedSticker);
|
export default memo(AnimatedSticker);
|
||||||
|
|
||||||
function isFrozen(forceOnHeavyAnimation = false, forceInBackground = false) {
|
function isFrozen(forceOnHeavyAnimation = false) {
|
||||||
return (!forceOnHeavyAnimation && isHeavyAnimating())
|
return (!forceOnHeavyAnimation && isHeavyAnimating()) || isPriorityPlaybackActive() || isBackgroundModeActive();
|
||||||
|| isPriorityPlaybackActive()
|
|
||||||
|| (!forceInBackground && isBackgroundModeActive());
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,7 +51,6 @@ export type OwnProps = {
|
|||||||
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onClick?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
onContextMenu?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onContextMenu?: (e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
onMouseDown?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
||||||
onMouseUp?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
|
||||||
onMouseEnter?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
onMouseEnter?: (e: ReactMouseEvent<HTMLButtonElement>) => void;
|
||||||
onMouseLeave?: NoneToVoidFunction;
|
onMouseLeave?: NoneToVoidFunction;
|
||||||
onFocus?: NoneToVoidFunction;
|
onFocus?: NoneToVoidFunction;
|
||||||
@ -68,7 +67,6 @@ const Button: FC<OwnProps> = ({
|
|||||||
onClick,
|
onClick,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
onMouseDown,
|
onMouseDown,
|
||||||
onMouseUp,
|
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
onFocus,
|
onFocus,
|
||||||
@ -187,7 +185,6 @@ const Button: FC<OwnProps> = ({
|
|||||||
onClick={IS_TOUCH_ENV || noFastClick ? handleClick : undefined}
|
onClick={IS_TOUCH_ENV || noFastClick ? handleClick : undefined}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseUp={onMouseUp}
|
|
||||||
onMouseEnter={onMouseEnter && !disabled ? onMouseEnter : undefined}
|
onMouseEnter={onMouseEnter && !disabled ? onMouseEnter : undefined}
|
||||||
onMouseLeave={onMouseLeave && !disabled ? onMouseLeave : undefined}
|
onMouseLeave={onMouseLeave && !disabled ? onMouseLeave : undefined}
|
||||||
onTransitionEnd={onTransitionEnd}
|
onTransitionEnd={onTransitionEnd}
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import useLastCallback from '../../hooks/useLastCallback';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
ref?: RefObject<HTMLDivElement>;
|
ref?: RefObject<HTMLDivElement>;
|
||||||
style?: string;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
items?: any[];
|
items?: any[];
|
||||||
itemSelector?: string;
|
itemSelector?: string;
|
||||||
@ -42,7 +41,6 @@ const DEFAULT_SENSITIVE_AREA = 800;
|
|||||||
|
|
||||||
const InfiniteScroll: FC<OwnProps> = ({
|
const InfiniteScroll: FC<OwnProps> = ({
|
||||||
ref,
|
ref,
|
||||||
style,
|
|
||||||
className,
|
className,
|
||||||
items,
|
items,
|
||||||
itemSelector = DEFAULT_LIST_SELECTOR,
|
itemSelector = DEFAULT_LIST_SELECTOR,
|
||||||
@ -240,7 +238,6 @@ const InfiniteScroll: FC<OwnProps> = ({
|
|||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
onDragLeave={onDragLeave}
|
onDragLeave={onDragLeave}
|
||||||
style={style}
|
|
||||||
>
|
>
|
||||||
{beforeChildren}
|
{beforeChildren}
|
||||||
{withAbsolutePositioning && items?.length ? (
|
{withAbsolutePositioning && items?.length ? (
|
||||||
|
|||||||
@ -62,7 +62,6 @@ interface OwnProps {
|
|||||||
href?: string;
|
href?: string;
|
||||||
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
onClick?: (e: React.MouseEvent<HTMLElement>, arg?: any) => void;
|
onClick?: (e: React.MouseEvent<HTMLElement>, arg?: any) => void;
|
||||||
onContextMenu?: (e: React.MouseEvent<HTMLElement>) => void;
|
|
||||||
clickArg?: any;
|
clickArg?: any;
|
||||||
onSecondaryIconClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
onSecondaryIconClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
onDragEnter?: (e: React.DragEvent<HTMLDivElement>) => void;
|
onDragEnter?: (e: React.DragEvent<HTMLDivElement>) => void;
|
||||||
@ -93,7 +92,6 @@ const ListItem: FC<OwnProps> = ({
|
|||||||
href,
|
href,
|
||||||
onMouseDown,
|
onMouseDown,
|
||||||
onClick,
|
onClick,
|
||||||
onContextMenu,
|
|
||||||
clickArg,
|
clickArg,
|
||||||
onSecondaryIconClick,
|
onSecondaryIconClick,
|
||||||
onDragEnter,
|
onDragEnter,
|
||||||
@ -227,7 +225,7 @@ const ListItem: FC<OwnProps> = ({
|
|||||||
tabIndex={!isStatic ? 0 : undefined}
|
tabIndex={!isStatic ? 0 : undefined}
|
||||||
onClick={(!inactive && IS_TOUCH_ENV) ? handleClick : handleClickEvent}
|
onClick={(!inactive && IS_TOUCH_ENV) ? handleClick : handleClickEvent}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onContextMenu={onContextMenu || ((!inactive && contextActions) ? handleContextMenu : undefined)}
|
onContextMenu={(!inactive && contextActions) ? handleContextMenu : undefined}
|
||||||
>
|
>
|
||||||
{leftElement}
|
{leftElement}
|
||||||
{icon && (
|
{icon && (
|
||||||
|
|||||||
@ -262,13 +262,7 @@ addActionHandler('joinGroupCall', async (global, actions, payload): Promise<void
|
|||||||
chatId, id, accessHash, inviteHash, tabId = getCurrentTabId(),
|
chatId, id, accessHash, inviteHash, tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
if (!ARE_CALLS_SUPPORTED) {
|
if (!ARE_CALLS_SUPPORTED) return;
|
||||||
actions.showNotification({
|
|
||||||
message: "Sorry, your browser doesn't support group calls",
|
|
||||||
tabId,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (global.phoneCall) {
|
if (global.phoneCall) {
|
||||||
actions.toggleGroupCallPanel({ tabId });
|
actions.toggleGroupCallPanel({ tabId });
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { GlobalState } from '../types';
|
import type { GlobalState } from '../types';
|
||||||
import { selectChat, selectChatFullInfo } from './chats';
|
import { selectChat, selectChatFullInfo } from './chats';
|
||||||
import { getMainUsername, isChatBasicGroup } from '../helpers';
|
import { isChatBasicGroup } from '../helpers';
|
||||||
import { selectUser } from './users';
|
import { selectUser } from './users';
|
||||||
|
|
||||||
export function selectChatGroupCall<T extends GlobalState>(global: T, chatId: string) {
|
export function selectChatGroupCall<T extends GlobalState>(global: T, chatId: string) {
|
||||||
@ -49,24 +49,3 @@ export function selectPhoneCallUser<T extends GlobalState>(global: T) {
|
|||||||
const id = phoneCall.adminId === currentUserId ? phoneCall.participantId : phoneCall.adminId;
|
const id = phoneCall.adminId === currentUserId ? phoneCall.participantId : phoneCall.adminId;
|
||||||
return selectUser(global, id);
|
return selectUser(global, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectCanInviteToActiveGroupCall<T extends GlobalState>(global: T) {
|
|
||||||
const groupCall = selectActiveGroupCall(global);
|
|
||||||
|
|
||||||
if (!groupCall || !groupCall.chatId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chat = selectChat(global, groupCall.chatId);
|
|
||||||
if (!chat) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasPublicUsername = Boolean(getMainUsername(chat));
|
|
||||||
if (hasPublicUsername) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inviteLink = selectChatFullInfo(global, chat.id)?.inviteLink;
|
|
||||||
return Boolean(inviteLink);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import type { Scheduler } from '../util/schedulers';
|
|||||||
import {
|
import {
|
||||||
throttle, debounce, throttleWith,
|
throttle, debounce, throttleWith,
|
||||||
} from '../util/schedulers';
|
} from '../util/schedulers';
|
||||||
|
import useEffectOnce from './useEffectOnce';
|
||||||
import useHeavyAnimationCheck from './useHeavyAnimationCheck';
|
import useHeavyAnimationCheck from './useHeavyAnimationCheck';
|
||||||
import useLastCallback from './useLastCallback';
|
import useLastCallback from './useLastCallback';
|
||||||
|
|
||||||
@ -170,10 +171,10 @@ export function useIntersectionObserver({
|
|||||||
export function useOnIntersect(
|
export function useOnIntersect(
|
||||||
targetRef: RefObject<HTMLDivElement>, observe?: ObserveFn, callback?: TargetCallback,
|
targetRef: RefObject<HTMLDivElement>, observe?: ObserveFn, callback?: TargetCallback,
|
||||||
) {
|
) {
|
||||||
useEffect(() => {
|
useEffectOnce(() => {
|
||||||
return observe ? observe(targetRef.current!, callback) : undefined;
|
return observe ? observe(targetRef.current!, callback) : undefined;
|
||||||
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
|
// Arguments should never change
|
||||||
}, [observe]);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useIsIntersecting(
|
export function useIsIntersecting(
|
||||||
|
|||||||
@ -191,16 +191,12 @@ class RLottie {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
playSegment([startFrameIndex, stopFrameIndex]: [number, number], forceRestart = false, viewId?: string) {
|
playSegment([startFrameIndex, stopFrameIndex]: [number, number], viewId?: string) {
|
||||||
if (viewId) {
|
if (viewId) {
|
||||||
this.views.get(viewId)!.isPaused = false;
|
this.views.get(viewId)!.isPaused = false;
|
||||||
}
|
}
|
||||||
|
this.approxFrameIndex = Math.floor(startFrameIndex / this.reduceFactor);
|
||||||
const frameIndex = Math.round(this.approxFrameIndex);
|
|
||||||
this.stopFrameIndex = Math.floor(stopFrameIndex / this.reduceFactor);
|
this.stopFrameIndex = Math.floor(stopFrameIndex / this.reduceFactor);
|
||||||
if (frameIndex !== stopFrameIndex || forceRestart) {
|
|
||||||
this.approxFrameIndex = Math.floor(startFrameIndex / this.reduceFactor);
|
|
||||||
}
|
|
||||||
this.direction = startFrameIndex < stopFrameIndex ? 1 : -1;
|
this.direction = startFrameIndex < stopFrameIndex ? 1 : -1;
|
||||||
|
|
||||||
this.doPlay();
|
this.doPlay();
|
||||||
|
|||||||
@ -380,6 +380,7 @@ export async function handleUpdateGroupCallParticipants(updatedParticipants: Gro
|
|||||||
|
|
||||||
const newEndpoints: string[] = [];
|
const newEndpoints: string[] = [];
|
||||||
updatedParticipants.forEach((participant) => {
|
updatedParticipants.forEach((participant) => {
|
||||||
|
console.log('handleUpdateGroupCallParticipants', participant);
|
||||||
if (participant.isSelf) {
|
if (participant.isSelf) {
|
||||||
if (participant.isMuted && !participant.canSelfUnmute) {
|
if (participant.isMuted && !participant.canSelfUnmute) {
|
||||||
// Muted by admin
|
// Muted by admin
|
||||||
@ -481,6 +482,7 @@ export async function handleUpdateGroupCallParticipants(updatedParticipants: Gro
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sdp = buildSdp(conference as Conference);
|
const sdp = buildSdp(conference as Conference);
|
||||||
|
console.log('build sdp!', sdp);
|
||||||
await connection.setRemoteDescription({
|
await connection.setRemoteDescription({
|
||||||
type: 'offer',
|
type: 'offer',
|
||||||
sdp,
|
sdp,
|
||||||
@ -565,6 +567,8 @@ export async function handleUpdateGroupCallConnection(data: GroupCallConnectionD
|
|||||||
...(!isPresentation ? { conference: newConference } : { screenshareConference: newConference }),
|
...(!isPresentation ? { conference: newConference } : { screenshareConference: newConference }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.warn('update remote description', newConference, buildSdp(newConference, true, isPresentation));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connection.setRemoteDescription({
|
await connection.setRemoteDescription({
|
||||||
type: 'answer',
|
type: 'answer',
|
||||||
@ -679,6 +683,7 @@ function initializeConnection(
|
|||||||
if (!isPresentation) {
|
if (!isPresentation) {
|
||||||
connection.oniceconnectionstatechange = () => {
|
connection.oniceconnectionstatechange = () => {
|
||||||
const connectionState = connection.iceConnectionState;
|
const connectionState = connection.iceConnectionState;
|
||||||
|
console.log('ice', connectionState);
|
||||||
if (connectionState === 'connected' || connectionState === 'completed') {
|
if (connectionState === 'connected' || connectionState === 'completed') {
|
||||||
updateConnectionState('connected');
|
updateConnectionState('connected');
|
||||||
} else if (connectionState === 'checking' || connectionState === 'new') {
|
} else if (connectionState === 'checking' || connectionState === 'new') {
|
||||||
@ -702,6 +707,8 @@ function initializeConnection(
|
|||||||
offerToReceiveAudio: !isPresentation,
|
offerToReceiveAudio: !isPresentation,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('created offer!', offer);
|
||||||
|
|
||||||
await connection.setLocalDescription(offer);
|
await connection.setLocalDescription(offer);
|
||||||
|
|
||||||
if (!offer.sdp) {
|
if (!offer.sdp) {
|
||||||
@ -709,6 +716,7 @@ function initializeConnection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sdp = parseSdp(offer);
|
const sdp = parseSdp(offer);
|
||||||
|
console.log('parsed sdp', sdp);
|
||||||
const audioSsrc: Ssrc | undefined = !isPresentation ? {
|
const audioSsrc: Ssrc | undefined = !isPresentation ? {
|
||||||
userId: '',
|
userId: '',
|
||||||
sourceGroups: [
|
sourceGroups: [
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -230,7 +230,6 @@ $color-message-reaction-own-hover: #b5e0a4;
|
|||||||
--z-header-menu: 990;
|
--z-header-menu: 990;
|
||||||
--z-header-menu-backdrop: 980;
|
--z-header-menu-backdrop: 980;
|
||||||
--z-modal: 1510;
|
--z-modal: 1510;
|
||||||
--z-modal-menu: 1600;
|
|
||||||
--z-media-viewer: 1500;
|
--z-media-viewer: 1500;
|
||||||
--z-video-player-controls: 3;
|
--z-video-player-controls: 3;
|
||||||
--z-drop-area: 55;
|
--z-drop-area: 55;
|
||||||
|
|||||||
@ -49,12 +49,6 @@
|
|||||||
.icon-volume-3:before {
|
.icon-volume-3:before {
|
||||||
content: "\e991";
|
content: "\e991";
|
||||||
}
|
}
|
||||||
.icon-settings-filled:before {
|
|
||||||
content: "\e9c1";
|
|
||||||
}
|
|
||||||
.icon-share-screen-stop:before {
|
|
||||||
content: "\e9c2";
|
|
||||||
}
|
|
||||||
.icon-user-online:before {
|
.icon-user-online:before {
|
||||||
content: "\e9c0";
|
content: "\e9c0";
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user