Raise Hand in Group Calls
This commit is contained in:
parent
45aeaeefd4
commit
d6db3f7943
33 changed files with 1050 additions and 51 deletions
|
@ -1772,6 +1772,10 @@
|
||||||
"messageformat": "More options",
|
"messageformat": "More options",
|
||||||
"description": "Tooltip label for button in the calling screen that opens a menu with other call actions such as React or Raise Hand."
|
"description": "Tooltip label for button in the calling screen that opens a menu with other call actions such as React or Raise Hand."
|
||||||
},
|
},
|
||||||
|
"icu:CallingRaisedHandsList__Title": {
|
||||||
|
"messageformat": "Raised hands · {count, plural, one {# person} other {# people}}",
|
||||||
|
"description": "Shown in the call raised hands list to describe how many people have active raised hands"
|
||||||
|
},
|
||||||
"icu:CallingReactions--me": {
|
"icu:CallingReactions--me": {
|
||||||
"messageformat": "You",
|
"messageformat": "You",
|
||||||
"description": "Label next to in-call reactions to indicate that the current user sent that reaction."
|
"description": "Label next to in-call reactions to indicate that the current user sent that reaction."
|
||||||
|
@ -3591,6 +3595,38 @@
|
||||||
"messageformat": "Ringing off",
|
"messageformat": "Ringing off",
|
||||||
"description": "Shown in a group call lobby when call ringing is enabled, then the user disables ringing using the Ringing toggle button."
|
"description": "Shown in a group call lobby when call ringing is enabled, then the user disables ringing using the Ringing toggle button."
|
||||||
},
|
},
|
||||||
|
"icu:CallControls__RaiseHandsToast--you": {
|
||||||
|
"messageformat": "Your hand is raised.",
|
||||||
|
"description": "Shown in a call when the user raises their hand."
|
||||||
|
},
|
||||||
|
"icu:CallControls__RaiseHandsToast--one": {
|
||||||
|
"messageformat": "{name} raised a hand.",
|
||||||
|
"description": "Shown in a call when someone else raises their hand."
|
||||||
|
},
|
||||||
|
"icu:CallControls__RaiseHandsToast--two": {
|
||||||
|
"messageformat": "{name} and {otherName} raised a hand.",
|
||||||
|
"description": "Shown in a call when 2 persons raise their hands."
|
||||||
|
},
|
||||||
|
"icu:CallControls__RaiseHandsToast--more": {
|
||||||
|
"messageformat": "{name}, {otherName}, and {overflowCount, number} more raised a hand.",
|
||||||
|
"description": "Shown in a call when 3 or more persons raise their hands."
|
||||||
|
},
|
||||||
|
"icu:CallControls__RaiseHands--open-queue": {
|
||||||
|
"messageformat": "Open queue",
|
||||||
|
"description": "Link in call raised hands list and in toast shown when someone else raises their hand. Link opens the list of all raised hands."
|
||||||
|
},
|
||||||
|
"icu:CallControls__RaiseHands--lower": {
|
||||||
|
"messageformat": "Lower",
|
||||||
|
"description": "Link in call raised hands list and in toast shown when user raises their hand. Link allows user to lower their hand."
|
||||||
|
},
|
||||||
|
"icu:CallControls__MenuItemRaiseHand": {
|
||||||
|
"messageformat": "Raise Hand",
|
||||||
|
"description": "Menu item to raise your hand during a call."
|
||||||
|
},
|
||||||
|
"icu:CallControls__MenuItemRaiseHand--lower": {
|
||||||
|
"messageformat": "Lower Hand",
|
||||||
|
"description": "Menu item to lower your previously raised hand during a call."
|
||||||
|
},
|
||||||
"icu:callingDeviceSelection__settings": {
|
"icu:callingDeviceSelection__settings": {
|
||||||
"messageformat": "Settings",
|
"messageformat": "Settings",
|
||||||
"description": "Title for device selection settings"
|
"description": "Title for device selection settings"
|
||||||
|
|
1
images/icons/v3/raise_hand/raise_hand-bold.svg
Normal file
1
images/icons/v3/raise_hand/raise_hand-bold.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M17.018 13.436a6.668 6.668 0 0 1-12.678 1.81l-2.75-5.173a1.667 1.667 0 0 1 .569-2.249c.046-.03.1-.057.142-.08l.033-.018c.094-.05.174-.092.25-.126a2.5 2.5 0 0 1 2.833.551V4.167a2.083 2.083 0 0 1 2.532-2.035 2.084 2.084 0 0 1 3.992-.39 2.085 2.085 0 0 1 2.642 2.008v.042a2.083 2.083 0 0 1 2.5 2.042v7.083c0 .18-.022.353-.065.52Zm-9.101-9.27a.417.417 0 0 0-.834 0v7.5H5.504L4.411 9.612a3.95 3.95 0 0 0-.092-.17.833.833 0 0 0-1.051-.321 3.941 3.941 0 0 0-.23.12l-.003.002.001.002.031.059 1.922 3.614h.003l.834 1.568a5.001 5.001 0 0 0 9.574-1.568h.017V5.833a.417.417 0 0 0-.834 0v3.75h-1.666V3.75a.417.417 0 0 0-.834 0v5.833h-1.666V2.5a.417.417 0 0 0-.834 0v7.083H7.917V4.166Z" fill="#000"/></svg>
|
After Width: | Height: | Size: 776 B |
1
images/icons/v3/raise_hand/raise_hand-compact-bold.svg
Normal file
1
images/icons/v3/raise_hand/raise_hand-compact-bold.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.75 10h.002c0 .046 0 .09-.002.135v.032c0 .061-.003.122-.01.182a5.418 5.418 0 0 1-10.345 1.88L1.198 8.097c-.018-.034-.042-.078-.062-.122a1.417 1.417 0 0 1 .547-1.786c.04-.025.085-.05.119-.067l.027-.015c.074-.039.14-.074.204-.103a2.084 2.084 0 0 1 2.217.323V3.333a1.75 1.75 0 0 1 2.044-1.725 1.75 1.75 0 0 1 3.309-.313 1.75 1.75 0 0 1 2.146 1.64 1.75 1.75 0 0 1 2.001 1.732V10Zm-7.5-6.667a.25.25 0 1 0-.5 0v6H4.31l-.855-1.605a3.227 3.227 0 0 0-.07-.131.583.583 0 0 0-.736-.225 1.912 1.912 0 0 0-.108.056l1.71 3.215v.002l.502.945a3.918 3.918 0 0 0 7.497-1.475V4.667a.25.25 0 0 0-.5 0v3h-1.5V3a.25.25 0 1 0-.5 0v4.667h-1.5V2a.25.25 0 0 0-.5 0v5.667h-1.5V3.333Z" fill="#000"/></svg>
|
After Width: | Height: | Size: 764 B |
1
images/icons/v3/raise_hand/raise_hand-compact-light.svg
Normal file
1
images/icons/v3/raise_hand/raise_hand-compact-light.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.5 10.725a5.218 5.218 0 0 1-9.923 1.422L1.374 8.004a1.217 1.217 0 0 1 .413-1.645c.035-.022.074-.042.109-.061l.025-.013a1.883 1.883 0 0 1 2.53.567v-3.52a1.55 1.55 0 0 1 2.006-1.481 1.55 1.55 0 0 1 3.023-.313A1.55 1.55 0 0 1 11.55 3v.184a1.55 1.55 0 0 1 2 1.484v5.666c0 .136-.017.267-.05.392ZM6.45 3.332a.45.45 0 0 0-.9.001v6H4.535l-.903-1.7a3.124 3.124 0 0 0-.078-.142.783.783 0 0 0-.988-.302 3.127 3.127 0 0 0-.192.1l-.009.005a.117.117 0 0 0-.044.146l.004.009.026.048L4.4 11.35v.003l.178.335a4.117 4.117 0 0 0 7.859-1.356h.013V4.667a.45.45 0 1 0-.9 0v3h-1.1V3a.45.45 0 1 0-.9 0v4.667h-1.1V2a.45.45 0 0 0-.9 0v5.667h-1.1V3.332Z" fill="#000"/></svg>
|
After Width: | Height: | Size: 734 B |
1
images/icons/v3/raise_hand/raise_hand-compact.svg
Normal file
1
images/icons/v3/raise_hand/raise_hand-compact.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M13.65 10h.002c0 .046 0 .09-.002.134v.033a5.318 5.318 0 0 1-10.166 2.018L1.287 8.051c-.018-.035-.04-.077-.059-.117A1.317 1.317 0 0 1 1.85 6.21l.026-.014a1.984 1.984 0 0 1 2.475.367v-3.23a1.65 1.65 0 0 1 2.023-1.607 1.65 1.65 0 0 1 3.17-.312A1.651 1.651 0 0 1 11.65 3v.054a1.65 1.65 0 0 1 2 1.613V10Zm-7.3-6.667a.35.35 0 0 0-.7 0v6H4.422l-.878-1.652a3.16 3.16 0 0 0-.075-.137.683.683 0 0 0-.862-.263 3.166 3.166 0 0 0-.185.097l-.004.002a.017.017 0 0 0-.006.019l.002.004.025.047L3.795 10h.002l.85 1.597a4.019 4.019 0 0 0 7.703-1.48v-5.45a.35.35 0 1 0-.7 0v3h-1.3V3a.35.35 0 1 0-.7 0v4.667h-1.3V2a.35.35 0 1 0-.7 0v5.667h-1.3V3.333Z" fill="#000"/></svg>
|
After Width: | Height: | Size: 734 B |
1
images/icons/v3/raise_hand/raise_hand-light.svg
Normal file
1
images/icons/v3/raise_hand/raise_hand-light.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16.814 13.395a6.46 6.46 0 0 1-12.286 1.76l-2.754-5.18A1.458 1.458 0 0 1 2.4 7.928l.03-.015c.096-.052.17-.091.239-.122a2.292 2.292 0 0 1 2.956.999V4.167a1.875 1.875 0 0 1 2.503-1.768 1.875 1.875 0 0 1 3.681-.393 1.875 1.875 0 0 1 2.566 1.744v.315a1.875 1.875 0 0 1 2.5 1.768v7.084c0 .165-.021.325-.061.478Zm-8.689-9.23a.625.625 0 0 0-1.25.002v7.5H5.74L4.595 9.513a3.907 3.907 0 0 0-.1-.182 1.042 1.042 0 0 0-1.313-.401 3.902 3.902 0 0 0-.244.127l-.013.008a.208.208 0 0 0-.074.275l.033.06 1.87 3.517h.01l.842 1.582a5.21 5.21 0 0 0 10.003-1.582h.016V5.833a.625.625 0 0 0-1.25 0v3.75h-1.25V3.75a.625.625 0 0 0-1.25 0v5.833h-1.25V2.5a.625.625 0 0 0-1.25 0v7.083h-1.25V4.166Z" fill="#000"/></svg>
|
After Width: | Height: | Size: 775 B |
1
images/icons/v3/raise_hand/raise_hand.svg
Normal file
1
images/icons/v3/raise_hand/raise_hand.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="20" height="20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16.916 13.416A6.564 6.564 0 0 1 4.434 15.2l-2.753-5.177c-.023-.044-.05-.093-.07-.14a1.562 1.562 0 0 1 .602-1.971c.044-.028.094-.054.137-.077l.032-.017c.095-.05.172-.092.244-.124a2.396 2.396 0 0 1 2.895.739V4.167A1.98 1.98 0 0 1 8.035 2.26a1.98 1.98 0 0 1 3.842-.39 1.98 1.98 0 0 1 2.602 1.88v.172a1.982 1.982 0 0 1 2.5 1.91v7.084a2 2 0 0 1-.063.499ZM8.02 4.166a.52.52 0 0 0-1.042 0v7.5H5.625v.006l-1.122-2.11a3.922 3.922 0 0 0-.096-.176.937.937 0 0 0-1.182-.361 3.911 3.911 0 0 0-.237.124l-.01.005a.104.104 0 0 0-.039.129v-.001.002-.001l.005.009.031.06 2.65 4.983v.006l.142.268a5.105 5.105 0 0 0 9.737-1.692h.017V5.833a.52.52 0 0 0-1.042 0v3.75h-1.458V3.75a.52.52 0 0 0-1.042 0v5.833h-1.458V2.5a.52.52 0 0 0-1.042 0v7.083H8.021V4.166Z" fill="#000"/></svg>
|
After Width: | Height: | Size: 840 B |
|
@ -3806,6 +3806,12 @@ button.module-image__border-overlay:focus {
|
||||||
&__grid {
|
&__grid {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
.module-ongoing-call__group-call-remote-participant--hand-raised
|
||||||
|
.module-ongoing-call__group-call-remote-participant__info__contact-name {
|
||||||
|
display: block;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__overflow {
|
&__overflow {
|
||||||
|
@ -4005,24 +4011,30 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__footer {
|
||||||
align-items: flex-end;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
justify-content: space-between;
|
|
||||||
padding-block: 0 16px;
|
padding-block: 0 16px;
|
||||||
padding-inline: 16px;
|
padding-inline: 16px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: $z-index-above-base;
|
z-index: $z-index-above-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-self: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
&__contact-name {
|
&__contact-name {
|
||||||
|
flex-grow: 1;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
color: $color-white;
|
color: $color-white;
|
||||||
margin-inline-end: 20px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
@ -4030,17 +4042,39 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--hand-raised &__footer {
|
||||||
|
background: transparent;
|
||||||
|
padding-block: 0 8px;
|
||||||
|
padding-inline: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--hand-raised &__info {
|
||||||
|
background: $color-white;
|
||||||
|
border-radius: 40px;
|
||||||
|
|
||||||
|
&__contact-name {
|
||||||
|
display: none;
|
||||||
|
color: $color-black;
|
||||||
|
margin-inline-end: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
.module-ongoing-call__group-call-remote-participant__info {
|
.module-ongoing-call__group-call-remote-participant__info__contact-name {
|
||||||
|
display: block;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(
|
||||||
|
.module-ongoing-call__group-call-remote-participant--hand-raised
|
||||||
|
) {
|
||||||
|
.module-ongoing-call__group-call-remote-participant__footer {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
180deg,
|
180deg,
|
||||||
transparent,
|
transparent,
|
||||||
$color-black-alpha-60 100%
|
$color-black-alpha-60 100%
|
||||||
);
|
);
|
||||||
|
|
||||||
&__contact-name {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4332,6 +4366,7 @@ button.module-image__border-overlay:focus {
|
||||||
|
|
||||||
height: 18px;
|
height: 18px;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
|
margin-inline-end: 4px;
|
||||||
z-index: $z-index-above-base;
|
z-index: $z-index-above-base;
|
||||||
|
|
||||||
@include keyboard-mode {
|
@include keyboard-mode {
|
||||||
|
@ -4344,6 +4379,9 @@ button.module-image__border-overlay:focus {
|
||||||
&__status {
|
&__status {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-basis: 64px;
|
flex-basis: 64px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__muted {
|
&__muted {
|
||||||
|
|
|
@ -68,6 +68,7 @@ $color-ultramarine-dark: #1851b4;
|
||||||
$color-ultramarine-icon: #3a76f0;
|
$color-ultramarine-icon: #3a76f0;
|
||||||
$color-ultramarine-light: #6191f3;
|
$color-ultramarine-light: #6191f3;
|
||||||
$color-ultramarine-dawn: #406ec9;
|
$color-ultramarine-dawn: #406ec9;
|
||||||
|
$color-ultramarine-pastel: #abc4f8;
|
||||||
$color-ultramarine: #2c6bed;
|
$color-ultramarine: #2c6bed;
|
||||||
|
|
||||||
// Flat colors
|
// Flat colors
|
||||||
|
|
|
@ -110,6 +110,10 @@
|
||||||
margin-block: -5px;
|
margin-block: -5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CallControls__MoreOptionsButtonContainer--menu-shown .module-tooltip {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.CallControls__OuterSpacer {
|
.CallControls__OuterSpacer {
|
||||||
// Defined in _modules but duplicated here for ease of refactor
|
// Defined in _modules but duplicated here for ease of refactor
|
||||||
$local-preview-width: 108px;
|
$local-preview-width: 108px;
|
||||||
|
@ -120,13 +124,14 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset-inline-start: min(48%, 40vw);
|
inset-inline-start: min(48%, 40vw);
|
||||||
inset-block-end: 70px;
|
inset-block-end: 70px;
|
||||||
z-index: $z-index-calling;
|
z-index: $z-index-toast;
|
||||||
}
|
}
|
||||||
|
|
||||||
.CallControls__MoreOptionsMenu {
|
.CallControls__MoreOptionsMenu {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-height: calc(var(--window-height) - 155px);
|
max-height: calc(var(--window-height) - 155px);
|
||||||
|
font-size: 13px;
|
||||||
filter: drop-shadow(0px 4px 3px $color-black-alpha-20);
|
filter: drop-shadow(0px 4px 3px $color-black-alpha-20);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
@ -136,6 +141,43 @@
|
||||||
max-width: calc(var(--window-width) / 2 + 20px);
|
max-width: calc(var(--window-width) / 2 + 20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.CallControls__MoreOptionsButtonContainer--menu-shown .module-tooltip {
|
.CallControls__MoreOptionsMenu
|
||||||
opacity: 0;
|
.module-emoji-picker
|
||||||
|
+ .CallControls__MenuItemRaiseHand {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallControls__MoreOptionsMenu .module-ReactionPickerPicker {
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
animation-duration: 200ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallControls__MenuItemRaiseHand {
|
||||||
|
@include button-reset;
|
||||||
|
display: flex;
|
||||||
|
min-width: 290px;
|
||||||
|
padding-block: 12px;
|
||||||
|
padding-inline: 12px;
|
||||||
|
margin-block-start: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
align-items: center;
|
||||||
|
text-align: start;
|
||||||
|
background-color: $color-gray-75;
|
||||||
|
color: $color-white;
|
||||||
|
filter: drop-shadow(0px 4px 3px $color-black-alpha-20);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallControls__MenuItemRaiseHand:hover {
|
||||||
|
background-color: $color-gray-65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallControls__MenuItemRaiseHandIcon {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v3/raise_hand/raise_hand-light.svg',
|
||||||
|
$color-gray-15
|
||||||
|
);
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
margin-inline: 2px 12px;
|
||||||
}
|
}
|
||||||
|
|
96
stylesheets/components/CallingRaisedHandsList.scss
Normal file
96
stylesheets/components/CallingRaisedHandsList.scss
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.CallingRaisedHandsList {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin-block-end: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingRaisedHandsList__width-container {
|
||||||
|
width: 320px;
|
||||||
|
height: auto;
|
||||||
|
margin-block-end: 72px;
|
||||||
|
margin-inline-start: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingRaisedHandsList__overlay {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingRaisedHandsList__overlay-container {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingRaisedHandsList__Overlay {
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingRaisedHandsList__Button {
|
||||||
|
@include button-reset;
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: 16px;
|
||||||
|
inset-block-end: 16px;
|
||||||
|
display: flex;
|
||||||
|
padding-block: 14px;
|
||||||
|
padding-inline: 12px;
|
||||||
|
background: $color-gray-78;
|
||||||
|
border-radius: 24px;
|
||||||
|
color: $color-white;
|
||||||
|
font-size: 14px;
|
||||||
|
z-index: $z-index-above-above-base;
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid $color-ultramarine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingRaisedHandsList__ButtonIcon {
|
||||||
|
display: inline-block;
|
||||||
|
$icon-size: 20px;
|
||||||
|
width: $icon-size;
|
||||||
|
height: $icon-size;
|
||||||
|
margin-inline-end: 4px;
|
||||||
|
content: '';
|
||||||
|
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v3/raise_hand/raise_hand-light.svg',
|
||||||
|
$color-gray-15
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingRaisedHandsList__AvatarAndName {
|
||||||
|
max-width: 205px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingRaisedHandsList__NameHandIcon {
|
||||||
|
display: inline-block;
|
||||||
|
$icon-size: 16px;
|
||||||
|
width: $icon-size;
|
||||||
|
height: $icon-size;
|
||||||
|
content: '';
|
||||||
|
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v3/raise_hand/raise_hand-light.svg',
|
||||||
|
$color-gray-15
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingRaisedHandsList__LowerMyHandLink {
|
||||||
|
@include button-reset;
|
||||||
|
margin-inline-end: 24px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: $color-ultramarine-pastel;
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid $color-ultramarine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
stylesheets/components/CallingRaisedHandsToasts.scss
Normal file
40
stylesheets/components/CallingRaisedHandsToasts.scss
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.CallingReactionsToast__Content {
|
||||||
|
display: flex;
|
||||||
|
margin-block: 4px;
|
||||||
|
margin-inline: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingReactionsToast__HandIcon {
|
||||||
|
display: inline-block;
|
||||||
|
$icon-size: 16px;
|
||||||
|
width: $icon-size;
|
||||||
|
height: $icon-size;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
content: '';
|
||||||
|
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v3/raise_hand/raise_hand-light.svg',
|
||||||
|
$color-white
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingRaisedHandsToasts__Link {
|
||||||
|
@include button-reset;
|
||||||
|
color: $color-ultramarine-pastel;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-inline-start: 16px;
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid $color-ultramarine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-calling-participants-list__status {
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
|
@ -22,6 +22,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CallingStatusIndicator--HandRaised {
|
||||||
|
background: $color-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CallingStatusIndicator--HandRaised::after {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v3/raise_hand/raise_hand-light.svg',
|
||||||
|
$color-black
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
.CallingStatusIndicator--Video::after {
|
.CallingStatusIndicator--Video::after {
|
||||||
@include color-svg(
|
@include color-svg(
|
||||||
'../images/icons/v3/video/video-slash-fill-light.svg',
|
'../images/icons/v3/video/video-slash-fill-light.svg',
|
||||||
|
@ -29,9 +40,25 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-ongoing-call__footer__local-preview .CallingStatusIndicator--Video {
|
.module-ongoing-call__footer__local-preview .CallingStatusIndicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
|
||||||
inset-inline-start: 6px;
|
|
||||||
z-index: $z-index-base;
|
z-index: $z-index-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-ongoing-call__footer__local-preview .CallingStatusIndicator--Video {
|
||||||
|
top: 6px;
|
||||||
|
inset-inline-start: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-ongoing-call__footer__local-preview
|
||||||
|
.CallingStatusIndicator--HandRaised {
|
||||||
|
bottom: 6px;
|
||||||
|
inset-inline-start: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-ongoing-call__participants__grid
|
||||||
|
.module-ongoing-call__group-call-remote-participant--hand-raised
|
||||||
|
.CallingStatusIndicator--HandRaised {
|
||||||
|
margin-block: 1px;
|
||||||
|
margin-inline-start: 5px;
|
||||||
|
}
|
||||||
|
|
|
@ -46,6 +46,8 @@
|
||||||
@import './components/CallingScreenSharingController.scss';
|
@import './components/CallingScreenSharingController.scss';
|
||||||
@import './components/CallingSelectPresentingSourcesModal.scss';
|
@import './components/CallingSelectPresentingSourcesModal.scss';
|
||||||
@import './components/CallingToast.scss';
|
@import './components/CallingToast.scss';
|
||||||
|
@import './components/CallingRaisedHandsList.scss';
|
||||||
|
@import './components/CallingRaisedHandsToasts.scss';
|
||||||
@import './components/CallingReactionsToasts.scss';
|
@import './components/CallingReactionsToasts.scss';
|
||||||
@import './components/ChatColorPicker.scss';
|
@import './components/ChatColorPicker.scss';
|
||||||
@import './components/Checkbox.scss';
|
@import './components/Checkbox.scss';
|
||||||
|
|
|
@ -74,6 +74,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
hangUpActiveCall: action('hang-up-active-call'),
|
hangUpActiveCall: action('hang-up-active-call'),
|
||||||
i18n,
|
i18n,
|
||||||
isGroupCallOutboundRingEnabled: true,
|
isGroupCallOutboundRingEnabled: true,
|
||||||
|
isGroupCallRaiseHandEnabled: true,
|
||||||
isGroupCallReactionsEnabled: true,
|
isGroupCallReactionsEnabled: true,
|
||||||
keyChangeOk: action('key-change-ok'),
|
keyChangeOk: action('key-change-ok'),
|
||||||
me: {
|
me: {
|
||||||
|
@ -90,6 +91,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
renderEmojiPicker: () => <>EmojiPicker</>,
|
renderEmojiPicker: () => <>EmojiPicker</>,
|
||||||
renderReactionPicker: () => <div />,
|
renderReactionPicker: () => <div />,
|
||||||
renderSafetyNumberViewer: (_: SafetyNumberProps) => <div />,
|
renderSafetyNumberViewer: (_: SafetyNumberProps) => <div />,
|
||||||
|
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
|
||||||
sendGroupCallReaction: action('send-group-call-reaction'),
|
sendGroupCallReaction: action('send-group-call-reaction'),
|
||||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||||
setIsCallActive: action('set-is-call-active'),
|
setIsCallActive: action('set-is-call-active'),
|
||||||
|
@ -159,6 +161,7 @@ export function OngoingGroupCall(): JSX.Element {
|
||||||
groupMembers: [],
|
groupMembers: [],
|
||||||
isConversationTooBigToRing: false,
|
isConversationTooBigToRing: false,
|
||||||
peekedParticipants: [],
|
peekedParticipants: [],
|
||||||
|
raisedHands: new Set<number>(),
|
||||||
remoteParticipants: [],
|
remoteParticipants: [],
|
||||||
remoteAudioLevels: new Map<number, number>(),
|
remoteAudioLevels: new Map<number, number>(),
|
||||||
},
|
},
|
||||||
|
@ -247,6 +250,7 @@ export function GroupCallSafetyNumberChanged(): JSX.Element {
|
||||||
groupMembers: [],
|
groupMembers: [],
|
||||||
isConversationTooBigToRing: false,
|
isConversationTooBigToRing: false,
|
||||||
peekedParticipants: [],
|
peekedParticipants: [],
|
||||||
|
raisedHands: new Set<number>(),
|
||||||
remoteParticipants: [],
|
remoteParticipants: [],
|
||||||
remoteAudioLevels: new Map<number, number>(),
|
remoteAudioLevels: new Map<number, number>(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -33,6 +33,7 @@ import type {
|
||||||
CancelCallType,
|
CancelCallType,
|
||||||
DeclineCallType,
|
DeclineCallType,
|
||||||
KeyChangeOkType,
|
KeyChangeOkType,
|
||||||
|
SendGroupCallRaiseHandType,
|
||||||
SendGroupCallReactionType,
|
SendGroupCallReactionType,
|
||||||
SetGroupCallVideoRequestType,
|
SetGroupCallVideoRequestType,
|
||||||
SetLocalAudioType,
|
SetLocalAudioType,
|
||||||
|
@ -87,6 +88,7 @@ export type PropsType = {
|
||||||
declineCall: (_: DeclineCallType) => void;
|
declineCall: (_: DeclineCallType) => void;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isGroupCallOutboundRingEnabled: boolean;
|
isGroupCallOutboundRingEnabled: boolean;
|
||||||
|
isGroupCallRaiseHandEnabled: boolean;
|
||||||
isGroupCallReactionsEnabled: boolean;
|
isGroupCallReactionsEnabled: boolean;
|
||||||
me: ConversationType;
|
me: ConversationType;
|
||||||
notifyForCall: (
|
notifyForCall: (
|
||||||
|
@ -96,6 +98,7 @@ export type PropsType = {
|
||||||
) => unknown;
|
) => unknown;
|
||||||
openSystemPreferencesAction: () => unknown;
|
openSystemPreferencesAction: () => unknown;
|
||||||
playRingtone: () => unknown;
|
playRingtone: () => unknown;
|
||||||
|
sendGroupCallRaiseHand: (payload: SendGroupCallRaiseHandType) => void;
|
||||||
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
|
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
|
||||||
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
|
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
|
||||||
setIsCallActive: (_: boolean) => void;
|
setIsCallActive: (_: boolean) => void;
|
||||||
|
@ -130,6 +133,7 @@ function ActiveCallManager({
|
||||||
hangUpActiveCall,
|
hangUpActiveCall,
|
||||||
i18n,
|
i18n,
|
||||||
isGroupCallOutboundRingEnabled,
|
isGroupCallOutboundRingEnabled,
|
||||||
|
isGroupCallRaiseHandEnabled,
|
||||||
isGroupCallReactionsEnabled,
|
isGroupCallReactionsEnabled,
|
||||||
keyChangeOk,
|
keyChangeOk,
|
||||||
getGroupCallVideoFrameSource,
|
getGroupCallVideoFrameSource,
|
||||||
|
@ -141,6 +145,7 @@ function ActiveCallManager({
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
renderReactionPicker,
|
renderReactionPicker,
|
||||||
renderSafetyNumberViewer,
|
renderSafetyNumberViewer,
|
||||||
|
sendGroupCallRaiseHand,
|
||||||
sendGroupCallReaction,
|
sendGroupCallReaction,
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
|
@ -341,11 +346,13 @@ function ActiveCallManager({
|
||||||
groupMembers={groupMembers}
|
groupMembers={groupMembers}
|
||||||
hangUpActiveCall={hangUpActiveCall}
|
hangUpActiveCall={hangUpActiveCall}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
isGroupCallRaiseHandEnabled={isGroupCallRaiseHandEnabled}
|
||||||
isGroupCallReactionsEnabled={isGroupCallReactionsEnabled}
|
isGroupCallReactionsEnabled={isGroupCallReactionsEnabled}
|
||||||
me={me}
|
me={me}
|
||||||
openSystemPreferencesAction={openSystemPreferencesAction}
|
openSystemPreferencesAction={openSystemPreferencesAction}
|
||||||
renderEmojiPicker={renderEmojiPicker}
|
renderEmojiPicker={renderEmojiPicker}
|
||||||
renderReactionPicker={renderReactionPicker}
|
renderReactionPicker={renderReactionPicker}
|
||||||
|
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
|
||||||
sendGroupCallReaction={sendGroupCallReaction}
|
sendGroupCallReaction={sendGroupCallReaction}
|
||||||
setGroupCallVideoRequest={setGroupCallVideoRequestForConversation}
|
setGroupCallVideoRequest={setGroupCallVideoRequestForConversation}
|
||||||
setLocalPreview={setLocalPreview}
|
setLocalPreview={setLocalPreview}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import enMessages from '../../_locales/en/messages.json';
|
||||||
import { CallingToastProvider, useCallingToasts } from './CallingToast';
|
import { CallingToastProvider, useCallingToasts } from './CallingToast';
|
||||||
|
|
||||||
const MAX_PARTICIPANTS = 75;
|
const MAX_PARTICIPANTS = 75;
|
||||||
|
const LOCAL_DEMUX_ID = 1;
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -66,6 +67,7 @@ type GroupCallOverrideProps = OverridePropsBase & {
|
||||||
callMode: CallMode.Group;
|
callMode: CallMode.Group;
|
||||||
connectionState?: GroupCallConnectionState;
|
connectionState?: GroupCallConnectionState;
|
||||||
peekedParticipants?: Array<ConversationType>;
|
peekedParticipants?: Array<ConversationType>;
|
||||||
|
raisedHands?: Set<number>;
|
||||||
remoteParticipants?: Array<GroupCallRemoteParticipantType>;
|
remoteParticipants?: Array<GroupCallRemoteParticipantType>;
|
||||||
remoteAudioLevel?: number;
|
remoteAudioLevel?: number;
|
||||||
};
|
};
|
||||||
|
@ -92,12 +94,8 @@ const createActiveDirectCallProp = (
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
const getConversationsByDemuxId = (overrideProps: GroupCallOverrideProps) => {
|
||||||
callMode: CallMode.Group as CallMode.Group,
|
const conversationsByDemuxId = new Map<number, ConversationType>(
|
||||||
connectionState:
|
|
||||||
overrideProps.connectionState || GroupCallConnectionState.Connected,
|
|
||||||
conversationsWithSafetyNumberChanges: [],
|
|
||||||
conversationsByDemuxId: new Map<number, ConversationType>(
|
|
||||||
overrideProps.remoteParticipants?.map((participant, index) => [
|
overrideProps.remoteParticipants?.map((participant, index) => [
|
||||||
participant.demuxId,
|
participant.demuxId,
|
||||||
getDefaultConversationWithServiceId({
|
getDefaultConversationWithServiceId({
|
||||||
|
@ -105,9 +103,19 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
||||||
title: `Participant ${index + 1}`,
|
title: `Participant ${index + 1}`,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
),
|
);
|
||||||
|
conversationsByDemuxId.set(LOCAL_DEMUX_ID, conversation);
|
||||||
|
return conversationsByDemuxId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
||||||
|
callMode: CallMode.Group as CallMode.Group,
|
||||||
|
connectionState:
|
||||||
|
overrideProps.connectionState || GroupCallConnectionState.Connected,
|
||||||
|
conversationsWithSafetyNumberChanges: [],
|
||||||
|
conversationsByDemuxId: getConversationsByDemuxId(overrideProps),
|
||||||
joinState: GroupCallJoinState.Joined,
|
joinState: GroupCallJoinState.Joined,
|
||||||
localDemuxId: 1,
|
localDemuxId: LOCAL_DEMUX_ID,
|
||||||
maxDevices: 5,
|
maxDevices: 5,
|
||||||
deviceCount: (overrideProps.remoteParticipants || []).length,
|
deviceCount: (overrideProps.remoteParticipants || []).length,
|
||||||
groupMembers: overrideProps.remoteParticipants || [],
|
groupMembers: overrideProps.remoteParticipants || [],
|
||||||
|
@ -116,6 +124,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
||||||
isConversationTooBigToRing: false,
|
isConversationTooBigToRing: false,
|
||||||
peekedParticipants:
|
peekedParticipants:
|
||||||
overrideProps.peekedParticipants || overrideProps.remoteParticipants || [],
|
overrideProps.peekedParticipants || overrideProps.remoteParticipants || [],
|
||||||
|
raisedHands: overrideProps.raisedHands || new Set<number>(),
|
||||||
remoteParticipants: overrideProps.remoteParticipants || [],
|
remoteParticipants: overrideProps.remoteParticipants || [],
|
||||||
remoteAudioLevels: new Map<number, number>(
|
remoteAudioLevels: new Map<number, number>(
|
||||||
overrideProps.remoteParticipants?.map((_participant, index) => [
|
overrideProps.remoteParticipants?.map((_participant, index) => [
|
||||||
|
@ -163,6 +172,7 @@ const createProps = (
|
||||||
getPresentingSources: action('get-presenting-sources'),
|
getPresentingSources: action('get-presenting-sources'),
|
||||||
hangUpActiveCall: action('hang-up'),
|
hangUpActiveCall: action('hang-up'),
|
||||||
i18n,
|
i18n,
|
||||||
|
isGroupCallRaiseHandEnabled: true,
|
||||||
isGroupCallReactionsEnabled: true,
|
isGroupCallReactionsEnabled: true,
|
||||||
me: getDefaultConversation({
|
me: getDefaultConversation({
|
||||||
color: AvatarColors[1],
|
color: AvatarColors[1],
|
||||||
|
@ -175,6 +185,7 @@ const createProps = (
|
||||||
openSystemPreferencesAction: action('open-system-preferences-action'),
|
openSystemPreferencesAction: action('open-system-preferences-action'),
|
||||||
renderEmojiPicker: () => <>EmojiPicker</>,
|
renderEmojiPicker: () => <>EmojiPicker</>,
|
||||||
renderReactionPicker: () => <div />,
|
renderReactionPicker: () => <div />,
|
||||||
|
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
|
||||||
sendGroupCallReaction: action('send-group-call-reaction'),
|
sendGroupCallReaction: action('send-group-call-reaction'),
|
||||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||||
setLocalAudio: action('set-local-audio'),
|
setLocalAudio: action('set-local-audio'),
|
||||||
|
@ -299,6 +310,7 @@ export function GroupCall1(): JSX.Element {
|
||||||
demuxId: 0,
|
demuxId: 0,
|
||||||
hasRemoteAudio: true,
|
hasRemoteAudio: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
isHandRaised: false,
|
||||||
presenting: false,
|
presenting: false,
|
||||||
sharingScreen: false,
|
sharingScreen: false,
|
||||||
videoAspectRatio: 1.3,
|
videoAspectRatio: 1.3,
|
||||||
|
@ -314,12 +326,41 @@ export function GroupCall1(): JSX.Element {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GroupCallYourHandRaised(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<CallScreen
|
||||||
|
{...createProps({
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
remoteParticipants: [
|
||||||
|
{
|
||||||
|
aci: generateAci(),
|
||||||
|
demuxId: 0,
|
||||||
|
hasRemoteAudio: true,
|
||||||
|
hasRemoteVideo: true,
|
||||||
|
isHandRaised: false,
|
||||||
|
presenting: false,
|
||||||
|
sharingScreen: false,
|
||||||
|
videoAspectRatio: 1.3,
|
||||||
|
...getDefaultConversation({
|
||||||
|
isBlocked: false,
|
||||||
|
serviceId: generateAci(),
|
||||||
|
title: 'Tyler',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
raisedHands: new Set([LOCAL_DEMUX_ID]),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// We generate these upfront so that the list is stable when you move the slider.
|
// We generate these upfront so that the list is stable when you move the slider.
|
||||||
const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
|
const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
|
||||||
aci: generateAci(),
|
aci: generateAci(),
|
||||||
demuxId: index,
|
demuxId: index,
|
||||||
hasRemoteAudio: index % 3 !== 0,
|
hasRemoteAudio: index % 3 !== 0,
|
||||||
hasRemoteVideo: index % 4 !== 0,
|
hasRemoteVideo: index % 4 !== 0,
|
||||||
|
isHandRaised: (index - 3) % 10 === 0,
|
||||||
presenting: false,
|
presenting: false,
|
||||||
sharingScreen: false,
|
sharingScreen: false,
|
||||||
videoAspectRatio: Math.random() < 0.7 ? 1.3 : Math.random() * 0.4 + 0.6,
|
videoAspectRatio: Math.random() < 0.7 ? 1.3 : Math.random() * 0.4 + 0.6,
|
||||||
|
@ -406,6 +447,7 @@ export function GroupCallReconnecting(): JSX.Element {
|
||||||
demuxId: 0,
|
demuxId: 0,
|
||||||
hasRemoteAudio: true,
|
hasRemoteAudio: true,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
isHandRaised: false,
|
||||||
presenting: false,
|
presenting: false,
|
||||||
sharingScreen: false,
|
sharingScreen: false,
|
||||||
videoAspectRatio: 1.3,
|
videoAspectRatio: 1.3,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import classNames from 'classnames';
|
||||||
import type { VideoFrameSource } from '@signalapp/ringrtc';
|
import type { VideoFrameSource } from '@signalapp/ringrtc';
|
||||||
import type {
|
import type {
|
||||||
ActiveCallStateType,
|
ActiveCallStateType,
|
||||||
|
SendGroupCallRaiseHandType,
|
||||||
SendGroupCallReactionType,
|
SendGroupCallReactionType,
|
||||||
SetLocalAudioType,
|
SetLocalAudioType,
|
||||||
SetLocalPreviewType,
|
SetLocalPreviewType,
|
||||||
|
@ -74,6 +75,7 @@ import { handleOutsideClick } from '../util/handleOutsideClick';
|
||||||
import type { Props as ReactionPickerProps } from './conversation/ReactionPicker';
|
import type { Props as ReactionPickerProps } from './conversation/ReactionPicker';
|
||||||
import type { SmartReactionPicker } from '../state/smart/ReactionPicker';
|
import type { SmartReactionPicker } from '../state/smart/ReactionPicker';
|
||||||
import { Emoji } from './emoji/Emoji';
|
import { Emoji } from './emoji/Emoji';
|
||||||
|
import { CallingRaisedHandsList } from './CallingRaisedHandsList';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
activeCall: ActiveCallType;
|
activeCall: ActiveCallType;
|
||||||
|
@ -82,12 +84,14 @@ export type PropsType = {
|
||||||
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
||||||
hangUpActiveCall: (reason: string) => void;
|
hangUpActiveCall: (reason: string) => void;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
isGroupCallRaiseHandEnabled: boolean;
|
||||||
isGroupCallReactionsEnabled: boolean;
|
isGroupCallReactionsEnabled: boolean;
|
||||||
me: ConversationType;
|
me: ConversationType;
|
||||||
openSystemPreferencesAction: () => unknown;
|
openSystemPreferencesAction: () => unknown;
|
||||||
renderReactionPicker: (
|
renderReactionPicker: (
|
||||||
props: React.ComponentProps<typeof SmartReactionPicker>
|
props: React.ComponentProps<typeof SmartReactionPicker>
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
|
sendGroupCallRaiseHand: (payload: SendGroupCallRaiseHandType) => void;
|
||||||
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
|
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
|
||||||
setGroupCallVideoRequest: (
|
setGroupCallVideoRequest: (
|
||||||
_: Array<GroupCallVideoRequest>,
|
_: Array<GroupCallVideoRequest>,
|
||||||
|
@ -155,12 +159,14 @@ export function CallScreen({
|
||||||
groupMembers,
|
groupMembers,
|
||||||
hangUpActiveCall,
|
hangUpActiveCall,
|
||||||
i18n,
|
i18n,
|
||||||
|
isGroupCallRaiseHandEnabled,
|
||||||
isGroupCallReactionsEnabled,
|
isGroupCallReactionsEnabled,
|
||||||
me,
|
me,
|
||||||
openSystemPreferencesAction,
|
openSystemPreferencesAction,
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
renderReactionPicker,
|
renderReactionPicker,
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
|
sendGroupCallRaiseHand,
|
||||||
sendGroupCallReaction,
|
sendGroupCallReaction,
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
|
@ -232,6 +238,11 @@ export function CallScreen({
|
||||||
setShowMoreOptions(prevValue => !prevValue);
|
setShowMoreOptions(prevValue => !prevValue);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [showRaisedHandsList, setShowRaisedHandsList] = useState(false);
|
||||||
|
const toggleRaisedHandsList = useCallback(() => {
|
||||||
|
setShowRaisedHandsList(prevValue => !prevValue);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [controlsHover, setControlsHover] = useState(false);
|
const [controlsHover, setControlsHover] = useState(false);
|
||||||
|
|
||||||
const onControlsMouseEnter = useCallback(() => {
|
const onControlsMouseEnter = useCallback(() => {
|
||||||
|
@ -460,7 +471,8 @@ export function CallScreen({
|
||||||
});
|
});
|
||||||
|
|
||||||
const isGroupCall = activeCall.callMode === CallMode.Group;
|
const isGroupCall = activeCall.callMode === CallMode.Group;
|
||||||
const isMoreOptionsButtonEnabled = isGroupCall && isGroupCallReactionsEnabled;
|
const isMoreOptionsButtonEnabled =
|
||||||
|
isGroupCall && (isGroupCallRaiseHandEnabled || isGroupCallReactionsEnabled);
|
||||||
|
|
||||||
let presentingButtonType: CallingButtonType;
|
let presentingButtonType: CallingButtonType;
|
||||||
if (presentingSource) {
|
if (presentingSource) {
|
||||||
|
@ -471,6 +483,110 @@ export function CallScreen({
|
||||||
presentingButtonType = CallingButtonType.PRESENTING_OFF;
|
presentingButtonType = CallingButtonType.PRESENTING_OFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const raisedHands =
|
||||||
|
activeCall.callMode === CallMode.Group ? activeCall.raisedHands : undefined;
|
||||||
|
|
||||||
|
// This is the value of our hand raised as seen by remote clients. We should prefer
|
||||||
|
// to use it in UI so the user understands what remote clients see.
|
||||||
|
const syncedLocalHandRaised = isHandRaised(raisedHands, localDemuxId);
|
||||||
|
|
||||||
|
// Don't call setLocalHandRaised because it only sets local state. Instead call
|
||||||
|
// toggleRaiseHand() which will set ringrtc state and call setLocalHandRaised.
|
||||||
|
const [localHandRaised, setLocalHandRaised] = useState<boolean>(
|
||||||
|
syncedLocalHandRaised
|
||||||
|
);
|
||||||
|
const previousLocalHandRaised = usePrevious(localHandRaised, localHandRaised);
|
||||||
|
const toggleRaiseHand = useCallback(
|
||||||
|
(raise?: boolean) => {
|
||||||
|
const nextValue = raise ?? !localHandRaised;
|
||||||
|
if (nextValue === previousLocalHandRaised) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalHandRaised(nextValue);
|
||||||
|
// It's possible that the ringrtc call can fail due to flaky network connection.
|
||||||
|
// In that case, local and remote state (localHandRaised and raisedHands) can
|
||||||
|
// get out of sync. The user might need to manually toggle raise hand to get to
|
||||||
|
// a coherent state. It would be nice if this returned a Promise (but it doesn't)
|
||||||
|
sendGroupCallRaiseHand({
|
||||||
|
conversationId: conversation.id,
|
||||||
|
raise: nextValue,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[
|
||||||
|
localHandRaised,
|
||||||
|
previousLocalHandRaised,
|
||||||
|
conversation.id,
|
||||||
|
sendGroupCallRaiseHand,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderRaisedHandsToast = React.useCallback(
|
||||||
|
(hands: Array<number>) => {
|
||||||
|
const names = hands.map(demuxId =>
|
||||||
|
demuxId === localDemuxId
|
||||||
|
? i18n('icu:you')
|
||||||
|
: conversationsByDemuxId.get(demuxId)?.title
|
||||||
|
);
|
||||||
|
|
||||||
|
let message: string;
|
||||||
|
let buttonOverride: JSX.Element | undefined;
|
||||||
|
const count = names.length;
|
||||||
|
switch (count) {
|
||||||
|
case 0:
|
||||||
|
return undefined;
|
||||||
|
case 1:
|
||||||
|
if (names[0] === i18n('icu:you')) {
|
||||||
|
message = i18n('icu:CallControls__RaiseHandsToast--you');
|
||||||
|
buttonOverride = (
|
||||||
|
<button
|
||||||
|
className="CallingRaisedHandsToasts__Link"
|
||||||
|
onClick={() => toggleRaiseHand(false)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{i18n('icu:CallControls__RaiseHands--lower')}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
message = i18n('icu:CallControls__RaiseHandsToast--one', {
|
||||||
|
name: names[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
message = i18n('icu:CallControls__RaiseHandsToast--two', {
|
||||||
|
name: names[0],
|
||||||
|
otherName: names[1],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
message = i18n('icu:CallControls__RaiseHandsToast--more', {
|
||||||
|
name: names[0],
|
||||||
|
otherName: names[1],
|
||||||
|
overflowCount: names.length - 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="CallingReactionsToast__Content">
|
||||||
|
<span className="CallingReactionsToast__HandIcon" />
|
||||||
|
{message}
|
||||||
|
{buttonOverride || (
|
||||||
|
<button
|
||||||
|
className="link CallingRaisedHandsToasts__Link"
|
||||||
|
onClick={() => setShowRaisedHandsList(true)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{i18n('icu:CallControls__RaiseHands--open-queue')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[i18n, localDemuxId, conversationsByDemuxId, toggleRaiseHand]
|
||||||
|
);
|
||||||
|
|
||||||
|
const raisedHandsCount: number = raisedHands?.size ?? 0;
|
||||||
|
|
||||||
const callStatus: ReactNode | string = React.useMemo(() => {
|
const callStatus: ReactNode | string = React.useMemo(() => {
|
||||||
if (isRinging) {
|
if (isRinging) {
|
||||||
return i18n('icu:outgoingCallRinging');
|
return i18n('icu:outgoingCallRinging');
|
||||||
|
@ -599,6 +715,39 @@ export function CallScreen({
|
||||||
localDemuxId={localDemuxId}
|
localDemuxId={localDemuxId}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
|
{raisedHands && raisedHandsCount > 0 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="CallingRaisedHandsList__Button"
|
||||||
|
onClick={toggleRaisedHandsList}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="CallingRaisedHandsList__ButtonIcon" />
|
||||||
|
{syncedLocalHandRaised ? (
|
||||||
|
<>
|
||||||
|
{i18n('icu:you')}
|
||||||
|
{raisedHandsCount > 1 && ` + ${String(raisedHandsCount - 1)}`}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
raisedHandsCount
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{showRaisedHandsList && (
|
||||||
|
<CallingRaisedHandsList
|
||||||
|
i18n={i18n}
|
||||||
|
onClose={() => setShowRaisedHandsList(false)}
|
||||||
|
onLowerMyHand={() => {
|
||||||
|
toggleRaiseHand(false);
|
||||||
|
setShowRaisedHandsList(false);
|
||||||
|
}}
|
||||||
|
localDemuxId={localDemuxId}
|
||||||
|
conversationsByDemuxId={conversationsByDemuxId}
|
||||||
|
raisedHands={raisedHands}
|
||||||
|
localHandRaised={syncedLocalHandRaised}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="module-ongoing-call__footer">
|
<div className="module-ongoing-call__footer">
|
||||||
<div className="module-calling__spacer CallControls__OuterSpacer" />
|
<div className="module-calling__spacer CallControls__OuterSpacer" />
|
||||||
<div
|
<div
|
||||||
|
@ -616,6 +765,8 @@ export function CallScreen({
|
||||||
<CallingButtonToastsContainer
|
<CallingButtonToastsContainer
|
||||||
hasLocalAudio={hasLocalAudio}
|
hasLocalAudio={hasLocalAudio}
|
||||||
outgoingRing={undefined}
|
outgoingRing={undefined}
|
||||||
|
raisedHands={raisedHands}
|
||||||
|
renderRaisedHandsToast={renderRaisedHandsToast}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -625,18 +776,34 @@ export function CallScreen({
|
||||||
className="CallControls__MoreOptionsMenu"
|
className="CallControls__MoreOptionsMenu"
|
||||||
ref={moreOptionsMenuRef}
|
ref={moreOptionsMenuRef}
|
||||||
>
|
>
|
||||||
{renderReactionPicker({
|
{isGroupCallReactionsEnabled &&
|
||||||
ref: reactionPickerRef,
|
renderReactionPicker({
|
||||||
onClose: () => setShowMoreOptions(false),
|
ref: reactionPickerRef,
|
||||||
onPick: emoji => {
|
onClose: () => setShowMoreOptions(false),
|
||||||
setShowMoreOptions(false);
|
onPick: emoji => {
|
||||||
sendGroupCallReaction({
|
setShowMoreOptions(false);
|
||||||
conversationId: conversation.id,
|
sendGroupCallReaction({
|
||||||
value: emoji,
|
conversationId: conversation.id,
|
||||||
});
|
value: emoji,
|
||||||
},
|
});
|
||||||
renderEmojiPicker,
|
},
|
||||||
})}
|
renderEmojiPicker,
|
||||||
|
})}
|
||||||
|
{isGroupCallRaiseHandEnabled && (
|
||||||
|
<button
|
||||||
|
className="CallControls__MenuItemRaiseHand"
|
||||||
|
onClick={() => {
|
||||||
|
setShowMoreOptions(false);
|
||||||
|
toggleRaiseHand();
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="CallControls__MenuItemRaiseHandIcon" />
|
||||||
|
{localHandRaised
|
||||||
|
? i18n('icu:CallControls__MenuItemRaiseHand--lower')
|
||||||
|
: i18n('icu:CallControls__MenuItemRaiseHand')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -715,6 +882,9 @@ export function CallScreen({
|
||||||
audioLevel={localAudioLevel}
|
audioLevel={localAudioLevel}
|
||||||
shouldShowSpeaking={isSpeaking}
|
shouldShowSpeaking={isSpeaking}
|
||||||
/>
|
/>
|
||||||
|
{syncedLocalHandRaised && (
|
||||||
|
<div className="CallingStatusIndicator CallingStatusIndicator--HandRaised" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="module-ongoing-call__footer__local-preview" />
|
<div className="module-ongoing-call__footer__local-preview" />
|
||||||
|
@ -875,3 +1045,14 @@ function CallingReactionsToasts(props: CallingReactionsToastsType) {
|
||||||
useReactionsToast(props);
|
useReactionsToast(props);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isHandRaised(
|
||||||
|
raisedHands: Set<number> | undefined,
|
||||||
|
demuxId: number | undefined
|
||||||
|
): boolean {
|
||||||
|
if (raisedHands === undefined || demuxId === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return raisedHands.has(demuxId);
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ function createParticipant(
|
||||||
demuxId: 2,
|
demuxId: 2,
|
||||||
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
|
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
|
||||||
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
|
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
|
||||||
|
isHandRaised: Boolean(participantProps.isHandRaised),
|
||||||
presenting: Boolean(participantProps.presenting),
|
presenting: Boolean(participantProps.presenting),
|
||||||
sharingScreen: Boolean(participantProps.sharingScreen),
|
sharingScreen: Boolean(participantProps.sharingScreen),
|
||||||
videoAspectRatio: 1.3,
|
videoAspectRatio: 1.3,
|
||||||
|
|
|
@ -140,6 +140,7 @@ export function GroupCall(args: PropsType): JSX.Element {
|
||||||
maxDevices: 5,
|
maxDevices: 5,
|
||||||
deviceCount: 0,
|
deviceCount: 0,
|
||||||
peekedParticipants: [],
|
peekedParticipants: [],
|
||||||
|
raisedHands: new Set<number>(),
|
||||||
remoteParticipants: [],
|
remoteParticipants: [],
|
||||||
remoteAudioLevels: new Map<number, number>(),
|
remoteAudioLevels: new Map<number, number>(),
|
||||||
}}
|
}}
|
||||||
|
|
97
ts/components/CallingRaisedHandsList.stories.tsx
Normal file
97
ts/components/CallingRaisedHandsList.stories.tsx
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { times } from 'lodash';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import type { Meta } from '@storybook/react';
|
||||||
|
import type { PropsType } from './CallingRaisedHandsList';
|
||||||
|
import { CallingRaisedHandsList } from './CallingRaisedHandsList';
|
||||||
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import { AvatarColors } from '../types/Colors';
|
||||||
|
import { getDefaultConversationWithServiceId } from '../test-both/helpers/getDefaultConversation';
|
||||||
|
import { setupI18n } from '../util/setupI18n';
|
||||||
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
const MAX_HANDS = 20;
|
||||||
|
const LOCAL_DEMUX_ID = 1;
|
||||||
|
const NAMES = [
|
||||||
|
'Tom Ato',
|
||||||
|
'Ann Chovy',
|
||||||
|
'Longanisa Lisa Duchess of Summer Pumpkin',
|
||||||
|
'Rick Astley',
|
||||||
|
'Ash Ketchup',
|
||||||
|
'Kiki',
|
||||||
|
];
|
||||||
|
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
const conversation = getDefaultConversationWithServiceId({
|
||||||
|
id: '3051234567',
|
||||||
|
avatarPath: undefined,
|
||||||
|
color: AvatarColors[0],
|
||||||
|
title: 'Rick Sanchez',
|
||||||
|
name: 'Rick Sanchez',
|
||||||
|
phoneNumber: '3051234567',
|
||||||
|
profileName: 'Rick Sanchez',
|
||||||
|
});
|
||||||
|
|
||||||
|
const conversationsByDemuxId = new Map<number, ConversationType>(
|
||||||
|
times(MAX_HANDS).map(index => [
|
||||||
|
LOCAL_DEMUX_ID + index + 1,
|
||||||
|
getDefaultConversationWithServiceId({
|
||||||
|
title: NAMES[index] || `Participant ${index + 1}`,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
conversationsByDemuxId.set(LOCAL_DEMUX_ID, conversation);
|
||||||
|
|
||||||
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
|
i18n,
|
||||||
|
onClose: action('on-close'),
|
||||||
|
onLowerMyHand: action('on-lower-my-hand'),
|
||||||
|
localDemuxId: LOCAL_DEMUX_ID,
|
||||||
|
conversationsByDemuxId,
|
||||||
|
localHandRaised: overrideProps.localHandRaised || false,
|
||||||
|
raisedHands: overrideProps.raisedHands || new Set<number>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/CallingRaisedHandsList',
|
||||||
|
} satisfies Meta<PropsType>;
|
||||||
|
|
||||||
|
export function Me(): JSX.Element {
|
||||||
|
const props = createProps({
|
||||||
|
localHandRaised: true,
|
||||||
|
raisedHands: new Set([LOCAL_DEMUX_ID]),
|
||||||
|
});
|
||||||
|
return <CallingRaisedHandsList {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MeOnAnotherDevice(): JSX.Element {
|
||||||
|
const props = createProps({
|
||||||
|
raisedHands: new Set([LOCAL_DEMUX_ID]),
|
||||||
|
});
|
||||||
|
return <CallingRaisedHandsList {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MeAndOne(): JSX.Element {
|
||||||
|
const props = createProps({
|
||||||
|
localHandRaised: true,
|
||||||
|
raisedHands: new Set([LOCAL_DEMUX_ID, LOCAL_DEMUX_ID + 1]),
|
||||||
|
});
|
||||||
|
return <CallingRaisedHandsList {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function One(): JSX.Element {
|
||||||
|
const props = createProps({ raisedHands: new Set([LOCAL_DEMUX_ID + 1]) });
|
||||||
|
return <CallingRaisedHandsList {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Many(): JSX.Element {
|
||||||
|
const props = createProps({
|
||||||
|
raisedHands: new Set([...conversationsByDemuxId.keys()]),
|
||||||
|
});
|
||||||
|
return <CallingRaisedHandsList {...props} />;
|
||||||
|
}
|
137
ts/components/CallingRaisedHandsList.tsx
Normal file
137
ts/components/CallingRaisedHandsList.tsx
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
|
import { ContactName } from './conversation/ContactName';
|
||||||
|
import type { ConversationsByDemuxIdType } from '../types/Calling';
|
||||||
|
import type { ServiceIdString } from '../types/ServiceId';
|
||||||
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import { ModalHost } from './ModalHost';
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
|
export type PropsType = {
|
||||||
|
readonly i18n: LocalizerType;
|
||||||
|
readonly onClose: () => void;
|
||||||
|
readonly onLowerMyHand: () => void;
|
||||||
|
readonly localDemuxId: number | undefined;
|
||||||
|
readonly conversationsByDemuxId: ConversationsByDemuxIdType;
|
||||||
|
readonly raisedHands: Set<number>;
|
||||||
|
readonly localHandRaised: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CallingRaisedHandsList({
|
||||||
|
i18n,
|
||||||
|
onClose,
|
||||||
|
onLowerMyHand,
|
||||||
|
localDemuxId,
|
||||||
|
conversationsByDemuxId,
|
||||||
|
raisedHands,
|
||||||
|
localHandRaised,
|
||||||
|
}: PropsType): JSX.Element | null {
|
||||||
|
const ourServiceId: ServiceIdString | undefined = localDemuxId
|
||||||
|
? conversationsByDemuxId.get(localDemuxId)?.serviceId
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const participants = React.useMemo<Array<ConversationType>>(() => {
|
||||||
|
const serviceIds: Set<ServiceIdString> = new Set();
|
||||||
|
const conversations: Array<ConversationType> = [];
|
||||||
|
raisedHands.forEach(demuxId => {
|
||||||
|
const conversation = conversationsByDemuxId.get(demuxId);
|
||||||
|
if (!conversation) {
|
||||||
|
log.warn(
|
||||||
|
'CallingRaisedHandsList: Failed to get conversationsByDemuxId for demuxId',
|
||||||
|
{ demuxId }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { serviceId } = conversation;
|
||||||
|
if (serviceId) {
|
||||||
|
if (serviceIds.has(serviceId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceIds.add(serviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
conversations.push(conversation);
|
||||||
|
});
|
||||||
|
return conversations;
|
||||||
|
}, [raisedHands, conversationsByDemuxId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalHost
|
||||||
|
modalName="CallingRaisedHandsList"
|
||||||
|
moduleClassName="CallingRaisedHandsList"
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className="CallingRaisedHandsList module-calling-participants-list">
|
||||||
|
<div className="module-calling-participants-list__header">
|
||||||
|
<div className="module-calling-participants-list__title">
|
||||||
|
{i18n('icu:CallingRaisedHandsList__Title', {
|
||||||
|
count: participants.length,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="module-calling-participants-list__close"
|
||||||
|
onClick={onClose}
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={i18n('icu:close')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ul className="module-calling-participants-list__list">
|
||||||
|
{participants.map((participant: ConversationType, index: number) => (
|
||||||
|
<li
|
||||||
|
className="module-calling-participants-list__contact"
|
||||||
|
key={participant.serviceId ?? index}
|
||||||
|
>
|
||||||
|
<div className="CallingRaisedHandsList__AvatarAndName module-calling-participants-list__avatar-and-name">
|
||||||
|
<Avatar
|
||||||
|
acceptedMessageRequest={participant.acceptedMessageRequest}
|
||||||
|
avatarPath={participant.avatarPath}
|
||||||
|
badge={undefined}
|
||||||
|
color={participant.color}
|
||||||
|
conversationType="direct"
|
||||||
|
i18n={i18n}
|
||||||
|
isMe={participant.isMe}
|
||||||
|
profileName={participant.profileName}
|
||||||
|
title={participant.title}
|
||||||
|
sharedGroupNames={participant.sharedGroupNames}
|
||||||
|
size={AvatarSize.THIRTY_TWO}
|
||||||
|
/>
|
||||||
|
{ourServiceId && participant.serviceId === ourServiceId ? (
|
||||||
|
<span className="module-calling-participants-list__name">
|
||||||
|
{i18n('icu:you')}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<ContactName
|
||||||
|
module="module-calling-participants-list__name"
|
||||||
|
title={participant.title}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="module-calling-participants-list__status">
|
||||||
|
{localHandRaised &&
|
||||||
|
ourServiceId &&
|
||||||
|
participant.serviceId === ourServiceId && (
|
||||||
|
<button
|
||||||
|
className="CallingRaisedHandsList__LowerMyHandLink"
|
||||||
|
type="button"
|
||||||
|
onClick={onLowerMyHand}
|
||||||
|
>
|
||||||
|
{i18n('icu:CallControls__RaiseHands--lower')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className="CallingRaisedHandsList__NameHandIcon" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</ModalHost>
|
||||||
|
);
|
||||||
|
}
|
|
@ -8,6 +8,11 @@ import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import { CallingToastProvider, useCallingToasts } from './CallingToast';
|
import { CallingToastProvider, useCallingToasts } from './CallingToast';
|
||||||
import { usePrevious } from '../hooks/usePrevious';
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
|
import {
|
||||||
|
difference as setDifference,
|
||||||
|
isEqual as setIsEqual,
|
||||||
|
} from '../util/setUtil';
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
activeCall: ActiveCallType;
|
activeCall: ActiveCallType;
|
||||||
|
@ -136,9 +141,101 @@ function useOutgoingRingToast({
|
||||||
}, [outgoingRing, previousOutgoingRing, hideToast, showToast, i18n]);
|
}, [outgoingRing, previousOutgoingRing, hideToast, showToast, i18n]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useRaisedHandsToast({
|
||||||
|
raisedHands,
|
||||||
|
renderRaisedHandsToast,
|
||||||
|
}: {
|
||||||
|
raisedHands?: Set<number>;
|
||||||
|
renderRaisedHandsToast?: (
|
||||||
|
hands: Array<number>
|
||||||
|
) => JSX.Element | string | undefined;
|
||||||
|
}): void {
|
||||||
|
const RAISED_HANDS_TOAST_KEY = 'raised-hands';
|
||||||
|
const LOAD_DELAY = 2000;
|
||||||
|
const { showToast, hideToast } = useCallingToasts();
|
||||||
|
|
||||||
|
// Hand state is updated after a delay upon joining a call, so it can appear that
|
||||||
|
// hands were raised immediately when you join a call. To avoid spurious toasts, add
|
||||||
|
// an initial delay before showing toasts.
|
||||||
|
const [isLoaded, setIsLoaded] = React.useState<boolean>(false);
|
||||||
|
React.useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setIsLoaded(true);
|
||||||
|
}, LOAD_DELAY);
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const previousRaisedHands = usePrevious(raisedHands, raisedHands);
|
||||||
|
const [newHands, loweredHands]: [Set<number>, Set<number>] = isLoaded
|
||||||
|
? [
|
||||||
|
setDifference(
|
||||||
|
raisedHands ?? new Set(),
|
||||||
|
previousRaisedHands ?? new Set()
|
||||||
|
),
|
||||||
|
setDifference(
|
||||||
|
previousRaisedHands ?? new Set(),
|
||||||
|
raisedHands ?? new Set()
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [new Set(), new Set()];
|
||||||
|
|
||||||
|
const raisedHandsInLastShownToastRef = useRef<Set<number>>(new Set());
|
||||||
|
const raisedHandsInLastShownToast = raisedHandsInLastShownToastRef.current;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// 1. If no hands are raised, then hide any raise hand toast.
|
||||||
|
// 2. Check if someone lowered their hand which they had recently raised. The
|
||||||
|
// previous toast saying they raised their hand would now be out of date, so we
|
||||||
|
// should hide it.
|
||||||
|
if (
|
||||||
|
raisedHands?.size === 0 ||
|
||||||
|
(raisedHandsInLastShownToast.size > 0 &&
|
||||||
|
loweredHands.size > 0 &&
|
||||||
|
setIsEqual(raisedHandsInLastShownToast, loweredHands))
|
||||||
|
) {
|
||||||
|
hideToast(RAISED_HANDS_TOAST_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newHands.size === 0 || !renderRaisedHandsToast) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = renderRaisedHandsToast([...newHands].reverse());
|
||||||
|
if (!content) {
|
||||||
|
log.warn(
|
||||||
|
'CallingToastManager useRaisedHandsToast: Failed to call renderRaisedHandsToast()'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideToast(RAISED_HANDS_TOAST_KEY);
|
||||||
|
// Note: Don't set { dismissable: true } or else the links (Lower or View Queue)
|
||||||
|
// will cause nested buttons (dismissable toasts are <button>s)
|
||||||
|
showToast({
|
||||||
|
key: RAISED_HANDS_TOAST_KEY,
|
||||||
|
content,
|
||||||
|
autoClose: true,
|
||||||
|
});
|
||||||
|
raisedHandsInLastShownToastRef.current = newHands;
|
||||||
|
}, [
|
||||||
|
raisedHands,
|
||||||
|
previousRaisedHands,
|
||||||
|
newHands,
|
||||||
|
raisedHandsInLastShownToast,
|
||||||
|
loweredHands,
|
||||||
|
renderRaisedHandsToast,
|
||||||
|
hideToast,
|
||||||
|
showToast,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
type CallingButtonToastsType = {
|
type CallingButtonToastsType = {
|
||||||
hasLocalAudio: boolean;
|
hasLocalAudio: boolean;
|
||||||
outgoingRing: boolean | undefined;
|
outgoingRing: boolean | undefined;
|
||||||
|
raisedHands?: Set<number>;
|
||||||
|
renderRaisedHandsToast?: (
|
||||||
|
hands: Array<number>
|
||||||
|
) => JSX.Element | string | undefined;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -161,10 +258,13 @@ export function CallingButtonToastsContainer(
|
||||||
function CallingButtonToasts({
|
function CallingButtonToasts({
|
||||||
hasLocalAudio,
|
hasLocalAudio,
|
||||||
outgoingRing,
|
outgoingRing,
|
||||||
|
raisedHands,
|
||||||
|
renderRaisedHandsToast,
|
||||||
i18n,
|
i18n,
|
||||||
}: CallingButtonToastsType) {
|
}: CallingButtonToastsType) {
|
||||||
useMutedToast({ hasLocalAudio, i18n });
|
useMutedToast({ hasLocalAudio, i18n });
|
||||||
useOutgoingRingToast({ outgoingRing, i18n });
|
useOutgoingRingToast({ outgoingRing, i18n });
|
||||||
|
useRaisedHandsToast({ raisedHands, renderRaisedHandsToast });
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
|
||||||
demuxId: index,
|
demuxId: index,
|
||||||
hasRemoteAudio: index % 3 !== 0,
|
hasRemoteAudio: index % 3 !== 0,
|
||||||
hasRemoteVideo: index % 4 !== 0,
|
hasRemoteVideo: index % 4 !== 0,
|
||||||
|
isHandRaised: (index - 2) % 8 === 0,
|
||||||
presenting: false,
|
presenting: false,
|
||||||
sharingScreen: false,
|
sharingScreen: false,
|
||||||
videoAspectRatio: 1.3,
|
videoAspectRatio: 1.3,
|
||||||
|
|
|
@ -38,10 +38,12 @@ const createProps = (
|
||||||
isBlocked = false,
|
isBlocked = false,
|
||||||
hasRemoteAudio = false,
|
hasRemoteAudio = false,
|
||||||
presenting = false,
|
presenting = false,
|
||||||
|
isHandRaised = false,
|
||||||
}: {
|
}: {
|
||||||
isBlocked?: boolean;
|
isBlocked?: boolean;
|
||||||
hasRemoteAudio?: boolean;
|
hasRemoteAudio?: boolean;
|
||||||
presenting?: boolean;
|
presenting?: boolean;
|
||||||
|
isHandRaised?: boolean;
|
||||||
} = {}
|
} = {}
|
||||||
): PropsType => ({
|
): PropsType => ({
|
||||||
getFrameBuffer,
|
getFrameBuffer,
|
||||||
|
@ -55,6 +57,7 @@ const createProps = (
|
||||||
demuxId: 123,
|
demuxId: 123,
|
||||||
hasRemoteAudio,
|
hasRemoteAudio,
|
||||||
hasRemoteVideo: true,
|
hasRemoteVideo: true,
|
||||||
|
isHandRaised,
|
||||||
presenting,
|
presenting,
|
||||||
sharingScreen: false,
|
sharingScreen: false,
|
||||||
videoAspectRatio: 1.3,
|
videoAspectRatio: 1.3,
|
||||||
|
@ -119,6 +122,23 @@ export function Speaking(): JSX.Element {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function HandRaised(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<GroupCallRemoteParticipant
|
||||||
|
{...createProps(
|
||||||
|
{
|
||||||
|
isInPip: false,
|
||||||
|
height: 120,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{ isHandRaised: true }
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function IsInPip(): JSX.Element {
|
export function IsInPip(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<GroupCallRemoteParticipant
|
<GroupCallRemoteParticipant
|
||||||
|
|
|
@ -80,6 +80,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||||
demuxId,
|
demuxId,
|
||||||
hasRemoteAudio,
|
hasRemoteAudio,
|
||||||
hasRemoteVideo,
|
hasRemoteVideo,
|
||||||
|
isHandRaised,
|
||||||
isBlocked,
|
isBlocked,
|
||||||
isMe,
|
isMe,
|
||||||
profileName,
|
profileName,
|
||||||
|
@ -295,7 +296,9 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||||
isSpeaking &&
|
isSpeaking &&
|
||||||
!isActiveSpeakerInSpeakerView &&
|
!isActiveSpeakerInSpeakerView &&
|
||||||
remoteParticipantsCount > 1 &&
|
remoteParticipantsCount > 1 &&
|
||||||
'module-ongoing-call__group-call-remote-participant--speaking'
|
'module-ongoing-call__group-call-remote-participant--speaking',
|
||||||
|
isHandRaised &&
|
||||||
|
'module-ongoing-call__group-call-remote-participant--hand-raised'
|
||||||
)}
|
)}
|
||||||
ref={intersectionRef}
|
ref={intersectionRef}
|
||||||
style={containerStyles}
|
style={containerStyles}
|
||||||
|
@ -307,15 +310,16 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
||||||
audioLevel={props.audioLevel}
|
audioLevel={props.audioLevel}
|
||||||
shouldShowSpeaking={isSpeaking}
|
shouldShowSpeaking={isSpeaking}
|
||||||
/>
|
/>
|
||||||
<div
|
<div className="module-ongoing-call__group-call-remote-participant__footer">
|
||||||
className={classNames(
|
<div className="module-ongoing-call__group-call-remote-participant__info">
|
||||||
'module-ongoing-call__group-call-remote-participant__info'
|
{isHandRaised && (
|
||||||
)}
|
<div className="CallingStatusIndicator CallingStatusIndicator--HandRaised" />
|
||||||
>
|
)}
|
||||||
<ContactName
|
<ContactName
|
||||||
module="module-ongoing-call__group-call-remote-participant__info__contact-name"
|
module="module-ongoing-call__group-call-remote-participant__info__contact-name"
|
||||||
title={title}
|
title={title}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -163,6 +163,7 @@ type CallingReduxInterface = Pick<
|
||||||
| 'callStateChange'
|
| 'callStateChange'
|
||||||
| 'cancelIncomingGroupCallRing'
|
| 'cancelIncomingGroupCallRing'
|
||||||
| 'groupCallAudioLevelsChange'
|
| 'groupCallAudioLevelsChange'
|
||||||
|
| 'groupCallRaisedHandsChange'
|
||||||
| 'groupCallStateChange'
|
| 'groupCallStateChange'
|
||||||
| 'outgoingCall'
|
| 'outgoingCall'
|
||||||
| 'receiveGroupCallReactions'
|
| 'receiveGroupCallReactions'
|
||||||
|
@ -847,8 +848,11 @@ export class CallingClass {
|
||||||
reactions,
|
reactions,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onRaisedHands: (_groupCall, _raisedHands) => {
|
onRaisedHands: (_groupCall, raisedHands) => {
|
||||||
// TODO: Implement handling of raised hands.
|
this.reduxInterface?.groupCallRaisedHandsChange({
|
||||||
|
conversationId,
|
||||||
|
raisedHands,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onPeekChanged: groupCall => {
|
onPeekChanged: groupCall => {
|
||||||
const localDeviceState = groupCall.getLocalDeviceState();
|
const localDeviceState = groupCall.getLocalDeviceState();
|
||||||
|
@ -1153,6 +1157,14 @@ export class CallingClass {
|
||||||
groupCall.resendMediaKeys();
|
groupCall.resendMediaKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sendGroupCallRaiseHand(conversationId: string, raise: boolean): void {
|
||||||
|
const groupCall = this.getGroupCall(conversationId);
|
||||||
|
if (!groupCall) {
|
||||||
|
throw new Error('Could not find matching call');
|
||||||
|
}
|
||||||
|
groupCall.raiseHand(raise);
|
||||||
|
}
|
||||||
|
|
||||||
public sendGroupCallReaction(conversationId: string, value: string): void {
|
public sendGroupCallReaction(conversationId: string, value: string): void {
|
||||||
const groupCall = this.getGroupCall(conversationId);
|
const groupCall = this.getGroupCall(conversationId);
|
||||||
if (!groupCall) {
|
if (!groupCall) {
|
||||||
|
|
|
@ -116,6 +116,7 @@ export type GroupCallStateType = {
|
||||||
localDemuxId: number | undefined;
|
localDemuxId: number | undefined;
|
||||||
joinState: GroupCallJoinState;
|
joinState: GroupCallJoinState;
|
||||||
peekInfo?: GroupCallPeekInfoType;
|
peekInfo?: GroupCallPeekInfoType;
|
||||||
|
raisedHands?: Array<number>;
|
||||||
remoteParticipants: Array<GroupCallParticipantInfoType>;
|
remoteParticipants: Array<GroupCallParticipantInfoType>;
|
||||||
remoteAudioLevels?: Map<number, number>;
|
remoteAudioLevels?: Map<number, number>;
|
||||||
} & GroupCallRingStateType;
|
} & GroupCallRingStateType;
|
||||||
|
@ -222,11 +223,15 @@ type IncomingGroupCallType = ReadonlyDeep<{
|
||||||
ringerAci: AciString;
|
ringerAci: AciString;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type SendGroupCallRaiseHandType = ReadonlyDeep<{
|
||||||
|
conversationId: string;
|
||||||
|
raise: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type SendGroupCallReactionType = ReadonlyDeep<{
|
export type SendGroupCallReactionType = ReadonlyDeep<{
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type SendGroupCallReactionLocalCopyType = ReadonlyDeep<{
|
type SendGroupCallReactionLocalCopyType = ReadonlyDeep<{
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -445,6 +450,7 @@ const CHANGE_IO_DEVICE_FULFILLED = 'calling/CHANGE_IO_DEVICE_FULFILLED';
|
||||||
const CLOSE_NEED_PERMISSION_SCREEN = 'calling/CLOSE_NEED_PERMISSION_SCREEN';
|
const CLOSE_NEED_PERMISSION_SCREEN = 'calling/CLOSE_NEED_PERMISSION_SCREEN';
|
||||||
const DECLINE_DIRECT_CALL = 'calling/DECLINE_DIRECT_CALL';
|
const DECLINE_DIRECT_CALL = 'calling/DECLINE_DIRECT_CALL';
|
||||||
const GROUP_CALL_AUDIO_LEVELS_CHANGE = 'calling/GROUP_CALL_AUDIO_LEVELS_CHANGE';
|
const GROUP_CALL_AUDIO_LEVELS_CHANGE = 'calling/GROUP_CALL_AUDIO_LEVELS_CHANGE';
|
||||||
|
const GROUP_CALL_RAISED_HANDS_CHANGE = 'calling/GROUP_CALL_RAISED_HANDS_CHANGE';
|
||||||
const GROUP_CALL_STATE_CHANGE = 'calling/GROUP_CALL_STATE_CHANGE';
|
const GROUP_CALL_STATE_CHANGE = 'calling/GROUP_CALL_STATE_CHANGE';
|
||||||
const GROUP_CALL_REACTIONS_RECEIVED = 'calling/GROUP_CALL_REACTIONS_RECEIVED';
|
const GROUP_CALL_REACTIONS_RECEIVED = 'calling/GROUP_CALL_REACTIONS_RECEIVED';
|
||||||
const GROUP_CALL_REACTIONS_EXPIRED = 'calling/GROUP_CALL_REACTIONS_EXPIRED';
|
const GROUP_CALL_REACTIONS_EXPIRED = 'calling/GROUP_CALL_REACTIONS_EXPIRED';
|
||||||
|
@ -455,6 +461,7 @@ const MARK_CALL_TRUSTED = 'calling/MARK_CALL_TRUSTED';
|
||||||
const MARK_CALL_UNTRUSTED = 'calling/MARK_CALL_UNTRUSTED';
|
const MARK_CALL_UNTRUSTED = 'calling/MARK_CALL_UNTRUSTED';
|
||||||
const OUTGOING_CALL = 'calling/OUTGOING_CALL';
|
const OUTGOING_CALL = 'calling/OUTGOING_CALL';
|
||||||
const PEEK_GROUP_CALL_FULFILLED = 'calling/PEEK_GROUP_CALL_FULFILLED';
|
const PEEK_GROUP_CALL_FULFILLED = 'calling/PEEK_GROUP_CALL_FULFILLED';
|
||||||
|
const RAISE_HAND_GROUP_CALL = 'calling/RAISE_HAND_GROUP_CALL';
|
||||||
const REFRESH_IO_DEVICES = 'calling/REFRESH_IO_DEVICES';
|
const REFRESH_IO_DEVICES = 'calling/REFRESH_IO_DEVICES';
|
||||||
const REMOTE_SHARING_SCREEN_CHANGE = 'calling/REMOTE_SHARING_SCREEN_CHANGE';
|
const REMOTE_SHARING_SCREEN_CHANGE = 'calling/REMOTE_SHARING_SCREEN_CHANGE';
|
||||||
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
||||||
|
@ -525,6 +532,16 @@ type GroupCallAudioLevelsChangeActionType = ReadonlyDeep<{
|
||||||
payload: GroupCallAudioLevelsChangeActionPayloadType;
|
payload: GroupCallAudioLevelsChangeActionPayloadType;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type GroupCallRaisedHandsChangeActionPayloadType = ReadonlyDeep<{
|
||||||
|
conversationId: string;
|
||||||
|
raisedHands: ReadonlyArray<number>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type GroupCallRaisedHandsChangeActionType = ReadonlyDeep<{
|
||||||
|
type: 'calling/GROUP_CALL_RAISED_HANDS_CHANGE';
|
||||||
|
payload: GroupCallRaisedHandsChangeActionPayloadType;
|
||||||
|
}>;
|
||||||
|
|
||||||
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
// eslint-disable-next-line local-rules/type-alias-readonlydeep
|
||||||
export type GroupCallStateChangeActionType = {
|
export type GroupCallStateChangeActionType = {
|
||||||
type: 'calling/GROUP_CALL_STATE_CHANGE';
|
type: 'calling/GROUP_CALL_STATE_CHANGE';
|
||||||
|
@ -580,6 +597,11 @@ type KeyChangeOkActionType = ReadonlyDeep<{
|
||||||
payload: null;
|
payload: null;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type SendGroupCallRaiseHandActionType = ReadonlyDeep<{
|
||||||
|
type: 'calling/RAISE_HAND_GROUP_CALL';
|
||||||
|
payload: SendGroupCallRaiseHandType;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type SendGroupCallReactionActionType = ReadonlyDeep<{
|
export type SendGroupCallReactionActionType = ReadonlyDeep<{
|
||||||
type: 'calling/SEND_GROUP_CALL_REACTION';
|
type: 'calling/SEND_GROUP_CALL_REACTION';
|
||||||
payload: SendGroupCallReactionLocalCopyType;
|
payload: SendGroupCallReactionLocalCopyType;
|
||||||
|
@ -692,6 +714,7 @@ export type CallingActionType =
|
||||||
| ConversationRemovedActionType
|
| ConversationRemovedActionType
|
||||||
| DeclineCallActionType
|
| DeclineCallActionType
|
||||||
| GroupCallAudioLevelsChangeActionType
|
| GroupCallAudioLevelsChangeActionType
|
||||||
|
| GroupCallRaisedHandsChangeActionType
|
||||||
| GroupCallStateChangeActionType
|
| GroupCallStateChangeActionType
|
||||||
| GroupCallReactionsReceivedActionType
|
| GroupCallReactionsReceivedActionType
|
||||||
| GroupCallReactionsExpiredActionType
|
| GroupCallReactionsExpiredActionType
|
||||||
|
@ -950,6 +973,12 @@ function receiveGroupCallReactions(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupCallRaisedHandsChange(
|
||||||
|
payload: GroupCallRaisedHandsChangeActionPayloadType
|
||||||
|
): GroupCallRaisedHandsChangeActionType {
|
||||||
|
return { type: GROUP_CALL_RAISED_HANDS_CHANGE, payload };
|
||||||
|
}
|
||||||
|
|
||||||
function groupCallStateChange(
|
function groupCallStateChange(
|
||||||
payload: GroupCallStateChangeArgumentType
|
payload: GroupCallStateChangeArgumentType
|
||||||
): ThunkAction<void, RootStateType, unknown, GroupCallStateChangeActionType> {
|
): ThunkAction<void, RootStateType, unknown, GroupCallStateChangeActionType> {
|
||||||
|
@ -1075,6 +1104,19 @@ function keyChangeOk(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendGroupCallRaiseHand(
|
||||||
|
payload: SendGroupCallRaiseHandType
|
||||||
|
): ThunkAction<void, RootStateType, unknown, SendGroupCallRaiseHandActionType> {
|
||||||
|
return dispatch => {
|
||||||
|
calling.sendGroupCallRaiseHand(payload.conversationId, payload.raise);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: RAISE_HAND_GROUP_CALL,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function sendGroupCallReaction(
|
function sendGroupCallReaction(
|
||||||
payload: SendGroupCallReactionType
|
payload: SendGroupCallReactionType
|
||||||
): ThunkAction<
|
): ThunkAction<
|
||||||
|
@ -1612,6 +1654,7 @@ export const actions = {
|
||||||
declineCall,
|
declineCall,
|
||||||
getPresentingSources,
|
getPresentingSources,
|
||||||
groupCallAudioLevelsChange,
|
groupCallAudioLevelsChange,
|
||||||
|
groupCallRaisedHandsChange,
|
||||||
groupCallStateChange,
|
groupCallStateChange,
|
||||||
hangUpActiveCall,
|
hangUpActiveCall,
|
||||||
keyChangeOk,
|
keyChangeOk,
|
||||||
|
@ -1630,6 +1673,7 @@ export const actions = {
|
||||||
remoteSharingScreenChange,
|
remoteSharingScreenChange,
|
||||||
remoteVideoChange,
|
remoteVideoChange,
|
||||||
returnToActiveCall,
|
returnToActiveCall,
|
||||||
|
sendGroupCallRaiseHand,
|
||||||
sendGroupCallReaction,
|
sendGroupCallReaction,
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
setIsCallActive,
|
setIsCallActive,
|
||||||
|
@ -2137,6 +2181,7 @@ export function reducer(
|
||||||
localDemuxId,
|
localDemuxId,
|
||||||
peekInfo: newPeekInfo,
|
peekInfo: newPeekInfo,
|
||||||
remoteParticipants,
|
remoteParticipants,
|
||||||
|
raisedHands: existingCall?.raisedHands ?? [],
|
||||||
...newRingState,
|
...newRingState,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -2267,6 +2312,29 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === GROUP_CALL_RAISED_HANDS_CHANGE) {
|
||||||
|
const { conversationId, raisedHands } = action.payload;
|
||||||
|
|
||||||
|
const { activeCallState } = state;
|
||||||
|
const existingCall = getGroupCall(conversationId, state);
|
||||||
|
|
||||||
|
if (
|
||||||
|
state.activeCallState?.conversationId !== conversationId ||
|
||||||
|
!activeCallState ||
|
||||||
|
!existingCall
|
||||||
|
) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
callsByConversation: {
|
||||||
|
...callsByConversation,
|
||||||
|
[conversationId]: { ...existingCall, raisedHands: [...raisedHands] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (action.type === REMOTE_SHARING_SCREEN_CHANGE) {
|
if (action.type === REMOTE_SHARING_SCREEN_CHANGE) {
|
||||||
const { conversationId, isSharingScreen } = action.payload;
|
const { conversationId, isSharingScreen } = action.payload;
|
||||||
const call = getOwn(state.callsByConversation, conversationId);
|
const call = getOwn(state.callsByConversation, conversationId);
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { getActiveCall } from '../ducks/calling';
|
||||||
import type { ConversationType } from '../ducks/conversations';
|
import type { ConversationType } from '../ducks/conversations';
|
||||||
import { getIncomingCall } from '../selectors/calling';
|
import { getIncomingCall } from '../selectors/calling';
|
||||||
import { isGroupCallOutboundRingEnabled } from '../../util/isGroupCallOutboundRingEnabled';
|
import { isGroupCallOutboundRingEnabled } from '../../util/isGroupCallOutboundRingEnabled';
|
||||||
|
import { isGroupCallRaiseHandEnabled } from '../../util/isGroupCallRaiseHandEnabled';
|
||||||
import { isGroupCallReactionsEnabled } from '../../util/isGroupCallReactionsEnabled';
|
import { isGroupCallReactionsEnabled } from '../../util/isGroupCallReactionsEnabled';
|
||||||
import type {
|
import type {
|
||||||
ActiveCallBaseType,
|
ActiveCallBaseType,
|
||||||
|
@ -201,6 +202,8 @@ const mapStateToActiveCallProp = (
|
||||||
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
|
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
|
||||||
const peekedParticipants: Array<ConversationType> = [];
|
const peekedParticipants: Array<ConversationType> = [];
|
||||||
const conversationsByDemuxId: ConversationsByDemuxIdType = new Map();
|
const conversationsByDemuxId: ConversationsByDemuxIdType = new Map();
|
||||||
|
const { localDemuxId } = call;
|
||||||
|
const raisedHands: Set<number> = new Set(call.raisedHands ?? []);
|
||||||
|
|
||||||
const { memberships = [] } = conversation;
|
const { memberships = [] } = conversation;
|
||||||
|
|
||||||
|
@ -243,6 +246,7 @@ const mapStateToActiveCallProp = (
|
||||||
demuxId: remoteParticipant.demuxId,
|
demuxId: remoteParticipant.demuxId,
|
||||||
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
|
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
|
||||||
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
|
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
|
||||||
|
isHandRaised: raisedHands.has(remoteParticipant.demuxId),
|
||||||
presenting: remoteParticipant.presenting,
|
presenting: remoteParticipant.presenting,
|
||||||
sharingScreen: remoteParticipant.sharingScreen,
|
sharingScreen: remoteParticipant.sharingScreen,
|
||||||
speakerTime: remoteParticipant.speakerTime,
|
speakerTime: remoteParticipant.speakerTime,
|
||||||
|
@ -254,6 +258,17 @@ const mapStateToActiveCallProp = (
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (localDemuxId !== undefined) {
|
||||||
|
conversationsByDemuxId.set(localDemuxId, getMe(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter raisedHands to ensure valid demuxIds.
|
||||||
|
raisedHands.forEach(demuxId => {
|
||||||
|
if (!conversationsByDemuxId.has(demuxId)) {
|
||||||
|
raisedHands.delete(demuxId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
for (
|
for (
|
||||||
let i = 0;
|
let i = 0;
|
||||||
i < activeCallState.safetyNumberChangedAcis.length;
|
i < activeCallState.safetyNumberChangedAcis.length;
|
||||||
|
@ -293,9 +308,10 @@ const mapStateToActiveCallProp = (
|
||||||
groupMembers,
|
groupMembers,
|
||||||
isConversationTooBigToRing: isConversationTooBigToRing(conversation),
|
isConversationTooBigToRing: isConversationTooBigToRing(conversation),
|
||||||
joinState: call.joinState,
|
joinState: call.joinState,
|
||||||
localDemuxId: call.localDemuxId,
|
localDemuxId,
|
||||||
maxDevices: peekInfo.maxDevices,
|
maxDevices: peekInfo.maxDevices,
|
||||||
peekedParticipants,
|
peekedParticipants,
|
||||||
|
raisedHands,
|
||||||
remoteParticipants,
|
remoteParticipants,
|
||||||
remoteAudioLevels: call.remoteAudioLevels || new Map<number, number>(),
|
remoteAudioLevels: call.remoteAudioLevels || new Map<number, number>(),
|
||||||
} satisfies ActiveGroupCallType;
|
} satisfies ActiveGroupCallType;
|
||||||
|
@ -360,6 +376,7 @@ const mapStateToProps = (state: StateType) => {
|
||||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
isGroupCallOutboundRingEnabled: isGroupCallOutboundRingEnabled(),
|
isGroupCallOutboundRingEnabled: isGroupCallOutboundRingEnabled(),
|
||||||
|
isGroupCallRaiseHandEnabled: isGroupCallRaiseHandEnabled(),
|
||||||
isGroupCallReactionsEnabled: isGroupCallReactionsEnabled(),
|
isGroupCallReactionsEnabled: isGroupCallReactionsEnabled(),
|
||||||
incomingCall,
|
incomingCall,
|
||||||
me: getMe(state),
|
me: getMe(state),
|
||||||
|
|
|
@ -940,6 +940,7 @@ describe('calling duck', () => {
|
||||||
videoAspectRatio: 4 / 3,
|
videoAspectRatio: 4 / 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
raisedHands: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -997,6 +998,7 @@ describe('calling duck', () => {
|
||||||
videoAspectRatio: 16 / 9,
|
videoAspectRatio: 16 / 9,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
raisedHands: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -96,6 +96,7 @@ export type ActiveGroupCallType = ActiveCallBaseType & {
|
||||||
groupMembers: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
groupMembers: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
||||||
isConversationTooBigToRing: boolean;
|
isConversationTooBigToRing: boolean;
|
||||||
peekedParticipants: Array<ConversationType>;
|
peekedParticipants: Array<ConversationType>;
|
||||||
|
raisedHands: Set<number>;
|
||||||
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
||||||
remoteAudioLevels: Map<number, number>;
|
remoteAudioLevels: Map<number, number>;
|
||||||
};
|
};
|
||||||
|
@ -158,6 +159,7 @@ export type GroupCallRemoteParticipantType = ConversationType & {
|
||||||
demuxId: number;
|
demuxId: number;
|
||||||
hasRemoteAudio: boolean;
|
hasRemoteAudio: boolean;
|
||||||
hasRemoteVideo: boolean;
|
hasRemoteVideo: boolean;
|
||||||
|
isHandRaised: boolean;
|
||||||
presenting: boolean;
|
presenting: boolean;
|
||||||
sharingScreen: boolean;
|
sharingScreen: boolean;
|
||||||
speakerTime?: number;
|
speakerTime?: number;
|
||||||
|
|
8
ts/util/isGroupCallRaiseHandEnabled.ts
Normal file
8
ts/util/isGroupCallRaiseHandEnabled.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// Copyright 2023 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as RemoteConfig from '../RemoteConfig';
|
||||||
|
|
||||||
|
export function isGroupCallRaiseHandEnabled(): boolean {
|
||||||
|
return Boolean(RemoteConfig.isEnabled('desktop.internalUser'));
|
||||||
|
}
|
|
@ -3058,6 +3058,13 @@
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2023-10-26T13:57:41.860Z"
|
"updated": "2023-10-26T13:57:41.860Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/CallingToastManager.tsx",
|
||||||
|
"line": " const raisedHandsInLastShownToastRef = useRef<Set<number>>(new Set());",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2023-12-05T22:11:41.559Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/CallsList.tsx",
|
"path": "ts/components/CallsList.tsx",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue