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",
|
||||
"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": {
|
||||
"messageformat": "You",
|
||||
"description": "Label next to in-call reactions to indicate that the current user sent that reaction."
|
||||
|
@ -3591,6 +3595,38 @@
|
|||
"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."
|
||||
},
|
||||
"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": {
|
||||
"messageformat": "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 {
|
||||
flex-grow: 1;
|
||||
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 {
|
||||
|
@ -4005,24 +4011,30 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
align-items: flex-end;
|
||||
bottom: 0;
|
||||
&__footer {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
height: 60px;
|
||||
justify-content: space-between;
|
||||
padding-block: 0 16px;
|
||||
padding-inline: 16px;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
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 {
|
||||
flex-grow: 1;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
color: $color-white;
|
||||
margin-inline-end: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
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 {
|
||||
.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(
|
||||
180deg,
|
||||
transparent,
|
||||
$color-black-alpha-60 100%
|
||||
);
|
||||
|
||||
&__contact-name {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4332,6 +4366,7 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-inline-end: 4px;
|
||||
z-index: $z-index-above-base;
|
||||
|
||||
@include keyboard-mode {
|
||||
|
@ -4344,6 +4379,9 @@ button.module-image__border-overlay:focus {
|
|||
&__status {
|
||||
display: flex;
|
||||
flex-basis: 64px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
&__muted {
|
||||
|
|
|
@ -68,6 +68,7 @@ $color-ultramarine-dark: #1851b4;
|
|||
$color-ultramarine-icon: #3a76f0;
|
||||
$color-ultramarine-light: #6191f3;
|
||||
$color-ultramarine-dawn: #406ec9;
|
||||
$color-ultramarine-pastel: #abc4f8;
|
||||
$color-ultramarine: #2c6bed;
|
||||
|
||||
// Flat colors
|
||||
|
|
|
@ -110,6 +110,10 @@
|
|||
margin-block: -5px;
|
||||
}
|
||||
|
||||
.CallControls__MoreOptionsButtonContainer--menu-shown .module-tooltip {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.CallControls__OuterSpacer {
|
||||
// Defined in _modules but duplicated here for ease of refactor
|
||||
$local-preview-width: 108px;
|
||||
|
@ -120,13 +124,14 @@
|
|||
position: absolute;
|
||||
inset-inline-start: min(48%, 40vw);
|
||||
inset-block-end: 70px;
|
||||
z-index: $z-index-calling;
|
||||
z-index: $z-index-toast;
|
||||
}
|
||||
|
||||
.CallControls__MoreOptionsMenu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(var(--window-height) - 155px);
|
||||
font-size: 13px;
|
||||
filter: drop-shadow(0px 4px 3px $color-black-alpha-20);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
@ -136,6 +141,43 @@
|
|||
max-width: calc(var(--window-width) / 2 + 20px);
|
||||
}
|
||||
|
||||
.CallControls__MoreOptionsButtonContainer--menu-shown .module-tooltip {
|
||||
opacity: 0;
|
||||
.CallControls__MoreOptionsMenu
|
||||
.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 {
|
||||
@include color-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;
|
||||
top: 6px;
|
||||
inset-inline-start: 6px;
|
||||
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/CallingSelectPresentingSourcesModal.scss';
|
||||
@import './components/CallingToast.scss';
|
||||
@import './components/CallingRaisedHandsList.scss';
|
||||
@import './components/CallingRaisedHandsToasts.scss';
|
||||
@import './components/CallingReactionsToasts.scss';
|
||||
@import './components/ChatColorPicker.scss';
|
||||
@import './components/Checkbox.scss';
|
||||
|
|
|
@ -74,6 +74,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
hangUpActiveCall: action('hang-up-active-call'),
|
||||
i18n,
|
||||
isGroupCallOutboundRingEnabled: true,
|
||||
isGroupCallRaiseHandEnabled: true,
|
||||
isGroupCallReactionsEnabled: true,
|
||||
keyChangeOk: action('key-change-ok'),
|
||||
me: {
|
||||
|
@ -90,6 +91,7 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
renderEmojiPicker: () => <>EmojiPicker</>,
|
||||
renderReactionPicker: () => <div />,
|
||||
renderSafetyNumberViewer: (_: SafetyNumberProps) => <div />,
|
||||
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
|
||||
sendGroupCallReaction: action('send-group-call-reaction'),
|
||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||
setIsCallActive: action('set-is-call-active'),
|
||||
|
@ -159,6 +161,7 @@ export function OngoingGroupCall(): JSX.Element {
|
|||
groupMembers: [],
|
||||
isConversationTooBigToRing: false,
|
||||
peekedParticipants: [],
|
||||
raisedHands: new Set<number>(),
|
||||
remoteParticipants: [],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
},
|
||||
|
@ -247,6 +250,7 @@ export function GroupCallSafetyNumberChanged(): JSX.Element {
|
|||
groupMembers: [],
|
||||
isConversationTooBigToRing: false,
|
||||
peekedParticipants: [],
|
||||
raisedHands: new Set<number>(),
|
||||
remoteParticipants: [],
|
||||
remoteAudioLevels: new Map<number, number>(),
|
||||
},
|
||||
|
|
|
@ -33,6 +33,7 @@ import type {
|
|||
CancelCallType,
|
||||
DeclineCallType,
|
||||
KeyChangeOkType,
|
||||
SendGroupCallRaiseHandType,
|
||||
SendGroupCallReactionType,
|
||||
SetGroupCallVideoRequestType,
|
||||
SetLocalAudioType,
|
||||
|
@ -87,6 +88,7 @@ export type PropsType = {
|
|||
declineCall: (_: DeclineCallType) => void;
|
||||
i18n: LocalizerType;
|
||||
isGroupCallOutboundRingEnabled: boolean;
|
||||
isGroupCallRaiseHandEnabled: boolean;
|
||||
isGroupCallReactionsEnabled: boolean;
|
||||
me: ConversationType;
|
||||
notifyForCall: (
|
||||
|
@ -96,6 +98,7 @@ export type PropsType = {
|
|||
) => unknown;
|
||||
openSystemPreferencesAction: () => unknown;
|
||||
playRingtone: () => unknown;
|
||||
sendGroupCallRaiseHand: (payload: SendGroupCallRaiseHandType) => void;
|
||||
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
|
||||
setGroupCallVideoRequest: (_: SetGroupCallVideoRequestType) => void;
|
||||
setIsCallActive: (_: boolean) => void;
|
||||
|
@ -130,6 +133,7 @@ function ActiveCallManager({
|
|||
hangUpActiveCall,
|
||||
i18n,
|
||||
isGroupCallOutboundRingEnabled,
|
||||
isGroupCallRaiseHandEnabled,
|
||||
isGroupCallReactionsEnabled,
|
||||
keyChangeOk,
|
||||
getGroupCallVideoFrameSource,
|
||||
|
@ -141,6 +145,7 @@ function ActiveCallManager({
|
|||
renderEmojiPicker,
|
||||
renderReactionPicker,
|
||||
renderSafetyNumberViewer,
|
||||
sendGroupCallRaiseHand,
|
||||
sendGroupCallReaction,
|
||||
setGroupCallVideoRequest,
|
||||
setLocalAudio,
|
||||
|
@ -341,11 +346,13 @@ function ActiveCallManager({
|
|||
groupMembers={groupMembers}
|
||||
hangUpActiveCall={hangUpActiveCall}
|
||||
i18n={i18n}
|
||||
isGroupCallRaiseHandEnabled={isGroupCallRaiseHandEnabled}
|
||||
isGroupCallReactionsEnabled={isGroupCallReactionsEnabled}
|
||||
me={me}
|
||||
openSystemPreferencesAction={openSystemPreferencesAction}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
renderReactionPicker={renderReactionPicker}
|
||||
sendGroupCallRaiseHand={sendGroupCallRaiseHand}
|
||||
sendGroupCallReaction={sendGroupCallReaction}
|
||||
setGroupCallVideoRequest={setGroupCallVideoRequestForConversation}
|
||||
setLocalPreview={setLocalPreview}
|
||||
|
|
|
@ -35,6 +35,7 @@ import enMessages from '../../_locales/en/messages.json';
|
|||
import { CallingToastProvider, useCallingToasts } from './CallingToast';
|
||||
|
||||
const MAX_PARTICIPANTS = 75;
|
||||
const LOCAL_DEMUX_ID = 1;
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -66,6 +67,7 @@ type GroupCallOverrideProps = OverridePropsBase & {
|
|||
callMode: CallMode.Group;
|
||||
connectionState?: GroupCallConnectionState;
|
||||
peekedParticipants?: Array<ConversationType>;
|
||||
raisedHands?: Set<number>;
|
||||
remoteParticipants?: Array<GroupCallRemoteParticipantType>;
|
||||
remoteAudioLevel?: number;
|
||||
};
|
||||
|
@ -92,12 +94,8 @@ const createActiveDirectCallProp = (
|
|||
],
|
||||
});
|
||||
|
||||
const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
||||
callMode: CallMode.Group as CallMode.Group,
|
||||
connectionState:
|
||||
overrideProps.connectionState || GroupCallConnectionState.Connected,
|
||||
conversationsWithSafetyNumberChanges: [],
|
||||
conversationsByDemuxId: new Map<number, ConversationType>(
|
||||
const getConversationsByDemuxId = (overrideProps: GroupCallOverrideProps) => {
|
||||
const conversationsByDemuxId = new Map<number, ConversationType>(
|
||||
overrideProps.remoteParticipants?.map((participant, index) => [
|
||||
participant.demuxId,
|
||||
getDefaultConversationWithServiceId({
|
||||
|
@ -105,9 +103,19 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
|||
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,
|
||||
localDemuxId: 1,
|
||||
localDemuxId: LOCAL_DEMUX_ID,
|
||||
maxDevices: 5,
|
||||
deviceCount: (overrideProps.remoteParticipants || []).length,
|
||||
groupMembers: overrideProps.remoteParticipants || [],
|
||||
|
@ -116,6 +124,7 @@ const createActiveGroupCallProp = (overrideProps: GroupCallOverrideProps) => ({
|
|||
isConversationTooBigToRing: false,
|
||||
peekedParticipants:
|
||||
overrideProps.peekedParticipants || overrideProps.remoteParticipants || [],
|
||||
raisedHands: overrideProps.raisedHands || new Set<number>(),
|
||||
remoteParticipants: overrideProps.remoteParticipants || [],
|
||||
remoteAudioLevels: new Map<number, number>(
|
||||
overrideProps.remoteParticipants?.map((_participant, index) => [
|
||||
|
@ -163,6 +172,7 @@ const createProps = (
|
|||
getPresentingSources: action('get-presenting-sources'),
|
||||
hangUpActiveCall: action('hang-up'),
|
||||
i18n,
|
||||
isGroupCallRaiseHandEnabled: true,
|
||||
isGroupCallReactionsEnabled: true,
|
||||
me: getDefaultConversation({
|
||||
color: AvatarColors[1],
|
||||
|
@ -175,6 +185,7 @@ const createProps = (
|
|||
openSystemPreferencesAction: action('open-system-preferences-action'),
|
||||
renderEmojiPicker: () => <>EmojiPicker</>,
|
||||
renderReactionPicker: () => <div />,
|
||||
sendGroupCallRaiseHand: action('send-group-call-raise-hand'),
|
||||
sendGroupCallReaction: action('send-group-call-reaction'),
|
||||
setGroupCallVideoRequest: action('set-group-call-video-request'),
|
||||
setLocalAudio: action('set-local-audio'),
|
||||
|
@ -299,6 +310,7 @@ export function GroupCall1(): JSX.Element {
|
|||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isHandRaised: false,
|
||||
presenting: false,
|
||||
sharingScreen: false,
|
||||
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.
|
||||
const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
|
||||
aci: generateAci(),
|
||||
demuxId: index,
|
||||
hasRemoteAudio: index % 3 !== 0,
|
||||
hasRemoteVideo: index % 4 !== 0,
|
||||
isHandRaised: (index - 3) % 10 === 0,
|
||||
presenting: false,
|
||||
sharingScreen: false,
|
||||
videoAspectRatio: Math.random() < 0.7 ? 1.3 : Math.random() * 0.4 + 0.6,
|
||||
|
@ -406,6 +447,7 @@ export function GroupCallReconnecting(): JSX.Element {
|
|||
demuxId: 0,
|
||||
hasRemoteAudio: true,
|
||||
hasRemoteVideo: true,
|
||||
isHandRaised: false,
|
||||
presenting: false,
|
||||
sharingScreen: false,
|
||||
videoAspectRatio: 1.3,
|
||||
|
|
|
@ -8,6 +8,7 @@ import classNames from 'classnames';
|
|||
import type { VideoFrameSource } from '@signalapp/ringrtc';
|
||||
import type {
|
||||
ActiveCallStateType,
|
||||
SendGroupCallRaiseHandType,
|
||||
SendGroupCallReactionType,
|
||||
SetLocalAudioType,
|
||||
SetLocalPreviewType,
|
||||
|
@ -74,6 +75,7 @@ import { handleOutsideClick } from '../util/handleOutsideClick';
|
|||
import type { Props as ReactionPickerProps } from './conversation/ReactionPicker';
|
||||
import type { SmartReactionPicker } from '../state/smart/ReactionPicker';
|
||||
import { Emoji } from './emoji/Emoji';
|
||||
import { CallingRaisedHandsList } from './CallingRaisedHandsList';
|
||||
|
||||
export type PropsType = {
|
||||
activeCall: ActiveCallType;
|
||||
|
@ -82,12 +84,14 @@ export type PropsType = {
|
|||
groupMembers?: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
||||
hangUpActiveCall: (reason: string) => void;
|
||||
i18n: LocalizerType;
|
||||
isGroupCallRaiseHandEnabled: boolean;
|
||||
isGroupCallReactionsEnabled: boolean;
|
||||
me: ConversationType;
|
||||
openSystemPreferencesAction: () => unknown;
|
||||
renderReactionPicker: (
|
||||
props: React.ComponentProps<typeof SmartReactionPicker>
|
||||
) => JSX.Element;
|
||||
sendGroupCallRaiseHand: (payload: SendGroupCallRaiseHandType) => void;
|
||||
sendGroupCallReaction: (payload: SendGroupCallReactionType) => void;
|
||||
setGroupCallVideoRequest: (
|
||||
_: Array<GroupCallVideoRequest>,
|
||||
|
@ -155,12 +159,14 @@ export function CallScreen({
|
|||
groupMembers,
|
||||
hangUpActiveCall,
|
||||
i18n,
|
||||
isGroupCallRaiseHandEnabled,
|
||||
isGroupCallReactionsEnabled,
|
||||
me,
|
||||
openSystemPreferencesAction,
|
||||
renderEmojiPicker,
|
||||
renderReactionPicker,
|
||||
setGroupCallVideoRequest,
|
||||
sendGroupCallRaiseHand,
|
||||
sendGroupCallReaction,
|
||||
setLocalAudio,
|
||||
setLocalVideo,
|
||||
|
@ -232,6 +238,11 @@ export function CallScreen({
|
|||
setShowMoreOptions(prevValue => !prevValue);
|
||||
}, []);
|
||||
|
||||
const [showRaisedHandsList, setShowRaisedHandsList] = useState(false);
|
||||
const toggleRaisedHandsList = useCallback(() => {
|
||||
setShowRaisedHandsList(prevValue => !prevValue);
|
||||
}, []);
|
||||
|
||||
const [controlsHover, setControlsHover] = useState(false);
|
||||
|
||||
const onControlsMouseEnter = useCallback(() => {
|
||||
|
@ -460,7 +471,8 @@ export function CallScreen({
|
|||
});
|
||||
|
||||
const isGroupCall = activeCall.callMode === CallMode.Group;
|
||||
const isMoreOptionsButtonEnabled = isGroupCall && isGroupCallReactionsEnabled;
|
||||
const isMoreOptionsButtonEnabled =
|
||||
isGroupCall && (isGroupCallRaiseHandEnabled || isGroupCallReactionsEnabled);
|
||||
|
||||
let presentingButtonType: CallingButtonType;
|
||||
if (presentingSource) {
|
||||
|
@ -471,6 +483,110 @@ export function CallScreen({
|
|||
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(() => {
|
||||
if (isRinging) {
|
||||
return i18n('icu:outgoingCallRinging');
|
||||
|
@ -599,6 +715,39 @@ export function CallScreen({
|
|||
localDemuxId={localDemuxId}
|
||||
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-calling__spacer CallControls__OuterSpacer" />
|
||||
<div
|
||||
|
@ -616,6 +765,8 @@ export function CallScreen({
|
|||
<CallingButtonToastsContainer
|
||||
hasLocalAudio={hasLocalAudio}
|
||||
outgoingRing={undefined}
|
||||
raisedHands={raisedHands}
|
||||
renderRaisedHandsToast={renderRaisedHandsToast}
|
||||
i18n={i18n}
|
||||
/>
|
||||
|
||||
|
@ -625,18 +776,34 @@ export function CallScreen({
|
|||
className="CallControls__MoreOptionsMenu"
|
||||
ref={moreOptionsMenuRef}
|
||||
>
|
||||
{renderReactionPicker({
|
||||
ref: reactionPickerRef,
|
||||
onClose: () => setShowMoreOptions(false),
|
||||
onPick: emoji => {
|
||||
setShowMoreOptions(false);
|
||||
sendGroupCallReaction({
|
||||
conversationId: conversation.id,
|
||||
value: emoji,
|
||||
});
|
||||
},
|
||||
renderEmojiPicker,
|
||||
})}
|
||||
{isGroupCallReactionsEnabled &&
|
||||
renderReactionPicker({
|
||||
ref: reactionPickerRef,
|
||||
onClose: () => setShowMoreOptions(false),
|
||||
onPick: emoji => {
|
||||
setShowMoreOptions(false);
|
||||
sendGroupCallReaction({
|
||||
conversationId: conversation.id,
|
||||
value: emoji,
|
||||
});
|
||||
},
|
||||
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>
|
||||
)}
|
||||
|
@ -715,6 +882,9 @@ export function CallScreen({
|
|||
audioLevel={localAudioLevel}
|
||||
shouldShowSpeaking={isSpeaking}
|
||||
/>
|
||||
{syncedLocalHandRaised && (
|
||||
<div className="CallingStatusIndicator CallingStatusIndicator--HandRaised" />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="module-ongoing-call__footer__local-preview" />
|
||||
|
@ -875,3 +1045,14 @@ function CallingReactionsToasts(props: CallingReactionsToastsType) {
|
|||
useReactionsToast(props);
|
||||
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,
|
||||
hasRemoteAudio: Boolean(participantProps.hasRemoteAudio),
|
||||
hasRemoteVideo: Boolean(participantProps.hasRemoteVideo),
|
||||
isHandRaised: Boolean(participantProps.isHandRaised),
|
||||
presenting: Boolean(participantProps.presenting),
|
||||
sharingScreen: Boolean(participantProps.sharingScreen),
|
||||
videoAspectRatio: 1.3,
|
||||
|
|
|
@ -140,6 +140,7 @@ export function GroupCall(args: PropsType): JSX.Element {
|
|||
maxDevices: 5,
|
||||
deviceCount: 0,
|
||||
peekedParticipants: [],
|
||||
raisedHands: new Set<number>(),
|
||||
remoteParticipants: [],
|
||||
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 { CallingToastProvider, useCallingToasts } from './CallingToast';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
import {
|
||||
difference as setDifference,
|
||||
isEqual as setIsEqual,
|
||||
} from '../util/setUtil';
|
||||
import * as log from '../logging/log';
|
||||
|
||||
type PropsType = {
|
||||
activeCall: ActiveCallType;
|
||||
|
@ -136,9 +141,101 @@ function useOutgoingRingToast({
|
|||
}, [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 = {
|
||||
hasLocalAudio: boolean;
|
||||
outgoingRing: boolean | undefined;
|
||||
raisedHands?: Set<number>;
|
||||
renderRaisedHandsToast?: (
|
||||
hands: Array<number>
|
||||
) => JSX.Element | string | undefined;
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
|
@ -161,10 +258,13 @@ export function CallingButtonToastsContainer(
|
|||
function CallingButtonToasts({
|
||||
hasLocalAudio,
|
||||
outgoingRing,
|
||||
raisedHands,
|
||||
renderRaisedHandsToast,
|
||||
i18n,
|
||||
}: CallingButtonToastsType) {
|
||||
useMutedToast({ hasLocalAudio, i18n });
|
||||
useOutgoingRingToast({ outgoingRing, i18n });
|
||||
useRaisedHandsToast({ raisedHands, renderRaisedHandsToast });
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ const allRemoteParticipants = times(MAX_PARTICIPANTS).map(index => ({
|
|||
demuxId: index,
|
||||
hasRemoteAudio: index % 3 !== 0,
|
||||
hasRemoteVideo: index % 4 !== 0,
|
||||
isHandRaised: (index - 2) % 8 === 0,
|
||||
presenting: false,
|
||||
sharingScreen: false,
|
||||
videoAspectRatio: 1.3,
|
||||
|
|
|
@ -38,10 +38,12 @@ const createProps = (
|
|||
isBlocked = false,
|
||||
hasRemoteAudio = false,
|
||||
presenting = false,
|
||||
isHandRaised = false,
|
||||
}: {
|
||||
isBlocked?: boolean;
|
||||
hasRemoteAudio?: boolean;
|
||||
presenting?: boolean;
|
||||
isHandRaised?: boolean;
|
||||
} = {}
|
||||
): PropsType => ({
|
||||
getFrameBuffer,
|
||||
|
@ -55,6 +57,7 @@ const createProps = (
|
|||
demuxId: 123,
|
||||
hasRemoteAudio,
|
||||
hasRemoteVideo: true,
|
||||
isHandRaised,
|
||||
presenting,
|
||||
sharingScreen: false,
|
||||
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 {
|
||||
return (
|
||||
<GroupCallRemoteParticipant
|
||||
|
|
|
@ -80,6 +80,7 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
|||
demuxId,
|
||||
hasRemoteAudio,
|
||||
hasRemoteVideo,
|
||||
isHandRaised,
|
||||
isBlocked,
|
||||
isMe,
|
||||
profileName,
|
||||
|
@ -295,7 +296,9 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
|||
isSpeaking &&
|
||||
!isActiveSpeakerInSpeakerView &&
|
||||
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}
|
||||
style={containerStyles}
|
||||
|
@ -307,15 +310,16 @@ export const GroupCallRemoteParticipant: React.FC<PropsType> = React.memo(
|
|||
audioLevel={props.audioLevel}
|
||||
shouldShowSpeaking={isSpeaking}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-ongoing-call__group-call-remote-participant__info'
|
||||
)}
|
||||
>
|
||||
<ContactName
|
||||
module="module-ongoing-call__group-call-remote-participant__info__contact-name"
|
||||
title={title}
|
||||
/>
|
||||
<div className="module-ongoing-call__group-call-remote-participant__footer">
|
||||
<div className="module-ongoing-call__group-call-remote-participant__info">
|
||||
{isHandRaised && (
|
||||
<div className="CallingStatusIndicator CallingStatusIndicator--HandRaised" />
|
||||
)}
|
||||
<ContactName
|
||||
module="module-ongoing-call__group-call-remote-participant__info__contact-name"
|
||||
title={title}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -163,6 +163,7 @@ type CallingReduxInterface = Pick<
|
|||
| 'callStateChange'
|
||||
| 'cancelIncomingGroupCallRing'
|
||||
| 'groupCallAudioLevelsChange'
|
||||
| 'groupCallRaisedHandsChange'
|
||||
| 'groupCallStateChange'
|
||||
| 'outgoingCall'
|
||||
| 'receiveGroupCallReactions'
|
||||
|
@ -847,8 +848,11 @@ export class CallingClass {
|
|||
reactions,
|
||||
});
|
||||
},
|
||||
onRaisedHands: (_groupCall, _raisedHands) => {
|
||||
// TODO: Implement handling of raised hands.
|
||||
onRaisedHands: (_groupCall, raisedHands) => {
|
||||
this.reduxInterface?.groupCallRaisedHandsChange({
|
||||
conversationId,
|
||||
raisedHands,
|
||||
});
|
||||
},
|
||||
onPeekChanged: groupCall => {
|
||||
const localDeviceState = groupCall.getLocalDeviceState();
|
||||
|
@ -1153,6 +1157,14 @@ export class CallingClass {
|
|||
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 {
|
||||
const groupCall = this.getGroupCall(conversationId);
|
||||
if (!groupCall) {
|
||||
|
|
|
@ -116,6 +116,7 @@ export type GroupCallStateType = {
|
|||
localDemuxId: number | undefined;
|
||||
joinState: GroupCallJoinState;
|
||||
peekInfo?: GroupCallPeekInfoType;
|
||||
raisedHands?: Array<number>;
|
||||
remoteParticipants: Array<GroupCallParticipantInfoType>;
|
||||
remoteAudioLevels?: Map<number, number>;
|
||||
} & GroupCallRingStateType;
|
||||
|
@ -222,11 +223,15 @@ type IncomingGroupCallType = ReadonlyDeep<{
|
|||
ringerAci: AciString;
|
||||
}>;
|
||||
|
||||
export type SendGroupCallRaiseHandType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
raise: boolean;
|
||||
}>;
|
||||
|
||||
export type SendGroupCallReactionType = ReadonlyDeep<{
|
||||
conversationId: string;
|
||||
value: string;
|
||||
}>;
|
||||
|
||||
type SendGroupCallReactionLocalCopyType = ReadonlyDeep<{
|
||||
conversationId: 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 DECLINE_DIRECT_CALL = 'calling/DECLINE_DIRECT_CALL';
|
||||
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_REACTIONS_RECEIVED = 'calling/GROUP_CALL_REACTIONS_RECEIVED';
|
||||
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 OUTGOING_CALL = 'calling/OUTGOING_CALL';
|
||||
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 REMOTE_SHARING_SCREEN_CHANGE = 'calling/REMOTE_SHARING_SCREEN_CHANGE';
|
||||
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
||||
|
@ -525,6 +532,16 @@ type GroupCallAudioLevelsChangeActionType = ReadonlyDeep<{
|
|||
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
|
||||
export type GroupCallStateChangeActionType = {
|
||||
type: 'calling/GROUP_CALL_STATE_CHANGE';
|
||||
|
@ -580,6 +597,11 @@ type KeyChangeOkActionType = ReadonlyDeep<{
|
|||
payload: null;
|
||||
}>;
|
||||
|
||||
type SendGroupCallRaiseHandActionType = ReadonlyDeep<{
|
||||
type: 'calling/RAISE_HAND_GROUP_CALL';
|
||||
payload: SendGroupCallRaiseHandType;
|
||||
}>;
|
||||
|
||||
export type SendGroupCallReactionActionType = ReadonlyDeep<{
|
||||
type: 'calling/SEND_GROUP_CALL_REACTION';
|
||||
payload: SendGroupCallReactionLocalCopyType;
|
||||
|
@ -692,6 +714,7 @@ export type CallingActionType =
|
|||
| ConversationRemovedActionType
|
||||
| DeclineCallActionType
|
||||
| GroupCallAudioLevelsChangeActionType
|
||||
| GroupCallRaisedHandsChangeActionType
|
||||
| GroupCallStateChangeActionType
|
||||
| GroupCallReactionsReceivedActionType
|
||||
| GroupCallReactionsExpiredActionType
|
||||
|
@ -950,6 +973,12 @@ function receiveGroupCallReactions(
|
|||
};
|
||||
}
|
||||
|
||||
function groupCallRaisedHandsChange(
|
||||
payload: GroupCallRaisedHandsChangeActionPayloadType
|
||||
): GroupCallRaisedHandsChangeActionType {
|
||||
return { type: GROUP_CALL_RAISED_HANDS_CHANGE, payload };
|
||||
}
|
||||
|
||||
function groupCallStateChange(
|
||||
payload: GroupCallStateChangeArgumentType
|
||||
): 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(
|
||||
payload: SendGroupCallReactionType
|
||||
): ThunkAction<
|
||||
|
@ -1612,6 +1654,7 @@ export const actions = {
|
|||
declineCall,
|
||||
getPresentingSources,
|
||||
groupCallAudioLevelsChange,
|
||||
groupCallRaisedHandsChange,
|
||||
groupCallStateChange,
|
||||
hangUpActiveCall,
|
||||
keyChangeOk,
|
||||
|
@ -1630,6 +1673,7 @@ export const actions = {
|
|||
remoteSharingScreenChange,
|
||||
remoteVideoChange,
|
||||
returnToActiveCall,
|
||||
sendGroupCallRaiseHand,
|
||||
sendGroupCallReaction,
|
||||
setGroupCallVideoRequest,
|
||||
setIsCallActive,
|
||||
|
@ -2137,6 +2181,7 @@ export function reducer(
|
|||
localDemuxId,
|
||||
peekInfo: newPeekInfo,
|
||||
remoteParticipants,
|
||||
raisedHands: existingCall?.raisedHands ?? [],
|
||||
...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) {
|
||||
const { conversationId, isSharingScreen } = action.payload;
|
||||
const call = getOwn(state.callsByConversation, conversationId);
|
||||
|
|
|
@ -13,6 +13,7 @@ import { getActiveCall } from '../ducks/calling';
|
|||
import type { ConversationType } from '../ducks/conversations';
|
||||
import { getIncomingCall } from '../selectors/calling';
|
||||
import { isGroupCallOutboundRingEnabled } from '../../util/isGroupCallOutboundRingEnabled';
|
||||
import { isGroupCallRaiseHandEnabled } from '../../util/isGroupCallRaiseHandEnabled';
|
||||
import { isGroupCallReactionsEnabled } from '../../util/isGroupCallReactionsEnabled';
|
||||
import type {
|
||||
ActiveCallBaseType,
|
||||
|
@ -201,6 +202,8 @@ const mapStateToActiveCallProp = (
|
|||
const remoteParticipants: Array<GroupCallRemoteParticipantType> = [];
|
||||
const peekedParticipants: Array<ConversationType> = [];
|
||||
const conversationsByDemuxId: ConversationsByDemuxIdType = new Map();
|
||||
const { localDemuxId } = call;
|
||||
const raisedHands: Set<number> = new Set(call.raisedHands ?? []);
|
||||
|
||||
const { memberships = [] } = conversation;
|
||||
|
||||
|
@ -243,6 +246,7 @@ const mapStateToActiveCallProp = (
|
|||
demuxId: remoteParticipant.demuxId,
|
||||
hasRemoteAudio: remoteParticipant.hasRemoteAudio,
|
||||
hasRemoteVideo: remoteParticipant.hasRemoteVideo,
|
||||
isHandRaised: raisedHands.has(remoteParticipant.demuxId),
|
||||
presenting: remoteParticipant.presenting,
|
||||
sharingScreen: remoteParticipant.sharingScreen,
|
||||
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 (
|
||||
let i = 0;
|
||||
i < activeCallState.safetyNumberChangedAcis.length;
|
||||
|
@ -293,9 +308,10 @@ const mapStateToActiveCallProp = (
|
|||
groupMembers,
|
||||
isConversationTooBigToRing: isConversationTooBigToRing(conversation),
|
||||
joinState: call.joinState,
|
||||
localDemuxId: call.localDemuxId,
|
||||
localDemuxId,
|
||||
maxDevices: peekInfo.maxDevices,
|
||||
peekedParticipants,
|
||||
raisedHands,
|
||||
remoteParticipants,
|
||||
remoteAudioLevels: call.remoteAudioLevels || new Map<number, number>(),
|
||||
} satisfies ActiveGroupCallType;
|
||||
|
@ -360,6 +376,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||
i18n: getIntl(state),
|
||||
isGroupCallOutboundRingEnabled: isGroupCallOutboundRingEnabled(),
|
||||
isGroupCallRaiseHandEnabled: isGroupCallRaiseHandEnabled(),
|
||||
isGroupCallReactionsEnabled: isGroupCallReactionsEnabled(),
|
||||
incomingCall,
|
||||
me: getMe(state),
|
||||
|
|
|
@ -940,6 +940,7 @@ describe('calling duck', () => {
|
|||
videoAspectRatio: 4 / 3,
|
||||
},
|
||||
],
|
||||
raisedHands: [],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -997,6 +998,7 @@ describe('calling duck', () => {
|
|||
videoAspectRatio: 16 / 9,
|
||||
},
|
||||
],
|
||||
raisedHands: [],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -96,6 +96,7 @@ export type ActiveGroupCallType = ActiveCallBaseType & {
|
|||
groupMembers: Array<Pick<ConversationType, 'id' | 'firstName' | 'title'>>;
|
||||
isConversationTooBigToRing: boolean;
|
||||
peekedParticipants: Array<ConversationType>;
|
||||
raisedHands: Set<number>;
|
||||
remoteParticipants: Array<GroupCallRemoteParticipantType>;
|
||||
remoteAudioLevels: Map<number, number>;
|
||||
};
|
||||
|
@ -158,6 +159,7 @@ export type GroupCallRemoteParticipantType = ConversationType & {
|
|||
demuxId: number;
|
||||
hasRemoteAudio: boolean;
|
||||
hasRemoteVideo: boolean;
|
||||
isHandRaised: boolean;
|
||||
presenting: boolean;
|
||||
sharingScreen: boolean;
|
||||
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",
|
||||
"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",
|
||||
"path": "ts/components/CallsList.tsx",
|
||||
|
|
Loading…
Add table
Reference in a new issue